diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index 5eeacc5..8d0cb19 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -49,6 +49,9 @@ class Display(View): def is_oledhat(self): return self._implementation.name == 'oledhat' + def is_adafruitssd1306i2c(self): + return self._implementation.name == 'adafruitssd1306i2c' + def is_lcdhat(self): return self._implementation.name == 'lcdhat' @@ -69,7 +72,7 @@ class Display(View): def is_waveshare213bc(self): return self._implementation.name == 'waveshare213bc' - + def is_waveshare213inb_v4(self): return self._implementation.name == 'waveshare213inb_v4' diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index c738826..b5353ed 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -1,6 +1,7 @@ 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.adafruitssd1306i2c import AdafruitSSD1306i2c from pwnagotchi.ui.hw.lcdhat import LcdHat from pwnagotchi.ui.hw.dfrobot1 import DFRobotV1 from pwnagotchi.ui.hw.dfrobot2 import DFRobotV2 @@ -28,6 +29,9 @@ def display_for(config): if config['ui']['display']['type'] == 'oledhat': return OledHat(config) + if config['ui']['display']['type'] == 'adafruitssd1306i2c': + return AdafruitSSD1306i2c(config) + if config['ui']['display']['type'] == 'lcdhat': return LcdHat(config) diff --git a/pwnagotchi/ui/hw/adafruitssd1306i2c.py b/pwnagotchi/ui/hw/adafruitssd1306i2c.py new file mode 100644 index 0000000..7676392 --- /dev/null +++ b/pwnagotchi/ui/hw/adafruitssd1306i2c.py @@ -0,0 +1,46 @@ +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + + +class AdafruitSSD1306i2c(DisplayImpl): + def __init__(self, config): + super(AdafruitSSD1306i2c, self).__init__(config, 'adafruitssd1306i2c') + self._display = None + + def layout(self): + fonts.setup(8, 8, 8, 8, 25, 9) + 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.status_font(fonts.Small), + 'max': 18 + } + return self._layout + + def initialize(self): + logging.info("initializing adafruitssd1306i2c display") + from pwnagotchi.ui.hw.libs.adafruit.adafruitssd1306i2c.epd import EPD + logging.info("EPD loaded") + 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/libs/adafruit/adafruitssd1306i2c/SSD1306.py b/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/SSD1306.py new file mode 100644 index 0000000..5312bb6 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/SSD1306.py @@ -0,0 +1,252 @@ +# Adaption of the Python driver for 0.96" 128*64 i2c OLED displays, +# based on the Adafruit driver without using any Adafruit dependency libraries, +# instead it's using the usual smbus and RPi.GPIO +# Adaptation made by: MD Raqibul Islam (github.com/erajtob) +# +# -- +# 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 OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import logging +import RPi.GPIO as GPIO +import time +from smbus import SMBus + +width = 128 #LCD width +height = 64 #LCD height + +# Constants +SSD1306_I2C_ADDRESS = 0x3C # 011110+SA0+RW - 0x3C or 0x3D +SSD1306_SETCONTRAST = 0x81 +SSD1306_DISPLAYALLON_RESUME = 0xA4 +SSD1306_DISPLAYALLON = 0xA5 +SSD1306_NORMALDISPLAY = 0xA6 +SSD1306_INVERTDISPLAY = 0xA7 +SSD1306_DISPLAYOFF = 0xAE +SSD1306_DISPLAYON = 0xAF +SSD1306_SETDISPLAYOFFSET = 0xD3 +SSD1306_SETCOMPINS = 0xDA +SSD1306_SETVCOMDETECT = 0xDB +SSD1306_SETDISPLAYCLOCKDIV = 0xD5 +SSD1306_SETPRECHARGE = 0xD9 +SSD1306_SETMULTIPLEX = 0xA8 +SSD1306_SETLOWCOLUMN = 0x00 +SSD1306_SETHIGHCOLUMN = 0x10 +SSD1306_SETSTARTLINE = 0x40 +SSD1306_MEMORYMODE = 0x20 +SSD1306_COLUMNADDR = 0x21 +SSD1306_PAGEADDR = 0x22 +SSD1306_COMSCANINC = 0xC0 +SSD1306_COMSCANDEC = 0xC8 +SSD1306_SEGREMAP = 0xA0 +SSD1306_CHARGEPUMP = 0x8D +SSD1306_EXTERNALVCC = 0x1 +SSD1306_SWITCHCAPVCC = 0x2 + +# Scrolling constants +SSD1306_ACTIVATE_SCROLL = 0x2F +SSD1306_DEACTIVATE_SCROLL = 0x2E +SSD1306_SET_VERTICAL_SCROLL_AREA = 0xA3 +SSD1306_RIGHT_HORIZONTAL_SCROLL = 0x26 +SSD1306_LEFT_HORIZONTAL_SCROLL = 0x27 +SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29 +SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A + +class SSD1306Base(object): + def __init__(self, width, height, rst, i2c_bus=None, i2c_address=SSD1306_I2C_ADDRESS, i2c=None): + self._i2c = None + self.width = width + self.height = height + self._pages = height//8 + self._buffer = [0]*(width*self._pages) + if i2c_bus is None: + self._i2c = SMBus(1) + else: + self._i2c = SMBus(i2c_bus) + self._gpio = GPIO + self._gpio.setmode(GPIO.BCM) + self._gpio.setwarnings(False) + # Setup reset pin. + self._rst = rst + if not self._rst is None: + self._gpio.setup(self._rst, GPIO.OUT) + + def _initialize(self): + raise NotImplementedError + + def write8(self, register, value): + """Write an 8-bit value to the specified register.""" + value = value & 0xFF + self._i2c.write_byte_data(SSD1306_I2C_ADDRESS, register, value) + + def command(self, c): + """Send command byte to display.""" + # I2C write. + control = 0x00 # Co = 0, DC = 0 + self.write8(control, c) + time.sleep(0.01) + + def data(self, c): + """Send byte of data to display.""" + # I2C write. + control = 0x40 # Co = 0, DC = 0 + self.write8(control, c) + time.sleep(0.01) + + def begin(self, vccstate=SSD1306_SWITCHCAPVCC): + """Initialize display.""" + # Save vcc state. + self._vccstate = vccstate + # Reset and initialize display. + self.reset() + self._initialize() + # Turn on the display. + self.command(SSD1306_DISPLAYON) + + def reset(self): + """Reset the display.""" + if self._rst is None: + return + # Set reset high for a millisecond. + GPIO.output(self._rst, GPIO.HIGH) + time.sleep(0.001) + # Set reset low for 10 milliseconds. + GPIO.output(self._rst, GPIO.LOW) + time.sleep(0.010) + # Set reset high again. + GPIO.output(self._rst, GPIO.HIGH) + + def writeList(self, register, data): + """Write bytes to the specified register.""" + self._i2c.write_i2c_block_data(SSD1306_I2C_ADDRESS, register, data) + + def display(self): + """Write display buffer to physical display.""" + self.command(SSD1306_COLUMNADDR) + self.command(0) # Column start address. (0 = reset) + self.command(self.width-1) # Column end address. + self.command(SSD1306_PAGEADDR) + self.command(0) # Page start address. (0 = reset) + self.command(self._pages-1) # Page end address. + # Write buffer data. + for i in range(0, len(self._buffer), 16): + control = 0x40 # Co = 0, DC = 0 + # data = bytearray(len(self._buffer[i:i+16]) + 1) + # data[0] = control & 0xFF + # data[1:] = self._buffer[i:i+16] + self.writeList(control, self._buffer[i:i+16]) + + def image(self, image): + """Set buffer to value of Python Imaging Library image. The image should + be in 1 bit mode and a size equal to the display size. + """ + if image.mode != '1': + raise ValueError('Image must be in mode 1.') + 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)) + # Grab all the pixels from the image, faster than getpixel. + pix = image.load() + # Iterate through the memory pages + index = 0 + for page in range(self._pages): + # Iterate through all x axis columns. + for x in range(self.width): + # Set the bits for the column of pixels at the current position. + bits = 0 + # Don't use range here as it's a bit slow + for bit in [0, 1, 2, 3, 4, 5, 6, 7]: + bits = bits << 1 + bits |= 0 if pix[(x, page*8+7-bit)] == 0 else 1 + # Update buffer byte and increment to next byte. + self._buffer[index] = bits + index += 1 + + def clear(self): + """Clear contents of image buffer.""" + self._buffer = [0]*(self.width*self._pages) + + def set_contrast(self, contrast): + """Sets the contrast of the display. Contrast should be a value between + 0 and 255.""" + if contrast < 0 or contrast > 255: + raise ValueError('Contrast must be a value from 0 to 255 (inclusive).') + self.command(SSD1306_SETCONTRAST) + self.command(contrast) + + def dim(self, dim): + """Adjusts contrast to dim the display if dim is True, otherwise sets the + contrast to normal brightness if dim is False. + """ + # Assume dim display. + contrast = 0 + # Adjust contrast based on VCC if not dimming. + if not dim: + if self._vccstate == SSD1306_EXTERNALVCC: + contrast = 0x9F + else: + contrast = 0xCF + self.set_contrast(contrast) + +class SSD1306_128_64(SSD1306Base): + def __init__(self, rst, i2c_bus=None, i2c_address=SSD1306_I2C_ADDRESS, + i2c=None): + # Call base class constructor. + super(SSD1306_128_64, self).__init__(128, 64, rst, i2c_bus, i2c_address, i2c) + + def _initialize(self): + # 128x64 pixel specific initialization. + self.command(SSD1306_DISPLAYOFF) # 0xAE + self.command(SSD1306_SETDISPLAYCLOCKDIV) # 0xD5 + self.command(0x80) # the suggested ratio 0x80 + self.command(SSD1306_SETMULTIPLEX) # 0xA8 + self.command(0x3F) + self.command(SSD1306_SETDISPLAYOFFSET) # 0xD3 + self.command(0x0) # no offset + self.command(SSD1306_SETSTARTLINE | 0x0) # line #0 + self.command(SSD1306_CHARGEPUMP) # 0x8D + if self._vccstate == SSD1306_EXTERNALVCC: + self.command(0x10) + else: + self.command(0x14) + self.command(SSD1306_MEMORYMODE) # 0x20 + self.command(0x00) # 0x0 act like ks0108 + self.command(SSD1306_SEGREMAP | 0x1) + self.command(SSD1306_COMSCANDEC) + self.command(SSD1306_SETCOMPINS) # 0xDA + self.command(0x12) + self.command(SSD1306_SETCONTRAST) # 0x81 + if self._vccstate == SSD1306_EXTERNALVCC: + self.command(0x9F) + else: + self.command(0xCF) + self.command(SSD1306_SETPRECHARGE) # 0xd9 + if self._vccstate == SSD1306_EXTERNALVCC: + self.command(0x22) + else: + self.command(0xF1) + self.command(SSD1306_SETVCOMDETECT) # 0xDB + self.command(0x40) + self.command(SSD1306_DISPLAYALLON_RESUME) # 0xA4 + self.command(SSD1306_NORMALDISPLAY) # 0xA6 diff --git a/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/__init__.py b/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/epd.py b/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/epd.py new file mode 100644 index 0000000..b5a7d05 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/adafruit/adafruitssd1306i2c/epd.py @@ -0,0 +1,28 @@ +from . import SSD1306 + +RST = 24 + +disp = SSD1306.SSD1306_128_64(rst=RST, i2c_bus=1, i2c_address=0x3C) + +class EPD(object): + + def __init__(self, rst=RST, i2c_bus=None, i2c_address=0x3c): + self.width = 128 + self.height = 64 + if i2c_bus is None: + self.i2c_bus = 1 + else: + self.i2c_bus = i2c_bus + self.i2c_address = i2c_address + self.disp = disp + + def init(self): + self.disp.begin() + + def Clear(self): + self.disp.clear() + self.disp.display() + + def display(self, image): + self.disp.image(image) + self.disp.display() \ No newline at end of file diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 899acce..05527be 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -242,6 +242,9 @@ def load_config(args): elif config['ui']['display']['type'] in ('oledhat',): config['ui']['display']['type'] = 'oledhat' + elif config['ui']['display']['type'] in ('adafruitssd1306i2c',): + config['ui']['display']['type'] = 'adafruitssd1306i2c' + elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1'): config['ui']['display']['type'] = 'waveshare_1' @@ -277,7 +280,7 @@ def load_config(args): elif config['ui']['display']['type'] in ('ws_213bc', 'ws213bc', 'waveshare_213bc', 'waveshare213bc'): config['ui']['display']['type'] = 'waveshare213bc' - + elif config['ui']['display']['type'] in ('ws_213bv4', 'ws213bv4', 'waveshare_213bv4', 'waveshare213inb_v4'): config['ui']['display']['type'] = 'waveshare213inb_v4'