diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index 751f19e..27a7b30 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -61,6 +61,9 @@ class Display(View): def is_waveshare213d(self): return self._implementation.name == 'waveshare213d' + def is_spotpear24inch(self): + return self._implementation.name == 'spotpear24inch' + def is_waveshare_any(self): return self.is_waveshare_v1() or self.is_waveshare_v2() diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index 77fd60e..1bbb602 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -9,7 +9,7 @@ from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch from pwnagotchi.ui.hw.waveshare213d import Waveshare213d - +from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch def display_for(config): # config has been normalized already in utils.load_config @@ -44,4 +44,7 @@ def display_for(config): return Waveshare154inch(config) elif config['ui']['display']['type'] == 'waveshare213d': - return Waveshare213d(config) \ No newline at end of file + return Waveshare213d(config) + + elif config['ui']['display']['type'] == 'spotpear24inch': + return Spotpear24inch(config) \ No newline at end of file diff --git a/pwnagotchi/ui/hw/libs/fb/__init__.py b/pwnagotchi/ui/hw/libs/fb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pwnagotchi/ui/hw/libs/fb/fb.py b/pwnagotchi/ui/hw/libs/fb/fb.py new file mode 100644 index 0000000..f3c0f72 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/fb/fb.py @@ -0,0 +1,144 @@ +FBIOGET_VSCREENINFO=0x4600 +FBIOPUT_VSCREENINFO=0x4601 +FBIOGET_FSCREENINFO=0x4602 +FBIOGETCMAP=0x4604 +FBIOPUTCMAP=0x4605 +FBIOPAN_DISPLAY=0x4606 + +FBIOGET_CON2FBMAP=0x460F +FBIOPUT_CON2FBMAP=0x4610 +FBIOBLANK=0x4611 +FBIO_ALLOC=0x4613 +FBIO_FREE=0x4614 +FBIOGET_GLYPH=0x4615 +FBIOGET_HWCINFO=0x4616 +FBIOPUT_MODEINFO=0x4617 +FBIOGET_DISPINFO=0x4618 + +from mmap import mmap +from fcntl import ioctl +import struct + +mm = None +bpp, w, h = 0, 0, 0 # framebuffer bpp and size +bytepp = 0 +vx, vy, vw, vh = 0, 0, 0, 0 #virtual window offset and size +vi, fi = None, None +_fb_cmap = 'IIPPPP' # start, len, r, g, b, a +RGB = False +_verbose = False +msize_kb = 0 + +def report_fb(i=0, layer=0): + with open('/dev/fb'+str(i), 'r+b')as f: + vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160)) + vi = list(struct.unpack('I'*40, vi)) + ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH' + fic = struct.calcsize(ffm) + fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic))) + +def ready_fb(_bpp=None, i=0, layer=0, _win=None): + global mm, bpp, w, h, vi, fi, RGB, msize_kb, vx, vy, vw, vh, bytepp + if mm and bpp == _bpp: return mm, w, h, bpp + with open('/dev/fb'+str(i), 'r+b')as f: + vi = ioctl(f, FBIOGET_VSCREENINFO, bytes(160)) + vi = list(struct.unpack('I'*40, vi)) + bpp = vi[6] + bytepp = bpp//8 + if _bpp: + vi[6] = _bpp # 24 bit = BGR 888 mode + try: + vi = ioctl(f, FBIOPUT_VSCREENINFO, struct.pack('I'*40, *vi)) # fb_var_screeninfo + vi = struct.unpack('I'*40,vi) + bpp = vi[6] + bytepp = bpp//8 + except: + pass + + if vi[8] == 0 : RGB = True + + ffm = 'c'*16+'L'+'I'*4+'H'*3+'ILIIHHH' + fic = struct.calcsize(ffm) + fi = struct.unpack(ffm, ioctl(f, FBIOGET_FSCREENINFO, bytes(fic))) + msize = fi[17] # = w*h*bpp//8 + ll, start = fi[-7:-5] + w, h = ll//bytepp, vi[1] # when screen is vertical, width becomes wrong. ll//3 is more accurate at such time. + if _win and len(_win)==4: # virtual window settings + vx, vy, vw, vh = _win + if vw == 'w': vw = w + if vh == 'h': vh = h + vx, vy, vw, vh = map(int, (vx, vy, vw, vh)) + if vx>=w: vx = 0 + if vy>=h: vy = 0 + if vx>w: vw = w - vx + else: vw -= vx + if vy>h: vh = h - vy + else: vh -= vy + else: + vx, vy, vw, vh = 0,0,w,h + msize_kb = vw*vh*bytepp//1024 # more accurate FB memory size in kb + + mm = mmap(f.fileno(), msize, offset=start) + return mm, w, h, bpp#ll//(bpp//8), h + +def fill_scr(r,g,b): + if bpp == 32: + seed = struct.pack('BBBB', b, g, r, 255) + elif bpp == 24: + seed = struct.pack('BBB', b, g, r) + elif bpp == 16: + seed = struct.pack('H', r>>3<<11 | g>>2<<5 | b>>3) + mm.seek(0) + show_img(seed * vw * vh) + +def black_scr(): + fill_scr(0,0,0) + +def white_scr(): + fill_scr(255,255,255) + +def mmseekto(x,y): + mm.seek((x + y*w) * bytepp) + +def dot(x, y, r, g, b): + mmseekto(x,y) + mm.write(struct.pack('BBB',*((r,g,b) if RGB else (b,g,r)))) + +def get_pixel(x,y): + mmseekto(x,y) + return mm.read(bytepp) + +def _888_to_565(bt): + b = b'' + for i in range(0, len(bt),3): + b += int.to_bytes(bt[i]>>3<<11|bt[i+1]>>2<<5|bt[i+2]>>3, 2, 'little') + return b + +def numpy_888_565(bt): + import numpy as np + arr = np.fromstring(bt, dtype=np.uint32) + return (((0xF80000 & arr)>>8)|((0xFC00 & arr)>>5)|((0xF8 & arr)>>3)).astype(np.uint16).tostring() + +def show_img(img): + if not type(img) is bytes: + if not RGB: + if bpp == 24: # for RPI + img = img.tobytes('raw', 'BGR') + else: + img = img.convert('RGBA').tobytes('raw', 'BGRA') + if bpp == 16: + img = numpy_888_565(img) + else: + if bpp == 24: + img = img.tobytes() + else: + img = img.convert('RGBA').tobytes() + if bpp == 16: + img = numpy_888_565(img) + from io import BytesIO + b = BytesIO(img) + s = vw*bytepp + for y in range(vh): # virtual window drawing + mmseekto(vx,vy+y) + mm.write(b.read(s)) + diff --git a/pwnagotchi/ui/hw/spotpear24inch.py b/pwnagotchi/ui/hw/spotpear24inch.py new file mode 100644 index 0000000..e93b071 --- /dev/null +++ b/pwnagotchi/ui/hw/spotpear24inch.py @@ -0,0 +1,52 @@ +import logging + +import pwnagotchi.ui.fonts as fonts +from pwnagotchi.ui.hw.base import DisplayImpl + +import os,time + +class Spotpear24inch(DisplayImpl): + def __init__(self, config): + super(Spotpear24inch, self).__init__(config, 'spotpear24inch') + self._display = None + + def layout(self): + fonts.setup(12, 10, 12, 70) + 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.Medium, + 'max': 20 + } + + return self._layout + + def refresh(self): + time.sleep(0.1) + + def initialize(self): + from pwnagotchi.ui.hw.libs.fb import fb + self._display = fb + logging.info("initializing spotpear 24inch lcd display") + self._display.ready_fb(i=1) + self._display.black_scr() + + def render(self, canvas): + self._display.show_img(canvas.rotate(180)) + self.refresh() + + def clear(self): + self._display.black_scr() + self.refresh() diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 239e5e8..4384dc8 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -112,6 +112,9 @@ def load_config(args): elif config['ui']['display']['type'] in ('ws_213d', 'ws213d', 'waveshare_213d', 'waveshare213d'): config['ui']['display']['type'] = 'waveshare213d' + elif config['ui']['display']['type'] in ('spotpear24inch'): + config['ui']['display']['type'] = 'spotpear24inch' + else: print("unsupported display type %s" % config['ui']['display']['type']) exit(1)