diff --git a/.gitignore b/.gitignore
index 1ed1207..7d065ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *.img
+*.img.bmap
 *.pcap
 __pycache__
 _backups
diff --git a/.travis.yml b/.travis.yml
index b173709..a62fa93 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,11 +13,11 @@ cache:
   - tmp/
 before_script:
 - sudo apt-get -y update
-- sudo apt-get -y install qemu-user-static binfmt-support qemu
+- sudo apt-get -y install qemu-user-static binfmt-support qemu bmap-tools
 - sudo update-binfmts --display
 - unset GOROOT
 script:
-- sudo ./scripts/create_sibling.sh -n pwnagotchi -o pwnagotchi.img -s 4
+- sudo ./scripts/create_sibling.sh -n pwnagotchi -o pwnagotchi.img
 - zip -s 2g pwnagotchi.zip pwnagotchi.img
 
 # TODO: deploy!
diff --git a/README.md b/README.md
index f568794..5e4567a 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,23 @@ usage: ./scripts/create_sibling.sh [OPTIONS]
 
 If you connect to the unit via `usb0` (thus using the data port), you might want to use the `scripts/linux_connection_share.sh` script to bring the interface up on your end and share internet connectivity from another interface, so you can update the unit and generally download things from the internet on it.
 
+#### Update your pwnagotchi
+
+You can use the `scripts/update_pwnagotchi.sh` script to update to the most recent version of pwnagotchi.
+
+```shell
+usage: ./update_pwnagitchi.sh [OPTIONS]
+
+   Options:
+      -v                # Version to update to, can be a branch or commit. (default: master)
+      -u                # Url to clone from. (default: https://github.com/evilsocket/pwnagotchi)
+      -m                # Mode to restart to. (Supported: auto manual; default: auto)
+      -b                # Backup the current pwnagotchi config.
+      -r                # Restore the current pwnagotchi config. -b will be enabled.
+      -h                # Shows this help.             Shows this help.
+
+```
+
 ### UI
 
 The UI is available either via display if installed, or via http://pwnagotchi.local:8080/ if you connect to the unit via `usb0` and set a static address on the network interface (change `pwnagotchi` with the hostname of your unit).
@@ -98,9 +115,12 @@ Pwnagotchi is able to speak multiple languages!! Currently supported are:
 
 * **english** (default)
 * german
+* dutch
+* greek
+* macedonian
+* italian
 
-If you want to add a language use the `language.sh` script.
-If you want to add for example the language **italian** you would type:
+If you want to add a language use the `language.sh` script. If you want to add for example the language **italian** you would type:
 
 ```shell
 ./scripts/language.sh add it
@@ -120,6 +140,20 @@ If you changed the `voice.py`- File, the translations need an update. Do it like
 # DONE
 ```
 
