From dca99bf24787abe9a0376c6253db7cd7e915d6c4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 3 Nov 2020 18:43:24 +0000 Subject: [PATCH] Scale widget added. Pass initial test. --- drivers/ssd1306/ssd1306.py | 155 +++++++++++++++++++++++++ gui/core/colors.py | 15 +++ gui/core/nanogui.py | 187 ------------------------------ gui/demos/aclock.py | 4 +- gui/demos/alevel.py | 3 +- gui/demos/asnano.py | 5 +- gui/demos/asnano_sync.py | 5 +- gui/demos/color15.py | 7 +- gui/demos/color96.py | 7 +- gui/demos/fpt.py | 19 +-- gui/demos/scale.py | 59 ++++++++++ gui/fonts/font10.py | 229 +++++++++++++++++++++++++++++++++++++ gui/widgets/__init__.py | 0 gui/widgets/dial.py | 78 +++++++++++++ gui/widgets/label.py | 45 ++++++++ gui/widgets/led.py | 28 +++++ gui/widgets/meter.py | 63 ++++++++++ gui/widgets/scale.py | 123 ++++++++++++++++++++ ssd1351_setup.py | 34 ++++++ 19 files changed, 859 insertions(+), 207 deletions(-) create mode 100644 drivers/ssd1306/ssd1306.py create mode 100644 gui/core/colors.py create mode 100644 gui/demos/scale.py create mode 100644 gui/fonts/font10.py create mode 100644 gui/widgets/__init__.py create mode 100644 gui/widgets/dial.py create mode 100644 gui/widgets/label.py create mode 100644 gui/widgets/led.py create mode 100644 gui/widgets/meter.py create mode 100644 gui/widgets/scale.py create mode 100644 ssd1351_setup.py diff --git a/drivers/ssd1306/ssd1306.py b/drivers/ssd1306/ssd1306.py new file mode 100644 index 0000000..6359c85 --- /dev/null +++ b/drivers/ssd1306/ssd1306.py @@ -0,0 +1,155 @@ +# MicroPython SSD1306 OLED driver, I2C and SPI interfaces + +from micropython import const +import framebuf + + +# register definitions +SET_CONTRAST = const(0x81) +SET_ENTIRE_ON = const(0xA4) +SET_NORM_INV = const(0xA6) +SET_DISP = const(0xAE) +SET_MEM_ADDR = const(0x20) +SET_COL_ADDR = const(0x21) +SET_PAGE_ADDR = const(0x22) +SET_DISP_START_LINE = const(0x40) +SET_SEG_REMAP = const(0xA0) +SET_MUX_RATIO = const(0xA8) +SET_COM_OUT_DIR = const(0xC0) +SET_DISP_OFFSET = const(0xD3) +SET_COM_PIN_CFG = const(0xDA) +SET_DISP_CLK_DIV = const(0xD5) +SET_PRECHARGE = const(0xD9) +SET_VCOM_DESEL = const(0xDB) +SET_CHARGE_PUMP = const(0x8D) + +# Subclassing FrameBuffer provides support for graphics primitives +# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html +class SSD1306(framebuf.FrameBuffer): + def __init__(self, width, height, external_vcc): + self.width = width + self.height = height + self.external_vcc = external_vcc + self.pages = self.height // 8 + self.buffer = bytearray(self.pages * self.width) + super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) + self.init_display() + + def init_display(self): + for cmd in ( + SET_DISP | 0x00, # off + # address setting + SET_MEM_ADDR, + 0x00, # horizontal + # resolution and layout + SET_DISP_START_LINE | 0x00, + SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 + SET_MUX_RATIO, + self.height - 1, + SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 + SET_DISP_OFFSET, + 0x00, + SET_COM_PIN_CFG, + 0x02 if self.width > 2 * self.height else 0x12, + # timing and driving scheme + SET_DISP_CLK_DIV, + 0x80, + SET_PRECHARGE, + 0x22 if self.external_vcc else 0xF1, + SET_VCOM_DESEL, + 0x30, # 0.83*Vcc + # display + SET_CONTRAST, + 0xFF, # maximum + SET_ENTIRE_ON, # output follows RAM contents + SET_NORM_INV, # not inverted + # charge pump + SET_CHARGE_PUMP, + 0x10 if self.external_vcc else 0x14, + SET_DISP | 0x01, + ): # on + self.write_cmd(cmd) + self.fill(0) + self.show() + + def poweroff(self): + self.write_cmd(SET_DISP | 0x00) + + def poweron(self): + self.write_cmd(SET_DISP | 0x01) + + def contrast(self, contrast): + self.write_cmd(SET_CONTRAST) + self.write_cmd(contrast) + + def invert(self, invert): + self.write_cmd(SET_NORM_INV | (invert & 1)) + + def show(self): + x0 = 0 + x1 = self.width - 1 + if self.width == 64: + # displays with width of 64 pixels are shifted by 32 + x0 += 32 + x1 += 32 + self.write_cmd(SET_COL_ADDR) + self.write_cmd(x0) + self.write_cmd(x1) + self.write_cmd(SET_PAGE_ADDR) + self.write_cmd(0) + self.write_cmd(self.pages - 1) + self.write_data(self.buffer) + + +class SSD1306_I2C(SSD1306): + def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False): + self.i2c = i2c + self.addr = addr + self.temp = bytearray(2) + self.write_list = [b"\x40", None] # Co=0, D/C#=1 + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.temp[0] = 0x80 # Co=1, D/C#=0 + self.temp[1] = cmd + self.i2c.writeto(self.addr, self.temp) + + def write_data(self, buf): + self.write_list[1] = buf + self.i2c.writevto(self.addr, self.write_list) + + +class SSD1306_SPI(SSD1306): + def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): + self.rate = 10 * 1024 * 1024 + dc.init(dc.OUT, value=0) + res.init(res.OUT, value=0) + cs.init(cs.OUT, value=1) + self.spi = spi + self.dc = dc + self.res = res + self.cs = cs + import time + + self.res(1) + time.sleep_ms(1) + self.res(0) + time.sleep_ms(10) + self.res(1) + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(0) + self.cs(0) + self.spi.write(bytearray([cmd])) + self.cs(1) + + def write_data(self, buf): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(1) + self.cs(0) + self.spi.write(buf) + self.cs(1) diff --git a/gui/core/colors.py b/gui/core/colors.py new file mode 100644 index 0000000..365e4e4 --- /dev/null +++ b/gui/core/colors.py @@ -0,0 +1,15 @@ +# colors.py Standard color constants for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +from drivers.ssd1351.ssd1351 import SSD1351 as SSD + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 +WHITE = SSD.rgb(255, 255, 255) +LIGHTGREEN = SSD.rgb(0, 100, 0) + diff --git a/gui/core/nanogui.py b/gui/core/nanogui.py index 2a1ba81..4ab6cf0 100644 --- a/gui/core/nanogui.py +++ b/gui/core/nanogui.py @@ -177,190 +177,3 @@ class DObject(): self.label.value(text, invert, fgcolor, bgcolor, bdcolor) else: raise ValueError('Attempt to update nonexistent label.') - -# text: str display string int save width -class Label(DObject): - def __init__(self, writer, row, col, text, invert=False, fgcolor=None, bgcolor=None, bdcolor=False): - # Determine width of object - if isinstance(text, int): - width = text - text = None - else: - width = writer.stringlen(text) - height = writer.height - super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) - if text is not None: - self.value(text, invert) - - def value(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): - txt = super().value(text) - # Redraw even if no text supplied: colors may have changed. - self.invert = invert - self.fgcolor = self.def_fgcolor if fgcolor is None else fgcolor - self.bgcolor = self.def_bgcolor if bgcolor is None else bgcolor - if bdcolor is False: - self.def_bdcolor = False - self.bdcolor = self.def_bdcolor if bdcolor is None else bdcolor - self.show() - return txt - - def show(self): - txt = super().value() - if txt is None: # No content to draw. Future use. - return - super().show() # Draw or erase border - wri = self.writer - dev = self.device - wri.setcolor(self.fgcolor, self.bgcolor) - Writer.set_textpos(dev, self.row, self.col) - wri.setcolor(self.fgcolor, self.bgcolor) - wri.printstring(txt, self.invert) - wri.setcolor() # Restore defaults - -class Meter(DObject): - BAR = 1 - LINE = 0 - def __init__(self, writer, row, col, *, height=50, width=10, - fgcolor=None, bgcolor=None, ptcolor=None, bdcolor=None, - divisions=5, label=None, style=0, legends=None, value=None): - super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) - self.divisions = divisions - if label is not None: - Label(writer, row + height + 3, col, label) - self.style = style - self.legends = legends - self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor - self.value(value) - - def value(self, n=None, color=None): - if n is None: - return super().value() - n = super().value(min(1, max(0, n))) - if color is not None: - self.ptcolor = color - self.show() - return n - - def show(self): - super().show() # Draw or erase border - val = super().value() - wri = self.writer - dev = self.device - width = self.width - height = self.height - legends = self.legends - x0 = self.col - x1 = self.col + width - y0 = self.row - y1 = self.row + height - if self.divisions > 0: - dy = height / (self.divisions) # Tick marks - for tick in range(self.divisions + 1): - ypos = int(y0 + dy * tick) - dev.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) - - if legends is not None: # Legends - dy = 0 if len(legends) <= 1 else height / (len(legends) -1) - yl = y1 - wri.height / 2 # Start at bottom - for legend in legends: - Label(wri, int(yl), x1 + 4, legend) - yl -= dy - y = int(y1 - val * height) # y position of slider - if self.style == self.LINE: - dev.hline(x0, y, width, self.ptcolor) # Draw pointer - else: - w = width / 2 - dev.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) - - -class LED(DObject): - def __init__(self, writer, row, col, *, height=12, - fgcolor=None, bgcolor=None, bdcolor=None, label=None): - super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) - if label is not None: - self.label = Label(writer, row + height + 3, col, label) - self.radius = self.height // 2 - - def color(self, c=None): - self.fgcolor = self.bgcolor if c is None else c - self.show() - - def show(self): - super().show() - wri = self.writer - dev = self.device - r = self.radius - fillcircle(dev, self.col + r, self.row + r, r, self.fgcolor) - if isinstance(self.bdcolor, int): - circle(dev, self.col + r, self.row + r, r, self.bdcolor) - - -class Pointer(): - def __init__(self, dial): - self.dial = dial - self.val = 0 + 0j - self.color = None - - def value(self, v=None, color=None): - self.color = color - if v is not None: - if isinstance(v, complex): - l = cmath.polar(v)[0] - if l > 1: - self.val = v/l - else: - self.val = v - else: - raise ValueError('Pointer value must be complex.') - self.dial.vectors.add(self) - self.dial._set_pend(self.dial) # avoid redrawing for each vector - return self.val - -class Dial(DObject): - CLOCK = 0 - COMPASS = 1 - def __init__(self, writer, row, col, *, height=50, - fgcolor=None, bgcolor=None, bdcolor=False, ticks=4, - label=None, style=0, pip=None): - super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) - self.style = style - self.pip = self.fgcolor if pip is None else pip - if label is not None: - self.label = Label(writer, row + height + 3, col, label) - radius = int(height / 2) - self.radius = radius - self.ticks = ticks - self.xorigin = col + radius - self.yorigin = row + radius - self.vectors = set() - - def show(self): - super().show() - # cache bound variables - dev = self.device - ticks = self.ticks - radius = self.radius - xo = self.xorigin - yo = self.yorigin - # vectors (complex) - vor = xo + 1j * yo - vtstart = 0.9 * radius + 0j # start of tick - vtick = 0.1 * radius + 0j # tick - vrot = cmath.exp(2j * cmath.pi/ticks) # unit rotation - for _ in range(ticks): - polar(dev, vor + conj(vtstart), vtick, self.fgcolor) - vtick *= vrot - vtstart *= vrot - circle(dev, xo, yo, radius, self.fgcolor) - vshort = 1000 # Length of shortest vector - for v in self.vectors: - color = self.fgcolor if v.color is None else v.color - val = v.value() * radius # val is complex - vshort = min(vshort, cmath.polar(val)[0]) - if self.style == Dial.CLOCK: - polar(dev, vor, val, color) - else: - arrow(dev, vor, val, 5, color) - if isinstance(self.pip, int) and vshort > 5: - fillcircle(dev, xo, yo, 2, self.pip) - diff --git a/gui/demos/aclock.py b/gui/demos/aclock.py index f3ff647..7421463 100644 --- a/gui/demos/aclock.py +++ b/gui/demos/aclock.py @@ -17,7 +17,9 @@ # Initialise hardware from ssd1351_setup import ssd, height # Create a display instance -from gui.core.nanogui import Dial, Pointer, refresh, Label +from gui.core.nanogui import refresh +from gui.widgets.label import Label +from gui.widgets.dial import Dial, Pointer refresh(ssd) # Initialise and clear display. # Now import other modules diff --git a/gui/demos/alevel.py b/gui/demos/alevel.py index 358334a..4e9965d 100644 --- a/gui/demos/alevel.py +++ b/gui/demos/alevel.py @@ -24,7 +24,8 @@ import gc # Initialise hardware from ssd1351_setup import ssd # Create a display instance -from gui.core.nanogui import Dial, Pointer, refresh +from gui.core.nanogui import refresh +from gui.widgets.dial import Dial, Pointer refresh(ssd) # Initialise and clear display. # Now import other modules diff --git a/gui/demos/asnano.py b/gui/demos/asnano.py index eafde26..cf55657 100644 --- a/gui/demos/asnano.py +++ b/gui/demos/asnano.py @@ -13,7 +13,10 @@ import uasyncio as asyncio import pyb import uos from gui.core.writer import CWriter -from gui.core.nanogui import LED, Meter, refresh +from gui.core.nanogui import refresh +from gui.widgets.led import LED +from gui.widgets.meter import Meter + refresh(ssd) # Fonts diff --git a/gui/demos/asnano_sync.py b/gui/demos/asnano_sync.py index 6f129b7..419b6bb 100644 --- a/gui/demos/asnano_sync.py +++ b/gui/demos/asnano_sync.py @@ -13,7 +13,10 @@ import uasyncio as asyncio import pyb import uos from gui.core.writer import CWriter -from gui.core.nanogui import LED, Meter, refresh +from gui.core.nanogui import refresh +from gui.widgets.led import LED +from gui.widgets.meter import Meter + refresh(ssd) # Fonts diff --git a/gui/demos/color15.py b/gui/demos/color15.py index 751c8b8..3dc7a7e 100644 --- a/gui/demos/color15.py +++ b/gui/demos/color15.py @@ -12,7 +12,11 @@ import cmath import utime import uos from gui.core.writer import Writer, CWriter -from gui.core.nanogui import Label, Meter, LED, Dial, Pointer, refresh +from gui.core.nanogui import refresh +from gui.widgets.led import LED +from gui.widgets.meter import Meter +from gui.widgets.label import Label +from gui.widgets.dial import Dial, Pointer # Fonts import gui.fonts.arial10 as arial10 @@ -149,6 +153,7 @@ def compass(x): refresh(ssd) print('Color display test is running.') +print('Test runs to completion.') clock(70) compass(70) meter() diff --git a/gui/demos/color96.py b/gui/demos/color96.py index cfebe4a..3941bba 100644 --- a/gui/demos/color96.py +++ b/gui/demos/color96.py @@ -7,7 +7,11 @@ from ssd1351_setup import ssd # Create a display instance -from gui.core.nanogui import Label, Meter, LED, refresh +from gui.core.nanogui import refresh +from gui.widgets.led import LED +from gui.widgets.meter import Meter +from gui.widgets.label import Label + refresh(ssd) # Fonts import gui.fonts.arial10 as arial10 @@ -96,6 +100,7 @@ def vari_fields(): refresh(ssd) print('Color display test is running.') +print('Test runs to completion.') meter() multi_fields(t = 10) vari_fields() diff --git a/gui/demos/fpt.py b/gui/demos/fpt.py index 2a80cd5..7c08cd3 100644 --- a/gui/demos/fpt.py +++ b/gui/demos/fpt.py @@ -6,19 +6,6 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2018-2020 Peter Hinch -# WIRING (Adafruit pin nos and names) -# Pyb SSD -# 3v3 Vin (10) -# Gnd Gnd (11) -# X1 DC (3 DC) -# X2 CS (5 OC OLEDCS) -# X3 Rst (4 R RESET) -# X6 CLK (2 CL SCK) -# X8 DATA (1 SI MOSI) - -height = 96 # 1.27 inch 96*128 (rows*cols) display -# height = 128 # 1.5 inch 128*128 display - from ssd1351_setup import ssd # Create a display instance import cmath @@ -27,7 +14,10 @@ import utime import uos from gui.core.writer import Writer, CWriter from gui.core.fplot import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence -from gui.core.nanogui import Label, refresh +from gui.core.nanogui import refresh +from gui.widgets.label import Label + + refresh(ssd) # Fonts @@ -163,6 +153,7 @@ def seq(): refresh(ssd) utime.sleep_ms(100) +print('Test runs to completion.') seq() utime.sleep(1.5) liss() diff --git a/gui/demos/scale.py b/gui/demos/scale.py new file mode 100644 index 0000000..7a6bf58 --- /dev/null +++ b/gui/demos/scale.py @@ -0,0 +1,59 @@ +# scale.py Test/demo of scale widget for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# Usage: +# import gui.demos.scale +# Initialise hardware +from ssd1351_setup import ssd # Create a display instance +from gui.core.nanogui import refresh +from gui.core.writer import CWriter + +import uasyncio as asyncio +from gui.core.colors import * +import gui.fonts.arial10 as arial10 +from gui.widgets.label import Label +from gui.widgets.scale import Scale + +# COROUTINES +async def radio(scale): + cv = 88.0 # Current value + val = 108.0 # Target value + while True: + v1, v2 = val, cv + steps = 200 + delta = (val - cv) / steps + for _ in range(steps): + cv += delta + # Map user variable to -1.0..+1.0 + scale.value(2 * (cv - 88)/(108 - 88) - 1) + await asyncio.sleep_ms(200) + val, cv = v2, v1 + +async def default(scale, lbl): + cv = -1.0 # Current + val = 1.0 + while True: + v1, v2 = val, cv + steps = 400 + delta = (val - cv) / steps + for _ in range(steps): + cv += delta + scale.value(cv) + lbl.value('{:4.3f}'.format(cv)) + refresh(ssd) + await asyncio.sleep_ms(250) + val, cv = v2, v1 + + +def test(): + refresh(ssd) # Initialise and clear display. + CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + wri.set_clip(True, True, False) + lbl = Label(wri, ssd.height - wri.height - 2, 0, 50) + scale = Scale(wri, 5, 5) + asyncio.run(default(scale, lbl)) + +test() diff --git a/gui/fonts/font10.py b/gui/fonts/font10.py new file mode 100644 index 0000000..4e82368 --- /dev/null +++ b/gui/fonts/font10.py @@ -0,0 +1,229 @@ +# Code generated by font-to-py.py. +# Font: FreeSans.ttf +version = '0.1' + +def height(): + return 17 + +def max_width(): + return 17 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +_font =\ +b'\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x06\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x80'\ +b'\x00\xc0\x00\x00\x00\x00\x06\x00\x00\xf0\xf0\xf0\xa0\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x19'\ +b'\x00\x19\x00\x13\x00\x7f\x80\x12\x00\x32\x00\x32\x00\xff\x80\x26'\ +b'\x00\x24\x00\x64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x10'\ +b'\x00\x3c\x00\x56\x00\xd3\x00\xd3\x00\xd0\x00\xd0\x00\x3c\x00\x17'\ +b'\x00\x13\x00\xd3\x00\xd6\x00\x7c\x00\x10\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0f\x00\x00\x00\x78\x20\xcc\x40\xcc\x80\xcc\x80\xc9\x00\x31'\ +b'\x00\x02\x78\x04\xcc\x04\xcc\x08\xcc\x08\xcc\x10\x78\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x1e\x00\x33\x00\x33\x00\x33'\ +b'\x00\x1e\x00\x18\x00\x74\xc0\xe6\xc0\xc3\x80\xc1\x80\xe3\x80\x3c'\ +b'\x40\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\xc0\xc0\xc0\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x20'\ +b'\x20\x60\x40\xc0\xc0\xc0\xc0\xc0\xc0\x40\x60\x20\x30\x10\x00\x06'\ +b'\x00\x80\xc0\x40\x60\x20\x30\x30\x30\x30\x30\x30\x20\x60\x40\xc0'\ +b'\x80\x00\x07\x00\x20\xa8\x70\x50\x50\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x30\x00\x30\x00\x30\x00\xfc\x00\x30\x00\x30\x00\x30'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xc0\x40\x40\x80\x00\x06\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x04'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x00'\ +b'\x00\x00\x05\x00\x08\x08\x10\x10\x10\x20\x20\x20\x40\x40\x40\x80'\ +b'\x80\x00\x00\x00\x00\x09\x00\x00\x00\x3c\x00\x66\x00\x42\x00\xc3'\ +b'\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\x42\x00\x66\x00\x3c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x10\x00\x30'\ +b'\x00\xf0\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30'\ +b'\x00\x30\x00\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x3c\x00\x66\x00\xc3\x00\xc3\x00\x03\x00\x06\x00\x0c\x00\x38'\ +b'\x00\x60\x00\x40\x00\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x09\x00\x00\x00\x7c\x00\xe7\x00\xc3\x00\x03\x00\x02\x00\x1c'\ +b'\x00\x07\x00\x03\x00\x03\x00\xc3\x00\xe6\x00\x3c\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x09\x00\x00\x00\x0c\x00\x0c\x00\x1c\x00\x2c'\ +b'\x00\x2c\x00\x4c\x00\x8c\x00\x8c\x00\xfe\x00\x0c\x00\x0c\x00\x0c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x7e\x00\x40'\ +b'\x00\x40\x00\x80\x00\xbc\x00\xe6\x00\x03\x00\x03\x00\x03\x00\xc3'\ +b'\x00\x66\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x3c\x00\x66\x00\x43\x00\xc0\x00\xc0\x00\xfc\x00\xe6\x00\xc3'\ +b'\x00\xc3\x00\xc3\x00\x66\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x09\x00\x00\x00\xff\x00\x03\x00\x02\x00\x06\x00\x04\x00\x0c'\ +b'\x00\x08\x00\x18\x00\x18\x00\x10\x00\x30\x00\x30\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x09\x00\x00\x00\x3c\x00\x66\x00\xc3\x00\xc3'\ +b'\x00\x66\x00\x3c\x00\x66\x00\xc3\x00\xc3\x00\xc3\x00\x66\x00\x3c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x3c\x00\x66'\ +b'\x00\xc3\x00\xc3\x00\xc3\x00\x67\x00\x3b\x00\x03\x00\x03\x00\xc2'\ +b'\x00\x66\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00'\ +b'\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00'\ +b'\x04\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\xc0\x40'\ +b'\x40\x80\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03'\ +b'\x00\x0e\x00\x38\x00\xc0\x00\xe0\x00\x38\x00\x07\x00\x01\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\xff\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x70\x00\x1c\x00\x03\x00\x07'\ +b'\x00\x1c\x00\xe0\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09'\ +b'\x00\x3c\x00\xc7\x00\xc3\x00\x03\x00\x03\x00\x06\x00\x0c\x00\x08'\ +b'\x00\x18\x00\x18\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x11\x00\x07\xe0\x00\x0c\x38\x00\x30\x0c\x00\x20\x06'\ +b'\x00\x63\xb7\x00\x4c\x73\x00\xcc\x63\x00\xd8\x63\x00\xd8\x63\x00'\ +b'\xd8\x46\x00\xdc\xce\x00\x6f\x78\x00\x30\x00\x00\x18\x00\x00\x0f'\ +b'\xe0\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x06\x00\x0e\x00\x0b\x00'\ +b'\x1b\x00\x1b\x00\x11\x80\x31\x80\x31\x80\x3f\xc0\x60\xc0\x60\x40'\ +b'\x40\x60\xc0\x60\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\xfe\x00'\ +b'\xc3\x80\xc1\x80\xc1\x80\xc1\x80\xc3\x00\xfe\x00\xc1\x80\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc1\x80\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0c\x00\x1f\x80\x30\xc0\x60\x60\x40\x60\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\x40\x60\x60\x60\x30\xc0\x1f\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0c\x00\xff\x00\xc1\x80\xc0\xc0\xc0\x60\xc0\x60'\ +b'\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\xc0\xc1\x80\xff\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\xff\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xff\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\xff\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xfe\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0d\x00\x0f\xc0\x30\x60\x60\x30\x60\x00\xc0\x00\xc0\x00\xc1\xf0'\ +b'\xc0\x30\xc0\x30\x60\x30\x60\x70\x30\xf0\x0f\x10\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0c\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xff\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x09\x00\x06\x00\x06'\ +b'\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\xc6'\ +b'\x00\xc6\x00\xc4\x00\x78\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b'\ +b'\x00\xc0\xc0\xc1\x80\xc3\x00\xc6\x00\xcc\x00\xd8\x00\xfc\x00\xe6'\ +b'\x00\xc6\x00\xc3\x00\xc1\x80\xc1\x80\xc0\xc0\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0a\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0'\ +b'\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xfe\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0e\x00\xe0\x38\xe0\x38\xf0\x78\xf0'\ +b'\x78\xd0\x58\xd8\xd8\xd8\xd8\xc8\x98\xcd\x98\xcd\x98\xc5\x18\xc7'\ +b'\x18\xc7\x18\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\xe0\x60\xe0'\ +b'\x60\xf0\x60\xd0\x60\xd8\x60\xcc\x60\xc4\x60\xc6\x60\xc3\x60\xc3'\ +b'\x60\xc1\xe0\xc0\xe0\xc0\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x0d'\ +b'\x00\x1f\x80\x30\xc0\x60\x60\xe0\x60\xc0\x30\xc0\x30\xc0\x30\xc0'\ +b'\x30\xc0\x30\xe0\x60\x60\x60\x30\xc0\x1f\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\xff\x00\xc1\x80\xc0\xc0\xc0\xc0\xc0\xc0\xc1'\ +b'\x80\xff\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x1f\x80\x30\xc0\x60\x60\xe0'\ +b'\x60\xc0\x30\xc0\x30\xc0\x30\xc0\x30\xc0\x30\xe1\x60\x61\xe0\x30'\ +b'\xc0\x1f\xe0\x00\x20\x00\x00\x00\x00\x00\x00\x0c\x00\xff\x00\xc1'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc1\x80\xff\x00\xc1\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x0b'\ +b'\x00\x3f\x00\x61\x80\xc0\xc0\xc0\x00\xc0\x00\x60\x00\x3e\x00\x07'\ +b'\x80\x01\xc0\xc0\xc0\xc0\xc0\x61\x80\x3f\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\xff\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18'\ +b'\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x61'\ +b'\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\xc0\x60\x40'\ +b'\x40\x60\xc0\x60\xc0\x20\x80\x31\x80\x31\x80\x11\x00\x1b\x00\x0b'\ +b'\x00\x0a\x00\x0e\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10'\ +b'\x00\xc1\x83\xc1\x82\x42\x86\x62\xc6\x62\xc6\x62\x44\x24\x44\x24'\ +b'\x6c\x34\x2c\x3c\x28\x18\x38\x18\x38\x18\x18\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x60\x40\x20\xc0\x31\x80\x19\x00\x1b\x00\x0e'\ +b'\x00\x06\x00\x0e\x00\x1b\x00\x11\x80\x31\x80\x60\xc0\x40\x60\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x40\x60\x60\x60\x30\xc0\x30'\ +b'\xc0\x19\x80\x0d\x00\x0f\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06'\ +b'\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\xff\x80\x01'\ +b'\x80\x03\x00\x06\x00\x06\x00\x0c\x00\x18\x00\x18\x00\x30\x00\x60'\ +b'\x00\x60\x00\xc0\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x05'\ +b'\x00\xe0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xe0\x05\x00\x80\x80\x40\x40\x40\x20\x20\x20\x10\x10\x10\x08'\ +b'\x08\x00\x00\x00\x00\x05\x00\xe0\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\xe0\x08\x00\x00\x30\x30\x78\x48\x48'\ +b'\x84\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x04'\ +b'\x00\xc0\x40\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7c\x00\xc6\x00'\ +b'\x06\x00\x06\x00\x7e\x00\xc6\x00\xc6\x00\xce\x00\x77\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x09\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xde\x00\xe3\x00\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xe3\x00'\ +b'\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x3c\x00\x66\x00\xc3\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc3\x00\x66\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00'\ +b'\x03\x00\x03\x00\x03\x00\x03\x00\x3b\x00\x67\x00\xc3\x00\xc3\x00'\ +b'\xc3\x00\xc3\x00\xc3\x00\x67\x00\x3b\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x00\x66\x00'\ +b'\xc3\x00\xc3\x00\xff\x00\xc0\x00\xc3\x00\x66\x00\x3c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x05\x00\x30\x60\x60\x60\xf0\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x3b\x00\x67\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3'\ +b'\x00\x67\x00\x3b\x00\x03\x00\x03\x00\xc6\x00\x7c\x00\x09\x00\xc0'\ +b'\x00\xc0\x00\xc0\x00\xc0\x00\xde\x00\xe3\x00\xc3\x00\xc3\x00\xc3'\ +b'\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x04\x00\xc0\x00\x00\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\x00\x00\x00\x00\x04\x00\x60\x00\x00\x00\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\xc0\x09\x00\xc0\x00\xc0\x00\xc0\x00\xc0'\ +b'\x00\xc6\x00\xcc\x00\xd8\x00\xf8\x00\xe8\x00\xcc\x00\xc6\x00\xc6'\ +b'\x00\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xdd\xe0\xe7\x30\xc6\x30\xc6\x30'\ +b'\xc6\x30\xc6\x30\xc6\x30\xc6\x30\xc6\x30\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\x00\xe3\x00'\ +b'\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3c\x00\x66\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\x66\x00'\ +b'\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\xde\x00\xe3\x00\xc1\x80\xc1\x80\xc1\x80\xc1\x80'\ +b'\xc1\x80\xe3\x00\xde\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x0a\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x3b\x00\x67\x00\xc3\x00\xc3\x00'\ +b'\xc3\x00\xc3\x00\xc3\x00\x67\x00\x3b\x00\x03\x00\x03\x00\x03\x00'\ +b'\x00\x00\x06\x00\x00\x00\x00\x00\xd8\xe0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\x70'\ +b'\x0e\xc6\xc6\x7c\x00\x00\x00\x00\x05\x00\x00\x00\x60\x60\xf0\x60'\ +b'\x60\x60\x60\x60\x60\x60\x70\x00\x00\x00\x00\x09\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3\x00\xc3'\ +b'\x00\xc3\x00\xc7\x00\x7b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08'\ +b'\x00\x00\x00\x00\x00\xc3\x43\x62\x66\x26\x34\x3c\x18\x18\x00\x00'\ +b'\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6\x30\x46\x30'\ +b'\x47\x20\x6f\x20\x69\x60\x29\x60\x29\xc0\x39\xc0\x10\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x42\x66\x34\x18'\ +b'\x18\x1c\x24\x66\x43\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\xc3'\ +b'\x42\x42\x66\x24\x24\x3c\x18\x18\x18\x10\x30\x60\x08\x00\x00\x00'\ +b'\x00\x00\xfe\x0c\x08\x18\x30\x60\x40\xc0\xfe\x00\x00\x00\x00\x06'\ +b'\x00\x30\x60\x60\x60\x60\x60\x60\xe0\xc0\xe0\x60\x60\x60\x60\x60'\ +b'\x60\x30\x04\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\x00\x06\x00\xc0\x60\x60\x60\x60\x60\x60\x70\x30'\ +b'\x70\x60\x60\x60\x60\x60\x60\xc0\x09\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x62\x00\x9e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\x13\x00\x26\x00\x39\x00\x5d\x00\x81\x00\xa5\x00\xc9\x00'\ +b'\xdc\x00\xef\x00\x02\x01\x15\x01\x39\x01\x4c\x01\x5f\x01\x72\x01'\ +b'\x85\x01\xa9\x01\xcd\x01\xf1\x01\x15\x02\x39\x02\x5d\x02\x81\x02'\ +b'\xa5\x02\xc9\x02\xed\x02\x00\x03\x13\x03\x37\x03\x5b\x03\x7f\x03'\ +b'\xa3\x03\xd8\x03\xfc\x03\x20\x04\x44\x04\x68\x04\x8c\x04\xb0\x04'\ +b'\xd4\x04\xf8\x04\x0b\x05\x2f\x05\x53\x05\x77\x05\x9b\x05\xbf\x05'\ +b'\xe3\x05\x07\x06\x2b\x06\x4f\x06\x73\x06\x97\x06\xbb\x06\xdf\x06'\ +b'\x03\x07\x27\x07\x4b\x07\x6f\x07\x82\x07\x95\x07\xa8\x07\xbb\x07'\ +b'\xdf\x07\xf2\x07\x16\x08\x3a\x08\x5e\x08\x82\x08\xa6\x08\xb9\x08'\ +b'\xdd\x08\x01\x09\x14\x09\x27\x09\x4b\x09\x5e\x09\x82\x09\xa6\x09'\ +b'\xca\x09\xee\x09\x12\x0a\x25\x0a\x38\x0a\x4b\x0a\x6f\x0a\x82\x0a'\ +b'\xa6\x0a\xb9\x0a\xcc\x0a\xdf\x0a\xf2\x0a\x05\x0b\x18\x0b\x3c\x0b'\ + +_mvfont = memoryview(_font) + +def _chr_addr(ordch): + offset = 2 * (ordch - 32) + return int.from_bytes(_index[offset:offset + 2], 'little') + +def get_ch(ch): + ordch = ord(ch) + ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?') + offset = _chr_addr(ordch) + width = int.from_bytes(_font[offset:offset + 2], 'little') + next_offs = _chr_addr(ordch +1) + return _mvfont[offset + 2:next_offs], 17, width + diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/widgets/dial.py b/gui/widgets/dial.py new file mode 100644 index 0000000..b43bd48 --- /dev/null +++ b/gui/widgets/dial.py @@ -0,0 +1,78 @@ +# dial.py Dial and Pointer classes for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +import cmath +from gui.core.nanogui import DObject, polar, conj, circle, arrow, fillcircle +from gui.widgets.label import Label + +class Pointer(): + def __init__(self, dial): + self.dial = dial + self.val = 0 + 0j + self.color = None + + def value(self, v=None, color=None): + self.color = color + if v is not None: + if isinstance(v, complex): + l = cmath.polar(v)[0] + if l > 1: + self.val = v/l + else: + self.val = v + else: + raise ValueError('Pointer value must be complex.') + self.dial.vectors.add(self) + self.dial._set_pend(self.dial) # avoid redrawing for each vector + return self.val + +class Dial(DObject): + CLOCK = 0 + COMPASS = 1 + def __init__(self, writer, row, col, *, height=50, + fgcolor=None, bgcolor=None, bdcolor=False, ticks=4, + label=None, style=0, pip=None): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) + self.style = style + self.pip = self.fgcolor if pip is None else pip + if label is not None: + self.label = Label(writer, row + height + 3, col, label) + radius = int(height / 2) + self.radius = radius + self.ticks = ticks + self.xorigin = col + radius + self.yorigin = row + radius + self.vectors = set() + + def show(self): + super().show() + # cache bound variables + dev = self.device + ticks = self.ticks + radius = self.radius + xo = self.xorigin + yo = self.yorigin + # vectors (complex) + vor = xo + 1j * yo + vtstart = 0.9 * radius + 0j # start of tick + vtick = 0.1 * radius + 0j # tick + vrot = cmath.exp(2j * cmath.pi/ticks) # unit rotation + for _ in range(ticks): + polar(dev, vor + conj(vtstart), vtick, self.fgcolor) + vtick *= vrot + vtstart *= vrot + circle(dev, xo, yo, radius, self.fgcolor) + vshort = 1000 # Length of shortest vector + for v in self.vectors: + color = self.fgcolor if v.color is None else v.color + val = v.value() * radius # val is complex + vshort = min(vshort, cmath.polar(val)[0]) + if self.style == Dial.CLOCK: + polar(dev, vor, val, color) + else: + arrow(dev, vor, val, 5, color) + if isinstance(self.pip, int) and vshort > 5: + fillcircle(dev, xo, yo, 2, self.pip) + diff --git a/gui/widgets/label.py b/gui/widgets/label.py new file mode 100644 index 0000000..6d6d36f --- /dev/null +++ b/gui/widgets/label.py @@ -0,0 +1,45 @@ +# label.py Label class for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +from gui.core.nanogui import DObject +from gui.core.writer import Writer + +# text: str display string int save width +class Label(DObject): + def __init__(self, writer, row, col, text, invert=False, fgcolor=None, bgcolor=None, bdcolor=False): + # Determine width of object + if isinstance(text, int): + width = text + text = None + else: + width = writer.stringlen(text) + height = writer.height + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + if text is not None: + self.value(text, invert) + + def value(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): + txt = super().value(text) + # Redraw even if no text supplied: colors may have changed. + self.invert = invert + self.fgcolor = self.def_fgcolor if fgcolor is None else fgcolor + self.bgcolor = self.def_bgcolor if bgcolor is None else bgcolor + if bdcolor is False: + self.def_bdcolor = False + self.bdcolor = self.def_bdcolor if bdcolor is None else bdcolor + self.show() + return txt + + def show(self): + txt = super().value() + if txt is None: # No content to draw. Future use. + return + super().show() # Draw or erase border + wri = self.writer + dev = self.device + Writer.set_textpos(dev, self.row, self.col) + wri.setcolor(self.fgcolor, self.bgcolor) + wri.printstring(txt, self.invert) + wri.setcolor() # Restore defaults diff --git a/gui/widgets/led.py b/gui/widgets/led.py new file mode 100644 index 0000000..f489d73 --- /dev/null +++ b/gui/widgets/led.py @@ -0,0 +1,28 @@ +# led.py LED class for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +from gui.core.nanogui import DObject, fillcircle, circle +from gui.widgets.label import Label + +class LED(DObject): + def __init__(self, writer, row, col, *, height=12, + fgcolor=None, bgcolor=None, bdcolor=None, label=None): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) + if label is not None: + self.label = Label(writer, row + height + 3, col, label) + self.radius = self.height // 2 + + def color(self, c=None): + self.fgcolor = self.bgcolor if c is None else c + self.show() + + def show(self): + super().show() + wri = self.writer + dev = self.device + r = self.radius + fillcircle(dev, self.col + r, self.row + r, r, self.fgcolor) + if isinstance(self.bdcolor, int): + circle(dev, self.col + r, self.row + r, r, self.bdcolor) diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py new file mode 100644 index 0000000..5261673 --- /dev/null +++ b/gui/widgets/meter.py @@ -0,0 +1,63 @@ +# meter.py Meter class for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +from gui.core.nanogui import DObject +from gui.widgets.label import Label + + +class Meter(DObject): + BAR = 1 + LINE = 0 + def __init__(self, writer, row, col, *, height=50, width=10, + fgcolor=None, bgcolor=None, ptcolor=None, bdcolor=None, + divisions=5, label=None, style=0, legends=None, value=None): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + self.divisions = divisions + if label is not None: + Label(writer, row + height + 3, col, label) + self.style = style + self.legends = legends + self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor + self.value(value) + + def value(self, n=None, color=None): + if n is None: + return super().value() + n = super().value(min(1, max(0, n))) + if color is not None: + self.ptcolor = color + self.show() + return n + + def show(self): + super().show() # Draw or erase border + val = super().value() + wri = self.writer + dev = self.device + width = self.width + height = self.height + legends = self.legends + x0 = self.col + x1 = self.col + width + y0 = self.row + y1 = self.row + height + if self.divisions > 0: + dy = height / (self.divisions) # Tick marks + for tick in range(self.divisions + 1): + ypos = int(y0 + dy * tick) + dev.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) + + if legends is not None: # Legends + dy = 0 if len(legends) <= 1 else height / (len(legends) -1) + yl = y1 - wri.height / 2 # Start at bottom + for legend in legends: + Label(wri, int(yl), x1 + 4, legend) + yl -= dy + y = int(y1 - val * height) # y position of slider + if self.style == self.LINE: + dev.hline(x0, y, width, self.ptcolor) # Draw pointer + else: + w = width / 2 + dev.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) diff --git a/gui/widgets/scale.py b/gui/widgets/scale.py new file mode 100644 index 0000000..d2b8421 --- /dev/null +++ b/gui/widgets/scale.py @@ -0,0 +1,123 @@ +# scale.py Extension to nano-gui providing the Scale class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# Usage: +# from gui.widgets.scale import Scale + +# NEED print_left, get_stringsize + +from gui.core.nanogui import DObject +from gui.core.writer import Writer +from gui.core.colors import BLACK + +class Scale(DObject): + def __init__(self, writer, row, col, *, + ticks=200, legendcb=None, tickcb=None, + height=0, width=100, border=2, fgcolor=None, bgcolor=None, + pointercolor=None, fontcolor=None): + if ticks % 2: + raise ValueError('ticks arg must be divisible by 2') + self.ticks = ticks + self.tickcb = tickcb + def lcb(f): + return '{:3.1f}'.format(f) + self.legendcb = legendcb if legendcb is not None else lcb + bgcolor = BLACK if bgcolor is None else bgcolor + text_ht = writer.font.height() + ctrl_ht = 12 # Minimum height for ticks + min_ht = text_ht + 2 * border + 2 # Ht of text, borders and gap between text and ticks + if height < min_ht + ctrl_ht: + height = min_ht + ctrl_ht # min workable height + else: + ctrl_ht = height - min_ht # adjust ticks for greater height + width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, fgcolor) + self.x0 = col + border + self.x1 = col + self.width - border + self.y0 = row + border + self.y1 = row + self.height - border + self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor + # Define tick dimensions + ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap) + ycl = ytop + (self.y1 - ytop) // 2 # Centre line + self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick. + self.sdy0 = ycl - self.sdl // 2 + self.mdl = round(ctrl_ht * 2 / 3) # Medium tick + self.mdy0 = ycl - self.mdl // 2 + self.ldl = ctrl_ht # Large tick + self.ldy0 = ycl - self.ldl // 2 + + def show(self): + wri = self.writer + dev = self.device + x0: int = self.x0 # Internal rectangle occupied by scale and text + x1: int = self.x1 + y0: int = self.y0 + y1: int = self.y1 + dev.fill_rect(x0, y0, x1 - x0, y1 - y0, self.bgcolor) + super().show() + # Scale is drawn using ints. Each division is 10 units. + val: int = self._value # 0..ticks*10 + # iv increments for each tick. Its value modulo N determines tick length + iv: int # val / 10 at a tick position + d: int # val % 10: offset relative to a tick position + fx: int # X offset of current tick in value units + if val >= 100: # Whole LHS of scale will be drawn + iv, d = divmod(val - 100, 10) # Initial value + fx = 10 - d + iv += 1 + else: # Scale will scroll right + iv = 0 + fx = 100 - val + + # Window shows 20 divisions, each of which corresponds to 10 units of value. + # So pixels per unit value == win_width/200 + win_width: int = x1 - x0 + ticks: int = self.ticks # Total # of ticks visible and hidden + while True: + x: int = x0 + (fx * win_width) // 200 # Current X position + ys: int # Start Y position for tick + yl: int # tick length + if x > x1 or iv > ticks: # Out of space or data (scroll left) + break + if not iv % 10: + txt = self.legendcb(self._fvalue(iv * 10)) + tlen = wri.stringlen(txt) + Writer.set_textpos(dev, y0, min(x, x1 - tlen)) + wri.setcolor(self.fgcolor, self.bgcolor) + wri.printstring(txt) + wri.setcolor() + ys = self.ldy0 # Large tick + yl = self.ldl + elif not iv % 5: + ys = self.mdy0 + yl = self.mdl + else: + ys = self.sdy0 + yl = self.sdl + if self.tickcb is None: + color = self.fgcolor + else: + color = self.tickcb(self._fvalue(iv * 10), self.fgcolor) + dev.vline(x, ys, yl, color) # Draw tick + fx += 10 + iv += 1 + + dev.vline(x0 + (x1 - x0) // 2, y0, y1 - y0, self.ptrcolor) # Draw pointer + + def _to_int(self, v): + return round((v + 1.0) * self.ticks * 5) # 0..self.ticks*10 + + def _fvalue(self, v=None): + return v / (5 * self.ticks) - 1.0 + + def value(self, val=None): # User method to get or set value + if val is not None: + val = min(max(val, - 1.0), 1.0) + v = self._to_int(val) + if v != self._value: + self._value = v + self.show() + return self._fvalue(self._value) diff --git a/ssd1351_setup.py b/ssd1351_setup.py new file mode 100644 index 0000000..28dfbac --- /dev/null +++ b/ssd1351_setup.py @@ -0,0 +1,34 @@ +# ssd1351_setup.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING (Adafruit pin nos and names) +# Pyb SSD +# 3v3 Vin (10) +# Gnd Gnd (11) +# Y1 DC (3 DC) +# Y2 CS (5 OC OLEDCS) +# Y3 Rst (4 R RESET) +# Y6 CLK (2 CL SCK) +# Y8 DATA (1 SI MOSI) + +height = 96 # 1.27 inch 96*128 (rows*cols) display + +import machine +import gc +from drivers.ssd1351.ssd1351 import SSD1351 as SSD + +height = 96 # 1.27 inch 96*128 (rows*cols) display +# height = 128 # 1.5 inch 128*128 display + +pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(2) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance