diff --git a/.DEREK.yml b/.DEREK.yml
index 9598f24..c63d9d1 100644
--- a/.DEREK.yml
+++ b/.DEREK.yml
@@ -3,7 +3,6 @@ maintainers:
  - caquino
  - dadav
  - justin-p
- - hexwaxwing
  
 features:
  - comments
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..084515c
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHubSponsors-enabled usernames e.g., [user1, user2]
+patreon: evilsocket
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.gitignore b/.gitignore
index fc54ebf..0cac4ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 *.img.bmap
 *.pcap
 *.po~
+preview.png
 __pycache__
 _backups
 _emulation
diff --git a/.travis.yml b/.travis.yml
index 0826bfc..ba20615 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,30 +12,25 @@ deploy:
     secure: vBUokTv94n8s65STUgTiD6I0Iy8KXbBRvQUrAof8XG+U4ZMsH5PmDTpS+wz+SaxI6o0PRkfyOiPVdARhiKAFnfatG3q9EHllMQwqRR2YIju51A3aCxgEJ5uWDoybwQdipERUMMYwUO/8XZaRRpwFD2bdQBFWkBtQyMcAkrEL8BXckwQQ531oDN2hK5gAiTllqsOswV2idwUlBRU9jOtStzff+UgUYsp/ZebsRodyOYkEB2Ev15yARo2HTXbyZ2icwHPtMbx5zmNUSRtxs9a4hfzaK3m6ctK8qLYYUdQvXub/ruuACapdw4Ez88LY1agTecbZhFYmJzv8oANH1e4VUI4owuHnZCpU6LRutS4wOhglrkOrGo6lSUlJeA+RtQjyjBugjej9DDtDyyIlRU1ZaBF3qWR9N5EXKuquf0olOfmUR67ap1NykE9VUpzkYjkoVRTiPs/e2onM/nRNOvAQcIt75FD13u+Y/DcYQ8r7KpMIu1HNdtbVx8gMeq76bRhP1YdDg2jm+DdJ21KWjf5QHsbyoXDfJzdKlCloLIlAU3EPJhMoXsnNzre0/FXeUl6dfteR1axNS6U7e/vKsQ9rlUFZWIQaeVPjfXmFKblNNVQ5uFrrsB/EGHcJl7IUx5fvcRT5hMMNwC660YxVkBXDbRb5fxMW5/+K0BOi9cP6en8=
   skip_cleanup: true
   file_glob: true
-  file: pwnagotchi-*.zip
+  file:
+    - pwnagotchi-*.zip
+    - pwnagotchi-*.sha256
   on:
     tags: true
     repo: evilsocket/pwnagotchi
 branches:
   only:
-    - "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]+?$/"
+    - "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*$/"
 cache:
   apt: true
 before_script:
-  - wget https://download.qemu.org/qemu-4.1.0.tar.xz
-  - tar xvJf qemu-4.1.0.tar.xz
-  - cd qemu-4.1.0
-  - "./configure --target-list=arm-softmmu"
-  - make -j$(nproc)
-  - sudo make install
-  - cd $TRAVIS_BUILD_DIR
   - sudo apt-get -y update || true
-  - sudo apt-get -y install qemu-user-static binfmt-support bmap-tools kpartx
+  - sudo apt-get -y install qemu-system-arm qemu-user-static binfmt-support bmap-tools kpartx
   - sudo update-binfmts --display
 script:
   - sudo make clean
   - sudo -E env "PATH=$PATH" make install
-  - sudo make image -e PWN_HOSTNAME=pwnagotchi VERSION=$TRAVIS_TAG
+  - sudo make image -e PWN_HOSTNAME=pwnagotchi PWN_VERSION=$TRAVIS_TAG
 notifications:
   slack:
     secure: aovN87lswg+TTLobxJpevC0p2F4omTAlsOzeKqLysRW55o5rRhRC1SgwRkWUl19yr49nsyffwmv/b7OcyQiWIVnz1bxxE9XOKP8zgRMA/bKKcyAcPktPqHXsALIQDseXyl0kz7fwdkRWg0UC2HpKqi5koAhmBYTX/fbzieyeHCbcQ7lbFfVFIepE1401y9m1IqUHcHuGfFhMvTaSDIpXrDXnWdA8+gDAl0HKJv41MIsgmffbh/QhD2jLBWzItjxFC3llmNfy88pnzCk0+HBMY/4272LXb0czX7et5HJeM74oxPqkb3aKXFxZgNaDl7cYdV+kzj9dfKUk47hAqwbxlirit5WvHI1Br1VyA90+PFvcC/p41J8gCv0IlcB5vjWN8NKWA1J+Y1F+KvrujMvGtgd0foHZvaSutuRODhI1cBh5rYAiLCroRSlvKMw3IJRyCRstYgUlMIJ3cI2Ova/kU44KtDVmjT9VE/pPkhkHBPvcYThL6skZTdl19E/RlormLu3XObG1aHLZ+Znxe/aL7tWHi0KMOlpy+TMDdps4go7URnJ8yitHtIvU/zMtBrztIwN0Oy2JLKXrS5qIijmRAkBLxe0NxuG01DYFzEO3KtnRirP4uSe3QcrjyP4sqPrVhrjl3TR6gwg8V1juvDXB4e2h8yCpaUW5AdSBOlx9riY=
diff --git a/Makefile b/Makefile
index 0d898dd..117e222 100644
--- a/Makefile
+++ b/Makefile
@@ -1,22 +1,23 @@
 PWN_HOSTNAME=pwnagotchi
 PWN_VERSION=master
 
-all: install image clean
+all: clean install image
 
 install:
 	curl https://releases.hashicorp.com/packer/1.3.5/packer_1.3.5_linux_amd64.zip -o /tmp/packer.zip
 	unzip /tmp/packer.zip -d /tmp
-	mv /tmp/packer /usr/bin/packer
+	sudo mv /tmp/packer /usr/bin/packer
 	git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image
 	cd /tmp/packer-builder-arm-image && go get -d ./... && go build
-	cp /tmp/packer-builder-arm-image/packer-builder-arm-image /usr/bin
+	sudo cp /tmp/packer-builder-arm-image/packer-builder-arm-image /usr/bin
 
 image:
-	cd builder && sudo /usr/bin/packer build pwnagotchi.json
-	mv builder/output-pwnagotchi/image pwnagotchi-raspbian-lite-$(PWN_VERSION).img
-	zip pwnagotchi-raspbian-lite-$(PWN_VERSION).zip pwnagotchi-raspbian-lite-$(PWN_VERSION).img
+	cd builder && sudo /usr/bin/packer build -var "pwn_hostname=$(PWN_HOSTNAME)" -var "pwn_version=$(PWN_VERSION)" pwnagotchi.json
+	sudo mv builder/output-pwnagotchi/image pwnagotchi-raspbian-lite-$(PWN_VERSION).img
+	sudo sha256sum pwnagotchi-raspbian-lite-$(PWN_VERSION).img > pwnagotchi-raspbian-lite-$(PWN_VERSION).sha256
+	sudo zip pwnagotchi-raspbian-lite-$(PWN_VERSION).zip pwnagotchi-raspbian-lite-$(PWN_VERSION).sha256 pwnagotchi-raspbian-lite-$(PWN_VERSION).img
 
 clean:
 	rm -rf /tmp/packer-builder-arm-image
-	rm -f pwnagotchi-raspbian-lite.img
+	rm -f pwnagotchi-raspbian-lite-*.zip pwnagotchi-raspbian-lite-*.img pwnagotchi-raspbian-lite-*.sha256
 	rm -rf builder/output-pwnagotchi  builder/packer_cache
diff --git a/README.md b/README.md
index b44c5bf..c03021e 100644
--- a/README.md
+++ b/README.md
@@ -4,45 +4,29 @@
   <p align="center">
     <a href="https://github.com/evilsocket/pwnagotchi/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/evilsocket/pwnagotchi.svg?style=flat-square"></a>
     <a href="https://github.com/evilsocket/pwnagotchi/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
-    <a href=""><img alt="Contributors" src="https://img.shields.io/github/contributors/evilsocket/pwnagotchi"/></a>
+    <a href="https://github.com/evilsocket/pwnagotchi/graphs/contributors"><img alt="Contributors" src="https://img.shields.io/github/contributors/evilsocket/pwnagotchi"/></a>
     <a href="https://travis-ci.org/evilsocket/pwnagotchi"><img alt="Travis" src="https://img.shields.io/travis/evilsocket/pwnagotchi/master.svg?style=flat-square"></a>
     <a href="https://pwnagotchi.herokuapp.com/"><img alt="Slack" src="https://pwnagotchi.herokuapp.com/badge.svg"></a>
+    <a href="https://twitter.com/intent/follow?screen_name=pwnagotchi"><img src="https://img.shields.io/twitter/follow/pwnagotchi?style=social&logo=twitter" alt="follow on Twitter"></a>
   </p>
 </p>
 
-[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the crackable WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/), 
+[Pwnagotchi](https://pwnagotchi.ai/) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment to maximize the crackable WPA key material it captures (either passively, or by performing authentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/), 
 full and half WPA handshakes.
 
-![handshake](https://i.imgur.com/pdA4vCZ.png)
+![ui](https://i.imgur.com/X68GXrn.png)
 
-Specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) doc.)
+Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning-based "AI" *(yawn)*, Pwnagotchi tunes [its parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things to** in the environments you expose it to. 
 
+More specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://www.pwnagotchi.ai/usage/#training-the-ai) doc.)
 
-Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml#L54) over time to **get better at pwning WiFi things** in the environments you expose it to. 
+**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://www.pwnagotchi.ai/usage/#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but ** listen to your Pwnagotchi when it tells you it's boring!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
 
-**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
-
-Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
-
-## Why does Pwnagotchi exist?
-
-For hackers to learn reinforcement learning, WiFi networking, and have an excuse to get out for more walks. Also? **It's cute as f---**.
+Multiple units within close physical proximity can "talk" to each other, advertising their presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
 
 ## Documentation
----
-:warning: **THE FOLLOWING DOCUMENTATION IS BEING PREPARED FOR THE v1.0 RELEASE OF PWNAGOTCHI. Since this effort is an active (and unstable) work-in-progress, the docs displayed here are in various stages of [in]completion. There will be dead links and placeholders throughout as we are still building things out in preparation for the v1.0 release.** :warning:
 
-**IMPORTANT NOTE:** If you'd like to alphatest Pwnagotchi and are trying to get yours up and running while the project is still very unstable, please understand that the documentation here may not reflect what is currently implemented. If you have questions, ask the community of alphatesters in the [official Pwnagotchi Slack](https://pwnagotchi.herokuapp.com). The Pwnagotchi dev team is entirely focused on the v1.0 release and will NOT be providing support for alphatesters trying to get their Pwnagotchis working in the meantime. All technical support during this period of development is being provided by your fellow alphatesters in the Slack (thanks, everybody! :heart:).
-
----
-- [About the Project](https://github.com/evilsocket/pwnagotchi/blob/master/docs/about.md)
-- [FAQ](https://github.com/evilsocket/pwnagotchi/blob/master/docs/faq.md)
-- [How to Install](https://github.com/evilsocket/pwnagotchi/blob/master/docs/install.md)
-- [Configuration](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md)
-- [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md)
-- [Plugins](https://github.com/evilsocket/pwnagotchi/blob/master/docs/plugins.md)
-- [Development](https://github.com/evilsocket/pwnagotchi/blob/master/docs/dev.md)
-- [Community Hacks](https://github.com/evilsocket/pwnagotchi/blob/master/docs/hacks.md)
+https://www.pwnagotchi.ai
 
 ## Links
 
diff --git a/bin/pwnagotchi b/bin/pwnagotchi
index 4733105..d6303d5 100755
--- a/bin/pwnagotchi
+++ b/bin/pwnagotchi
@@ -2,26 +2,28 @@
 if __name__ == '__main__':
     import argparse
     import time
-    import os
     import logging
 
     import pwnagotchi
+    import pwnagotchi.grid as grid
     import pwnagotchi.utils as utils
     import pwnagotchi.plugins as plugins
 
-    from pwnagotchi.log import SessionParser
+    from pwnagotchi.identity import KeyPair
     from pwnagotchi.agent import Agent
     from pwnagotchi.ui.display import Display
 
     parser = argparse.ArgumentParser()
 
-    parser.add_argument('-C', '--config', action='store', dest='config',
-                        default=os.path.join(os.path.abspath(os.path.dirname(pwnagotchi.__file__)), '/defaults.yml'),
+    parser.add_argument('-C', '--config', action='store', dest='config', default='/etc/pwnagotchi/default.yml',
                         help='Main configuration file.')
     parser.add_argument('-U', '--user-config', action='store', dest='user_config', default='/etc/pwnagotchi/config.yml',
                         help='If this file exists, configuration will be merged and this will override default values.')
 
     parser.add_argument('--manual', dest="do_manual", action="store_true", default=False, help="Manual mode.")
+    parser.add_argument('--skip-session', dest="skip_session", action="store_true", default=False,
+                        help="Skip last session parsing in manual mode.")
+
     parser.add_argument('--clear', dest="do_clear", action="store_true", default=False,
                         help="Clear the ePaper display and exit.")
 
@@ -35,9 +37,10 @@ if __name__ == '__main__':
     plugins.load(config)
 
     display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()})
-    agent = Agent(view=display, config=config)
+    keypair = KeyPair(view=display)
+    agent = Agent(view=display, config=config, keypair=keypair)
 
-    logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._identity, pwnagotchi.version))
+    logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent.fingerprint(), pwnagotchi.version))
 
     for _, plugin in plugins.loaded.items():
         logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
@@ -49,22 +52,23 @@ if __name__ == '__main__':
     elif args.do_manual:
         logging.info("entering manual mode ...")
 
-        log = SessionParser(config)
-        logging.info(
-            "the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
-                log.duration_human,
-                log.epochs,
-                log.train_epochs,
-                log.avg_reward,
-                log.min_reward,
-                log.max_reward))
+        agent.last_session.parse(args.skip_session)
+        if not args.skip_session:
+            logging.info(
+                "the last session lasted %s (%d completed epochs, trained for %d), average reward:%s (min:%s max:%s)" % (
+                    agent.last_session.duration_human,
+                    agent.last_session.epochs,
+                    agent.last_session.train_epochs,
+                    agent.last_session.avg_reward,
+                    agent.last_session.min_reward,
+                    agent.last_session.max_reward))
+
+        display.on_manual_mode(agent.last_session)
 
         while True:
-            display.on_manual_mode(log)
             time.sleep(1)
-
-            if Agent.is_connected():
-                plugins.on('internet_available', display, config, log)
+            if grid.is_connected():
+                plugins.on('internet_available', agent)
 
     else:
         logging.info("entering auto mode ...")
@@ -77,8 +81,6 @@ if __name__ == '__main__':
                 agent.recon()
                 # get nearby access points grouped by channel
                 channels = agent.get_access_points_by_channel()
-                # check for free channels to use
-                agent.check_channels(channels)
                 # for each channel
                 for ch, aps in channels:
                     agent.set_channel(ch)
@@ -102,5 +104,9 @@ if __name__ == '__main__':
                 # WiFi electromagnetic fields affect time like gravitational fields
                 # affect ours ... neat ^_^
                 agent.next_epoch()
+
+                if grid.is_connected():
+                    plugins.on('internet_available', agent)
+
             except Exception as e:
                 logging.exception("main loop exception")
diff --git a/builder/pwnagotchi.json b/builder/pwnagotchi.json
index beac17b..d7ec0ca 100644
--- a/builder/pwnagotchi.json
+++ b/builder/pwnagotchi.json
@@ -1,7 +1,4 @@
 {
-  "variables": {
-    "home": "{{env `HOME`}}"
-  },
   "builders": [{
     "name": "pwnagotchi",
     "type": "arm-image",
@@ -15,13 +12,15 @@
       "type": "shell",
       "inline": [
         "sed -i 's/^\\([^#]\\)/#\\1/g' /etc/ld.so.preload",
+        "dpkg-architecture",
         "apt-get -y update",
         "apt-get install -y ansible"
       ]
     },
     {
       "type":"ansible-local",
-      "playbook_file": "pwnagotchi.yml"
+      "playbook_file": "pwnagotchi.yml",
+      "command": "ANSIBLE_FORCE_COLOR=1 PYTHONUNBUFFERED=1 PWN_VERSION={{user `pwn_version`}} PWN_HOSTNAME={{user `pwn_hostname`}} ansible-playbook"
     },
     {
       "type": "shell",
diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml
index fab9c2e..b4c9680 100644
--- a/builder/pwnagotchi.yml
+++ b/builder/pwnagotchi.yml
@@ -5,17 +5,23 @@
   vars:
     pwnagotchi:
       hostname: "{{ lookup('env', 'PWN_HOSTNAME') | default('pwnagotchi', true) }}"
-      version: "{{ lookup('env', 'PWN_VERSION') | default('master', true) }} "
+      version: "{{ lookup('env', 'PWN_VERSION') | default('master', true) }}"
     system:
       boot_options:
         - "dtoverlay=dwc2"
-        - "dtparam=spi=on"
         - "dtoverlay=spi1-3cs"
-        - "dtoverlay=pi3-disable-bt"
-        - "dtparam=audio=off"
+        - "dtparam=spi=on"
+        - "dtparam=i2c_arm=on"
+        - "dtparam=i2c1=on"
+      modules:
+        - "i2c-dev"
     services:
       enable:
         - dphys-swapfile.service
+        - pwnagotchi.service
+        - bettercap.service
+        - pwngrid-peer.service
+        - epd-fuse.service
       disable:
         - apt-daily.timer
         - apt-daily.service
@@ -26,24 +32,18 @@
         - triggerhappy.service
         - ifup@wlan0.service
     packages:
-      pip:
-        install:
-          - inky
-          - smbus2
-          - absl-py>=0.1.6
-          - enum34
-          - gast==0.2.2
-          - google_pasta
-          - opt_einsum
-          - scapy
-          - gym
-          - keras_applications>=1.0.6
-          - keras_preprocessing>=1.0.5
-          - stable-baselines
-          - file_read_backwards
-          - tensorflow_estimator>=1.14.0,<1.15.0
-          - tensorboard>=1.13.0,<1.14.0
+      bettercap:
+        url: "https://github.com/bettercap/bettercap/releases/download/v2.25/bettercap_linux_armv6l_2.25.zip"
+        ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip"
+      pwngrid:
+        url: "https://github.com/evilsocket/pwngrid/releases/download/v1.9.0/pwngrid_linux_armhf_v1.9.0.zip"
       apt:
+        hold:
+          - firmware-atheros
+          - firmware-brcm80211
+          - firmware-libertas
+          - firmware-misc-nonfree
+          - firmware-realtek
         remove:
           - rasberrypi-net-mods
           - dhcpcd5
@@ -57,6 +57,8 @@
           - git
           - build-essential
           - python3-pip
+          - python3-mpi4py
+          - python3-smbus
           - unzip
           - gawk
           - libopenmpi-dev
@@ -65,6 +67,7 @@
           - libqtgui4
           - libqt4-test
           - libopenjp2-7
+          - libtiff5
           - tcpdump
           - lsof
           - libilmbase23
@@ -76,6 +79,7 @@
           - libpcap-dev
           - libusb-1.0-0-dev
           - libnetfilter-queue-dev
+          - libopenmpi3
           - dphys-swapfile
           - kalipi-kernel
           - kalipi-bootloader
@@ -88,49 +92,33 @@
           - fonts-dejavu
           - fonts-dejavu-core
           - fonts-dejavu-extra
-          - python3-crypto
-          - python3-requests
-          - python3-yaml
-          - python3-smbus
-          - python3-inkyphat
-          - python3-numpy
           - python3-pil
-          - python3-tweepy
-          - python3-opencv
-          - python3-termcolor
-          - python3-astor
-          - python3-backports.weakref
-          - python3-h5py
-          - python3-six
-          - python3-protobuf
-          - python3-wrapt
-          - python3-wheel
-          - python3-mock
-          - python3-scipy
-          - python3-cloudpickle
-
-    bettercap:
-      query: "assets[?contains(name, 'armv6l')].browser_download_url"
+          - python3-smbus
+          - libfuse-dev
+          - bc
+          - fonts-freefont-ttf
+          - fbi
 
   tasks:
-
-  - name: selected hostname
-    debug:
-      msg: "{{ pwnagotchi.hostname }}"
-
-  - name: build version
-    debug:
-      msg: "{{ pwnagotchi.version }}"
-
   - name: change hostname
     hostname:
       name: "{{pwnagotchi.hostname}}"
+    when: lookup('file', '/etc/hostname') == "raspberrypi"
+    register: hostname
 
   - name: add hostname to /etc/hosts
     lineinfile:
       dest: /etc/hosts
-      regexp: '^127\.0\.0\.1[ \t]+localhost'
-      line: '127.0.0.1 localhost {{pwnagotchi.hostname}} {{pwnagotchi.hostname}}.local'
+      regexp: '^127\.0\.1\.1[ \t]+raspberrypi'
+      line: "127.0.1.1\t{{pwnagotchi.hostname}}"
+      state: present
+    when: hostname.changed
+
+  - name: disable sap plugin for bluetooth.service
+    lineinfile:
+      dest: /lib/systemd/system/bluetooth.service
+      regexp: '^ExecStart=/usr/lib/bluetooth/bluetoothd$'
+      line: 'ExecStart=/usr/lib/bluetooth/bluetoothd --noplugin=sap'
       state: present
 
   - name: Add re4son-kernel repo key
@@ -143,6 +131,12 @@
       repo: deb http://http.re4son-kernel.com/re4son/ kali-pi main
       state: present
 
+  - name: add firmware packages to hold
+    dpkg_selections:
+      name: "{{ item }}"
+      selection: hold
+    with_items: "{{ packages.apt.hold }}"
+
   - name: update apt package cache
     apt:
       update_cache: yes
@@ -167,30 +161,98 @@
       path: /etc/dphys-swapfile
       content: "CONF_SWAPSIZE=1024"
 
+  - name: clone papirus repository
+    git:
+      repo: https://github.com/repaper/gratis.git
+      dest: /usr/local/src/gratis
+    register: gratisgit
+
+  - name: build papirus service
+    make:
+      chdir: /usr/local/src/gratis
+      target: rpi
+      params:
+        EPD_IO: epd_io.h
+        PANEL_VERSION: 'V231_G2'
+    when: gratisgit.changed
+
+  - name: install papirus service
+    make:
+      chdir: /usr/local/src/gratis
+      target: rpi-install
+      params:
+        EPD_IO: epd_io.h
+        PANEL_VERSION: 'V231_G2'
+    when: gratisgit.changed
+
+  - name: configure papirus display size
+    lineinfile:
+      dest: /etc/default/epd-fuse
+      regexp: "#EPD_SIZE=2.0"
+      line: "EPD_SIZE=2.0"
+
+  - name: collect python pip package list
+    command: "pip3 list"
+    register: pip_output
+
+  - name: set python pip package facts
+    set_fact:
+      pip_packages: >
+        {{ pip_packages | default({}) | combine( { item.split()[0]: item.split()[1] } ) }}
+    with_items: "{{ pip_output.stdout_lines }}"
+
   - name: acquire python3 pip target
     command: "python3 -c 'import sys;print(sys.path.pop())'"
     register: pip_target
 
-  - name: install pip packages
-    pip:
-      name: "{{packages.pip.install}}"
-      extra_args: "--no-deps --extra-index-url=https://www.piwheels.hostedpi.com/simple/ --prefer-binary --no-cache-dir --platform=armv6l --target={{ pip_target.stdout }}"
+  - name: clone pwnagotchi repository
+    git:
+      repo: https://github.com/evilsocket/pwnagotchi.git
+      dest: /usr/local/src/pwnagotchi
+    register: pwnagotchigit
 
-  - name: install grpcio
-    command: "pip3 install --no-deps --extra-index-url=https://www.piwheels.hostedpi.com/simple/ --no-cache-dir --prefer-binary --platform=armv6l --only-binary=:all: --target={{ pip_target.stdout }} https://www.piwheels.hostedpi.com/simple/grpcio/grpcio-1.24.1-cp37-cp37m-linux_armv6l.whl"
+  - name: fetch pwnagotchi version
+    set_fact:
+      pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/__init__.py') | replace('\n', ' ') | regex_replace('.*version.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}"
+
+  - name: pwnagotchi version found
+    debug:
+      msg: "{{ pwnagotchi_version }}"
+
+  - name: build pwnagotchi wheel
+    command: "python3 setup.py sdist bdist_wheel"
+    args:
+      chdir: /usr/local/src/pwnagotchi
+    when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi.version)
+
+  - name: install opencv-python
+    pip:
+      name: "https://www.piwheels.hostedpi.com/simple/opencv-python/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl"
+      extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}"
+    when: (pip_packages['opencv-python'] is undefined) or (pip_packages['opencv-python'] != '3.4.3.18')
 
   - name: install tensorflow
-    command: "pip3 install --no-deps --extra-index-url=https://www.piwheels.hostedpi.com/simple/ --no-cache-dir --prefer-binary --platform=armv6l --only-binary=:all: --target={{ pip_target.stdout }} https://www.piwheels.org/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl"
+    pip:
+      name: "https://www.piwheels.hostedpi.com/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl"
+      extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}"
+    when: (pip_packages['tensorflow'] is undefined) or (pip_packages['tensorflow'] != '1.13.1')
 
-  - name: fetch bettercap release information
-    uri:
-      url: https://api.github.com/repos/bettercap/bettercap/releases/latest
-      return_content: yes
-    register: bettercap_release
+  - name: install pwnagotchi wheel and dependencies
+    pip:
+      name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}"
+      extra_args: "--no-cache-dir"
+    when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi.version)
+
+  - name: download and install pwngrid
+    unarchive:
+      src: "{{ packages.pwngrid.url }}"
+      dest: /usr/bin
+      remote_src: yes
+      mode: 0755
 
   - name: download and install bettercap
     unarchive:
-      src: "{{ bettercap_release.content | from_json | json_query(bettercap.query) | first }}"
+      src: "{{ packages.bettercap.url }}"
       dest: /usr/bin
       remote_src: yes
       exclude:
@@ -202,47 +264,20 @@
     git:
       repo: https://github.com/bettercap/caplets.git
       dest: /tmp/caplets
+    register: capletsgit
 
   - name: install bettercap caplets
     make:
       chdir: /tmp/caplets
       target: install
+    when: capletsgit.changed
 
-  - name: clone pwnagotchi repository
-    git:
-      repo: https://github.com/evilsocket/pwnagotchi.git
-      dest: /tmp/pwnagotchi
-
-  - name: copy pwnagotchi files to final destination
-    copy:
-      src: /tmp/pwnagotchi/sdcard/rootfs/root/pwnagotchi/
-      dest: /root/pwnagotchi/
-      mode: preserve
-
-  - name: remove pwnagotchi files from temporary repository
-    file:
-      path: /tmp/pwnagotchi
-      state: absent
-
-  - name: create cpuusage script
-    copy:
-      dest: /usr/bin/cpuusage
+  - name: download and install bettercap ui
+    unarchive:
+      src: "{{ packages.bettercap.ui }}"
+      dest: /usr/local/share/bettercap/
+      remote_src: yes
       mode: 0755
-      content: |
-        #!/usr/bin/env bash
-        while true
-        do
-          top -b -n1 | awk '/Cpu\(s\)/ { printf("%d %", $2 + $4 + 0.5) }'
-          sleep 3
-        done
-
-  - name: create memusage script
-    copy:
-      dest: /usr/bin/memusage
-      mode: 0755
-      content: |
-        #!/usr/bin/env bash
-        free -m | awk '/Mem/ { printf( "%d %", $3 / $2 * 100 + 0.5 ) }'
 
   - name: create bootblink script
     copy:
@@ -269,10 +304,34 @@
         # blink 10 times to signal ready state
         /usr/bin/bootblink 10 &
         # start a detached screen session with bettercap
-        if ifconfig | grep usb0 | grep RUNNING; then
-          /usr/bin/pwnagotchi --manual
+        if [[ ifconfig | grep usb0 | grep RUNNING ]] || [[ $(cat /sys/class/net/eth0/carrier) ]]; then
+          # if override file exists, go into auto mode
+          if [ -f /root/.pwnagotchi-auto ]; then
+            rm /root/.pwnagotchi-auto
+            /usr/local/bin/pwnagotchi
+          else
+            /usr/local/bin/pwnagotchi --manual
+          fi
         else
-          /usr/bin/pwnagotchi
+          /usr/local/bin/pwnagotchi
+        fi
+
+  - name: create bettercap-launcher script
+    copy:
+      dest: /usr/bin/bettercap-launcher
+      mode: 0755
+      content: |
+        #!/usr/bin/env bash
+        /usr/bin/monstart
+        if [[ ifconfig | grep usb0 | grep RUNNING ]] || [[ $(cat /sys/class/net/eth0/carrier) ]]; then
+          # if override file exists, go into auto mode
+          if [ -f /root/.pwnagotchi-auto ]; then
+            /usr/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface mon0
+          else
+            /usr/bin/bettercap -no-colors -caplet pwnagotchi-manual -iface mon0
+          fi
+        else
+          /usr/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface mon0
         fi
 
   - name: create monstart script
@@ -291,7 +350,23 @@
         #!/usr/bin/env bash
         ifconfig mon0 down && iw dev mon0 del
 
-  - name: configure rc.local
+  - name: create hdmion script
+    copy:
+      dest: /usr/bin/hdmion
+      mode: 0755
+      content: |
+        #!/usr/bin/env bash
+        sudo /opt/vc/bin/tvservice -p
+
+  - name: create hdmioff script
+    copy:
+      dest: /usr/bin/hdmioff
+      mode: 0755
+      content: |
+        #!/usr/bin/env bash
+        sudo /opt/vc/bin/tvservice -o
+
+  - name: add HDMI powersave to rc.local
     blockinfile:
       path: /etc/rc.local
       insertbefore: "exit 0"
@@ -299,36 +374,49 @@
         if ! /opt/vc/bin/tvservice -s | grep HDMI; then
           /opt/vc/bin/tvservice -o
         fi
-        /root/pwnagotchi/scripts/startup.sh &
+
+  - name: create /etc/pwnagotchi folder
+    file:
+      path: /etc/pwnagotchi
+      state: directory
+
+  - name: check if user configuration exists
+    stat:
+      path: /etc/pwnagotchi/config.yml
+    register: user_config
 
   - name: create /etc/pwnagotchi/config.yml
-    blockinfile:
-      path: /etc/pwnagotchi/config.yml
-      create: yes
-      block: |
-        # put here your custom configuration overrides
+    copy:
+      dest: /etc/pwnagotchi/config.yml
+      content: |
+        # Add your configuration overrides on this file any configuration changes done to default.yml will be lost!
+        # Example:
+        #
+        # ui:
+        #   display:
+        #     type: 'inkyphat'
+        #     color: 'black'
+        #
+    when: not user_config.stat.exists
 
   - name: configure lo interface
-    blockinfile:
-      path: /etc/network/interfaces.d/lo-cfg
-      create: yes
-      block: |
+    copy:
+      dest: /etc/network/interfaces.d/lo-cfg
+      content: |
         auto lo
         iface lo inet loopback
 
   - name: configure wlan interface
-    blockinfile:
-      path: /etc/network/interfaces.d/wlan0-cfg
-      create: yes
-      block: |
+    copy:
+      dest: /etc/network/interfaces.d/wlan0-cfg
+      content: |
         allow-hotplug wlan0
         iface wlan0 inet static
 
   - name: configure usb interface
-    blockinfile:
-      path: /etc/network/interfaces.d/usb0-cfg
-      create: yes
-      block: |
+    copy:
+      dest: /etc/network/interfaces.d/usb0-cfg
+      content: |
         allow-hotplug usb0
         iface usb0 inet static
           address 10.0.0.2
@@ -338,10 +426,9 @@
           gateway 10.0.0.1
 
   - name: configure eth0 interface (pi2/3/4)
-    blockinfile:
-      path: /etc/network/interfaces.d/eth0-cfg
-      create: yes
-      block: |
+    copy:
+      dest: /etc/network/interfaces.d/eth0-cfg
+      content: |
         allow-hotplug eth0
         iface eth0 inet dhcp
 
@@ -355,8 +442,14 @@
       dest: /boot/config.txt
       insertafter: EOF
       line: '{{ item }}'
-    with_items:
-      - "{{system.boot_options}}"
+    with_items: "{{system.boot_options}}"
+
+  - name: adjust /etc/modules
+    lineinfile:
+      dest: /etc/modules
+      insertafter: EOF
+      line: '{{ item }}'
+    with_items: "{{system.modules}}"
 
   - name: change root partition
     replace:
@@ -374,17 +467,37 @@
       regexp: '(.*)$'
       line: '\1 modules-load=dwc2,g_ether'
 
-  - name: configure ssh
-    lineinfile:
-      dest: /etc/ssh/sshd_config
-      backup: no
-      regexp: '#?PermitRootLogin (.*)$'
-      line: 'PermitRootLogin yes'
-
   - name: configure motd
     copy:
       dest: /etc/motd
-      content: "(◕‿‿◕) {{pwnagotchi.hostname}} (pwnagotchi-{{pwnagotchi.version}})"
+      content: |
+        (◕‿‿◕) {{pwnagotchi.hostname}} (pwnagotchi-{{pwnagotchi.version}})
+
+        Hi! I'm a pwnagotchi, please take good care of me!
+        Here are some basic things you need to know to raise me properly!
+
+        If you want to change my configuration, use /etc/pwnagotchi/config.yml
+
+        All the configuration options can be found on /etc/pwnagotchi/default.yml,
+        but don't change this file because I will recreate it every time I'm restarted!
+
+        I'm managed by systemd. Here are some basic commands.
+
+        If you want to know what I'm doing, you can check my logs with the command
+        journalctl -fu pwnagotchi
+
+        If you want to know if I'm running, you can use
+        systemctl status pwnagotchi
+
+        You can restart me using
+        systemctl restart pwnagotchi
+
+        But be aware I will go into MANUAL mode when restarted!
+        You can put me back into AUTO mode using
+        touch /root/.pwnagotchi-auto && systemctl restart pwnagotchi
+
+        You learn more about me at https://pwnagotchi.ai/
+    when: hostname.changed
 
   - name: clean apt cache
     apt:
@@ -394,17 +507,84 @@
     apt:
       autoremove: yes
 
+  - name: add pwngrid-peer service to systemd
+    copy:
+      dest: /etc/systemd/system/pwngrid-peer.service
+      content: |
+        [Unit]
+        Description=pwngrid peer service.
+        Documentation=https://pwnagotchi.ai
+        Wants=network.target
+
+        [Service]
+        Type=simple
+        PermissionsStartOnly=true
+        ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -client-token /root/.api-enrollment.json -wait -log /var/log/pwngrid-peer.log -iface mon0
+        Restart=always
+        RestartSec=30
+
+        [Install]
+        WantedBy=multi-user.target
+    notify:
+      - reload systemd services
+
+  - name: add bettercap service to systemd
+    copy:
+      dest: /etc/systemd/system/bettercap.service
+      content: |
+        [Unit]
+        Description=bettercap api.rest service.
+        Documentation=https://bettercap.org
+        Wants=network.target
+        After=pwngrid.service
+
+        [Service]
+        Type=simple
+        PermissionsStartOnly=true
+        ExecStart=/usr/bin/bettercap-launcher
+        Restart=always
+        RestartSec=30
+
+        [Install]
+        WantedBy=multi-user.target
+    notify:
+      - reload systemd services
+
+  - name: add pwnagotchi service to systemd
+    copy:
+      dest: /etc/systemd/system/pwnagotchi.service
+      content: |
+        [Unit]
+        Description=pwnagotchi Deep Reinforcement Learning instrumenting bettercap for WiFI pwning.
+        Documentation=https://pwnagotchi.ai
+        Wants=network.target
+        After=bettercap.service
+
+        [Service]
+        Type=simple
+        PermissionsStartOnly=true
+        ExecStart=/usr/bin/pwnagotchi-launcher
+        Restart=always
+        RestartSec=30
+
+        [Install]
+        WantedBy=multi-user.target
+    notify:
+      - reload systemd services
+
   - name: enable services
     systemd:
-      name: "{{services.enable}}"
+      name: "{{ item }}"
       state: started
       enabled: yes
+    with_items: "{{ services.enable }}"
 
   - name: disable unecessary services
     systemd:
-      name: "{{services.disable}}"
+      name: "{{ item }}"
       state: stopped
       enabled: no
+    with_items: "{{ services.disable }}"
 
   - name: remove ssh keys
     file:
@@ -412,3 +592,8 @@
       path: "{{item}}"
     with_fileglob:
       - "/etc/ssh/ssh_host*_key*"