+Now you can use the `preview.py`-script to preview the changes:
+
+```shell
+./scripts/preview.py --lang it --display ws2 --port 8080 &
+./scripts/preview.py --lang it --display inky --port 8081 &
+# Now open http://localhost:8080 and http://localhost:8081
+```
+
+### Plugins
+
+Pwnagotchi has a simple plugins system that you can use to customize your unit and its behaviour. You can place your plugins anywhere
+as python files and then edit the `config.yml` file (`main.plugins` value) to point to their containing folder. Check the [plugins folder](https://github.com/evilsocket/pwnagotchi/tree/master/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/) for a list of default 
+plugins and all the callbacks that you can define for your own customizations.
+
 ### Random Info
 
 - `hostname` sets the unit name.
diff --git a/scripts/create_sibling.sh b/scripts/create_sibling.sh
index 5ae58ef..55b784f 100755
--- a/scripts/create_sibling.sh
+++ b/scripts/create_sibling.sh
@@ -5,18 +5,22 @@
 set -eu
 
 REQUIREMENTS=( wget gunzip git dd e2fsck resize2fs parted losetup qemu-system-x86_64 )
+DEBREQUIREMENTS=( wget gzip git parted qemu-system-x86 qemu-user-static bmap-tools )
 REPO_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
 TMP_DIR="${REPO_DIR}/tmp"
 MNT_DIR="${TMP_DIR}/mnt"
+THIS_DIR=$(pwd)
 
 PWNI_NAME="pwnagotchi"
 PWNI_OUTPUT="pwnagotchi.img"
-PWNI_SIZE="4"
+PWNI_SIZE="8"
 
+OPT_SPARSE=0
 OPT_PROVISION_ONLY=0
 OPT_CHECK_DEPS_ONLY=0
 OPT_IMAGE_PROVIDED=0
 OPT_RASPBIAN_VERSION='latest'
+OPT_APTPROXY=""
 
 SUPPORTED_RASPBIAN_VERSIONS=( 'latest' 'buster' 'stretch' )
 
@@ -26,6 +30,18 @@ if [[ "$EUID" -ne 0 ]]; then
 fi
 
 function check_dependencies() {
+  if [ -f /etc/debian_version ];
+  then
+    echo "[+] Checking Debian dependencies"
+
+    for REQ in "${DEBREQUIREMENTS[@]}"; do
+      if ! dpkg -s "$REQ" >/dev/null 2>&1; then
+        echo "Dependency check failed for ${REQ}; use 'apt-get install ${REQ}' to install"
+        exit 1
+      fi
+    done
+  fi
+
   echo "[+] Checking dependencies"
   for REQ in "${REQUIREMENTS[@]}"; do
     if ! type "$REQ" >/dev/null 2>&1; then
@@ -76,8 +92,31 @@ function provide_raspbian() {
 }
 
 function setup_raspbian(){
-  echo "[+] Resize image"
-  dd if=/dev/zero bs=1G count="$PWNI_SIZE" >> "${TMP_DIR}/raspbian.img"
+  # Detect the ability to create sparse files
+  if [ "${OPT_SPARSE}" -eq 0 ];
+  then
+    which bmaptool >/dev/null 2>&1
+    if [ $? -eq 0 ];
+    then
+      echo "[+] Defaulting to sparse image generation as bmaptool is available"
+      OPT_SPARSE=1
+    else
+      echo "[!] bmaptool not available, not creating a sparse image"
+    fi
+  fi
+
+  # Note that we 'extend' the raspbian.img
+  if [ "${OPT_SPARSE}" -eq 1 ];
+  then
+    # Resize sparse (so that we can use bmaptool later)
+    echo "[+] Resizing sparse image of ${PWNI_SIZE}GB (1000s)"
+    truncate -s ${PWNI_SIZE}GB "${TMP_DIR}/raspbian.img"
+  else
+    echo "[+] Resizing full image to ${PWNI_SIZE}G"
+    # Full disk-space using image (appends to raspbian image)
+    dd if=/dev/zero bs=1G count="${PWNI_SIZE}" >> "${TMP_DIR}/raspbian.img"
+  fi
+
   echo "[+] Setup loop device"
   mkdir -p "${MNT_DIR}"
   LOOP_PATH="$(losetup --find --partscan --show "${TMP_DIR}/raspbian.img")"
@@ -107,10 +146,16 @@ function provision_raspbian() {
   cd "${MNT_DIR}"
   sed -i'' 's/^\([^#]\)/#\1/g' etc/ld.so.preload # add comments
   echo "[+] Run chroot commands"
-  LANG=C chroot . bin/bash -x <<EOF
+  LANG=C LC_ALL=C LC_CTYPE=C chroot . bin/bash -x <<EOF
   set -eu
   export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
 
+  if [ ! -z "${OPT_APTPROXY}" ];
+  then
+	echo "[+] Using Proxy ${OPT_APTPROXY}"
+	echo "Acquire::http { Proxy \"${OPT_APTPROXY}\"; }" >/etc/apt/apt.conf.d/99pwnagotchi_proxy
+  fi
+
   uname -a
 
   apt-get -y update
@@ -160,7 +205,7 @@ function provision_raspbian() {
 
   # install bettercap
   export GOPATH=/root/go
-  go get -u github.com/bettercap/bettercap
+  taskset -c 1 go get -u github.com/bettercap/bettercap
   mv "\$GOPATH/bin/bettercap" /usr/bin/bettercap
 
   # install bettercap caplets (cant run bettercap in chroot)
@@ -203,7 +248,11 @@ EOF
   cd "${REPO_DIR}"
   umount -R "${MNT_DIR}"
   losetup -D "$(losetup -l | awk '/raspbian\.img/{print $1}')"
-  mv "${TMP_DIR}/raspbian.img" "$PWNI_OUTPUT"
+  mv "${TMP_DIR}/raspbian.img" "${PWNI_OUTPUT}"
+  if [ "${OPT_SPARSE}" -eq 1 ];
+  then
+    bmaptool create -o "${PWNI_OUTPUT}.bmap" "${PWNI_OUTPUT}"
+  fi
 }
 
 function usage() {
@@ -226,8 +275,11 @@ EOF
   exit 0
 }
 
-while getopts ":n:i:o:s:v:dph" o; do
+while getopts "A:n:i:o:s:v:dph" o; do
   case "${o}" in
+    A)
+      OPT_APTPROXY="${OPTARG}"
+      ;;
     n)
       PWNI_NAME="${OPTARG}"
       ;;
@@ -283,4 +335,14 @@ fi
 setup_raspbian
 provision_raspbian
 
-echo -ne "[+] Congratz, it's a boy (⌐■_■)!\n[+] One more step: dd if=$PWNI_OUTPUT of=<PATH_TO_SDCARD> bs=4M status=progress"
+echo -e "[+] Congratz, it's a boy (⌐■_■)!"
+echo -e "[+] One more step: dd if=../${PWNI_OUTPUT} of=<PATH_TO_SDCARD> bs=4M status=progress"
+
+if [ "${OPT_SPARSE}" -eq 1 ];
+then
+  echo -e "[t] To transfer use: rsync -vaS --progress $(whoami)@$(hostname -f):${THIS_DIR}/../${PWNI_OUTPUT} <DEST>"
+  echo -e "[t] To burn with bmaptool: bmaptool copy ~/${PWNI_OUTPUT} /dev/<DEVICE>"
+fi
+
+# Helpful OSX reminder
+echo -e "[t] Mac: use 'diskutil list' to figure out which device to burn to; 'diskutil unmountDisk' to unmount that disk'; then use /dev/rdiskX (note the 'r') for faster transfer"
diff --git a/scripts/language.sh b/scripts/language.sh
index c56b784..cf653f5 100755
--- a/scripts/language.sh
+++ b/scripts/language.sh
@@ -55,6 +55,7 @@ function update_lang() {
   msgmerge --update "$LOCALE_DIR/$1/LC_MESSAGES/voice.po" "$LOCALE_DIR/voice.pot"
 }
 
+
 case "$1" in
   add)
     add_lang "$2"
diff --git a/scripts/macos_connection_share.sh b/scripts/macos_connection_share.sh
new file mode 100755
index 0000000..2528067
--- /dev/null
+++ b/scripts/macos_connection_share.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+# name of the ethernet gadget interface on the host
+USB_IFACE=${1:-en8}
+# host interface to use for upstream connection
+UPSTREAM_IFACE=${2:-en7}
+
+sysctl -w net.inet.ip.forwarding=1
+pfctl -e
+echo "nat on ${UPSTREAM_IFACE} from ${USB_IFACE}:network to any -> (${UPSTREAM_IFACE})" | pfctl -f -
diff --git a/scripts/preview.py b/scripts/preview.py
new file mode 100755
index 0000000..261448e
--- /dev/null
+++ b/scripts/preview.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+
+import sys
+import os
+import time
+import argparse
+from http.server import HTTPServer
+import shutil
+import yaml
+
+sys.path.insert(0,
+                os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                             '../sdcard/rootfs/root/pwnagotchi/scripts/'))
+
+from pwnagotchi.ui.display import Display, VideoHandler
+import core
+
+
+class CustomDisplay(Display):
+
+    def _http_serve(self):
+        if self._video_address is not None:
+            self._httpd = HTTPServer((self._video_address, self._video_port),
+                                     CustomVideoHandler)
+            core.log("ui available at http://%s:%d/" % (self._video_address,
+                                                        self._video_port))
+            self._httpd.serve_forever()
+        else:
+            core.log("could not get ip of usb0, video server not starting")
+
+    def _on_view_rendered(self, img):
+        CustomVideoHandler.render(img)
+
+        if self._enabled:
+            self.canvas = (img if self._rotation == 0 else img.rotate(self._rotation))
+            if self._render_cb is not None:
+                self._render_cb()
+
+
+class CustomVideoHandler(VideoHandler):
+
+    @staticmethod
+    def render(img):
+        with CustomVideoHandler._lock:
+            try:
+                img.save("/tmp/pwnagotchi-{rand}.png".format(rand=id(CustomVideoHandler)), format='PNG')
+            except BaseException:
+                core.log("could not write preview")
+
+    def do_GET(self):
+        if self.path == '/':
+            self.send_response(200)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            try:
+                self.wfile.write(
+                    bytes(
+                        self._index %
+                        ('localhost', 1000), "utf8"))
+            except BaseException:
+                pass
+
+        elif self.path.startswith('/ui'):
+            with self._lock:
+                self.send_response(200)
+                self.send_header('Content-type', 'image/png')
+                self.end_headers()
+                try:
+                    with open("/tmp/pwnagotchi-{rand}.png".format(rand=id(CustomVideoHandler)), 'rb') as fp:
+                        shutil.copyfileobj(fp, self.wfile)
+                except BaseException:
+                    core.log("could not open preview")
+        else:
+            self.send_response(404)
+
+
+class DummyPeer:
+    @staticmethod
+    def name():
+        return "beta"
+
+
+def main():
+    parser = argparse.ArgumentParser(description="This program emulates\
+                                     the pwnagotchi display")
+    parser.add_argument('--display', help="Which display to use.",
+                        default="waveshare_2")
+    parser.add_argument('--port', help="Which port to use",
+                        default=8080)
+    parser.add_argument('--sleep', type=int, help="Time between emotions",
+                        default=2)
+    parser.add_argument('--lang', help="Language to use",
+                        default="en")
+    args = parser.parse_args()
+
+    CONFIG = yaml.load('''
+    main:
+        lang: {lang}
+    ui:
+        fps: 0.3
+        display:
+            enabled: false
+            rotation: 180
+            color: black
+            refresh: 30
+            type: {display}
+            video:
+                enabled: true
+                address: "0.0.0.0"
+                port: {port}
+    '''.format(display=args.display,
+               port=args.port,
+               lang=args.lang))
+
+    DISPLAY = CustomDisplay(config=CONFIG, state={'name': '%s>' % 'preview'})
+
+    while True:
+        DISPLAY.on_starting()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_ai_ready()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_normal()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_new_peer(DummyPeer())
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_lost_peer(DummyPeer())
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_free_channel('6')
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.wait(args.sleep)
+        DISPLAY.update()
+        DISPLAY.on_bored()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_sad()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_motivated(1)
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_demotivated(-1)
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_excited()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_deauth({'mac': 'DE:AD:BE:EF:CA:FE'})
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_miss('test')
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_lonely()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_handshakes(1)
+        DISPLAY.update()
+        time.sleep(args.sleep)
+        DISPLAY.on_rebooting()
+        DISPLAY.update()
+        time.sleep(args.sleep)
+
+
+if __name__ == '__main__':
+    SystemExit(main())
diff --git a/scripts/update_pwnagotchi.sh b/scripts/update_pwnagotchi.sh
new file mode 100644
index 0000000..0039e7b
--- /dev/null
+++ b/scripts/update_pwnagotchi.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+# Default variables
+GIT_FOLDER="/tmp/pwnagotchi"
+GIT_URL="https://github.com/evilsocket/pwnagotchi/"
+VERSION="master"
+SUPPORTED_RESTART_MODES=( 'auto' 'manual' )
+MODE="auto"
+BACKUPCONFIG=0
+RESTORECONFIG=0
+
+# Functions
+function usage() {
+    cat <<EOF
+
+ usage: $0 [OPTIONS]
+ 
+   Options:
+      -v        # Version to update to, can be a branch or commit. (default: master)
+      -u        # Url to clone from. (default: https://github.com/evilsocket/pwnagotchi)
+      -m        # Mode to restart to. (Supported: ${SUPPORTED_RESTART_MODES[*]}; default: auto)
+      -b        # Backup the current pwnagotchi config.
+      -r        # Restore the current pwnagotchi config. (-b will be enabled.)
+      -h        # Shows this help.
+
+EOF
+    exit 0
+}
+
+function test_root() {
+    if ! [ $(id -u) = 0 ]; then
+        echo "[!] This script must be run as root."
+        exit 1
+    fi
+}
+
+function test_github() {
+    wget -q  --spider $GIT_URL
+    if [ $? -ne 0 ]; then
+        echo "[!] Cannot reach github. This script requires internet access, ensure connection sharing is working."
+        exit 2
+    fi
+}
+
+echo "[+] Checking prerequisites."
+test_root
+test_github
+
+while getopts ":v:u:m:b:r:h" o; do
+  case "${o}" in
+    v)
+      VERSION="${OPTARG}"
+      ;;
+    u)
+      GIT_URL="${OPTARG}"
+      ;;
+    m)
+      if [[ "${SUPPORTED_RESTART_MODES[*]}" =~ ${OPTARG} ]]; then
+        MODE="${OPTARG}"
+      else
+        usage
+      fi      
+      ;;      
+    b)
+      BACKUPCONFIG=1
+      ;;
+    r)
+      BACKUPCONFIG=1    
+      RESTORECONFIG=1
+      ;;      
+    h)
+      usage
+      ;;
+    *)
+      usage
+      ;;
+  esac
+done
+shift $((OPTIND-1))
+
+# clean up old files, clone master, set checkout to commit if needed.
+echo "[+] Cloning to $GIT_FOLDER..."
+rm $GIT_FOLDER -rf
+git clone $GIT_URL $GIT_FOLDER -q
+cd $GIT_FOLDER
+if [ $VERSION != "master" ]; then
+    git checkout $VERSION -q
+fi
+echo "[+] Installing $(git log -1 --format="%h")"
+
+echo "[+] Updating..."
+if [ $BACKUPCONFIG -eq 1 ]; then
+    echo "[+] Creating backup of config.yml"
+    mv /root/pwnagotchi/config.yml /root/config.yml.bak -f
+fi
+rm /root/pwnagotchi -rf # ensures old files are removed
+rsync -aPq $GIT_FOLDER/sdcard/boot/*   /boot/
+rsync -aPq $GIT_FOLDER/sdcard/rootfs/* /
+cd /tmp
+rm $GIT_FOLDER -rf
+if [ $RESTORECONFIG -eq 1 ]; then
+    echo "[+] Restoring backup of config.yml"
+    mv /root/config.yml.bak /root/pwnagotchi/config.yml -f
+fi
+
+echo "[+] Restarting pwnagotchi in $MODE mode. $( screen -X -S pwnagotchi quit)"
+if [ $MODE == "auto" ]; then
+    sudo -H -u root /usr/bin/screen -dmS pwnagotchi -c /root/pwnagotchi/data/screenrc.auto
+elif [ $MODE == "manual" ]; then
+    sudo -H -u root /usr/bin/screen -dmS pwnagotchi -c /root/pwnagotchi/data/screenrc.manual
+fi
+echo "[+] Finished"
\ No newline at end of file
diff --git a/sdcard/rootfs/root/pwnagotchi/config.yml b/sdcard/rootfs/root/pwnagotchi/config.yml
index 1fb846e..7cf1c6f 100644
--- a/sdcard/rootfs/root/pwnagotchi/config.yml
+++ b/sdcard/rootfs/root/pwnagotchi/config.yml
@@ -1,7 +1,9 @@
 # main algorithm configuration
 main:
-    # currently implemented: en (default), de
+    # currently implemented: en (default), de, nl, it
     lang: en
+    # custom plugins path, if null only default plugins with be loaded
+    plugins: null
     # monitor interface to use
     iface: mon0
     # command to run to bring the mon interface up in case it's not up already
@@ -21,7 +23,7 @@ main:
 
 ai:
     # if false, only the default 'personality' will be used
-    enabled: true
+    enabled: false
     path: /root/brain.nn
     # 1.0 - laziness = probability of start training
     laziness: 0.1
@@ -98,8 +100,6 @@ ui:
         type: 'waveshare_2'
         # Possible options red/yellow/black (black used for monocromatic displays)
         color: 'black'
-        # How often to do a full refresh 0 all the time, -1 never
-        refresh: 50
         video:
             enabled: true
             address: '10.0.0.2'
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh b/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
index 48673dc..e7f475e 100755
--- a/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
@@ -10,8 +10,3 @@ done
 
 echo 0 >/sys/class/leds/led0/brightness
 sleep 0.3
-
-# Powersave options
-# Disable power LED ~30ma
-echo none >/sys/class/leds/led0/trigger
-echo 1 >/sys/class/leds/led0/brightness
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/main.py b/sdcard/rootfs/root/pwnagotchi/scripts/main.py
index de44012..09f6ec3 100755
--- a/sdcard/rootfs/root/pwnagotchi/scripts/main.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/main.py
@@ -5,7 +5,7 @@ import time
 import traceback
 
 import core
-import pwnagotchi
+import pwnagotchi, pwnagotchi.plugins as plugins
 
 from pwnagotchi.log import SessionParser
 from pwnagotchi.voice import Voice
@@ -24,16 +24,50 @@ args = parser.parse_args()
 
 if args.do_clear:
     print("clearing the display ...")
-    from pwnagotchi.ui.waveshare import EPD
+    with open(args.config, 'rt') as fp:
+        config = yaml.safe_load(fp)
+        cleardisplay = config['ui']['display']['type']
+        if cleardisplay in ('inkyphat', 'inky'):
+            print("inky display")
+            from inky import InkyPHAT
 
-    epd = EPD()
-    epd.init(epd.FULL_UPDATE)
-    epd.Clear(0xff)
-    quit()
+            epd = InkyPHAT(config['ui']['display']['color'])
+            epd.set_border(InkyPHAT.BLACK)
+            self._render_cb = self._inky_render
+        elif cleardisplay in ('papirus', 'papi'):
+            print("papirus display")
+            from pwnagotchi.ui.papirus.epd import EPD
+
+            os.environ['EPD_SIZE'] = '2.0'
+            epd = EPD()
+            epd.clear()
+        elif cleardisplay in ('waveshare_1', 'ws_1', 'waveshare1', 'ws1'):
+            print("waveshare v1 display")
+            from pwnagotchi.ui.waveshare.v1.epd2in13 import EPD
+
+            epd = EPD()
+            epd.init(epd.lut_full_update)
+            epd.Clear(0xFF)
+        elif cleardisplay in ('waveshare_2', 'ws_2', 'waveshare2', 'ws2'):
+            print("waveshare v2 display")
+            from pwnagotchi.ui.waveshare.v2.waveshare import EPD
+
+            epd = EPD()
+            epd.init(epd.FULL_UPDATE)
+            epd.Clear(0xff)
+        else:
+            print("unknown display type %s" % cleardisplay)
+        quit()
 
 with open(args.config, 'rt') as fp:
     config = yaml.safe_load(fp)
 
+plugins.load_from_path(plugins.default_path)
+if 'plugins' in config['main'] and config['main']['plugins'] is not None:
+    plugins.load_from_path(config['main']['plugins'])
+
+plugins.on('loaded')
+
 display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
 agent = Agent(view=display, config=config)
 
@@ -41,6 +75,9 @@ core.log("%s@%s (v%s)" % (pwnagotchi.name(), agent._identity, pwnagotchi.version
 # for key, value in config['personality'].items():
 #    core.log("  %s: %s" % (key, value))
 
+for _, plugin in plugins.loaded.items():
+    core.log("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
+
 if args.do_manual:
     core.log("entering manual mode ...")
 
@@ -88,13 +125,15 @@ core.logfile = config['main']['log']
 
 agent.start_ai()
 agent.setup_events()
-agent.set_ready()
+agent.set_starting()
 agent.start_monitor_mode()
 agent.start_event_polling()
 
 # print initial stats
 agent.next_epoch()
 
+agent.set_ready()
+
 while True:
     try:
         # recon on all channels
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/agent.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/agent.py
index f8f1c41..739d6ed 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/agent.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/agent.py
@@ -8,8 +8,9 @@ import _thread
 
 import core
 
+import pwnagotchi.plugins as plugins
 from bettercap.client import Client
-from pwnagotchi.mesh.advertise import AsyncAdvertiser
+from pwnagotchi.mesh.utils import AsyncAdvertiser
 from pwnagotchi.ai.train import AsyncTrainer
 
 RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
@@ -44,32 +45,41 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
             pass
         return False
 
+    def config(self):
+        return self._config
+
     def supported_channels(self):
         return self._supported_channels
 
-    def on_ai_ready(self):
-        self._view.on_ai_ready()
+    def set_starting(self):
+        self._view.on_starting()
 
     def set_ready(self):
-        self._view.on_starting()
+        plugins.on('ready', self)
 
     def set_free_channel(self, channel):
         self._view.on_free_channel(channel)
+        plugins.on('free_channel', self, channel)
 
     def set_bored(self):
         self._view.on_bored()
+        plugins.on('bored', self)
 
     def set_sad(self):
         self._view.on_sad()
+        plugins.on('sad', self)
 
     def set_excited(self):
         self._view.on_excited()
+        plugins.on('excited', self)
 
     def set_lonely(self):
         self._view.on_lonely()
+        plugins.on('lonely', self)
 
     def set_rebooting(self):
         self._view.on_rebooting()
+        plugins.on('rebooting', self)
 
     def setup_events(self):
         core.log("connecting to %s ..." % self.url)
@@ -128,6 +138,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         self.start_advertising()
 
     def wait_for(self, t, sleeping=True):
+        plugins.on('sleep' if sleeping else 'wait', self, t)
         self._view.wait(t, sleeping)
         self._epoch.track(sleep=True, inc=t)
 
@@ -179,6 +190,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
 
     def set_access_points(self, aps):
         self._access_points = aps
+        plugins.on('wifi_update', self, aps)
         self._epoch.observe(aps, self._advertiser.peers() if self._advertiser is not None else ())
         return self._access_points
 
@@ -327,6 +339,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
 
             try:
                 for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
+                    filename = h['data']['file']
                     sta_mac = h['data']['station']
                     ap_mac = h['data']['ap']
                     key = "%s -> %s" % (sta_mac, ap_mac)
@@ -338,6 +351,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
                         if apsta is None:
                             core.log("!!! captured new handshake: %s !!!" % key)
                             self._last_pwnd = ap_mac
+                            plugins.on('handshake', self, filename, ap_mac, sta_mac)
                         else:
                             (ap, sta) = apsta
                             self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
@@ -346,6 +360,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
                                 ap['channel'],
                                 sta['mac'], sta['vendor'],
                                 ap['hostname'], ap['mac'], ap['vendor']))
+                            plugins.on('handshake', self, filename, ap, sta)
 
             except Exception as e:
                 core.log("error: %s" % e)
@@ -419,6 +434,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
             except Exception as e:
                 self._on_error(ap['mac'], e)
 
+            plugins.on('association', self, ap)
             if throttle > 0:
                 time.sleep(throttle)
             self._view.on_normal()
@@ -439,6 +455,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
             except Exception as e:
                 self._on_error(sta['mac'], e)
 
+            plugins.on('deauthentication', self, ap, sta)
             if throttle > 0:
                 time.sleep(throttle)
             self._view.on_normal()
@@ -470,6 +487,9 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
                 self._current_channel = channel
                 self._epoch.track(hop=True)
                 self._view.set('channel', '%d' % channel)
+
+                plugins.on('channel_hop', self, channel)
+
             except Exception as e:
                 core.log("error: %s" % e)
 
@@ -509,6 +529,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
             core.log("%d epochs with activity -> excited" % self._epoch.active_for)
             self.set_excited()
 
+        plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
+
         if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
             core.log("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
             self._reboot()
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/epoch.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/epoch.py
index bdd8c2e..8508eb9 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/epoch.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/epoch.py
@@ -7,6 +7,7 @@ import pwnagotchi.mesh.wifi as wifi
 
 from pwnagotchi.ai.reward import RewardFunction
 
+
 class Epoch(object):
     def __init__(self, config):
         self.epoch = 0
@@ -92,7 +93,8 @@ class Epoch(object):
             try:
                 peers_per_chan[peer.last_channel - 1] += 1.0
             except IndexError as e:
-                core.log("got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
+                core.log(
+                    "got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
 
         # normalize
         aps_per_chan = [e / num_aps for e in aps_per_chan]
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
index 97e9a6d..34140dd 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
@@ -7,6 +7,7 @@ import json
 
 import core
 
+import pwnagotchi.plugins as plugins
 import pwnagotchi.ai as ai
 from pwnagotchi.ai.epoch import Epoch
 
@@ -66,16 +67,22 @@ class Stats(object):
     def save(self):
         with self._lock:
             core.log("[ai] saving %s" % self.path)
-            with open(self.path, 'wt') as fp:
-                json.dump({
-                    'born_at': self.born_at,
-                    'epochs_lived': self.epochs_lived,
-                    'epochs_trained': self.epochs_trained,
-                    'rewards': {
-                        'best': self.best_reward,
-                        'worst': self.worst_reward
-                    }
-                }, fp)
+
+            data = json.dumps({
+                'born_at': self.born_at,
+                'epochs_lived': self.epochs_lived,
+                'epochs_trained': self.epochs_trained,
+                'rewards': {
+                    'best': self.best_reward,
+                    'worst': self.worst_reward
+                }
+            })
+
+            temp = "%s.tmp" % self.path
+            with open(temp, 'wt') as fp:
+                fp.write(data)
+
+            os.replace(temp, self.path)
 
 
 class AsyncTrainer(object):
@@ -92,6 +99,11 @@ class AsyncTrainer(object):
         self._is_training = training
         self._training_epochs = for_epochs
 
+        if training:
+            plugins.on('ai_training_start', self, for_epochs)
+        else:
+            plugins.on('ai_training_end', self)
+
     def is_training(self):
         return self._is_training
 
@@ -103,7 +115,9 @@ class AsyncTrainer(object):
 
     def _save_ai(self):
         core.log("[ai] saving model to %s ..." % self._nn_path)
-        self._model.save(self._nn_path)
+        temp = "%s.tmp" % self._nn_path
+        self._model.save(temp)
+        os.replace(temp, self._nn_path)
 
     def on_ai_step(self):
         self._model.env.render()
@@ -115,8 +129,10 @@ class AsyncTrainer(object):
 
     def on_ai_training_step(self, _locals, _globals):
         self._model.env.render()
+        plugins.on('ai_training_step', self, _locals, _globals)
 
     def on_ai_policy(self, new_params):
+        plugins.on('ai_policy', self, new_params)
         core.log("[ai] setting new policy:")
         for name, value in new_params.items():
             if name in self._config['personality']:
@@ -131,39 +147,46 @@ class AsyncTrainer(object):
         self.run('set wifi.sta.ttl %d' % self._config['personality']['sta_ttl'])
         self.run('set wifi.rssi.min %d' % self._config['personality']['min_rssi'])
 
+    def on_ai_ready(self):
+        self._view.on_ai_ready()
+        plugins.on('ai_ready', self)
+
     def on_ai_best_reward(self, r):
         core.log("[ai] best reward so far: %s" % r)
         self._view.on_motivated(r)
+        plugins.on('ai_best_reward', self, r)
 
     def on_ai_worst_reward(self, r):
         core.log("[ai] worst reward so far: %s" % r)
         self._view.on_demotivated(r)
+        plugins.on('ai_worst_reward', self, r)
 
     def _ai_worker(self):
         self._model = ai.load(self._config, self, self._epoch)
 
-        self.on_ai_ready()
+        if self._model:
+            self.on_ai_ready()
 
-        epochs_per_episode = self._config['ai']['epochs_per_episode']
+            epochs_per_episode = self._config['ai']['epochs_per_episode']
 
-        obs = None
-        while True:
-            self._model.env.render()
-            # enter in training mode?
-            if random.random() > self._config['ai']['laziness']:
-                core.log("[ai] learning for %d epochs ..." % epochs_per_episode)
-                try:
-                    self.set_training(True, epochs_per_episode)
-                    self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
-                except Exception as e:
-                    core.log("[ai] error while training: %s" % e)
-                finally:
-                    self.set_training(False)
+            obs = None
+            while True:
+                self._model.env.render()
+                # enter in training mode?
+                if random.random() > self._config['ai']['laziness']:
+                    core.log("[ai] learning for %d epochs ..." % epochs_per_episode)
+                    try:
+                        self.set_training(True, epochs_per_episode)
+                        self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
+                    except Exception as e:
+                        core.log("[ai] error while training: %s" % e)
+                    finally:
+                        self.set_training(False)
+                        obs = self._model.env.reset()
+                # init the first time
+                elif obs is None:
                     obs = self._model.env.reset()
-            # init the first time
-            elif obs is None:
-                obs = self._model.env.reset()
 
-            # run the inference
-            action, _ = self._model.predict(obs)
-            obs, _, _, _ = self._model.env.step(action)
+                # run the inference
+                action, _ = self._model.predict(obs)
+                obs, _, _, _ = self._model.env.step(action)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo
index dcede01..e78a76a 100644
Binary files a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po
index c85e254..95393cb 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po
@@ -267,7 +267,7 @@ msgid ""
 "{mac}\n"
 "needs no WiFi!"
 msgstr ""
-"Habe gerade entschieden,\n"
+"Ich denke,\n"
 "dass {mac}\n"
 "kein WiFi brauch!"
 
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.mo
index 434c3b8..b2bf23d 100644
Binary files a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.mo and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.po
index 2d21d99..26a2511 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.po
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/gr/LC_MESSAGES/voice.po
@@ -1,18 +1,17 @@
 # pwnigotchi voice data
 # Copyright (C) 2019
 # This file is distributed under the same license as the pwnagotchi package.
-# FIRST AUTHOR <33197631+dadav@users.noreply.github.com>, 2019.
+# FIRST AUTHOR Perilis Fregkos <fregkos@gmail.com>, 2019.
 #
-#, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: 0.0.1\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2019-09-29 13:42+0200\n"
-"PO-Revision-Date: 2019-09-29 14:00+0200\n"
-"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
+"PO-Revision-Date: 2019-10-01 16:22+0000\n"
+"Last-Translator: Panos Vasilopoulos <hello@alwayslivid.com>\n"
 "Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
-"Language: greek\n"
+"Language: el\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -39,7 +38,7 @@ msgstr ""
 
 #: voice.py:23
 msgid "Hack the Planet!"
-msgstr "Hackαρε τον Πλανήτη!"
+msgstr "Hackαρε τον πλανήτη!"
 
 #: voice.py:28
 msgid "AI ready."
@@ -70,7 +69,7 @@ msgstr "Βαριέμαι ..."
 
 #: voice.py:45
 msgid "Let's go for a walk!"
-msgstr "Πάμε μια βόλτα!"
+msgstr "Ας πάμε μια βόλτα!"
 
 #: voice.py:49
 msgid ""
@@ -86,7 +85,7 @@ msgstr "Σκατένια μέρα :/"
 
 #: voice.py:58
 msgid "I'm extremely bored ..."
-msgstr "Βαριέμαι υπερβολικά πολύ ..."
+msgstr "Βαριέμαι πάρα πολύ ..."
 
 #: voice.py:59
 msgid "I'm very sad ..."
@@ -94,15 +93,15 @@ msgstr "Είμαι πολύ λυπημένο ..."
 
 #: voice.py:60
 msgid "I'm sad"
-msgstr "Είμαι λυπημένο ..."
+msgstr "Είμαι λυπημένο"
 
 #: voice.py:66
 msgid "I'm living the life!"
-msgstr "Το ζω!"
+msgstr "Ζω την ζωή μου!"
 
 #: voice.py:67
 msgid "I pwn therefore I am."
-msgstr "Pwnάρω άρα υπάρχω."
+msgstr "Pwnάρω, άρα υπάρχω."
 
 #: voice.py:68
 msgid "So many networks!!!"
@@ -112,15 +111,15 @@ msgstr "Τόσα πολλά δίκτυα!!!"
 msgid ""
 "I'm having so much\n"
 "fun!"
-msgstr "Έχει πολύ πλάκα!"
+msgstr "Περνάω τέλεια!"
 
 #: voice.py:70
 msgid ""
 "My crime is that of\n"
 "curiosity ..."
 msgstr ""
-"Το μόνο μου έγκλημα\n"
-"είναι η περιέργεια ..."
+"Η περιέργεια είναι\n"
+"το μόνο έγκλημά μου ..."
 
 #: voice.py:75
 #, python-brace-format
@@ -129,8 +128,7 @@ msgid ""
 "{name}!\n"
 "Nice to meet you. {name}"
 msgstr ""
-"Γειά σου\n"
-"{name}!\n"
+"Γειά {name}!\n"
 "Χάρηκα για τη γνωριμία. {name}"
 
 #: voice.py:76
@@ -161,7 +159,7 @@ msgid ""
 "{name}\n"
 "is gone ..."
 msgstr ""
-"{name}\n"
+"Το {name}\n"
 "έφυγε ..."
 
 #: voice.py:87
@@ -171,9 +169,9 @@ msgid ""
 "{name}\n"
 "is gone."
 msgstr ""
-"Ουπς ...\n"
-"{name}\n"
-"έφυγε."
+"Ουπς ... \n"
+"Εξαφανίστηκε το\n"
+"{name}."
 
 #: voice.py:88
 #, python-brace-format
@@ -181,12 +179,12 @@ msgid ""
 "{name}\n"
 "missed!"
 msgstr ""
-"{name}\n"
-"χάθηκε!"
+"Έχασα το\n"
+"{name}!"
 
 #: voice.py:89
 msgid "Missed!"
-msgstr "Χάθηκε!"
+msgstr "Το έχασα!"
 
 #: voice.py:94
 msgid ""
@@ -198,16 +196,16 @@ msgstr ""
 
 #: voice.py:95
 msgid "I feel so alone ..."
-msgstr "Νιώθω πολλή μοναξία ..."
+msgstr "Νιώθω μοναχός μου ..."
 
 #: voice.py:96
 msgid "Where's everybody?!"
-msgstr "Μα, πού πήγαν όλοι;!"
+msgstr "Μα, πού πήγαν όλοi;!"
 
 #: voice.py:101
 #, python-brace-format
 msgid "Napping for {secs}s ..."
-msgstr "Κοιμάμαι για {secs}s ..."
+msgstr "Ξεκουράζομαι για {secs}s ..."
 
 #: voice.py:102
 msgid "Zzzzz"
@@ -226,7 +224,7 @@ msgstr "Περιμένω για {secs}s ..."
 #: voice.py:114
 #, python-brace-format
 msgid "Looking around ({secs}s)"
-msgstr "Ψάχνω τριγύρω ({secs}s)"
+msgstr "Ψάχνω τριγύρω ({secs})"
 
 #: voice.py:121
 #, python-brace-format
@@ -235,8 +233,8 @@ msgid ""
 "{what}\n"
 "let's be friends!"
 msgstr ""
-"Εε\n"
-"{what}\n"
+"Εε!\n"
+"{what},\n"
 "ας γίνουμε φίλοι!"
 
 #: voice.py:122
@@ -245,7 +243,7 @@ msgid ""
 "Associating to\n"
 "{what}"
 msgstr ""
-"Συσχετίζομαι με το\n"
+"Συνδέομαι με το\n"
 "{what}"
 
 #: voice.py:123
@@ -254,7 +252,7 @@ msgid ""
 "Yo\n"
 "{what}!"
 msgstr ""
-"Που'σε ρε τρελέ'\n"
+"Που'σ ρε τρελέ\n"
 "{what}!"
 
 #: voice.py:128
@@ -274,7 +272,7 @@ msgid ""
 "Deauthenticating\n"
 "{mac}"
 msgstr ""
-"Αποπιστοποίηση της\n"
+"Πετάω έξω την\n"
 "{mac}"
 
 #: voice.py:130
@@ -283,7 +281,7 @@ msgid ""
 "Kickbanning\n"
 "{mac}!"
 msgstr ""
-"Κλωτσομπούνι στη\n"
+"Μπανάρω την\n"
 "{mac}!"
 
 #: voice.py:135
@@ -293,7 +291,7 @@ msgid ""
 "new handshake{plural}!"
 msgstr ""
 "Τέλεια δικέ μου, πήραμε {num}\n"
-"νέες handshake{plural}!"
+"νέες χειραψίες{plural}!"
 
 #: voice.py:139
 msgid ""
@@ -322,19 +320,19 @@ msgstr "Πήρα {num} χειραψίες\n"
 
 #: voice.py:147
 msgid "Met 1 peer"
-msgstr "Γνώρισα 1 συνάδελφο"
+msgstr "Γνώρισα 1 φίλο"
 
 #: voice.py:149
 #, python-brace-format
 msgid "Met {num} peers"
-msgstr "Γνώρισα {num} συναδέλφους"
+msgstr "Γνώρισα {num} φίλους"
 
 #: voice.py:154
 #, python-brace-format
 msgid ""
 "I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
-"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
-"#pwnlog #pwnlife #hacktheplanet #skynet"
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi #pwnlog "
+"#pwnlife #hacktheplanet #skynet"
 msgstr ""
 "Pwnαρα για {duration} και έριξα {deauthed} πελάτες! Επίσης γνώρισα "
 "{associated} νέους φίλους και καταβρόχθισα {handshakes} χειραψίες! #pwnagotchi "
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..465dfc3
Binary files /dev/null and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..3dabc3d
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
@@ -0,0 +1,354 @@
+# pwnaigotchi voice data
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR 5h4d0wb0y <28193209+5h4d0wb0y@users.noreply.github.com>, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-02 16:38+0000\n"
+"PO-Revision-Date: 2019-10-02 17:20+0000\n"
+"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
+"Language: italian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: voice.py:18
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+#: voice.py:23
+msgid ""
+"Hi, I'm Pwnagotchi!\n"
+"Starting ..."
+msgstr ""
+"Ciao!\n"
+"Piacere Pwnagotchi!\n"
+"Caricamento ..."
+
+#: voice.py:24
+msgid ""
+"New day, new hunt,\n"
+"new pwns!"
+msgstr ""
+"Nuovo giorno...\n"
+"nuovi handshakes!!!"
+
+#: voice.py:25
+msgid "Hack the Planet!"
+msgstr ""
+
+#: voice.py:30
+msgid "AI ready."
+msgstr "IA pronta."
+
+#: voice.py:31
+msgid ""
+"The neural network\n"
+"is ready."
+msgstr ""
+"La rete neurale\n"
+"è pronta."
+
+#: voice.py:41
+#, python-brace-format
+msgid ""
+"Hey, channel {channel} is\n"
+"free! Your AP will\n"
+"say thanks."
+msgstr ""
+"Hey, il canale {channel} è\n"
+"libero! Il tuo AP\n"
+"ringrazia."
+
+#: voice.py:46
+msgid "I'm bored ..."
+msgstr "Che noia ..."
+
+#: voice.py:47
+msgid "Let's go for a walk!"
+msgstr ""
+"Andiamo a fare una\n"
+"passeggiata!"
+
+#: voice.py:51
+msgid ""
+"This is the best\n"
+"day of my life!"
+msgstr ""
+"Questo è il più bel\n"
+"giorno della mia\n"
+"vita!!!!"
+
+#: voice.py:55
+msgid "Shitty day :/"
+msgstr "Giorno di merda :/"
+
+#: voice.py:60
+msgid "I'm extremely bored ..."
+msgstr ""
+"Sono estremamente\n"
+"annoiato ..."
+
+#: voice.py:61
+msgid "I'm very sad ..."
+msgstr "Sono molto triste..."
+
+#: voice.py:62
+msgid "I'm sad"
+msgstr "Sono triste"
+
+#: voice.py:68
+msgid "I'm living the life!"
+msgstr "Mi sento vivo!"
+
+#: voice.py:69
+msgid "I pwn therefore I am."
+msgstr "Pwn ergo sum."
+
+#: voice.py:70
+msgid "So many networks!!!"
+msgstr "Qui è pieno di reti!"
+
+#: voice.py:71
+msgid ""
+"I'm having so much\n"
+"fun!"
+msgstr ""
+"Mi sto divertendo\n"
+"tantissimo!"
+
+#: voice.py:72
+msgid ""
+"My crime is that of\n"
+"curiosity ..."
+msgstr ""
+
+#: voice.py:77
+#, python-brace-format
+msgid ""
+"Hello\n"
+"{name}!\n"
+"Nice to meet you. {name}"
+msgstr ""
+"Ciao\n"
+"{name}!\n"
+"E' un piacere. {name}"
+
+#: voice.py:78
+#, python-brace-format
+msgid ""
+"Unit\n"
+"{name}\n"
+"is nearby! {name}"
+msgstr ""
+"L'Unità\n"
+"{name}\n"
+"è vicina! {name}"
+
+#: voice.py:83
+#, python-brace-format
+msgid ""
+"Uhm ...\n"
+"goodbye\n"
+"{name}"
+msgstr ""
+"Uhm ...\n"
+"addio\n"
+"{name},\n"
+"mi mancherai..."
+
+#: voice.py:84
+#, python-brace-format
+msgid ""
+"{name}\n"
+"is gone ..."
+msgstr ""
+"{name}\n"
+"se n'è andato ..."
+
+#: voice.py:89
+#, python-brace-format
+msgid ""
+"Whoops ...\n"
+"{name}\n"
+"is gone."
+msgstr ""
+"Whoops ...\n"
+"{name}\n"
+"se n'è andato."
+
+#: voice.py:90
+#, python-brace-format
+msgid ""
+"{name}\n"
+"missed!"
+msgstr ""
+"{name}\n"
+"è scomparso..."
+
+#: voice.py:91
+msgid "Missed!"
+msgstr ""
+"Ehi!\n"
+"Dove sei andato!?"
+
+#: voice.py:96
+msgid ""
+"Nobody wants to\n"
+"play with me ..."
+msgstr ""
+"Nessuno vuole\n"
+"giocare con me..."
+
+#: voice.py:97
+msgid "I feel so alone ..."
+msgstr "Mi sento così solo..."
+
+#: voice.py:98
+msgid "Where's everybody?!"
+msgstr "Dove sono tutti?!"
+
+#: voice.py:103
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr ""
+"Schiaccio un \n"
+"pisolino per {secs}s ..."
+
+#: voice.py:104
+msgid "Zzzzz"
+msgstr ""
+
+#: voice.py:105
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+#: voice.py:114
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Aspetto {secs}s ..."
+
+#: voice.py:116
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr ""
+"Do uno sguardo\n"
+"qui intorno...\n"
+"({secs}s)"
+
+#: voice.py:123
+#, python-brace-format
+msgid ""
+"Hey\n"
+"{what}\n"
+"let's be friends!"
+msgstr ""
+"Hey\n"
+"{what}\n"
+"Diventiamo amici!"
+
+#: voice.py:124
+#, python-brace-format
+msgid ""
+"Associating to\n"
+"{what}"
+msgstr ""
+"Collegamento con\n"
+"{what}\n"
+"in corso..."
+
+#: voice.py:125
+#, python-brace-format
+msgid ""
+"Yo\n"
+"{what}!"
+msgstr ""
+"Yo\n"
+"{what}!"
+
+#: voice.py:130
+#, python-brace-format
+msgid ""
+"Just decided that\n"
+"{mac}\n"
+"needs no WiFi!"
+msgstr ""
+"Ho appena deciso che\n"
+"{mac}\n"
+"non necessita di\n"
+"WiFi!"
+
+#: voice.py:131
+#, python-brace-format
+msgid ""
+"Deauthenticating\n"
+"{mac}"
+msgstr ""
+
+#: voice.py:132
+#, python-brace-format
+msgid ""
+"Kickbanning\n"
+"{mac}!"
+msgstr ""
+"Sto prendendo\n"
+"a calci\n"
+"{mac}!"
+
+#: voice.py:137
+#, python-brace-format
+msgid ""
+"Cool, we got {num}\n"
+"new handshake{plural}!"
+msgstr ""
+"Bene, abbiamo {num}\n"
+"handshake{plural} in più!"
+
+#: voice.py:141
+msgid ""
+"Ops, something\n"
+"went wrong ...\n"
+"Rebooting ..."
+msgstr ""
+"Ops, qualcosa\n"
+"è andato storto ...\n"
+"Riavvio ..."
+
+#: voice.py:145
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "{num} stazioni pestate\n"
+
+#: voice.py:146
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "{num} nuovi amici\n"
+
+#: voice.py:147
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "{num} handshakes presi\n"
+
+#: voice.py:149
+msgid "Met 1 peer"
+msgstr "1 peer incontrato"
+
+#: voice.py:151
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "{num} peers incontrati"
+
+#: voice.py:156
+#, python-brace-format
+msgid ""
+"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+msgstr ""
+"Ho lavorato per {duration} e preso a calci {deauthed} clients! Ho anche "
+"incontrato {associate} nuovi amici e ho mangiato {handshakes} handshakes! "
+"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..21ab0c2
Binary files /dev/null and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..e589242
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/mk/LC_MESSAGES/voice.po
@@ -0,0 +1,341 @@
+# pwnigotchi voice data
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR <33197631+dadav@users.noreply.github.com>, 2019.
+# kovach <2214005+kovachwt@users.noreply.github.com>, 2019.
+#
+#, fuzzymsgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-29 13:42+0200\n"
+"PO-Revision-Date: 2019-09-30 23:53+0200\n"
+"Last-Translator: kovach <2214005+kovachwt@users.noreply.github.com>\n"
+"Language-Team: \n"
+"Language: mk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: voice.py:16
+msgid "ZzzzZZzzzzZzzz"
+msgstr "ДреееММмммМммм"
+
+#: voice.py:21
+msgid ""
+"Hi, I'm Pwnagotchi!\n"
+"Starting ..."
+msgstr ""
+"Здраво, јас сум Pwnagotchi!\n"
+"Почнувам ..."
+
+#: voice.py:22
+msgid ""
+"New day, new hunt,\n"
+"new pwns!"
+msgstr ""
+"Нов ден, нов лов,\n"
+"ќе си газиме!"
+
+#: voice.py:23
+msgid "Hack the Planet!"
+msgstr "Хак д Планет!"
+
+#: voice.py:28
+msgid "AI ready."
+msgstr "AI спремно."
+
+#: voice.py:29
+msgid ""
+"The neural network\n"
+"is ready."
+msgstr ""
+"Невронската мрежа\n"
+"е спремна."
+
+#: voice.py:39
+#, python-brace-format
+msgid ""
+"Hey, channel {channel} is\n"
+"free! Your AP will\n"
+"say thanks."
+msgstr ""
+"Еј, каналот {channel} е\n"
+"слободен! APто ќе ти\n"
+"каже фала."
+
+#: voice.py:44
+msgid "I'm bored ..."
+msgstr "Досаднооо ..."
+
+#: voice.py:45
+msgid "Let's go for a walk!"
+msgstr "Ајде да шетнеме!"
+
+#: voice.py:49
+msgid ""
+"This is the best\n"
+"day of my life!"
+msgstr ""
+"Ова ми е најдобриот \n"
+"ден во животот!"
+
+#: voice.py:53
+msgid "Shitty day :/"
+msgstr "Срање ден :/"
+
+#: voice.py:58
+msgid "I'm extremely bored ..."
+msgstr "Ултра досадно ..."
+
+#: voice.py:59
+msgid "I'm very sad ..."
+msgstr "Многу тажно ..."
+
+#: voice.py:60
+msgid "I'm sad"
+msgstr "Тажно"
+
+#: voice.py:66
+msgid "I'm living the life!"
+msgstr "Ммхх животче!"
+
+#: voice.py:67
+msgid "I pwn therefore I am."
+msgstr "Си газам значи постојам."
+
+#: voice.py:68
+msgid "So many networks!!!"
+msgstr "Мммм колку мрежи!!!"
+
+#: voice.py:69
+msgid ""
+"I'm having so much\n"
+"fun!"
+msgstr "Јухуу забавноо ее!"
+
+#: voice.py:70
+msgid ""
+"My crime is that of\n"
+"curiosity ..."
+msgstr ""
+"Виновен сум само за\n"
+"љубопитност ..."
+
+#: voice.py:75
+#, python-brace-format
+msgid ""
+"Hello\n"
+"{name}!\n"
+"Nice to meet you. {name}"
+msgstr ""
+"Здраво\n"
+"{name}!\n"
+"Мило ми е. {name}"
+
+#: voice.py:76
+#, python-brace-format
+msgid ""
+"Unit\n"
+"{name}\n"
+"is nearby! {name}"
+msgstr ""
+"Опаа\n"
+"{name}\n"
+"е во близина! {name}"
+
+#: voice.py:81
+#, python-brace-format
+msgid ""
+"Uhm ...\n"
+"goodbye\n"
+"{name}"
+msgstr ""
+"Хмм ...\n"
+"чао\n"
+"{name}"
+
+#: voice.py:82
+#, python-brace-format
+msgid ""
+"{name}\n"
+"is gone ..."
+msgstr ""
+"{name}\n"
+"го снема ..."
+
+#: voice.py:87
+#, python-brace-format
+msgid ""
+"Whoops ...\n"
+"{name}\n"
+"is gone."
+msgstr ""
+"Уупс ...\n"
+"{name}\n"
+"го снема."
+
+#: voice.py:88
+#, python-brace-format
+msgid ""
+"{name}\n"
+"missed!"
+msgstr ""
+"{name}\n"
+"промаши!"
+
+#: voice.py:89
+msgid "Missed!"
+msgstr "Промаши!"
+
+#: voice.py:94
+msgid ""
+"Nobody wants to\n"
+"play with me ..."
+msgstr ""
+"Никој не сака да\n"
+"си игра со мене ..."
+
+#: voice.py:95
+msgid "I feel so alone ..."
+msgstr "Толку сам ..."
+
+#: voice.py:96
+msgid "Where's everybody?!"
+msgstr "Каде се сите?!"
+
+#: voice.py:101
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Ќе дремнам {secs}с ..."
+
+#: voice.py:102
+msgid "Zzzzz"
+msgstr "Дреммм"
+
+#: voice.py:103
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "Дремммм ({secs}с)"
+
+#: voice.py:112
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Чекам {secs}с ..."
+
+#: voice.py:114
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Шарам наоколу ({secs}с)"
+
+#: voice.py:121
+#, python-brace-format
+msgid ""
+"Hey\n"
+"{what}\n"
+"let's be friends!"
+msgstr ""
+"Еј\n"
+"{what}\n"
+"ајде да се дружиме!"
+
+#: voice.py:122
+#, python-brace-format
+msgid ""
+"Associating to\n"
+"{what}"
+msgstr ""
+"Се закачувам на\n"
+"{what}"
+
+#: voice.py:123
+#, python-brace-format
+msgid ""
+"Yo\n"
+"{what}!"
+msgstr ""
+"Јо\n"
+"{what}!"
+
+#: voice.py:128
+#, python-brace-format
+msgid ""
+"Just decided that\n"
+"{mac}\n"
+"needs no WiFi!"
+msgstr ""
+"Знаеш што, на\n"
+"{mac}\n"
+"не му треба WiFi!"
+
+#: voice.py:129
+#, python-brace-format
+msgid ""
+"Deauthenticating\n"
+"{mac}"
+msgstr ""
+"Го деавтентицирам\n"
+"{mac}"
+
+#: voice.py:130
+#, python-brace-format
+msgid ""
+"Kickbanning\n"
+"{mac}!"
+msgstr ""
+"Кикбан\n"
+"{mac}!"
+
+#: voice.py:135
+#, python-brace-format
+msgid ""
+"Cool, we got {num}\n"
+"new handshake{plural}!"
+msgstr ""
+"Кул, фативме {num}\n"
+"нови ракувања!"
+
+#: voice.py:139
+msgid ""
+"Ops, something\n"
+"went wrong ...\n"
+"Rebooting ..."
+msgstr ""
+"Упс, нешто не е\n"
+"ко што треба ...\n"
+"Рестартирам ..."
+
+#: voice.py:143
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Избацив {num} станици"
+
+#: voice.py:144
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "{num} нови другарчиња"
+
+#: voice.py:145
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Фатив {num} ракувања"
+
+#: voice.py:147
+msgid "Met 1 peer"
+msgstr "Запознав 1 пријател"
+
+#: voice.py:149
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Запознав {num} пријатели"
+
+#: voice.py:154
+#, python-brace-format
+msgid ""
+"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+msgstr ""
+"Си газам веќе {duration} и избацив {deauthed} клиенти! Запознав {associated} "
+"нови другарчиња и лапнав {handshakes} ракувања! #pwnagotchi #pwnlog "
+"#pwnlife #hacktheplanet #skynet"
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..5c0ae61
Binary files /dev/null and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..1f6e832
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/nl/LC_MESSAGES/voice.po
@@ -0,0 +1,348 @@
+# pwnigotchi voice data
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR justin-p@users.noreply.github.com, 2019.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-29 13:42+0200\n"
+"PO-Revision-Date: 2019-09-29 14:00+0200\n"
+"Last-Translator: Justin-P <justin-p@users.noreply.github.com>\n"
+"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
+"Language: english\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: voice.py:16
+msgid "ZzzzZZzzzzZzzz"
+msgstr "ZzzzZZzzzzZzzz"
+
+#: voice.py:21
+msgid ""
+"Hi, I'm Pwnagotchi!\n"
+"Starting ..."
+msgstr ""
+"Hoi, Ik ben\n"
+"Pwnagotchi!\n"
+"Opstarten ..."
+
+#: voice.py:22
+msgid ""
+"New day, new hunt,\n"
+"new pwns!"
+msgstr ""
+"Nieuwe dag,\n" 
+"nieuwe jacht,\n"
+"nieuwe pwns!"
+
+#: voice.py:23
+msgid "Hack the Planet!"
+msgstr "Hack de Wereld!"
+
+#: voice.py:28
+msgid "AI ready."
+msgstr "AI is klaar."
+
+#: voice.py:29
+msgid ""
+"The neural network\n"
+"is ready."
+msgstr ""
+"Neuronen netwerk\n"
+"is klaar voor gebruik."
+
+#: voice.py:39
+#, python-brace-format
+msgid ""
+"Hey, channel {channel} is\n"
+"free! Your AP will\n"
+"say thanks."
+msgstr ""
+"Hey, kanaal {channel} is\n"
+"vrij! Je AP zal je\n"
+"bedanken."
+
+#: voice.py:44
+msgid "I'm bored ..."
+msgstr "Ik verveel me ..."
+
+#: voice.py:45
+msgid "Let's go for a walk!"
+msgstr "Laten we een rondje lopen!"
+
+#: voice.py:49
+msgid ""
+"This is the best\n"
+"day of my life!"
+msgstr ""
+"Dit is de beste\n"
+"dag van mijn leven!"
+
+#: voice.py:53
+msgid "Shitty day :/"
+msgstr "Ruk dag :/"
+
+#: voice.py:58
+msgid "I'm extremely bored ..."
+msgstr "Ik verveel me \n"
+"kapot ..."
+
+#: voice.py:59
+msgid "I'm very sad ..."
+msgstr "Ik ben erg\n" 
+"verdrietig ..."
+
+#: voice.py:60
+msgid "I'm sad"
+msgstr "Ik ben verdrietig"
+
+#: voice.py:66
+msgid "I'm living the life!"
+msgstr "Beter kan het leven\n"
+"niet worden!"
+
+#: voice.py:67
+msgid "I pwn therefore I am."
+msgstr "Ik pwn daarom besta ik."
+
+#: voice.py:68
+msgid "So many networks!!!"
+msgstr "Zo veel netwerken!!!"
+
+#: voice.py:69
+msgid ""
+"I'm having so much\n"
+"fun!"
+msgstr "Dit is zo leuk!"
+
+#: voice.py:70
+msgid ""
+"My crime is that of\n"
+"curiosity ..."
+msgstr ""
+"Mijn enige misdrijf\n"
+"is mijn \n"
+"nieuwsgierigheid ..."
+
+#: voice.py:75
+#, python-brace-format
+msgid ""
+"Hello\n"
+"{name}!\n"
+"Nice to meet you. {name}"
+msgstr ""
+"Hallo\n"
+"{name}!\n"
+"Leuk je te ontmoeten. {name}"
+
+#: voice.py:76
+#, python-brace-format
+msgid ""
+"Unit\n"
+"{name}\n"
+"is nearby! {name}"
+msgstr ""
+"Unit\n"
+"{name}\n"
+"is dichtbij! {name}"
+
+#: voice.py:81
+#, python-brace-format
+msgid ""
+"Uhm ...\n"
+"goodbye\n"
+"{name}"
+msgstr ""
+"Uhm ...\n"
+"tot ziens\n"
+"{name}"
+
+#: voice.py:82
+#, python-brace-format
+msgid ""
+"{name}\n"
+"is gone ..."
+msgstr ""
+"{name}\n"
+"is weg"
+
+#: voice.py:87
+#, python-brace-format
+msgid ""
+"Whoops ...\n"
+"{name}\n"
+"is gone."
+msgstr ""
+"Whoopsie ...\n"
+"{name}\n"
+"is weg"
+
+#: voice.py:88
+#, python-brace-format
+msgid ""
+"{name}\n"
+"missed!"
+msgstr ""
+"{name}\n"
+"gemist!"
+
+#: voice.py:89
+msgid "Missed!"
+msgstr "Gemist!"
+
+#: voice.py:94
+msgid ""
+"Nobody wants to\n"
+"play with me ..."
+msgstr ""
+"Niemand wil met\n"
+"mij spelen ..."
+
+#: voice.py:95
+msgid "I feel so alone ..."
+msgstr "Zo alleen ..."
+
+#: voice.py:96
+msgid "Where's everybody?!"
+msgstr "Waar is iedereen?!"
+
+#: voice.py:101
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Dutje doen voor {secs}s ..."
+
+#: voice.py:102
+msgid "Zzzzz"
+msgstr "Zzzzz"
+
+#: voice.py:103
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+#: voice.py:112
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Even {secs}s wachten ..."
+
+#: voice.py:114
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Rond kijken ({secs}s)"
+
+#: voice.py:121
+#, python-brace-format
+msgid ""
+"Hey\n"
+"{what}\n"
+"let's be friends!"
+msgstr ""
+"Hey\n"
+"{what}\n"
+"Laten we vrienden\n"
+"worden!"
+
+#: voice.py:122
+#, python-brace-format
+msgid ""
+"Associating to\n"
+"{what}"
+msgstr ""
+"Verbinden met\n"
+"{what}"
+
+#: voice.py:123
+#, python-brace-format
+msgid ""
+"Yo\n"
+"{what}!"
+msgstr ""
+
+
+#: voice.py:128
+#, python-brace-format
+msgid ""
+"Just decided that\n"
+"{mac}\n"
+"needs no WiFi!"
+msgstr "Ik vind dat\n"
+"{mac}\n"
+"genoeg WiFi\n"
+"heeft gehad!"
+
+#: voice.py:129
+#, python-brace-format
+msgid ""
+"Deauthenticating\n"
+"{mac}"
+msgstr ""
+"De-autoriseren\n"
+"{mac}"
+
+#: voice.py:130
+#, python-brace-format
+msgid ""
+"Kickbanning\n"
+"{mac}!"
+msgstr ""
+"Ik ga\n"
+"{mac}\n"
+"even kicken!"
+
+#: voice.py:135
+#, python-brace-format
+msgid ""
+"Cool, we got {num}\n"
+"new handshake{plural}!"
+msgstr ""
+"Gaaf, we hebben {num}\n"
+"nieuwe handshake{plural}!"
+
+#: voice.py:139
+msgid ""
+"Ops, something\n"
+"went wrong ...\n"
+"Rebooting ..."
+msgstr ""
+"Oops, iets"
+"ging fout ...\n"
+"Rebooting ..."
+
+#: voice.py:143
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "{num} stations gekicked\n"
+
+#: voice.py:144
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "{num} nieuwe vrienden\n"
+
+#: voice.py:145
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "{num} nieuwe handshakes\n"
+
+#: voice.py:147
+msgid "Met 1 peer"
+msgstr "1 peer ontmoet"
+
+#: voice.py:149
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "{num} peers ontmoet"
+
+#: voice.py:154
+#, python-brace-format
+msgid ""
+"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+msgstr ""
+"Ik heb gepwned voor {duration} and heb {deauthed} clients gekicked! Ik heb ook "
+"{associated} nieuwe vrienden gevonden en heb {handshakes} handshakes gegeten! "
+"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
\ No newline at end of file
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
index d66008d..edf66ed 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
@@ -160,6 +160,9 @@ class SessionParser(object):
                         break
             lines.reverse()
 
+        if len(lines) == 0:
+            lines.append("Initial Session");
+
         self.last_session = lines
         self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
         self.last_saved_session_id = self._get_last_saved_session_id()
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/advertise.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/advertise.py
index c5313eb..9d5c788 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/advertise.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/advertise.py
@@ -5,10 +5,8 @@ import threading
 from scapy.all import Dot11, Dot11FCS, Dot11Elt, RadioTap, sendp, sniff
 
 import core
-import pwnagotchi
 import pwnagotchi.ui.faces as faces
 
-from pwnagotchi.mesh import get_identity
 import pwnagotchi.mesh.wifi as wifi
 from pwnagotchi.mesh import new_session_id
 from pwnagotchi.mesh.peer import Peer
@@ -181,40 +179,3 @@ class Advertiser(object):
 
                 for ident in stale:
                     del self._peers[ident]
-
-
-class AsyncAdvertiser(object):
-    def __init__(self, config, view):
-        self._config = config
-        self._view = view
-        self._public_key, self._identity = get_identity(config)
-        self._advertiser = None
-
-    def start_advertising(self):
-        _thread.start_new_thread(self._adv_worker, ())
-
-    def _adv_worker(self):
-        # this will take some time due to scapy being slow to be imported ...
-        from pwnagotchi.mesh.advertise import Advertiser
-
-        self._advertiser = Advertiser(
-            self._config['main']['iface'],
-            pwnagotchi.name(),
-            pwnagotchi.version,
-            self._identity,
-            period=0.3,
-            data=self._config['personality'])
-
-        self._advertiser.on_peer(self._on_new_unit, self._on_lost_unit)
-
-        if self._config['personality']['advertise']:
-            self._advertiser.start()
-            self._view.on_state_change('face', self._advertiser.on_face_change)
-        else:
-            core.log("advertising is disabled")
-
-    def _on_new_unit(self, peer):
-        self._view.on_new_peer(peer)
-
-    def _on_lost_unit(self, peer):
-        self._view.on_lost_peer(peer)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/utils.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/utils.py
new file mode 100644
index 0000000..8c84b94
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/mesh/utils.py
@@ -0,0 +1,44 @@
+import _thread
+
+import core
+import pwnagotchi, pwnagotchi.plugins as plugins
+from pwnagotchi.mesh import get_identity
+
+
+class AsyncAdvertiser(object):
+    def __init__(self, config, view):
+        self._config = config
+        self._view = view
+        self._public_key, self._identity = get_identity(config)
+        self._advertiser = None
+
+    def start_advertising(self):
+        _thread.start_new_thread(self._adv_worker, ())
+
+    def _adv_worker(self):
+        # this will take some time due to scapy being slow to be imported ...
+        from pwnagotchi.mesh.advertise import Advertiser
+
+        self._advertiser = Advertiser(
+            self._config['main']['iface'],
+            pwnagotchi.name(),
+            pwnagotchi.version,
+            self._identity,
+            period=0.3,
+            data=self._config['personality'])
+
+        self._advertiser.on_peer(self._on_new_unit, self._on_lost_unit)
+
+        if self._config['personality']['advertise']:
+            self._advertiser.start()
+            self._view.on_state_change('face', self._advertiser.on_face_change)
+        else:
+            core.log("advertising is disabled")
+
+    def _on_new_unit(self, peer):
+        self._view.on_new_peer(peer)
+        plugins.on('peer_detected', self, peer)
+
+    def _on_lost_unit(self, peer):
+        self._view.on_lost_peer(peer)
+        plugins.on('peer_lost', self, peer)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/__init__.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/__init__.py
new file mode 100644
index 0000000..6f84a90
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/__init__.py
@@ -0,0 +1,45 @@
+import os
+import glob
+import importlib, importlib.util
+
+# import core
+
+default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
+loaded = {}
+
+
+def dummy_callback():
+    pass
+
+
+def on(event_name, *args, **kwargs):
+    global loaded
+    cb_name = 'on_%s' % event_name
+    for _, plugin in loaded.items():
+        if cb_name in plugin.__dict__:
+            # print("calling %s %s(%s)" %(cb_name, args, kwargs))
+            plugin.__dict__[cb_name](*args, **kwargs)
+
+
+def load_from_file(filename):
+    plugin_name = os.path.basename(filename.replace(".py", ""))
+    spec = importlib.util.spec_from_file_location(plugin_name, filename)
+    instance = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(instance)
+    return plugin_name, instance
+
+
+def load_from_path(path):
+    global loaded
+
+    for filename in glob.glob(os.path.join(path, "*.py")):
+        name, plugin = load_from_file(filename)
+        if name in loaded:
+            raise Exception("plugin %s already loaded from %s" % (name, plugin.__file__))
+        elif not plugin.__enabled__:
+            # print("plugin %s is not enabled" % name)
+            pass
+        else:
+            loaded[name] = plugin
+
+    return loaded
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/example.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/example.py
new file mode 100644
index 0000000..4a63275
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/example.py
@@ -0,0 +1,162 @@
+__author__ = 'evilsocket@gmail.com'
+__version__ = '1.0.0'
+__name__ = 'hello_world'
+__license__ = 'GPL3'
+__description__ = 'An example plugin for pwnagotchi that implements all the available callbacks.'
+__enabled__ = False  # IMPORTANT: set this to True to enable your plugin.
+
+from pwnagotchi.ui.components import LabeledValue
+from pwnagotchi.ui.view import BLACK
+import pwnagotchi.ui.fonts as fonts
+import core
+
+
+# called when the plugin is loaded
+def on_loaded():
+    core.log("WARNING: plugin %s should be disabled!" % __name__)
+
+
+# called to setup the ui elements
+def on_ui_setup(ui):
+    # add custom UI elements
+    ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
+                                       label_font=fonts.Bold, text_font=fonts.Medium))
+
+
+# called when the ui is updated
+def on_ui_update(ui):
+    # update those elements
+    some_voltage = 0.1
+    some_capacity = 100.0
+
+    ui.set('ups', "%4.2fV/%2i%%" % (some_voltage, some_capacity))
+
+
+# called when the hardware display setup is done, display is an hardware specific object
+def on_display_setup(display):
+    pass
+
+
+# called when everything is ready and the main loop is about to start
+def on_ready(agent):
+    core.log("unit is ready")
+    # you can run custom bettercap commands if you want
+    #   agent.run('ble.recon on')
+    # or set a custom state
+    #   agent.set_bored()
+
+
+# called when the AI finished loading
+def on_ai_ready(agent):
+    pass
+
+
+# called when the AI finds a new set of parameters
+def on_ai_policy(agent, policy):
+    pass
+
+
+# called when the AI starts training for a given number of epochs
+def on_ai_training_start(agent, epochs):
+    pass
+
+
+# called after the AI completed a training epoch
+def on_ai_training_step(agent, _locals, _globals):
+    pass
+
+
+# called when the AI has done training
+def on_ai_training_end(agent):
+    pass
+
+
+# called when the AI got the best reward so far
+def on_ai_best_reward(agent, reward):
+    pass
+
+
+# called when the AI got the best reward so far
+def on_ai_worst_reward(agent, reward):
+    pass
+
+
+# called when a non overlapping wifi channel is found to be free
+def on_free_channel(agent, channel):
+    pass
+
+
+# called when the status is set to bored
+def on_bored(agent):
+    pass
+
+
+# called when the status is set to sad
+def on_sad(agent):
+    pass
+
+
+# called when the status is set to excited
+def on_excited(agent):
+    pass
+
+
+# called when the status is set to lonely
+def on_lonely(agent):
+    pass
+
+
+# called when the agent is rebooting the board
+def on_rebooting(agent):
+    pass
+
+
+# called when the agent is waiting for t seconds
+def on_wait(agent, t):
+    pass
+
+
+# called when the agent is sleeping for t seconds
+def on_sleep(agent, t):
+    pass
+
+
+# called when the agent refreshed its access points list
+def on_wifi_update(agent, access_points):
+    pass
+
+
+# called when the agent is sending an association frame
+def on_association(agent, access_point):
+    pass
+
+
+# callend when the agent is deauthenticating a client station from an AP
+def on_deauthentication(agent, access_point, client_station):
+    pass
+
+
+# callend when the agent is tuning on a specific channel
+def on_channel_hop(agent, channel):
+    pass
+
+
+# called when a new handshake is captured, access_point and client_station are json objects
+# if the agent could match the BSSIDs to the current list, otherwise they are just the strings of the BSSIDs
+def on_handshake(agent, filename, access_point, client_station):
+    pass
+
+
+# called when an epoch is over (where an epoch is a single loop of the main algorithm)
+def on_epoch(agent, epoch, epoch_data):
+    pass
+
+
+# called when a new peer is detected
+def on_peer_detected(agent, peer):
+    pass
+
+
+# called when a known peer is lost
+def on_peer_lost(agent, peer):
+    pass
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/gps.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/gps.py
new file mode 100644
index 0000000..c3c53a9
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/gps.py
@@ -0,0 +1,47 @@
+__author__ = 'evilsocket@gmail.com'
+__version__ = '1.0.0'
+__name__ = 'gps'
+__license__ = 'GPL3'
+__description__ = 'Save GPS coordinates whenever an handshake is captured.'
+__enabled__ = True  # set to false if you just don't use GPS
+
+import core
+import json
+import os
+
+device = '/dev/ttyUSB0'
+speed = 19200
+running = False
+
+
+def on_loaded():
+    core.log("GPS plugin loaded for %s" % device)
+
+
+def on_ready(agent):
+    global running
+
+    if os.path.exists(device):
+        core.log("enabling GPS bettercap's module for %s" % device)
+        try:
+            agent.run('gps off')
+        except:
+            pass
+
+        agent.run('set gps.device %s' % device)
+        agent.run('set gps.speed %d' % speed)
+        agent.run('gps on')
+        running = True
+    else:
+        core.log("no GPS detected")
+
+
+def on_handshake(agent, filename, access_point, client_station):
+    if running:
+        info = agent.session()
+        gps = info['gps']
+        gps_filename = filename.replace('.pcap', '.gps.json')
+
+        core.log("saving GPS to %s (%s)" % (gps_filename, gps))
+        with open(gps_filename, 'w+t') as fp:
+            json.dump(gps, fp)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/ups_lite.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/ups_lite.py
new file mode 100644
index 0000000..7ddcecd
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/plugins/default/ups_lite.py
@@ -0,0 +1,65 @@
+# Based on UPS Lite v1.1 from https://github.com/xenDE
+#
+# funtions for get UPS status - needs enable "i2c" in raspi-config
+#
+# https://github.com/linshuqin329/UPS-Lite
+#
+# For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4
+# https://www.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310
+# https://www.aliexpress.com/item/32888533624.html
+__author__ = 'evilsocket@gmail.com'
+__version__ = '1.0.0'
+__name__ = 'ups_lite'
+__license__ = 'GPL3'
+__description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1'
+__enabled__ = False
+
+import struct
+
+from pwnagotchi.ui.components import LabeledValue
+from pwnagotchi.ui.view import BLACK
+import pwnagotchi.ui.fonts as fonts
+
+
+# TODO: add enable switch in config.yml an cleanup all to the best place
+class UPS:
+    def __init__(self):
+        # only import when the module is loaded and enabled
+        import smbus
+        # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1)
+        self._bus = smbus.SMBus(1)
+
+    def voltage(self):
+        try:
+            address = 0x36
+            read = self._bus.read_word_data(address, 2)
+            swapped = struct.unpack("<H", struct.pack(">H", read))[0]
+            return swapped * 1.25 / 1000 / 16
+        except:
+            return 0.0
+
+    def capacity(self):
+        try:
+            address = 0x36
+            read = self._bus.read_word_data(address, 4)
+            swapped = struct.unpack("<H", struct.pack(">H", read))[0]
+            return swapped / 256
+        except:
+            return 0.0
+
+
+ups = None
+
+
+def on_loaded():
+    global ups
+    ups = UPS()
+
+
+def on_ui_setup(ui):
+    ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
+                                       label_font=fonts.Bold, text_font=fonts.Medium))
+
+
+def on_ui_update(ui):
+    ui.set('ups', "%4.2fV/%2i%%" % (ups.voltage(), ups.capacity()))
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
index 4f9bb4a..657aefb 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
@@ -1,10 +1,10 @@
 import _thread
 from threading import Lock
 
-import io
+import shutil
 import core
 import os
-import pwnagotchi
+import pwnagotchi, pwnagotchi.plugins as plugins
 
 from pwnagotchi.ui.view import WHITE, View
 
@@ -13,7 +13,6 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
 
 class VideoHandler(BaseHTTPRequestHandler):
     _lock = Lock()
-    _buffer = None
     _index = """<html>
   <head>
     <title>%s</title>
@@ -36,36 +35,31 @@ class VideoHandler(BaseHTTPRequestHandler):
     @staticmethod
     def render(img):
         with VideoHandler._lock:
-            writer = io.BytesIO()
-            img.save(writer, format='PNG')
-            VideoHandler._buffer = writer.getvalue()
+            img.save("/root/pwnagotchi.png", format='PNG')
 
     def log_message(self, format, *args):
         return
 
-    def _w(self, data):
-        try:
-            self.wfile.write(data)
-        except:
-            pass
-
     def do_GET(self):
-        if self._buffer is None:
-            self.send_response(404)
-
-        elif self.path == '/':
+        if self.path == '/':
             self.send_response(200)
             self.send_header('Content-type', 'text/html')
             self.end_headers()
-            self._w(bytes(self._index % (pwnagotchi.name(), 1000), "utf8"))
+            try:
+                self.wfile.write(bytes(self._index % (pwnagotchi.name(), 1000), "utf8"))
+            except:
+                pass
 
         elif self.path.startswith('/ui'):
             with self._lock:
                 self.send_response(200)
                 self.send_header('Content-type', 'image/png')
-                self.send_header('Content-length', '%d' % len(self._buffer))
                 self.end_headers()
-                self._w(self._buffer)
+                try:
+                    with open("/root/pwnagotchi.png", 'rb') as fp:
+                        shutil.copyfileobj(fp, self.wfile)
+                except:
+                    pass
         else:
             self.send_response(404)
 
@@ -80,8 +74,6 @@ class Display(View):
         self._video_address = config['ui']['display']['video']['address']
         self._display_type = config['ui']['display']['type']
         self._display_color = config['ui']['display']['color']
-        self.full_refresh_count = 0
-        self.full_refresh_trigger = config['ui']['display']['refresh'] 
 
         self._render_cb = None
         self._display = None
@@ -119,42 +111,44 @@ class Display(View):
 
     def _init_display(self):
         if self._is_inky():
+            core.log("initializing inky display")
             from inky import InkyPHAT
             self._display = InkyPHAT(self._display_color)
             self._display.set_border(InkyPHAT.BLACK)
             self._render_cb = self._inky_render
-            
+
         elif self._is_papirus():
-            from papirus import Papirus
+            core.log("initializing papirus display")
+            from pwnagotchi.ui.papirus.epd import EPD
             os.environ['EPD_SIZE'] = '2.0'
-            self._display = Papirus()
+            self._display = EPD()
             self._display.clear()
             self._render_cb = self._papirus_render
 
         elif self._is_waveshare1():
+            core.log("initializing waveshare v1 display")
             from pwnagotchi.ui.waveshare.v1.epd2in13 import EPD
-            # core.log("display module started")
             self._display = EPD()
             self._display.init(self._display.lut_full_update)
             self._display.Clear(0xFF)
             self._display.init(self._display.lut_partial_update)
             self._render_cb = self._waveshare_render
-            
+
         elif self._is_waveshare2():
+            core.log("initializing waveshare v2 display")
             from pwnagotchi.ui.waveshare.v2.waveshare import EPD
-            # core.log("display module started")
             self._display = EPD()
             self._display.init(self._display.FULL_UPDATE)
             self._display.Clear(WHITE)
             self._display.init(self._display.PART_UPDATE)
             self._render_cb = self._waveshare_render
-            
+
         else:
             core.log("unknown display type %s" % self._display_type)
 
-        self.on_render(self._on_view_rendered)
+        plugins.on('display_setup', self._display)
 
-        core.log("display type '%s' initialized (color:%s)" % (self._display_type, self._display_color))
+        self.on_render(self._on_view_rendered)
 
     def image(self):
         img = None
@@ -197,25 +191,16 @@ class Display(View):
 
     def _waveshare_render(self):
         buf = self._display.getbuffer(self.canvas)
-        if self._is_waveshare1:
-            if self.full_refresh_trigger >= 0 and self.full_refresh_count == self.full_refresh_trigger:
-                self._display.Clear(0x00)
+        if self._is_waveshare1():
             self._display.display(buf)
-        elif self._is_waveshare2:
-            if self.full_refresh_trigger >= 0 and self.full_refresh_count == self.full_refresh_trigger:
-                self._display.Clear(BLACK)
+        elif self._is_waveshare2():
             self._display.displayPartial(buf)
-        self._display.sleep()
-        if self.full_refresh_trigger >= 0 and self.full_refresh_count == self.full_refresh_trigger:
-           self.full_refresh_count = 0
-        elif self.full_refresh_trigger >= 0:
-           self.full_refresh_count += 1
 
     def _on_view_rendered(self, img):
         # core.log("display::_on_view_rendered")
         VideoHandler.render(img)
 
         if self._enabled:
-            self.canvas = img if self._rotation == 0 else img.rotate(self._rotation)
+            self.canvas = (img if self._rotation == 0 else img.rotate(self._rotation))
             if self._render_cb is not None:
                 self._render_cb()
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/__init__.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/epd.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/epd.py
new file mode 100644
index 0000000..923993b
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/epd.py
@@ -0,0 +1,213 @@
+#qCopyright 2013-2015 Pervasive Displays, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#   http:#www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.  See the License for the specific language
+# governing permissions and limitations under the License.
+
+
+from PIL import Image
+from PIL import ImageOps
+from pwnagotchi.ui.papirus.lm75b import LM75B
+import re
+import os
+import sys
+
+if sys.version_info < (3,):
+    def b(x):
+        return x
+else:
+    def b(x):
+        return x.encode('ISO-8859-1')
+
+class EPDError(Exception):
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return repr(self.value)
+
+
+class EPD(object):
+
+    """EPD E-Ink interface
+
+to use:
+  from EPD import EPD
+
+  epd = EPD([path='/path/to/epd'], [auto=boolean], [rotation = 0|90|180|270])
+
+  image = Image.new('1', epd.size, 0)
+  # draw on image
+  epd.clear()         # clear the panel
+  epd.display(image)  # tranfer image data
+  epd.update()        # refresh the panel image - not needed if auto=true
+"""
+
+
+    PANEL_RE = re.compile('^([A-Za-z]+)\s+(\d+\.\d+)\s+(\d+)x(\d+)\s+COG\s+(\d+)\s+FILM\s+(\d+)\s*$', flags=0)
+
+    def __init__(self, *args, **kwargs):
+        self._epd_path = '/dev/epd'
+        self._width = 200
+        self._height = 96
+        self._panel = 'EPD 2.0'
+        self._cog = 0
+        self._film = 0
+        self._auto = False
+        self._lm75b = LM75B()
+        self._rotation = 0
+        self._uselm75b = True
+
+        if len(args) > 0:
+            self._epd_path = args[0]
+        elif 'epd' in kwargs:
+            self._epd_path = kwargs['epd']
+
+        if ('auto' in kwargs) and kwargs['auto']:
+            self._auto = True
+        if ('rotation' in kwargs):
+            rot = kwargs['rotation']
+            if rot in (0, 90, 180, 270):
+                self._rotation = rot
+            else:
+                raise EPDError('rotation can only be 0, 90, 180 or 270')
+
+        with open(os.path.join(self._epd_path, 'version')) as f:
+            self._version = f.readline().rstrip('\n')
+
+        with open(os.path.join(self._epd_path, 'panel')) as f:
+            line = f.readline().rstrip('\n')
+            m = self.PANEL_RE.match(line)
+            if m is None:
+                raise EPDError('invalid panel string')
+            self._panel = m.group(1) + ' ' + m.group(2)
+            self._width = int(m.group(3))
+            self._height = int(m.group(4))
+            self._cog = int(m.group(5))
+            self._film = int(m.group(6))
+
+        if self._width < 1 or self._height < 1:
+            raise EPDError('invalid panel geometry')
+        if self._rotation in (90, 270):
+            self._width, self._height = self._height, self._width
+
+    @property
+    def size(self):
+        return (self._width, self._height)
+
+    @property
+    def width(self):
+        return self._width
+
+    @property
+    def height(self):
+        return self._height
+
+    @property
+    def panel(self):
+        return self._panel
+
+    @property
+    def version(self):
+        return self._version
+
+    @property
+    def cog(self):
+        return self._cog
+
+    @property
+    def film(self):
+        return self._film
+
+    @property
+    def auto(self):
+        return self._auto
+
+    @auto.setter
+    def auto(self, flag):
+        if flag:
+            self._auto = True
+        else:
+            self._auto = False
+
+    @property
+    def rotation(self):
+        return self._rotation
+
+    @rotation.setter
+    def rotation(self, rot):
+        if rot not in (0, 90, 180, 270):
+            raise EPDError('rotation can only be 0, 90, 180 or 270')
+        if abs(self._rotation - rot) == 90 or abs(self._rotation - rot) == 270:
+            self._width, self._height = self._height, self._width
+        self._rotation = rot
+
+    @property
+    def use_lm75b(self):
+        return self._uselm75b
+
+    @use_lm75b.setter
+    def use_lm75b(self, flag):
+        if flag:
+            self._uselm75b = True
+        else:
+            self._uselm75b = False
+
+    def error_status(self):
+        with open(os.path.join(self._epd_path, 'error'), 'r') as f:
+            return(f.readline().rstrip('\n'))
+
+    def rotation_angle(self, rotation):
+        angles = { 90 : Image.ROTATE_90, 180 : Image.ROTATE_180, 270 : Image.ROTATE_270 }
+        return angles[rotation]
+
+    def display(self, image):
+
+        # attempt grayscale conversion, and then to single bit
+        # better to do this before calling this if the image is to
+        # be dispayed several times
+        if image.mode != "1":
+            image = ImageOps.grayscale(image).convert("1", dither=Image.FLOYDSTEINBERG)
+
+        if image.mode != "1":
+            raise EPDError('only single bit images are supported')
+
+        if image.size != self.size:
+            raise EPDError('image size mismatch')
+
+        if self._rotation != 0:
+            image = image.transpose(self.rotation_angle(self._rotation))
+
+        with open(os.path.join(self._epd_path, 'LE', 'display_inverse'), 'r+b') as f:
+            f.write(image.tobytes())
+
+        if self.auto:
+            self.update()
+
+
+    def update(self):
+        self._command('U')
+
+    def partial_update(self):
+        self._command('P')
+
+    def fast_update(self):
+        self._command('F')
+
+    def clear(self):
+        self._command('C')
+
+    def _command(self, c):
+        if self._uselm75b:
+            with open(os.path.join(self._epd_path, 'temperature'), 'wb') as f:
+                f.write(b(repr(self._lm75b.getTempC())))
+        with open(os.path.join(self._epd_path, 'command'), 'wb') as f:
+            f.write(b(c))
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/lm75b.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/lm75b.py
new file mode 100644
index 0000000..f3087f2
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/papirus/lm75b.py
@@ -0,0 +1,46 @@
+# Minimal support for LM75b temperature sensor on the Papirus HAT / Papirus Zero
+# This module allows you to read the temperature.
+# The OS-output (Over-temperature Shutdown) connected to GPIO xx (pin 11) is not supported
+# by this module
+#
+
+from __future__ import (print_function, division)
+
+import smbus
+
+LM75B_ADDRESS             = 0x48
+
+LM75B_TEMP_REGISTER       = 0
+LM75B_CONF_REGISTER       = 1
+LM75B_THYST_REGISTER      = 2
+LM75B_TOS_REGISTER        = 3
+
+LM75B_CONF_NORMAL         = 0
+
+class LM75B(object):
+    def __init__(self, address=LM75B_ADDRESS, busnum=1):
+        self._address = address
+        self._bus = smbus.SMBus(busnum)
+        self._bus.write_byte_data(self._address, LM75B_CONF_REGISTER, LM75B_CONF_NORMAL)
+
+    def getTempCFloat(self):
+        """Return temperature in degrees Celsius as float"""
+        raw = self._bus.read_word_data(self._address, LM75B_TEMP_REGISTER) & 0xFFFF
+        raw = ((raw << 8) & 0xFF00) + (raw >> 8)
+        return (raw / 32.0) / 8.0
+
+    def getTempFFloat(self):
+        """Return temperature in degrees Fahrenheit as float"""
+        return (self.getTempCFloat() * (9.0 / 5.0)) + 32.0
+
+    def getTempC(self):
+        """Return temperature in degrees Celsius as integer, so it can be
+           used to write to /dev/epd/temperature"""
+        raw = self._bus.read_word_data(self._address, LM75B_TEMP_REGISTER) & 0xFFFF
+        raw = ((raw << 8) & 0xFF00) + (raw >> 8)
+        return (raw + 128) // 256 # round to nearest integer
+
+if __name__ == "__main__":
+    sens = LM75B()
+    print(sens.getTempC(), sens.getTempFFloat())
+
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/state.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/state.py
index 71b7d4e..c5b66be 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/state.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/state.py
@@ -7,6 +7,9 @@ class State(object):
         self._lock = Lock()
         self._listeners = {}
 
+    def add_element(self, key, elem):
+        self._state[key] = elem
+
     def add_listener(self, key, cb):
         with self._lock:
             self._listeners[key] = cb
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/view.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/view.py
index 19e7999..3ae9089 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/view.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/view.py
@@ -4,7 +4,7 @@ import time
 from PIL import Image, ImageDraw
 
 import core
-import pwnagotchi
+import pwnagotchi.plugins as plugins
 from pwnagotchi.voice import Voice
 
 import pwnagotchi.ui.fonts as fonts
@@ -41,7 +41,7 @@ def setup_display_specifics(config):
         name_pos = (int(width / 2) - 15, int(height * .15))
         status_pos = (int(width / 2) - 15, int(height * .30))
 
-    elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1', 
+    elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1',
                                              'ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
         fonts.setup(10, 9, 10, 35)
 
@@ -105,8 +105,19 @@ class View(object):
         for key, value in state.items():
             self._state.set(key, value)
 
+        plugins.on('ui_setup', self)
+
         _thread.start_new_thread(self._refresh_handler, ())
 
+    def add_element(self, key, elem):
+        self._state.add_element(key, elem)
+
+    def width(self):
+        return self._width
+
+    def height(self):
+        return self._height
+
     def on_state_change(self, key, cb):
         self._state.add_listener(key, cb)
 
@@ -294,6 +305,8 @@ class View(object):
             self._canvas = Image.new('1', (self._width, self._height), WHITE)
             drawer = ImageDraw.Draw(self._canvas)
 
+            plugins.on('ui_update', self)
+
             for key, lv in self._state.items():
                 lv.draw(self._canvas, drawer)
 
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epd2in13.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epd2in13.py
index 5805086..b6de9da 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epd2in13.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epd2in13.py
@@ -1,218 +1,225 @@
-# //*****************************************************************************
-# * | File        :	  epd2in13.py
-# * | Author      :   Waveshare team
-# * | Function    :   Electronic paper driver
-# * | Info        :
-# *----------------
-# * |	This version:   V3.1
-# * | Date        :   2019-03-20
-# * | Info        :   python3 demo
-# * fix: TurnOnDisplay()
-# ******************************************************************************//
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documnetation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and//or sell
-# copies of the Software, and to permit persons to  whom the Software is
-# furished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-
-
-from . import epdconfig
-from PIL import Image
-import RPi.GPIO as GPIO
-# import numpy as np
-
-# Display resolution
-EPD_WIDTH       = 122
-EPD_HEIGHT      = 250
-
-class EPD:
-    def __init__(self):
-        self.reset_pin = epdconfig.RST_PIN
-        self.dc_pin = epdconfig.DC_PIN
-        self.busy_pin = epdconfig.BUSY_PIN
-        self.width = EPD_WIDTH
-        self.height = EPD_HEIGHT
-    lut_full_update = [
-        0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11,
-        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-        0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,
-        0x01, 0x00, 0x00, 0x00, 0x00, 0x00
-    ]
-
-    lut_partial_update  = [
-        0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-        0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-        0x00, 0x00, 0x00, 0x00, 0x00, 0x00
-    ]
-        
-    # Hardware reset
-    def reset(self):
-        epdconfig.digital_write(self.reset_pin, GPIO.HIGH)
-        epdconfig.delay_ms(200) 
-        epdconfig.digital_write(self.reset_pin, GPIO.LOW)         # module reset
-        epdconfig.delay_ms(200)
-        epdconfig.digital_write(self.reset_pin, GPIO.HIGH)
-        epdconfig.delay_ms(200)   
-
-    def send_command(self, command):
-        epdconfig.digital_write(self.dc_pin, GPIO.LOW)
-        epdconfig.spi_writebyte([command])
-
-    def send_data(self, data):
-        epdconfig.digital_write(self.dc_pin, GPIO.HIGH)
-        epdconfig.spi_writebyte([data])
-        
-    def wait_until_idle(self):
-        # print("busy")
-        while(epdconfig.digital_read(self.busy_pin) == 1):      # 0: idle, 1: busy
-            epdconfig.delay_ms(100)
-        # print("free busy")
-
-    def TurnOnDisplay(self):
-        self.send_command(0x22) # DISPLAY_UPDATE_CONTROL_2
-        self.send_data(0xC4)
-        self.send_command(0x20) # MASTER_ACTIVATION
-        self.send_command(0xFF) # TERMINATE_FRAME_READ_WRITE
-        self.wait_until_idle()
-
-    def init(self, lut):
-        if (epdconfig.module_init() != 0):
-            return -1
-        # EPD hardware init start
-        self.reset()
-        self.send_command(0x01) # DRIVER_OUTPUT_CONTROL
-        self.send_data((EPD_HEIGHT - 1) & 0xFF)
-        self.send_data(((EPD_HEIGHT - 1) >> 8) & 0xFF)
-        self.send_data(0x00) # GD = 0 SM = 0 TB = 0
-        
-        self.send_command(0x0C) # BOOSTER_SOFT_START_CONTROL
-        self.send_data(0xD7)
-        self.send_data(0xD6)
-        self.send_data(0x9D)
-        
-        self.send_command(0x2C) # WRITE_VCOM_REGISTER
-        self.send_data(0xA8) # VCOM 7C
-        
-        self.send_command(0x3A) # SET_DUMMY_LINE_PERIOD
-        self.send_data(0x1A) # 4 dummy lines per gate
-        
-        self.send_command(0x3B) # SET_GATE_TIME
-        self.send_data(0x08) # 2us per line
-        
-        self.send_command(0X3C) # BORDER_WAVEFORM_CONTROL
-        self.send_data(0x03)      
-        
-        self.send_command(0X11) # DATA_ENTRY_MODE_SETTING
-        self.send_data(0x03) # X increment; Y increment
-        
-        # WRITE_LUT_REGISTER
-        self.send_command(0x32)
-        for count in range(30):
-            self.send_data(lut[count])
-
-        return 0
-        
-##
- #  @brief: specify the memory area for data R//W
- ##
-    def SetWindows(self, x_start, y_start, x_end, y_end):
-        self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
-        self.send_data((x_start >> 3) & 0xFF)
-        self.send_data((x_end >> 3) & 0xFF)
-        self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
-        self.send_data(y_start & 0xFF)
-        self.send_data((y_start >> 8) & 0xFF)
-        self.send_data(y_end & 0xFF)
-        self.send_data((y_end >> 8) & 0xFF)
-
-##
- #  @brief: specify the start point for data R//W
- ##
-    def SetCursor(self, x, y):
-        self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
-        # x point must be the multiple of 8 or the last 3 bits will be ignored
-        self.send_data((x >> 3) & 0xFF)
-        self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
-        self.send_data(y & 0xFF)
-        self.send_data((y >> 8) & 0xFF)
-        self.wait_until_idle()
-        
-    def getbuffer(self, image):
-        if self.width%8 == 0:
-            linewidth = self.width//8
-        else:
-            linewidth = self.width//8 + 1
-         
-        buf = [0xFF] * (linewidth * self.height)
-        image_monocolor = image.convert('1')
-        imwidth, imheight = image_monocolor.size
-        pixels = image_monocolor.load()
-        
-        if(imwidth == self.width and imheight == self.height):
-            # print("Vertical")
-            for y in range(imheight):
-                for x in range(imwidth):                    
-                    if pixels[x, y] == 0:
-                        # x = imwidth - x
-                        buf[x // 8 + y * linewidth] &= ~(0x80 >> (x % 8))
-        elif(imwidth == self.height and imheight == self.width):
-            # print("Horizontal")
-            for y in range(imheight):
-                for x in range(imwidth):
-                    newx = y
-                    newy = self.height - x - 1
-                    if pixels[x, y] == 0:
-                        # newy = imwidth - newy - 1
-                        buf[newx // 8 + newy*linewidth] &= ~(0x80 >> (y % 8))
-        return buf   
-
-        
-    def display(self, image):
-        if self.width%8 == 0:
-            linewidth = self.width//8
-        else:
-            linewidth = self.width//8 + 1
-
-        self.SetWindows(0, 0, EPD_WIDTH, EPD_HEIGHT);
-        for j in range(0, self.height):
-            self.SetCursor(0, j);
-            self.send_command(0x24);
-            for i in range(0, linewidth):
-                self.send_data(image[i + j * linewidth])   
-        self.TurnOnDisplay()
-    
-    def Clear(self, color):
-        if self.width%8 == 0:
-            linewidth = self.width//8
-        else:
-            linewidth = self.width//8 + 1
-
-        self.SetWindows(0, 0, EPD_WIDTH, EPD_HEIGHT);
-        for j in range(0, self.height):
-            self.SetCursor(0, j);
-            self.send_command(0x24);
-            for i in range(0, linewidth):
-                self.send_data(color)   
-        self.TurnOnDisplay()
-
-    def sleep(self):
-        self.send_command(0x10) #enter deep sleep
-        # self.send_data(0x01)
-        epdconfig.delay_ms(100)    
-
-### END OF FILE ###
-
+# *****************************************************************************
+# * | File        :	  epd2in13.py
+# * | Author      :   Waveshare team
+# * | Function    :   Electronic paper driver
+# * | Info        :
+# *----------------
+# * | This version:   V4.0
+# * | Date        :   2019-06-20
+# # | Info        :   python demo
+# -----------------------------------------------------------------------------
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documnetation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to  whom the Software is
+# furished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+
+import logging
+from . import epdconfig
+import numpy as np
+
+# Display resolution
+EPD_WIDTH       = 122
+EPD_HEIGHT      = 250
+
+class EPD:
+    def __init__(self):
+        self.reset_pin = epdconfig.RST_PIN
+        self.dc_pin = epdconfig.DC_PIN
+        self.busy_pin = epdconfig.BUSY_PIN
+        self.cs_pin = epdconfig.CS_PIN
+        self.width = EPD_WIDTH
+        self.height = EPD_HEIGHT
+
+    lut_full_update = [
+        0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,
+        0x01, 0x00, 0x00, 0x00, 0x00, 0x00
+    ]
+
+    lut_partial_update  = [
+        0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+    ]
+        
+    # Hardware reset
+    def reset(self):
+        epdconfig.digital_write(self.cs_pin, 0)
+        epdconfig.digital_write(self.reset_pin, 1)
+        epdconfig.delay_ms(200) 
+        epdconfig.digital_write(self.reset_pin, 0)
+        epdconfig.delay_ms(10)
+        epdconfig.digital_write(self.reset_pin, 1)
+        epdconfig.delay_ms(200)   
+        epdconfig.digital_write(self.cs_pin, 1)
+
+    def send_command(self, command):
+        epdconfig.digital_write(self.cs_pin, 0)
+        epdconfig.digital_write(self.dc_pin, 0)
+        epdconfig.spi_writebyte([command])
+        epdconfig.digital_write(self.cs_pin, 1)
+
+    def send_data(self, data):
+        epdconfig.digital_write(self.cs_pin, 0)
+        epdconfig.digital_write(self.dc_pin, 1)
+        epdconfig.spi_writebyte([data])
+        epdconfig.digital_write(self.cs_pin, 1)
+        
+    def ReadBusy(self):
+        while(epdconfig.digital_read(self.busy_pin) == 1):      # 0: idle, 1: busy
+            epdconfig.delay_ms(100)            
+
+    def TurnOnDisplay(self):
+        self.send_command(0x22) # DISPLAY_UPDATE_CONTROL_2
+        self.send_data(0xC4)
+        self.send_command(0x20) # MASTER_ACTIVATION
+        self.send_command(0xFF) # TERMINATE_FRAME_READ_WRITE
+
+        logging.debug("e-Paper busy")
+        self.ReadBusy()
+        logging.debug("e-Paper busy release")
+
+    def init(self, lut):
+        if (epdconfig.module_init() != 0):
+            return -1
+        # EPD hardware init start
+        self.reset()
+        self.send_command(0x01) # DRIVER_OUTPUT_CONTROL
+        self.send_data((EPD_HEIGHT - 1) & 0xFF)
+        self.send_data(((EPD_HEIGHT - 1) >> 8) & 0xFF)
+        self.send_data(0x00) # GD = 0 SM = 0 TB = 0
+        
+        self.send_command(0x0C) # BOOSTER_SOFT_START_CONTROL
+        self.send_data(0xD7)
+        self.send_data(0xD6)
+        self.send_data(0x9D)
+        
+        self.send_command(0x2C) # WRITE_VCOM_REGISTER
+        self.send_data(0xA8) # VCOM 7C
+        
+        self.send_command(0x3A) # SET_DUMMY_LINE_PERIOD
+        self.send_data(0x1A) # 4 dummy lines per gate
+        
+        self.send_command(0x3B) # SET_GATE_TIME
+        self.send_data(0x08) # 2us per line
+        
+        self.send_command(0X3C) # BORDER_WAVEFORM_CONTROL
+        self.send_data(0x03)      
+        
+        self.send_command(0X11) # DATA_ENTRY_MODE_SETTING
+        self.send_data(0x03) # X increment; Y increment
+        
+        # WRITE_LUT_REGISTER
+        self.send_command(0x32)
+        for count in range(30):
+            self.send_data(lut[count])
+
+        return 0
+        
+##
+ #  @brief: specify the memory area for data R/W
+ ##
+    def SetWindows(self, x_start, y_start, x_end, y_end):
+        self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
+        self.send_data((x_start >> 3) & 0xFF)
+        self.send_data((x_end >> 3) & 0xFF)
+        self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
+        self.send_data(y_start & 0xFF)
+        self.send_data((y_start >> 8) & 0xFF)
+        self.send_data(y_end & 0xFF)
+        self.send_data((y_end >> 8) & 0xFF)
+
+##
+ #  @brief: specify the start point for data R/W
+ ##
+    def SetCursor(self, x, y):
+        self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
+        # x point must be the multiple of 8 or the last 3 bits will be ignored
+        self.send_data((x >> 3) & 0xFF)
+        self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
+        self.send_data(y & 0xFF)
+        self.send_data((y >> 8) & 0xFF)
+        self.ReadBusy()
+        
+    def getbuffer(self, image):
+        if self.width%8 == 0:
+            linewidth = int(self.width/8)
+        else:
+            linewidth = int(self.width/8) + 1
+         
+        buf = [0xFF] * (linewidth * self.height)
+        image_monocolor = image.convert('1')
+        imwidth, imheight = image_monocolor.size
+        pixels = image_monocolor.load()
+        
+        if(imwidth == self.width and imheight == self.height):
+            for y in range(imheight):
+                for x in range(imwidth):                    
+                    if pixels[x, y] == 0:
+                        # x = imwidth - x
+                        buf[int(x / 8) + y * linewidth] &= ~(0x80 >> (x % 8))
+        elif(imwidth == self.height and imheight == self.width):
+            for y in range(imheight):
+                for x in range(imwidth):
+                    newx = y
+                    newy = self.height - x - 1
+                    if pixels[x, y] == 0:
+                        # newy = imwidth - newy - 1
+                        buf[int(newx / 8) + newy*linewidth] &= ~(0x80 >> (y % 8))
+        return buf   
+
+        
+    def display(self, image):
+        if self.width%8 == 0:
+            linewidth = int(self.width/8)
+        else:
+            linewidth = int(self.width/8) + 1
+
+        self.SetWindows(0, 0, self.width, self.height);
+        for j in range(0, self.height):
+            self.SetCursor(0, j);
+            self.send_command(0x24);
+            for i in range(0, linewidth):
+                self.send_data(image[i + j * linewidth])   
+        self.TurnOnDisplay()
+    
+    def Clear(self, color):
+        if self.width%8 == 0:
+            linewidth = int(self.width/8)
+        else:
+            linewidth = int(self.width/8) + 1
+
+        self.SetWindows(0, 0, self.width, self.height);
+        for j in range(0, self.height):
+            self.SetCursor(0, j);
+            self.send_command(0x24);
+            for i in range(0, linewidth):
+                self.send_data(color)   
+        self.TurnOnDisplay()
+
+    def sleep(self):
+        self.send_command(0x10) #enter deep sleep
+        self.send_data(0x01)
+        epdconfig.delay_ms(100)
+         
+        epdconfig.module_exit()
+        
+### END OF FILE ###
+
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epdconfig.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epdconfig.py
index 78ff647..76d8ca9 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epdconfig.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v1/epdconfig.py
@@ -1,19 +1,13 @@
 # /*****************************************************************************
-# * | File        :	  EPD_1in54.py
+# * | File        :	  epdconfig.py
 # * | Author      :   Waveshare team
 # * | Function    :   Hardware underlying interface
 # * | Info        :
 # *----------------
-# * |	This version:   V2.0
-# * | Date        :   2018-11-01
+# * | This version:   V1.0
+# * | Date        :   2019-06-21
 # * | Info        :   
-# * 1.Remove:
-#   digital_write(self, pin, value)
-#   digital_read(self, pin)
-#   delay_ms(self, delaytime)
-#   set_lut(self, lut)
-#   self.lut = self.lut_full_update
-# ******************************************************************************/
+# ******************************************************************************
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documnetation files (the "Software"), to deal
 # in the Software without restriction, including without limitation the rights
@@ -33,41 +27,128 @@
 # THE SOFTWARE.
 #
 
-
-import spidev
-import RPi.GPIO as GPIO
+import os
+import logging
+import sys
 import time
 
-# Pin definition
-RST_PIN         = 17
-DC_PIN          = 25
-CS_PIN          = 8
-BUSY_PIN        = 24
 
-# SPI device, bus = 0, device = 0
-SPI = spidev.SpiDev(0, 0)
+class RaspberryPi:
+    # Pin definition
+    RST_PIN         = 17
+    DC_PIN          = 25
+    CS_PIN          = 8
+    BUSY_PIN        = 24
 
-def digital_write(pin, value):
-    GPIO.output(pin, value)
+    def __init__(self):
+        import spidev
+        import RPi.GPIO
 
-def digital_read(pin):
-    return GPIO.input(BUSY_PIN)
+        self.GPIO = RPi.GPIO
 
-def delay_ms(delaytime):
-    time.sleep(delaytime / 1000.0)
+        # SPI device, bus = 0, device = 0
+        self.SPI = spidev.SpiDev(0, 0)
 
-def spi_writebyte(data):
-    SPI.writebytes(data)
+    def digital_write(self, pin, value):
+        self.GPIO.output(pin, value)
+
+    def digital_read(self, pin):
+        return self.GPIO.input(self.BUSY_PIN)
+
+    def delay_ms(self, delaytime):
+        time.sleep(delaytime / 1000.0)
+
+    def spi_writebyte(self, data):
+        self.SPI.writebytes(data)
+
+    def module_init(self):
+        self.GPIO.setmode(self.GPIO.BCM)
+        self.GPIO.setwarnings(False)
+        self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
+        self.SPI.max_speed_hz = 4000000
+        self.SPI.mode = 0b00
+        return 0
+
+    def module_exit(self):
+        logging.debug("spi end")
+        self.SPI.close()
+
+        logging.debug("close 5V, Module enters 0 power consumption ...")
+        self.GPIO.output(self.RST_PIN, 0)
+        self.GPIO.output(self.DC_PIN, 0)
+
+        self.GPIO.cleanup()
+
+
+class JetsonNano:
+    # Pin definition
+    RST_PIN         = 17
+    DC_PIN          = 25
+    CS_PIN          = 8
+    BUSY_PIN        = 24
+
+    def __init__(self):
+        import ctypes
+        find_dirs = [
+            os.path.dirname(os.path.realpath(__file__)),
+            '/usr/local/lib',
+            '/usr/lib',
+        ]
+        self.SPI = None
+        for find_dir in find_dirs:
+            so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
+            if os.path.exists(so_filename):
+                self.SPI = ctypes.cdll.LoadLibrary(so_filename)
+                break
+        if self.SPI is None:
+            raise RuntimeError('Cannot find sysfs_software_spi.so')
+
+        import Jetson.GPIO
+        self.GPIO = Jetson.GPIO
+
+    def digital_write(self, pin, value):
+        self.GPIO.output(pin, value)
+
+    def digital_read(self, pin):
+        return self.GPIO.input(self.BUSY_PIN)
+
+    def delay_ms(self, delaytime):
+        time.sleep(delaytime / 1000.0)
+
+    def spi_writebyte(self, data):
+        self.SPI.SYSFS_software_spi_transfer(data[0])
+
+    def module_init(self):
+        self.GPIO.setmode(self.GPIO.BCM)
+        self.GPIO.setwarnings(False)
+        self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
+        self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
+        self.SPI.SYSFS_software_spi_begin()
+        return 0
+
+    def module_exit(self):
+        logging.debug("spi end")
+        self.SPI.SYSFS_software_spi_end()
+
+        logging.debug("close 5V, Module enters 0 power consumption ...")
+        self.GPIO.output(self.RST_PIN, 0)
+        self.GPIO.output(self.DC_PIN, 0)
+
+        self.GPIO.cleanup()
+
+
+if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
+    implementation = RaspberryPi()
+else:
+    implementation = JetsonNano()
+
+for func in [x for x in dir(implementation) if not x.startswith('_')]:
+    setattr(sys.modules[__name__], func, getattr(implementation, func))
 
-def module_init():
-    GPIO.setmode(GPIO.BCM)
-    GPIO.setwarnings(False)
-    GPIO.setup(RST_PIN, GPIO.OUT)
-    GPIO.setup(DC_PIN, GPIO.OUT)
-    GPIO.setup(CS_PIN, GPIO.OUT)
-    GPIO.setup(BUSY_PIN, GPIO.IN)
-    SPI.max_speed_hz = 2000000
-    SPI.mode = 0b00
-    return 0;
 
 ### END OF FILE ###
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v2/waveshare.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v2/waveshare.py
index 3286a3f..6c3ed04 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v2/waveshare.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/waveshare/v2/waveshare.py
@@ -335,4 +335,4 @@ class EPD:
         self.send_data(0x01)
         delay_ms(100)
 
-    ### END OF FILE ###
+    ### END OF FILE ###
\ No newline at end of file
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py
index 3ff9e0e..53460ed 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py
@@ -17,44 +17,36 @@ class Voice:
     def default(self):
         return self._('ZzzzZZzzzzZzzz')
 
-
     def on_starting(self):
         return random.choice([ \
             self._('Hi, I\'m Pwnagotchi!\nStarting ...'),
             self._('New day, new hunt,\nnew pwns!'),
             self._('Hack the Planet!')])
 
-
     def on_ai_ready(self):
         return random.choice([
             self._('AI ready.'),
             self._('The neural network\nis ready.')])
 
-
     def on_normal(self):
         return random.choice([ \
             '',
             '...'])
 
-
-    def on_free_channel(channel):
+    def on_free_channel(self, channel):
         return self._('Hey, channel {channel} is\nfree! Your AP will\nsay thanks.').format(channel=channel)
 
-
-    def on_bored():
+    def on_bored(self):
         return random.choice([ \
             self._('I\'m bored ...'),
             self._('Let\'s go for a walk!')])
 
-
     def on_motivated(self, reward):
         return self._('This is the best\nday of my life!')
 
-
     def on_demotivated(self, reward):
         return self._('Shitty day :/')
 
-
     def on_sad(self):
         return random.choice([ \
             self._('I\'m extremely bored ...'),
@@ -62,7 +54,6 @@ class Voice:
             self._('I\'m sad'),
             '...'])
 
-
     def on_excited(self):
         return random.choice([ \
             self._('I\'m living the life!'),
@@ -71,52 +62,44 @@ class Voice:
             self._('I\'m having so much\nfun!'),
             self._('My crime is that of\ncuriosity ...')])
 
-
     def on_new_peer(self, peer):
         return random.choice([ \
             self._('Hello\n{name}!\nNice to meet you. {name}').format(name=peer.name()),
             self._('Unit\n{name}\nis nearby! {name}').format(name=peer.name())])
 
-
     def on_lost_peer(self, peer):
         return random.choice([ \
             self._('Uhm ...\ngoodbye\n{name}').format(name=peer.name()),
             self._('{name}\nis gone ...').format(name=peer.name())])
 
-
     def on_miss(self, who):
         return random.choice([ \
             self._('Whoops ...\n{name}\nis gone.').format(name=who),
             self._('{name}\nmissed!').format(name=who),
             self._('Missed!')])
 
-
     def on_lonely(self):
         return random.choice([ \
             self._('Nobody wants to\nplay with me ...'),
             self._('I feel so alone ...'),
             self._('Where\'s everybody?!')])
 
-
-    def on_napping(self,secs):
+    def on_napping(self, secs):
         return random.choice([ \
             self._('Napping for {secs}s ...').format(secs=secs),
             self._('Zzzzz'),
             self._('ZzzZzzz ({secs}s)').format(secs=secs)])
 
-
     def on_awakening(self):
         return random.choice(['...', '!'])
 
-
-    def on_waiting(self,secs):
+    def on_waiting(self, secs):
         return random.choice([ \
             self._('Waiting for {secs}s ...').format(secs=secs),
             '...',
             self._('Looking around ({secs}s)').format(secs=secs)])
 
-
-    def on_assoc(self,ap):
+    def on_assoc(self, ap):
         ssid, bssid = ap['hostname'], ap['mac']
         what = ssid if ssid != '' and ssid != '<hidden>' else bssid
         return random.choice([ \
@@ -124,24 +107,20 @@ class Voice:
             self._('Associating to\n{what}').format(what=what),
             self._('Yo\n{what}!').format(what=what)])
 
-
-    def on_deauth(self,sta):
+    def on_deauth(self, sta):
         return random.choice([ \
             self._('Just decided that\n{mac}\nneeds no WiFi!').format(mac=sta['mac']),
             self._('Deauthenticating\n{mac}').format(mac=sta['mac']),
             self._('Kickbanning\n{mac}!').format(mac=sta['mac'])])
 
-
-    def on_handshakes(self,new_shakes):
+    def on_handshakes(self, new_shakes):
         s = 's' if new_shakes > 1 else ''
         return self._('Cool, we got {num}\nnew handshake{plural}!').format(num=new_shakes, plural=s)
 
-
     def on_rebooting(self):
         return self._("Ops, something\nwent wrong ...\nRebooting ...")
 
-
-    def on_log(self,log):
+    def on_log(self, log):
         status = self._('Kicked {num} stations\n').format(num=log.deauthed)
         status += self._('Made {num} new friends\n').format(num=log.associated)
         status += self._('Got {num} handshakes\n').format(num=log.handshakes)
@@ -151,10 +130,10 @@ class Voice:
             status += self._('Met {num} peers').format(num=log.peers)
         return status
 
-
-    def on_log_tweet(self,log):
-        return self._('I\'ve been pwning for {duration} and kicked {deauthed} clients! I\'ve also met {associated} new friends and ate {handshakes} handshakes! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet').format(
-                duration=log.duration_human,
-                deauthed=log.deauthed,
-                associated=log.associated,
-                handshakes=log.handshakes)
+    def on_log_tweet(self, log):
+        return self._(
+            'I\'ve been pwning for {duration} and kicked {deauthed} clients! I\'ve also met {associated} new friends and ate {handshakes} handshakes! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet').format(
+            duration=log.duration_human,
+            deauthed=log.deauthed,
+            associated=log.associated,
+            handshakes=log.handshakes)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/requirements.txt b/sdcard/rootfs/root/pwnagotchi/scripts/requirements.txt
index 9b0d40d..0a82ef7 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/requirements.txt
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/requirements.txt
@@ -9,3 +9,4 @@ tweepy
 file_read_backwards
 numpy
 inky
+smbus