diff --git a/bin/pwnagotchi b/bin/pwnagotchi index 98c5b561..35a9cf5e 100755 --- a/bin/pwnagotchi +++ b/bin/pwnagotchi @@ -69,7 +69,7 @@ def do_auto_mode(agent): # deauth all client stations in order to get a full handshake for sta in ap['clients']: agent.deauth(ap, sta) - time.sleep(1) + time.sleep(1) # delay to not trigger nexmon firmware bugs # An interesting effect of this: # diff --git a/builder/data/usr/bin/pwnlib b/builder/data/usr/bin/pwnlib index ddef951e..14c412fc 100755 --- a/builder/data/usr/bin/pwnlib +++ b/builder/data/usr/bin/pwnlib @@ -25,20 +25,27 @@ reload_brcm() { if ! modprobe -r brcmfmac; then return 1 fi + sleep 1 if ! modprobe brcmfmac; then return 1 fi + sleep 2 + iw dev wlan0 set power_save off return 0 } # starts mon0 start_monitor_interface() { - airmon-ng start wlan0 + iw dev wlan0 set power_save off + iw phy "$(iw phy | head -1 | cut -d" " -f2)" interface add mon0 type monitor + sleep 2 + ifconfig wlan0 down + ifconfig mon0 up } # stops mon0 stop_monitor_interface() { - airmon-ng stop wlan0 + ifconfig mon0 down && iw dev mon0 del } # returns 0 if the specificed network interface is up @@ -67,12 +74,12 @@ is_auto_mode() { # if usb0 is up, we're in MANU if is_interface_up usb0; then - return 1 + return 0 fi # if eth0 is up (for other boards), we're in MANU if is_interface_up eth0; then - return 1 + return 0 fi # no override, but none of the interfaces is up -> AUTO diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py index 1714d7bc..447e9833 100644 --- a/pwnagotchi/__init__.py +++ b/pwnagotchi/__init__.py @@ -9,7 +9,7 @@ from pwnagotchi._version import __version__ _name = None config = None - +_cpu_stats = {} def set_name(new_name): if new_name is None: @@ -83,13 +83,18 @@ def _cpu_stat(): return list(map(int,fp.readline().split()[1:])) -def cpu_load(): +def cpu_load(tag=None): """ Returns the current cpuload """ - parts0 = _cpu_stat() - time.sleep(0.1) + if tag and tag in _cpu_stats.keys(): + parts0 = _cpu_stats[tag] + else: + parts0 = _cpu_stat() + time.sleep(0.1) # only need to sleep when no tag parts1 = _cpu_stat() + if tag: _cpu_stats[tag] = parts1 + parts_diff = [p1 - p0 for (p0, p1) in zip(parts0, parts1)] user, nice, sys, idle, iowait, irq, softirq, steal, _guest, _guest_nice = parts_diff idle_sum = idle + iowait @@ -162,3 +167,4 @@ def reboot(mode=None): os.system("sync") os.system("shutdown -r now") + os.system("service pwnagotchi restart") diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 7fe6a76d..1f882218 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -312,18 +312,47 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer): def _fetch_stats(self): while True: - s = self.session() - self._update_uptime(s) - self._update_advertisement(s) - self._update_peers() - self._update_counters() - self._update_handshakes(0) - time.sleep(1) + try: + s = self.session() + except Exception as err: + logging.error("[agent:_fetch_stats] self.session: %s" % repr(err)) + + try: + self._update_uptime(s) + except Exception as err: + logging.error("[agent:_fetch_stats] self.update_uptimes: %s" % repr(err)) + + try: + self._update_advertisement(s) + except Exception as err: + logging.error("[agent:_fetch_stats] self.update_advertisements: %s" % repr(err)) + + try: + self._update_peers() + except Exception as err: + logging.error("[agent:_fetch_stats] self.update_peers: %s" % repr(err)) + try: + self._update_counters() + except Exception as err: + logging.error("[agent:_fetch_stats] self.update_counters: %s" % repr(err)) + try: + self._update_handshakes(0) + except Exception as err: + logging.error("[agent:_fetch_stats] self.update_handshakes: %s" % repr(err)) + + time.sleep(5) + async def _on_event(self, msg): found_handshake = False jmsg = json.loads(msg) + # give plugins access to the events + try: + plugins.on('bcap_%s' % re.sub(r"[^a-z0-9_]+", "_", jmsg['tag'].lower()), self, jmsg) + except Exception as err: + logging.error("Processing event: %s" % err) + if jmsg['tag'] == 'wifi.client.handshake': filename = jmsg['data']['file'] sta_mac = jmsg['data']['station'] @@ -356,12 +385,13 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer): self.run('events.clear') while True: - logging.debug("polling events ...") + logging.debug("[agent:_event_poller] polling events ...") try: loop.create_task(self.start_websocket(self._on_event)) loop.run_forever() + logging.debug("[agent:_event_poller] loop loop loop") except Exception as ex: - logging.debug("Error while polling via websocket (%s)", ex) + logging.debug("[agent:_event_poller] Error while polling via websocket (%s)", ex) def start_event_polling(self): # start a thread and pass in the mainloop diff --git a/pwnagotchi/ai/__init__.py b/pwnagotchi/ai/__init__.py index 21df1ffe..fedcc80a 100644 --- a/pwnagotchi/ai/__init__.py +++ b/pwnagotchi/ai/__init__.py @@ -14,10 +14,11 @@ def load(config, agent, epoch, from_disk=True): try: begin = time.time() + logging.info("[ai] bootstrapping dependencies ...") start = time.time() - SB_BACKEND = "stable_baselines3" + SB_BACKEND = "stable_baselines3"; try: from stable_baselines3 import A2C @@ -51,7 +52,6 @@ def load(config, agent, epoch, from_disk=True): start = time.time() import pwnagotchi.ai.gym as wrappers - logging.debug("[ai] gym wrapper imported in %.2fs" % (time.time() - start)) env = wrappers.Environment(agent, epoch) @@ -79,5 +79,5 @@ def load(config, agent, epoch, from_disk=True): except Exception as e: logging.exception("error while starting AI (%s)", e) - logging.warning("[ai] AI not loaded!") - return False + logging.warning("[ai] AI not loaded!") + return False diff --git a/pwnagotchi/ai/epoch.py b/pwnagotchi/ai/epoch.py index 88d73e09..707b7488 100644 --- a/pwnagotchi/ai/epoch.py +++ b/pwnagotchi/ai/epoch.py @@ -177,7 +177,7 @@ class Epoch(object): self.bored_for = 0 now = time.time() - cpu = pwnagotchi.cpu_load() + cpu = pwnagotchi.cpu_load("epoch") mem = pwnagotchi.mem_usage() temp = pwnagotchi.temperature() diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py index 4ca26389..cba14dfe 100644 --- a/pwnagotchi/ai/train.py +++ b/pwnagotchi/ai/train.py @@ -77,9 +77,12 @@ class Stats(object): }) temp = "%s.tmp" % self.path + back = "%s.bak" % self.path with open(temp, 'wt') as fp: fp.write(data) + if os.path.isfile(self.path): + os.replace(self.path, back) os.replace(temp, self.path) @@ -174,7 +177,13 @@ class AsyncTrainer(object): logging.info("[ai] learning for %d epochs ..." % epochs_per_episode) try: self.set_training(True, epochs_per_episode) + # back up brain file before starting new training set + if os.path.isfile(self._nn_path): + back = "%s.bak" % self._nn_path + os.replace(self._nn_path, back) + self._view.set("mode", " ai") self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step) + self._view.set("mode", " AI") except Exception as e: logging.exception("[ai] error while training (%s)", e) finally: diff --git a/pwnagotchi/bettercap.py b/pwnagotchi/bettercap.py index 1f9217d0..592ac5c2 100644 --- a/pwnagotchi/bettercap.py +++ b/pwnagotchi/bettercap.py @@ -31,8 +31,10 @@ class Client(object): self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port) self.auth = HTTPBasicAuth(username, password) - def session(self): - r = requests.get("%s/session" % self.url, auth=self.auth) + # session takes optional argument to pull a sub-dictionary + # ex.: "session/wifi", "session/ble" + def session(self, sess="session"): + r = requests.get("%s/%s" % (self.url, sess), auth=self.auth) return decode(r) async def start_websocket(self, consumer): diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py index 21a5bc9f..994825bb 100644 --- a/pwnagotchi/plugins/__init__.py +++ b/pwnagotchi/plugins/__init__.py @@ -46,13 +46,13 @@ def toggle_plugin(name, enable=True): if name not in pwnagotchi.config['main']['plugins']: pwnagotchi.config['main']['plugins'][name] = dict() pwnagotchi.config['main']['plugins'][name]['enabled'] = enable - save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml') if not enable and name in loaded: if getattr(loaded[name], 'on_unload', None): loaded[name].on_unload(view.ROOT) del loaded[name] - + if pwnagotchi.config: + save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml') return True if enable and name in database and name not in loaded: @@ -64,6 +64,8 @@ def toggle_plugin(name, enable=True): one(name, 'config_changed', pwnagotchi.config) one(name, 'ui_setup', view.ROOT) one(name, 'ready', view.ROOT._agent) + if pwnagotchi.config: + save_config(pwnagotchi.config, '/etc/pwnagotchi/config.toml') return True return False diff --git a/pwnagotchi/plugins/default/memtemp.py b/pwnagotchi/plugins/default/memtemp.py index 898df2f2..d9ac75ce 100644 --- a/pwnagotchi/plugins/default/memtemp.py +++ b/pwnagotchi/plugins/default/memtemp.py @@ -41,6 +41,7 @@ class MemTemp(plugins.Plugin): ALLOWED_FIELDS = { 'mem': 'mem_usage', 'cpu': 'cpu_load', + 'cpus': 'cpu_load_since', 'temp': 'cpu_temp', 'freq': 'cpu_freq' } @@ -50,6 +51,7 @@ class MemTemp(plugins.Plugin): FIELD_WIDTH = 4 def on_loaded(self): + self._last_cpu_load = self._cpu_stat() logging.info("memtemp plugin loaded.") def mem_usage(self): @@ -58,6 +60,28 @@ class MemTemp(plugins.Plugin): def cpu_load(self): return f"{int(pwnagotchi.cpu_load() * 100)}%" + def _cpu_stat(self): + """ + Returns the splitted first line of the /proc/stat file + """ + with open('/proc/stat', 'rt') as fp: + return list(map(int,fp.readline().split()[1:])) + + def cpu_load_since(self): + """ + Returns the % load, since last time called + """ + parts0 = self._cpu_stat() + parts1 = self._last_cpu_load + self._last_cpu_load = parts0 + + parts_diff = [p1 - p0 for (p0, p1) in zip(parts0, parts1)] + user, nice, sys, idle, iowait, irq, softirq, steal, _guest, _guest_nice = parts_diff + idle_sum = idle + iowait + non_idle_sum = user + nice + sys + irq + softirq + steal + total = idle_sum + non_idle_sum + return f"{int(non_idle_sum / total * 100)}%" + def cpu_temp(self): if self.options['scale'] == "fahrenheit": temp = (pwnagotchi.temperature() * 9 / 5) + 32 diff --git a/pwnagotchi/plugins/default/webcfg.py b/pwnagotchi/plugins/default/webcfg.py index adb494a3..39df44b4 100644 --- a/pwnagotchi/plugins/default/webcfg.py +++ b/pwnagotchi/plugins/default/webcfg.py @@ -2,8 +2,9 @@ import logging import json import toml import _thread +import pwnagotchi from pwnagotchi import restart, plugins -from pwnagotchi.utils import save_config +from pwnagotchi.utils import save_config, merge_config from flask import abort from flask import render_template_string @@ -180,7 +181,10 @@ INDEX = """ - +
+ + +
{% endblock %} @@ -240,6 +244,24 @@ INDEX = """ }); } } + + function saveConfigNoRestart(){ + // get table + var table = document.getElementById("tableOptions"); + if (table) { + var json = tableToJson(table); + sendJSON("webcfg/merge-save-config", json, function(response) { + if (response) { + if (response.status == "200") { + alert("Config got updated"); + } else { + alert("Error while updating the config (err-code: " + response.status + ")"); + } + } + }); + } + } + var searchInput = document.getElementById("searchText"); searchInput.onkeyup = function() { var filter, table, tr, td, i, txtValue; @@ -471,15 +493,18 @@ class WebConfig(plugins.Plugin): def __init__(self): self.ready = False self.mode = 'MANU' + self._agent = None def on_config_changed(self, config): self.config = config self.ready = True def on_ready(self, agent): + self._agent = agent self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO' def on_internet_available(self, agent): + self._agent = agent self.mode = 'MANU' if agent.mode == 'manual' else 'AUTO' def on_loaded(self): @@ -513,4 +538,18 @@ class WebConfig(plugins.Plugin): except Exception as ex: logging.error(ex) return "config error", 500 + elif path == "merge-save-config": + try: + self.config = merge_config(request.get_json(), self.config) + pwnagotchi.config = merge_config(request.get_json(), pwnagotchi.config) + logging.debug("PWNAGOTCHI CONFIG:\n%s" % repr(pwnagotchi.config)) + if self._agent: + self._agent._config = merge_config(request.get_json(), self._agent._config) + logging.debug(" Agent CONFIG:\n%s" % repr(self._agent._config)) + logging.debug(" Updated CONFIG:\n%s" % request.get_json()) + save_config(request.get_json(), '/etc/pwnagotchi/config.toml') # test + return "success" + except Exception as ex: + logging.error("[webcfg mergesave] %s" % ex) + return "config error", 500 abort(404) diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index ab0f3a15..4dc5f8a1 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -43,6 +43,9 @@ class Display(View): def is_waveshare27inch(self): return self._implementation.name == 'waveshare27inch' + def is_waveshare27inchPartial(self): + return self._implementation.name == 'waveshare27inchPartial' + def is_waveshare29inch(self): return self._implementation.name == 'waveshare29inch' @@ -75,6 +78,9 @@ class Display(View): def is_spotpear24inch(self): return self._implementation.name == 'spotpear24inch' + + def is_displayhatmini(self): + return self._implementation.name == 'displayhatmini' def is_waveshare_any(self): return self.is_waveshare_v1() or self.is_waveshare_v2() @@ -101,7 +107,8 @@ class Display(View): while True: self._canvas_next_event.wait() - self._canvas_next_event.clear() + if self._implementation.name != 'waveshare27inchPartial': + self._canvas_next_event.clear() self._implementation.render(self._canvas_next) def _on_view_rendered(self, img): diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index eb9beec0..6dc47f62 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -8,6 +8,7 @@ from pwnagotchi.ui.hw.waveshare1 import WaveshareV1 from pwnagotchi.ui.hw.waveshare2 import WaveshareV2 from pwnagotchi.ui.hw.waveshare3 import WaveshareV3 from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch +from pwnagotchi.ui.hw.waveshare27inchPartial import Waveshare27inchPartial from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch @@ -15,6 +16,7 @@ from pwnagotchi.ui.hw.waveshare213d import Waveshare213d from pwnagotchi.ui.hw.waveshare213bc import Waveshare213bc from pwnagotchi.ui.hw.waveshare35lcd import Waveshare35lcd from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch +from pwnagotchi.ui.hw.displayhatmini import DisplayHatMini def display_for(config): # config has been normalized already in utils.load_config @@ -48,6 +50,9 @@ def display_for(config): elif config['ui']['display']['type'] == 'waveshare27inch': return Waveshare27inch(config) + elif config['ui']['display']['type'] == 'waveshare27inchPartial': + return Waveshare27inchPartial(config) + elif config['ui']['display']['type'] == 'waveshare29inch': return Waveshare29inch(config) @@ -68,3 +73,6 @@ def display_for(config): elif config['ui']['display']['type'] == 'spotpear24inch': return Spotpear24inch(config) + + elif config['ui']['display']['type'] == 'displayhatmini': + return DisplayHatMini(config) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py new file mode 100644 index 00000000..55cb86ef --- /dev/null +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -0,0 +1,44 @@ +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class DisplayHatMini(DisplayImpl): + def __init__(self, config): + super(DisplayHatMini, self).__init__(config, 'displayhatmini') + self._display = None + + def layout(self): + fonts.setup(12, 10, 12, 70, 25, 9) + self._layout['width'] = 320 + self._layout['height'] = 240 + self._layout['face'] = (35, 50) + self._layout['name'] = (5, 20) + self._layout['channel'] = (0, 0) + self._layout['aps'] = (40, 0) + self._layout['uptime'] = (240, 0) + self._layout['line1'] = [0, 14, 320, 14] + self._layout['line2'] = [0, 220, 320, 220] + self._layout['friend_face'] = (0, 130) + self._layout['friend_name'] = (40, 135) + self._layout['shakes'] = (0, 220) + self._layout['mode'] = (280, 220) + self._layout['status'] = { + 'pos': (80, 160), + 'font': fonts.status_font(fonts.Medium), + 'max': 20 + } + + return self._layout + + def initialize(self): + logging.info("initializing Display Hat Mini") + from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 + self._display = ST7789(0,1,9,13) + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() diff --git a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py new file mode 100644 index 00000000..37a53e12 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py @@ -0,0 +1,360 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation 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 +# furnished 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 FOR 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 numbers +import time +import numpy as np + +import spidev +import RPi.GPIO as GPIO + + +__version__ = '0.0.4' + +BG_SPI_CS_BACK = 0 +BG_SPI_CS_FRONT = 1 + +SPI_CLOCK_HZ = 16000000 + +ST7789_NOP = 0x00 +ST7789_SWRESET = 0x01 +ST7789_RDDID = 0x04 +ST7789_RDDST = 0x09 + +ST7789_SLPIN = 0x10 +ST7789_SLPOUT = 0x11 +ST7789_PTLON = 0x12 +ST7789_NORON = 0x13 + +ST7789_INVOFF = 0x20 +ST7789_INVON = 0x21 +ST7789_DISPOFF = 0x28 +ST7789_DISPON = 0x29 + +ST7789_CASET = 0x2A +ST7789_RASET = 0x2B +ST7789_RAMWR = 0x2C +ST7789_RAMRD = 0x2E + +ST7789_PTLAR = 0x30 +ST7789_MADCTL = 0x36 +ST7789_COLMOD = 0x3A + +ST7789_FRMCTR1 = 0xB1 +ST7789_FRMCTR2 = 0xB2 +ST7789_FRMCTR3 = 0xB3 +ST7789_INVCTR = 0xB4 +ST7789_DISSET5 = 0xB6 + +ST7789_GCTRL = 0xB7 +ST7789_GTADJ = 0xB8 +ST7789_VCOMS = 0xBB + +ST7789_LCMCTRL = 0xC0 +ST7789_IDSET = 0xC1 +ST7789_VDVVRHEN = 0xC2 +ST7789_VRHS = 0xC3 +ST7789_VDVS = 0xC4 +ST7789_VMCTR1 = 0xC5 +ST7789_FRCTRL2 = 0xC6 +ST7789_CABCCTRL = 0xC7 + +ST7789_RDID1 = 0xDA +ST7789_RDID2 = 0xDB +ST7789_RDID3 = 0xDC +ST7789_RDID4 = 0xDD + +ST7789_GMCTRP1 = 0xE0 +ST7789_GMCTRN1 = 0xE1 + +ST7789_PWCTR6 = 0xFC + + +class ST7789(object): + """Representation of an ST7789 TFT LCD.""" + + def __init__(self, port, cs, dc, backlight, rst=None, width=320, + height=240, rotation=0, invert=True, spi_speed_hz=60 * 1000 * 1000, + offset_left=0, + offset_top=0): + """Create an instance of the display using SPI communication. + + Must provide the GPIO pin number for the D/C pin and the SPI driver. + + Can optionally provide the GPIO pin number for the reset pin as the rst parameter. + + :param port: SPI port number + :param cs: SPI chip-select number (0 or 1 for BCM + :param backlight: Pin for controlling backlight + :param rst: Reset pin for ST7789 + :param width: Width of display connected to ST7789 + :param height: Height of display connected to ST7789 + :param rotation: Rotation of display connected to ST7789 + :param invert: Invert display + :param spi_speed_hz: SPI speed (in Hz) + + """ + if rotation not in [0, 90, 180, 270]: + raise ValueError("Invalid rotation {}".format(rotation)) + + if width != height and rotation in [90, 270]: + raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height)) + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + self._spi = spidev.SpiDev(port, cs) + self._spi.mode = 0 + self._spi.lsbfirst = False + self._spi.max_speed_hz = spi_speed_hz + + self._dc = dc + self._rst = rst + self._width = width + self._height = height + self._rotation = rotation + self._invert = invert + + self._offset_left = offset_left + self._offset_top = offset_top + + # Set DC as output. + GPIO.setup(dc, GPIO.OUT) + + # Setup backlight as output (if provided). + self._backlight = backlight + if backlight is not None: + GPIO.setup(backlight, GPIO.OUT) + GPIO.output(backlight, GPIO.LOW) + time.sleep(0.1) + GPIO.output(backlight, GPIO.HIGH) + + # Setup reset as output (if provided). + if rst is not None: + GPIO.setup(self._rst, GPIO.OUT) + self.reset() + self._init() + + def send(self, data, is_data=True, chunk_size=4096): + """Write a byte or array of bytes to the display. Is_data parameter + controls if byte should be interpreted as display data (True) or command + data (False). Chunk_size is an optional size of bytes to write in a + single SPI transaction, with a default of 4096. + """ + # Set DC low for command, high for data. + GPIO.output(self._dc, is_data) + # Convert scalar argument to list so either can be passed as parameter. + if isinstance(data, numbers.Number): + data = [data & 0xFF] + # Write data a chunk at a time. + for start in range(0, len(data), chunk_size): + end = min(start + chunk_size, len(data)) + self._spi.xfer(data[start:end]) + + def set_backlight(self, value): + """Set the backlight on/off.""" + if self._backlight is not None: + GPIO.output(self._backlight, value) + + @property + def width(self): + return self._width if self._rotation == 0 or self._rotation == 180 else self._height + + @property + def height(self): + return self._height if self._rotation == 0 or self._rotation == 180 else self._width + + def command(self, data): + """Write a byte or array of bytes to the display as command data.""" + self.send(data, False) + + def data(self, data): + """Write a byte or array of bytes to the display as display data.""" + self.send(data, True) + + def reset(self): + """Reset the display, if reset pin is connected.""" + if self._rst is not None: + GPIO.output(self._rst, 1) + time.sleep(0.500) + GPIO.output(self._rst, 0) + time.sleep(0.500) + GPIO.output(self._rst, 1) + time.sleep(0.500) + + def _init(self): + # Initialize the display. + + self.command(ST7789_SWRESET) # Software reset + time.sleep(0.150) # delay 150 ms + + self.command(ST7789_MADCTL) + self.data(0x70) + + self.command(ST7789_FRMCTR2) # Frame rate ctrl - idle mode + self.data(0x0C) + self.data(0x0C) + self.data(0x00) + self.data(0x33) + self.data(0x33) + + self.command(ST7789_COLMOD) + self.data(0x05) + + self.command(ST7789_GCTRL) + self.data(0x14) + + self.command(ST7789_VCOMS) + self.data(0x37) + + self.command(ST7789_LCMCTRL) # Power control + self.data(0x2C) + + self.command(ST7789_VDVVRHEN) # Power control + self.data(0x01) + + self.command(ST7789_VRHS) # Power control + self.data(0x12) + + self.command(ST7789_VDVS) # Power control + self.data(0x20) + + self.command(0xD0) + self.data(0xA4) + self.data(0xA1) + + self.command(ST7789_FRCTRL2) + self.data(0x0F) + + self.command(ST7789_GMCTRP1) # Set Gamma + 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(ST7789_GMCTRN1) # Set Gamma + 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) + + if self._invert: + self.command(ST7789_INVON) # Invert display + else: + self.command(ST7789_INVOFF) # Don't invert display + + self.command(ST7789_SLPOUT) + + self.command(ST7789_DISPON) # Display on + time.sleep(0.100) # 100 ms + + def begin(self): + """Set up the display + + Deprecated. Included in __init__. + + """ + pass + + def set_window(self, x0=0, y0=0, x1=None, y1=None): + """Set the pixel address window for proceeding drawing commands. x0 and + x1 should define the minimum and maximum x pixel bounds. y0 and y1 + should define the minimum and maximum y pixel bound. If no parameters + are specified the default will be to update the entire display from 0,0 + to width-1,height-1. + """ + if x1 is None: + x1 = self._width - 1 + + if y1 is None: + y1 = self._height - 1 + + y0 += self._offset_top + y1 += self._offset_top + + x0 += self._offset_left + x1 += self._offset_left + + self.command(ST7789_CASET) # Column addr set + self.data(x0 >> 8) + self.data(x0 & 0xFF) # XSTART + self.data(x1 >> 8) + self.data(x1 & 0xFF) # XEND + self.command(ST7789_RASET) # Row addr set + self.data(y0 >> 8) + self.data(y0 & 0xFF) # YSTART + self.data(y1 >> 8) + self.data(y1 & 0xFF) # YEND + self.command(ST7789_RAMWR) # write to RAM + + def display(self, image): + """Write the provided image to the hardware. + + :param image: Should be RGB format and the same dimensions as the display hardware. + + """ + # Set address bounds to entire display. + self.set_window() + + # Convert image to 16bit RGB565 format and + # flatten into bytes. + pixelbytes = self.image_to_data(image, self._rotation) + + # Write data to hardware. + for i in range(0, len(pixelbytes), 4096): + self.data(pixelbytes[i:i + 4096]) + + def image_to_data(self, image, rotation=0): + if not isinstance(image, np.ndarray): + image = np.array(image.convert('RGB')) + + # Rotate the image + pb = np.rot90(image, rotation // 90).astype('uint16') + + # Mask and shift the 888 RGB into 565 RGB + red = (pb[..., [0]] & 0xf8) << 8 + green = (pb[..., [1]] & 0xfc) << 3 + blue = (pb[..., [2]] & 0xf8) >> 3 + + # Stick 'em together + result = red | green | blue + + # Output the raw bytes + return result.byteswap().tobytes() diff --git a/pwnagotchi/ui/hw/waveshare27inchPartial.py b/pwnagotchi/ui/hw/waveshare27inchPartial.py new file mode 100644 index 00000000..e5e3f95f --- /dev/null +++ b/pwnagotchi/ui/hw/waveshare27inchPartial.py @@ -0,0 +1,77 @@ +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl +from PIL import Image + + +class Waveshare27inchPartial(DisplayImpl): + def __init__(self, config): + super(Waveshare27inchPartial, self).__init__(config, 'waveshare27inchPartial') + self._display = None + self.counter = 0 + + def layout(self): + fonts.setup(10, 9, 10, 35, 25, 9) + 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.status_font(fonts.Medium), + 'max': 40 + } + return self._layout + + + + def initialize(self): + logging.info("initializing waveshare v1 2.7 inch display") + from rpi_epd2in7.epd import EPD + self._display = EPD(fast_refresh=True) + self._display.init() + + + def render(self, canvas): + # have to rotate, because lib work with portrait mode only + # also I have 180 degrees screen rotation inn config, not tested with other valuesjk:w + rotated = canvas.rotate(90, expand=True) + if self.counter == 0: + self._display.smart_update(rotated) + + # print invert + # print true image + elif self.counter % 35 == 0: + inverted_image = rotated.point(lambda x: 255-x) + self._display.display_partial_frame(inverted_image, 0, 0, 264, 176, fast=True) + self._display.display_partial_frame(rotated, 0, 0, 264, 176, fast=True) + + # partial update full screen + elif self.counter % 7 == 0: + # face + text under + #self._display.display_partial_frame(rotated, 35, 35, 190, 115, fast=True) + # full screen partial update + self._display.display_partial_frame(rotated, 0, 0, 264, 176, fast=True) + + # partial update face + self._display.display_partial_frame(rotated, 110, 84, 92, 40, fast=True) + + if self.counter >= 100: + self.counter = 0 + else: + self.counter += 1 + + + def clear(self): + pass + diff --git a/pwnagotchi/ui/hw/waveshare35lcd.py b/pwnagotchi/ui/hw/waveshare35lcd.py index 97011b3a..1c5979b6 100644 --- a/pwnagotchi/ui/hw/waveshare35lcd.py +++ b/pwnagotchi/ui/hw/waveshare35lcd.py @@ -40,7 +40,7 @@ class Waveshare35lcd(DisplayImpl): from pwnagotchi.ui.hw.libs.fb import fb self._display = fb logging.info("initializing waveshare 3,5inch lcd display") - self._display.ready_fb(i=1) + self._display.ready_fb(i=0) self._display.black_scr() def render(self, canvas): diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index 9ecc857f..70174541 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -16,8 +16,10 @@ from pwnagotchi.ui.components import * from pwnagotchi.ui.state import State from pwnagotchi.voice import Voice -WHITE = 0xff -BLACK = 0x00 +import RPi.GPIO as GPIO + +WHITE = 0x181010 +BLACK = 0xffcccc ROOT = None @@ -122,7 +124,7 @@ class View(object): while True: try: name = self._state.get('name') - self.set('name', name.rstrip('█').strip() if '█' in name else (name + ' █')) + self.set('name', name.rstrip('-').strip() if '-' in name else (name + ' -')) self.update() except Exception as e: logging.warning("non fatal error while updating view: %s" % e) @@ -245,9 +247,9 @@ class View(object): def wait(self, secs, sleeping=True): was_normal = self.is_normal() - part = secs / 10.0 + part = secs/3.0 - for step in range(0, 10): + for step in range(0, 3): # if we weren't in a normal state before going # to sleep, keep that face and status on for # a while, otherwise the sleep animation will @@ -257,11 +259,13 @@ class View(object): if secs > 1: self.set('face', faces.SLEEP) self.set('status', self._voice.on_napping(int(secs))) + plugins.on('sleep', self, secs) else: self.set('face', faces.SLEEP2) self.set('status', self._voice.on_awakening()) else: self.set('status', self._voice.on_waiting(int(secs))) + plugins.on('wait', self, secs) good_mood = self._agent.in_good_mood() if step % 2 == 0: self.set('face', faces.LOOK_R_HAPPY if good_mood else faces.LOOK_R) @@ -371,7 +375,9 @@ class View(object): state = self._state changes = state.changes(ignore=self._ignore_changes) if force or len(changes): - self._canvas = Image.new('1', (self._width, self._height), WHITE) + colormode = '1' if not 'colormode' in self._config['ui'] else self._config['ui']['colormode'] + backgroundcolor = WHITE if not 'backgroundcolor' in self._config['ui'] else self._config['ui']['backgroundcolor'] + self._canvas = Image.new(colormode, (self._width, self._height), backgroundcolor) drawer = ImageDraw.Draw(self._canvas) plugins.on('ui_update', self) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 29c36327..ecddeec4 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -254,6 +254,9 @@ def load_config(args): elif config['ui']['display']['type'] in ('ws_27inch', 'ws27inch', 'waveshare_27inch', 'waveshare27inch'): config['ui']['display']['type'] = 'waveshare27inch' + elif config['ui']['display']['type'] in ('ws_27inchPartial', 'ws27inchPartial', 'waveshare_27inchPartial', 'waveshare27inchPartial'): + config['ui']['display']['type'] = 'waveshare27inchPartial' + elif config['ui']['display']['type'] in ('ws_29inch', 'ws29inch', 'waveshare_29inch', 'waveshare29inch'): config['ui']['display']['type'] = 'waveshare29inch' @@ -283,7 +286,10 @@ def load_config(args): elif config['ui']['display']['type'] in ('spotpear24inch'): config['ui']['display']['type'] = 'spotpear24inch' - + + elif config['ui']['display']['type'] in ('displayhatmini'): + config['ui']['display']['type'] = 'displayhatmini' + else: print("unsupported display type %s" % config['ui']['display']['type']) sys.exit(1) @@ -304,11 +310,13 @@ def total_unique_handshakes(path): def iface_channels(ifname): channels = [] - output = subprocess.getoutput("/sbin/iwlist %s freq" % ifname) + + phy = subprocess.getoutput("/sbin/iw %s info | grep wiphy | cut -d ' ' -f 2" % ifname) + output = subprocess.getoutput("/sbin/iw phy%s channels | grep ' MHz' | sed 's/^.*\[//g' | sed s/\].*\$//g" % phy) for line in output.split("\n"): line = line.strip() - if line.startswith("Channel "): - channels.append(int(line.split()[1])) + channels.append(int(line)) + return channels