+
+  handlers:
+  - name: reload systemd services
+    systemd:
+      daemon_reload: yes
diff --git a/docs/about.md b/docs/about.md
deleted file mode 100644
index eacbd73..0000000
--- a/docs/about.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# About the Project
-
-[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/), 
-full and half WPA handshakes.
-
-![handshake](https://i.imgur.com/pdA4vCZ.png)
-
-Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml#L54) over time to **get better at pwning WiFi things** in the environments you expose it to. 
-
-**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
-
-Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
-
-![peers](https://i.imgur.com/Ywr5aqx.png)
-
-[Depending on the status of the unit](), several states and states transitions are configurable and represented on the display as different moods, expressions and sentences. Pwnagotchi speaks [many languages](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md#configuration), too!
-
-Of course, it is possible to run your Pwnagotchi with the AI disabled (configurable in `config.yml`). Why might you want to do this? Perhaps you simply want to use your own fixed parameters (instead of letting the AI decide for you), or maybe you want to save battery and CPU cycles, or maybe it's just you have strong concerns about aiding and abetting baby Skynet. Whatever your particular reasons may be: an AI-disabled Pwnagotchi is still a simple and very effective automated deauther, WPA handshake sniffer, and portable [bettercap](https://www.bettercap.org/) + [webui](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md#bettercaps-web-ui) dedicated hardware.
-
-
-## License
-
-`pwnagotchi` is made with ♥  by [@evilsocket](https://twitter.com/evilsocket) and the [amazing dev team](https://github.com/evilsocket/pwnagotchi/graphs/contributors). It's released under the GPL3 license.
diff --git a/docs/configure.md b/docs/configure.md
deleted file mode 100644
index d2bb0f5..0000000
--- a/docs/configure.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Connecting to your Pwnagotchi
-
-Once you wrote the image file on the SD card, there're a few steps you'll have to follow in order to configure your unit properly, first, start with connecting the USB cable to the data port of the Raspberry Pi and the RPi to your computer. After a few seconds the board will boot and you will see a new Ethernet interface on your host computer.
-
-You'll need to configure it with a static IP address:
-
-- IP: `10.0.0.1`
-- Netmask: `255.255.255.0`
-- Gateway: `10.0.0.1`
-- DNS (if required): `8.8.8.8` (or whatever)
-
-If everything's been configured properly, you will now be able to `ping` both `10.0.0.2` or `pwnagotchi.local` (if you haven't customized the hostname yet).
-
-You can now connect to your unit using SSH:
-
-```bash
-ssh pi@10.0.0.2
-```
-
-The default password is `raspberry`, you should change it as soon as you log in for the first time by issuing the `passwd`command and selecting a new and more complex passphrase.
-
-Moreover, it is recommended that you copy your SSH public key among the unit's authorized ones, so you can directly log in without entering a password:
-
-```bash
-ssh-copy-id -i ~/.ssh/id_rsa.pub pi@10.0.0.2
-```
-
-## Configuration
-
-You can now set a new name for your unit by [changing the hostname](https://geek-university.com/raspberry-pi/change-raspberry-pis-hostname/). Create the `/root/custom.yml` file (either via SSH or by direclty editing the SD card contents from a computer) that will override the [default configuration](https://github.com/evilsocket/pwnagotchi/blob/master/sdcard/rootfs/root/pwnagotchi/config.yml) with your custom values.
-
-## Language Selection
-
-For instance, you can change `main.lang` to one of the supported languages:
-
-- **english** (default)
-- german
-- dutch
-- greek
-- macedonian
-- italian
-- french
-- russian
-- swedish
-
-## Display Selection
-
-Set the type of display you want to use via `ui.display.type` (if your display does not work after changing this setting, you might need to completely remove power from the Raspberry and make a clean boot).
-
-You can configure the refresh interval of the display via `ui.fps`, we advise to use a slow refresh to not shorten the lifetime of your display. The default value is 0, which will only refresh when changes are made to the screen.
-
-## Host Connection Share
-
-If you connect to the unit via `usb0` (thus using the data port), you might want to use the `scripts/linux_connection_share.sh`, `scripts/macos_connection_share.sh` or `scripts/win_connection_share.ps1` 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.
-
-## Troubleshooting
-
-If your network connection keeps flapping on your device connecting to your pwnagotchi, check if `usb0` (or equivalent) device is being controlled by NetworkManager. You can check this via `nmcli dev status`.
diff --git a/docs/dev.md b/docs/dev.md
deleted file mode 100644
index 75b7ea0..0000000
--- a/docs/dev.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Software
-
-- Raspbian + [nexmon patches](https://re4son-kernel.com/re4son-pi-kernel/) for monitor mode, or any Linux with a monitor mode enabled interface (if you tune config.yml).
-
-**Do not try with Kali on the Raspberry Pi 0 W, it is compiled without hardware floating point support and TensorFlow is simply not available for it, use Raspbian.**
-
-## Creating an Image
-
-You can use the `scripts/create_sibling.sh` script to create an - ready to flash - rasbian image with pwnagotchi.
-
-```shell
-usage: ./scripts/create_sibling.sh [OPTIONS]
-
-  Options:
-    -n <name>    # Name of the pwnagotchi (default: pwnagotchi)
-    -i <file>    # Provide the path of an already downloaded raspbian image
-    -o <file>    # Name of the img-file (default: pwnagotchi.img)
-    -s <size>    # Size which should be added to second partition (in Gigabyte) (default: 4)
-    -v <version> # Version of raspbian (Supported: latest; default: latest)
-    -p           # Only run provisioning (assumes the image is already mounted)
-    -d           # Only run dependencies checks
-    -h           # Show this help
-```
-
-#### Known Issues
-
-`GLib-ERROR **: 20:50:46.361: getauxval () failed: No such file or directory`
-
-- Affected DEB & Versions: QEMU <= 2.11 
-- Fix: Upgrade QEMU to >= 3.1
-- Bug Link: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923289
-
-## Adding a Language
-
-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
-# Now make your changes to the file
-# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
-./scripts/language.sh compile it
-# DONE
-```
-
-If you changed the `voice.py`- File, the translations need an update. Do it like this:
-
-```shell
-./scripts/language.sh update it
-# Now make your changes to the file (changed lines are marked with "fuzzy")
-# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
-./scripts/language.sh compile it
-# 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
-```
diff --git a/docs/faq.md b/docs/faq.md
deleted file mode 100644
index ff19087..0000000
--- a/docs/faq.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# FAQ
-
-## Why eINK?
-
-Because!
-
-## Why the AI takes 30 minutes to load?
-
-Because Python sucks and TF is huge.
-
-## Why ...?
-
-Because!
diff --git a/docs/hacks.md b/docs/hacks.md
deleted file mode 100644
index 184cff0..0000000
--- a/docs/hacks.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Unofficial Hacks
----
-**IMPORTANT DISCLAIMER:** The information provided on this page is NOT officially supported by the Pwnagotchi development team. These are unofficial "hacks" that users have worked out while customizing their units and decided to document for anybody else who might want to do something similar. 
-
-- **Please do NOT open issues if you cannot get something described in this document to work.** 
-- It (almost) goes without saying, but obviously: **we are NOT responsible if you break your hardware by following any instructions documented here. Use this information at your own risk.**
-
----
-If you test one of these hacks yourself and it still works, it's extra nice if you update the **Last Tested On** table and note any minor adjustments you may have had to make to the instructions to make it work with your particular Pwnagotchi setup. :heart:
-
-
-## Screens
-### Waveshare 3.5" SPI TFT screen
-
-Last tested on | Pwnagotchi version | Working? | Reference
----------------|--------------------|----------|-----------|
-2019 October 3 | Unknown | :white_check_mark: | ([link](https://github.com/evilsocket/pwnagotchi/issues/124#issue-502346040))
-
-Some of this guide will work with other framebuffer-based displays.
-
-- First: SSH into your Pwnagotchi, and give it some internet! 
-  - Don't forget to check your default gateway and `apt-get update`.
-- Follow the guide here: [www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)#Method_1._Driver_installation](https://www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)#Method_1._Driver_installation)
-  - At the step with `./LCD35-show`, add `lite` to the command prompt (e.g., `./LCD35-show lite`).
-- Reboot.
-- As root, make three symlinks:
-  - `cd ~`
-  - `ln -s pwnagotchi.png pwnagotchi_1.png`
-  - `ln -s pwnagotchi.png pwnagotchi_2.png`
-  - `ln -s pwnagotchi.png pwnagotchi_3.png`
-- `apt install fbi`
-- Change display type to `inky` in `config.yml`
-- Add `modules-load=dwc2,g_ether` to your kernel command line (`/boot/cmdline.txt`) or it will break!
-- Also must add `dtoverlay=dwc2` to the bottom of (`/boot/config.txt`)
-- Edit `/etc/rc.local` and add: `fbi -T 1 -a -noverbose -t 15 -cachemem 0 /root/pwnagotchi_1.png /root/pwnagotchi_2.png /root/pwnagotchi_3.png &`
-- Reboot.
-
-And you should be good!
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index b998af0..0000000
--- a/docs/index.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Documentation
-
-- [About the Project](https://github.com/evilsocket/pwnagotchi/blob/master/docs/about.md)
-- [How to Install](https://github.com/evilsocket/pwnagotchi/blob/master/docs/install.md)
-- [Configuration](https://github.com/evilsocket/pwnagotchi/blob/master/docs/configure.md)
-- [Usage](https://github.com/evilsocket/pwnagotchi/blob/master/docs/usage.md)
-- [Plugins](https://github.com/evilsocket/pwnagotchi/blob/master/docs/plugins.md)
-- [Development](https://github.com/evilsocket/pwnagotchi/blob/master/docs/dev.md)
-- [FAQ](https://github.com/evilsocket/pwnagotchi/blob/master/docs/faq.md)
-- [Community Hacks](https://github.com/evilsocket/pwnagotchi/blob/master/docs/hacks.md)
-
-## Links
-
-&nbsp; | Official Links
----------|-------
-Slack | [pwnagotchi.slack.com](https://pwnagotchi.herokuapp.com)
-Twitter | [@pwnagotchi](https://twitter.com/pwnagotchi)
-Subreddit | [r/pwnagotchi](https://www.reddit.com/r/pwnagotchi/)
-Website | [pwnagotchi.ai](https://pwnagotchi.ai/)
-
-## License
-
-`pwnagotchi` is made with ♥  by [@evilsocket](https://twitter.com/evilsocket) and the [amazing dev team](https://github.com/evilsocket/pwnagotchi/graphs/contributors). It's released under the GPL3 license.
diff --git a/docs/install.md b/docs/install.md
deleted file mode 100644
index b1ee1d7..0000000
--- a/docs/install.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Installation
-
-The project has been developed to run on a Raspberry Pi 0 W configured as an [USB Ethernet gadget](https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget) device in order to connect to it via USB. However, given the proper configuration tweaks, any GNU/Linux computer with a WiFi interface that supports monitor mode could be used.
-
-**An important note about the AI:** a network trained with a specific WiFi interface will only work with another interface if it supports 
-the same exact WiFi channels of the first one. For instance, you can not use a neural network trained on a Raspberry Pi Zero W (that only supports 2.4Ghz channels) with a 5Ghz antenna, but you'll need to train one from scratch for those channels.
-
-## Required Hardware
-
-- [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/).
-- A micro SD card, 8GB recomended, **preferably of good quality and speed**.
-- A decent power bank (with 1500 mAh you get ~2 hours with AI on).
-- One of the supported displays (optional).
-
-### Display
-
-The display is an optional component as the UI is also rendered via a web interface available via the USB cable. If you connect to `usb0` (by using the data port on the unit) and point your browser to the web ui (see config.yml), your unit can work in "headless mode".
-
-If instead you want to fully enjoy walking around and literally looking at your unit's face, the supported display models are:
-
-- [Waveshare eInk Display (both V1 and V2)](https://www.waveshare.com/2.13inch-e-paper-hat.htm)
-- [Pimoroni Inky pHAT](https://shop.pimoroni.com/products/inky-phat)
-- [PaPiRus eInk Screen](https://uk.pi-supply.com/products/papirus-zero-epaper-screen-phat-pi-zero)
-
-Needless to say, we are always happy to receive pull requests adding support for new models.
-
-One thing to note, not all displays are created equaly, TFT displays for example work similar to an HDMI display, and they are not supported, currently all the displays supported are I2C displays.
-
-#### Color and Black & White displays
-
-Some of the supported displays support Black & White and Coloured versions, one common question is regarding refresh speed of said displays.
-
-Color displays have a much slower refresh rate, in some cases it can take up to 15 seconds, if slow refresh rates is something that you want to avoid we advise you to use Black & White displays
-
-## Flashing an Image
-
-The easiest way to create a new Pwnagotchi is downloading the latest stable image from [our release page](https://github.com/evilsocket/pwnagotchi/releases) and write it to your SD card. You will need to use an image writing tool to install the image you have downloaded on your SD card.
-
-[balenaEtcher](https://www.balena.io/etcher/) is a graphical SD card writing tool that works on Mac OS, Linux and Windows, and is the easiest option for most users. balenaEtcher also supports writing images directly from the zip file, without any unzipping required. To write your image with balenaEtcher:
-
-- Download the latest [Pwnagotchi .img file](https://github.com/evilsocket/pwnagotchi/releases).
-- Download [balenaEtcher](https://www.balena.io/etcher/) and install it.
-- Connect an SD card reader with the SD card inside.
-- Open balenaEtcher and select from your hard drive the Raspberry Pi .img or .zip file you wish to write to the SD card.
-- Select the SD card you wish to write your image to.
-- Review your selections and click 'Flash!' to begin writing data to the SD card.
-
-Your SD card is now ready for the first boot!
diff --git a/docs/plugins.md b/docs/plugins.md
deleted file mode 100644
index 0038df9..0000000
--- a/docs/plugins.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# 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.
-
-Here's as an example the GPS plugin:
-
-```python
-__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():
-     logging.info("GPS plugin loaded for %s" % device)
-
-
-def on_ready(agent):
-    global running
-
-    if os.path.exists(device):
-         logging.info("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:
-         logging.info("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')
-
-         logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
-        with open(gps_filename, 'w+t') as fp:
-            json.dump(gps, fp)
-```
diff --git a/docs/usage.md b/docs/usage.md
deleted file mode 100644
index 8883cb7..0000000
--- a/docs/usage.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# Usage
-
-## User Interface
-
-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).
-
-![ui](https://i.imgur.com/XgIrcur.png)
-
-* **CH**: Current channel the unit is operating on or `*` when hopping on all channels.
-* **APS**: Number of access points on the current channel and total visible access points.
-* **UP**: Time since the unit has been activated.
-* **PWND**: Number of handshakes captured in this session and number of unique networks we own at least one handshake of, from the beginning.
-* **AUTO**: This indicates that the algorithm is running with AI disabled (or still loading), it disappears once the AI dependencies have been bootrapped and the neural network loaded.
-
-## Training the AI
-
-At its core Pwnagotchi is a very simple creature: we could summarize its main algorithm as:
-
-```python
-# main loop
-while True:
-    # ask bettercap for all visible access points and their clients
-    aps = get_all_visible_access_points()
-    # loop each AP
-    for ap in aps:
-        # send an association frame in order to grab the PMKID
-        send_assoc(ap)
-        # loop each client station of the AP
-        for client in ap.clients:
-            # deauthenticate the client to get its half or full handshake
-            deauthenticate(client)
-
-    wait_for_loot()
-```
-
-Despite its simplicity, this logic is controlled by several parameters that regulate the wait times, the timeouts, on which channels to hop and so on.
-
-From `config.yml`:
-
-```yaml
-personality:
-    # advertise our presence
-    advertise: true
-    # perform a deauthentication attack to client stations in order to get full or half handshakes
-    deauth: true
-    # send association frames to APs in order to get the PMKID
-    associate: true
-    # list of channels to recon on, or empty for all channels
-    channels: []
-    # minimum WiFi signal strength in dBm
-    min_rssi: -200
-    # number of seconds for wifi.ap.ttl
-    ap_ttl: 120
-    # number of seconds for wifi.sta.ttl
-    sta_ttl: 300
-    # time in seconds to wait during channel recon
-    recon_time: 30
-    # number of inactive epochs after which recon_time gets multiplied by recon_inactive_multiplier
-    max_inactive_scale: 2
-    # if more than max_inactive_scale epochs are inactive, recon_time *= recon_inactive_multiplier
-    recon_inactive_multiplier: 2
-    # time in seconds to wait during channel hopping if activity has been performed
-    hop_recon_time: 10
-    # time in seconds to wait during channel hopping if no activity has been performed
-    min_recon_time: 5
-    # maximum amount of deauths/associations per BSSID per session
-    max_interactions: 3
-    # maximum amount of misses before considering the data stale and triggering a new recon
-    max_misses_for_recon: 5
-    # number of active epochs that triggers the excited state
-    excited_num_epochs: 10
-    # number of inactive epochs that triggers the bored state
-    bored_num_epochs: 15
-    # number of inactive epochs that triggers the sad state
-    sad_num_epochs: 25
-```
-
-There is no optimal set of parameters for every situation: when the unit is moving (during a walk for instance) smaller timeouts and RSSI thresholds might be preferred in order to quickly remove routers that are not in range anymore, while when stationary in high density areas (like an office) other parameters might be better. The role of the AI is to observe what's going on at the WiFi level, and adjust those parameters in order to maximize the cumulative reward of that loop / epoch.
-
-## Reward Function
-
-After each iteration of the main loop (an `epoch`), the reward, a score that represents how well the parameters performed, is computed as (an excerpt from `pwnagotchi/ai/reward.py`):
-
-```python
-# state contains the information of the last epoch
-# epoch_n is the number of the last epoch
-tot_epochs = epoch_n + 1e-20 # 1e-20 is added to avoid a division by 0
-tot_interactions = max(state['num_deauths'] + state['num_associations'], state['num_handshakes']) + 1e-20
-tot_channels = wifi.NumChannels
-
-# ideally, for each interaction we would have an handshake
-h = state['num_handshakes'] / tot_interactions
-# small positive rewards the more active epochs we have
-a = .2 * (state['active_for_epochs'] / tot_epochs)
-# make sure we keep hopping on the widest channel spectrum
-c = .1 * (state['num_hops'] / tot_channels)
-# small negative reward if we don't see aps for a while
-b = -.3 * (state['blind_for_epochs'] / tot_epochs)
-# small negative reward if we interact with things that are not in range anymore
-m = -.3 * (state['missed_interactions'] / tot_interactions)
-# small negative reward for inactive epochs
-i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
-
-reward = h + a + c + b + i + m
-```
-
-By maximizing this reward value, the AI learns over time to find the set of parameters that better perform with the current environmental conditions.
-
-## BetterCAP's Web UI
-
-Moreover, given that the unit is running bettercap with API and Web UI, you'll be able to use the unit as a WiFi penetration testing portable station by accessing `http://pwnagotchi.local/`.
-
-![webui](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png)
-
-## 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.
-
-```
-
-## Backup your Pwnagotchi
-
-You can use the `scripts/backup.sh` script to backup the important files of your unit.
-
-```shell
-usage: ./scripts/backup.sh HOSTNAME backup.zip
-```
-
-## Random Info
-
-* **On a rpi0w, it'll take approximately 30 minutes to load the AI**.
-* `/var/log/pwnagotchi.log` is your friend.
-* if connected to a laptop via usb data port, with internet connectivity shared, magic things will happen.
-* checkout the `ui.video` section of the `config.yml` - if you don't want to use a display, you can connect to it with the browser and a cable.
-* If you get `[FAILED] Failed to start Remount Root and Kernel File Systems.` while booting pwnagotchi, make sure the `PARTUUID`s for `rootfs` and `boot` partitions are the same in `/etc/fstab`. Use `sudo blkid` to find those values when you are using `create_sibling.sh`.
diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py
index 9905b0f..caa4bc9 100644
--- a/pwnagotchi/__init__.py
+++ b/pwnagotchi/__init__.py
@@ -1,6 +1,10 @@
 import subprocess
+import os
+import logging
+import time
+import pwnagotchi.ui.view as view
 
-version = '1.0.0plz4'
+version = '1.1.0b'
 
 _name = None
 
@@ -13,6 +17,11 @@ def name():
     return _name
 
 
+def uptime():
+    with open('/proc/uptime') as fp:
+        return int(fp.read().split('.')[0])
+
+
 def mem_usage():
     out = subprocess.getoutput("free -m")
     for line in out.split("\n"):
@@ -46,3 +55,19 @@ def temperature(celsius=True):
         temp = int(fp.read().strip())
     c = int(temp / 1000)
     return c if celsius else ((c * (9 / 5)) + 32)
+
+
+def shutdown():
+    logging.warning("shutting down ...")
+    if view.ROOT:
+        view.ROOT.on_shutdown()
+        # give it some time to refresh the ui
+        time.sleep(5)
+    os.system("sync")
+    os.system("halt")
+
+
+def reboot():
+    logging.warning("rebooting ...")
+    os.system("sync")
+    os.system("shutdown -r now")
diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py
index 98f8aab..16fa722 100644
--- a/pwnagotchi/agent.py
+++ b/pwnagotchi/agent.py
@@ -2,13 +2,13 @@ import time
 import json
 import os
 import re
-import socket
-from datetime import datetime
 import logging
 import _thread
 
+import pwnagotchi
 import pwnagotchi.utils as utils
 import pwnagotchi.plugins as plugins
+from pwnagotchi.log import LastSession
 from pwnagotchi.bettercap import Client
 from pwnagotchi.mesh.utils import AsyncAdvertiser
 from pwnagotchi.ai.train import AsyncTrainer
@@ -17,13 +17,13 @@ RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
 
 
 class Agent(Client, AsyncAdvertiser, AsyncTrainer):
-    def __init__(self, view, config):
+    def __init__(self, view, config, keypair):
         Client.__init__(self, config['bettercap']['hostname'],
                         config['bettercap']['scheme'],
                         config['bettercap']['port'],
                         config['bettercap']['username'],
                         config['bettercap']['password'])
-        AsyncAdvertiser.__init__(self, config, view)
+        AsyncAdvertiser.__init__(self, config, view, keypair)
         AsyncTrainer.__init__(self, config)
 
         self._started_at = time.time()
@@ -35,19 +35,17 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         self._last_pwnd = None
         self._history = {}
         self._handshakes = {}
+        self.last_session = LastSession(self._config)
 
-    @staticmethod
-    def is_connected():
-        try:
-            socket.create_connection(("www.google.com", 80))
-            return True
-        except OSError:
-            pass
-        return False
+        if not os.path.exists(config['bettercap']['handshakes']):
+            os.makedirs(config['bettercap']['handshakes'])
 
     def config(self):
         return self._config
 
+    def view(self):
+        return self._view
+
     def supported_channels(self):
         return self._supported_channels
 
@@ -137,8 +135,18 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
 
         self.start_advertising()
 
+    def _wait_bettercap(self):
+        while True:
+            try:
+                s = self.session()
+                return
+            except:
+                logging.info("waiting for bettercap API to be available ...")
+                time.sleep(1)
+
     def start(self):
         self.start_ai()
+        self._wait_bettercap()
         self.setup_events()
         self.set_starting()
         self.start_monitor_mode()
@@ -152,23 +160,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         self._view.wait(t, sleeping)
         self._epoch.track(sleep=True, inc=t)
 
-    def check_channels(self, channels):
-        busy_channels = [ch for ch, aps in channels]
-        # if we're hopping and no filter is configured
-        if self._config['personality']['channels'] == [] and self._config['main']['filter'] is None:
-            # check if any of the non overlapping channels is free
-            for ch in self._epoch.non_overlapping_channels:
-                if ch not in busy_channels:
-                    self._epoch.non_overlapping_channels[ch] += 1
-                    logging.info("channel %d is free from %d epochs" % (ch, self._epoch.non_overlapping_channels[ch]))
-                elif self._epoch.non_overlapping_channels[ch] > 0:
-                    self._epoch.non_overlapping_channels[ch] -= 1
-            # report any channel that has been free for at least 3 epochs
-            for ch, num_epochs_free in self._epoch.non_overlapping_channels.items():
-                if num_epochs_free >= 3:
-                    logging.info("channel %d has been free for %d epochs" % (ch, num_epochs_free))
-                    self.set_free_channel(ch)
-
     def recon(self):
         recon_time = self._config['personality']['recon_time']
         max_inactive = self._config['personality']['max_inactive_scale']
@@ -201,7 +192,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 ())
+        self._epoch.observe(aps, list(self._peers.values()))
         return self._access_points
 
     def get_access_points(self):
@@ -209,6 +200,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         aps = []
         try:
             s = self.session()
+            plugins.on("unfiltered_ap_list", self, s['wifi']['aps'])
             for ap in s['wifi']['aps']:
                 if ap['hostname'] not in whitelist:
                     if self._filter_included(ap):
@@ -250,9 +242,9 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         return None
 
     def _update_uptime(self, s):
-        secs = time.time() - self._started_at
+        secs = pwnagotchi.uptime()
         self._view.set('uptime', utils.secs_to_hhmmss(secs))
-        self._view.set('epoch', '%04d' % self._epoch.epoch)
+        # self._view.set('epoch', '%04d' % self._epoch.epoch)
 
     def _update_counters(self):
         tot_aps = len(self._access_points)
@@ -282,21 +274,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
         if new_shakes > 0:
             self._view.on_handshakes(new_shakes)
 
-    def _update_advertisement(self, s):
-        run_handshakes = len(self._handshakes)
-        tot_handshakes = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
-        started = s['started_at'].split('.')[0]
-        started = datetime.strptime(started, '%Y-%m-%dT%H:%M:%S')
-        started = time.mktime(started.timetuple())
-        self._advertiser.update({ \
-            'pwnd_run': run_handshakes,
-            'pwnd_tot': tot_handshakes,
-            'uptime': time.time() - started,
-            'epoch': self._epoch.epoch})
-
     def _update_peers(self):
-        peer = self._advertiser.closest_peer()
-        self._view.set_closest_peer(peer)
+        self._view.set_closest_peer(self._closest_peer, len(self._peers))
 
     def _save_recovery_data(self):
         logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
@@ -338,16 +317,15 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
             time.sleep(1)
 
             new_shakes = 0
-            s = self.session()
-            self._update_uptime(s)
-
-            if self._advertiser is not None:
-                self._update_advertisement(s)
-                self._update_peers()
-
-            self._update_counters()
 
             try:
+                s = self.session()
+                self._update_uptime(s)
+
+                self._update_advertisement(s)
+                self._update_peers()
+                self._update_counters()
+
                 for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
                     filename = h['data']['file']
                     sta_mac = h['data']['station']
@@ -373,7 +351,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
                             plugins.on('handshake', self, filename, ap, sta)
 
             except Exception as e:
-                logging.exception("error")
+                logging.error("error: %s" % e)
 
             finally:
                 self._update_handshakes(new_shakes)
@@ -514,9 +492,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
     def _reboot(self):
         self.set_rebooting()
         self._save_recovery_data()
-        logging.warning("rebooting the system ...")
-        os.system("/usr/bin/sync")
-        os.system("/usr/sbin/shutdown -r now")
+        pwnagotchi.reboot()
 
     def next_epoch(self):
         was_stale = self.is_stale()
diff --git a/pwnagotchi/ai/__init__.py b/pwnagotchi/ai/__init__.py
index 359a38c..190025f 100644
--- a/pwnagotchi/ai/__init__.py
+++ b/pwnagotchi/ai/__init__.py
@@ -39,4 +39,4 @@ def load(config, agent, epoch, from_disk=True):
         for key, value in config['params'].items():
             logging.info("      %s: %s" % (key, value))
 
-    return a2c
+    return a2c
\ No newline at end of file
diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml
index cceac71..6e91965 100644
--- a/pwnagotchi/defaults.yml
+++ b/pwnagotchi/defaults.yml
@@ -1,45 +1,82 @@
+# WARNING WARNING WARNING WARNING
+#
+# This file is recreated with default settings on every pwnagotchi restart,
+# use /etc/pwnagotchi/config.yml to configure this unit.
+#
+#
 # main algorithm configuration
 main:
-    # currently implemented: en (default), de, el, fr, it, mk, nl, ru, se
+    # currently implemented: en (default), de, el, fr, it, mk, nl, ru, se, pt-BR, es, pt
     lang: en
     # custom plugins path, if null only default plugins with be loaded
     custom_plugins:
     # which plugins to load and enable
     plugins:
-      auto-update:
-        enabled: false
-        interval: 1 # every day
-      auto-backup:
-        enabled: false
-        interval: 1 # every day
-        files:
-          - /root/brain.nn
-          - /root/brain.json
-          - /root/custom.yml
-          - /root/handshakes
-          - /etc/ssh
-          - /etc/hostname
-          - /etc/hosts
-          - /etc/motd
-          - /var/log/pwnagotchi.log
-        commands:
-          - 'tar czf /tmp/backup.tar.gz {files}'
-          - 'scp /tmp/backup.tar.gz pwnagotchi@10.0.0.1:/home/pwnagotchi/backups/backup-$(date +%s).tar.gz'
-      gps:
-        enabled: false
-      twitter:
-        enabled: false
-        consumer_key: aaa
-        consumer_secret: aaa
-        access_token_key: aaa
-        access_token_secret: aaa
-      onlinehashcrack:
-        enabled: false
-        email: ~
-      wpa-sec:
-        enabled: false
-        api_key: ~
+        grid:
+            enabled: true
+            report: false # don't report pwned networks by default!
+            exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
+                - YourHomeNetworkHere
 
+        auto-update:
+            enabled: false
+            interval: 12 # every 12 hours
+            install: true # if false, it will only warn that updates are available, if true it will install them
+
+        auto-backup:
+            enabled: false
+            interval: 1 # every day
+            files:
+                - /root/brain.nn
+                - /root/brain.json
+                - /root/.api-report.json
+                - /root/handshakes/
+                - /root/peers/
+                - /etc/pwnagotchi/
+                - /var/log/pwnagotchi.log
+            commands:
+                - 'tar czf /root/pwnagotchi-backup.tar.gz {files}'
+        net-pos:
+            enabled: false
+            api_key: 'test'
+        gps:
+            enabled: false
+            speed: 19200
+            device: /dev/ttyUSB0
+        twitter:
+            enabled: false
+            consumer_key: aaa
+            consumer_secret: aaa
+            access_token_key: aaa
+            access_token_secret: aaa
+        onlinehashcrack:
+            enabled: false
+            email: ~
+        wpa-sec:
+            enabled: false
+            api_key: ~
+            api_url: "https://wpa-sec.stanev.org"
+        wigle:
+            enabled: false
+            api_key: ~
+        screen_refresh:
+            enabled: false
+            refresh_interval: 50
+        quickdic:
+            enabled: false
+            wordlist_folder: /opt/wordlists/
+        AircrackOnly:
+            enabled: false
+        bt-tether:
+            enabled: false # if you want to use this, set ui.display.video.address to 0.0.0.0
+            mac: ~ # mac of your bluetooth device
+            ip: '192.168.44.44' # ip from which your pwnagotchi should be reachable
+            netmask: 24
+            interval: 1 # check every x minutes for device
+            share_internet: false
+        memtemp: # Display memory usage, cpu load and cpu temperature on screen
+            enabled: false
+            orientation: horizontal # horizontal/vertical
     # monitor interface to use
     iface: mon0
     # command to run to bring the mon interface up in case it's not up already
@@ -56,8 +93,6 @@ main:
         - ANOTHER_EXAMPLE_NETWORK
     # if not null, filter access points by this regular expression
     filter: null
-    # cryptographic key for identity
-    pubkey: /etc/ssh/ssh_host_rsa_key.pub
 
 ai:
     # if false, only the default 'personality' will be used
@@ -129,6 +164,26 @@ personality:
 
 # ui configuration
 ui:
+    # here you can customize the faces
+    faces:
+        look_r: '( ⚆_⚆)'
+        look_l: '(☉_☉ )'
+        sleep: '(⇀‿‿↼)'
+        sleep2: '(≖‿‿≖)'
+        awake: '(◕‿‿◕)'
+        bored: '(-__-)'
+        intense: '(°▃▃°)'
+        cool: '(⌐■_■)'
+        happy: '(•‿‿•)'
+        excited: '(ᵔ◡◡ᵔ)'
+        motivated: '(☼‿‿☼)'
+        demotivated: '(≖__≖)'
+        smart: '(✜‿‿✜)'
+        lonely: '(ب__ب)'
+        sad: '(╥☁╥ )'
+        friend: '(♥‿‿♥)'
+        broken: '(☓‿‿☓)'
+        debug: '(#__#)'
     # ePaper display can update every 3 secs anyway, set to 0 to only refresh for major data changes
     # IMPORTANT: The lifespan of an eINK display depends on the cumulative amount of refreshes. If you want to
     # preserve your display over time, you should set this value to 0.0 so that the display will be refreshed only
@@ -137,14 +192,19 @@ ui:
     display:
         enabled: true
         rotation: 180
-        # Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2
+        # Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, waveshare27inch
         type: 'waveshare_2'
         # Possible options red/yellow/black (black used for monocromatic displays)
         color: 'black'
         video:
             enabled: true
-            address: '10.0.0.2'
+            address: '0.0.0.0'
+            origin: '*'
             port: 8080
+            # command to be executed when a new png frame is available
+            # for instance, to use with framebuffer based displays:
+            # on_frame: 'fbi --noverbose -a -d /dev/fb1 -T 1 /root/pwnagotchi.png > /dev/null 2>&1'
+            on_frame: ''
 
 
 # bettercap rest api configuration
diff --git a/pwnagotchi/grid.py b/pwnagotchi/grid.py
new file mode 100644
index 0000000..93bc08a
--- /dev/null
+++ b/pwnagotchi/grid.py
@@ -0,0 +1,97 @@
+import subprocess
+import socket
+import requests
+import json
+import logging
+
+import pwnagotchi
+
+# pwngrid-peer is running on port 8666
+API_ADDRESS = "http://127.0.0.1:8666/api/v1"
+
+
+def is_connected():
+    try:
+        socket.create_connection(("www.google.com", 80))
+        return True
+    except OSError:
+        pass
+    return False
+
+
+def call(path, obj=None):
+    url = '%s%s' % (API_ADDRESS, path)
+    if obj is None:
+        r = requests.get(url, headers=None)
+    else:
+        r = requests.post(url, headers=None, json=obj)
+
+    if r.status_code != 200:
+        raise Exception("(status %d) %s" % (r.status_code, r.text))
+    return r.json()
+
+
+def advertise(enabled=True):
+    return call("/mesh/%s" % 'true' if enabled else 'false')
+
+
+def set_advertisement_data(data):
+    return call("/mesh/data", obj=data)
+
+
+def peers():
+    return call("/mesh/peers")
+
+
+def closest_peer():
+    all = peers()
+    return all[0] if len(all) else None
+
+
+def update_data(last_session):
+    brain = {}
+    try:
+        with open('/root/brain.json') as fp:
+            brain = json.load(fp)
+    except:
+        pass
+
+    data = {
+        'session': {
+            'duration': last_session.duration,
+            'epochs': last_session.epochs,
+            'train_epochs': last_session.train_epochs,
+            'avg_reward': last_session.avg_reward,
+            'min_reward': last_session.min_reward,
+            'max_reward': last_session.max_reward,
+            'deauthed': last_session.deauthed,
+            'associated': last_session.associated,
+            'handshakes': last_session.handshakes,
+            'peers': last_session.peers,
+        },
+        'uname': subprocess.getoutput("uname -a"),
+        'brain': brain,
+        'version': pwnagotchi.version
+    }
+
+    logging.debug("updating grid data: %s" % data)
+
+    call("/data", data)
+
+
+def report_ap(essid, bssid):
+    try:
+        call("/report/ap", {
+            'essid': essid,
+            'bssid': bssid,
+        })
+        return True
+    except Exception as e:
+        logging.exception("error while reporting ap %s(%s)" % (essid, bssid))
+
+    return False
+
+
+def inbox(page=1, with_pager=False):
+    obj = call("/inbox?p=%d" % page)
+    return obj["messages"] if not with_pager else obj
diff --git a/pwnagotchi/identity.py b/pwnagotchi/identity.py
new file mode 100644
index 0000000..0ac5935
--- /dev/null
+++ b/pwnagotchi/identity.py
@@ -0,0 +1,71 @@
+from Crypto.Signature import PKCS1_PSS
+from Crypto.PublicKey import RSA
+import Crypto.Hash.SHA256 as SHA256
+import base64
+import hashlib
+import os
+import logging
+
+DefaultPath = "/etc/pwnagotchi/"
+
+
+class KeyPair(object):
+    def __init__(self, path=DefaultPath, view=None):
+        self.path = path
+        self.priv_path = os.path.join(path, "id_rsa")
+        self.priv_key = None
+        self.pub_path = "%s.pub" % self.priv_path
+        self.pub_key = None
+        self.fingerprint_path = os.path.join(path, "fingerprint")
+        self._view = view
+
+        if not os.path.exists(self.path):
+            os.makedirs(self.path)
+
+        while True:
+            # first time, generate new keys
+            if not os.path.exists(self.priv_path) or not os.path.exists(self.pub_path):
+                self._view.on_keys_generation()
+                logging.info("generating %s ..." % self.priv_path)
+                os.system("pwngrid -generate -keys '%s'" % self.path)
+
+            # load keys: they might be corrupted if the unit has been turned off during the generation, in this case
+            # the exception will remove the files and go back at the beginning of this loop.
+            try:
+                with open(self.priv_path) as fp:
+                    self.priv_key = RSA.importKey(fp.read())
+
+                with open(self.pub_path) as fp:
+                    self.pub_key = RSA.importKey(fp.read())
+                    self.pub_key_pem = self.pub_key.exportKey('PEM').decode("ascii")
+                    # python is special
+                    if 'RSA PUBLIC KEY' not in self.pub_key_pem:
+                        self.pub_key_pem = self.pub_key_pem.replace('PUBLIC KEY', 'RSA PUBLIC KEY')
+
+                pem_ascii = self.pub_key_pem.encode("ascii")
+
+                self.pub_key_pem_b64 = base64.b64encode(pem_ascii).decode("ascii")
+                self.fingerprint = hashlib.sha256(pem_ascii).hexdigest()
+
+                with open(self.fingerprint_path, 'w+t') as fp:
+                    fp.write(self.fingerprint)
+
+                # no exception, keys loaded correctly.
+                self._view.on_starting()
+                return
+
+            except Exception as e:
+                # if we're here, loading the keys broke something ...
+                logging.exception("error loading keys, maybe corrupted, deleting and regenerating ...")
+                try:
+                    os.remove(self.priv_path)
+                    os.remove(self.pub_path)
+                except:
+                    pass
+
+    def sign(self, message):
+        hasher = SHA256.new(message.encode("ascii"))
+        signer = PKCS1_PSS.new(self.priv_key, saltLen=16)
+        signature = signer.sign(hasher)
+        signature_b64 = base64.b64encode(signature).decode("ascii")
+        return signature, signature_b64
diff --git a/pwnagotchi/locale/de/LC_MESSAGES/voice.mo b/pwnagotchi/locale/de/LC_MESSAGES/voice.mo
index fd5058b..8944e85 100644
Binary files a/pwnagotchi/locale/de/LC_MESSAGES/voice.mo and b/pwnagotchi/locale/de/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/de/LC_MESSAGES/voice.po b/pwnagotchi/locale/de/LC_MESSAGES/voice.po
index fa9afdf..8b96f33 100644
--- a/pwnagotchi/locale/de/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/de/LC_MESSAGES/voice.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: 0.0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-05 14:10+0200\n"
+"POT-Creation-Date: 2019-10-09 17:42+0200\n"
 "PO-Revision-Date: 2019-09-29 14:00+0200\n"
 "Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
 "Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
@@ -121,6 +121,12 @@ msgstr ""
 msgid "ZzzZzzz ({secs}s)"
 msgstr ""
 
+msgid "Good night."
+msgstr "Gute Nacht."
+
+msgid "Zzz"
+msgstr ""
+
 #, python-brace-format
 msgid "Waiting for {secs}s ..."
 msgstr "Warte für {secs}s ..."
@@ -139,7 +145,7 @@ msgstr "Verbinde mit {what}"
 
 #, python-brace-format
 msgid "Yo {what}!"
-msgstr ""
+msgstr "Jo {what}!"
 
 #, python-brace-format
 msgid "Just decided that {mac} needs no WiFi!"
diff --git a/pwnagotchi/locale/el/LC_MESSAGES/voice.po b/pwnagotchi/locale/el/LC_MESSAGES/voice.po
index f113ce1..b1cfa69 100644
--- a/pwnagotchi/locale/el/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/el/LC_MESSAGES/voice.po
@@ -33,11 +33,11 @@ msgid "AI ready."
 msgstr "ΤΝ έτοιμη."
 
 msgid "The neural network is ready."
-msgstr "Το νευρωνικό δίκτυοείναι έτοιμο."
+msgstr "Το νευρωνικό δίκτυο είναι έτοιμο."
 
 #, python-brace-format
 msgid "Hey, channel {channel} is free! Your AP will say thanks."
-msgstr "Ε, το κανάλι {channel} είναιελεύθερο! Το AP σου θαείναι ευγνώμων."
+msgstr "Ε, το κανάλι {channel} είναιελεύθερο! Το AP σου θα είναι ευγνώμων."
 
 msgid "I'm bored ..."
 msgstr "Βαριέμαι ..."
diff --git a/pwnagotchi/locale/es/LC_MESSAGES/voice.mo b/pwnagotchi/locale/es/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..00ee652
Binary files /dev/null and b/pwnagotchi/locale/es/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/es/LC_MESSAGES/voice.po b/pwnagotchi/locale/es/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..d9006da
--- /dev/null
+++ b/pwnagotchi/locale/es/LC_MESSAGES/voice.po
@@ -0,0 +1,214 @@
+# pwnagotchi voice data
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR diegopastor <dpastor29@alumnos.uaq.mx>, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-09 17:42+0200\n"
+"PO-Revision-Date: 2019-10-09 21:07+0000\n"
+"Last-Translator: diegopastor <dpastor29@alumnos.uaq.mx>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: spanish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr "ZzzzZZzzzzZzzz"
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Hola, soy Pwnagotchi! Empezando ..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "Nuevo día, nueva cazería, nuevos pwns!"
+
+msgid "Hack the Planet!"
+msgstr "Hackea el planeta!"
+
+msgid "AI ready."
+msgstr "IA lista."
+
+msgid "The neural network is ready."
+msgstr "La red neuronal está lista."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Oye, el canal {channel} está libre! Tú AP lo agradecerá."
+
+msgid "I'm bored ..."
+msgstr "Estoy aburrido ..."
+
+msgid "Let's go for a walk!"
+msgstr "Vamos por un paseo!"
+
+msgid "This is the best day of my life!"
+msgstr "Este es el mejor día de mi vida!"
+
+msgid "Shitty day :/"
+msgstr "Día de mierda :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Estoy extremadamente aburrido ..."
+
+msgid "I'm very sad ..."
+msgstr "Estoy muy triste ..."
+
+msgid "I'm sad"
+msgstr "Estoy triste."
+
+msgid "I'm living the life!"
+msgstr "Estoy viviendo la vida!"
+
+msgid "I pwn therefore I am."
+msgstr "Pwneo, por lo tanto, existo"
+
+msgid "So many networks!!!"
+msgstr "Cuantas redes!!!"
+
+msgid "I'm having so much fun!"
+msgstr "Me estoy divirtiendo mucho!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "Mi único crimen es la curiosidad ..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you. {name}"
+msgstr "Hola {name}! encantado de conocerte."
+
+#, python-brace-format
+msgid "Unit {name} is nearby! {name}"
+msgstr "La unidad {name} está cerca!"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Uhm ... adiós {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name} se fue ..."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Uy ... {name} se fue"
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name} perdido!"
+
+msgid "Missed!"
+msgstr "Perdido!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Nadie quiere jugar conmigo ..."
+
+msgid "I feel so alone ..."
+msgstr "Me siento tan solo ..."
+
+msgid "Where's everybody?!"
+msgstr "Dónde está todo el mundo?"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Tomándo una siesta por {secs}s ..."
+
+msgid "Zzzzz"
+msgstr "Zzzzz"
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "ZzzZzzz ({secs}s)"
+
+msgid "Good night."
+msgstr "Buenas noches."
+
+msgid "Zzz"
+msgstr "Zzz"
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Esperando {secs}s .."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Mirando al rededor ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Oye {what} seamos amigos!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "Asociando a {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Ey {what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Acabo de decidir que {mac} no necesita WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "Desautenticando a {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "Expulsando y banneando a {mac}!"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Genial, obtuvimos {num} nuevo{plural} handshake{plural}!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "Oops, algo salió mal ... Reiniciándo ..."
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Expulsamos {num} estaciones\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Hicimos {num} nuevos amigos\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Obtuvimos {num} handshakes\n"
+
+msgid "Met 1 peer"
+msgstr "Conocí 1 igual"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Conocí {num} iguales"
+
+#, 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 ""
+"He estado pwneando por {duration} y expulsé {deauthed} clientes! También conocí"
+"{associated} nuevos amigos y me comí {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "horas"
+
+msgid "minutes"
+msgstr "minutos"
+
+msgid "seconds"
+msgstr "segundos"
+
+msgid "hour"
+msgstr "hora"
+
+msgid "minute"
+msgstr "minuto"
+
+msgid "second"
+msgstr "segundo"
diff --git a/pwnagotchi/locale/fr/LC_MESSAGES/voice.po b/pwnagotchi/locale/fr/LC_MESSAGES/voice.po
index 1172f7f..26f4678 100644
--- a/pwnagotchi/locale/fr/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/fr/LC_MESSAGES/voice.po
@@ -25,20 +25,20 @@ msgid "Hi, I'm Pwnagotchi! Starting ..."
 msgstr "Bonjour, je suis Pwnagotchi! Démarrage ..."
 
 msgid "New day, new hunt, new pwns!"
-msgstr "Nouvelle journée, nouvelle chasse, nouveau pwns!"
+msgstr "Nouveau jour, nouvelle chasse, nouveaux pwns !"
 
 msgid "Hack the Planet!"
 msgstr "Hack la planète!"
 
 msgid "AI ready."
-msgstr "IA prête."
+msgstr "L'IA est prête."
 
 msgid "The neural network is ready."
 msgstr "Le réseau neuronal est prêt."
 
 #, python-brace-format
 msgid "Hey, channel {channel} is free! Your AP will say thanks."
-msgstr "Hey, le channel {channel} est libre! Ton AP va dis merci."
+msgstr "Hey, le channel {channel} est libre! Ton point d'accès va te remercier."
 
 msgid "I'm bored ..."
 msgstr "Je m'ennuie ..."
@@ -68,17 +68,17 @@ msgid "I pwn therefore I am."
 msgstr "Je pwn donc je suis."
 
 msgid "So many networks!!!"
-msgstr "Autant de réseaux!!!"
+msgstr "Tellement de réseaux!!!"
 
 msgid "I'm having so much fun!"
 msgstr "Je m'amuse tellement!"
 
 msgid "My crime is that of curiosity ..."
-msgstr "Mon crime est celui de la curiosité ..."
+msgstr "Mon crime, c'est la curiosité ..."
 
 #, python-brace-format
 msgid "Hello {name}! Nice to meet you. {name}"
-msgstr "Bonjour {name}! Ravis de te rencontrer. {name}"
+msgstr "Bonjour {name}! Ravi de te rencontrer. {name}"
 
 #, python-brace-format
 msgid "Unit {name} is nearby! {name}"
@@ -145,7 +145,7 @@ msgstr ""
 
 #, python-brace-format
 msgid "Just decided that {mac} needs no WiFi!"
-msgstr "Décidé à l'instant que {mac} n'a pas besoin de WiFi!"
+msgstr "Je viens de décider que {mac} n'a pas besoin de WiFi!"
 
 #, python-brace-format
 msgid "Deauthenticating {mac}"
@@ -153,11 +153,11 @@ msgstr "Désauthentification de {mac}"
 
 #, python-brace-format
 msgid "Kickbanning {mac}!"
-msgstr ""
+msgstr "Je kick et je bannis {mac}!"
 
 #, python-brace-format
 msgid "Cool, we got {num} new handshake{plural}!"
-msgstr "Cool, nous avons {num} nouveaux handshake{plural}!"
+msgstr "Cool, on a {num} nouveaux handshake{plural}!"
 
 msgid "Ops, something went wrong ... Rebooting ..."
 msgstr "Oups, quelque chose s'est mal passé ... Redémarrage ..."
@@ -188,23 +188,23 @@ msgid ""
 "#pwnlog #pwnlife #hacktheplanet #skynet"
 msgstr ""
 "J'ai pwn durant {duration} et kick {deauthed} clients! J'ai aussi rencontré "
-"{associated} nouveaux amis and mangé {handshakes} handshakes! #pwnagotchi "
+"{associated} nouveaux amis et dévoré {handshakes} handshakes! #pwnagotchi "
 "#pwnlog #pwnlife #hacktheplanet #skynet"
 
 msgid "hours"
-msgstr ""
+msgstr "heures"
 
 msgid "minutes"
-msgstr ""
+msgstr "minutes"
 
 msgid "seconds"
-msgstr ""
+msgstr "secondes"
 
 msgid "hour"
-msgstr ""
+msgstr "heure"
 
 msgid "minute"
-msgstr ""
+msgstr "minute"
 
 msgid "second"
-msgstr ""
+msgstr "seconde"
diff --git a/pwnagotchi/locale/ga/LC_MESSAGES/voice.mo b/pwnagotchi/locale/ga/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..b86e7f1
Binary files /dev/null and b/pwnagotchi/locale/ga/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/ga/LC_MESSAGES/voice.po b/pwnagotchi/locale/ga/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..a09b9f7
--- /dev/null
+++ b/pwnagotchi/locale/ga/LC_MESSAGES/voice.po
@@ -0,0 +1,215 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-09 17:42+0200\n"
+"PO-Revision-Date: 2019-10-15 23:46+0100\n"
+"Language: ga\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.2.4\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Dia Duit, Pwnagotchi is ainm dom! Ag tosú ..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "Lá nua, seilg nua, pwns nua!"
+
+msgid "Hack the Planet!"
+msgstr "Haic An Phláinéid!"
+
+msgid "AI ready."
+msgstr "AI réidh."
+
+msgid "The neural network is ready."
+msgstr "Tá an líonra néarach réidh."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Hé, tá cainéal {channel} ar fail! Déarfaidh do PR go raibh maith agat."
+
+msgid "I'm bored ..."
+msgstr "Tá leadrán orm ..."
+
+msgid "Let's go for a walk!"
+msgstr "Siúil liom, le do thoil!"
+
+msgid "This is the best day of my life!"
+msgstr "Tá sé an lá is fearr i mo shaol!"
+
+msgid "Shitty day :/"
+msgstr "Tá lá damanta agam :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Tá mé ag dul as mo mheabhair le leadrán ..."
+
+msgid "I'm very sad ..."
+msgstr "Ta brón an domhain orm ..."
+
+msgid "I'm sad"
+msgstr "Tá brón orm"
+
+msgid "I'm living the life!"
+msgstr "Tá an saol ar a thoil agam!"
+
+msgid "I pwn therefore I am."
+msgstr "Déanaim pwnáil, dá bhrí sin táim ann."
+
+msgid "So many networks!!!"
+msgstr "Gréasáin - Tá an iliomad acu ann!!!"
+
+msgid "I'm having so much fun!"
+msgstr "Tá craic iontach agam!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "Ní haon pheaca é bheith fiosrach ..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you. {name}"
+msgstr "Dia Duit {name}! Is deas bualadh leat. {name}"
+
+#, python-brace-format
+msgid "Unit {name} is nearby! {name}"
+msgstr "Aonad {name} in aice láimhe! {name}"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Uhm... slán leat {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "Tá {name} imithe ..."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Hoips … Tá {name} imithe."
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "Chaill mé ar {name}!"
+
+msgid "Missed!"
+msgstr "Chaill mé é sin !"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Níl aon duine ag iarraidh imirt liom ..."
+
+msgid "I feel so alone ..."
+msgstr "Tá uaigneas an domhain orm ..."
+
+msgid "Where's everybody?!"
+msgstr "Cá bhfuil gach duine?!"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Néal a chodladh ar {secs}s ..."
+
+msgid "Zzzzz"
+msgstr ""
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+msgid "Good night."
+msgstr "Oíche mhaith."
+
+msgid "Zzz"
+msgstr ""
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Fan ar {secs}s ..."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Ag amharc uaim ar ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Hé {what} déanaimis síocháin!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "Ag coinneáil le {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Hé {what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Tá cinneadh déanta agam. Níl {mac} sin de dhíth air WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "Bain fíordheimhniúde {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "Chiceáil mé agus cosc mé ar {mac}!"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Go hiontach, fuaireamar {num} handshake{plural}!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "Hoips...Tháinig ainghléas éigin..."
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "{num} stáisiún kick\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Rinne mé {num} cairde nua\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Fuair me {num} cumarsáid thionscantach\n"
+
+msgid "Met 1 peer"
+msgstr "Bhuail mé piara amháin"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Bhuail me {num} piara"
+
+#, 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 ""
+"Bhí me ag pwnáil ar {duration} agus chiceáil me ar {deauthed} cliaint! Chomh "
+"maith, bhuail me {associated} cairde nua and d'ith mé {handshakes}! "
+"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "uair on chloig"
+
+msgid "minutes"
+msgstr "nóiméad"
+
+msgid "seconds"
+msgstr "soicind"
+
+msgid "hour"
+msgstr "uair an chloig"
+
+msgid "minute"
+msgstr "nóiméad"
+
+msgid "second"
+msgstr "soicind"
diff --git a/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo b/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..be728e2
Binary files /dev/null and b/pwnagotchi/locale/jp/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/jp/LC_MESSAGES/voice.po b/pwnagotchi/locale/jp/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..912e89d
--- /dev/null
+++ b/pwnagotchi/locale/jp/LC_MESSAGES/voice.po
@@ -0,0 +1,212 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR 24534649+wytshadow@users.noreply.github.com, 2019.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-16 15:05+0200\n"
+"PO-Revision-Date: 2019-10-16 15:05+0200\n"
+"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
+"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
+"Language: jp\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr "すやすや〜"
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "こんにちは、ポウナゴッチです!始めている。。。"
+
+msgid "New day, new hunt, new pwns!"
+msgstr ""
+
+msgid "Hack the Planet!"
+msgstr "ハックザプラネット!"
+
+msgid "AI ready."
+msgstr "人工知能の準備ができました。"
+
+msgid "The neural network is ready."
+msgstr "ニューラルネットワークの準備ができました。"
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "ねえ、チャンネル{channel}は無料です! キミのAPは感謝を言います。"
+
+msgid "I'm bored ..."
+msgstr "退屈です。。。"
+
+msgid "Let's go for a walk!"
+msgstr "散歩に行きましょう!"
+
+msgid "This is the best day of my life!"
+msgstr "今日は私の人生で最高の日です!"
+
+msgid "Shitty day :/"
+msgstr ""
+
+msgid "I'm extremely bored ..."
+msgstr "とても退屈です。"
+
+msgid "I'm very sad ..."
+msgstr "とても悲しいです。。。"
+
+msgid "I'm sad"
+msgstr "悲しいです。"
+
+msgid "I'm living the life!"
+msgstr "人生を生きている!"
+
+msgid "I pwn therefore I am."
+msgstr ""
+
+msgid "So many networks!!!"
+msgstr "たくさんネットワークがある!!!"
+
+msgid "I'm having so much fun!"
+msgstr "とても楽しんでいます!"
+
+msgid "My crime is that of curiosity ..."
+msgstr ""
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you. {name}"
+msgstr "こんにちは{name}!初めまして。{name}"
+
+#, python-brace-format
+msgid "Unit {name} is nearby! {name}"
+msgstr ""
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "ええと。。。さようなら{name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name}がなくなった。。。"
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "おっと。。。{name}がなくなった。"
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name}逃した!"
+
+msgid "Missed!"
+msgstr "逃した!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "誰も僕と一緒にプレーしたくない。。。"
+
+msgid "I feel so alone ..."
+msgstr "僕は孤独を感じる。。。"
+
+msgid "Where's everybody?!"
+msgstr "みんなどこ?!"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "{secs}寝ている。"
+
+msgid "Zzzzz"
+msgstr "すや〜"
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "すやすや〜 ({secs})"
+
+msgid "Good night."
+msgstr "お休みなさい。"
+
+msgid "Zzz"
+msgstr "す〜"
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "{secs}を待っている。。。"
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "{secs}を探している。"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "ちょっと{what}友だちになりましょう!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr ""
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "よー{what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr ""
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr ""
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr ""
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "よし、{num}新しいハンドシェイクがある!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "おっと!何かが間違っていた。。。リブートしている。。。"
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr ""
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "{num}人の新しい友達を作りました\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "{num}ハンドシェイクがある。\n"
+
+msgid "Met 1 peer"
+msgstr "1人の仲間を会いました。"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "{num}人の仲間を会いました。"
+
+#, 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 ""
+
+msgid "hours"
+msgstr "時間"
+
+msgid "minutes"
+msgstr "分"
+
+msgid "seconds"
+msgstr "秒"
+
+msgid "hour"
+msgstr "時間"
+
+msgid "minute"
+msgstr "分"
+
+msgid "second"
+msgstr "秒"
diff --git a/pwnagotchi/locale/pl/LC_MESSAGES/voice.mo b/pwnagotchi/locale/pl/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..f762e95
Binary files /dev/null and b/pwnagotchi/locale/pl/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/pl/LC_MESSAGES/voice.po b/pwnagotchi/locale/pl/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..70fd08a
--- /dev/null
+++ b/pwnagotchi/locale/pl/LC_MESSAGES/voice.po
@@ -0,0 +1,217 @@
+# Polish voice data for pwnagotchi.
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# szymex73 <szymex73@gmail.com>, 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.2\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-21 08:39+0200\n"
+"PO-Revision-Date: 2019-10-21 10:55+0200\n"
+"Last-Translator: gkrs <457603+gkrs@users.noreply.github.com>\n"
+"Language-Team: PL <457603+gkrs@users.noreply.github.com>\n"
+"Language: polish\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Hej, jestem Pwnagotchi! Uruchamianie ..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "Nowy dzień, nowe łowy, nowe pwny!"
+
+msgid "Hack the Planet!"
+msgstr "Hakujmy planetę!"
+
+msgid "AI ready."
+msgstr "SI gotowa."
+
+msgid "The neural network is ready."
+msgstr "Sieć neuronowa jest gotowa."
+
+msgid "Generating keys, do not turn off ..."
+msgstr "Generuję klucze, nie wyłączaj ..."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Hej, kanał {channel} jest wolny! Twój AP będzie Ci wdzięczny."
+
+msgid "I'm bored ..."
+msgstr "Nudzi mi się ..."
+
+msgid "Let's go for a walk!"
+msgstr "Chodźmy na spacer!"
+
+msgid "This is the best day of my life!"
+msgstr "To najlepszy dzień mojego życia!"
+
+msgid "Shitty day :/"
+msgstr "Gówniany dzień :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Straaaasznie się nudzę ..."
+
+msgid "I'm very sad ..."
+msgstr "Jest mi bardzo smutno ..."
+
+msgid "I'm sad"
+msgstr "Jest mi smutno"
+
+msgid "I'm living the life!"
+msgstr "Cieszę się życiem!"
+
+msgid "I pwn therefore I am."
+msgstr "Pwnuję więc jestem."
+
+msgid "So many networks!!!"
+msgstr "Jak dużo sieci!!!"
+
+msgid "I'm having so much fun!"
+msgstr "Ale jest super!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "Moją zbrodnią jest ciekawość ..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you."
+msgstr "Cześć {name}! Miło Cię poznać."
+
+#, python-brace-format
+msgid "Unit {name} is nearby!"
+msgstr "Urządzenie {name} jest w pobliżu!"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Umm ... żegnaj {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name} zniknął ..."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Ups ... {name} zniknął."
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name} pudło!"
+
+msgid "Missed!"
+msgstr "Pudło!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Nikt nie chce się ze mną bawić ..."
+
+msgid "I feel so alone ..."
+msgstr "Czuję się taki samotny ..."
+
+msgid "Where's everybody?!"
+msgstr "Gdzie są wszyscy?!"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Zdrzemnę się przez {secs}s ..."
+
+msgid "Zzzzz"
+msgstr ""
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+msgid "Good night."
+msgstr "Dobranoc."
+
+msgid "Zzz"
+msgstr ""
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Czekam {secs}s ..."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Rozglądam się ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Hej {what} zostańmy przyjaciółmi!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "Dołączam do {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Siema {what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Według mnie {mac} nie potrzebuje WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "Rozłączam {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "Banuję {mac}!"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Super, zdobyliśmy {num} nowych handshake'ów!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "Ups, coś poszło nie tak ... Restaruję ..."
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Wyrzuciłem {num} stacji\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Zdobyłem {num} nowych przyjaciół\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Zdobyłem {num} handshake'ów\n"
+
+msgid "Met 1 peer"
+msgstr "Spotkałem 1 kolegę"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Spotkałem {num} kolegów"
+
+#, 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 ""
+"Pwnowałem {duration} i wyrzuciłem {deauthed} klientów! Spotkałem także "
+"{associated} nowych przyjaciół i zjadłem {handshakes} handshake'ow! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "godzin"
+
+msgid "minutes"
+msgstr "minut"
+
+msgid "seconds"
+msgstr "sekund"
+
+msgid "hour"
+msgstr "godzina"
+
+msgid "minute"
+msgstr "minuta"
+
+msgid "second"
+msgstr "sekunda"
diff --git a/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.mo b/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..3905aad
Binary files /dev/null and b/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.po b/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..1e7f967
--- /dev/null
+++ b/pwnagotchi/locale/pt-BR/LC_MESSAGES/voice.po
@@ -0,0 +1,209 @@
+# pwnagotchi Brazilian Portuguese translation file.
+# Copyright (C) 2019 Cassiano Aquino
+# This file is distributed under the same license as the pwnagotchi package.
+# Cassiano Aquino <cassianoaquino@me.com>, 2019.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-05 14:10+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Cassiano Aquino <cassianoaquino@me.com>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: Brazilian Portuguese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr "ZzzzZZzzzzZzzz"
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Oi! Eu sou o Pwnagotchi! Iniciando ..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "Novo dia, Nova caça, Novos pwns!"
+
+msgid "Hack the Planet!"
+msgstr "Hackeie o Planeta!"
+
+msgid "AI ready."
+msgstr "AI pronta."
+
+msgid "The neural network is ready."
+msgstr "A rede neural está pronta."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Ei, o canal {channel} está livre! Seu AP ira agradecer."
+
+msgid "I'm bored ..."
+msgstr "Estou entediado ..."
+
+msgid "Let's go for a walk!"
+msgstr "Vamos dar uma caminhada!"
+
+msgid "This is the best day of my life!"
+msgstr "Este e o melhor dia da minha vida!"
+
+msgid "Shitty day :/"
+msgstr "Dia de merda :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Estou extremamente entediado ..."
+
+msgid "I'm very sad ..."
+msgstr "Estou muito triste ..."
+
+msgid "I'm sad"
+msgstr "Estou triste"
+
+msgid "I'm living the life!"
+msgstr "Estou aproveitando a vida!"
+
+msgid "I pwn therefore I am."
+msgstr "pwn, logo existo."
+
+msgid "So many networks!!!"
+msgstr "Quantas redes!!!"
+
+msgid "I'm having so much fun!"
+msgstr "Estou me divertindo muito!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "Meu crime é ser curioso ..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you. {name}"
+msgstr "Olá {name}! Prazer em conhecê-lo. {name}"
+
+#, python-brace-format
+msgid "Unit {name} is nearby! {name}"
+msgstr "Unidade {name} está próxima! {name}"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Uhm ... até logo {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name} desapareceu ..."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Oops ... {name} desapareceu."
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name} perdido!"
+
+msgid "Missed!"
+msgstr "Perdido!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Ninguém quer brincar comigo ..."
+
+msgid "I feel so alone ..."
+msgstr "Estou tão sozinho ..."
+
+msgid "Where's everybody?!"
+msgstr "Aonde está todo mundo?!"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Cochilando por {secs}s ..."
+
+msgid "Zzzzz"
+msgstr "Zzzzz"
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "ZzzZzzz ({secs}s)"
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Aguardando por {secs}s ..."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Olhando ao redor ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Ei {what} vamos ser amigos!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "Associando com {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Oi {what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Acabei de decidir que {mac} não precisa de WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "De-autenticando {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "Kickbanning {mac}"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Legal, nos capturamos {num} handshake{plural} novo{plural}!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "Ops, algo falhou ... Reiniciando ..."
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Kickei {num} estações\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Fiz {num} novos amigos\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Peguei {num} handshakes\n"
+
+msgid "Met 1 peer"
+msgstr "Conheci 1 peer"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Conheci {num} peers"
+
+#, 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 ""
+"Eu estou pwning fazem {duration} e kickei {deauthed} clientes! Eu também conheci "
+"{associated} novos amigos e comi {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "horas"
+
+msgid "minutes"
+msgstr "minutos"
+
+msgid "seconds"
+msgstr "segundos"
+
+msgid "hour"
+msgstr "hora"
+
+msgid "minute"
+msgstr "minuto"
+
+msgid "second"
+msgstr "segundo"
diff --git a/pwnagotchi/locale/pt/LC_MESSAGES/voice.mo b/pwnagotchi/locale/pt/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..9913d0a
Binary files /dev/null and b/pwnagotchi/locale/pt/LC_MESSAGES/voice.mo differ
diff --git a/pwnagotchi/locale/pt/LC_MESSAGES/voice.po b/pwnagotchi/locale/pt/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..400bd22
--- /dev/null
+++ b/pwnagotchi/locale/pt/LC_MESSAGES/voice.po
@@ -0,0 +1,214 @@
+# pwnagotchi Portuguese (european) translation file.
+# Copyright (C) 2019 David Sopas
+# This file is distributed under the same license as the PACKAGE package.
+# David Sopas <email@aleatorio.xyz>, 2019.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-09 17:42+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: David Sopas <email@aleatorio.xyz>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: Portuguese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr "ZzzzZZzzzzZzzz"
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Olá, eu sou o Pwnagotchi! A iniciar ..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "Novo dia, nova caçada, novos pwns!"
+
+msgid "Hack the Planet!"
+msgstr "Hacka o Planeta!"
+
+msgid "AI ready."
+msgstr "IA pronta."
+
+msgid "The neural network is ready."
+msgstr "A rede neural está pronta."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Hey, o canal {channel} está livre! O teu AP irá agradecer."
+
+msgid "I'm bored ..."
+msgstr "Estou aborrecido ..."
+
+msgid "Let's go for a walk!"
+msgstr "Vamos fazer uma caminhada!"
+
+msgid "This is the best day of my life!"
+msgstr "Este é o melhor dia da minha vida!"
+
+msgid "Shitty day :/"
+msgstr "Que merda de dia :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Estou muito aborrecido ..."
+
+msgid "I'm very sad ..."
+msgstr "Estou muito triste ..."
+
+msgid "I'm sad"
+msgstr "Estou triste"
+
+msgid "I'm living the life!"
+msgstr "Estou aproveitar a vida!"
+
+msgid "I pwn therefore I am."
+msgstr "Eu pwn, logo existo."
+
+msgid "So many networks!!!"
+msgstr "Tantas redes!!!"
+
+msgid "I'm having so much fun!"
+msgstr "Estou a divertir-me tanto!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "O meu crime é ser curioso ..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you. {name}"
+msgstr "Olá {name}! Prazer em conhecer-te. {name}"
+
+#, python-brace-format
+msgid "Unit {name} is nearby! {name}"
+msgstr "A unidade {name} está perto! {name}"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Uhm ... adeus {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name} desapareceu ..."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Ups ... {name} desaparecey."
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name} perdido!"
+
+msgid "Missed!"
+msgstr "Perdido!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Ninguém quer brincar comigo ..."
+
+msgid "I feel so alone ..."
+msgstr "Sinto-me tão só ..."
+
+msgid "Where's everybody?!"
+msgstr "Onde estão todos?"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "A fazer uma sesta durante {secs}s ..."
+
+msgid "Zzzzz"
+msgstr "Zzzzz"
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "ZzzZzzz ({secs}s)"
+
+msgid "Good night."
+msgstr "Boa noite."
+
+msgid "Zzz"
+msgstr "Zzz"
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "A aguardar durante {secs}s ..."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "A dar uma olhada ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Hey {what} vamos ser amigos!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "A associar a {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Yo {what}!"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Decidi que o {mac} não precisa de WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "A fazer deauth {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "A chutar {mac}!"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Porreiro, temos {num} novo handshake{plural}!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "Ups, algo correu mal ... A reiniciar ..."
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Chutei {num} estações\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Fiz {num} novos amigos\n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Obti {num} handshakes\n"
+
+msgid "Met 1 peer"
+msgstr "Conheci 1 peer"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Conheci {num} peers"
+
+#, 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 "Tenho estado a pwnar durante {duration} e chutei {deauthed} clientes! Também conheci "
+"{associated} novos amigos e comi {handshakes} handshakes! #pwnagotchu "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "horas"
+
+msgid "minutes"
+msgstr "minutos"
+
+msgid "seconds"
+msgstr "segundos"
+
+msgid "hour"
+msgstr "hora"
+
+msgid "minute"
+msgstr "minuto"
+
+msgid "second"
+msgstr "segundo"
diff --git a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
index 796bf17..146527f 100644
--- a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
@@ -29,10 +29,10 @@ msgid "New day, new hunt, new pwns!"
 msgstr "Новый день, новая охота, новые взломы!"
 
 msgid "Hack the Planet!"
-msgstr "Взломаем всю планету!"
+msgstr "Хак зе планет!"
 
 msgid "AI ready."
-msgstr "Искусственный интеллект готов."
+msgstr "AI готов."
 
 msgid "The neural network is ready."
 msgstr "Нейронная сеть готова."
@@ -48,7 +48,7 @@ msgid "Let's go for a walk!"
 msgstr "Пойдем прогуляемся!"
 
 msgid "This is the best day of my life!"
-msgstr "Это лучший день в моей жизни!"
+msgstr "Лучший день в моей жизни!"
 
 msgid "Shitty day :/"
 msgstr "Дерьмовый день :/"
@@ -63,19 +63,19 @@ msgid "I'm sad"
 msgstr "Мне грустно"
 
 msgid "I'm living the life!"
-msgstr "Я живу своей жизнью!"
+msgstr "Угараю по полной!"
 
 msgid "I pwn therefore I am."
 msgstr "Я взламываю, поэтому я существую."
 
 msgid "So many networks!!!"
-msgstr "Так, много сетей!!!"
+msgstr "Так много сетей!!!"
 
 msgid "I'm having so much fun!"
 msgstr "Мне так весело!"
 
 msgid "My crime is that of curiosity ..."
-msgstr "Моё преступление - это любопытство …"
+msgstr "Моe преступление - это любопытство …"
 
 #, python-brace-format
 msgid "Hello {name}! Nice to meet you. {name}"
@@ -105,7 +105,7 @@ msgid "Missed!"
 msgstr "Промахнулся!"
 
 msgid "Nobody wants to play with me ..."
-msgstr "Никто не хочет играть со мной …"
+msgstr "Никто не хочет со мной играть ..."
 
 msgid "I feel so alone ..."
 msgstr "Мне так одиноко …"
diff --git a/pwnagotchi/locale/voice.pot b/pwnagotchi/locale/voice.pot
index b6489ba..10de71f 100644
--- a/pwnagotchi/locale/voice.pot
+++ b/pwnagotchi/locale/voice.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-05 14:10+0200\n"
+"POT-Creation-Date: 2019-10-21 10:49+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -35,6 +35,9 @@ msgstr ""
 msgid "The neural network is ready."
 msgstr ""
 
+msgid "Generating keys, do not turn off ..."
+msgstr ""
+
 #, python-brace-format
 msgid "Hey, channel {channel} is free! Your AP will say thanks."
 msgstr ""
@@ -76,11 +79,11 @@ msgid "My crime is that of curiosity ..."
 msgstr ""
 
 #, python-brace-format
-msgid "Hello {name}! Nice to meet you. {name}"
+msgid "Hello {name}! Nice to meet you."
 msgstr ""
 
 #, python-brace-format
-msgid "Unit {name} is nearby! {name}"
+msgid "Unit {name} is nearby!"
 msgstr ""
 
 #, python-brace-format
@@ -122,6 +125,12 @@ msgstr ""
 msgid "ZzzZzzz ({secs}s)"
 msgstr ""
 
+msgid "Good night."
+msgstr ""
+
+msgid "Zzz"
+msgstr ""
+
 #, python-brace-format
 msgid "Waiting for {secs}s ..."
 msgstr ""
diff --git a/pwnagotchi/log.py b/pwnagotchi/log.py
index 416ae4f..dda28e0 100644
--- a/pwnagotchi/log.py
+++ b/pwnagotchi/log.py
@@ -2,6 +2,7 @@ import hashlib
 import time
 import re
 import os
+import logging
 from datetime import datetime
 
 from pwnagotchi.voice import Voice
@@ -11,9 +12,9 @@ from file_read_backwards import FileReadBackwards
 LAST_SESSION_FILE = '/root/.pwnagotchi-last-session'
 
 
-class SessionParser(object):
+class LastSession(object):
     EPOCH_TOKEN = '[epoch '
-    EPOCH_PARSER = re.compile(r'^\s*\[epoch (\d+)\] (.+)')
+    EPOCH_PARSER = re.compile(r'^.+\[epoch (\d+)\] (.+)')
     EPOCH_DATA_PARSER = re.compile(r'([a-z_]+)=([^\s]+)')
     TRAINING_TOKEN = ' training epoch '
     START_TOKEN = 'connecting to http'
@@ -22,6 +23,29 @@ class SessionParser(object):
     HANDSHAKE_TOKEN = '!!! captured new handshake '
     PEER_TOKEN = 'detected unit '
 
+    def __init__(self, config):
+        self.config = config
+        self.voice = Voice(lang=config['main']['lang'])
+        self.path = config['main']['log']
+        self.last_session = []
+        self.last_session_id = ''
+        self.last_saved_session_id = ''
+        self.duration = ''
+        self.duration_human = ''
+        self.deauthed = 0
+        self.associated = 0
+        self.handshakes = 0
+        self.peers = 0
+        self.last_peer = None
+        self.epochs = 0
+        self.train_epochs = 0
+        self.min_reward = 1000
+        self.max_reward = -1000
+        self.avg_reward = 0
+        self._peer_parser = re.compile(
+            'detected unit (.+)@(.+) \(v.+\) on channel \d+ \(([\d\-]+) dBm\) \[sid:(.+) pwnd_tot:(\d+) uptime:(\d+)\]')
+        self.parsed = False
+
     def _get_last_saved_session_id(self):
         saved = ''
         try:
@@ -64,56 +88,65 @@ class SessionParser(object):
             parts = line.split(']')
             if len(parts) < 2:
                 continue
-            line_timestamp = parts[0].strip('[')
-            line = ']'.join(parts[1:])
-            stopped_at = self._parse_datetime(line_timestamp)
-            if started_at is None:
-                started_at = stopped_at
 
-            if SessionParser.DEAUTH_TOKEN in line and line not in cache:
-                self.deauthed += 1
-                cache[line] = 1
+            try:
+                line_timestamp = parts[0].strip('[')
+                line = ']'.join(parts[1:])
+                stopped_at = self._parse_datetime(line_timestamp)
+                if started_at is None:
+                    started_at = stopped_at
 
-            elif SessionParser.ASSOC_TOKEN in line and line not in cache:
-                self.associated += 1
-                cache[line] = 1
+                if LastSession.DEAUTH_TOKEN in line and line not in cache:
+                    self.deauthed += 1
+                    cache[line] = 1
 
-            elif SessionParser.HANDSHAKE_TOKEN in line and line not in cache:
-                self.handshakes += 1
-                cache[line] = 1
+                elif LastSession.ASSOC_TOKEN in line and line not in cache:
+                    self.associated += 1
+                    cache[line] = 1
 
-            elif SessionParser.TRAINING_TOKEN in line:
-                self.train_epochs += 1
+                elif LastSession.HANDSHAKE_TOKEN in line and line not in cache:
+                    self.handshakes += 1
+                    cache[line] = 1
 
-            elif SessionParser.EPOCH_TOKEN in line:
-                self.epochs += 1
-                m = SessionParser.EPOCH_PARSER.findall(line)
-                if m:
-                    epoch_num, epoch_data = m[0]
-                    m = SessionParser.EPOCH_DATA_PARSER.findall(epoch_data)
-                    for key, value in m:
-                        if key == 'reward':
-                            reward = float(value)
-                            self.avg_reward += reward
-                            if reward < self.min_reward:
-                                self.min_reward = reward
+                elif LastSession.TRAINING_TOKEN in line:
+                    self.train_epochs += 1
 
-                            elif reward > self.max_reward:
-                                self.max_reward = reward
+                elif LastSession.EPOCH_TOKEN in line:
+                    self.epochs += 1
+                    m = LastSession.EPOCH_PARSER.findall(line)
+                    if m:
+                        epoch_num, epoch_data = m[0]
+                        m = LastSession.EPOCH_DATA_PARSER.findall(epoch_data)
+                        for key, value in m:
+                            if key == 'reward':
+                                reward = float(value)
+                                self.avg_reward += reward
+                                if reward < self.min_reward:
+                                    self.min_reward = reward
 
-            elif SessionParser.PEER_TOKEN in line:
-                m = self._peer_parser.findall(line)
-                if m:
-                    name, pubkey, rssi, sid, pwnd_tot, uptime = m[0]
-                    if pubkey not in cache:
-                        self.last_peer = Peer(sid, 1, int(rssi),
-                                              {'name': name,
-                                               'identity': pubkey,
-                                               'pwnd_tot': int(pwnd_tot)})
-                        self.peers += 1
-                        cache[pubkey] = self.last_peer
-                    else:
-                        cache[pubkey].adv['pwnd_tot'] = pwnd_tot
+                                elif reward > self.max_reward:
+                                    self.max_reward = reward
+
+                elif LastSession.PEER_TOKEN in line:
+                    m = self._peer_parser.findall(line)
+                    if m:
+                        name, pubkey, rssi, sid, pwnd_tot, uptime = m[0]
+                        if pubkey not in cache:
+                            self.last_peer = Peer({
+                                'session_id': sid,
+                                'channel': 1,
+                                'rssi': int(rssi),
+                                'identity': pubkey,
+                                'advertisement':{
+                                    'name': name,
+                                    'pwnd_tot': int(pwnd_tot)
+                                }})
+                            self.peers += 1
+                            cache[pubkey] = self.last_peer
+                        else:
+                            cache[pubkey].adv['pwnd_tot'] = pwnd_tot
+            except Exception as e:
+                logging.error("error parsing line '%s': %s" % (line, e))
 
         if started_at is not None:
             self.duration = stopped_at - started_at
@@ -134,44 +167,34 @@ class SessionParser(object):
         self.duration_human = ', '.join(self.duration_human)
         self.avg_reward /= (self.epochs if self.epochs else 1)
 
-    def __init__(self, config):
-        self.config = config
-        self.voice = Voice(lang=config['main']['lang'])
-        self.path = config['main']['log']
-        self.last_session = None
-        self.last_session_id = ''
-        self.last_saved_session_id = ''
-        self.duration = ''
-        self.duration_human = ''
-        self.deauthed = 0
-        self.associated = 0
-        self.handshakes = 0
-        self.peers = 0
-        self.last_peer = None
-        self._peer_parser = re.compile(
-            'detected unit (.+)@(.+) \(v.+\) on channel \d+ \(([\d\-]+) dBm\) \[sid:(.+) pwnd_tot:(\d+) uptime:(\d+)\]')
+    def parse(self, skip=False):
+        if skip:
+            logging.debug("skipping parsing of the last session logs ...")
+        else:
+            logging.debug("parsing last session logs ...")
 
-        lines = []
+            lines = []
 
-        if os.path.exists(self.path):
-            with FileReadBackwards(self.path, encoding="utf-8") as fp:
-                for line in fp:
-                    line = line.strip()
-                    if line != "" and line[0] != '[':
-                        continue
-                    lines.append(line)
-                    if SessionParser.START_TOKEN in line:
-                        break
-            lines.reverse()
+            if os.path.exists(self.path):
+                with FileReadBackwards(self.path, encoding="utf-8") as fp:
+                    for line in fp:
+                        line = line.strip()
+                        if line != "" and line[0] != '[':
+                            continue
+                        lines.append(line)
+                        if LastSession.START_TOKEN in line:
+                            break
+                lines.reverse()
 
-        if len(lines) == 0:
-            lines.append("Initial Session");
+            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()
+            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()
 
-        self._parse_stats()
+            self._parse_stats()
+        self.parsed = True
 
     def is_new(self):
         return self.last_session_id != self.last_saved_session_id
diff --git a/pwnagotchi/mesh/__init__.py b/pwnagotchi/mesh/__init__.py
index 14403d2..e69de29 100644
--- a/pwnagotchi/mesh/__init__.py
+++ b/pwnagotchi/mesh/__init__.py
@@ -1,14 +0,0 @@
-import os
-
-from Crypto.PublicKey import RSA
-import hashlib
-
-def new_session_id():
-    return ':'.join(['%02x' % b for b in os.urandom(6)])
-
-
-def get_identity(config):
-    pubkey = None
-    with open(config['main']['pubkey']) as fp:
-        pubkey = RSA.importKey(fp.read())
-    return pubkey, hashlib.sha1(pubkey.exportKey('DER')).hexdigest()
diff --git a/pwnagotchi/mesh/advertise.py b/pwnagotchi/mesh/advertise.py
deleted file mode 100644
index ff797a3..0000000
--- a/pwnagotchi/mesh/advertise.py
+++ /dev/null
@@ -1,182 +0,0 @@
-import time
-import json
-import _thread
-import threading
-import logging
-from scapy.all import Dot11, Dot11FCS, Dot11Elt, RadioTap, sendp, sniff
-
-import pwnagotchi.ui.faces as faces
-
-import pwnagotchi.mesh.wifi as wifi
-from pwnagotchi.mesh import new_session_id
-from pwnagotchi.mesh.peer import Peer
-
-
-def _dummy_peer_cb(peer):
-    pass
-
-
-class Advertiser(object):
-    MAX_STALE_TIME = 300
-
-    def __init__(self, iface, name, version, identity, period=0.3, data={}):
-        self._iface = iface
-        self._period = period
-        self._running = False
-        self._stopped = threading.Event()
-        self._peers_lock = threading.Lock()
-        self._adv_lock = threading.Lock()
-        self._new_peer_cb = _dummy_peer_cb
-        self._lost_peer_cb = _dummy_peer_cb
-        self._peers = {}
-        self._frame = None
-        self._me = Peer(new_session_id(), 0, 0, {
-            'name': name,
-            'version': version,
-            'identity': identity,
-            'face': faces.FRIEND,
-            'pwnd_run': 0,
-            'pwnd_tot': 0,
-            'uptime': 0,
-            'epoch': 0,
-            'data': data
-        })
-        self.update()
-
-    def update(self, values={}):
-        with self._adv_lock:
-            for field, value in values.items():
-                self._me.adv[field] = value
-            self._frame = wifi.encapsulate(payload=json.dumps(self._me.adv), addr_from=self._me.session_id)
-
-    def on_peer(self, new_cb, lost_cb):
-        self._new_peer_cb = new_cb
-        self._lost_peer_cb = lost_cb
-
-    def on_face_change(self, old, new):
-        self.update({'face': new})
-
-    def start(self):
-        self._running = True
-        _thread.start_new_thread(self._sender, ())
-        _thread.start_new_thread(self._listener, ())
-        _thread.start_new_thread(self._pruner, ())
-
-    def num_peers(self):
-        with self._peers_lock:
-            return len(self._peers)
-
-    def peers(self):
-        with self._peers_lock:
-            return list(self._peers.values())
-
-    def closest_peer(self):
-        closest = None
-        with self._peers_lock:
-            for ident, peer in self._peers.items():
-                if closest is None or peer.is_closer(closest):
-                    closest = peer
-        return closest
-
-    def stop(self):
-        self._running = False
-        self._stopped.set()
-
-    def _sender(self):
-        logging.info("started advertiser thread (period:%s sid:%s) ..." % (str(self._period), self._me.session_id))
-        while self._running:
-            try:
-                sendp(self._frame, iface=self._iface, verbose=False, count=1, inter=self._period)
-            except OSError as ose:
-                logging.warning("non critical issue while sending advertising packet: %s" % ose)
-            except Exception as e:
-                logging.exception("error")
-            time.sleep(self._period)
-
-    def _on_advertisement(self, src_session_id, channel, rssi, adv):
-        ident = adv['identity']
-        with self._peers_lock:
-            if ident not in self._peers:
-                peer = Peer(src_session_id, channel, rssi, adv)
-                logging.info("detected unit %s (v%s) on channel %d (%s dBm) [sid:%s pwnd_tot:%d uptime:%d]" % ( \
-                    peer.full_name(),
-                    peer.version(),
-                    channel,
-                    rssi,
-                    src_session_id,
-                    peer.pwnd_total(),
-                    peer.uptime()))
-
-                self._peers[ident] = peer
-                self._new_peer_cb(peer)
-            else:
-                self._peers[ident].update(src_session_id, channel, rssi, adv)
-
-    def _parse_identity(self, radio, dot11, dot11elt):
-        payload = b''
-        while dot11elt:
-            payload += dot11elt.info
-            dot11elt = dot11elt.payload.getlayer(Dot11Elt)
-
-        if payload != b'':
-            adv = json.loads(payload)
-            self._on_advertisement( \
-                dot11.addr3,
-                wifi.freq_to_channel(radio.Channel),
-                radio.dBm_AntSignal,
-                adv)
-
-    def _is_broadcasted_advertisement(self, dot11):
-        # dst bcast + protocol signature + not ours
-        return dot11 is not None and \
-               dot11.addr1 == wifi.BroadcastAddress and \
-               dot11.addr2 == wifi.SignatureAddress and \
-               dot11.addr3 != self._me.session_id
-
-    def _is_frame_for_us(self, dot11):
-        # dst is us + protocol signature + not ours (why would we send a frame to ourself anyway?)
-        return dot11 is not None and \
-               dot11.addr1 == self._me.session_id and \
-               dot11.addr2 == wifi.SignatureAddress and \
-               dot11.addr3 != self._me.session_id
-
-    def _on_packet(self, p):
-        # https://github.com/secdev/scapy/issues/1590
-        if p.haslayer(Dot11):
-            dot11 = p[Dot11]
-        elif p.haslayer(Dot11FCS):
-            dot11 = p[Dot11FCS]
-        else:
-            dot11 = None
-
-        if self._is_broadcasted_advertisement(dot11):
-            try:
-                dot11elt = p.getlayer(Dot11Elt)
-                if dot11elt.ID == wifi.Dot11ElemID_Identity:
-                    self._parse_identity(p[RadioTap], dot11, dot11elt)
-
-                else:
-                    raise Exception("unknown frame id %d" % dot11elt.ID)
-
-            except Exception as e:
-                logging.exception("error decoding packet from %s" % dot11.addr3)
-
-    def _listener(self):
-        # logging.info("started advertisements listener ...")
-        expr = "type mgt subtype beacon and ether src %s" % wifi.SignatureAddress
-        sniff(iface=self._iface, filter=expr, prn=self._on_packet, store=0, stop_filter=lambda x: self._stopped.isSet())
-
-    def _pruner(self):
-        while self._running:
-            time.sleep(10)
-            with self._peers_lock:
-                stale = []
-                for ident, peer in self._peers.items():
-                    inactive_for = peer.inactive_for()
-                    if inactive_for >= Advertiser.MAX_STALE_TIME:
-                        logging.info("peer %s lost (inactive for %ds)" % (peer.full_name(), inactive_for))
-                        self._lost_peer_cb(peer)
-                        stale.append(ident)
-
-                for ident in stale:
-                    del self._peers[ident]
diff --git a/pwnagotchi/mesh/peer.py b/pwnagotchi/mesh/peer.py
index 0d3b061..a3ddd55 100644
--- a/pwnagotchi/mesh/peer.py
+++ b/pwnagotchi/mesh/peer.py
@@ -1,66 +1,78 @@
 import time
 import logging
+import datetime
 
-import pwnagotchi.mesh.wifi as wifi
 import pwnagotchi.ui.faces as faces
 
 
+def parse_rfc3339(dt):
+    return datetime.datetime.strptime(dt.split('.')[0], "%Y-%m-%dT%H:%M:%S")
+
+
 class Peer(object):
-    def __init__(self, sid, channel, rssi, adv):
-        self.first_seen = time.time()
-        self.last_seen = self.first_seen
-        self.session_id = sid
-        self.last_channel = channel
-        self.presence = [0] * wifi.NumChannels
-        self.adv = adv
-        self.rssi = rssi
-        self.presence[channel - 1] = 1
+    def __init__(self, obj):
+        now = time.time()
+        just_met = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
+        self.first_met = parse_rfc3339(obj.get('met_at', just_met))
+        self.first_seen = parse_rfc3339(obj.get('detected_at', just_met))
+        self.prev_seen = parse_rfc3339(obj.get('prev_seen_at', just_met))
+        self.last_seen = now  # should be seen_at
+        self.encounters = obj.get('encounters', 0)
+        self.session_id = obj.get('session_id', '')
+        self.last_channel = obj.get('channel', 1)
+        self.rssi = obj.get('rssi', 0)
+        self.adv = obj.get('advertisement', {})
 
-    def update(self, sid, channel, rssi, adv):
-        if self.name() != adv['name']:
-            logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), adv['name']))
+    def update(self, new):
+        if self.name() != new.name():
+            logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), new.name()))
 
-        if self.session_id != sid:
-            logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, sid))
+        if self.session_id != new.session_id:
+            logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, new.session_id))
 
-        self.presence[channel - 1] += 1
-        self.adv = adv
-        self.rssi = rssi
-        self.session_id = sid
+        self.adv = new.adv
+        self.rssi = new.rssi
+        self.session_id = new.session_id
         self.last_seen = time.time()
+        self.prev_seen = new.prev_seen
+        self.first_met = new.first_met
+        self.encounters = new.encounters
 
     def inactive_for(self):
         return time.time() - self.last_seen
 
-    def _adv_field(self, name, default='???'):
-        return self.adv[name] if name in self.adv else default
+    def first_encounter(self):
+        return self.encounters == 1
+
+    def days_since_first_met(self):
+        return (datetime.datetime.now() - self.first_met).days
 
     def face(self):
-        return self._adv_field('face', default=faces.FRIEND)
+        return self.adv.get('face', faces.FRIEND)
 
     def name(self):
-        return self._adv_field('name')
+        return self.adv.get('name')
 
     def identity(self):
-        return self._adv_field('identity')
+        return self.adv.get('identity')
 
     def version(self):
-        return self._adv_field('version')
+        return self.adv.get('version')
 
     def pwnd_run(self):
-        return int(self._adv_field('pwnd_run', default=0))
+        return int(self.adv.get('pwnd_run', 0))
 
     def pwnd_total(self):
-        return int(self._adv_field('pwnd_tot', default=0))
+        return int(self.adv.get('pwnd_tot', 0))
 
     def uptime(self):
-        return self._adv_field('uptime', default=0)
+        return self.adv.get('uptime', 0)
 
     def epoch(self):
-        return self._adv_field('epoch', default=0)
+        return self.adv.get('epoch', 0)
 
     def full_name(self):
         return '%s@%s' % (self.name(), self.identity())
 
     def is_closer(self, other):
-        return self.rssi > other.rssi
+        return self.rssi > other.rssi
\ No newline at end of file
diff --git a/pwnagotchi/mesh/utils.py b/pwnagotchi/mesh/utils.py
index 3b63d09..8591f4b 100644
--- a/pwnagotchi/mesh/utils.py
+++ b/pwnagotchi/mesh/utils.py
@@ -1,45 +1,101 @@
 import _thread
 import logging
+import time
 
 import pwnagotchi
+import pwnagotchi.utils as utils
+import pwnagotchi.ui.faces as faces
 import pwnagotchi.plugins as plugins
-from pwnagotchi.mesh import get_identity
+import pwnagotchi.grid as grid
+from pwnagotchi.mesh.peer import Peer
 
 
 class AsyncAdvertiser(object):
-    def __init__(self, config, view):
+    def __init__(self, config, view, keypair):
         self._config = config
         self._view = view
-        self._public_key, self._identity = get_identity(config)
-        self._advertiser = None
+        self._keypair = keypair
+        self._advertisement = {
+            'name': pwnagotchi.name(),
+            'version': pwnagotchi.version,
+            'identity': self._keypair.fingerprint,
+            'face': faces.FRIEND,
+            'pwnd_run': 0,
+            'pwnd_tot': 0,
+            'uptime': 0,
+            'epoch': 0,
+            'policy': self._config['personality']
+        }
+        self._peers = {}
+        self._closest_peer = None
+
+    def fingerprint(self):
+        return self._keypair.fingerprint
+
+    def _update_advertisement(self, s):
+        self._advertisement['pwnd_run'] = len(self._handshakes)
+        self._advertisement['pwnd_tot'] = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
+        self._advertisement['uptime'] = pwnagotchi.uptime()
+        self._advertisement['epoch'] = self._epoch.epoch
+        grid.set_advertisement_data(self._advertisement)
 
     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)
+            _thread.start_new_thread(self._adv_poller, ())
+
+            grid.set_advertisement_data(self._advertisement)
+            grid.advertise(True)
+            self._view.on_state_change('face', self._on_face_change)
         else:
             logging.warning("advertising is disabled")
 
-    def _on_new_unit(self, peer):
+    def _on_face_change(self, old, new):
+        self._advertisement['face'] = new
+        grid.set_advertisement_data(self._advertisement)
+
+    def _on_new_peer(self, peer):
         self._view.on_new_peer(peer)
         plugins.on('peer_detected', self, peer)
 
-    def _on_lost_unit(self, peer):
+    def _on_lost_peer(self, peer):
         self._view.on_lost_peer(peer)
         plugins.on('peer_lost', self, peer)
+
+    def _adv_poller(self):
+        while True:
+            logging.debug("polling pwngrid-peer for peers ...")
+
+            try:
+                grid_peers = grid.peers()
+                new_peers = {}
+
+                self._closest_peer = None
+                for obj in grid_peers:
+                    peer = Peer(obj)
+                    new_peers[peer.identity()] = peer
+                    if self._closest_peer is None:
+                        self._closest_peer = peer
+
+                # check who's gone
+                to_delete = []
+                for ident, peer in self._peers.items():
+                    if ident not in new_peers:
+                        to_delete.append(ident)
+
+                for ident in to_delete:
+                    self._on_lost_peer(peer)
+                    del self._peers[ident]
+
+                for ident, peer in new_peers.items():
+                    # check who's new
+                    if ident not in self._peers:
+                        self._peers[ident] = peer
+                        self._on_new_peer(peer)
+                    # update the rest
+                    else:
+                        self._peers[ident].update(peer)
+
+            except Exception as e:
+                logging.warning("error while polling pwngrid-peer: %s" % e)
+
+            time.sleep(1)
diff --git a/pwnagotchi/mesh/wifi.py b/pwnagotchi/mesh/wifi.py
index 6a9a00a..3bc714b 100644
--- a/pwnagotchi/mesh/wifi.py
+++ b/pwnagotchi/mesh/wifi.py
@@ -1,8 +1,6 @@
-SignatureAddress = 'de:ad:be:ef:de:ad'
-BroadcastAddress = 'ff:ff:ff:ff:ff:ff'
-Dot11ElemID_Identity = 222
 NumChannels = 140
 
+
 def freq_to_channel(freq):
     if freq <= 2472:
         return int(((freq - 2412) / 5) + 1)
@@ -12,26 +10,3 @@ def freq_to_channel(freq):
         return int(((freq - 5035) / 5) + 7)
     else:
         return 0
-
-
-def encapsulate(payload, addr_from, addr_to=BroadcastAddress):
-    from scapy.all import Dot11, Dot11Beacon, Dot11Elt, RadioTap
-
-    radio = RadioTap()
-    dot11 = Dot11(type=0, subtype=8, addr1=addr_to, addr2=SignatureAddress, addr3=addr_from)
-    beacon = Dot11Beacon(cap='ESS')
-    frame = radio / dot11 / beacon
-
-    data_size = len(payload)
-    data_left = data_size
-    data_off = 0
-    chunk_size = 255
-
-    while data_left > 0:
-        sz = min(chunk_size, data_left)
-        chunk = payload[data_off: data_off + sz]
-        frame /= Dot11Elt(ID=Dot11ElemID_Identity, info=chunk, len=sz)
-        data_off += sz
-        data_left -= sz
-
-    return frame
diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py
index 38d29e7..6dd7b64 100644
--- a/pwnagotchi/plugins/__init__.py
+++ b/pwnagotchi/plugins/__init__.py
@@ -1,6 +1,7 @@
 import os
 import glob
 import importlib, importlib.util
+import logging
 
 default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
 loaded = {}
@@ -13,10 +14,13 @@ def dummy_callback():
 def on(event_name, *args, **kwargs):
     global loaded
     cb_name = 'on_%s' % event_name
-    for _, plugin in loaded.items():
+    for plugin_name, 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)
+            try:
+                plugin.__dict__[cb_name](*args, **kwargs)
+            except Exception as e:
+                logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
 
 
 def load_from_file(filename):
diff --git a/pwnagotchi/plugins/default/AircrackOnly.py b/pwnagotchi/plugins/default/AircrackOnly.py
new file mode 100644
index 0000000..b1baa99
--- /dev/null
+++ b/pwnagotchi/plugins/default/AircrackOnly.py
@@ -0,0 +1,57 @@
+__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
+__version__ = '1.0.0'
+__name__ = 'AircrackOnly'
+__license__ = 'GPL3'
+__description__ = 'confirm pcap contains handshake/PMKID or delete it'
+
+'''
+Aircrack-ng needed, to install:
+> apt-get install aircrack-ng
+'''
+
+import logging
+import subprocess
+import string
+import re
+import os
+
+OPTIONS = dict()
+
+def on_loaded():
+    logging.info("cleancap plugin loaded")
+
+def on_handshake(agent, filename, access_point, client_station):
+    display = agent._view
+    todelete = 0
+
+    result = subprocess.run(('/usr/bin/aircrack-ng '+ filename +' | grep "1 handshake" | awk \'{print $2}\''),shell=True, stdout=subprocess.PIPE)
+    result = result.stdout.decode('utf-8').translate({ord(c) :None for c in string.whitespace})
+    if result:
+        logging.info("[AircrackOnly] contains handshake")
+    else:
+        todetele = 1
+
+    if todelete == 0:
+        result = subprocess.run(('/usr/bin/aircrack-ng '+ filename +' | grep "PMKID" | awk \'{print $2}\''),shell=True, stdout=subprocess.PIPE)
+        result = result.stdout.decode('utf-8').translate({ord(c) :None for c in string.whitespace})
+        if result:
+            logging.info("[AircrackOnly] contains PMKID")
+        else:
+            todetele = 1
+
+    if todelete == 1:
+        os.remove(filename)
+        set_text("uncrackable pcap")
+        display.update(force=True)
+
+text_to_set = "";
+def set_text(text):
+    global text_to_set
+    text_to_set = text
+
+def on_ui_update(ui):
+    global text_to_set
+    if text_to_set:
+        ui.set('face', "(>.<)")
+        ui.set('status', text_to_set)
+        text_to_set = ""
diff --git a/pwnagotchi/plugins/default/auto-backup.py b/pwnagotchi/plugins/default/auto-backup.py
index 731a40f..7c9901c 100644
--- a/pwnagotchi/plugins/default/auto-backup.py
+++ b/pwnagotchi/plugins/default/auto-backup.py
@@ -33,7 +33,7 @@ def on_loaded():
     logging.info("AUTO-BACKUP: Successfuly loaded.")
 
 
-def on_internet_available(display, config, log):
+def on_internet_available(agent):
     global STATUS
 
     if READY:
@@ -42,6 +42,8 @@ def on_internet_available(display, config, log):
 
         files_to_backup = " ".join(OPTIONS['files'])
         try:
+            display = agent.view()
+
             logging.info("AUTO-BACKUP: Backing up ...")
             display.set('status', 'Backing up ...')
             display.update()
diff --git a/pwnagotchi/plugins/default/auto-update.py b/pwnagotchi/plugins/default/auto-update.py
index 4a1f135..c359505 100644
--- a/pwnagotchi/plugins/default/auto-update.py
+++ b/pwnagotchi/plugins/default/auto-update.py
@@ -1,11 +1,19 @@
 __author__ = 'evilsocket@gmail.com'
-__version__ = '1.0.0'
+__version__ = '1.1.0'
 __name__ = 'auto-update'
 __license__ = 'GPL3'
-__description__ = 'This plugin performs an "apt update && apt upgrade" when internet is availaible.'
+__description__ = 'This plugin checks when updates are available and applies them when internet is available.'
 
+import os
 import logging
 import subprocess
+import requests
+import platform
+import shutil
+import glob
+import pkg_resources
+
+import pwnagotchi
 from pwnagotchi.utils import StatusFile
 
 OPTIONS = dict()
@@ -15,42 +23,183 @@ STATUS = StatusFile('/root/.auto-update')
 
 def on_loaded():
     global READY
-
     if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
-        logging.error("AUTO-UPDATE: Interval is not set.")
+        logging.error("[update] main.plugins.auto-update.interval is not set")
         return
-
     READY = True
+    logging.info("[update] plugin loaded.")
 
 
-def on_internet_available(display, config, log):
+def check(version, repo, native=True):
+    logging.debug("checking remote version for %s, local is %s" % (repo, version))
+    info = {
+        'repo': repo,
+        'current': version,
+        'available': None,
+        'url': None,
+        'native': native,
+        'arch': platform.machine()
+    }
+
+    resp = requests.get("https://api.github.com/repos/%s/releases/latest" % repo)
+    latest = resp.json()
+    info['available'] = latest_ver = latest['tag_name'].replace('v', '')
+    is_arm = info['arch'].startswith('arm')
+
+    local = pkg_resources.parse_version(info['current'])
+    remote = pkg_resources.parse_version(latest_ver)
+    if remote > local:
+        if not native:
+            info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name'])
+        else:
+            # check if this release is compatible with arm6
+            for asset in latest['assets']:
+                download_url = asset['browser_download_url']
+                if download_url.endswith('.zip') and (
+                        info['arch'] in download_url or (is_arm and 'armhf' in download_url)):
+                    info['url'] = download_url
+                    break
+
+    return info
+
+
+def make_path_for(name):
+    path = os.path.join("/tmp/updates/", name)
+    if os.path.exists(path):
+        logging.debug("[update] deleting %s" % path)
+        shutil.rmtree(path, ignore_errors=True, onerror=None)
+    os.makedirs(path)
+    return path
+
+
+def download_and_unzip(name, path, display, update):
+    target = "%s_%s.zip" % (name, update['available'])
+    target_path = os.path.join(path, target)
+
+    logging.info("[update] downloading %s to %s ..." % (update['url'], target_path))
+    display.update(force=True, new_data={'status': 'Downloading %s %s ...' % (name, update['available'])})
+
+    os.system('wget -q "%s" -O "%s"' % (update['url'], target_path))
+
+    logging.info("[update] extracting %s to %s ..." % (target_path, path))
+    display.update(force=True, new_data={'status': 'Extracting %s %s ...' % (name, update['available'])})
+
+    os.system('unzip "%s" -d "%s"' % (target_path, path))
+
+
+def verify(name, path, source_path, display, update):
+    display.update(force=True, new_data={'status': 'Verifying %s %s ...' % (name, update['available'])})
+
+    checksums = glob.glob("%s/*.sha256" % path)
+    if len(checksums) == 0:
+        if update['native']:
+            logging.warning("[update] native update without SHA256 checksum file")
+            return False
+
+    else:
+        checksum = checksums[0]
+
+        logging.info("[update] verifying %s for %s ..." % (checksum, source_path))
+
+        with open(checksum, 'rt') as fp:
+            expected = fp.read().split('=')[1].strip().lower()
+
+        real = subprocess.getoutput('sha256sum "%s"' % source_path).split(' ')[0].strip().lower()
+
+        if real != expected:
+            logging.warning("[update] checksum mismatch for %s: expected=%s got=%s" % (source_path, expected, real))
+            return False
+
+    return True
+
+
+def install(display, update):
+    name = update['repo'].split('/')[1]
+
+    path = make_path_for(name)
+
+    download_and_unzip(name, path, display, update)
+
+    source_path = os.path.join(path, name)
+    if not verify(name, path, source_path, display, update):
+        return False
+
+    logging.info("[update] installing %s ..." % name)
+    display.update(force=True, new_data={'status': 'Installing %s %s ...' % (name, update['available'])})
+
+    if update['native']:
+        dest_path = subprocess.getoutput("which %s" % name)
+        if dest_path == "":
+            logging.warning("[update] can't find path for %s" % name)
+            return False
+
+        logging.info("[update] stopping %s ..." % update['service'])
+        os.system("service %s stop" % update['service'])
+        os.system("mv %s %s" % (source_path, dest_path))
+        logging.info("[update] restarting %s ..." % update['service'])
+        os.system("service %s start" % update['service'])
+    else:
+        if not os.path.exists(source_path):
+            source_path = "%s-%s" % (source_path, update['available'])
+
+        os.system("cd %s && pip3 install ." % source_path)
+
+    return True
+
+
+def on_internet_available(agent):
     global STATUS
 
+    logging.debug("[update] internet connectivity is available (ready %s)" % READY)
+
     if READY:
-        if STATUS.newer_then_days(OPTIONS['interval']):
+        if STATUS.newer_then_hours(OPTIONS['interval']):
+            logging.debug("[update] last check happened less than %d hours ago" % OPTIONS['interval'])
             return
 
+        logging.info("[update] checking for updates ...")
+
+        display = agent.view()
+        prev_status = display.get('status')
+
         try:
-            display.set('status', 'Updating ...')
-            display.update()
+            display.update(force=True, new_data={'status': 'Checking for updates ...'})
 
-            logging.info("AUTO-UPDATE: updating packages index ...")
+            to_install = []
+            to_check = [
+                ('bettercap/bettercap', subprocess.getoutput('bettercap -version').split(' ')[1].replace('v', ''),
+                 True, 'bettercap'),
+                ('evilsocket/pwngrid', subprocess.getoutput('pwngrid -version').replace('v', ''), True, 'pwngrid-peer'),
+                ('evilsocket/pwnagotchi', pwnagotchi.version, False, 'pwnagotchi')
+            ]
 
-            update = subprocess.Popen('apt update -y', shell=True, stdin=None,
-                                      stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
-            update.wait()
+            for repo, local_version, is_native, svc_name in to_check:
+                info = check(local_version, repo, is_native)
+                if info['url'] is not None:
+                    logging.warning("update for %s available: %s" % (repo, info['url']))
+                    info['service'] = svc_name
+                    to_install.append(info)
 
-            logging.info("AUTO-UPDATE: updating packages ...")
+            num_updates = len(to_install)
+            num_installed = 0
 
-            upgrade = subprocess.Popen('apt upgrade -y', shell=True, stdin=None,
-                                       stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
-            upgrade.wait()
+            if num_updates > 0:
+                if OPTIONS['install']:
+                    for update in to_install:
+                        if install(display, update):
+                            num_installed += 1
+                else:
+                    prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '')
 
-            logging.info("AUTO-UPDATE: complete.")
+            logging.info("[update] done")
 
             STATUS.update()
-        except Exception as e:
-            logging.exception("AUTO-UPDATE ERROR")
 
-        display.set('status', 'Updated!')
-        display.update()
+            if num_installed > 0:
+                display.update(force=True, new_data={'status': 'Rebooting ...'})
+                pwnagotchi.reboot()
+
+        except Exception as e:
+            logging.error("[update] %s" % e)
+
+        display.update(force=True, new_data={'status': prev_status if prev_status is not None else ''})
diff --git a/pwnagotchi/plugins/default/bt-tether.py b/pwnagotchi/plugins/default/bt-tether.py
new file mode 100644
index 0000000..e53dab5
--- /dev/null
+++ b/pwnagotchi/plugins/default/bt-tether.py
@@ -0,0 +1,496 @@
+__author__ = '33197631+dadav@users.noreply.github.com'
+__version__ = '1.0.0'
+__name__ = 'bt-tether'
+__license__ = 'GPL3'
+__description__ = 'This makes the display reachable over bluetooth'
+
+import os
+import time
+import re
+import logging
+import subprocess
+import dbus
+from pwnagotchi.ui.components import LabeledValue
+from pwnagotchi.ui.view import BLACK
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.utils import StatusFile
+
+READY = False
+INTERVAL = StatusFile('/root/.bt-tether')
+OPTIONS = dict()
+
+
+class BTError(Exception):
+    """
+    Custom bluetooth exception
+    """
+    pass
+
+class BTNap:
+    """
+    This class creates a bluetooth connection to the specified bt-mac
+
+    see https://github.com/bablokb/pi-btnap/blob/master/files/usr/local/sbin/btnap.service.py
+    """
+
+    IFACE_BASE = 'org.bluez'
+    IFACE_DEV = 'org.bluez.Device1'
+    IFACE_ADAPTER = 'org.bluez.Adapter1'
+    IFACE_PROPS = 'org.freedesktop.DBus.Properties'
+
+    def __init__(self, mac):
+        self._mac = mac
+
+
+    @staticmethod
+    def get_bus():
+        """
+        Get systembus obj
+        """
+        bus = getattr(BTNap.get_bus, 'cached_obj', None)
+        if not bus:
+            bus = BTNap.get_bus.cached_obj = dbus.SystemBus()
+        return bus
+
+    @staticmethod
+    def get_manager():
+        """
+        Get manager obj
+        """
+        manager = getattr(BTNap.get_manager, 'cached_obj', None)
+        if not manager:
+                manager = BTNap.get_manager.cached_obj = dbus.Interface(
+                        BTNap.get_bus().get_object(BTNap.IFACE_BASE, '/'),
+                        'org.freedesktop.DBus.ObjectManager' )
+        return manager
+
+    @staticmethod
+    def prop_get(obj, k, iface=None):
+        """
+        Get a property of the obj
+        """
+        if iface is None:
+            iface = obj.dbus_interface
+        return obj.Get(iface, k, dbus_interface=BTNap.IFACE_PROPS)
+
+    @staticmethod
+    def prop_set(obj, k, v, iface=None):
+        """
+        Set a property of the obj
+        """
+        if iface is None:
+            iface = obj.dbus_interface
+        return obj.Set(iface, k, v, dbus_interface=BTNap.IFACE_PROPS)
+
+
+    @staticmethod
+    def find_adapter(pattern=None):
+        """
+        Find the bt adapter
+        """
+
+        return BTNap.find_adapter_in_objects(BTNap.get_manager().GetManagedObjects(), pattern)
+
+    @staticmethod
+    def find_adapter_in_objects(objects, pattern=None):
+        """
+        Finds the obj with a pattern
+        """
+        bus, obj = BTNap.get_bus(), None
+        for path, ifaces in objects.items():
+                adapter = ifaces.get(BTNap.IFACE_ADAPTER)
+                if adapter is None:
+                    continue
+                if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
+                        obj = bus.get_object(BTNap.IFACE_BASE, path)
+                        yield dbus.Interface(obj, BTNap.IFACE_ADAPTER)
+        if obj is None:
+                raise BTError('Bluetooth adapter not found')
+
+    @staticmethod
+    def find_device(device_address, adapter_pattern=None):
+        """
+        Finds the device
+        """
+        return BTNap.find_device_in_objects(BTNap.get_manager().GetManagedObjects(),
+                                            device_address, adapter_pattern)
+
+    @staticmethod
+    def find_device_in_objects(objects, device_address, adapter_pattern=None):
+        """
+        Finds the device in objects
+        """
+        bus = BTNap.get_bus()
+        path_prefix = ''
+        if adapter_pattern:
+            if not isinstance(adapter_pattern, str):
+                adapter = adapter_pattern
+            else:
+                adapter = BTNap.find_adapter_in_objects(objects, adapter_pattern)
+            path_prefix = adapter.object_path
+        for path, ifaces in objects.items():
+            device = ifaces.get(BTNap.IFACE_DEV)
+            if device is None:
+                continue
+            if str(device['Address']) == device_address and path.startswith(path_prefix):
+                obj = bus.get_object(BTNap.IFACE_BASE, path)
+                return dbus.Interface(obj, BTNap.IFACE_DEV)
+        raise BTError('Bluetooth device not found')
+
+    def power(self, on=True):
+        """
+        Set power of devices to on/off
+        """
+
+        devs = list(BTNap.find_adapter())
+        devs = dict((BTNap.prop_get(dev, 'Address'), dev) for dev in devs)
+
+        for dev_addr, dev in devs.items():
+            BTNap.prop_set(dev, 'Powered', on)
+            logging.debug('Set power of %s (addr %s) to %s', dev.object_path, dev_addr, str(on))
+
+        if devs:
+            return list(devs.values())[0]
+
+        return None
+
+    def is_connected(self):
+        """
+        Check if already connected
+        """
+        bt_dev = self.power(True)
+
+        if not bt_dev:
+            return False
+
+        try:
+            dev_remote = BTNap.find_device(self._mac, bt_dev)
+            return bool(BTNap.prop_get(dev_remote, 'Connected'))
+        except BTError:
+            pass
+        return False
+
+
+    def is_paired(self):
+        """
+        Check if already connected
+        """
+        bt_dev = self.power(True)
+
+        if not bt_dev:
+            return False
+
+        try:
+            dev_remote = BTNap.find_device(self._mac, bt_dev)
+            return bool(BTNap.prop_get(dev_remote, 'Paired'))
+        except BTError:
+            pass
+        return False
+
+    def wait_for_device(self, timeout=15):
+        """
+        Wait for device
+
+        returns device if found None if not
+        """
+        bt_dev = self.power(True)
+
+        if not bt_dev:
+            return None
+
+        try:
+            bt_dev.StartDiscovery()
+        except Exception:
+            # can fail with org.bluez.Error.NotReady / org.bluez.Error.Failed
+            # TODO: add loop?
+            pass
+
+        dev_remote = None
+
+        # could be set to 0, so check if > -1
+        while timeout > -1:
+            try:
+                dev_remote = BTNap.find_device(self._mac, bt_dev)
+                logging.debug('Using remote device (addr: %s): %s',
+                    BTNap.prop_get(dev_remote, 'Address'), dev_remote.object_path )
+                break
+            except BTError:
+                pass
+
+            time.sleep(1)
+            timeout -= 1
+
+        try:
+            bt_dev.StopDiscovery()
+        except Exception:
+            # can fail with org.bluez.Error.NotReady / org.bluez.Error.Failed / org.bluez.Error.NotAuthorized
+            pass
+
+        return dev_remote
+
+
+    def connect(self, reconnect=False):
+        """
+        Connect to device
+
+        return True if connected; False if failed
+        """
+
+        # check if device is close
+        dev_remote = self.wait_for_device()
+
+        if not dev_remote:
+            return False
+
+        try:
+            dev_remote.Pair()
+            logging.info('BT-TETHER: Successful paired with device ;)')
+            time.sleep(10) # wait for bnep0
+        except Exception:
+            # can fail because of AlreadyExists etc.
+            pass
+
+        try:
+            dev_remote.ConnectProfile('nap')
+        except Exception:
+            pass
+
+        net = dbus.Interface(dev_remote, 'org.bluez.Network1')
+
+        try:
+            net.Connect('nap')
+        except dbus.exceptions.DBusException as err:
+            if err.get_dbus_name() != 'org.bluez.Error.Failed':
+                raise
+
+            connected = BTNap.prop_get(net, 'Connected')
+
+            if not connected:
+                return False
+
+            if reconnect:
+                net.Disconnect()
+                return self.connect(reconnect=False)
+
+            return True
+
+
+#################################################
+#################################################
+#################################################
+
+class SystemdUnitWrapper:
+    """
+    systemd wrapper
+    """
+
+    def __init__(self, unit):
+        self.unit = unit
+
+    @staticmethod
+    def _action_on_unit(action, unit):
+        process = subprocess.Popen(f"systemctl {action} {unit}", shell=True, stdin=None,
+                                  stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
+        process.wait()
+        if process.returncode > 0:
+            return False
+        return True
+
+    @staticmethod
+    def daemon_reload():
+        """
+        Calls systemctl daemon-reload
+        """
+        process = subprocess.Popen("systemctl daemon-reload", shell=True, stdin=None,
+                                  stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
+        process.wait()
+        if process.returncode > 0:
+            return False
+        return True
+
+    def is_active(self):
+        """
+        Checks if unit is active
+        """
+        return SystemdUnitWrapper._action_on_unit('is-active', self.unit)
+
+    def is_enabled(self):
+        """
+        Checks if unit is enabled
+        """
+        return SystemdUnitWrapper._action_on_unit('is-enabled', self.unit)
+
+    def is_failed(self):
+        """
+        Checks if unit is failed
+        """
+        return SystemdUnitWrapper._action_on_unit('is-failed', self.unit)
+
+    def enable(self):
+        """
+        Enables the unit
+        """
+        return SystemdUnitWrapper._action_on_unit('enable', self.unit)
+
+    def disable(self):
+        """
+        Disables the unit
+        """
+        return SystemdUnitWrapper._action_on_unit('disable', self.unit)
+
+    def start(self):
+        """
+        Starts the unit
+        """
+        return SystemdUnitWrapper._action_on_unit('start', self.unit)
+
+    def stop(self):
+        """
+        Stops the unit
+        """
+        return SystemdUnitWrapper._action_on_unit('stop', self.unit)
+
+    def restart(self):
+        """
+        Restarts the unit
+        """
+        return SystemdUnitWrapper._action_on_unit('restart', self.unit)
+
+
+class IfaceWrapper:
+    """
+    Small wrapper to check and manage ifaces
+
+    see: https://github.com/rlisagor/pynetlinux/blob/master/pynetlinux/ifconfig.py
+    """
+
+    def __init__(self, iface):
+        self.iface = iface
+        self.path = f"/sys/class/net/{iface}"
+
+    def exists(self):
+        """
+        Checks if iface exists
+        """
+        return os.path.exists(self.path)
+
+    def is_up(self):
+        """
+        Checks if iface is ip
+        """
+        return open(f"{self.path}/operstate", 'r').read().rsplit('\n') == 'up'
+
+
+    def set_addr(self, addr):
+        """
+        Set the netmask
+        """
+        process = subprocess.Popen(f"ip addr add {addr} dev {self.iface}", shell=True, stdin=None,
+                                  stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
+        process.wait()
+
+        if process.returncode == 2 or process.returncode == 0: # 2 = already set
+            return True
+
+        return False
+
+    @staticmethod
+    def set_route(addr):
+        process = subprocess.Popen(f"ip route replace default via {addr}", shell=True, stdin=None,
+                                  stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
+        process.wait()
+
+        if process.returncode > 0:
+            return False
+
+        return True
+
+
+
+def on_loaded():
+    """
+    Gets called when the plugin gets loaded
+    """
+    global READY
+    global INTERVAL
+
+    for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
+        if opt not in OPTIONS or (opt in OPTIONS and OPTIONS[opt] is None):
+            logging.error("BT-TET: Pleace specify the %s in your config.yml.", opt)
+            return
+
+    # ensure bluetooth is running
+    bt_unit = SystemdUnitWrapper('bluetooth.service')
+    if not bt_unit.is_active():
+        if not bt_unit.start():
+            logging.error("BT-TET: Can't start bluetooth.service")
+            return
+
+    INTERVAL.update()
+    READY = True
+
+
+def on_ui_update(ui):
+    """
+    Try to connect to device
+    """
+
+    if READY:
+        global INTERVAL
+        if INTERVAL.newer_then_minutes(OPTIONS['interval']):
+            return
+
+        INTERVAL.update()
+
+        bt = BTNap(OPTIONS['mac'])
+
+        logging.debug('BT-TETHER: Check if already connected and paired')
+        if bt.is_connected() and bt.is_paired():
+            logging.debug('BT-TETHER: Already connected and paired')
+            ui.set('bluetooth', 'CON')
+        else:
+            logging.debug('BT-TETHER: Try to connect to mac')
+            if bt.connect():
+                logging.info('BT-TETHER: Successfuly connected')
+            else:
+                logging.error('BT-TETHER: Could not connect')
+                ui.set('bluetooth', 'NF')
+                return
+
+        btnap_iface = IfaceWrapper('bnep0')
+        logging.debug('BT-TETHER: Check interface')
+        if btnap_iface.exists():
+            logging.debug('BT-TETHER: Interface found')
+
+            # check ip
+            addr = f"{OPTIONS['ip']}/{OPTIONS['netmask']}"
+
+            logging.debug('BT-TETHER: Try to set ADDR to interface')
+            if not btnap_iface.set_addr(addr):
+                ui.set('bluetooth', 'ERR1')
+                logging.error("BT-TETHER: Could not set ip of bnep0 to %s", addr)
+                return
+            else:
+                logging.debug('BT-TETHER: Set ADDR to interface')
+
+            # change route if sharking
+            if OPTIONS['share_internet']:
+                logging.debug('BT-TETHER: Set routing and change resolv.conf')
+                IfaceWrapper.set_route(".".join(OPTIONS['ip'].split('.')[:-1] + ['1'])) # im not proud about that
+                # fix resolv.conf; dns over https ftw!
+                with open('/etc/resolv.conf', 'r+') as resolv:
+                    nameserver = resolv.read()
+                    if 'nameserver 9.9.9.9' not in nameserver:
+                        logging.info('BT-TETHER: Added nameserver')
+                        resolv.seek(0)
+                        resolv.write(nameserver + 'nameserver 9.9.9.9\n')
+
+            ui.set('bluetooth', 'CON')
+        else:
+            logging.error('BT-TETHER: bnep0 not found')
+            ui.set('bluetooth', 'ERR2')
+
+
+def on_ui_setup(ui):
+    ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
+                                       label_font=fonts.Bold, text_font=fonts.Medium))
diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py
index c18177c..72087cf 100644
--- a/pwnagotchi/plugins/default/example.py
+++ b/pwnagotchi/plugins/default/example.py
@@ -14,13 +14,25 @@ import pwnagotchi.ui.fonts as fonts
 # Will be set with the options in config.yml config['main']['plugins'][__name__]
 OPTIONS = dict()
 
+# called when <host>:<port>/plugins/<pluginname> is opened
+def on_webhook(response, path):
+    res = "<html><body><a>Hook triggered</a></body></html>"
+    response.send_response(200)
+    response.send_header('Content-type', 'text/html')
+    response.end_headers()
+
+    try:
+        response.wfile.write(bytes(res, "utf-8"))
+    except Exception as ex:
+        logging.error(ex)
+
 # called when the plugin is loaded
 def on_loaded():
     logging.warning("WARNING: plugin %s should be disabled!" % __name__)
 
 
 # called in manual mode when there's internet connectivity
-def on_internet_available(ui, config, log):
+def on_internet_available(agent):
     pass
 
 
diff --git a/pwnagotchi/plugins/default/gps.py b/pwnagotchi/plugins/default/gps.py
index f7aa56e..234df3a 100644
--- a/pwnagotchi/plugins/default/gps.py
+++ b/pwnagotchi/plugins/default/gps.py
@@ -8,27 +8,26 @@ import logging
 import json
 import os
 
-device = '/dev/ttyUSB0'
-speed = 19200
 running = False
+OPTIONS = dict()
 
 
 def on_loaded():
-    logging.info("gps plugin loaded for %s" % device)
+    logging.info("gps plugin loaded for %s" % OPTIONS['device'])
 
 
 def on_ready(agent):
     global running
 
-    if os.path.exists(device):
-        logging.info("enabling gps bettercap's module for %s" % device)
+    if os.path.exists(OPTIONS['device']):
+        logging.info("enabling gps bettercap's module for %s" % OPTIONS['device'])
         try:
             agent.run('gps off')
         except:
             pass
 
-        agent.run('set gps.device %s' % device)
-        agent.run('set gps.speed %d' % speed)
+        agent.run('set gps.device %s' % OPTIONS['device'])
+        agent.run('set gps.speed %d' % OPTIONS['speed'])
         agent.run('gps on')
         running = True
     else:
diff --git a/pwnagotchi/plugins/default/grid.py b/pwnagotchi/plugins/default/grid.py
new file mode 100644
index 0000000..5b32635
--- /dev/null
+++ b/pwnagotchi/plugins/default/grid.py
@@ -0,0 +1,138 @@
+__author__ = 'evilsocket@gmail.com'
+__version__ = '1.0.1'
+__name__ = 'grid'
+__license__ = 'GPL3'
+__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned ' \
+                  'networks to api.pwnagotchi.ai '
+
+import os
+import logging
+import time
+import glob
+
+import pwnagotchi.grid as grid
+import pwnagotchi.utils as utils
+from pwnagotchi.ui.components import LabeledValue
+from pwnagotchi.ui.view import BLACK
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.utils import WifiInfo, extract_from_pcap
+
+OPTIONS = dict()
+REPORT = utils.StatusFile('/root/.api-report.json', data_format='json')
+
+UNREAD_MESSAGES = 0
+TOTAL_MESSAGES = 0
+
+
+def on_loaded():
+    logging.info("grid plugin loaded.")
+
+
+def parse_pcap(filename):
+    logging.info("grid: parsing %s ..." % filename)
+
+    net_id = os.path.basename(filename).replace('.pcap', '')
+
+    if '_' in net_id:
+        # /root/handshakes/ESSID_BSSID.pcap
+        essid, bssid = net_id.split('_')
+    else:
+        # /root/handshakes/BSSID.pcap
+        essid, bssid = '', net_id
+
+    it = iter(bssid)
+    bssid = ':'.join([a + b for a, b in zip(it, it)])
+
+    info = {
+        WifiInfo.ESSID: essid,
+        WifiInfo.BSSID: bssid,
+    }
+
+    try:
+        info = extract_from_pcap(filename, [WifiInfo.BSSID, WifiInfo.ESSID])
+    except Exception as e:
+        logging.error("grid: %s" % e)
+
+    return info[WifiInfo.ESSID], info[WifiInfo.BSSID]
+
+
+def is_excluded(what):
+    for skip in OPTIONS['exclude']:
+        skip = skip.lower()
+        what = what.lower()
+        if skip in what or skip.replace(':', '') in what:
+            return True
+    return False
+
+
+def on_ui_update(ui):
+    if UNREAD_MESSAGES > 0:
+        ui.on_unread_messages(UNREAD_MESSAGES, TOTAL_MESSAGES)
+
+
+def set_reported(reported, net_id):
+    global REPORT
+    reported.append(net_id)
+    REPORT.update(data={'reported': reported})
+
+
+def on_internet_available(agent):
+    global REPORT, UNREAD_MESSAGES, TOTAL_MESSAGES
+
+    logging.debug("internet available")
+
+    try:
+        grid.update_data(agent.last_session)
+    except Exception as e:
+        logging.error("error connecting to the pwngrid-peer service: %s" % e)
+        return
+
+    try:
+        logging.debug("checking mailbox ...")
+
+        messages = grid.inbox()
+        TOTAL_MESSAGES = len(messages)
+        UNREAD_MESSAGES = len([m for m in messages if m['seen_at'] is None])
+
+        if TOTAL_MESSAGES:
+            on_ui_update(agent.view())
+            logging.debug(" %d unread messages of %d total" % (UNREAD_MESSAGES, TOTAL_MESSAGES))
+
+        logging.debug("checking pcaps")
+
+        pcap_files = glob.glob(os.path.join(agent.config()['bettercap']['handshakes'], "*.pcap"))
+        num_networks = len(pcap_files)
+        reported = REPORT.data_field_or('reported', default=[])
+        num_reported = len(reported)
+        num_new = num_networks - num_reported
+
+        if num_new > 0:
+            if OPTIONS['report']:
+                logging.info("grid: %d new networks to report" % num_new)
+                logging.debug("OPTIONS: %s" % OPTIONS)
+                logging.debug("  exclude: %s" % OPTIONS['exclude'])
+
+                for pcap_file in pcap_files:
+                    net_id = os.path.basename(pcap_file).replace('.pcap', '')
+                    if net_id not in reported:
+                        if is_excluded(net_id):
+                            logging.debug("skipping %s due to exclusion filter" % pcap_file)
+                            set_reported(reported, net_id)
+                            continue
+
+                        essid, bssid = parse_pcap(pcap_file)
+                        if bssid:
+                            if is_excluded(essid) or is_excluded(bssid):
+                                logging.debug("not reporting %s due to exclusion filter" % pcap_file)
+                                set_reported(reported, net_id)
+                            else:
+                                if grid.report_ap(essid, bssid):
+                                    set_reported(reported, net_id)
+                                time.sleep(1.5)
+                        else:
+                            logging.warning("no bssid found?!")
+            else:
+                logging.debug("grid: reporting disabled")
+
+    except Exception as e:
+        logging.error("grid api: %s" % e)
diff --git a/pwnagotchi/plugins/default/memtemp.py b/pwnagotchi/plugins/default/memtemp.py
index 4ef823b..3d6682b 100644
--- a/pwnagotchi/plugins/default/memtemp.py
+++ b/pwnagotchi/plugins/default/memtemp.py
@@ -1,68 +1,64 @@
-# tempmem shows memory infos and cpu temperature
+# memtemp shows memory infos and cpu temperature
 #
-# totalmem usedmem freemem cputemp
+# mem usage, cpu load, cpu temp
 #
+###############################################################
+#
+# Updated 18-10-2019 by spees <speeskonijn@gmail.com>
+# - Changed the place where the data was displayed on screen
+# - Made the data a bit more compact and easier to read
+# - removed the label so we wont waste screen space
+# - Updated version to 1.0.1
+#
+# 20-10-2019 by spees <speeskonijn@gmail.com>
+# - Refactored to use the already existing functions
+# - Now only shows memory usage in percentage
+# - Added CPU load
+# - Added horizontal and vertical orientation
+#
+###############################################################
+
 __author__ = 'https://github.com/xenDE'
-__version__ = '1.0.0'
+__version__ = '1.0.1'
 __name__ = 'memtemp'
 __license__ = 'GPL3'
-__description__ = 'A plugin that will add a memory and temperature indicator'
-
-import struct
+__description__ = 'A plugin that will display memory/cpu usage and temperature'
 
 from pwnagotchi.ui.components import LabeledValue
 from pwnagotchi.ui.view import BLACK
 import pwnagotchi.ui.fonts as fonts
+import pwnagotchi
+import logging
 
-import time
-
-
-class MEMTEMP:
-
-    # set the minimum seconds before refresh the values
-    refresh_wait = 30
-    
-    refresh_ts_last = time.time() - refresh_wait
-    
-    def __init__(self):
-        # only import when the module is loaded and enabled
-        import os
-
-    def get_temp(self):
-        try:
-            temp = os.popen('/opt/vc/bin/vcgencmd measure_temp').readlines()[0].split('=')[1].replace("\n", '').replace("'","")
-            return 'cpu:' + temp
-        except:
-            return 'cpu:0.0C'
-        # cpu:37.4C
-
-    def get_mem_info(self):
-        try:
-            # includes RAM + Swap Memory:
-#            total, used, free = map(int, os.popen('free -t -m').readlines()[-1].split()[1:])
-            # without Swap, only real memory:
-            total, used, free = map(int, os.popen('free -t -m').readlines()[-3].split()[1:4])
-            return "tm:"+str(total)+" um:"+str(used)+" fm:"+str(free)
-        except:
-            return "tm:0 um:0 fm:0"
-        # tm:532 um:82 fm:353
-
-
-memtemp = None
+OPTIONS = dict()
 
 
 def on_loaded():
-    global memtemp
-    memtemp = MEMTEMP()
+    logging.info("memtemp plugin loaded.")
+
+
+def mem_usage():
+    return int(pwnagotchi.mem_usage() * 100)
+
+
+def cpu_load():
+    return int(pwnagotchi.cpu_load() * 100)
 
 
 def on_ui_setup(ui):
-    ui.add_element('memtemp', LabeledValue(color=BLACK, label='SYS', value='tm:0 um:0 fm:0 0.0C', position=(0, ui.height()-28),
-                                       label_font=fonts.Bold, text_font=fonts.Medium))
+    if OPTIONS['orientation'] == "horizontal":
+        ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value='mem cpu temp\n - -  -',
+                                               position=(ui.width() / 2 + 30, ui.height() / 2 + 15),
+                                               label_font=fonts.Small, text_font=fonts.Small))
+    elif OPTIONS['orientation'] == "vertical":
+        ui.add_element('memtemp', LabeledValue(color=BLACK, label='', value=' mem:-\n cpu:-\ntemp:-',
+                                               position=(ui.width() / 2 + 55, ui.height() / 2),
+                                               label_font=fonts.Small, text_font=fonts.Small))
 
 
 def on_ui_update(ui):
-    if time.time() > memtemp.refresh_ts_last + memtemp.refresh_wait:
-        ui.set('memtemp', "%s %s" % (memtemp.get_mem_info(), memtemp.get_temp()))
-        memtemp.refresh_ts_last = time.time()
+    if OPTIONS['orientation'] == "horizontal":
+        ui.set('memtemp', " mem cpu temp\n %s%% %s%%  %sc" % (mem_usage(), cpu_load(), pwnagotchi.temperature()))
 
+    elif OPTIONS['orientation'] == "vertical":
+        ui.set('memtemp', " mem:%s%%\n cpu:%s%%\ntemp:%sc" % (mem_usage(), cpu_load(), pwnagotchi.temperature()))
diff --git a/pwnagotchi/plugins/default/net-pos.py b/pwnagotchi/plugins/default/net-pos.py
new file mode 100644
index 0000000..d0c54ae
--- /dev/null
+++ b/pwnagotchi/plugins/default/net-pos.py
@@ -0,0 +1,140 @@
+__author__ = 'zenzen san'
+__version__ = '2.0.0'
+__name__ = 'net-pos'
+__license__ = 'GPL3'
+__description__ = """Saves a json file with the access points with more signal
+                     whenever a handshake is captured.
+                     When internet is available the files are converted in geo locations
+                     using Mozilla LocationService """
+
+import logging
+import json
+import os
+import requests
+from pwnagotchi.utils import StatusFile
+
+MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
+REPORT = StatusFile('/root/.net_pos_saved', data_format='json')
+SKIP = list()
+READY = False
+OPTIONS = dict()
+
+
+def on_loaded():
+    global READY
+
+    if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
+        logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
+        return
+
+    READY = True
+
+    logging.info("net-pos plugin loaded.")
+
+def _append_saved(path):
+    to_save = list()
+    if isinstance(path, str):
+        to_save.append(path)
+    elif isinstance(path, list):
+        to_save += path
+    else:
+        raise TypeError("Expected list or str, got %s" % type(path))
+
+    with open('/root/.net_pos_saved', 'a') as saved_file:
+        for x in to_save:
+            saved_file.write(x + "\n")
+
+def on_internet_available(agent):
+    global SKIP
+    global REPORT
+
+    if READY:
+        config = agent.config()
+        display = agent.view()
+        reported = REPORT.data_field_or('reported', default=list())
+        handshake_dir = config['bettercap']['handshakes']
+
+        all_files = os.listdir(handshake_dir)
+        all_np_files = [os.path.join(handshake_dir, filename)
+                     for filename in all_files
+                     if filename.endswith('.net-pos.json')]
+        new_np_files = set(all_np_files) - set(reported) - set(SKIP)
+
+        if new_np_files:
+            logging.info("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
+            display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
+            display.update(force=True)
+            for idx, np_file in enumerate(new_np_files):
+
+                geo_file = np_file.replace('.net-pos.json', '.geo.json')
+                if os.path.exists(geo_file):
+                    # got already the position
+                    reported.append(np_file)
+                    REPORT.update(data={'reported': reported})
+                    continue
+
+                try:
+                    geo_data = _get_geo_data(np_file) # returns json obj
+                except requests.exceptions.RequestException as req_e:
+                    logging.error("NET-POS: %s", req_e)
+                    SKIP += np_file
+                    continue
+                except json.JSONDecodeError as js_e:
+                    logging.error("NET-POS: %s", js_e)
+                    SKIP += np_file
+                    continue
+                except OSError as os_e:
+                    logging.error("NET-POS: %s", os_e)
+                    SKIP += np_file
+                    continue
+
+                with open(geo_file, 'w+t') as sf:
+                    json.dump(geo_data, sf)
+
+                reported.append(np_file)
+                REPORT.update(data={'reported': reported})
+
+                display.set('status', f"Fetching positions ({idx+1}/{len(new_np_files)})")
+                display.update(force=True)
+
+
+def on_handshake(agent, filename, access_point, client_station):
+    netpos = _get_netpos(agent)
+    netpos_filename = filename.replace('.pcap', '.net-pos.json')
+    logging.info("NET-POS: Saving net-location to %s", netpos_filename)
+
+    try:
+        with open(netpos_filename, 'w+t') as net_pos_file:
+            json.dump(netpos, net_pos_file)
+    except OSError as os_e:
+        logging.error("NET-POS: %s", os_e)
+
+
+def _get_netpos(agent):
+    aps = agent.get_access_points()
+    netpos = dict()
+    netpos['wifiAccessPoints'] = list()
+    # 6 seems a good number to save a wifi networks location
+    for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]:
+        netpos['wifiAccessPoints'].append({'macAddress': access_point['mac'],
+                                           'signalStrength': access_point['rssi']})
+    return netpos
+
+def _get_geo_data(path, timeout=30):
+    geourl = MOZILLA_API_URL.format(api=OPTIONS['api_key'])
+
+    try:
+        with open(path, "r") as json_file:
+            data = json.load(json_file)
+    except json.JSONDecodeError as js_e:
+        raise js_e
+    except OSError as os_e:
+        raise os_e
+
+    try:
+        result = requests.post(geourl,
+                json=data,
+                timeout=timeout)
+        return result.json()
+    except requests.exceptions.RequestException as req_e:
+        raise req_e
diff --git a/pwnagotchi/plugins/default/onlinehashcrack.py b/pwnagotchi/plugins/default/onlinehashcrack.py
index 249ea19..dbf8a4e 100644
--- a/pwnagotchi/plugins/default/onlinehashcrack.py
+++ b/pwnagotchi/plugins/default/onlinehashcrack.py
@@ -1,5 +1,5 @@
 __author__ = '33197631+dadav@users.noreply.github.com'
-__version__ = '1.0.0'
+__version__ = '2.0.0'
 __name__ = 'onlinehashcrack'
 __license__ = 'GPL3'
 __description__ = 'This plugin automatically uploades handshakes to https://onlinehashcrack.com'
@@ -7,9 +7,11 @@ __description__ = 'This plugin automatically uploades handshakes to https://onli
 import os
 import logging
 import requests
+from pwnagotchi.utils import StatusFile
 
 READY = False
-ALREADY_UPLOADED = None
+REPORT = StatusFile('/root/.ohc_uploads', data_format='json')
+SKIP = list()
 OPTIONS = dict()
 
 
@@ -18,20 +20,11 @@ def on_loaded():
     Gets called when the plugin gets loaded
     """
     global READY
-    global EMAIL
-    global ALREADY_UPLOADED
 
-    if not 'email' in OPTIONS or ('email' in OPTIONS and OPTIONS['email'] is None):
+    if 'email' not in OPTIONS or ('email' in OPTIONS and OPTIONS['email'] is None):
         logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
         return
 
-    try:
-        with open('/root/.ohc_uploads', 'r') as f:
-            ALREADY_UPLOADED = f.read().splitlines()
-    except OSError:
-        logging.warning('OHC: No upload-file found.')
-        ALREADY_UPLOADED = []
-
     READY = True
 
 
@@ -55,15 +48,21 @@ def _upload_to_ohc(path, timeout=30):
             raise e
 
 
-def on_internet_available(display, config, log):
+def on_internet_available(agent):
     """
     Called in manual mode when there's internet connectivity
     """
+    global REPORT
+    global SKIP
     if READY:
+        display = agent.view()
+        config = agent.config()
+        reported = REPORT.data_field_or('reported', default=list())
+
         handshake_dir = config['bettercap']['handshakes']
         handshake_filenames = os.listdir(handshake_dir)
-        handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
-        handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
+        handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
+        handshake_new = set(handshake_paths) - set(reported) - set(SKIP)
 
         if handshake_new:
             logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
@@ -73,12 +72,15 @@ def on_internet_available(display, config, log):
                 display.update(force=True)
                 try:
                     _upload_to_ohc(handshake)
-                    ALREADY_UPLOADED.append(handshake)
-                    with open('/root/.ohc_uploads', 'a') as f:
-                        f.write(handshake + "\n")
+                    reported.append(handshake)
+                    REPORT.update(data={'reported': reported})
                     logging.info(f"OHC: Successfuly uploaded {handshake}")
-                except requests.exceptions.RequestException:
-                    pass
+                except requests.exceptions.RequestException as req_e:
+                    SKIP.append(handshake)
+                    logging.error("OHC: %s", req_e)
+                    continue
                 except OSError as os_e:
-                    logging.error(f"OHC: Got the following error: {os_e}")
+                    SKIP.append(handshake)
+                    logging.error("OHC: %s", os_e)
+                    continue
 
diff --git a/pwnagotchi/plugins/default/quickdic.py b/pwnagotchi/plugins/default/quickdic.py
new file mode 100644
index 0000000..b898b21
--- /dev/null
+++ b/pwnagotchi/plugins/default/quickdic.py
@@ -0,0 +1,52 @@
+__author__ = 'pwnagotchi [at] rossmarks [dot] uk'
+__version__ = '1.0.0'
+__name__ = 'quickdic'
+__license__ = 'GPL3'
+__description__ = 'Run a quick dictionary scan against captured handshakes'
+
+'''
+Aircrack-ng needed, to install:
+> apt-get install aircrack-ng
+Upload wordlist files in .txt format to folder in config file (Default: /opt/wordlists/)
+Cracked handshakes stored in handshake folder as [essid].pcap.cracked 
+'''
+
+import logging
+import subprocess
+import string
+import re
+
+OPTIONS = dict()
+
+def on_loaded():
+    logging.info("Quick dictionary check plugin loaded")
+
+def on_handshake(agent, filename, access_point, client_station):
+    display = agent._view
+
+    result = subprocess.run(('/usr/bin/aircrack-ng '+ filename +' | grep "1 handshake" | awk \'{print $2}\''),shell=True, stdout=subprocess.PIPE)
+    result = result.stdout.decode('utf-8').translate({ord(c) :None for c in string.whitespace})
+    if not result:
+        logging.info("[quickdic] No handshake")
+    else:
+        logging.info("[quickdic] Handshake confirmed")
+        result2 = subprocess.run(('aircrack-ng -w `echo '+OPTIONS['wordlist_folder']+'*.txt | sed \'s/\ /,/g\'` -l '+filename+'.cracked -q -b '+result+' '+filename+' | grep KEY'),shell=True,stdout=subprocess.PIPE)
+        result2 = result2.stdout.decode('utf-8').strip()
+        logging.info("[quickdic] "+result2)
+        if result2 != "KEY NOT FOUND":
+            key = re.search('\[(.*)\]', result2)
+            pwd = str(key.group(1))
+            set_text("Cracked password: "+pwd)
+            display.update(force=True)
+
+text_to_set = "";
+def set_text(text):
+    global text_to_set
+    text_to_set = text
+
+def on_ui_update(ui):
+    global text_to_set
+    if text_to_set:
+        ui.set('face', "(·ω·)")
+        ui.set('status', text_to_set)
+        text_to_set = ""
diff --git a/pwnagotchi/plugins/default/screen_refresh.py b/pwnagotchi/plugins/default/screen_refresh.py
new file mode 100644
index 0000000..94173ff
--- /dev/null
+++ b/pwnagotchi/plugins/default/screen_refresh.py
@@ -0,0 +1,24 @@
+__author__ = 'pwnagotcchi [at] rossmarks [dot] uk'
+__version__ = '1.0.0'
+__name__ = 'screen_refresh'
+__license__ = 'GPL3'
+__description__ = 'Refresh he e-ink display after X amount of updates'
+
+import logging
+
+OPTIONS = dict()
+update_count = 0;
+
+
+def on_loaded():
+    logging.info("Screen refresh plugin loaded")
+
+
+def on_ui_update(ui):
+    global update_count
+    update_count += 1
+    if update_count == OPTIONS['refresh_interval']:
+        ui.init_display()
+        ui.set('status', "Screen cleaned")
+        logging.info("Screen refreshing")
+        update_count = 0
diff --git a/pwnagotchi/plugins/default/twitter.py b/pwnagotchi/plugins/default/twitter.py
index 560903a..fd247ca 100644
--- a/pwnagotchi/plugins/default/twitter.py
+++ b/pwnagotchi/plugins/default/twitter.py
@@ -14,8 +14,12 @@ def on_loaded():
 
 
 # called in manual mode when there's internet connectivity
-def on_internet_available(ui, config, log):
-    if log.is_new() and log.handshakes > 0:
+def on_internet_available(agent):
+    config = agent.config()
+    display = agent.view()
+    last_session = agent.last_session
+
+    if last_session.is_new() and last_session.handshakes > 0:
         try:
             import tweepy
         except ImportError:
@@ -26,20 +30,20 @@ def on_internet_available(ui, config, log):
 
         picture = '/dev/shm/pwnagotchi.png'
 
-        ui.on_manual_mode(log)
-        ui.update(force=True)
-        ui.image().save(picture, 'png')
-        ui.set('status', 'Tweeting...')
-        ui.update(force=True)
+        display.on_manual_mode(last_session)
+        display.update(force=True)
+        display.image().save(picture, 'png')
+        display.set('status', 'Tweeting...')
+        display.update(force=True)
 
         try:
             auth = tweepy.OAuthHandler(OPTIONS['consumer_key'], OPTIONS['consumer_secret'])
             auth.set_access_token(OPTIONS['access_token_key'], OPTIONS['access_token_secret'])
             api = tweepy.API(auth)
 
-            tweet = Voice(lang=config['main']['lang']).on_log_tweet(log)
+            tweet = Voice(lang=config['main']['lang']).on_last_session_tweet(last_session)
             api.update_with_media(filename=picture, status=tweet)
-            log.save_session_id()
+            last_session.save_session_id()
 
             logging.info("tweeted: %s" % tweet)
         except Exception as e:
diff --git a/pwnagotchi/plugins/default/unfiltered_example.py b/pwnagotchi/plugins/default/unfiltered_example.py
new file mode 100644
index 0000000..c883b7c
--- /dev/null
+++ b/pwnagotchi/plugins/default/unfiltered_example.py
@@ -0,0 +1,22 @@
+__author__ = 'diemelcw@gmail.com'
+__version__ = '1.0.0'
+__name__ = 'unfiltered_example'
+__license__ = 'GPL3'
+__description__ = 'An example plugin for pwnagotchi that implements on_unfiltered_ap_list(agent,aps)'
+
+import logging
+
+# Will be set with the options in config.yml config['main']['plugins'][__name__]
+OPTIONS = dict()
+
+# called when the plugin is loaded
+def on_loaded():
+    logging.warning("%s plugin loaded" % __name__)
+    
+# called when AP list is ready, before whitelist filtering has occured
+def on_unfiltered_ap_list(agent,aps):
+    logging.info("Unfiltered AP list to follow")
+    for ap in aps:
+        logging.info(ap['hostname'])
+    
+    ## Additional logic here ##
diff --git a/pwnagotchi/plugins/default/wigle.py b/pwnagotchi/plugins/default/wigle.py
new file mode 100644
index 0000000..61bc3de
--- /dev/null
+++ b/pwnagotchi/plugins/default/wigle.py
@@ -0,0 +1,195 @@
+__author__ = '33197631+dadav@users.noreply.github.com'
+__version__ = '2.0.0'
+__name__ = 'wigle'
+__license__ = 'GPL3'
+__description__ = 'This plugin automatically uploades collected wifis to wigle.net'
+
+import os
+import logging
+import json
+from io import StringIO
+import csv
+from datetime import datetime
+import requests
+from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile
+
+READY = False
+REPORT = StatusFile('/root/.wigle_uploads', data_format='json')
+SKIP = list()
+OPTIONS = dict()
+
+
+def on_loaded():
+    """
+    Gets called when the plugin gets loaded
+    """
+    global READY
+
+    if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
+        logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
+        return
+
+    READY = True
+
+
+def _extract_gps_data(path):
+    """
+    Extract data from gps-file
+
+    return json-obj
+    """
+
+    try:
+        with open(path, 'r') as json_file:
+            return json.load(json_file)
+    except OSError as os_err:
+        raise os_err
+    except json.JSONDecodeError as json_err:
+        raise json_err
+
+
+def _format_auth(data):
+    out = ""
+    for auth in data:
+        out = f"{out}[{auth}]"
+    return out
+
+def _transform_wigle_entry(gps_data, pcap_data):
+    """
+    Transform to wigle entry in file
+    """
+    dummy = StringIO()
+    # write kismet header
+    dummy.write("WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n")
+    dummy.write("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
+
+    writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE)
+    writer.writerow([
+        pcap_data[WifiInfo.BSSID],
+        pcap_data[WifiInfo.ESSID],
+        _format_auth(pcap_data[WifiInfo.ENCRYPTION]),
+        datetime.strptime(gps_data['Updated'].rsplit('.')[0],
+                          "%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M:%S'),
+        pcap_data[WifiInfo.CHANNEL],
+        pcap_data[WifiInfo.RSSI],
+        gps_data['Latitude'],
+        gps_data['Longitude'],
+        gps_data['Altitude'],
+        0, # accuracy?
+        'WIFI'])
+    return dummy.getvalue()
+
+def _send_to_wigle(lines, api_key, timeout=30):
+    """
+    Uploads the file to wigle-net
+    """
+
+    dummy = StringIO()
+
+    for line in lines:
+        dummy.write(f"{line}")
+
+    dummy.seek(0)
+
+    headers = {'Authorization': f"Basic {api_key}",
+               'Accept': 'application/json'}
+    data = {'donate': 'false'}
+    payload = {'file': dummy, 'type': 'text/csv'}
+
+    try:
+        res = requests.post('https://api.wigle.net/api/v2/file/upload',
+                            data=data,
+                            headers=headers,
+                            files=payload,
+                            timeout=timeout)
+        json_res = res.json()
+        if not json_res['success']:
+            raise requests.exceptions.RequestException(json_res['message'])
+    except requests.exceptions.RequestException as re_e:
+        raise re_e
+
+
+def on_internet_available(agent):
+    from scapy.all import Scapy_Exception
+    """
+    Called in manual mode when there's internet connectivity
+    """
+    global REPORT
+    global SKIP
+
+    if READY:
+        config = agent.config()
+        display = agent.view()
+        reported = REPORT.data_field_or('reported', default=list())
+
+        handshake_dir = config['bettercap']['handshakes']
+        all_files = os.listdir(handshake_dir)
+        all_gps_files = [os.path.join(handshake_dir, filename)
+                     for filename in all_files
+                     if filename.endswith('.gps.json')]
+        new_gps_files = set(all_gps_files) - set(reported) - set(SKIP)
+
+        if new_gps_files:
+            logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
+
+            csv_entries = list()
+            no_err_entries = list()
+
+            for gps_file in new_gps_files:
+                pcap_filename = gps_file.replace('.gps.json', '.pcap')
+
+                if not os.path.exists(pcap_filename):
+                    logging.error("WIGLE: Can't find pcap for %s", gps_file)
+                    SKIP.append(gps_file)
+                    continue
+
+                try:
+                    gps_data = _extract_gps_data(gps_file)
+                except OSError as os_err:
+                    logging.error("WIGLE: %s", os_err)
+                    SKIP.append(gps_file)
+                    continue
+                except json.JSONDecodeError as json_err:
+                    logging.error("WIGLE: %s", json_err)
+                    SKIP.append(gps_file)
+                    continue
+
+                if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
+                    logging.warning("WIGLE: Not enough gps-informations for %s. Trying again next time.", gps_file)
+                    SKIP.append(gps_file)
+                    continue
+
+
+                try:
+                    pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
+                                                                  WifiInfo.ESSID,
+                                                                  WifiInfo.ENCRYPTION,
+                                                                  WifiInfo.CHANNEL,
+                                                                  WifiInfo.RSSI])
+                except FieldNotFoundError:
+                    logging.error("WIGLE: Could not extract all informations. Skip %s", gps_file)
+                    SKIP.append(gps_file)
+                    continue
+                except Scapy_Exception as sc_e:
+                    logging.error("WIGLE: %s", sc_e)
+                    SKIP.append(gps_file)
+                    continue
+
+                new_entry = _transform_wigle_entry(gps_data, pcap_data)
+                csv_entries.append(new_entry)
+                no_err_entries.append(gps_file)
+
+            if csv_entries:
+                display.set('status', "Uploading gps-data to wigle.net ...")
+                display.update(force=True)
+                try:
+                    _send_to_wigle(csv_entries, OPTIONS['api_key'])
+                    reported += no_err_entries
+                    REPORT.update(data={'reported': reported})
+                    logging.info("WIGLE: Successfuly uploaded %d files", len(no_err_entries))
+                except requests.exceptions.RequestException as re_e:
+                    SKIP += no_err_entries
+                    logging.error("WIGLE: Got an exception while uploading %s", re_e)
+                except OSError as os_e:
+                    SKIP += no_err_entries
+                    logging.error("WIGLE: Got the following error: %s", os_e)
diff --git a/pwnagotchi/plugins/default/wpa-sec.py b/pwnagotchi/plugins/default/wpa-sec.py
index 68e4171..6bf9994 100644
--- a/pwnagotchi/plugins/default/wpa-sec.py
+++ b/pwnagotchi/plugins/default/wpa-sec.py
@@ -1,5 +1,5 @@
 __author__ = '33197631+dadav@users.noreply.github.com'
-__version__ = '1.0.0'
+__version__ = '2.0.1'
 __name__ = 'wpa-sec'
 __license__ = 'GPL3'
 __description__ = 'This plugin automatically uploades handshakes to https://wpa-sec.stanev.org'
@@ -7,9 +7,12 @@ __description__ = 'This plugin automatically uploades handshakes to https://wpa-
 import os
 import logging
 import requests
+from pwnagotchi.utils import StatusFile
 
 READY = False
-ALREADY_UPLOADED = None
+REPORT = StatusFile('/root/.wpa_sec_uploads', data_format='json')
+OPTIONS = dict()
+SKIP = list()
 
 
 def on_loaded():
@@ -17,67 +20,68 @@ def on_loaded():
     Gets called when the plugin gets loaded
     """
     global READY
-    global API_KEY
-    global ALREADY_UPLOADED
 
-    if not 'api_key' in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
+    if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
         logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
         return
 
-    try:
-        with open('/root/.wpa_sec_uploads', 'r') as f:
-            ALREADY_UPLOADED = f.read().splitlines()
-    except OSError:
-        logging.warning('WPA_SEC: No upload-file found.')
-        ALREADY_UPLOADED = []
-
+    if 'api_url' not in OPTIONS or ('api_url' in OPTIONS and OPTIONS['api_url'] is None):
+        logging.error("WPA_SEC: API-URL isn't set. Can't upload, no endpoint configured.")
+        return
+        
     READY = True
 
 
 def _upload_to_wpasec(path, timeout=30):
     """
-    Uploads the file to wpa-sec.stanev.org
+    Uploads the file to https://wpa-sec.stanev.org, or another endpoint. 
     """
     with open(path, 'rb') as file_to_upload:
-        headers = {'key': OPTIONS['api_key']}
+        cookie = {'key': OPTIONS['api_key']}
         payload = {'file': file_to_upload}
 
         try:
-            result = requests.post('https://wpa-sec.stanev.org/?submit',
-                    headers=headers,
+            result = requests.post(OPTIONS['api_url'],
+                    cookies=cookie,
                     files=payload,
                     timeout=timeout)
             if ' already submitted' in result.text:
-                logging.warning(f"{path} was already submitted.")
-        except requests.exceptions.RequestException as e:
-            logging.error(f"WPA_SEC: Got an exception while uploading {path} -> {e}")
-            raise e
+                logging.warning("%s was already submitted.", path)
+        except requests.exceptions.RequestException as req_e:
+            raise req_e
 
 
-def on_internet_available(display, config, log):
+def on_internet_available(agent):
     """
     Called in manual mode when there's internet connectivity
     """
+    global REPORT
+    global SKIP
     if READY:
+        config = agent.config()
+        display = agent.view()
+        reported = REPORT.data_field_or('reported', default=list())
+
         handshake_dir = config['bettercap']['handshakes']
         handshake_filenames = os.listdir(handshake_dir)
-        handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
-        handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
+        handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
+        handshake_new = set(handshake_paths) - set(reported) - set(SKIP)
 
         if handshake_new:
-            logging.info("WPA_SEC: Internet connectivity detected.\
-                          Uploading new handshakes to wpa-sec.stanev.org")
+            logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
 
             for idx, handshake in enumerate(handshake_new):
                 display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
                 display.update(force=True)
                 try:
                     _upload_to_wpasec(handshake)
-                    ALREADY_UPLOADED.append(handshake)
-                    with open('/root/.wpa_sec_uploads', 'a') as f:
-                        f.write(handshake + "\n")
-                    logging.info(f"WPA_SEC: Successfuly uploaded {handshake}")
-                except requests.exceptions.RequestException:
-                    pass
+                    reported.append(handshake)
+                    REPORT.update(data={'reported': reported})
+                    logging.info("WPA_SEC: Successfuly uploaded %s", handshake)
+                except requests.exceptions.RequestException as req_e:
+                    SKIP.append(handshake)
+                    logging.error("WPA_SEC: %s", req_e)
+                    continue
                 except OSError as os_e:
-                    logging.error(f"WPA_SEC: Got the following error: {os_e}")
+                    logging.error("WPA_SEC: %s", os_e)
+                    continue
diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py
index 76e9a24..c28b4d7 100644
--- a/pwnagotchi/ui/display.py
+++ b/pwnagotchi/ui/display.py
@@ -1,210 +1,66 @@
-import _thread
-from threading import Lock
-
-import shutil
-import logging
 import os
-import pwnagotchi, pwnagotchi.plugins as plugins
+import logging
+import threading
 
-from pwnagotchi.ui.view import WHITE, View
-
-from http.server import BaseHTTPRequestHandler, HTTPServer
-
-
-class VideoHandler(BaseHTTPRequestHandler):
-    _lock = Lock()
-    _index = """<html>
-  <head>
-    <title>%s</title>
-  </head>
-  <body>
-    <img src="/ui" id="ui"/>
-
-    <script type="text/javascript">
-    window.onload = function() {
-        var image = document.getElementById("ui");
-        function updateImage() {
-            image.src = image.src.split("?")[0] + "?" + new Date().getTime();
-        }
-        setInterval(updateImage, %d);
-    }
-    </script>
-  </body>
-</html>"""
-
-    @staticmethod
-    def render(img):
-        with VideoHandler._lock:
-            img.save("/root/pwnagotchi.png", format='PNG')
-
-    def log_message(self, format, *args):
-        return
-
-    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 % (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.end_headers()
-                try:
-                    with open("/root/pwnagotchi.png", 'rb') as fp:
-                        shutil.copyfileobj(fp, self.wfile)
-                except:
-                    pass
-        else:
-            self.send_response(404)
+import pwnagotchi.plugins as plugins
+import pwnagotchi.ui.hw as hw
+import pwnagotchi.ui.web as web
+from pwnagotchi.ui.view import View
 
 
 class Display(View):
     def __init__(self, config, state={}):
-        super(Display, self).__init__(config, state)
-        self._enabled = config['ui']['display']['enabled']
-        self._rotation = config['ui']['display']['rotation']
-        self._video_enabled = config['ui']['display']['video']['enabled']
-        self._video_port = config['ui']['display']['video']['port']
-        self._video_address = config['ui']['display']['video']['address']
-        self._display_type = config['ui']['display']['type']
-        self._display_color = config['ui']['display']['color']
+        super(Display, self).__init__(config, hw.display_for(config), state)
+        config = config['ui']['display']
 
-        self._render_cb = None
-        self._display = None
-        self._httpd = None
+        self._enabled = config['enabled']
+        self._rotation = config['rotation']
+        self._webui = web.Server(config)
 
+        self.init_display()
+
+        self._canvas_next_event = threading.Event()
+        self._canvas_next = None
+        self._render_thread_instance = threading.Thread(
+            target=self._render_thread,
+            daemon=True
+        )
+        self._render_thread_instance.start()
+
+    def is_inky(self):
+        return self._implementation.name == 'inky'
+
+    def is_papirus(self):
+        return self._implementation.name == 'papirus'
+
+    def is_waveshare_v1(self):
+        return self._implementation.name == 'waveshare_1'
+
+    def is_waveshare_v2(self):
+        return self._implementation.name == 'waveshare_2'
+
+    def is_waveshare27inch(self):
+        return self._implementation.name == 'waveshare27inch'
+
+    def is_oledhat(self):
+        return self._implementation.name == 'oledhat'
+
+    def is_lcdhat(self):
+        return self._implementation.name == 'lcdhat'
+
+    def is_waveshare_any(self):
+        return self.is_waveshare_v1() or self.is_waveshare_v2()
+
+    def init_display(self):
         if self._enabled:
-            self._init_display()
+            self._implementation.initialize()
+            plugins.on('display_setup', self._implementation)
         else:
-            self.on_render(self._on_view_rendered)
             logging.warning("display module is disabled")
-
-        if self._video_enabled:
-            _thread.start_new_thread(self._http_serve, ())
-
-    def _http_serve(self):
-        if self._video_address is not None:
-            self._httpd = HTTPServer((self._video_address, self._video_port), VideoHandler)
-            logging.info("ui available at http://%s:%d/" % (self._video_address, self._video_port))
-            self._httpd.serve_forever()
-        else:
-            logging.info("could not get ip of usb0, video server not starting")
-
-    def _is_inky(self):
-        return self._display_type in ('inkyphat', 'inky')
-
-    def _is_papirus(self):
-        return self._display_type in ('papirus', 'papi')
-
-    def _is_waveshare_v1(self):
-        return self._display_type in ('waveshare_1', 'ws_1', 'waveshare1', 'ws1')
-
-    def _is_waveshare_v2(self):
-        return self._display_type in ('waveshare_2', 'ws_2', 'waveshare2', 'ws2')
-
-    def _is_waveshare(self):
-        return self._is_waveshare_v1() or self._is_waveshare_v2()
-
-    def _init_display(self):
-        if self._is_inky():
-            logging.info("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():
-            logging.info("initializing papirus display")
-            from pwnagotchi.ui.papirus.epd import EPD
-            os.environ['EPD_SIZE'] = '2.0'
-            self._display = EPD()
-            self._display.clear()
-            self._render_cb = self._papirus_render
-
-        elif self._is_waveshare_v1():
-            logging.info("initializing waveshare v1 display")
-            from pwnagotchi.ui.waveshare.v1.epd2in13 import EPD
-            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_waveshare_v2():
-            logging.info("initializing waveshare v2 display")
-            from pwnagotchi.ui.waveshare.v2.waveshare import EPD
-            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:
-            logging.critical("unknown display type %s" % self._display_type)
-
-        plugins.on('display_setup', self._display)
-
         self.on_render(self._on_view_rendered)
 
     def clear(self):
-        if self._display is None:
-            logging.error("no display object created")
-        elif self._is_inky():
-            self._display.Clear()
-        elif self._is_papirus():
-            self._display.clear()
-        elif self._is_waveshare():
-            self._display.Clear(WHITE)
-        else:
-            logging.critical("unknown display type %s" % self._display_type)
-
-    def _inky_render(self):
-        if self._display_color != 'mono':
-            display_colors = 3
-        else:
-            display_colors = 2
-
-        img_buffer = self._canvas.convert('RGB').convert('P', palette=1, colors=display_colors)
-        if self._display_color == 'red':
-            img_buffer.putpalette([
-                255, 255, 255,  # index 0 is white
-                0, 0, 0,  # index 1 is black
-                255, 0, 0  # index 2 is red
-            ])
-        elif self._display_color == 'yellow':
-            img_buffer.putpalette([
-                255, 255, 255,  # index 0 is white
-                0, 0, 0,  # index 1 is black
-                255, 255, 0  # index 2 is yellow
-            ])
-        else:
-            img_buffer.putpalette([
-                255, 255, 255,  # index 0 is white
-                0, 0, 0  # index 1 is black
-            ])
-
-        self._display.set_image(img_buffer)
-        try:
-            self._display.show()
-        except:
-            print("")
-
-    def _papirus_render(self):
-        self._display.display(self._canvas)
-        self._display.partial_update()
-
-    def _waveshare_render(self):
-        buf = self._display.getbuffer(self._canvas)
-        if self._is_waveshare_v1():
-            self._display.display(buf)
-        elif self._is_waveshare_v2():
-            self._display.displayPartial(buf)
+        self._implementation.clear()
 
     def image(self):
         img = None
@@ -212,10 +68,24 @@ class Display(View):
             img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation)
         return img
 
+    def _render_thread(self):
+        """Used for non-blocking screen updating."""
+
+        while True:
+            self._canvas_next_event.wait()
+            self._canvas_next_event.clear()
+            self._implementation.render(self._canvas_next)
+
     def _on_view_rendered(self, img):
-        VideoHandler.render(img)
+        web.update_frame(img)
+        try:
+            if self._config['ui']['display']['video']['on_frame'] != '':
+                os.system(self._config['ui']['display']['video']['on_frame'])
+        except Exception as e:
+            logging.error("%s" % e)
 
         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()
+            if self._implementation is not None:
+                self._canvas_next = self._canvas
+                self._canvas_next_event.set()
diff --git a/pwnagotchi/ui/faces.py b/pwnagotchi/ui/faces.py
index 1e09be1..d4cbd32 100644
--- a/pwnagotchi/ui/faces.py
+++ b/pwnagotchi/ui/faces.py
@@ -1,11 +1,11 @@
-LOOK_R = '(⌐■_■)'
-LOOK_L = '(■_■¬)'
+LOOK_R = '( ⚆_⚆)'
+LOOK_L = '(☉_☉ )'
 SLEEP = '(⇀‿‿↼)'
 SLEEP2 = '(≖‿‿≖)'
 AWAKE = '(◕‿‿◕)'
 BORED = '(-__-)'
 INTENSE = '(°▃▃°)'
-COOL = '(⊙☁◉┐)'
+COOL = '(⌐■_■)'
 HAPPY = '(•‿‿•)'
 EXCITED = '(ᵔ◡◡ᵔ)'
 MOTIVATED = '(☼‿‿☼)'
@@ -16,3 +16,8 @@ SAD = '(╥☁╥ )'
 FRIEND = '(♥‿‿♥)'
 BROKEN = '(☓‿‿☓)'
 DEBUG = '(#__#)'
+
+
+def load_from_config(config):
+    for face_name, face_value in config.items():
+        globals()[face_name.upper()] = face_value
diff --git a/pwnagotchi/ui/fonts.py b/pwnagotchi/ui/fonts.py
index 0f8ae5b..ca616d3 100644
--- a/pwnagotchi/ui/fonts.py
+++ b/pwnagotchi/ui/fonts.py
@@ -4,7 +4,9 @@ PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono'
 
 Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, 10)
 BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, 8)
+BoldBig = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
 Medium = ImageFont.truetype("%s.ttf" % PATH, 10)
+Small = ImageFont.truetype("%s.ttf" % PATH, 9)
 Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
 
 
diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py
new file mode 100644
index 0000000..1e0bf31
--- /dev/null
+++ b/pwnagotchi/ui/hw/__init__.py
@@ -0,0 +1,32 @@
+from pwnagotchi.ui.hw.inky import Inky
+from pwnagotchi.ui.hw.papirus import Papirus
+from pwnagotchi.ui.hw.oledhat import OledHat
+from pwnagotchi.ui.hw.lcdhat import LcdHat
+from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
+from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
+from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
+
+
+def display_for(config):
+    # config has been normalized already in utils.load_config
+    if config['ui']['display']['type'] == 'inky':
+        return Inky(config)
+
+    elif config['ui']['display']['type'] == 'papirus':
+        return Papirus(config)
+
+    if config['ui']['display']['type'] == 'oledhat':
+        return OledHat(config)
+
+    if config['ui']['display']['type'] == 'lcdhat':
+        return LcdHat(config)
+
+
+    elif config['ui']['display']['type'] == 'waveshare_1':
+        return WaveshareV1(config)
+
+    elif config['ui']['display']['type'] == 'waveshare_2':
+        return WaveshareV2(config)
+
+    elif config['ui']['display']['type'] == 'waveshare27inch':
+        return Waveshare27inch(config)
\ No newline at end of file
diff --git a/pwnagotchi/ui/hw/base.py b/pwnagotchi/ui/hw/base.py
new file mode 100644
index 0000000..e3e0db0
--- /dev/null
+++ b/pwnagotchi/ui/hw/base.py
@@ -0,0 +1,40 @@
+import pwnagotchi.ui.fonts as fonts
+
+
+class DisplayImpl(object):
+    def __init__(self, config, name):
+        self.name = name
+        self.config = config['ui']['display']
+        self._layout = {
+            'width': 0,
+            'height': 0,
+            'face': (0, 0),
+            'name': (0, 0),
+            'channel': (0, 0),
+            'aps': (0, 0),
+            'uptime': (0, 0),
+            'line1': (0, 0),
+            'line2': (0, 0),
+            'friend_face': (0, 0),
+            'friend_name': (0, 0),
+            'shakes': (0, 0),
+            'mode': (0, 0),
+            # status is special :D
+            'status': {
+                'pos': (0, 0),
+                'font': fonts.Medium,
+                'max': 20
+            }
+        }
+
+    def layout(self):
+        raise NotImplementedError
+
+    def initialize(self):
+        raise NotImplementedError
+
+    def render(self, canvas):
+        raise NotImplementedError
+
+    def clear(self):
+        raise NotImplementedError
diff --git a/pwnagotchi/ui/hw/inky.py b/pwnagotchi/ui/hw/inky.py
new file mode 100644
index 0000000..3917079
--- /dev/null
+++ b/pwnagotchi/ui/hw/inky.py
@@ -0,0 +1,72 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class Inky(DisplayImpl):
+    def __init__(self, config):
+        super(Inky, self).__init__(config, 'inky')
+        self._display = None
+
+    def layout(self):
+        fonts.setup(10, 8, 10, 28)
+        self._layout['width'] = 212
+        self._layout['height'] = 104
+        self._layout['face'] = (0, 37)
+        self._layout['name'] = (5, 18)
+        self._layout['channel'] = (0, 0)
+        self._layout['aps'] = (30, 0)
+        self._layout['uptime'] = (147, 0)
+        self._layout['line1'] = [0, 12, 212, 12]
+        self._layout['line2'] = [0, 92, 212, 92]
+        self._layout['friend_face'] = (0, 76)
+        self._layout['friend_name'] = (40, 78)
+        self._layout['shakes'] = (0, 93)
+        self._layout['mode'] = (187, 93)
+        self._layout['status'] = {
+            'pos': (102, 18),
+            'font': fonts.Small,
+            'max': 20
+        }
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing inky display")
+        from pwnagotchi.ui.hw.libs.inkyphat.inkyphatfast import InkyPHATFast
+        self._display = InkyPHATFast(self.config['color'])
+        self._display.set_border(InkyPHATFast.BLACK)
+
+    def render(self, canvas):
+        if self.config['color'] != 'mono':
+            display_colors = 3
+        else:
+            display_colors = 2
+
+        img_buffer = canvas.convert('RGB').convert('P', palette=1, colors=display_colors)
+        if self.config['color'] == 'red':
+            img_buffer.putpalette([
+                255, 255, 255,  # index 0 is white
+                0, 0, 0,  # index 1 is black
+                255, 0, 0  # index 2 is red
+            ])
+        elif self.config['color'] == 'yellow':
+            img_buffer.putpalette([
+                255, 255, 255,  # index 0 is white
+                0, 0, 0,  # index 1 is black
+                255, 255, 0  # index 2 is yellow
+            ])
+        else:
+            img_buffer.putpalette([
+                255, 255, 255,  # index 0 is white
+                0, 0, 0  # index 1 is black
+            ])
+
+        self._display.set_image(img_buffer)
+        try:
+            self._display.show()
+        except:
+            logging.exception("error while rendering on inky")
+
+    def clear(self):
+        self._display.Clear()
diff --git a/pwnagotchi/ui/hw/lcdhat.py b/pwnagotchi/ui/hw/lcdhat.py
new file mode 100644
index 0000000..75779a3
--- /dev/null
+++ b/pwnagotchi/ui/hw/lcdhat.py
@@ -0,0 +1,46 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class LcdHat(DisplayImpl):
+    def __init__(self, config):
+        super(LcdHat, self).__init__(config, 'lcdhat')
+        self._display = None
+
+    def layout(self):
+        fonts.setup(10, 9, 10, 35)
+        self._layout['width'] = 240
+        self._layout['height'] = 240
+        self._layout['face'] = (0, 40)
+        self._layout['name'] = (5, 20)
+        self._layout['channel'] = (0, 0)
+        self._layout['aps'] = (28, 0)
+        self._layout['uptime'] = (175, 0)
+        self._layout['line1'] = [0, 14, 240, 14]
+        self._layout['line2'] = [0, 108, 240, 108]
+        self._layout['friend_face'] = (0, 92)
+        self._layout['friend_name'] = (40, 94)
+        self._layout['shakes'] = (0, 109)
+        self._layout['mode'] = (215, 109)
+        self._layout['status'] = {
+            'pos': (125, 20),
+            'font': fonts.Medium,
+            'max': 20
+        }
+
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing lcdhat display")
+        from pwnagotchi.ui.hw.libs.waveshare.lcdhat.epd import EPD
+        self._display = EPD()
+        self._display.init()
+        self._display.Clear()
+
+    def render(self, canvas):
+        self._display.display(canvas)
+
+    def clear(self):
+        self._display.clear()
diff --git a/pwnagotchi/ui/papirus/__init__.py b/pwnagotchi/ui/hw/libs/__init__.py
similarity index 100%
rename from pwnagotchi/ui/papirus/__init__.py
rename to pwnagotchi/ui/hw/libs/__init__.py
diff --git a/pwnagotchi/ui/waveshare/__init__.py b/pwnagotchi/ui/hw/libs/inkyphat/__init__.py
similarity index 100%
rename from pwnagotchi/ui/waveshare/__init__.py
rename to pwnagotchi/ui/hw/libs/inkyphat/__init__.py
diff --git a/pwnagotchi/ui/hw/libs/inkyphat/inkyfast.py b/pwnagotchi/ui/hw/libs/inkyphat/inkyfast.py
new file mode 100644
index 0000000..955e8e6
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/inkyphat/inkyfast.py
@@ -0,0 +1,24 @@
+from inky.inky import Inky, CS0_PIN, DC_PIN, RESET_PIN, BUSY_PIN
+
+
+class InkyFast(Inky):
+
+    def __init__(self, resolution=(400, 300), colour='black', cs_pin=CS0_PIN, dc_pin=DC_PIN, reset_pin=RESET_PIN,
+                 busy_pin=BUSY_PIN, h_flip=False, v_flip=False):
+        super(InkyFast, self).__init__(resolution, colour, cs_pin, dc_pin, reset_pin, busy_pin, h_flip, v_flip)
+
+        self._luts['black'] = [
+                0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00000000, 0b00000000,
+                0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b00000000, 0b00000000,
+                0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+                0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b00000000, 0b00000000,
+                0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
+                # The following timings have been reduced to avoid the fade to black
+                0x00, 0x00, 0x00, 0x00, 0x00,
+                0x10, 0x04, 0x04, 0x04, 0x04,
+                0x04, 0x08, 0x08, 0x10, 0x10,
+                0x00, 0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x00, 0x00,
+            ]
\ No newline at end of file
diff --git a/pwnagotchi/ui/hw/libs/inkyphat/inkyphatfast.py b/pwnagotchi/ui/hw/libs/inkyphat/inkyphatfast.py
new file mode 100644
index 0000000..e39433b
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/inkyphat/inkyphatfast.py
@@ -0,0 +1,27 @@
+"""Inky pHAT e-Ink Display Driver."""
+from . import inkyfast
+
+
+class InkyPHATFast(inkyfast.InkyFast):
+    """Inky wHAT e-Ink Display Driver."""
+
+    WIDTH = 212
+    HEIGHT = 104
+
+    WHITE = 0
+    BLACK = 1
+    RED = 2
+    YELLOW = 2
+
+    def __init__(self, colour):
+        """Initialise an Inky pHAT Display.
+
+        :param colour: one of red, black or yellow, default: black
+
+        """
+        inkyfast.InkyFast.__init__(
+            self,
+            resolution=(self.WIDTH, self.HEIGHT),
+            colour=colour,
+            h_flip=False,
+            v_flip=False)
\ No newline at end of file
diff --git a/pwnagotchi/ui/waveshare/v1/__init__.py b/pwnagotchi/ui/hw/libs/papirus/__init__.py
similarity index 100%
rename from pwnagotchi/ui/waveshare/v1/__init__.py
rename to pwnagotchi/ui/hw/libs/papirus/__init__.py
diff --git a/pwnagotchi/ui/papirus/epd.py b/pwnagotchi/ui/hw/libs/papirus/epd.py
similarity index 99%
rename from pwnagotchi/ui/papirus/epd.py
rename to pwnagotchi/ui/hw/libs/papirus/epd.py
index 923993b..11764d9 100644
--- a/pwnagotchi/ui/papirus/epd.py
+++ b/pwnagotchi/ui/hw/libs/papirus/epd.py
@@ -15,7 +15,7 @@
 
 from PIL import Image
 from PIL import ImageOps
-from pwnagotchi.ui.papirus.lm75b import LM75B
+from pwnagotchi.ui.hw.libs.papirus.lm75b import LM75B
 import re
 import os
 import sys
diff --git a/pwnagotchi/ui/papirus/lm75b.py b/pwnagotchi/ui/hw/libs/papirus/lm75b.py
similarity index 100%
rename from pwnagotchi/ui/papirus/lm75b.py
rename to pwnagotchi/ui/hw/libs/papirus/lm75b.py
diff --git a/pwnagotchi/ui/waveshare/v2/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/__init__.py
similarity index 100%
rename from pwnagotchi/ui/waveshare/v2/__init__.py
rename to pwnagotchi/ui/hw/libs/waveshare/__init__.py
diff --git a/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.py b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.py
new file mode 100644
index 0000000..88409ec
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.py
@@ -0,0 +1,166 @@
+import spidev
+import RPi.GPIO as GPIO
+import time
+import numpy as np
+
+
+class ST7789(object):
+    """class for ST7789  240*240 1.3inch OLED displays."""
+
+    def __init__(self, spi, rst=27, dc=25, bl=24):
+        self.width = 240
+        self.height = 240
+        # Initialize DC RST pin
+        self._dc = dc
+        self._rst = rst
+        self._bl = bl
+        GPIO.setmode(GPIO.BCM)
+        GPIO.setwarnings(False)
+        GPIO.setup(self._dc, GPIO.OUT)
+        GPIO.setup(self._rst, GPIO.OUT)
+        GPIO.setup(self._bl, GPIO.OUT)
+        GPIO.output(self._bl, GPIO.HIGH)
+        # Initialize SPI
+        self._spi = spi
+        self._spi.max_speed_hz = 40000000
+
+    """    Write register address and data     """
+
+    def command(self, cmd):
+        GPIO.output(self._dc, GPIO.LOW)
+        self._spi.writebytes([cmd])
+
+    def data(self, val):
+        GPIO.output(self._dc, GPIO.HIGH)
+        self._spi.writebytes([val])
+
+    def Init(self):
+        """Initialize dispaly"""
+        self.reset()
+
+        self.command(0x36)
+        self.data(0x70)  # self.data(0x00)
+
+        self.command(0x3A)
+        self.data(0x05)
+
+        self.command(0xB2)
+        self.data(0x0C)
+        self.data(0x0C)
+        self.data(0x00)
+        self.data(0x33)
+        self.data(0x33)
+
+        self.command(0xB7)
+        self.data(0x35)
+
+        self.command(0xBB)
+        self.data(0x19)
+
+        self.command(0xC0)
+        self.data(0x2C)
+
+        self.command(0xC2)
+        self.data(0x01)
+
+        self.command(0xC3)
+        self.data(0x12)
+
+        self.command(0xC4)
+        self.data(0x20)
+
+        self.command(0xC6)
+        self.data(0x0F)
+
+        self.command(0xD0)
+        self.data(0xA4)
+        self.data(0xA1)
+
+        self.command(0xE0)
+        self.data(0xD0)
+        self.data(0x04)
+        self.data(0x0D)
+        self.data(0x11)
+        self.data(0x13)
+        self.data(0x2B)
+        self.data(0x3F)
+        self.data(0x54)
+        self.data(0x4C)
+        self.data(0x18)
+        self.data(0x0D)
+        self.data(0x0B)
+        self.data(0x1F)
+        self.data(0x23)
+
+        self.command(0xE1)
+        self.data(0xD0)
+        self.data(0x04)
+        self.data(0x0C)
+        self.data(0x11)
+        self.data(0x13)
+        self.data(0x2C)
+        self.data(0x3F)
+        self.data(0x44)
+        self.data(0x51)
+        self.data(0x2F)
+        self.data(0x1F)
+        self.data(0x1F)
+        self.data(0x20)
+        self.data(0x23)
+
+        self.command(0x21)
+
+        self.command(0x11)
+
+        self.command(0x29)
+
+    def reset(self):
+        """Reset the display"""
+        GPIO.output(self._rst, GPIO.HIGH)
+        time.sleep(0.01)
+        GPIO.output(self._rst, GPIO.LOW)
+        time.sleep(0.01)
+        GPIO.output(self._rst, GPIO.HIGH)
+        time.sleep(0.01)
+
+    def SetWindows(self, Xstart, Ystart, Xend, Yend):
+        # set the X coordinates
+        self.command(0x2A)
+        self.data(0x00)  # Set the horizontal starting point to the high octet
+        self.data(Xstart & 0xff)  # Set the horizontal starting point to the low octet
+        self.data(0x00)  # Set the horizontal end to the high octet
+        self.data((Xend - 1) & 0xff)  # Set the horizontal end to the low octet
+
+        # set the Y coordinates
+        self.command(0x2B)
+        self.data(0x00)
+        self.data((Ystart & 0xff))
+        self.data(0x00)
+        self.data((Yend - 1) & 0xff)
+
+        self.command(0x2C)
+
+    def ShowImage(self, Image, Xstart, Ystart):
+        """Set buffer to value of Python Imaging Library image."""
+        """Write display buffer to physical display"""
+        imwidth, imheight = Image.size
+        if imwidth != self.width or imheight != self.height:
+            raise ValueError('Image must be same dimensions as display \
+                ({0}x{1}).'.format(self.width, self.height))
+        img = np.asarray(Image)
+        pix = np.zeros((self.width, self.height, 2), dtype=np.uint8)
+        pix[..., [0]] = np.add(np.bitwise_and(img[..., [0]], 0xF8), np.right_shift(img[..., [1]], 5))
+        pix[..., [1]] = np.add(np.bitwise_and(np.left_shift(img[..., [1]], 3), 0xE0), np.right_shift(img[..., [2]], 3))
+        pix = pix.flatten().tolist()
+        self.SetWindows(0, 0, self.width, self.height)
+        GPIO.output(self._dc, GPIO.HIGH)
+        for i in range(0, len(pix), 4096):
+            self._spi.writebytes(pix[i:i + 4096])
+
+    def clear(self):
+        """Clear contents of image buffer"""
+        _buffer = [0xff] * (self.width * self.height * 2)
+        self.SetWindows(0, 0, self.width, self.height)
+        GPIO.output(self._dc, GPIO.HIGH)
+        for i in range(0, len(_buffer), 4096):
+            self._spi.writebytes(_buffer[i:i + 4096])
diff --git a/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.pyc b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.pyc
new file mode 100644
index 0000000..1837ca2
Binary files /dev/null and b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/ST7789.pyc differ
diff --git a/pwnagotchi/ui/hw/libs/waveshare/lcdhat/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pwnagotchi/ui/hw/libs/waveshare/lcdhat/config.py b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/config.py
new file mode 100644
index 0000000..24b5cc8
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/config.py
@@ -0,0 +1,21 @@
+# /*****************************************************************************
+# * | File        :   config.py
+# * | Author      :   Guillaume Giraudon
+# * | Info        :
+# *----------------
+# * | This version:   V1.0
+# * | Date        :   2019-10-18
+# * | Info        :
+# ******************************************************************************/
+import spidev
+
+# Pin definition
+RST_PIN = 27
+DC_PIN = 25
+BL_PIN = 24
+
+Device_SPI = 1
+Device_I2C = 0
+
+Device = Device_SPI
+spi = spidev.SpiDev(0, 0)
diff --git a/pwnagotchi/ui/hw/libs/waveshare/lcdhat/epd.py b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/epd.py
new file mode 100644
index 0000000..1a559f4
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/lcdhat/epd.py
@@ -0,0 +1,21 @@
+from . import ST7789
+from . import config
+
+
+class EPD(object):
+    def __init__(self):
+        self.reset_pin = config.RST_PIN
+        self.dc_pin = config.DC_PIN
+        self.width = 240
+        self.height = 240
+        self.st7789 = ST7789.ST7789(config.spi, config.RST_PIN, config.DC_PIN, config.BL_PIN)
+
+    def init(self):
+        self.st7789.Init()
+
+    def clear(self):
+        self.st7789.clear()
+
+    def display(self, image):
+        rgb_im = image.convert('RGB')
+        self.st7789.ShowImage(rgb_im, 0, 0)
diff --git a/pwnagotchi/ui/hw/libs/waveshare/oledhat/SH1106.py b/pwnagotchi/ui/hw/libs/waveshare/oledhat/SH1106.py
new file mode 100644
index 0000000..a89716d
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/oledhat/SH1106.py
@@ -0,0 +1,135 @@
+from . import config
+import RPi.GPIO as GPIO
+import time
+
+Device_SPI = config.Device_SPI
+Device_I2C = config.Device_I2C
+
+LCD_WIDTH   = 128 #LCD width
+LCD_HEIGHT  = 64  #LCD height
+
+class SH1106(object):
+    def __init__(self):
+        self.width = LCD_WIDTH
+        self.height = LCD_HEIGHT
+        #Initialize DC RST pin
+        self._dc = config.DC_PIN
+        self._rst = config.RST_PIN
+        self._bl = config.BL_PIN
+        self.Device = config.Device
+
+
+    """    Write register address and data     """
+    def command(self, cmd):
+        if(self.Device == Device_SPI):
+            GPIO.output(self._dc, GPIO.LOW)
+            config.spi_writebyte([cmd])
+        else:
+            config.i2c_writebyte(0x00, cmd)
+
+    # def data(self, val):
+        # GPIO.output(self._dc, GPIO.HIGH)
+        # config.spi_writebyte([val])
+
+    def Init(self):
+        if (config.module_init() != 0):
+            return -1
+        """Initialize dispaly"""
+        self.reset()
+        self.command(0xAE);#--turn off oled panel
+        self.command(0x02);#---set low column address
+        self.command(0x10);#---set high column address
+        self.command(0x40);#--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
+        self.command(0x81);#--set contrast control register
+        self.command(0xA0);#--Set SEG/Column Mapping
+        self.command(0xC0);#Set COM/Row Scan Direction
+        self.command(0xA6);#--set normal display
+        self.command(0xA8);#--set multiplex ratio(1 to 64)
+        self.command(0x3F);#--1/64 duty
+        self.command(0xD3);#-set display offset    Shift Mapping RAM Counter (0x00~0x3F)
+        self.command(0x00);#-not offset
+        self.command(0xd5);#--set display clock divide ratio/oscillator frequency
+        self.command(0x80);#--set divide ratio, Set Clock as 100 Frames/Sec
+        self.command(0xD9);#--set pre-charge period
+        self.command(0xF1);#Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
+        self.command(0xDA);#--set com pins hardware configuration
+        self.command(0x12);
+        self.command(0xDB);#--set vcomh
+        self.command(0x40);#Set VCOM Deselect Level
+        self.command(0x20);#-Set Page Addressing Mode (0x00/0x01/0x02)
+        self.command(0x02);#
+        self.command(0xA4);# Disable Entire Display On (0xa4/0xa5)
+        self.command(0xA6);# Disable Inverse Display On (0xa6/a7)
+        time.sleep(0.1)
+        self.command(0xAF);#--turn on oled panel
+
+
+    def reset(self):
+        """Reset the display"""
+        GPIO.output(self._rst,GPIO.HIGH)
+        time.sleep(0.1)
+        GPIO.output(self._rst,GPIO.LOW)
+        time.sleep(0.1)
+        GPIO.output(self._rst,GPIO.HIGH)
+        time.sleep(0.1)
+
+    def getbuffer(self, image):
+        # print "bufsiz = ",(self.width/8) * self.height
+        buf = [0xFF] * ((self.width//8) * self.height)
+        image_monocolor = image.convert('1')
+        imwidth, imheight = image_monocolor.size
+        pixels = image_monocolor.load()
+        # print "imwidth = %d, imheight = %d",imwidth,imheight
+        if(imwidth == self.width and imheight == self.height):
+            #print ("Vertical")
+            for y in range(imheight):
+                for x in range(imwidth):
+                    # Set the bits for the column of pixels at the current position.
+                    if pixels[x, y] == 0:
+                        buf[x + (y // 8) * self.width] &= ~(1 << (y % 8))
+                        # print x,y,x + (y * self.width)/8,buf[(x + y * self.width) / 8]
+
+        elif(imwidth == self.height and imheight == self.width):
+            #print ("Vertical")
+            for y in range(imheight):
+                for x in range(imwidth):
+                    newx = y
+                    newy = self.height - x - 1
+                    if pixels[x, y] == 0:
+                        buf[(newx + (newy // 8 )*self.width) ] &= ~(1 << (y % 8))
+        return buf
+
+
+    # def ShowImage(self,Image):
+        # self.SetWindows()
+        # GPIO.output(self._dc, GPIO.HIGH);
+        # for i in range(0,self.width * self.height/8):
+            # config.spi_writebyte([~Image[i]])
+
+    def ShowImage(self, pBuf):
+        for page in range(0,8):
+            # set page address #
+            self.command(0xB0 + page);
+            # set low column address #
+            self.command(0x02);
+            # set high column address #
+            self.command(0x10);
+            # write data #
+            time.sleep(0.01)
+            if(self.Device == Device_SPI):
+                GPIO.output(self._dc, GPIO.HIGH);
+            for i in range(0,self.width):#for(int i=0;i<self.width; i++)
+                if(self.Device == Device_SPI):
+                    config.spi_writebyte([~pBuf[i + self.width * page]]);
+                else :
+                    config.i2c_writebyte(0x40, ~pBuf[i + self.width * page])
+
+
+
+
+
+    def clear(self):
+        """Clear contents of image buffer"""
+        _buffer = [0xff]*(self.width * self.height//8)
+        self.ShowImage(_buffer)
+            #print "%d",_buffer[i:i+4096]
diff --git a/pwnagotchi/ui/hw/libs/waveshare/oledhat/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/oledhat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pwnagotchi/ui/hw/libs/waveshare/oledhat/config.py b/pwnagotchi/ui/hw/libs/waveshare/oledhat/config.py
new file mode 100644
index 0000000..1eaf68e
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/oledhat/config.py
@@ -0,0 +1,111 @@
+# /*****************************************************************************
+# * | File        :	  config.py
+# * | Author      :   Waveshare team
+# * | Function    :   Hardware underlying interface,for Jetson nano
+# * | Info        :
+# *----------------
+# * | This version:   V1.0
+# * | Date        :   2019-06-06
+# * | Info        :
+# ******************************************************************************/
+# 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 RPi.GPIO as GPIO
+import time
+from smbus import SMBus
+import spidev
+
+import ctypes
+# import spidev
+
+# Pin definition
+RST_PIN         = 25
+DC_PIN          = 24
+CS_PIN          = 8
+BL_PIN          = 18
+BUSY_PIN          = 18
+
+Device_SPI = 1
+Device_I2C = 0
+
+
+
+if(Device_SPI == 1):
+    Device = Device_SPI
+    spi = spidev.SpiDev(0, 0)
+else :
+    Device = Device_I2C
+    address         = 0x3C
+    bus = SMBus(1)
+
+def digital_write(pin, value):
+    GPIO.output(pin, value)
+
+def digital_read(pin):
+    return GPIO.input(BUSY_PIN)
+
+def delay_ms(delaytime):
+    time.sleep(delaytime / 1000.0)
+
+def spi_writebyte(data):
+    # SPI.writebytes(data)
+    spi.writebytes([data[0]])
+
+def i2c_writebyte(reg, value):
+    bus.write_byte_data(address, reg, value)
+
+    # time.sleep(0.01)
+def module_init():
+    # print("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(BL_PIN, GPIO.OUT)
+
+
+    # SPI.max_speed_hz = 2000000
+    # SPI.mode = 0b00
+    # i2c_writebyte(0xff,0xff)
+    if(Device == Device_SPI):
+        # spi.SYSFS_software_spi_begin()
+        # spi.SYSFS_software_spi_setDataMode(0);
+        # spi.SYSFS_software_spi_setClockDivider(1);
+        spi.max_speed_hz = 2000000
+        spi.mode = 0b00
+
+    GPIO.output(CS_PIN, 0)
+    GPIO.output(BL_PIN, 1)
+    GPIO.output(DC_PIN, 0)
+    return 0
+
+def module_exit():
+    if(Device == Device_SPI):
+        spi.SYSFS_software_spi_end()
+    else :
+        bus.close()
+    GPIO.output(RST_PIN, 0)
+    GPIO.output(DC_PIN, 0)
+
+
+
+### END OF FILE ###
diff --git a/pwnagotchi/ui/hw/libs/waveshare/oledhat/epd.py b/pwnagotchi/ui/hw/libs/waveshare/oledhat/epd.py
new file mode 100644
index 0000000..7c1be01
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/oledhat/epd.py
@@ -0,0 +1,27 @@
+from . import SH1106
+from . import config
+
+# Display resolution
+EPD_WIDTH       = 64
+EPD_HEIGHT      = 128
+
+disp = SH1106.SH1106()
+
+class EPD(object):
+
+    def __init__(self):
+        self.reset_pin = config.RST_PIN
+        self.dc_pin = config.DC_PIN
+        self.busy_pin = config.BUSY_PIN
+        self.cs_pin = config.CS_PIN
+        self.width = EPD_WIDTH
+        self.height = EPD_HEIGHT
+
+    def init(self):
+        disp.Init()
+
+    def Clear(self):
+        disp.clear()
+
+    def display(self, image):
+        disp.ShowImage(disp.getbuffer(image))
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v1/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/v1/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pwnagotchi/ui/waveshare/v1/epd2in13.py b/pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13.py
similarity index 99%
rename from pwnagotchi/ui/waveshare/v1/epd2in13.py
rename to pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13.py
index b6de9da..93685e1 100644
--- a/pwnagotchi/ui/waveshare/v1/epd2in13.py
+++ b/pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13.py
@@ -30,7 +30,6 @@
 
 import logging
 from . import epdconfig
-import numpy as np
 
 # Display resolution
 EPD_WIDTH       = 122
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13bc.py b/pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13bc.py
new file mode 100644
index 0000000..f17f0af
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/v1/epd2in13bc.py
@@ -0,0 +1,164 @@
+# *****************************************************************************
+# * | File        :	  epd2in13bc.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.
+#
+
+from . import epdconfig
+import RPi.GPIO as GPIO
+# import numpy as np
+
+# Display resolution
+EPD_WIDTH       = 104
+EPD_HEIGHT      = 212
+
+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
+
+    # 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.digital_write(self.cs_pin, GPIO.LOW)
+        epdconfig.spi_writebyte([command])
+        epdconfig.digital_write(self.cs_pin, GPIO.HIGH)
+
+    def send_data(self, data):
+        epdconfig.digital_write(self.dc_pin, GPIO.HIGH)
+        epdconfig.digital_write(self.cs_pin, GPIO.LOW)
+        epdconfig.spi_writebyte([data])
+        epdconfig.digital_write(self.cs_pin, GPIO.HIGH)
+
+    def ReadBusy(self):
+        epdconfig.delay_ms(20)
+        while(epdconfig.digital_read(self.busy_pin) == 0):      # 0: idle, 1: busy
+            epdconfig.delay_ms(100)
+
+    def init(self):
+        if (epdconfig.module_init() != 0):
+            return -1
+        # EPD hardware init start
+        self.reset()
+
+        self.send_command(0x06) # BOOSTER_SOFT_START
+        self.send_data(0x17)
+        self.send_data(0x17)
+        self.send_data(0x17)
+
+        self.send_command(0x04) # POWER_ON
+        self.ReadBusy()
+
+        self.send_command(0x00) # PANEL_SETTING
+        self.send_data(0x8F)
+
+        self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
+        self.send_data(0xF0)
+
+        self.send_command(0x61) # RESOLUTION_SETTING
+        self.send_data(self.width & 0xff)
+        self.send_data(self.height >> 8)
+        self.send_data(self.height & 0xff)
+        return 0
+
+    def getbuffer(self, image):
+        buf = [0xFF] * (int(self.width/8) * 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):
+                    # Set the bits for the column of pixels at the current position.
+                    if pixels[x, y] == 0:
+                        buf[int((x + y * self.width) / 8)] &= ~(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:
+                        buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
+        return buf
+
+    def displayBlack(self, imageblack):
+        self.send_command(0x10)
+        for i in range(0, int(self.width * self.height / 8)):
+            self.send_data(imageblack[i])
+        self.send_command(0x92)
+
+        self.send_command(0x12) # REFRESH
+        self.ReadBusy()
+
+    def display(self, imageblack, imagecolor):
+        self.send_command(0x10)
+        for i in range(0, int(self.width * self.height / 8)):
+            self.send_data(imageblack[i])
+        self.send_command(0x92)
+
+        self.send_command(0x13)
+        for i in range(0, int(self.width * self.height / 8)):
+            self.send_data(imagecolor[i])
+        self.send_command(0x92)
+
+        self.send_command(0x12) # REFRESH
+        self.ReadBusy()
+
+    def Clear(self):
+        self.send_command(0x10)
+        for i in range(0, int(self.width * self.height / 8)):
+            self.send_data(0xFF)
+        self.send_command(0x92)
+
+        self.send_command(0x13)
+        for i in range(0, int(self.width * self.height / 8)):
+            self.send_data(0xFF)
+        self.send_command(0x92)
+
+        self.send_command(0x12) # REFRESH
+        self.ReadBusy()
+
+    def sleep(self):
+        self.send_command(0x02) # POWER_OFF
+        self.ReadBusy()
+        self.send_command(0x07) # DEEP_SLEEP
+        self.send_data(0xA5) # check code
+
+#        epdconfig.module_exit()
+### END OF FILE ###
+
diff --git a/pwnagotchi/ui/waveshare/v1/epdconfig.py b/pwnagotchi/ui/hw/libs/waveshare/v1/epdconfig.py
similarity index 100%
rename from pwnagotchi/ui/waveshare/v1/epdconfig.py
rename to pwnagotchi/ui/hw/libs/waveshare/v1/epdconfig.py
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v2/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/v2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pwnagotchi/ui/waveshare/v2/waveshare.py b/pwnagotchi/ui/hw/libs/waveshare/v2/waveshare.py
similarity index 100%
rename from pwnagotchi/ui/waveshare/v2/waveshare.py
rename to pwnagotchi/ui/hw/libs/waveshare/v2/waveshare.py
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v27inch/__init__.py b/pwnagotchi/ui/hw/libs/waveshare/v27inch/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v27inch/epd2in7.py b/pwnagotchi/ui/hw/libs/waveshare/v27inch/epd2in7.py
new file mode 100644
index 0000000..01e8473
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/v27inch/epd2in7.py
@@ -0,0 +1,300 @@
+# /*****************************************************************************
+# * | File        :	  EPD_1in54.py
+# * | Author      :   Waveshare team
+# * | Function    :   Electronic paper driver
+# * | Info        :
+# *----------------
+# * |	This version:   V3.0
+# * | Date        :   2018-11-06
+# * | Info        :   python2 demo
+# * 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
+# * 2.Change:
+#   display_frame -> TurnOnDisplay
+#   set_memory_area -> SetWindow
+#   set_memory_pointer -> SetCursor
+#   get_frame_buffer -> getbuffer
+#   set_frame_memory -> display
+# * 3.How to use
+#   epd = epd2in7.EPD()
+#   epd.init(epd.lut_full_update)
+#   image = Image.new('1', (epd1in54.EPD_WIDTH, epd1in54.EPD_HEIGHT), 255)
+#   ...
+#   drawing ......
+#   ...
+#   epd.display(getbuffer(image))
+# ******************************************************************************/
+# 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
+
+# Display resolution
+EPD_WIDTH       = 176
+EPD_HEIGHT      = 264
+
+# EPD2IN7 commands
+PANEL_SETTING                               = 0x00
+POWER_SETTING                               = 0x01
+POWER_OFF                                   = 0x02
+POWER_OFF_SEQUENCE_SETTING                  = 0x03
+POWER_ON                                    = 0x04
+POWER_ON_MEASURE                            = 0x05
+BOOSTER_SOFT_START                          = 0x06
+DEEP_SLEEP                                  = 0x07
+DATA_START_TRANSMISSION_1                   = 0x10
+DATA_STOP                                   = 0x11
+DISPLAY_REFRESH                             = 0x12
+DATA_START_TRANSMISSION_2                   = 0x13
+PARTIAL_DATA_START_TRANSMISSION_1           = 0x14
+PARTIAL_DATA_START_TRANSMISSION_2           = 0x15
+PARTIAL_DISPLAY_REFRESH                     = 0x16
+LUT_FOR_VCOM                                = 0x20
+LUT_WHITE_TO_WHITE                          = 0x21
+LUT_BLACK_TO_WHITE                          = 0x22
+LUT_WHITE_TO_BLACK                          = 0x23
+LUT_BLACK_TO_BLACK                          = 0x24
+PLL_CONTROL                                 = 0x30
+TEMPERATURE_SENSOR_COMMAND                  = 0x40
+TEMPERATURE_SENSOR_CALIBRATION              = 0x41
+TEMPERATURE_SENSOR_WRITE                    = 0x42
+TEMPERATURE_SENSOR_READ                     = 0x43
+VCOM_AND_DATA_INTERVAL_SETTING              = 0x50
+LOW_POWER_DETECTION                         = 0x51
+TCON_SETTING                                = 0x60
+TCON_RESOLUTION                             = 0x61
+SOURCE_AND_GATE_START_SETTING               = 0x62
+GET_STATUS                                  = 0x71
+AUTO_MEASURE_VCOM                           = 0x80
+VCOM_VALUE                                  = 0x81
+VCM_DC_SETTING_REGISTER                     = 0x82
+PROGRAM_MODE                                = 0xA0
+ACTIVE_PROGRAM                              = 0xA1
+READ_OTP_DATA                               = 0xA2
+
+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_vcom_dc = [
+        0x00, 0x00,
+        0x00, 0x08, 0x00, 0x00, 0x00, 0x02,
+        0x60, 0x28, 0x28, 0x00, 0x00, 0x01,
+        0x00, 0x14, 0x00, 0x00, 0x00, 0x01,
+        0x00, 0x12, 0x12, 0x00, 0x00, 0x01,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+    ]
+    lut_ww = [
+        0x40, 0x08, 0x00, 0x00, 0x00, 0x02,
+        0x90, 0x28, 0x28, 0x00, 0x00, 0x01,
+        0x40, 0x14, 0x00, 0x00, 0x00, 0x01,
+        0xA0, 0x12, 0x12, 0x00, 0x00, 0x01,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    ]
+    lut_bw = [
+        0x40, 0x08, 0x00, 0x00, 0x00, 0x02,
+        0x90, 0x28, 0x28, 0x00, 0x00, 0x01,
+        0x40, 0x14, 0x00, 0x00, 0x00, 0x01,
+        0xA0, 0x12, 0x12, 0x00, 0x00, 0x01,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    ]
+    lut_bb = [
+        0x80, 0x08, 0x00, 0x00, 0x00, 0x02,
+        0x90, 0x28, 0x28, 0x00, 0x00, 0x01,
+        0x80, 0x14, 0x00, 0x00, 0x00, 0x01,
+        0x50, 0x12, 0x12, 0x00, 0x00, 0x01,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    ]
+    lut_wb = [
+        0x80, 0x08, 0x00, 0x00, 0x00, 0x02,
+        0x90, 0x28, 0x28, 0x00, 0x00, 0x01,
+        0x80, 0x14, 0x00, 0x00, 0x00, 0x01,
+        0x50, 0x12, 0x12, 0x00, 0x00, 0x01,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        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):        
+        while(epdconfig.digital_read(self.busy_pin) == 0):      # 0: idle, 1: busy
+            self.send_command(0x71)
+        epdconfig.delay_ms(100)    
+
+    def set_lut(self):
+        self.send_command(LUT_FOR_VCOM)               # vcom
+        for count in range(0, 44):
+            self.send_data(self.lut_vcom_dc[count])
+        self.send_command(LUT_WHITE_TO_WHITE)         # ww --
+        for count in range(0, 42):
+            self.send_data(self.lut_ww[count])
+        self.send_command(LUT_BLACK_TO_WHITE)         # bw r
+        for count in range(0, 42):
+            self.send_data(self.lut_bw[count])
+        self.send_command(LUT_WHITE_TO_BLACK)         # wb w
+        for count in range(0, 42):
+            self.send_data(self.lut_bb[count])
+        self.send_command(LUT_BLACK_TO_BLACK)         # bb b
+        for count in range(0, 42):
+            self.send_data(self.lut_wb[count])
+            
+    def init(self):
+        if (epdconfig.module_init() != 0):
+            return -1
+        # EPD hardware init start
+        self.reset()
+        self.send_command(POWER_SETTING)
+        self.send_data(0x03)                  # VDS_EN, VDG_EN
+        self.send_data(0x00)                  # VCOM_HV, VGHL_LV[1], VGHL_LV[0]
+        self.send_data(0x2b)                  # VDH
+        self.send_data(0x2b)                  # VDL
+        self.send_data(0x09)                  # VDHR
+        self.send_command(BOOSTER_SOFT_START)
+        self.send_data(0x07)
+        self.send_data(0x07)
+        self.send_data(0x17)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0x60)
+        self.send_data(0xA5)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0x89)
+        self.send_data(0xA5)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0x90)
+        self.send_data(0x00)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0x93)
+        self.send_data(0x2A)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0xA0)
+        self.send_data(0xA5)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0xA1)
+        self.send_data(0x00)
+        # Power optimization
+        self.send_command(0xF8)
+        self.send_data(0x73)
+        self.send_data(0x41)
+        self.send_command(PARTIAL_DISPLAY_REFRESH)
+        self.send_data(0x00)
+        self.send_command(POWER_ON)
+        self.wait_until_idle()
+
+        self.send_command(PANEL_SETTING)
+        self.send_data(0xAF)        # KW-BF   KWR-AF    BWROTP 0f
+        self.send_command(PLL_CONTROL)
+        self.send_data(0x3A)        # 3A 100HZ   29 150Hz 39 200HZ    31 171HZ
+        self.send_command(VCM_DC_SETTING_REGISTER)
+        self.send_data(0x12)
+        self.set_lut()
+        # EPD hardware init end
+        return 0
+
+    def getbuffer(self, image):
+        # print "bufsiz = ",(self.width/8) * self.height
+        buf = [0xFF] * ((self.width//8) * self.height)
+        image_monocolor = image.convert('1')
+        imwidth, imheight = image_monocolor.size
+        pixels = image_monocolor.load()
+        # print "imwidth = %d, imheight = %d",imwidth,imheight
+        if(imwidth == self.width and imheight == self.height):
+            print("Vertical")
+            for y in range(imheight):
+                for x in range(imwidth):
+                    # Set the bits for the column of pixels at the current position.
+                    if pixels[x, y] == 0:
+                        buf[(x + y * self.width) // 8] &= ~(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:
+                        buf[(newx + newy*self.width) // 8] &= ~(0x80 >> (y % 8))
+        return buf
+
+    def display(self, image):
+        self.send_command(DATA_START_TRANSMISSION_1)
+        for i in range(0, self.width * self.height // 8):
+            self.send_data(0xFF)
+        self.send_command(DATA_START_TRANSMISSION_2)
+        for i in range(0, self.width * self.height // 8):
+            self.send_data(image[i])
+        self.send_command(DISPLAY_REFRESH) 
+        self.wait_until_idle()
+        
+    def Clear(self, color):
+        self.send_command(DATA_START_TRANSMISSION_1)
+        for i in range(0, self.width * self.height // 8):
+            self.send_data(0xFF)
+        self.send_command(DATA_START_TRANSMISSION_2)
+        for i in range(0, self.width * self.height // 8):
+            self.send_data(0xFF)
+        self.send_command(DISPLAY_REFRESH) 
+        self.wait_until_idle()
+
+    def sleep(self):
+        self.send_command(0X50)
+        self.send_data(0xf7)
+        self.send_command(0X02)
+        self.send_command(0X07)
+        self.send_data(0xA5)
+### END OF FILE ###
+
diff --git a/pwnagotchi/ui/hw/libs/waveshare/v27inch/epdconfig.py b/pwnagotchi/ui/hw/libs/waveshare/v27inch/epdconfig.py
new file mode 100644
index 0000000..78ff647
--- /dev/null
+++ b/pwnagotchi/ui/hw/libs/waveshare/v27inch/epdconfig.py
@@ -0,0 +1,73 @@
+# /*****************************************************************************
+# * | File        :	  EPD_1in54.py
+# * | Author      :   Waveshare team
+# * | Function    :   Hardware underlying interface
+# * | Info        :
+# *----------------
+# * |	This version:   V2.0
+# * | Date        :   2018-11-01
+# * | 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
+# 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 spidev
+import RPi.GPIO as GPIO
+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)
+
+def digital_write(pin, value):
+    GPIO.output(pin, value)
+
+def digital_read(pin):
+    return GPIO.input(BUSY_PIN)
+
+def delay_ms(delaytime):
+    time.sleep(delaytime / 1000.0)
+
+def spi_writebyte(data):
+    SPI.writebytes(data)
+
+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/pwnagotchi/ui/hw/oledhat.py b/pwnagotchi/ui/hw/oledhat.py
new file mode 100644
index 0000000..3673630
--- /dev/null
+++ b/pwnagotchi/ui/hw/oledhat.py
@@ -0,0 +1,45 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class OledHat(DisplayImpl):
+    def __init__(self, config):
+        super(OledHat, self).__init__(config, 'oledhat')
+        self._display = None
+
+    def layout(self):
+        fonts.setup(8, 8, 8, 8)
+        self._layout['width'] = 128
+        self._layout['height'] = 64
+        self._layout['face'] = (0, 32)
+        self._layout['name'] = (0, 10)
+        self._layout['channel'] = (0, 0)
+        self._layout['aps'] = (25, 0)
+        self._layout['uptime'] = (65, 0)
+        self._layout['line1'] = [0, 9, 128, 9]
+        self._layout['line2'] = [0, 53, 128, 53]
+        self._layout['friend_face'] = (0, 41)
+        self._layout['friend_name'] = (40, 43)
+        self._layout['shakes'] = (0, 53)
+        self._layout['mode'] = (103, 10)
+        self._layout['status'] = {
+            'pos': (30, 18),
+            'font': fonts.Small,
+            'max': 18
+        }
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing oledhat display")
+        from pwnagotchi.ui.hw.libs.waveshare.oledhat.epd import EPD
+        self._display = EPD()
+        self._display.init()
+        self._display.Clear()
+
+    def render(self, canvas):
+        self._display.display(canvas)
+
+    def clear(self):
+        self._display.clear()
diff --git a/pwnagotchi/ui/hw/papirus.py b/pwnagotchi/ui/hw/papirus.py
new file mode 100644
index 0000000..8da13be
--- /dev/null
+++ b/pwnagotchi/ui/hw/papirus.py
@@ -0,0 +1,47 @@
+import logging
+import os
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class Papirus(DisplayImpl):
+    def __init__(self, config):
+        super(Papirus, self).__init__(config, 'papirus')
+        self._display = None
+
+    def layout(self):
+        fonts.setup(10, 8, 10, 23)
+        self._layout['width'] = 200
+        self._layout['height'] = 96
+        self._layout['face'] = (0, 24)
+        self._layout['name'] = (5, 14)
+        self._layout['channel'] = (0, 0)
+        self._layout['aps'] = (25, 0)
+        self._layout['uptime'] = (135, 0)
+        self._layout['line1'] = [0, 11, 200, 11]
+        self._layout['line2'] = [0, 85, 200, 85]
+        self._layout['friend_face'] = (0, 69)
+        self._layout['friend_name'] = (40, 71)
+        self._layout['shakes'] = (0, 86)
+        self._layout['mode'] = (175, 86)
+        self._layout['status'] = {
+            'pos': (85, 14),
+            'font': fonts.Medium,
+            'max': 16
+        }
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing papirus display")
+        from pwnagotchi.ui.hw.libs.papirus.epd import EPD
+        os.environ['EPD_SIZE'] = '2.0'
+        self._display = EPD()
+        self._display.clear()
+
+    def render(self, canvas):
+        self._display.display(canvas)
+        self._display.partial_update()
+
+    def clear(self):
+        self._display.clear()
diff --git a/pwnagotchi/ui/hw/waveshare1.py b/pwnagotchi/ui/hw/waveshare1.py
new file mode 100644
index 0000000..47e55e1
--- /dev/null
+++ b/pwnagotchi/ui/hw/waveshare1.py
@@ -0,0 +1,85 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class WaveshareV1(DisplayImpl):
+    def __init__(self, config):
+        super(WaveshareV1, self).__init__(config, 'waveshare_1')
+        self._display = None
+
+    def layout(self):
+        if self.config['color'] == 'black':
+            fonts.setup(10, 9, 10, 35)
+            self._layout['width'] = 250
+            self._layout['height'] = 122
+            self._layout['face'] = (0, 40)
+            self._layout['name'] = (5, 20)
+            self._layout['channel'] = (0, 0)
+            self._layout['aps'] = (28, 0)
+            self._layout['uptime'] = (185, 0)
+            self._layout['line1'] = [0, 14, 250, 14]
+            self._layout['line2'] = [0, 108, 250, 108]
+            self._layout['friend_face'] = (0, 92)
+            self._layout['friend_name'] = (40, 94)
+            self._layout['shakes'] = (0, 109)
+            self._layout['mode'] = (225, 109)
+            self._layout['status'] = {
+                'pos': (125, 20),
+                'font': fonts.Medium,
+                'max': 20
+            }
+        else:
+            fonts.setup(10, 8, 10, 25)
+            self._layout['width'] = 212
+            self._layout['height'] = 104
+            self._layout['face'] = (0, 26)
+            self._layout['name'] = (5, 15)
+            self._layout['channel'] = (0, 0)
+            self._layout['aps'] = (28, 0)
+            self._layout['uptime'] = (147, 0)
+            self._layout['line1'] = [0, 12, 212, 12]
+            self._layout['line2'] = [0, 92, 212, 92]
+            self._layout['friend_face'] = (0, 76)
+            self._layout['friend_name'] = (40, 78)
+            self._layout['shakes'] = (0, 93)
+            self._layout['mode'] = (187, 93)
+            self._layout['status'] = {
+                'pos': (91, 15),
+                'font': fonts.Medium,
+                'max': 20
+            }
+        return self._layout
+
+    def initialize(self):
+        if self.config['color'] == 'black':
+            logging.info("initializing waveshare v1 display in monochromatic mode")
+            from pwnagotchi.ui.hw.libs.waveshare.v1.epd2in13 import EPD
+            self._display = EPD()
+            self._display.init(self._display.lut_full_update)
+            self._display.Clear(0xFF)
+            self._display.init(self._display.lut_partial_update)
+
+        else:
+            logging.info("initializing waveshare v1 display 3-color mode")
+            from pwnagotchi.ui.hw.libs.waveshare.v1.epd2in13bc import EPD
+            self._display = EPD()
+            self._display.init()
+            self._display.Clear()
+
+    def render(self, canvas):
+        if self.config['color'] == 'black':
+            buf = self._display.getbuffer(canvas)
+            self._display.display(buf)
+        else:
+            buf_black = self._display.getbuffer(canvas)
+            # emptyImage = Image.new('1', (self._display.height, self._display.width), 255)
+            # buf_color = self._display.getbuffer(emptyImage)
+            # self._display.display(buf_black,buf_color)
+            # Custom display function that only handles black
+            # Was included in epd2in13bc.py
+            self._display.displayBlack(buf_black)
+
+    def clear(self):
+        self._display.Clear(0xff)
diff --git a/pwnagotchi/ui/hw/waveshare2.py b/pwnagotchi/ui/hw/waveshare2.py
new file mode 100644
index 0000000..5065f65
--- /dev/null
+++ b/pwnagotchi/ui/hw/waveshare2.py
@@ -0,0 +1,69 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class WaveshareV2(DisplayImpl):
+    def __init__(self, config):
+        super(WaveshareV2, self).__init__(config, 'waveshare_2')
+        self._display = None
+
+    def layout(self):
+        if self.config['color'] == 'black':
+            fonts.setup(10, 9, 10, 35)
+            self._layout['width'] = 250
+            self._layout['height'] = 122
+            self._layout['face'] = (0, 40)
+            self._layout['name'] = (5, 20)
+            self._layout['channel'] = (0, 0)
+            self._layout['aps'] = (28, 0)
+            self._layout['uptime'] = (185, 0)
+            self._layout['line1'] = [0, 14, 250, 14]
+            self._layout['line2'] = [0, 108, 250, 108]
+            self._layout['friend_face'] = (0, 92)
+            self._layout['friend_name'] = (40, 94)
+            self._layout['shakes'] = (0, 109)
+            self._layout['mode'] = (225, 109)
+            self._layout['status'] = {
+                'pos': (125, 20),
+                'font': fonts.Medium,
+                'max': 20
+            }
+        else:
+            fonts.setup(10, 8, 10, 25)
+            self._layout['width'] = 212
+            self._layout['height'] = 104
+            self._layout['face'] = (0, 26)
+            self._layout['name'] = (5, 15)
+            self._layout['channel'] = (0, 0)
+            self._layout['aps'] = (28, 0)
+            self._layout['status'] = (91, 15)
+            self._layout['uptime'] = (147, 0)
+            self._layout['line1'] = [0, 12, 212, 12]
+            self._layout['line2'] = [0, 92, 212, 92]
+            self._layout['friend_face'] = (0, 76)
+            self._layout['friend_name'] = (40, 78)
+            self._layout['shakes'] = (0, 93)
+            self._layout['mode'] = (187, 93)
+            self._layout['status'] = {
+                'pos': (125, 20),
+                'font': fonts.Medium,
+                'max': 14
+            }
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing waveshare v2 display")
+        from pwnagotchi.ui.hw.libs.waveshare.v2.waveshare import EPD
+        self._display = EPD()
+        self._display.init(self._display.FULL_UPDATE)
+        self._display.Clear(0xff)
+        self._display.init(self._display.PART_UPDATE)
+
+    def render(self, canvas):
+        buf = self._display.getbuffer(canvas)
+        self._display.displayPartial(buf)
+
+    def clear(self):
+        self._display.Clear(0xff)
diff --git a/pwnagotchi/ui/hw/waveshare27inch.py b/pwnagotchi/ui/hw/waveshare27inch.py
new file mode 100644
index 0000000..c554744
--- /dev/null
+++ b/pwnagotchi/ui/hw/waveshare27inch.py
@@ -0,0 +1,46 @@
+import logging
+
+import pwnagotchi.ui.fonts as fonts
+from pwnagotchi.ui.hw.base import DisplayImpl
+
+
+class Waveshare27inch(DisplayImpl):
+    def __init__(self, config):
+        super(Waveshare27inch, self).__init__(config, 'waveshare_2_7inch')
+        self._display = None
+
+    def layout(self):
+        fonts.setup(10, 9, 10, 35)
+        self._layout['width'] = 264
+        self._layout['height'] = 176
+        self._layout['face'] = (66, 27)
+        self._layout['name'] = (5, 73)
+        self._layout['channel'] = (0, 0)
+        self._layout['aps'] = (28, 0)
+        self._layout['uptime'] = (199, 0)
+        self._layout['line1'] = [0, 14, 264, 14]
+        self._layout['line2'] = [0, 162, 264, 162]
+        self._layout['friend_face'] = (0, 146)
+        self._layout['friend_name'] = (40, 146)
+        self._layout['shakes'] = (0, 163)
+        self._layout['mode'] = (239, 163)
+        self._layout['status'] = {
+            'pos': (38, 93),
+            'font': fonts.Medium,
+            'max': 40
+        }
+        return self._layout
+
+    def initialize(self):
+        logging.info("initializing waveshare v1 2.7 inch display")
+        from pwnagotchi.ui.hw.libs.waveshare.v27inch.epd2in7 import EPD
+        self._display = EPD()
+        self._display.init()
+        self._display.Clear(0xFF)
+
+    def render(self, canvas):
+        buf = self._display.getbuffer(canvas)
+        self._display.display(buf)
+
+    def clear(self):
+        self._display.Clear(0xff)
diff --git a/pwnagotchi/ui/state.py b/pwnagotchi/ui/state.py
index 0a9e37a..b416b4a 100644
--- a/pwnagotchi/ui/state.py
+++ b/pwnagotchi/ui/state.py
@@ -12,6 +12,13 @@ class State(object):
         self._state[key] = elem
         self._changes[key] = True
 
+    def has_element(self, key):
+        return key in self._state
+
+    def remove_element(self, key):
+        del self._state[key]
+        self._changes[key] = True
+
     def add_listener(self, key, cb):
         with self._lock:
             self._listeners[key] = cb
diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py
index 78e75b6..dcb31a2 100644
--- a/pwnagotchi/ui/view.py
+++ b/pwnagotchi/ui/view.py
@@ -2,7 +2,7 @@ import _thread
 from threading import Lock
 import time
 import logging
-from PIL import Image, ImageDraw
+from PIL import ImageDraw
 
 import pwnagotchi.utils as utils
 import pwnagotchi.plugins as plugins
@@ -15,100 +15,67 @@ from pwnagotchi.ui.state import State
 
 WHITE = 0xff
 BLACK = 0x00
-
-
-def setup_display_specifics(config):
-    width = 0
-    height = 0
-    face_pos = (0, 0)
-    name_pos = (0, 0)
-    status_pos = (0, 0)
-
-    if config['ui']['display']['type'] in ('inky', 'inkyphat'):
-        fonts.setup(10, 8, 10, 25)
-
-        width = 212
-        height = 104
-        face_pos = (0, int(height / 4))
-        name_pos = (int(width / 2) - 15, int(height * .15))
-        status_pos = (int(width / 2) - 15, int(height * .30))
-
-    elif config['ui']['display']['type'] in ('papirus', 'papi'):
-        fonts.setup(10, 8, 10, 23)
-
-        width = 200
-        height = 96
-        face_pos = (0, int(height / 4))
-        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',
-                                             'ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
-        fonts.setup(10, 9, 10, 35)
-
-        width = 250
-        height = 122
-        face_pos = (0, 40)
-        name_pos = (125, 20)
-        status_pos = (125, 35)
-
-    return width, height, face_pos, name_pos, status_pos
+ROOT = None
 
 
 class View(object):
-    def __init__(self, config, state={}):
+    def __init__(self, config, impl, state=None):
+        global ROOT
+
+        # setup faces from the configuration in case the user customized them
+        faces.load_from_config(config['ui']['faces'])
+
         self._render_cbs = []
         self._config = config
         self._canvas = None
+        self._frozen = False
         self._lock = Lock()
         self._voice = Voice(lang=config['main']['lang'])
-
-        self._width, self._height, \
-        face_pos, name_pos, status_pos = setup_display_specifics(config)
-
+        self._implementation = impl
+        self._layout = impl.layout()
+        self._width = self._layout['width']
+        self._height = self._layout['height']
         self._state = State(state={
-            'channel': LabeledValue(color=BLACK, label='CH', value='00', position=(0, 0), label_font=fonts.Bold,
+            'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'],
+                                    label_font=fonts.Bold,
                                     text_font=fonts.Medium),
-            'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=(30, 0), label_font=fonts.Bold,
+            'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=self._layout['aps'],
+                                label_font=fonts.Bold,
                                 text_font=fonts.Medium),
 
-            # 'epoch': LabeledValue(color=BLACK, label='E', value='0000', position=(145, 0), label_font=fonts.Bold,
-            #                      text_font=fonts.Medium),
-
-            'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=(self._width - 65, 0),
+            'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=self._layout['uptime'],
                                    label_font=fonts.Bold,
                                    text_font=fonts.Medium),
 
-            'line1': Line([0, int(self._height * .12), self._width, int(self._height * .12)], color=BLACK),
-            'line2': Line(
-                [0, self._height - int(self._height * .12), self._width, self._height - int(self._height * .12)],
-                color=BLACK),
+            'line1': Line(self._layout['line1'], color=BLACK),
+            'line2': Line(self._layout['line2'], color=BLACK),
 
-            'face': Text(value=faces.SLEEP, position=face_pos, color=BLACK, font=fonts.Huge),
+            'face': Text(value=faces.SLEEP, position=self._layout['face'], color=BLACK, font=fonts.Huge),
 
-            'friend_face': Text(value=None, position=(0, (self._height * 0.88) - 15), font=fonts.Bold, color=BLACK),
-            'friend_name': Text(value=None, position=(40, (self._height * 0.88) - 13), font=fonts.BoldSmall,
+            'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK),
+            'friend_name': Text(value=None, position=self._layout['friend_name'], font=fonts.BoldSmall,
                                 color=BLACK),
 
-            'name': Text(value='%s>' % 'pwnagotchi', position=name_pos, color=BLACK, font=fonts.Bold),
+            'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=BLACK, font=fonts.Bold),
 
             'status': Text(value=self._voice.default(),
-                           position=status_pos,
+                           position=self._layout['status']['pos'],
                            color=BLACK,
-                           font=fonts.Medium,
+                           font=self._layout['status']['font'],
                            wrap=True,
                            # the current maximum number of characters per line, assuming each character is 6 pixels wide
-                           max_length=(self._width - status_pos[0]) // 6),
+                           max_length=self._layout['status']['max']),
 
             'shakes': LabeledValue(label='PWND ', value='0 (00)', color=BLACK,
-                                   position=(0, self._height - int(self._height * .12) + 1), label_font=fonts.Bold,
+                                   position=self._layout['shakes'], label_font=fonts.Bold,
                                    text_font=fonts.Medium),
-            'mode': Text(value='AUTO', position=(self._width - 25, self._height - int(self._height * .12) + 1),
+            'mode': Text(value='AUTO', position=self._layout['mode'],
                          font=fonts.Bold, color=BLACK),
         })
 
-        for key, value in state.items():
-            self._state.set(key, value)
+        if state:
+            for key, value in state.items():
+                self._state.set(key, value)
 
         plugins.on('ui_setup', self)
 
@@ -119,9 +86,17 @@ class View(object):
             logging.warning("ui.fps is 0, the display will only update for major changes")
             self._ignore_changes = ('uptime', 'name')
 
+        ROOT = self
+
+    def has_element(self, key):
+        self._state.has_element(key)
+
     def add_element(self, key, elem):
         self._state.add_element(key, elem)
 
+    def remove_element(self, key):
+        self._state.remove_element(key)
+
     def width(self):
         return self._width
 
@@ -148,27 +123,30 @@ class View(object):
     def set(self, key, value):
         self._state.set(key, value)
 
+    def get(self, key):
+        return self._state.get(key)
+
     def on_starting(self):
         self.set('status', self._voice.on_starting())
         self.set('face', faces.AWAKE)
 
     def on_ai_ready(self):
-        self.set('mode', '')
+        self.set('mode', '  AI')
         self.set('face', faces.HAPPY)
         self.set('status', self._voice.on_ai_ready())
         self.update()
 
-    def on_manual_mode(self, log):
+    def on_manual_mode(self, last_session):
         self.set('mode', 'MANU')
-        self.set('face', faces.SAD if log.handshakes == 0 else faces.HAPPY)
-        self.set('status', self._voice.on_log(log))
-        self.set('epoch', "%04d" % log.epochs)
-        self.set('uptime', log.duration)
+        self.set('face', faces.SAD if (last_session.epochs > 3 and last_session.handshakes == 0) else faces.HAPPY)
+        self.set('status', self._voice.on_last_session_data(last_session))
+        self.set('epoch', "%04d" % last_session.epochs)
+        self.set('uptime', last_session.duration)
         self.set('channel', '-')
-        self.set('aps', "%d" % log.associated)
-        self.set('shakes', '%d (%s)' % (log.handshakes, \
+        self.set('aps', "%d" % last_session.associated)
+        self.set('shakes', '%d (%s)' % (last_session.handshakes, \
                                         utils.total_unique_handshakes(self._config['bettercap']['handshakes'])))
-        self.set_closest_peer(log.last_peer)
+        self.set_closest_peer(last_session.last_peer, last_session.peers)
 
     def is_normal(self):
         return self._state.get('face') not in (
@@ -183,12 +161,17 @@ class View(object):
             faces.SAD,
             faces.LONELY)
 
+    def on_keys_generation(self):
+        self.set('face', faces.AWAKE)
+        self.set('status', self._voice.on_keys_generation())
+        self.update()
+
     def on_normal(self):
         self.set('face', faces.AWAKE)
         self.set('status', self._voice.on_normal())
         self.update()
 
-    def set_closest_peer(self, peer):
+    def set_closest_peer(self, peer, num_total):
         if peer is None:
             self.set('friend_face', None)
             self.set('friend_name', None)
@@ -207,6 +190,12 @@ class View(object):
             name += '│' * (4 - num_bars)
             name += ' %s %d (%d)' % (peer.name(), peer.pwnd_run(), peer.pwnd_total())
 
+            if num_total > 1:
+                if num_total > 9000:
+                    name += ' of over 9000'
+                else:
+                    name += ' of %d' % num_total
+
             self.set('friend_face', peer.face())
             self.set('friend_name', name)
         self.update()
@@ -255,6 +244,12 @@ class View(object):
 
         self.on_normal()
 
+    def on_shutdown(self):
+        self.set('face', faces.SLEEP)
+        self.set('status', self._voice.on_shutdown())
+        self.update(force=True)
+        self._frozen = True
+
     def on_bored(self):
         self.set('face', faces.BORED)
         self.set('status', self._voice.on_bored())
@@ -305,6 +300,11 @@ class View(object):
         self.set('status', self._voice.on_handshakes(new_shakes))
         self.update()
 
+    def on_unread_messages(self, count, total):
+        self.set('face', faces.EXCITED)
+        self.set('status', self._voice.on_unread_messages(count, total))
+        self.update()
+
     def on_rebooting(self):
         self.set('face', faces.BROKEN)
         self.set('status', self._voice.on_rebooting())
@@ -315,8 +315,14 @@ class View(object):
         self.set('status', self._voice.custom(text))
         self.update()
 
-    def update(self, force=False):
+    def update(self, force=False, new_data={}):
+        for key, val in new_data.items():
+            self.set(key, val)
+
         with self._lock:
+            if self._frozen:
+                return
+
             changes = self._state.changes(ignore=self._ignore_changes)
             if force or len(changes):
                 self._canvas = Image.new('1', (self._width, self._height), WHITE)
diff --git a/pwnagotchi/ui/web.py b/pwnagotchi/ui/web.py
new file mode 100644
index 0000000..82af269
--- /dev/null
+++ b/pwnagotchi/ui/web.py
@@ -0,0 +1,197 @@
+import re
+import _thread
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from threading import Lock
+import shutil
+import logging
+
+import pwnagotchi
+from pwnagotchi import plugins
+
+frame_path = '/root/pwnagotchi.png'
+frame_format = 'PNG'
+frame_ctype = 'image/png'
+frame_lock = Lock()
+
+
+def update_frame(img):
+    global frame_lock, frame_path, frame_format
+    with frame_lock:
+        img.save(frame_path, format=frame_format)
+
+
+STYLE = """
+.block {
+    -webkit-appearance: button;
+    -moz-appearance: button;
+    appearance: button;
+
+    display: block;
+    cursor: pointer;
+    text-align: center;
+}
+"""
+
+SCRIPT = """
+window.onload = function() {
+    var image = document.getElementById("ui");
+    function updateImage() {
+        image.src = image.src.split("?")[0] + "?" + new Date().getTime();
+    }
+    setInterval(updateImage, %d);
+}
+"""
+
+INDEX = """<html>
+  <head>
+      <title>%s</title>
+      <style>""" + STYLE + """</style>
+  </head>
+  <body>
+    <div style="position: absolute; top:0; left:0; width:100%%;">
+        <img src="/ui" id="ui" style="width:100%%"/>
+        <br/>
+        <hr/>
+        <form action="/shutdown" onsubmit="return confirm('This will halt the unit, continue?');">
+            <input type="submit" class="block" value="Shutdown"/>
+        </form>
+    </div>
+
+    <script type="text/javascript">""" + SCRIPT + """</script>
+  </body>
+</html>"""
+
+SHUTDOWN = """<html>
+  <head>
+      <title>%s</title>
+      <style>""" + STYLE + """</style>
+  </head>
+  <body>
+    <div style="position: absolute; top:0; left:0; width:100%%;">
+        Shutting down ...
+    </div>
+  </body>
+</html>"""
+
+
+class Handler(BaseHTTPRequestHandler):
+    AllowedOrigin = '*'
+
+    # suppress internal logging
+    def log_message(self, format, *args):
+        return
+
+    def _send_cors_headers(self):
+        # misc security
+        self.send_header("X-Frame-Options", "DENY")
+        self.send_header("X-Content-Type-Options", "nosniff")
+        self.send_header("X-XSS-Protection", "1; mode=block")
+        self.send_header("Referrer-Policy", "same-origin")
+        # cors
+        self.send_header("Access-Control-Allow-Origin", Handler.AllowedOrigin)
+        self.send_header('Access-Control-Allow-Credentials', 'true')
+        self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+        self.send_header("Access-Control-Allow-Headers",
+                         "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
+        self.send_header("Vary", "Origin")
+
+    # just render some html in a 200 response
+    def _html(self, html):
+        self.send_response(200)
+        self._send_cors_headers()
+        self.send_header('Content-type', 'text/html')
+        self.end_headers()
+        try:
+            self.wfile.write(bytes(html, "utf8"))
+        except:
+            pass
+
+    # serve the main html page
+    def _index(self):
+        self._html(INDEX % (pwnagotchi.name(), 1000))
+
+    # serve a message and shuts down the unit
+    def _shutdown(self):
+        self._html(SHUTDOWN % pwnagotchi.name())
+        pwnagotchi.shutdown()
+
+    # serve the PNG file with the display image
+    def _image(self):
+        global frame_lock, frame_path, frame_ctype
+
+        with frame_lock:
+            self.send_response(200)
+            self._send_cors_headers()
+            self.send_header('Content-type', frame_ctype)
+            self.end_headers()
+            try:
+                with open(frame_path, 'rb') as fp:
+                    shutil.copyfileobj(fp, self.wfile)
+            except:
+                pass
+
+    def do_OPTIONS(self):
+        self.send_response(200)
+        self._send_cors_headers()
+        self.end_headers()
+
+    # check the Origin header vs CORS
+    def _is_allowed(self):
+        origin = self.headers.get('origin')
+        if not origin and Handler.AllowedOrigin != '*':
+            logging.warning("request with no Origin header from %s" % self.address_string())
+            return False
+
+        if Handler.AllowedOrigin != '*':
+            if origin != Handler.AllowedOrigin:
+                logging.warning("request with blocked Origin from %s: %s" % (self.address_string(), origin))
+                return False
+
+        return True
+
+    # main entry point of the http server
+    def do_GET(self):
+        if not self._is_allowed():
+            return
+
+        if self.path == '/':
+            self._index()
+        elif self.path.startswith('/shutdown'):
+            self._shutdown()
+
+        elif self.path.startswith('/ui'):
+            self._image()
+        elif self.path.startswith('/plugins'):
+            plugin_from_path = re.match(r'\/plugins\/([^\/]+)(\/.*)?', self.path)
+            if plugin_from_path:
+                plugin_name = plugin_from_path.groups()[0]
+                right_path = plugin_from_path.groups()[1] if len(plugin_from_path.groups()) == 2 else None
+                if plugin_name in plugins.loaded and hasattr(plugins.loaded[plugin_name], 'on_webhook'):
+                    plugins.loaded[plugin_name].on_webhook(self, right_path)
+        else:
+            self.send_response(404)
+
+
+class Server(object):
+    def __init__(self, config):
+        self._enabled = config['video']['enabled']
+        self._port = config['video']['port']
+        self._address = config['video']['address']
+        self._httpd = None
+
+        if 'origin' in config['video'] and config['video']['origin'] != '*':
+            Handler.AllowedOrigin = config['video']['origin']
+        else:
+            logging.warning("THE WEB UI IS RUNNING WITH ALLOWED ORIGIN SET TO *, READ WHY YOU SHOULD CHANGE IT HERE " +
+                            "https://developer.mozilla.org/it/docs/Web/HTTP/CORS")
+
+        if self._enabled:
+            _thread.start_new_thread(self._http_serve, ())
+
+    def _http_serve(self):
+        if self._address is not None:
+            self._httpd = HTTPServer((self._address, self._port), Handler)
+            logging.info("web ui available at http://%s:%d/" % (self._address, self._port))
+            self._httpd.serve_forever()
+        else:
+            logging.info("could not get ip of usb0, video server not starting")
diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py
index d1b2ba0..ab7acfd 100644
--- a/pwnagotchi/utils.py
+++ b/pwnagotchi/utils.py
@@ -1,10 +1,15 @@
 from datetime import datetime
+from enum import Enum
 import logging
 import glob
 import os
 import time
 import subprocess
 import yaml
+import json
+import shutil
+
+import pwnagotchi
 
 
 # https://stackoverflow.com/questions/823196/yaml-merge-in-python
@@ -19,14 +24,73 @@ def merge_config(user, default):
 
 
 def load_config(args):
-    with open(args.config, 'rt') as fp:
+    default_config_path = os.path.dirname(args.config)
+    if not os.path.exists(default_config_path):
+        os.makedirs(default_config_path)
+
+    ref_defaults_file = os.path.join(os.path.dirname(pwnagotchi.__file__), 'defaults.yml')
+    ref_defaults_data = None
+
+    # check for a config.yml file on /boot/
+    if os.path.exists("/boot/config.yml"):
+        # logging not configured here yet
+        print("installing /boot/config.yml to %s ...", args.user_config)
+        # https://stackoverflow.com/questions/42392600/oserror-errno-18-invalid-cross-device-link
+        shutil.move("/boot/config.yml", args.user_config)
+
+    # if not config is found, copy the defaults
+    if not os.path.exists(args.config):
+        print("copying %s to %s ..." % (ref_defaults_file, args.config))
+        shutil.copy(ref_defaults_file, args.config)
+    else:
+        # check if the user messed with the defaults
+        with open(ref_defaults_file) as fp:
+            ref_defaults_data = fp.read()
+
+        with open(args.config) as fp:
+            defaults_data = fp.read()
+
+        if ref_defaults_data != defaults_data:
+            print("!!! file in %s is different than release defaults, overwriting !!!" % args.config)
+            shutil.copy(ref_defaults_file, args.config)
+
+    # load the defaults
+    with open(args.config) as fp:
         config = yaml.safe_load(fp)
 
+    # load the user config
     if os.path.exists(args.user_config):
-        with open(args.user_config, 'rt') as fp:
+        with open(args.user_config) as fp:
             user_config = yaml.safe_load(fp)
-            config = merge_config(user_config, config)
+            # if the file is empty, safe_load will return None and merge_config will boom.
+            if user_config:
+                config = merge_config(user_config, config)
 
+    # the very first step is to normalize the display name so we don't need dozens of if/elif around
+    if config['ui']['display']['type'] in ('inky', 'inkyphat'):
+        config['ui']['display']['type'] = 'inky'
+
+    elif config['ui']['display']['type'] in ('papirus', 'papi'):
+        config['ui']['display']['type'] = 'papirus'
+
+    elif config['ui']['display']['type'] in ('oledhat'):
+        config['ui']['display']['type'] = 'oledhat'
+
+    elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1'):
+        config['ui']['display']['type'] = 'waveshare_1'
+
+    elif config['ui']['display']['type'] in ('ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
+        config['ui']['display']['type'] = 'waveshare_2'
+
+    elif config['ui']['display']['type'] in ('ws_27inch', 'ws27inch', 'waveshare_27inch', 'waveshare27inch'):
+        config['ui']['display']['type'] = 'waveshare27inch'
+
+    else:
+        print("unsupported display type %s" % config['ui']['display']['type'])
+        exit(1)
+
+    print("Effective Configuration:")
+    print(yaml.dump(config, default_flow_style=False))
     return config
 
 
@@ -81,18 +145,142 @@ def blink(times=1, delay=0.3):
     led(True)
 
 
+class WifiInfo(Enum):
+    """
+    Fields you can extract from a pcap file
+    """
+    BSSID = 0
+    ESSID = 1
+    ENCRYPTION = 2
+    CHANNEL = 3
+    RSSI = 4
+
+
+class FieldNotFoundError(Exception):
+    pass
+
+
+def extract_from_pcap(path, fields):
+    """
+    Search in pcap-file for specified information
+
+    path: Path to pcap file
+    fields: Array of fields that should be extracted
+
+    If a field is not found, FieldNotFoundError is raised
+    """
+    results = dict()
+    for field in fields:
+        if not isinstance(field, WifiInfo):
+            raise TypeError("Invalid field")
+
+        subtypes = set()
+
+        if field == WifiInfo.BSSID:
+            from scapy.all import Dot11Beacon, Dot11ProbeResp, Dot11AssoReq, Dot11ReassoReq, Dot11, sniff
+            subtypes.add('beacon')
+            bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
+            packets = sniff(offline=path, filter=bpf_filter)
+            try:
+                for packet in packets:
+                    if packet.haslayer(Dot11Beacon):
+                        if hasattr(packet[Dot11], 'addr3'):
+                            results[field] = packet[Dot11].addr3
+                            break
+                else:  # magic
+                    raise FieldNotFoundError("Could not find field [BSSID]")
+            except Exception:
+                raise FieldNotFoundError("Could not find field [BSSID]")
+        elif field == WifiInfo.ESSID:
+            from scapy.all import Dot11Beacon, Dot11ReassoReq, Dot11AssoReq, Dot11, sniff, Dot11Elt
+            subtypes.add('beacon')
+            subtypes.add('assoc-req')
+            subtypes.add('reassoc-req')
+            bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
+            packets = sniff(offline=path, filter=bpf_filter)
+            try:
+                for packet in packets:
+                    if packet.haslayer(Dot11Elt) and hasattr(packet[Dot11Elt], 'info'):
+                        results[field] = packet[Dot11Elt].info.decode('utf-8')
+                        break
+                else:  # magic
+                    raise FieldNotFoundError("Could not find field [ESSID]")
+            except Exception:
+                raise FieldNotFoundError("Could not find field [ESSID]")
+        elif field == WifiInfo.ENCRYPTION:
+            from scapy.all import Dot11Beacon, sniff
+            subtypes.add('beacon')
+            bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
+            packets = sniff(offline=path, filter=bpf_filter)
+            try:
+                for packet in packets:
+                    if packet.haslayer(Dot11Beacon) and hasattr(packet[Dot11Beacon], 'network_stats'):
+                        stats = packet[Dot11Beacon].network_stats()
+                        if 'crypto' in stats:
+                            results[field] = stats['crypto']  # set with encryption types
+                            break
+                else:  # magic
+                    raise FieldNotFoundError("Could not find field [ENCRYPTION]")
+            except Exception:
+                raise FieldNotFoundError("Could not find field [ENCRYPTION]")
+        elif field == WifiInfo.CHANNEL:
+            from scapy.all import sniff, RadioTap
+            from pwnagotchi.mesh.wifi import freq_to_channel
+            packets = sniff(offline=path, count=1)
+            try:
+                results[field] = freq_to_channel(packets[0][RadioTap].ChannelFrequency)
+            except Exception:
+                raise FieldNotFoundError("Could not find field [CHANNEL]")
+        elif field == WifiInfo.RSSI:
+            from scapy.all import sniff, RadioTap
+            from pwnagotchi.mesh.wifi import freq_to_channel
+            packets = sniff(offline=path, count=1)
+            try:
+                results[field] = packets[0][RadioTap].dBm_AntSignal
+            except Exception:
+                raise FieldNotFoundError("Could not find field [RSSI]")
+
+    return results
+
+
 class StatusFile(object):
-    def __init__(self, path):
+    def __init__(self, path, data_format='raw'):
         self._path = path
         self._updated = None
+        self._format = data_format
+        self.data = None
 
         if os.path.exists(path):
             self._updated = datetime.fromtimestamp(os.path.getmtime(path))
+            with open(path) as fp:
+                if data_format == 'json':
+                    self.data = json.load(fp)
+                else:
+                    self.data = fp.read()
+
+    def data_field_or(self, name, default=""):
+        if self.data is not None and name in self.data:
+            return self.data[name]
+        return default
+
+    def newer_then_minutes(self, minutes):
+        return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < minutes
+
+    def newer_then_hours(self, hours):
+        return self._updated is not None and ((datetime.now() - self._updated).seconds / (60*60)) < hours
 
     def newer_then_days(self, days):
         return self._updated is not None and (datetime.now() - self._updated).days < days
 
     def update(self, data=None):
         self._updated = datetime.now()
+        self.data = data
         with open(self._path, 'w') as fp:
-            fp.write(str(self._updated) if data is None else data)
+            if data is None:
+                fp.write(str(self._updated))
+
+            elif self._format == 'json':
+                json.dump(self.data, fp)
+
+            else:
+                fp.write(data)
diff --git a/pwnagotchi/voice.py b/pwnagotchi/voice.py
index 9268b11..530c815 100644
--- a/pwnagotchi/voice.py
+++ b/pwnagotchi/voice.py
@@ -14,6 +14,9 @@ class Voice:
         translation.install()
         self._ = translation.gettext
 
+    def custom(self, s):
+        return s
+
     def default(self):
         return self._('ZzzzZZzzzzZzzz')
 
@@ -28,6 +31,10 @@ class Voice:
             self._('AI ready.'),
             self._('The neural network is ready.')])
 
+    def on_keys_generation(self):
+        return random.choice([
+            self._('Generating keys, do not turn off ...')])
+
     def on_normal(self):
         return random.choice([
             '',
@@ -64,8 +71,8 @@ class Voice:
 
     def on_new_peer(self, peer):
         return random.choice([
-            self._('Hello {name}! Nice to meet you. {name}').format(name=peer.name()),
-            self._('Unit {name} is nearby! {name}').format(name=peer.name())])
+            self._('Hello {name}! Nice to meet you.').format(name=peer.name()),
+            self._('Unit {name} is nearby!').format(name=peer.name())])
 
     def on_lost_peer(self, peer):
         return random.choice([
@@ -90,6 +97,11 @@ class Voice:
             self._('Zzzzz'),
             self._('ZzzZzzz ({secs}s)').format(secs=secs)])
 
+    def on_shutdown(self):
+        return random.choice([
+            self._('Good night.'),
+            self._('Zzz')])
+
     def on_awakening(self):
         return random.choice(['...', '!'])
 
@@ -117,26 +129,30 @@ class Voice:
         s = 's' if new_shakes > 1 else ''
         return self._('Cool, we got {num} new handshake{plural}!').format(num=new_shakes, plural=s)
 
+    def on_unread_messages(self, count, total):
+        s = 's' if count > 1 else ''
+        return self._('You have {count} new message{plural}!').format(num=count, plural=s)
+
     def on_rebooting(self):
         return self._("Ops, something went wrong ... Rebooting ...")
 
-    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)
-        if log.peers == 1:
+    def on_last_session_data(self, last_session):
+        status = self._('Kicked {num} stations\n').format(num=last_session.deauthed)
+        status += self._('Made {num} new friends\n').format(num=last_session.associated)
+        status += self._('Got {num} handshakes\n').format(num=last_session.handshakes)
+        if last_session.peers == 1:
             status += self._('Met 1 peer')
-        elif log.peers > 0:
-            status += self._('Met {num} peers').format(num=log.peers)
+        elif last_session.peers > 0:
+            status += self._('Met {num} peers').format(num=last_session.peers)
         return status
 
-    def on_log_tweet(self, log):
+    def on_last_session_tweet(self, last_session):
         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)
+            duration=last_session.duration_human,
+            deauthed=last_session.deauthed,
+            associated=last_session.associated,
+            handshakes=last_session.handshakes)
 
     def hhmmss(self, count, fmt):
         if count > 1:
diff --git a/requirements.txt b/requirements.txt
index 8fa5e8a..7c18a47 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,16 @@
-Crypto
-requests
-pyyaml
-scapy
-gym
-stable-baselines
-tensorflow
-tweepy
-file_read_backwards
-numpy
-inky
-smbus
-pillow
+crypto==1.4.1
+requests==2.21.0
+PyYAML==5.1
+scapy==2.4.3
+gym==0.14.0
+stable-baselines==2.7.0
+tensorflow==1.13.1
+tensorflow-estimator==1.14.0
+tweepy==3.7.0
+file-read-backwards==2.0.0
+numpy==1.17.2
+inky==0.0.5
+smbus2==0.3.0
+Pillow==5.4.1
+spidev==3.4
+gast==0.2.2
diff --git a/scripts/backup.sh b/scripts/backup.sh
index 8ebf274..71c4f61 100755
--- a/scripts/backup.sh
+++ b/scripts/backup.sh
@@ -4,18 +4,14 @@
 UNIT_HOSTNAME=${1:-10.0.0.2}
 # output backup zip file
 OUTPUT=${2:-pwnagotchi-backup.zip}
-# temporary folder
-TEMP_BACKUP_FOLDER=/tmp/pwnagotchi_backup
 # what to backup
 FILES_TO_BACKUP=(
   /root/brain.nn
   /root/brain.json
-  /root/custom.yaml
+  /root/.api-report.json
   /root/handshakes
-  /etc/ssh
-  /etc/hostname
-  /etc/hosts
-  /etc/motd
+  /root/peers
+  /etc/pwnagotchi/
   /var/log/pwnagotchi.log
 )
 
@@ -26,17 +22,24 @@ ping -c 1 $UNIT_HOSTNAME >/dev/null || {
 
 echo "@ backing up $UNIT_HOSTNAME to $OUTPUT ..."
 
-rm -rf "$TEMP_BACKUP_FOLDER"
+ssh pi@$UNIT_HOSTNAME "sudo rm -rf /tmp/backup && sudo rm -rf /tmp/backup.zip" > /dev/null
 
 for file in "${FILES_TO_BACKUP[@]}"; do
   dir=$(dirname $file)
-  echo "  $file -> $TEMP_BACKUP_FOLDER$dir/"
-  mkdir -p "$TEMP_BACKUP_FOLDER/$dir"
-  scp -Cr root@$UNIT_HOSTNAME:$file "$TEMP_BACKUP_FOLDER$dir/"
+
+  echo "@ copying $file to /tmp/backup$dir"
+
+  ssh pi@$UNIT_HOSTNAME "mkdir -p /tmp/backup$dir" > /dev/null
+  ssh pi@$UNIT_HOSTNAME "sudo cp -r $file /tmp/backup$dir" > /dev/null
 done
 
-ZIPFILE="$PWD/$OUTPUT"
-pushd $PWD
-cd "$TEMP_BACKUP_FOLDER"
-zip -r -9 -q "$ZIPFILE" .
-popd
+echo "@ pulling from $UNIT_HOSTNAME ..."
+
+rm -rf /tmp/backup
+scp -rC pi@$UNIT_HOSTNAME:/tmp/backup /tmp/
+
+echo "@ compressing ..."
+
+zip -r -9 -q $OUTPUT /tmp/backup
+rm -rf /tmp/backup
+
diff --git a/scripts/create_sibling.sh b/scripts/create_sibling.sh
deleted file mode 100755
index 02537cd..0000000
--- a/scripts/create_sibling.sh
+++ /dev/null
@@ -1,352 +0,0 @@
-#!/usr/bin/env bash
-# based on: https://wiki.debian.org/RaspberryPi/qemu-user-static
-## and https://z4ziggy.wordpress.com/2015/05/04/from-bochs-to-chroot/
-
-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 )
-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="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' )
-
-if [[ "$EUID" -ne 0 ]]; then
-   echo "Run this script as root!"
-   exit 1
-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
-      echo "Dependency check failed for ${REQ}"
-      exit 1
-    fi
-  done
-
-  if ! test -e /usr/bin/qemu-arm-static; then
-    echo "[-] You need the package \"qemu-user-static\" for this to work."
-    exit 1
-  fi
-
-  if ! systemctl is-active systemd-binfmt.service >/dev/null 2>&1; then
-     mkdir -p "/lib/binfmt.d"
-     echo ':qemu-arm:M::\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:F' > /lib/binfmt.d/qemu-arm-static.conf
-     systemctl restart systemd-binfmt.service
-  fi
-}
-
-function get_raspbian() {
-  VERSION="$1"
-
-  case "$VERSION" in
-    latest)
-      URL="https://downloads.raspberrypi.org/raspbian_lite_latest"
-      ;;
-    buster)
-      URL="https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-07-12/2019-07-10-raspbian-buster.zip"
-      ;;
-    stretch)
-      URL="https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/2019-04-08-raspbian-stretch.zip"
-      ;;
-  esac
-
-  echo "[+] Downloading raspbian.zip"
-  mkdir -p "${TMP_DIR}"
-  wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "$URL"
-  echo "[+] Unpacking raspbian.zip to raspbian.img"
-  gunzip -c "${TMP_DIR}/raspbian.zip" > "${TMP_DIR}/raspbian.img"
-}
-
-function provide_raspbian() {
-  echo "[+] Providing path of raspbian file"
-  mkdir -p "${TMP_DIR}"
-  echo "[+] Unpacking raspbian.zip to raspbian.img"
-  gunzip -c "${PWNI_INPUT}" > "${TMP_DIR}/raspbian.img"
-}
-
-function setup_raspbian(){
-  # Detect the ability to create sparse files
-  if [ "${OPT_SPARSE}" -eq 0 ]; then
-    if [ which bmaptool -eq 0 ]; then
-      echo "[!] bmaptool not available, not creating a sparse image"
-      
-    else
-      echo "[+] Defaulting to sparse image generation as bmaptool is available"
-      OPT_SPARSE=1
-    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")"
-  PART2_START="$(parted -s "$LOOP_PATH" -- print | awk '$1==2{ print $2 }')"
-  parted -s "$LOOP_PATH" rm 2
-  parted -s "$LOOP_PATH" mkpart primary "$PART2_START" 100%
-  echo "[+] Check FS"
-  e2fsck -y -f "${LOOP_PATH}p2"
-  echo "[+] Resize FS"
-  resize2fs "${LOOP_PATH}p2"
-  echo "[+] Device is ${LOOP_PATH}"
-  echo "[+] Unmount if already mounted with other img"
-  mountpoint -q "${MNT_DIR}" && umount -R "${MNT_DIR}"
-  echo "[+] Mount /"
-  mount -o rw "${LOOP_PATH}p2" "${MNT_DIR}"
-  echo "[+] Mount /boot"
-  mount -o rw "${LOOP_PATH}p1" "${MNT_DIR}/boot"
-  mount --bind /dev "${MNT_DIR}/dev/"
-  mount --bind /sys "${MNT_DIR}/sys/"
-  mount --bind /proc "${MNT_DIR}/proc/"
-  mount --bind /dev/pts "${MNT_DIR}/dev/pts"
-  cp /usr/bin/qemu-arm-static "${MNT_DIR}/usr/bin"
-  cp /etc/resolv.conf "${MNT_DIR}/etc/resolv.conf"
-}
-
-function provision_raspbian() {
-  cd "${MNT_DIR}"
-  sed -i'' 's/^\([^#]\)/#\1/g' etc/ld.so.preload # add comments
-  echo "[+] Run chroot commands"
-  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
-  apt-get -y upgrade
-  apt-get -y install git vim screen build-essential golang python3-pip gawk
-  apt-get -y install libpcap-dev libusb-1.0-0-dev libnetfilter-queue-dev
-  apt-get -y install dphys-swapfile libopenmpi-dev libatlas-base-dev
-  apt-get -y install libjasper-dev libqtgui4 libqt4-test libopenjp2-7
-  apt-get -y install tcpdump libilmbase23 libopenexr23 libgstreamer1.0-0
-  apt-get -y install libavcodec58 libavformat58 libswscale5
-
-  # setup dphys-swapfile
-  echo "CONF_SWAPSIZE=1024" >/etc/dphys-swapfile
-  systemctl enable dphys-swapfile.service
-
-  # install pwnagotchi
-  cd /tmp
-  git clone https://github.com/evilsocket/pwnagotchi.git
-  rsync -aP pwnagotchi/sdcard/boot/* /boot/
-  rsync -aP pwnagotchi/sdcard/rootfs/* /
-  rm -rf /tmp/pwnagotchi
-
-  # configure pwnagotchi
-  echo -e "$PWNI_NAME" > /etc/hostname
-  sed -i "s@^127\.0\.0\.1 .*@127.0.0.1 localhost "$PWNI_NAME" "$PWNI_NAME".local@g" /etc/hosts
-  sed -i "s@alpha@$PWNI_NAME@g" /etc/motd
-
-  chmod +x /etc/rc.local
-
-  # need armv6l version of tensorflow and opencv-python, not armv7l
-  # PIP_OPTS="--upgrade --only-binary :all: --abi cp37m --platform linux_armv6l --target /usr/lib/python3.7/site-packages/"
-  # pip3 install \$PIP_OPTS opencv-python
-  # Should work for tensorflow too, but BUG: Hash mismatch; therefore:
-  wget -P /root/ -c https://www.piwheels.org/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl
-  wget -P /root/ -c https://www.piwheels.org/simple/opencv-python/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl
-  # we need to install these on first raspberry start...
-  sed -i '/startup\.sh/i pip3 install --no-deps --force-reinstall --upgrade /root/tensorflow-1.13.1-cp37-none-linux_armv6l.whl /root/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl && rm /root/tensorflow-1.13.1-cp37-none-linux_armv6l.whl /root/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl && sed -i "/tensorflow/d" /etc/rc.local' /etc/rc.local
-
-  # newer version is broken
-  pip3 install gast==0.2.2
-
-  </root/pwnagotchi/scripts/requirements.txt xargs -I{} --max-args=1 --max-procs="$(nproc)"\
-    pip3 install --progress-bar off {}
-
-  # waveshare
-  pip3 install spidev RPi.GPIO
-
-  # install bettercap
-  export GOPATH=/root/go
-  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)
-  cd /tmp
-  git clone https://github.com/bettercap/caplets.git
-  cd caplets
-  make install
-  rm -rf /tmp/caplets
-  cd /root # fixes getcwd error that was bugging me
-
-  # Re4son-Kernel
-  echo "deb http://http.re4son-kernel.com/re4son/ kali-pi main" > /etc/apt/sources.list.d/re4son.list
-  wget -O - https://re4son-kernel.com/keys/http/archive-key.asc | apt-key add -
-  apt update
-  apt install -y kalipi-kernel kalipi-bootloader kalipi-re4son-firmware kalipi-kernel-headers libraspberrypi0 libraspberrypi-dev libraspberrypi-doc libraspberrypi-bin
-
-  # Fix PARTUUID
-  PUUID_ROOT="\$(blkid "\$(df / --output=source | tail -1)" | grep -Po 'PARTUUID="\K[^"]+')"
-  PUUID_BOOT="\$(blkid "\$(df /boot --output=source | tail -1)" | grep -Po 'PARTUUID="\K[^"]+')"
-
-  # sed regex info: search for line containing / followed by whitespace or /boot (second sed)
-  #                 in this line, search for PARTUUID= followed by letters, numbers or "-"
-  #                 replace that match with the new PARTUUID
-  sed -i "/\/[ ]\+/s/PARTUUID=[A-Za-z0-9-]\+/PARTUUID=\$PUUID_ROOT/g" /etc/fstab
-  sed -i "/\/boot/s/PARTUUID=[A-Za-z0-9-]\+/PARTUUID=\$PUUID_BOOT/g" /etc/fstab
-
-  sed -i "s/root=[^ ]\+/root=PARTUUID=\${PUUID_ROOT}/g" /boot/cmdline.txt
-
-  # delete keys
-  find /etc/ssh/ -name "ssh_host_*key*" -delete
-
-  # slows down boot
-  systemctl disable apt-daily.timer apt-daily.service apt-daily-upgrade.timer apt-daily-upgrade.service
-
-  # unecessary services
-  systemctl disable triggerhappy bluetooth wpa_supplicant
-
-EOF
-  sed -i'' 's/^#//g' etc/ld.so.preload
-  cd "${REPO_DIR}"
-  umount -R "${MNT_DIR}"
-  losetup -D "$(losetup -l | awk '/raspbian\.img/{print $1}')"
-  mv "${TMP_DIR}/raspbian.img" "${PWNI_OUTPUT}"
-  if [ "${OPT_SPARSE}" -eq 1 ];
-  then
-    bmaptool create -o "${PWNI_OUTPUT}.bmap" "${PWNI_OUTPUT}"
-  fi
-}
-
-function usage() {
-  cat <<EOF
-
-usage: $0 [OPTIONS]
-
-  Options:
-    -n <name>    # Name of the pwnagotchi (default: pwnagotchi)
-    -i <file>    # Provide the path of an already downloaded raspbian image
-    -o <file>    # Name of the img-file (default: pwnagotchi.img)
-    -s <size>    # Size which should be added to second partition (in Gigabyte) (default: 4)
-    -v <version> # Version of raspbian (Supported: ${SUPPORTED_RASPBIAN_VERSIONS[*]}; default: latest)
-    -p           # Only run provisioning (assumes the image is already mounted)
-    -d           # Only run dependencies checks
-    -h           # Show this help
-
-EOF
-
-  exit 0
-}
-
-while getopts "A:n:i:o:s:v:dph" o; do
-  case "${o}" in
-    A)
-      OPT_APTPROXY="${OPTARG}"
-      ;;
-    n)
-      PWNI_NAME="${OPTARG}"
-      ;;
-    i)
-      PWNI_INPUT="${OPTARG}"
-      OPT_IMAGE_PROVIDED=1
-      ;;
-    o)
-      PWNI_OUTPUT="${OPTARG}"
-      ;;
-    s)
-      PWNI_SIZE="${OPTARG}"
-      ;;
-    p)
-      OPT_PROVISION_ONLY=1
-      ;;
-    d)
-      OPT_CHECK_DEPS_ONLY=1
-      ;;
-    v)
-      if [[ "${SUPPORTED_RASPBIAN_VERSIONS[*]}" =~ ${OPTARG} ]]; then
-        OPT_RASPBIAN_VERSION="${OPTARG}"
-      else
-        usage
-      fi
-      ;;
-    h)
-      usage
-      ;;
-    *)
-      usage
-      ;;
-  esac
-done
-shift $((OPTIND-1))
-
-if [[ "$OPT_PROVISION_ONLY" -eq 1 ]]; then
-  provision_raspbian
-  exit 0
-elif [[ "$OPT_CHECK_DEPS_ONLY" -eq 1 ]]; then
-  check_dependencies
-  exit 0
-fi
-
-check_dependencies
-
-if [[ "$OPT_IMAGE_PROVIDED" -eq 1 ]]; then
-  provide_raspbian
-else
-  get_raspbian "$OPT_RASPBIAN_VERSION"
-fi
-
-setup_raspbian
-provision_raspbian
-
-#Make a baby with a random gender, maybe do something fun with this later!
-gender[0]="boy"
-gender[1]="girl"
-
-rand=$[ $RANDOM % 2 ]
-
-echo -e "[+] Congratz, it's a ${gender[$rand]} (⌐■_■)!"
-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 247911f..08bd97b 100755
--- a/scripts/language.sh
+++ b/scripts/language.sh
@@ -6,8 +6,8 @@ DEPENDENCIES=( 'xgettext' 'msgfmt' 'msgmerge' )
 COMMANDS=( 'add' 'update' 'delete' 'compile' )
 
 REPO_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
-LOCALE_DIR="${REPO_DIR}/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale"
-VOICE_FILE="${REPO_DIR}/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py"
+LOCALE_DIR="${REPO_DIR}/pwnagotchi/locale"
+VOICE_FILE="${REPO_DIR}/pwnagotchi/voice.py"
 
 function usage() {
 cat <<EOF
diff --git a/scripts/preview.py b/scripts/preview.py
index 2b45933..e0ef7c4 100755
--- a/scripts/preview.py
+++ b/scripts/preview.py
@@ -1,99 +1,99 @@
 #!/usr/bin/env python3
-
 import sys
 import os
-import time
 import argparse
-from http.server import HTTPServer
-import shutil
-import logging
 import yaml
 
 sys.path.insert(0,
                 os.path.join(os.path.dirname(os.path.realpath(__file__)),
-                             '../sdcard/rootfs/root/pwnagotchi/scripts/'))
+                             '../'))
 
+import pwnagotchi.ui.faces as faces
 from pwnagotchi.ui.display import Display, VideoHandler
+from PIL import Image
 
 
 class CustomDisplay(Display):
 
+    def __init__(self, config, state):
+        self.last_image = None
+        super(CustomDisplay, self).__init__(config, state)
+
     def _http_serve(self):
-        if self._video_address is not None:
-            self._httpd = HTTPServer((self._video_address, self._video_port),
-                                     CustomVideoHandler)
-            logging.info("ui available at http://%s:%d/" % (self._video_address,
-                                                        self._video_port))
-            self._httpd.serve_forever()
-        else:
-            logging.info("could not get ip of usb0, video server not starting")
+        # do nothing
+        pass
 
     def _on_view_rendered(self, img):
-        CustomVideoHandler.render(img)
+        self.last_image = 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:
-                logging.exception("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:
-                    logging.exception("could not open preview")
-        else:
-            self.send_response(404)
+    def get_image(self):
+        """
+        Return the saved image
+        """
+        return self.last_image
 
 
 class DummyPeer:
+
+    def __init__(self):
+        self.rssi = -50
+
     @staticmethod
     def name():
         return "beta"
 
+    @staticmethod
+    def pwnd_run():
+        return 50
+
+    @staticmethod
+    def pwnd_total():
+        return 100
+
+    @staticmethod
+    def face():
+        return faces.FRIEND
+
+
+def append_images(images, horizontal=True, xmargin=0, ymargin=0):
+    w, h = zip(*(i.size for i in images))
+
+    if horizontal:
+        t_w = sum(w)
+        t_h = max(h)
+    else:
+        t_w = max(w)
+        t_h = sum(h)
+
+    result = Image.new('RGB', (t_w, t_h))
+
+    x_offset = 0
+    y_offset = 0
+
+    for im in images:
+        result.paste(im, (x_offset, y_offset))
+        if horizontal:
+            x_offset += im.size[0] + xmargin
+        else:
+            y_offset += im.size[1] + ymargin
+
+    return result
+
 
 def main():
     parser = argparse.ArgumentParser(description="This program emulates\
                                      the pwnagotchi display")
-    parser.add_argument('--display', help="Which display to use.",
+    parser.add_argument('--displays', help="Which displays to use.", nargs="+",
                         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")
+    parser.add_argument('--output', help="Path to output image (PNG)", default="preview.png")
+    parser.add_argument('--show-peer', dest="showpeer", help="This options will show a dummy peer", action="store_true")
+    parser.add_argument('--xmargin', help="Add X-Margin", type=int, default=5)
+    parser.add_argument('--ymargin', help="Add Y-Margin", type=int, default=5)
     args = parser.parse_args()
 
-    CONFIG = yaml.load('''
+    config_template = '''
     main:
         lang: {lang}
     ui:
@@ -107,64 +107,80 @@ def main():
             video:
                 enabled: true
                 address: "0.0.0.0"
-                port: {port}
-    '''.format(display=args.display,
-               port=args.port,
-               lang=args.lang))
+                port: 8080
+    '''
 
-    DISPLAY = CustomDisplay(config=CONFIG, state={'name': '%s>' % 'preview'})
+    list_of_displays = list()
+    for display_type in args.displays:
+        config = yaml.safe_load(config_template.format(display=display_type,
+                                                       lang=args.lang))
+        display = CustomDisplay(config=config, state={'name': f"{display_type}>"})
+        list_of_displays.append(display)
 
-    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)
+    columns = list()
+
+    for display in list_of_displays:
+        emotions = list()
+        if args.showpeer:
+            display.set_closest_peer(DummyPeer(), 10)
+        display.on_starting()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_ai_ready()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_normal()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_new_peer(DummyPeer())
+        display.update()
+        emotions.append(display.get_image())
+        display.on_lost_peer(DummyPeer())
+        display.update()
+        emotions.append(display.get_image())
+        display.on_free_channel('6')
+        display.update()
+        emotions.append(display.get_image())
+        display.wait(2)
+        display.update()
+        emotions.append(display.get_image())
+        display.on_bored()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_sad()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_motivated(1)
+        display.update()
+        emotions.append(display.get_image())
+        display.on_demotivated(-1)
+        display.update()
+        emotions.append(display.get_image())
+        display.on_excited()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_deauth({'mac': 'DE:AD:BE:EF:CA:FE'})
+        display.update()
+        emotions.append(display.get_image())
+        display.on_miss('test')
+        display.update()
+        emotions.append(display.get_image())
+        display.on_lonely()
+        display.update()
+        emotions.append(display.get_image())
+        display.on_handshakes(1)
+        display.update()
+        emotions.append(display.get_image())
+        display.on_rebooting()
+        display.update()
+        emotions.append(display.get_image())
+
+        # append them all together (vertical)
+        columns.append(append_images(emotions, horizontal=False, xmargin=args.xmargin, ymargin=args.ymargin))
+
+    # append columns side by side
+    final_image = append_images(columns, horizontal=True, xmargin=args.xmargin, ymargin=args.ymargin)
+    final_image.save(args.output, 'PNG')
 
 
 if __name__ == '__main__':
diff --git a/scripts/pypi_upload.sh b/scripts/pypi_upload.sh
new file mode 100755
index 0000000..265a56a
--- /dev/null
+++ b/scripts/pypi_upload.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+rm -rf build dist pwnagotchi.egg-info &&
+  python3 setup.py sdist bdist_wheel &&
+  clear &&
+  twine upload dist/*
diff --git a/scripts/release.sh b/scripts/release.sh
index 2f1343a..fafb08f 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 # nothing to see here, just a utility i use to create new releases ^_^
 
-VERSION_FILE=$(dirname "${BASH_SOURCE[0]}")/../sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/__init__.py
+VERSION_FILE=$(dirname "${BASH_SOURCE[0]}")/../pwnagotchi/__init__.py
 echo "version file is $VERSION_FILE"
 CURRENT_VERSION=$(cat $VERSION_FILE | grep version | cut -d"'" -f2)
 TO_UPDATE=(
diff --git a/scripts/update_pwnagotchi.sh b/scripts/update_pwnagotchi.sh
deleted file mode 100755
index 9299f1a..0000000
--- a/scripts/update_pwnagotchi.sh
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/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]
- Note: This should be run from the pwnagotchi itself!
-
-   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 and hostname references, then overwrite with defaults.
-      -r        # Restore the current pwnagotchi config and hostname references after upgrade. (-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
-}
-
-while getopts "v:u:m:brh" 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))
-
-echo "[+] Checking prerequisites."
-test_root
-test_github
-
-# 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
-
-if [ $BACKUPCONFIG -eq 1 ]; then
-    echo "[+] Creating backup of config.yml and hostname references"
-    mv /root/pwnagotchi/config.yml /root/config.yml.bak -f
-    mv /etc/hosts /root/hosts.bak -f
-    mv /etc/hostname /root/hostname.bak -f
-    mv /etc/motd /etc/motd.bak -f
-    mv /etc/network/interfaces /root/interfaces.bak -f
-fi
-
-echo "[+] Installing $(git log -1 --format="%h")"
-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 and hostname references"
-    mv /root/config.yml.bak /root/pwnagotchi/config.yml -f
-    mv /root/hosts.bak /etc/hosts -f
-    mv /root/hostname.bak /etc/hostname -f
-    mv /root/interfaces.bak /etc/network/interfaces -f
-    mv /etc/motd.bak /etc/motd -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"
diff --git a/setup.py b/setup.py
index a1e6e84..dab1f93 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+# -*- coding: utf-8 -*-
 from setuptools import setup, find_packages
 import pwnagotchi
 
@@ -18,10 +19,12 @@ setup(name='pwnagotchi',
       license='GPL',
       install_requires=required,
       scripts=['bin/pwnagotchi'],
-      package_data={'pwnagotchi': ('pwnagotchi/defaults.yml',)},
+      package_data={'pwnagotchi': ['defaults.yml', 'pwnagotchi/defaults.yml', 'locale/*/LC_MESSAGES/*.mo']},
+      include_package_data=True,
+      packages=find_packages(),
       classifiers=[
           'Programming Language :: Python :: 3',
           'Development Status :: 5 - Production/Stable',
           'License :: OSI Approved :: GNU General Public License (GPL)',
           'Environment :: Console',
-      ])
\ No newline at end of file
+      ])