From 02a6063f12011b58203d578c6de03e9744833c98 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 10:49:14 +0800 Subject: [PATCH 01/39] Add Display Hat Support Add Pimoroni Display Hat support --- pwnagotchi/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 29c36327..0a9a5cf0 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -283,7 +283,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) From 22b00311bf9c651f7e134b7bda530088505a08d4 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 10:51:34 +0800 Subject: [PATCH 02/39] Add Pimoroni Display Hat Support Add Pimoroni Display Hat Support --- pwnagotchi/ui/display.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index ab0f3a15..5eaad12f 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -75,6 +75,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() From 22c8b4a7075d11f6289045fbc492a6268df8eee2 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 11:07:57 +0800 Subject: [PATCH 03/39] add display hat mini support add pimoroni display hat mini support --- pwnagotchi/ui/hw/displayhatmini.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 pwnagotchi/ui/hw/displayhatmini.py diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py new file mode 100644 index 00000000..1f0b9ff5 --- /dev/null +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -0,0 +1,46 @@ +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class HatMini(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 hatmini display") + from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 + self._display = ST7789(0,1,9) + # self.reset() + # self._display.clear() + + def render(self, canvas): + self._display.display(canvas) + + def clear(self): + self._display.clear() From caa05d00d98f14abf39443c4ebfb80daab887529 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 11:22:10 +0800 Subject: [PATCH 04/39] Create ST7789.py Add Pimoroni Display Hat Mini Support --- .../hw/libs/pimoroni/displayhatmini/ST7789.py | 360 ++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py 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..1c0ae4f3 --- /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=None, rst=None, width=240, + height=240, rotation=90, invert=True, spi_speed_hz=4000000, + 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() From fd559d3db5dfb86433a86695994e4183352c1f3d Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 11:29:06 +0800 Subject: [PATCH 05/39] Update __init__.py Add Pimoroni Hat Support --- pwnagotchi/ui/hw/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index eb9beec0..5f856f67 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -68,3 +68,6 @@ def display_for(config): elif config['ui']['display']['type'] == 'spotpear24inch': return Spotpear24inch(config) + + elif config['ui']['display']['type'] == 'displayhatmini': + return DisplayHatMini(config) From ca8a50a496f60b89c93885225c6667d16b1e89ac Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 11:30:34 +0800 Subject: [PATCH 06/39] Update displayhatmini.py fix class error --- pwnagotchi/ui/hw/displayhatmini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index 1f0b9ff5..7b451604 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -4,7 +4,7 @@ import pwnagotchi.ui.fonts as fonts from pwnagotchi.ui.hw.base import DisplayImpl -class HatMini(DisplayImpl): +class DisplayHatMini(DisplayImpl): def __init__(self, config): super(DisplayHatMini, self).__init__(config, 'displayhatmini') self._display = None From 8a78311b5f00f735c4e06ab7a3f902e678390b2f Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 11:48:43 +0800 Subject: [PATCH 07/39] Update ST7789.py change screen width, height and rotation --- pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py index 1c0ae4f3..9f4ab989 100644 --- a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py +++ b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py @@ -90,8 +90,8 @@ ST7789_PWCTR6 = 0xFC class ST7789(object): """Representation of an ST7789 TFT LCD.""" - def __init__(self, port, cs, dc, backlight=None, rst=None, width=240, - height=240, rotation=90, invert=True, spi_speed_hz=4000000, + def __init__(self, port, cs, dc, backlight=None, rst=None, width=320, + height=240, rotation=0, invert=True, spi_speed_hz=4000000, offset_left=0, offset_top=0): """Create an instance of the display using SPI communication. From 8652d016344ed91a13e4571be5a52bc4da3491ab Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 13:39:20 +0800 Subject: [PATCH 08/39] Update ST7789.py fix: added backlight pin --- pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py index 9f4ab989..650d30fb 100644 --- a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py +++ b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py @@ -90,7 +90,7 @@ ST7789_PWCTR6 = 0xFC class ST7789(object): """Representation of an ST7789 TFT LCD.""" - def __init__(self, port, cs, dc, backlight=None, rst=None, width=320, + def __init__(self, port, cs, dc, backlight, rst=None, width=320, height=240, rotation=0, invert=True, spi_speed_hz=4000000, offset_left=0, offset_top=0): From c6ee189aa0b3ea819f282b2ee1895168d434ffe6 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 13:39:58 +0800 Subject: [PATCH 09/39] Update displayhatmini.py fix: added backlight pin --- pwnagotchi/ui/hw/displayhatmini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index 7b451604..271e635b 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -35,7 +35,7 @@ class DisplayHatMini(DisplayImpl): def initialize(self): logging.info("initializing hatmini display") from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 - self._display = ST7789(0,1,9) + self._display = ST7789(0,1,9,13) # self.reset() # self._display.clear() From 7e8dd4cb97f7825208a198c37b42cd0f62ba445b Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 14:01:00 +0800 Subject: [PATCH 10/39] Update displayhatmini.py cleanup --- pwnagotchi/ui/hw/displayhatmini.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index 271e635b..da869a2a 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -36,8 +36,6 @@ class DisplayHatMini(DisplayImpl): logging.info("initializing hatmini display") from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 self._display = ST7789(0,1,9,13) - # self.reset() - # self._display.clear() def render(self, canvas): self._display.display(canvas) From c3ead8462d20141c8c4360e5c63b2c00f1ffd242 Mon Sep 17 00:00:00 2001 From: makerph Date: Thu, 11 Aug 2022 21:05:44 +0800 Subject: [PATCH 11/39] Update __init__.py fix missing lines --- pwnagotchi/ui/hw/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index 5f856f67..f5ffe6f8 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -15,6 +15,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 From 0e7b756a6e53dfad1af715c9e052f9613a07cb5c Mon Sep 17 00:00:00 2001 From: makerph Date: Fri, 12 Aug 2022 01:41:14 +0800 Subject: [PATCH 12/39] Update ST7789.py change spi speed --- pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py index 650d30fb..37a53e12 100644 --- a/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py +++ b/pwnagotchi/ui/hw/libs/pimoroni/displayhatmini/ST7789.py @@ -91,7 +91,7 @@ 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=4000000, + 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. From c7b296f1b07f2e2e87d18728dab581fa00b3e1d5 Mon Sep 17 00:00:00 2001 From: makerph Date: Tue, 22 Nov 2022 14:53:46 +0800 Subject: [PATCH 13/39] Update displayhatmini.py update logging info message --- pwnagotchi/ui/hw/displayhatmini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/displayhatmini.py b/pwnagotchi/ui/hw/displayhatmini.py index da869a2a..55cb86ef 100644 --- a/pwnagotchi/ui/hw/displayhatmini.py +++ b/pwnagotchi/ui/hw/displayhatmini.py @@ -33,7 +33,7 @@ class DisplayHatMini(DisplayImpl): return self._layout def initialize(self): - logging.info("initializing hatmini display") + logging.info("initializing Display Hat Mini") from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 self._display = ST7789(0,1,9,13) From 70bffe52001cc951814219a3f791008428ac707e Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Tue, 4 Apr 2023 14:11:23 -0700 Subject: [PATCH 14/39] changes to support plugins requesting parts of session, and get notice of events --- pwnagotchi/agent.py | 6 ++++++ pwnagotchi/bettercap.py | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 7fe6a76d..429b0ab1 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -324,6 +324,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer): 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'] 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): From 81f371f809311fdd57ca7bcbaf1895215c52f87f Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Wed, 19 Apr 2023 18:26:53 -0700 Subject: [PATCH 15/39] added 'save without restart' button to change variables on the fly. proabably doesn't work with everything. --- pwnagotchi/plugins/default/webcfg.py | 43 ++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) 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) From 62aaf6e3b136fefee1595ccc4f21e5fc83d7a57a Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Wed, 19 Apr 2023 20:58:37 -0700 Subject: [PATCH 16/39] copied cpu_stat from pwnagotchi/__index__.py, and calculate load since previous call, instead of sampling 0.1s while updating On Pi0 instead of always seeing 100%, I see 40-80% typical, which seems more in line with what is happening on the system --- pwnagotchi/plugins/default/memtemp.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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 From c90855840ccd2c372ed2c12caf5f2eec86d8f35e Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Thu, 27 Apr 2023 15:00:58 -0700 Subject: [PATCH 17/39] keep old version as backup when saving files --- pwnagotchi/ai/train.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py index 4ca26389..c9846ca6 100644 --- a/pwnagotchi/ai/train.py +++ b/pwnagotchi/ai/train.py @@ -77,9 +77,11 @@ class Stats(object): }) temp = "%s.tmp" % self.path + back = "%s.bak" % self.path with open(temp, 'wt') as fp: fp.write(data) + os.replace(self.path, back) os.replace(temp, self.path) @@ -113,7 +115,9 @@ class AsyncTrainer(object): def _save_ai(self): logging.info("[ai] saving model to %s ..." % self._nn_path) temp = "%s.tmp" % self._nn_path + back = "%s.bak" % self._nn_path self._model.save(temp) + os.replace(self._nn_path, back) os.replace(temp, self._nn_path) def on_ai_step(self): From 3f9e78af73ccc65879a182f773b90fded57e4ed1 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Fri, 19 May 2023 10:58:20 -0700 Subject: [PATCH 18/39] Change cpu_load to take a tag, and compute over interval since last call with same tag, instead of a 0.1s sampling --- pwnagotchi/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py index 1714d7bc..74e620a5 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() + if tag and tag in _cpu_stats.keys(): + parts0 = _cpu_stats[tag] + else: + parts0 = _cpu_stat() time.sleep(0.1) 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 From 4b89a3c8dd938234b36d4ecd367545174394e0ea Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Fri, 19 May 2023 10:59:41 -0700 Subject: [PATCH 19/39] use a tag on cpu_load for more accurate load over the last epoch --- pwnagotchi/ai/epoch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From d2c52e26731e25b09e2c5bedc3df602a3a23be5a Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Fri, 19 May 2023 11:01:08 -0700 Subject: [PATCH 20/39] save backups of brain files when saving --- pwnagotchi/ai/train.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py index c9846ca6..2d72e6e6 100644 --- a/pwnagotchi/ai/train.py +++ b/pwnagotchi/ai/train.py @@ -81,7 +81,8 @@ class Stats(object): with open(temp, 'wt') as fp: fp.write(data) - os.replace(self.path, back) + if os.path.isfile(self.path): + os.replace(self.path, back) os.replace(temp, self.path) @@ -117,7 +118,8 @@ class AsyncTrainer(object): temp = "%s.tmp" % self._nn_path back = "%s.bak" % self._nn_path self._model.save(temp) - os.replace(self._nn_path, back) + if os.path.isfile(self._nn_path): + os.replace(self._nn_path, back) os.replace(temp, self._nn_path) def on_ai_step(self): From fc2341b04c7c7f4f0b1e5d30359b99f268b6edc0 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sun, 21 May 2023 17:16:58 -0700 Subject: [PATCH 21/39] when toggling a plugin, changed to save config after running load/unload handlers, in case those change the config --- pwnagotchi/plugins/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py index 53ee9342..05cff26a 100644 --- a/pwnagotchi/plugins/__init__.py +++ b/pwnagotchi/plugins/__init__.py @@ -47,13 +47,13 @@ def toggle_plugin(name, enable=True): if not name 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: @@ -65,6 +65,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 From 28d8e8a2c8c5e957365beeb812360a260d7fdbb8 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Tue, 13 Jun 2023 13:48:14 -0700 Subject: [PATCH 22/39] bring wlan0 down so mon0 can come up after creating mon0 --- builder/data/usr/bin/pwnlib | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builder/data/usr/bin/pwnlib b/builder/data/usr/bin/pwnlib index 0e45d259..eb73a479 100755 --- a/builder/data/usr/bin/pwnlib +++ b/builder/data/usr/bin/pwnlib @@ -33,7 +33,9 @@ reload_brcm() { # starts mon0 start_monitor_interface() { - iw phy "$(iw phy | head -1 | cut -d" " -f2)" interface add mon0 type monitor && ifconfig mon0 up + iw phy "$(iw phy | head -1 | cut -d" " -f2)" interface add mon0 type monitor + ifconfig wlan0 down + ifconfig mon0 up } # stops mon0 From 0d6d9503d8ad87fc7b29b1f12400702b80c2c0df Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Tue, 13 Jun 2023 23:54:24 -0700 Subject: [PATCH 23/39] add sleeps to let kernel load and set up devices add powersave off as nexmon suggests pick auto mode more instead of manu --- builder/data/usr/bin/pwnlib | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/builder/data/usr/bin/pwnlib b/builder/data/usr/bin/pwnlib index eb73a479..14c412fc 100755 --- a/builder/data/usr/bin/pwnlib +++ b/builder/data/usr/bin/pwnlib @@ -25,15 +25,20 @@ 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() { + 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 } @@ -69,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 From d2227b939dc9267c11a73eb8fefe2d1f739eaf91 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sun, 18 Jun 2023 18:28:57 -0700 Subject: [PATCH 24/39] changed cutoff for "extended channels" from 140 to 150 to alleviate "shape into shape" ai error - more of a bandaid than a fix --- pwnagotchi/ai/gym.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/ai/gym.py b/pwnagotchi/ai/gym.py index de5c94ce..a79ebc9f 100644 --- a/pwnagotchi/ai/gym.py +++ b/pwnagotchi/ai/gym.py @@ -36,7 +36,7 @@ class Environment(gym.Env): # see https://github.com/evilsocket/pwnagotchi/issues/583 self._supported_channels = agent.supported_channels() - self._extended_spectrum = any(ch > 140 for ch in self._supported_channels) + self._extended_spectrum = any(ch > 150 for ch in self._supported_channels) self._histogram_size, self._observation_shape = featurizer.describe(self._extended_spectrum) Environment.params += [ From c52220d98e0ec096968f824dcba6978d82919e22 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sun, 18 Jun 2023 18:56:06 -0700 Subject: [PATCH 25/39] added support for stable_baselines3 AI backend. stable_baselines3 uses pytorch instead of tensorflow --- pwnagotchi/ai/__init__.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pwnagotchi/ai/__init__.py b/pwnagotchi/ai/__init__.py index 54933423..fedcc80a 100644 --- a/pwnagotchi/ai/__init__.py +++ b/pwnagotchi/ai/__init__.py @@ -18,16 +18,37 @@ def load(config, agent, epoch, from_disk=True): logging.info("[ai] bootstrapping dependencies ...") start = time.time() - from stable_baselines import A2C - logging.debug("[ai] A2C imported in %.2fs" % (time.time() - start)) + SB_BACKEND = "stable_baselines3"; - start = time.time() - from stable_baselines.common.policies import MlpLstmPolicy - logging.debug("[ai] MlpLstmPolicy imported in %.2fs" % (time.time() - start)) + try: + from stable_baselines3 import A2C + logging.debug("[ai] A2C imported in %.2fs" % (time.time() - start)) + + start = time.time() + from stable_baselines3.a2c import MlpPolicy + logging.debug("[ai] MlpPolicy imported in %.2fs" % (time.time() - start)) + SB_A2C_POLICY = MlpPolicy + + start = time.time() + from stable_baselines3.common.vec_env import DummyVecEnv + logging.debug("[ai] DummyVecEnv imported in %.2fs" % (time.time() - start)) + + except Exception as e: + logging.debug("[ai] stable_baselines3 not accessible. Trying stable_baselines") + + from stable_baselines import A2C + logging.debug("[ai] A2C imported in %.2fs" % (time.time() - start)) + SB_BACKEND = "stable_baselines" + + start = time.time() + from stable_baselines.common.policies import MlpLstmPolicy + logging.debug("[ai] MlpLstmPolicy imported in %.2fs" % (time.time() - start)) + SB_A2C_POLICY = MlpLstmPolicy + + start = time.time() + from stable_baselines.common.vec_env import DummyVecEnv + logging.debug("[ai] DummyVecEnv imported in %.2fs" % (time.time() - start)) - start = time.time() - from stable_baselines.common.vec_env import DummyVecEnv - logging.debug("[ai] DummyVecEnv imported in %.2fs" % (time.time() - start)) start = time.time() import pwnagotchi.ai.gym as wrappers @@ -39,7 +60,7 @@ def load(config, agent, epoch, from_disk=True): logging.info("[ai] creating model ...") start = time.time() - a2c = A2C(MlpLstmPolicy, env, **config['params']) + a2c = A2C(SB_A2C_POLICY, env, **config['params']) logging.debug("[ai] A2C created in %.2fs" % (time.time() - start)) if from_disk and os.path.exists(config['path']): From 0abdf54f4300e30cc13477ba9a1285fcb143aee6 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sun, 18 Jun 2023 20:04:26 -0700 Subject: [PATCH 26/39] changed channel detection to detect all channels. iwlist only returns the first 32 --- pwnagotchi/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 29c36327..cbba5f63 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -304,11 +304,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 From 645846e44d3939f4aa7bcf46fde64d8cac2ef015 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sun, 18 Jun 2023 20:06:00 -0700 Subject: [PATCH 27/39] only need the short sleep in cpu_load if not specifying a tag --- pwnagotchi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py index 74e620a5..96cdb5c1 100644 --- a/pwnagotchi/__init__.py +++ b/pwnagotchi/__init__.py @@ -91,7 +91,7 @@ def cpu_load(tag=None): parts0 = _cpu_stats[tag] else: parts0 = _cpu_stat() - time.sleep(0.1) + time.sleep(0.1) parts1 = _cpu_stat() if tag: _cpu_stats[tag] = parts1 From 74c757244ada25ea4d3131faf6ac4bb56ae3313c Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Tue, 20 Jun 2023 12:45:13 -0700 Subject: [PATCH 28/39] added "try..except" wrappers around calls in fetcher to keep the thread alive, even when fetches fail temporarily --- pwnagotchi/agent.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 429b0ab1..1f882218 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -312,13 +312,36 @@ 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 @@ -362,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 From abb041b228517303f86d068d46d450f7c3f9f38a Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:16:40 -0700 Subject: [PATCH 29/39] copied from https://github.com/daniilprohorov/pwnagotchi.git to add waveshare27Partial support to pwnagotchi. I don't know how to do it the right way by applying their commits to my tree. --- pwnagotchi/ui/hw/waveshare27inchPartial.py | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 pwnagotchi/ui/hw/waveshare27inchPartial.py 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 + From c66654b592b5dbdefa01ebaaf921f574e61151de Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:18:58 -0700 Subject: [PATCH 30/39] in cpu_stat, only sleep when no tag --- pwnagotchi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py index 74e620a5..447e9833 100644 --- a/pwnagotchi/__init__.py +++ b/pwnagotchi/__init__.py @@ -91,7 +91,7 @@ def cpu_load(tag=None): parts0 = _cpu_stats[tag] else: parts0 = _cpu_stat() - time.sleep(0.1) + time.sleep(0.1) # only need to sleep when no tag parts1 = _cpu_stat() if tag: _cpu_stats[tag] = parts1 @@ -167,3 +167,4 @@ def reboot(mode=None): os.system("sync") os.system("shutdown -r now") + os.system("service pwnagotchi restart") From f208d4775b31838744aabb2badf95b832d76a8cd Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:21:08 -0700 Subject: [PATCH 31/39] wrap "fetch_stats" updates in try: except: to keep the thread alive in case of temporary failure --- pwnagotchi/agent.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 429b0ab1..1f882218 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -312,13 +312,36 @@ 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 @@ -362,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 From 397fe5b7a73b6a6e1d2488c8629fdab4a26bfb4d Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:23:01 -0700 Subject: [PATCH 32/39] logic to not clear display every loop on waveshare27inchPartil --- pwnagotchi/ui/display.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index ab0f3a15..f77e002b 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' @@ -101,7 +104,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): From f092205bb010b88a5deb5268d245f3bad3b3a6d7 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:23:58 -0700 Subject: [PATCH 33/39] waveshare27inchPartial from https://github.com/daniilprohorov/pwnagotchi.git --- pwnagotchi/ui/hw/__init__.py | 4 ++++ pwnagotchi/ui/hw/waveshare35lcd.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index eb9beec0..e5d1aec0 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 @@ -48,6 +49,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) 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): From 0a7064d0260c34ecf7a256d36ab85dc6c7d2a969 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:25:01 -0700 Subject: [PATCH 34/39] include GPIO for waveshare27inchPartial --- pwnagotchi/ui/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index 9ecc857f..f452a940 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -16,6 +16,8 @@ from pwnagotchi.ui.components import * from pwnagotchi.ui.state import State from pwnagotchi.voice import Voice +import RPi.GPIO as GPIO + WHITE = 0xff BLACK = 0x00 ROOT = None From 24ebc417ad32b85bccc1d720021b8ad396bcb07f Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 13:25:32 -0700 Subject: [PATCH 35/39] waveshare27inchPartial from https://github.com/daniilprohorov/pwnagotchi.git --- pwnagotchi/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index cbba5f63..69915c9d 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' From 9978d5da36ab28dc015e0f2af882907eb0009d38 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Mon, 3 Jul 2023 14:45:38 -0700 Subject: [PATCH 36/39] added delay after deauth to avoid triggering some sort of firmware bug in nexmon. works but I don't really know why --- bin/pwnagotchi | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/pwnagotchi b/bin/pwnagotchi index aa462297..35a9cf5e 100755 --- a/bin/pwnagotchi +++ b/bin/pwnagotchi @@ -69,6 +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) # delay to not trigger nexmon firmware bugs # An interesting effect of this: # From cd92491e40c0d73edbb7409c1a68b663207caef9 Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Tue, 4 Jul 2023 13:18:18 -0700 Subject: [PATCH 37/39] move brain.nn backup into ai_worker before training session instead of backing up on every save. keep the "previous trained" brain --- pwnagotchi/ai/train.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py index 2d72e6e6..cba14dfe 100644 --- a/pwnagotchi/ai/train.py +++ b/pwnagotchi/ai/train.py @@ -116,10 +116,7 @@ class AsyncTrainer(object): def _save_ai(self): logging.info("[ai] saving model to %s ..." % self._nn_path) temp = "%s.tmp" % self._nn_path - back = "%s.bak" % self._nn_path self._model.save(temp) - if os.path.isfile(self._nn_path): - os.replace(self._nn_path, back) os.replace(temp, self._nn_path) def on_ai_step(self): @@ -180,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: From 890d9c27d14114ab9bde614f5886e445d8c8902d Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Thu, 6 Jul 2023 01:55:37 -0700 Subject: [PATCH 38/39] call plugin on_wait and on_sleep during updated partial wait/sleep delays --- pwnagotchi/ui/view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index f452a940..00028c26 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -247,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 @@ -259,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) From 5805bc02e2b4621e67516f87bfd671f9321e29ce Mon Sep 17 00:00:00 2001 From: Sniffleupagus Date: Sat, 15 Jul 2023 16:25:09 -0700 Subject: [PATCH 39/39] enable color in Image creation with config options --- pwnagotchi/ui/view.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index f452a940..1536d973 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -18,8 +18,8 @@ from pwnagotchi.voice import Voice import RPi.GPIO as GPIO -WHITE = 0xff -BLACK = 0x00 +WHITE = 0x181010 +BLACK = 0xffcccc ROOT = None @@ -124,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) @@ -373,7 +373,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)