├── LICENSE ├── README.md ├── SETUP.md ├── TOUCHPAD.md ├── drivers ├── boolpalette.py ├── gc9a01 │ ├── gc9a01.py │ ├── gc9a01_16_bit.py │ └── gc9a01_8_bit.py ├── ili93xx │ ├── ili9341.py │ └── ili9341_8bit.py ├── ili94xx │ ├── ili9486.py │ └── ili9488.py ├── ssd1306 │ └── ssd1306.py ├── ssd1331 │ ├── __init__.py │ ├── ssd1331.py │ ├── ssd1331_16bit.py │ └── test_colors.py ├── ssd1351 │ ├── __init__.py │ ├── package.json │ ├── ssd1351.py │ ├── ssd1351_16bit.py │ ├── ssd1351_4bit.py │ ├── ssd1351_generic.py │ ├── test128_row.py │ ├── test96_row.py │ └── test_colors_96.py ├── st7567s │ └── st7567s.py ├── st7735r │ ├── st7735r.py │ ├── st7735r144.py │ ├── st7735r144_4bit.py │ └── st7735r_4bit.py └── st7789 │ ├── st7789_4bit.py │ └── st7789_8bit.py ├── gui ├── core │ ├── colors.py │ ├── tgui.py │ └── writer.py ├── demos │ ├── aclock.py │ ├── active.py │ ├── audio.py │ ├── bitmap.py │ ├── calendar.py │ ├── checkbox.py │ ├── date.py │ ├── dialog.py │ ├── dropdown.py │ ├── dropdown_var.py │ ├── dropdown_var_tuple.py │ ├── keyboard.py │ ├── linked_sliders.py │ ├── listbox.py │ ├── listbox_var.py │ ├── menu.py │ ├── plot.py │ ├── primitives.py │ ├── qrcode.py │ ├── refresh_lock.py │ ├── round.py │ ├── screen_change.py │ ├── screen_replace.py │ ├── screens.py │ ├── simple.py │ ├── slider.py │ ├── slider_label.py │ ├── tbox.py │ ├── tstat.py │ ├── various.py │ └── vtest.py ├── fonts │ ├── arial10.py │ ├── arial35.py │ ├── arial_50.py │ ├── courier20.py │ ├── font10.py │ ├── font14.py │ ├── font6.py │ ├── freesans20.py │ └── icons.py └── widgets │ ├── __init__.py │ ├── bitmap.py │ ├── buttons.py │ ├── checkbox.py │ ├── dial.py │ ├── dialog.py │ ├── dropdown.py │ ├── graph.py │ ├── grid.py │ ├── knob.py │ ├── label.py │ ├── led.py │ ├── listbox.py │ ├── menu.py │ ├── meter.py │ ├── pad.py │ ├── parse2d.py │ ├── qrcode.py │ ├── region.py │ ├── scale.py │ ├── scale_log.py │ ├── sliders.py │ └── textbox.py ├── images ├── bernoulli.png ├── big_display.jpg ├── bitmap.JPG ├── calendar.JPG ├── cartesian.png ├── checkbox.JPG ├── chess.JPG ├── closebutton.JPG ├── dd_closed.JPG ├── dd_open.JPG ├── dial.JPG ├── dial1.JPG ├── dialog.JPG ├── filesystem.png ├── grid.JPG ├── iconbuttons.jpg ├── keyboard.JPG ├── knob.JPG ├── label.JPG ├── led.JPG ├── lissajous.png ├── listbox.JPG ├── log_scale.JPG ├── me.ppm ├── menu.JPG ├── meter.JPG ├── polar.png ├── pushbuttons.JPG ├── qrcode.JPG ├── radiobuttons.JPG ├── rp2_test_fixture.JPG ├── rp2_tsc2007.JPG ├── scale.JPG ├── sine.png ├── sliders.JPG ├── textbox.JPG └── tstat.JPG ├── optional ├── bitmaps │ ├── m00 │ ├── m01 │ ├── m02 │ └── m03 ├── chess │ ├── CHESS.md │ ├── chess_font.py │ └── chess_game.py ├── mqtt │ ├── MQTT_DEMO.md │ ├── mqtt.py │ ├── mqtt_as.py │ └── pubtest └── py │ └── uQR.py ├── package.json ├── setup_examples ├── gc9a01_ws_rp2040_touch.py ├── ili9341_cst820_esp32_2432S024c.py ├── ili9341_ft6206_pico.py ├── ili9341_tsc2007_pico.py ├── ili9341_xpt2046_esp32.py ├── ili9341_xpt2046_pico.py ├── ili9488_ws_pico_res_touch_pico.py └── st7789_ws_pico_res_touch_pico.py └── touch ├── check.py ├── cst816s.py ├── cst820.py ├── ft6206.py ├── setup.py ├── touch.py ├── tsc2007.py └── xpt2046.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Peter Hinch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /drivers/boolpalette.py: -------------------------------------------------------------------------------- 1 | # boolpalette.py Implement BoolPalette class 2 | # This is a 2-value color palette for rendering monochrome glyphs to color 3 | # FrameBuffer instances. Supports destinations with up to 16 bit color. 4 | 5 | # Copyright (c) Peter Hinch 2021 6 | # Released under the MIT license see LICENSE 7 | 8 | import framebuf 9 | 10 | class BoolPalette(framebuf.FrameBuffer): 11 | 12 | def __init__(self, mode): 13 | buf = bytearray(4) # OK for <= 16 bit color 14 | super().__init__(buf, 2, 1, mode) 15 | 16 | def fg(self, color): # Set foreground color 17 | self.pixel(1, 0, color) 18 | 19 | def bg(self, color): 20 | self.pixel(0, 0, color) 21 | -------------------------------------------------------------------------------- /drivers/ssd1306/ssd1306.py: -------------------------------------------------------------------------------- 1 | # MicroPython SSD1306 OLED driver, I2C and SPI interfaces 2 | 3 | from micropython import const 4 | import framebuf 5 | from drivers.boolpalette import BoolPalette 6 | 7 | # register definitions 8 | SET_CONTRAST = const(0x81) 9 | SET_ENTIRE_ON = const(0xA4) 10 | SET_NORM_INV = const(0xA6) 11 | SET_DISP = const(0xAE) 12 | SET_MEM_ADDR = const(0x20) 13 | SET_COL_ADDR = const(0x21) 14 | SET_PAGE_ADDR = const(0x22) 15 | SET_DISP_START_LINE = const(0x40) 16 | SET_SEG_REMAP = const(0xA0) 17 | SET_MUX_RATIO = const(0xA8) 18 | SET_COM_OUT_DIR = const(0xC0) 19 | SET_DISP_OFFSET = const(0xD3) 20 | SET_COM_PIN_CFG = const(0xDA) 21 | SET_DISP_CLK_DIV = const(0xD5) 22 | SET_PRECHARGE = const(0xD9) 23 | SET_VCOM_DESEL = const(0xDB) 24 | SET_CHARGE_PUMP = const(0x8D) 25 | 26 | # Subclassing FrameBuffer provides support for graphics primitives 27 | # http://docs.micropython.org/en/latest/pyboard/library/framebuf.html 28 | class SSD1306(framebuf.FrameBuffer): 29 | @staticmethod 30 | def rgb(r, g, b): # Color compatibility 31 | return int((r > 127) or (g > 127) or (b > 127)) 32 | 33 | def __init__(self, width, height, external_vcc): 34 | self.width = width 35 | self.height = height 36 | self.external_vcc = external_vcc 37 | self.pages = self.height // 8 38 | self.buffer = bytearray(self.pages * self.width) 39 | mode = framebuf.MONO_VLSB 40 | self.palette = BoolPalette(mode) # Ensure color compatibility 41 | super().__init__(self.buffer, self.width, self.height, mode) 42 | self.init_display() 43 | 44 | def init_display(self): 45 | for cmd in ( 46 | SET_DISP | 0x00, # off 47 | # address setting 48 | SET_MEM_ADDR, 49 | 0x00, # horizontal 50 | # resolution and layout 51 | SET_DISP_START_LINE | 0x00, 52 | SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 53 | SET_MUX_RATIO, 54 | self.height - 1, 55 | SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 56 | SET_DISP_OFFSET, 57 | 0x00, 58 | SET_COM_PIN_CFG, 59 | 0x02 if self.width > 2 * self.height else 0x12, 60 | # timing and driving scheme 61 | SET_DISP_CLK_DIV, 62 | 0x80, 63 | SET_PRECHARGE, 64 | 0x22 if self.external_vcc else 0xF1, 65 | SET_VCOM_DESEL, 66 | 0x30, # 0.83*Vcc 67 | # display 68 | SET_CONTRAST, 69 | 0xFF, # maximum 70 | SET_ENTIRE_ON, # output follows RAM contents 71 | SET_NORM_INV, # not inverted 72 | # charge pump 73 | SET_CHARGE_PUMP, 74 | 0x10 if self.external_vcc else 0x14, 75 | SET_DISP | 0x01, 76 | ): # on 77 | self.write_cmd(cmd) 78 | self.fill(0) 79 | self.show() 80 | 81 | def poweroff(self): 82 | self.write_cmd(SET_DISP | 0x00) 83 | 84 | def poweron(self): 85 | self.write_cmd(SET_DISP | 0x01) 86 | 87 | def contrast(self, contrast): 88 | self.write_cmd(SET_CONTRAST) 89 | self.write_cmd(contrast) 90 | 91 | def invert(self, invert): 92 | self.write_cmd(SET_NORM_INV | (invert & 1)) 93 | 94 | def show(self): 95 | x0 = 0 96 | x1 = self.width - 1 97 | if self.width == 64: 98 | # displays with width of 64 pixels are shifted by 32 99 | x0 += 32 100 | x1 += 32 101 | self.write_cmd(SET_COL_ADDR) 102 | self.write_cmd(x0) 103 | self.write_cmd(x1) 104 | self.write_cmd(SET_PAGE_ADDR) 105 | self.write_cmd(0) 106 | self.write_cmd(self.pages - 1) 107 | self.write_data(self.buffer) 108 | 109 | 110 | class SSD1306_I2C(SSD1306): 111 | def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False): 112 | self.i2c = i2c 113 | self.addr = addr 114 | self.temp = bytearray(2) 115 | self.write_list = [b"\x40", None] # Co=0, D/C#=1 116 | super().__init__(width, height, external_vcc) 117 | 118 | def write_cmd(self, cmd): 119 | self.temp[0] = 0x80 # Co=1, D/C#=0 120 | self.temp[1] = cmd 121 | self.i2c.writeto(self.addr, self.temp) 122 | 123 | def write_data(self, buf): 124 | self.write_list[1] = buf 125 | self.i2c.writevto(self.addr, self.write_list) 126 | 127 | 128 | class SSD1306_SPI(SSD1306): 129 | def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): 130 | self.rate = 10 * 1024 * 1024 131 | dc.init(dc.OUT, value=0) 132 | res.init(res.OUT, value=0) 133 | cs.init(cs.OUT, value=1) 134 | self.spi = spi 135 | self.dc = dc 136 | self.res = res 137 | self.cs = cs 138 | import time 139 | 140 | self.res(1) 141 | time.sleep_ms(1) 142 | self.res(0) 143 | time.sleep_ms(10) 144 | self.res(1) 145 | super().__init__(width, height, external_vcc) 146 | 147 | def write_cmd(self, cmd): 148 | self.spi.init(baudrate=self.rate, polarity=0, phase=0) 149 | self.cs(1) 150 | self.dc(0) 151 | self.cs(0) 152 | self.spi.write(bytearray([cmd])) 153 | self.cs(1) 154 | 155 | def write_data(self, buf): 156 | self.spi.init(baudrate=self.rate, polarity=0, phase=0) 157 | self.cs(1) 158 | self.dc(1) 159 | self.cs(0) 160 | self.spi.write(buf) 161 | self.cs(1) 162 | -------------------------------------------------------------------------------- /drivers/ssd1331/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/drivers/ssd1331/__init__.py -------------------------------------------------------------------------------- /drivers/ssd1331/ssd1331.py: -------------------------------------------------------------------------------- 1 | # SSD1331.py MicroPython driver for Adafruit 0.96" OLED display 2 | # https://www.adafruit.com/product/684 3 | 4 | # The MIT License (MIT) 5 | 6 | # Copyright (c) Peter Hinch 2018-2020 7 | # Released under the MIT license see LICENSE 8 | 9 | # Show command 10 | # 0x15, 0, 0x5f, 0x75, 0, 0x3f Col 0-95 row 0-63 11 | 12 | # Initialisation command 13 | # 0xae display off (sleep mode) 14 | # 0xa0, 0x32 256 color RGB, horizontal RAM increment 15 | # 0xa1, 0x00 Startline row 0 16 | # 0xa2, 0x00 Vertical offset 0 17 | # 0xa4 Normal display 18 | # 0xa8, 0x3f Set multiplex ratio 19 | # 0xad, 0x8e Ext supply 20 | # 0xb0, 0x0b Disable power save mode 21 | # 0xb1, 0x31 Phase period 22 | # 0xb3, 0xf0 Oscillator frequency 23 | # 0x8a, 0x64, 0x8b, 0x78, 0x8c, 0x64, # Precharge 24 | # 0xbb, 0x3a Precharge voltge 25 | # 0xbe, 0x3e COM deselect level 26 | # 0x87, 0x06 master current attenuation factor 27 | # 0x81, 0x91 contrast for all color "A" segment 28 | # 0x82, 0x50 contrast for all color "B" segment 29 | # 0x83, 0x7d contrast for all color "C" segment 30 | # 0xaf Display on 31 | 32 | import framebuf 33 | import utime 34 | import gc 35 | import sys 36 | from drivers.boolpalette import BoolPalette 37 | 38 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 39 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 40 | # Mode 0, 0 works on ESP and STM 41 | 42 | # Data sheet SPI spec: 150ns min clock period 6.66MHz 43 | 44 | class SSD1331(framebuf.FrameBuffer): 45 | # Convert r, g, b in range 0-255 to an 8 bit colour value 46 | # acceptable to hardware: rrrgggbb 47 | @staticmethod 48 | def rgb(r, g, b): 49 | return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) 50 | 51 | def __init__(self, spi, pincs, pindc, pinrs, height=64, width=96, init_spi=False): 52 | self._spi = spi 53 | self._pincs = pincs 54 | self._pindc = pindc # 1 = data 0 = cmd 55 | self.height = height # Required by Writer class 56 | self.width = width 57 | self._spi_init = init_spi 58 | mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. 59 | self.palette = BoolPalette(mode) 60 | gc.collect() 61 | self.buffer = bytearray(self.height * self.width) 62 | super().__init__(self.buffer, self.width, self.height, mode) 63 | pinrs(0) # Pulse the reset line 64 | utime.sleep_ms(1) 65 | pinrs(1) 66 | utime.sleep_ms(1) 67 | self._write(b'\xae\xa0\x32\xa1\x00\xa2\x00\xa4\xa8\x3f\xad\x8e\xb0'\ 68 | b'\x0b\xb1\x31\xb3\xf0\x8a\x64\x8b\x78\x8c\x64\xbb\x3a\xbe\x3e\x87'\ 69 | b'\x06\x81\x91\x82\x50\x83\x7d\xaf', 0) 70 | gc.collect() 71 | self.show() 72 | 73 | def _write(self, buf, dc): 74 | self._pincs(1) 75 | self._pindc(dc) 76 | self._pincs(0) 77 | self._spi.write(buf) 78 | self._pincs(1) 79 | 80 | def show(self, _cmd=b'\x15\x00\x5f\x75\x00\x3f'): # Pre-allocate 81 | if self._spi_init: # A callback was passed 82 | self._spi_init(spi) # Bus may be shared 83 | self._write(_cmd, 0) 84 | self._write(self.buffer, 1) 85 | -------------------------------------------------------------------------------- /drivers/ssd1331/ssd1331_16bit.py: -------------------------------------------------------------------------------- 1 | # SSD1331.py MicroPython driver for Adafruit 0.96" OLED display 2 | # https://www.adafruit.com/product/684 3 | 4 | # Copyright (c) Peter Hinch 2019-2020 5 | # Released under the MIT license see LICENSE 6 | 7 | # Show command 8 | # 0x15, 0, 0x5f, 0x75, 0, 0x3f Col 0-95 row 0-63 9 | 10 | # Initialisation command 11 | # 0xae display off (sleep mode) 12 | # 0xa0, 0x72 16 bit RGB, horizontal RAM increment 13 | # 0xa1, 0x00 Startline row 0 14 | # 0xa2, 0x00 Vertical offset 0 15 | # 0xa4 Normal display 16 | # 0xa8, 0x3f Set multiplex ratio 17 | # 0xad, 0x8e Ext supply 18 | # 0xb0, 0x0b Disable power save mode 19 | # 0xb1, 0x31 Phase period 20 | # 0xb3, 0xf0 Oscillator frequency 21 | # 0x8a, 0x64, 0x8b, 0x78, 0x8c, 0x64, # Precharge 22 | # 0xbb, 0x3a Precharge voltge 23 | # 0xbe, 0x3e COM deselect level 24 | # 0x87, 0x06 master current attenuation factor 25 | # 0x81, 0x91 contrast for all color "A" segment 26 | # 0x82, 0x50 contrast for all color "B" segment 27 | # 0x83, 0x7d contrast for all color "C" segment 28 | # 0xaf Display on 29 | 30 | import framebuf 31 | import utime 32 | import gc 33 | from drivers.boolpalette import BoolPalette 34 | 35 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 36 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 37 | # Mode 0, 0 works on ESP and STM 38 | 39 | # Data sheet SPI spec: 150ns min clock period 6.66MHz 40 | class SSD1331(framebuf.FrameBuffer): 41 | # Convert r, g, b in range 0-255 to a 16 bit colour value RGB565 42 | # acceptable to hardware: rrrrrggggggbbbbb 43 | # LS byte of 16 bit result is shifted out 1st 44 | @staticmethod 45 | def rgb(r, g, b): 46 | return ((b & 0xf8) << 5) | ((g & 0x1c) << 11) | (r & 0xf8) | ((g & 0xe0) >> 5) 47 | 48 | def __init__(self, spi, pincs, pindc, pinrs, height=64, width=96, init_spi=False): 49 | self._spi = spi 50 | self._pincs = pincs 51 | self._pindc = pindc # 1 = data 0 = cmd 52 | self.height = height # Required by Writer class 53 | self.width = width 54 | self._spi_init = init_spi 55 | mode = framebuf.RGB565 56 | self.palette = BoolPalette(mode) 57 | gc.collect() 58 | self.buffer = bytearray(self.height * self.width * 2) 59 | super().__init__(self.buffer, self.width, self.height, mode) 60 | pinrs(0) # Pulse the reset line 61 | utime.sleep_ms(1) 62 | pinrs(1) 63 | utime.sleep_ms(1) 64 | if self._spi_init: # A callback was passed 65 | self._spi_init(spi) # Bus may be shared 66 | self._write(b'\xae\xa0\x72\xa1\x00\xa2\x00\xa4\xa8\x3f\xad\x8e\xb0'\ 67 | b'\x0b\xb1\x31\xb3\xf0\x8a\x64\x8b\x78\x8c\x64\xbb\x3a\xbe\x3e\x87'\ 68 | b'\x06\x81\x91\x82\x50\x83\x7d\xaf', 0) 69 | gc.collect() 70 | self.show() 71 | 72 | def _write(self, buf, dc): 73 | self._pincs(1) 74 | self._pindc(dc) 75 | self._pincs(0) 76 | self._spi.write(buf) 77 | self._pincs(1) 78 | 79 | def show(self, _cmd=b'\x15\x00\x5f\x75\x00\x3f'): # Pre-allocate 80 | if self._spi_init: # A callback was passed 81 | self._spi_init(spi) # Bus may be shared 82 | self._write(_cmd, 0) 83 | self._write(self.buffer, 1) 84 | -------------------------------------------------------------------------------- /drivers/ssd1331/test_colors.py: -------------------------------------------------------------------------------- 1 | # test_colors.py Test color rendition on SSD1331 (Adafruit 0.96" OLED). 2 | # Author Peter Hinch 3 | # Released under the MIT licence. 4 | 5 | import machine 6 | # Can test either driver 7 | # from ssd1331 import SSD1331 as SSD 8 | from ssd1331_16bit import SSD1331 as SSD 9 | 10 | # Initialise hardware 11 | def setup(): 12 | pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) 13 | pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) 14 | prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) 15 | spi = machine.SPI(1) 16 | ssd = SSD(spi, pcs, pdc, prst) # Create a display instance 17 | return ssd 18 | 19 | ssd = setup() 20 | ssd.fill(0) 21 | # Operate in landscape mode 22 | x = 0 23 | for y in range(96): 24 | ssd.line(y, x, y, x+20, ssd.rgb(round(255*y/96), 0, 0)) 25 | x += 20 26 | for y in range(96): 27 | ssd.line(y, x, y, x+20, ssd.rgb(0, round(255*y/96), 0)) 28 | x += 20 29 | for y in range(96): 30 | ssd.line(y, x, y, x+20, ssd.rgb(0, 0, round(255*y/96))) 31 | ssd.show() 32 | -------------------------------------------------------------------------------- /drivers/ssd1351/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/drivers/ssd1351/__init__.py -------------------------------------------------------------------------------- /drivers/ssd1351/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["drivers/ssd1351/ssd1351.py", "github:peterhinch/micropython-nano-gui/drivers/ssd1351/ssd1351.py"], 4 | ["drivers/ssd1351/ssd1351_4bit.py", "github:peterhinch/micropython-nano-gui/drivers/ssd1351/ssd1351_4bit.py"], 5 | ["drivers/ssd1351/ssd1351_16bit.py", "github:peterhinch/micropython-nano-gui/drivers/ssd1351/ssd1351_16bit.py"], 6 | ["drivers/ssd1351/ssd1351_generic.py", "github:peterhinch/micropython-nano-gui/drivers/ssd1351/ssd1351_generic.py"], 7 | ["drivers/boolpalette.py", "github:peterhinch/micropython-nano-gui/drivers/boolpalette.py"] 8 | ], 9 | "version": "0.1" 10 | } 11 | -------------------------------------------------------------------------------- /drivers/ssd1351/ssd1351_16bit.py: -------------------------------------------------------------------------------- 1 | # SSD1351_16bit.py MicroPython driver for Adafruit color OLED displays. 2 | 3 | # Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 4 | # Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 5 | # For wiring details see drivers/ADAFRUIT.md in this repo. 6 | 7 | # This driver is based on the Adafruit C++ library for Arduino 8 | # https://github.com/adafruit/Adafruit-SSD1351-library.git 9 | 10 | # Copyright (c) Peter Hinch 2019-2020 11 | # Released under the MIT license see LICENSE 12 | 13 | import framebuf 14 | import utime 15 | import gc 16 | import micropython 17 | from drivers.boolpalette import BoolPalette 18 | 19 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 20 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 21 | # Now using 0,0 on STM and ESP32 22 | 23 | # Initialisation commands in cmd_init: 24 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 25 | # 0xae, # display off (sleep mode) 26 | # 0xb3, 0xf1, # clock div 27 | # 0xca, 0x7f, # mux ratio 28 | # 0xa0, 0x74, # setremap 0x74 29 | # 0x15, 0, 0x7f, # setcolumn 30 | # 0x75, 0, 0x7f, # setrow 31 | # 0xa1, 0, # set display start line 32 | # 0xa2, 0, # displayoffset 33 | # 0xb5, 0, # setgpio 34 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 35 | # 0xb1, 0x32, # Precharge 36 | # 0xbe, 0x05, # vcommh 37 | # 0xa6, # normaldisplay 38 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 39 | # 0xc7, 0x0f, # Master contrast 40 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 41 | # 0xb6, 1, # Precharge 2 42 | # 0xaf, # Display on 43 | 44 | 45 | class SSD1351(framebuf.FrameBuffer): 46 | # Convert r, g, b in range 0-255 to a 16 bit colour value RGB565 47 | # acceptable to hardware: rrrrrggggggbbbbb 48 | @staticmethod 49 | def rgb(r, g, b): 50 | return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5) 51 | 52 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 53 | if height not in (96, 128): 54 | raise ValueError('Unsupported height {}'.format(height)) 55 | self.spi = spi 56 | self.spi_init = init_spi 57 | self.pincs = pincs 58 | self.pindc = pindc # 1 = data 0 = cmd 59 | self.height = height # Required by Writer class 60 | self.width = width 61 | mode = framebuf.RGB565 62 | self.palette = BoolPalette(mode) 63 | gc.collect() 64 | self.buffer = bytearray(self.height * self.width * 2) 65 | super().__init__(self.buffer, self.width, self.height, mode) 66 | self.mvb = memoryview(self.buffer) 67 | pinrs(0) # Pulse the reset line 68 | utime.sleep_ms(1) 69 | pinrs(1) 70 | utime.sleep_ms(1) 71 | if self.spi_init: # A callback was passed 72 | self.spi_init(spi) # Bus may be shared 73 | # See above comment to explain this allocation-saving gibberish. 74 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 75 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 76 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 77 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 78 | self.show() 79 | gc.collect() 80 | 81 | def _write(self, mv, dc): 82 | self.pincs(1) 83 | self.pindc(dc) 84 | self.pincs(0) 85 | self.spi.write(bytes(mv)) 86 | self.pincs(1) 87 | 88 | # Write lines from the framebuf out of order to match the mapping of the 89 | # SSD1351 RAM to the OLED device. 90 | def show(self): 91 | mvb = self.mvb 92 | bw = self.width * 2 # Width in bytes 93 | if self.spi_init: # A callback was passed 94 | self.spi_init(self.spi) # Bus may be shared 95 | self._write(b'\x5c', 0) # Enable data write 96 | if self.height == 128: 97 | for l in range(128): 98 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126 .. 96 99 | start = l0 * self.width * 2 100 | self._write(mvb[start : start + bw], 1) # Send a line 101 | else: 102 | for l in range(128): 103 | if l < 64: 104 | start = (63 -l) * self.width * 2 # 63 62 .. 1 0 105 | elif l < 96: 106 | start = 0 107 | else: 108 | start = (191 - l) * self.width * 2 # 127 126 .. 95 109 | self._write(mvb[start : start + bw], 1) # Send a line 110 | -------------------------------------------------------------------------------- /drivers/ssd1351/ssd1351_generic.py: -------------------------------------------------------------------------------- 1 | # SSD1351_generic.py MicroPython driver for Adafruit color OLED displays. 2 | # This is cross-platform. It lacks STM optimisations and is slower than the 3 | # standard version. 4 | 5 | # Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 6 | # Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 7 | # For wiring details see drivers/ADAFRUIT.md in this repo. 8 | 9 | # This driver is based on the Adafruit C++ library for Arduino 10 | # https://github.com/adafruit/Adafruit-SSD1351-library.git 11 | 12 | # Copyright (c) Peter Hinch 2018-2020 13 | # Released under the MIT license see LICENSE 14 | 15 | import framebuf 16 | import utime 17 | import gc 18 | import micropython 19 | from drivers.boolpalette import BoolPalette 20 | 21 | import sys 22 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 23 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 24 | # Now using 0,0 on STM and ESP32 25 | 26 | # Timings with standard emitter 27 | # 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms 28 | # Buffer transfer time = 272-240 = 32ms which accords with expected: 29 | # 128*128*2/10500000 = 31.2ms (2 bytes/pixel, baudrate = 10.5MHz) 30 | # With viper emitter show() takes 47ms vs 41ms for assembler. 31 | 32 | @micropython.viper 33 | def _lcopy(dest:ptr8, source:ptr8, length:int): 34 | n = 0 35 | for x in range(length): 36 | c = source[x] 37 | dest[n] = ((c & 3) << 6) | ((c & 0x1c) >> 2) # Blue green 38 | n += 1 39 | dest[n] = (c & 0xe0) >> 3 # Red 40 | n += 1 41 | 42 | # Initialisation commands in cmd_init: 43 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 44 | # 0xae, # display off (sleep mode) 45 | # 0xb3, 0xf1, # clock div 46 | # 0xca, 0x7f, # mux ratio 47 | # 0xa0, 0x74, # setremap 0x74 48 | # 0x15, 0, 0x7f, # setcolumn 49 | # 0x75, 0, 0x7f, # setrow 50 | # 0xa1, 0, # set display start line 51 | # 0xa2, 0, # displayoffset 52 | # 0xb5, 0, # setgpio 53 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 54 | # 0xb1, 0x32, # Precharge 55 | # 0xbe, 0x05, # vcommh 56 | # 0xa6, # normaldisplay 57 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 58 | # 0xc7, 0x0f, # Master contrast 59 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 60 | # 0xb6, 1, # Precharge 2 61 | # 0xaf, # Display on 62 | 63 | class SSD1351(framebuf.FrameBuffer): 64 | # Convert r, g, b in range 0-255 to an 8 bit colour value 65 | # acceptable to hardware: rrrgggbb 66 | @staticmethod 67 | def rgb(r, g, b): 68 | return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) 69 | 70 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 71 | if height not in (96, 128): 72 | raise ValueError('Unsupported height {}'.format(height)) 73 | self.spi = spi 74 | self.spi_init = init_spi 75 | self.pincs = pincs 76 | self.pindc = pindc # 1 = data 0 = cmd 77 | self.height = height # Required by Writer class 78 | self.width = width 79 | mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. 80 | self.palette = BoolPalette(mode) 81 | gc.collect() 82 | self.buffer = bytearray(self.height * self.width) 83 | super().__init__(self.buffer, self.width, self.height, mode) 84 | self.linebuf = bytearray(self.width * 2) 85 | pinrs(0) # Pulse the reset line 86 | utime.sleep_ms(1) 87 | pinrs(1) 88 | utime.sleep_ms(1) 89 | if self.spi_init: # A callback was passed 90 | self.spi_init(spi) # Bus may be shared 91 | # See above comment to explain this allocation-saving gibberish. 92 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 93 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 94 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 95 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 96 | gc.collect() 97 | self.show() 98 | 99 | def _write(self, buf, dc): 100 | self.pincs(1) 101 | self.pindc(dc) 102 | self.pincs(0) 103 | self.spi.write(buf) 104 | self.pincs(1) 105 | 106 | # Write lines from the framebuf out of order to match the mapping of the 107 | # SSD1351 RAM to the OLED device. 108 | def show(self): 109 | lb = self.linebuf 110 | buf = memoryview(self.buffer) 111 | if self.spi_init: # A callback was passed 112 | self.spi_init(self.spi) # Bus may be shared 113 | self._write(b'\x5c', 0) # Enable data write 114 | if self.height == 128: 115 | for l in range(128): 116 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126... 117 | start = l0 * self.width 118 | _lcopy(lb, buf[start : start + self.width], self.width) 119 | self._write(lb, 1) # Send a line 120 | else: 121 | for l in range(128): 122 | if l < 64: 123 | start = (63 -l) * self.width 124 | _lcopy(lb, buf[start : start + self.width], self.width) 125 | self._write(lb, 1) # Send a line 126 | elif l < 96: # This is daft but I can't get setrow to work 127 | self._write(lb, 1) # Let RAM counter increase 128 | else: 129 | start = (191 - l) * self.width 130 | _lcopy(lb, buf[start : start + self.width], self.width) 131 | self._write(lb, 1) # Send a line 132 | 133 | -------------------------------------------------------------------------------- /drivers/ssd1351/test128_row.py: -------------------------------------------------------------------------------- 1 | # test128_row.py Test for device driver on 96 row display 2 | import machine 3 | from ssd1351 import SSD1351 as SSD 4 | 5 | # Initialise hardware 6 | def setup(): 7 | pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) 8 | pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) 9 | prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) 10 | spi = machine.SPI(1) 11 | ssd = SSD(spi, pcs, pdc, prst) # Create a display instance 12 | return ssd 13 | 14 | ssd = setup() 15 | ssd.fill(0) 16 | ssd.line(0, 0, 127, 127, ssd.rgb(0, 255, 0)) 17 | ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) 18 | ssd.show() 19 | -------------------------------------------------------------------------------- /drivers/ssd1351/test96_row.py: -------------------------------------------------------------------------------- 1 | # test96.py Test for device driver on 96 row display 2 | import machine 3 | from ssd1351 import SSD1351 as SSD 4 | 5 | # Initialise hardware 6 | def setup(): 7 | pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) 8 | pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) 9 | prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) 10 | spi = machine.SPI(1) 11 | ssd = SSD(spi, pcs, pdc, prst, height=96) # Create a display instance 12 | return ssd 13 | 14 | ssd = setup() 15 | ssd.fill(0) 16 | ssd.line(0, 0, 127, 95, ssd.rgb(0, 255, 0)) 17 | ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) 18 | ssd.show() 19 | -------------------------------------------------------------------------------- /drivers/ssd1351/test_colors_96.py: -------------------------------------------------------------------------------- 1 | # test_colors_96.py 2 | # Check color mapping. Runs on 96 row display (change height for 128 row) 3 | 4 | import machine 5 | from ssd1351_16bit import SSD1351 as SSD 6 | 7 | # Initialise hardware 8 | def setup(): 9 | pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) 10 | pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) 11 | prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) 12 | spi = machine.SPI(1) 13 | ssd = SSD(spi, pcs, pdc, prst, height=96) # Create a display instance 14 | return ssd 15 | 16 | ssd = setup() 17 | ssd.fill(0) 18 | x = 0 19 | for y in range(128): 20 | ssd.line(y, x, y, x+20, ssd.rgb(round(255*y/128), 0, 0)) 21 | x += 20 22 | for y in range(128): 23 | ssd.line(y, x, y, x+20, ssd.rgb(0, round(255*y/128), 0)) 24 | x += 20 25 | for y in range(128): 26 | ssd.line(y, x, y, x+20, ssd.rgb(0, 0, round(255*y/128))) 27 | ssd.show() 28 | -------------------------------------------------------------------------------- /drivers/st7567s/st7567s.py: -------------------------------------------------------------------------------- 1 | # st7567s.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | 5 | # Driver contributed by Enrico Rossini @EnricoRoss98 6 | # https://github.com/peterhinch/micropython-nano-gui/pull/57 7 | 8 | from micropython import const 9 | from gui.drivers.boolpalette import BoolPalette 10 | from time import sleep_ms 11 | import framebuf 12 | 13 | # LCD Commands definition 14 | _CMD_DISPLAY_ON = const(0xAF) 15 | _CMD_DISPLAY_OFF = const(0xAE) 16 | _CMD_SET_START_LINE = const(0x40) 17 | _CMD_SET_PAGE = const(0xB0) 18 | _CMD_COLUMN_UPPER = const(0x10) 19 | _CMD_COLUMN_LOWER = const(0x00) 20 | _CMD_SET_ADC_NORMAL = const(0xA0) 21 | _CMD_SET_ADC_REVERSE = const(0xA1) 22 | _CMD_SET_COL_NORMAL = const(0xC0) 23 | _CMD_SET_COL_REVERSE = const(0xC8) 24 | _CMD_SET_DISPLAY_NORMAL = const(0xA6) 25 | _CMD_SET_DISPLAY_REVERSE = const(0xA7) 26 | _CMD_SET_ALLPX_ON = const(0xA5) 27 | _CMD_SET_ALLPX_NORMAL = const(0xA4) 28 | _CMD_SET_BIAS_9 = const(0xA2) 29 | _CMD_SET_BIAS_7 = const(0xA3) 30 | _CMD_DISPLAY_RESET = const(0xE2) 31 | _CMD_NOP = const(0xE3) 32 | _CMD_TEST = const(0xF0) # Exit this mode with _CMD_NOP 33 | _CMD_SET_POWER = const(0x28) 34 | _CMD_SET_RESISTOR_RATIO = const(0x20) 35 | _CMD_SET_VOLUME = const(0x81) 36 | 37 | # Display parameters 38 | _DISPLAY_W = const(128) 39 | _DISPLAY_H = const(64) 40 | _DISPLAY_CONTRAST = const(0x1B) 41 | _DISPLAY_RESISTOR_RATIO = const(5) 42 | _DISPLAY_POWER_MODE = 7 43 | 44 | 45 | class ST7567(framebuf.FrameBuffer): 46 | @staticmethod 47 | def rgb(r, g, b): 48 | return min((r + g + b) >> 7, 3) # Greyscale in range 0 <= gs <= 3 49 | 50 | def __init__(self, width, height, i2c, addr=0x3F, external_vcc=False): 51 | self.i2c = i2c 52 | self.addr = addr 53 | self.temp = bytearray(2) 54 | self.write_list = [b"\x40", None] # Co=0, D/C#=1 55 | self.width = width 56 | self.height = height 57 | self.external_vcc = external_vcc 58 | self.pages = self.height // 8 59 | self.buffer = bytearray(self.pages * self.width) 60 | mode = framebuf.MONO_VLSB 61 | self.palette = BoolPalette(mode) # Ensure color compatibility 62 | super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) 63 | self.display_init() 64 | 65 | def display_init(self): 66 | self.write_cmd(_CMD_DISPLAY_RESET) 67 | sleep_ms(1) 68 | for cmd in ( 69 | _CMD_DISPLAY_OFF, # Display off 70 | _CMD_SET_BIAS_9, # Display drive voltage 1/9 bias 71 | _CMD_SET_ADC_NORMAL, # Normal SEG 72 | _CMD_SET_COL_REVERSE, # Commmon mode reverse direction 73 | _CMD_SET_RESISTOR_RATIO + _DISPLAY_RESISTOR_RATIO, # V5 R ratio 74 | _CMD_SET_VOLUME, # Contrast 75 | _DISPLAY_CONTRAST, # Contrast value 76 | _CMD_SET_POWER + _DISPLAY_POWER_MODE): 77 | self.write_cmd(cmd) 78 | 79 | self.show() 80 | self.write_cmd(_CMD_DISPLAY_ON) 81 | 82 | def set_contrast(self, value): 83 | if (0x1 <= value <= 0x3f): 84 | for cmd in (_CMD_SET_VOLUME, value): 85 | self.write_cmd(cmd) 86 | 87 | def show(self): 88 | for i in range(8): 89 | for cmd in ( 90 | _CMD_SET_START_LINE, 91 | _CMD_SET_PAGE + i, 92 | _CMD_COLUMN_UPPER, 93 | _CMD_COLUMN_LOWER): 94 | self.write_cmd(cmd) 95 | self.write_data(self.buffer[i * 128:(i + 1) * 128]) 96 | 97 | def write_cmd(self, cmd): 98 | self.temp[0] = 0x80 # Co=1, D/C#=0 99 | self.temp[1] = cmd 100 | self.i2c.writeto(self.addr, self.temp) 101 | 102 | def write_data(self, buf): 103 | self.write_list[1] = buf 104 | self.i2c.writevto(self.addr, self.write_list) 105 | -------------------------------------------------------------------------------- /gui/core/colors.py: -------------------------------------------------------------------------------- 1 | # colors.py Micropython GUI library for TFT displays: colors and shapes 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2024 Peter Hinch 5 | from touch_setup import SSD 6 | from gui.core.writer import CWriter 7 | 8 | # Code can be portable between 4-bit and other drivers by calling create_color 9 | def create_color(idx, r, g, b): 10 | return CWriter.create_color(SSD, idx, r, g, b) 11 | 12 | 13 | if hasattr(SSD, "lut"): # Colors defined by LUT 14 | BLACK = create_color(0, 0, 0, 0) 15 | GREEN = create_color(1, 0, 255, 0) 16 | RED = create_color(2, 255, 0, 0) 17 | LIGHTRED = create_color(3, 140, 0, 0) 18 | BLUE = create_color(4, 0, 0, 255) 19 | YELLOW = create_color(5, 255, 255, 0) 20 | GREY = create_color(6, 100, 100, 100) 21 | MAGENTA = create_color(7, 255, 0, 255) 22 | CYAN = create_color(8, 0, 255, 255) 23 | LIGHTGREEN = create_color(9, 0, 100, 0) 24 | DARKGREEN = create_color(10, 0, 80, 0) 25 | DARKBLUE = create_color(11, 0, 0, 90) 26 | # 12, 13, 14 free for user definition 27 | WHITE = create_color(15, 255, 255, 255) 28 | else: 29 | BLACK = SSD.rgb(0, 0, 0) 30 | GREEN = SSD.rgb(0, 255, 0) 31 | RED = SSD.rgb(255, 0, 0) 32 | LIGHTRED = SSD.rgb(140, 0, 0) 33 | BLUE = SSD.rgb(0, 0, 255) 34 | YELLOW = SSD.rgb(255, 255, 0) 35 | GREY = SSD.rgb(100, 100, 100) 36 | MAGENTA = SSD.rgb(255, 0, 255) 37 | CYAN = SSD.rgb(0, 255, 255) 38 | LIGHTGREEN = SSD.rgb(0, 100, 0) 39 | DARKGREEN = SSD.rgb(0, 80, 0) 40 | DARKBLUE = SSD.rgb(0, 0, 90) 41 | WHITE = SSD.rgb(255, 255, 255) 42 | 43 | CIRCLE = 1 44 | RECTANGLE = 2 45 | CLIPPED_RECT = 3 46 | 47 | # Accommodate nonstandard display hardware. Defaults are for TFT. 48 | FG = 0 49 | BG = 1 # Display background color (normally black) 50 | GREY_OUT = 2 # Greyed out color 51 | color_map = [WHITE, BLACK, GREY] 52 | -------------------------------------------------------------------------------- /gui/demos/aclock.py: -------------------------------------------------------------------------------- 1 | # aclock.py micropython-touch analog clock demo. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Initialise hardware and framebuf before importing modules. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | from gui.widgets import Label, Dial, Pointer, CloseButton 10 | 11 | # Now import other modules 12 | from cmath import rect, pi 13 | import asyncio 14 | import time 15 | from gui.core.writer import CWriter 16 | 17 | # Font for CWriter 18 | import gui.fonts.freesans20 as font 19 | from gui.core.colors import * 20 | 21 | 22 | # Adjust passed Dial and Label instances to show current time and date. 23 | async def aclock(dial, lbldate, lbltim): 24 | # Return a unit vector of phase phi. Multiplying by this will 25 | # rotate a vector anticlockwise which is mathematically correct. 26 | # Alas clocks, modelled on sundials, were invented in the northern 27 | # hemisphere. Otherwise they would have rotated widdershins like 28 | # the maths. Hence negative sign when called. 29 | def uv(phi): 30 | return rect(1, phi) 31 | 32 | def suffix(n): 33 | if n in (1, 21, 31): 34 | return "st" 35 | if n in (2, 22): 36 | return "nd" 37 | if n in (3, 23): 38 | return "rd" 39 | return "th" 40 | 41 | days = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") 42 | months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") 43 | 44 | hrs = Pointer(dial) 45 | mins = Pointer(dial) 46 | secs = Pointer(dial) 47 | 48 | hstart = 0 + 0.7j # Pointer lengths. Will rotate relative to top. 49 | mstart = 0 + 1j 50 | sstart = 0 + 1j 51 | 52 | while True: 53 | t = time.localtime() 54 | hrs.value(hstart * uv(-t[3] * pi / 6 - t[4] * pi / 360), CYAN) 55 | mins.value(mstart * uv(-t[4] * pi / 30), CYAN) 56 | secs.value(sstart * uv(-t[5] * pi / 30), RED) 57 | lbltim.value("{:02d}.{:02d}.{:02d}".format(t[3], t[4], t[5])) 58 | lbldate.value( 59 | "{} {}{} {} {}".format(days[t[6]], t[2], suffix(t[2]), months[t[1] - 1], t[0]) 60 | ) 61 | await asyncio.sleep(1) 62 | 63 | 64 | class BaseScreen(Screen): 65 | def __init__(self): 66 | super().__init__() 67 | labels = { 68 | "bdcolor": RED, 69 | "fgcolor": WHITE, 70 | "bgcolor": DARKGREEN, 71 | "justify": Label.CENTRE, 72 | } 73 | 74 | wri = CWriter(ssd, font, GREEN, BLACK) # verbose = True 75 | dial = Dial(wri, 2, 2, height=120, ticks=12, fgcolor=GREEN, pip=GREEN) 76 | # Set up clock display: instantiate labels 77 | # Demo of relative positioning. 78 | gap = 4 # Vertical gap between widgets 79 | row = dial.mrow + gap 80 | lbldate = Label(wri, row, 2, 200, **labels) 81 | row = lbldate.mrow + gap 82 | lbltim = Label(wri, row, 2, "00.00.00", **labels) 83 | self.reg_task(aclock(dial, lbldate, lbltim)) 84 | CloseButton(wri) 85 | 86 | 87 | def test(): 88 | print("Analog clock demo.") 89 | Screen.change(BaseScreen) 90 | 91 | 92 | test() 93 | -------------------------------------------------------------------------------- /gui/demos/active.py: -------------------------------------------------------------------------------- 1 | # active.py micropython-touch demo of touch widgets 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Create SSD instance. Must be done first because of RAM use. 7 | import touch_setup 8 | from gui.core.tgui import Screen, ssd 9 | from gui.core.writer import CWriter 10 | import gui.fonts.arial10 as arial10 # Font for CWriter 11 | import gui.fonts.freesans20 as font 12 | from gui.core.colors import * 13 | 14 | # Widgets 15 | from gui.widgets import ( 16 | Label, 17 | Scale, 18 | ScaleLog, 19 | Button, 20 | CloseButton, 21 | Slider, 22 | HorizSlider, 23 | Knob, 24 | Checkbox, 25 | ) 26 | 27 | 28 | class BaseScreen(Screen): 29 | def __init__(self): 30 | def tickcb(f, c): 31 | if f > 0.8: 32 | return RED 33 | if f < -0.8: 34 | return BLUE 35 | return c 36 | 37 | def tick_log_cb(f, c): 38 | if f > 20_000: 39 | return RED 40 | if f < 4: 41 | return BLUE 42 | return c 43 | 44 | super().__init__() 45 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 46 | wri_big = CWriter(ssd, font, GREEN, BLACK, verbose=False) 47 | 48 | col = 2 49 | row = 190 50 | Label(wri_big, row, col, "Result") 51 | row = 215 52 | self.lbl = Label(wri_big, row, col, 100, bdcolor=RED) 53 | 54 | self.vslider = Slider( 55 | wri, 56 | 25, 57 | 20, 58 | width=30, 59 | height=150, 60 | callback=self.slider_cb, 61 | bdcolor=RED, 62 | slotcolor=BLUE, 63 | legends=("0.0", "0.5", "1.0"), 64 | value=0.5, 65 | ) 66 | col = 85 67 | row = 25 68 | self.hslider = HorizSlider( 69 | wri, 70 | row, 71 | col, 72 | callback=self.slider_cb, 73 | bdcolor=GREEN, 74 | slotcolor=BLUE, 75 | legends=("0.0", "0.5", "1.0"), 76 | value=0.7, 77 | ) 78 | row += 30 79 | self.scale = Scale( 80 | wri, 81 | row, 82 | col, 83 | width=150, 84 | tickcb=tickcb, 85 | pointercolor=RED, 86 | fontcolor=YELLOW, 87 | bdcolor=CYAN, 88 | callback=self.cb, 89 | active=True, 90 | ) 91 | row += 40 92 | self.scale_log = ScaleLog( 93 | wri, 94 | row, 95 | col, 96 | width=150, 97 | tickcb=tick_log_cb, 98 | pointercolor=RED, 99 | fontcolor=YELLOW, 100 | bdcolor=CYAN, 101 | callback=self.cb, 102 | value=10, 103 | active=True, 104 | ) 105 | row += 40 106 | self.knob = Knob(wri, row, col, callback=self.cb, bgcolor=DARKGREEN, color=LIGHTRED) 107 | col = 180 108 | row = 155 109 | Checkbox(wri, row, col, callback=self.cbcb) 110 | row = 190 111 | Label(wri_big, row, col, "Enable/disable") 112 | CloseButton(wri_big) 113 | 114 | def cb(self, obj): 115 | self.lbl.value("{:5.3f}".format(obj.value())) 116 | 117 | def cbcb(self, cb): 118 | val = cb.value() 119 | self.vslider.greyed_out(val) 120 | self.hslider.greyed_out(val) 121 | self.scale.greyed_out(val) 122 | self.scale_log.greyed_out(val) 123 | self.knob.greyed_out(val) 124 | 125 | def slider_cb(self, s): 126 | self.cb(s) 127 | v = s.value() 128 | if v < 0.2: 129 | s.color(BLUE) 130 | elif v > 0.8: 131 | s.color(RED) 132 | else: 133 | s.color(GREEN) 134 | 135 | 136 | def test(): 137 | if ssd.height < 240 or ssd.width < 320: 138 | print(" This test requires a display of at least 320x240 pixels.") 139 | else: 140 | print("Testing micropython-touch...") 141 | Screen.change(BaseScreen) 142 | 143 | 144 | test() 145 | -------------------------------------------------------------------------------- /gui/demos/bitmap.py: -------------------------------------------------------------------------------- 1 | # bitmap.py micropython-touch Display a changing bitmap via the BitMap widget. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2022-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | from gui.widgets import Label, Button, CloseButton, BitMap 10 | from gui.core.writer import CWriter 11 | import gui.fonts.freesans20 as font 12 | from gui.core.colors import * 13 | 14 | 15 | class BaseScreen(Screen): 16 | def __init__(self): 17 | 18 | super().__init__() 19 | wri = CWriter(ssd, font, GREEN, BLACK) 20 | col = 2 21 | row = 2 22 | Label(wri, row, col, "Bitmap Demo.") 23 | row = 25 24 | self.graphic = BitMap(wri, row, col, 99, 99, fgcolor=WHITE, bgcolor=BLACK) 25 | col = 120 26 | Button(wri, row, col, height=25, text="Next", litcolor=WHITE, callback=self.cb) 27 | CloseButton(wri) # Quit the application 28 | self.image = 0 29 | 30 | def cb(self, _): 31 | try: 32 | self.graphic.value(path := f"optional/bitmaps/m{self.image:02d}") 33 | except OSError: 34 | print(f"File {path} not found.") 35 | self.image += 1 36 | self.image %= 4 37 | if self.image == 3: 38 | self.graphic.color(BLUE) 39 | else: 40 | self.graphic.color(WHITE) 41 | 42 | 43 | def test(): 44 | print("Bitmap demo.") 45 | Screen.change(BaseScreen) # A class is passed here, not an instance. 46 | 47 | 48 | test() 49 | -------------------------------------------------------------------------------- /gui/demos/calendar.py: -------------------------------------------------------------------------------- 1 | # calendar.py Test Grid class. Requires date.py. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import Grid, CloseButton, Label, Button 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.font10 as font 15 | import gui.fonts.font14 as font1 16 | from gui.core.colors import * 17 | from .date import DateCal 18 | 19 | 20 | class BaseScreen(Screen): 21 | def __init__(self): 22 | 23 | super().__init__() 24 | btn = {"height": 30, "callback": self.adjust, "shape": CIRCLE, "litcolor": WHITE} 25 | self.date = DateCal() 26 | wri = CWriter(ssd, font, GREEN, BLACK, False) 27 | wri1 = CWriter(ssd, font1, WHITE, BLACK, False) 28 | col = 2 29 | row = 2 30 | rows = 6 31 | cols = 7 32 | self.ncells = cols * (rows - 1) # Row 0 has day labels 33 | self.last_cell = cols * rows 34 | colwidth = 35 35 | self.lbl = Label(wri, row, col, text=(colwidth + 4) * cols, justify=Label.CENTRE) 36 | row = self.lbl.mrow 37 | self.grid = Grid(wri, row, col, colwidth, rows, cols, justify=Label.CENTRE) 38 | self.grid[0, 0:7] = iter([d[:3] for d in DateCal.days]) # 3-char day names 39 | 40 | row = self.grid.mrow + 4 41 | ht = 30 42 | b = Button(wri, row, col, text="y-", args=("y", -1), **btn) 43 | col = b.mcol + 2 44 | b = Button(wri, row, col, text="y+", args=("y", 1), **btn) 45 | col = b.mcol + 5 46 | b = Button(wri, row, col, text="m-", args=("m", -1), **btn) 47 | col = b.mcol + 2 48 | b = Button(wri, row, col, text="m+", args=("m", 1), **btn) 49 | col = b.mcol + 5 50 | b = Button(wri, row, col, text="w-", args=("d", -7), **btn) 51 | col = b.mcol + 2 52 | b = Button(wri, row, col, text="w+", args=("d", 7), **btn) 53 | col = b.mcol + 5 54 | b = Button(wri, row, col, text="d-", args=("d", -1), **btn) 55 | col = b.mcol + 2 56 | b = Button(wri, row, col, text="d+", args=("d", 1), **btn) 57 | col = b.mcol + 5 58 | b = Button(wri, row, col, fgcolor=BLUE, text="H", args=("h",), **btn) 59 | # row = b.mrow + 10 60 | col = 2 61 | row = ssd.height - (wri1.height + 2) 62 | self.lblnow = Label(wri1, row, col, text=ssd.width - 4, fgcolor=WHITE) 63 | self.update() 64 | CloseButton(wri) # Quit the application 65 | 66 | def adjust(self, _, f, n=0): 67 | d = self.date 68 | if f == "y": 69 | d.year += n 70 | elif f == "m": 71 | d.month += n 72 | elif f == "d": 73 | d.day += n 74 | elif f == "h": 75 | d.now() 76 | self.update() 77 | 78 | def days(self, month_length): # Produce content for every cell 79 | for n in range(self.ncells + 1): 80 | yield str(n + 1) if n < month_length else "" 81 | 82 | def update(self): 83 | grid = self.grid 84 | cur = self.date # Currency 85 | self.lbl.value(f"{DateCal.months[cur.month - 1]} {cur.year}") 86 | # Populate day number cells 87 | values = self.days(cur.month_length) # Instantiate generator 88 | idx_1 = 7 + cur.wday_n(1) # Index of 1st of month 89 | grid[idx_1 : self.last_cell] = values # Populate from mday 1 to last cell 90 | grid[7:idx_1] = values # Populate cells before 1st of month 91 | # Color cells of Sunday, today and currency. In collisions (e.g. today==Sun) 92 | # last color applied is effective 93 | grid[1:6, 6] = {"fgcolor": BLUE} # Sunday color 94 | grid[idx_1 + cur.mday - 1] = {"fgcolor": YELLOW} # Currency 95 | today = DateCal() 96 | if cur.year == today.year and cur.month == today.month: # Today is in current month 97 | grid[idx_1 + today.mday - 1] = {"fgcolor": RED} 98 | self.lblnow.value(f"{today.day_str} {today.mday} {today.month_str} {today.year}") 99 | 100 | 101 | def test(): 102 | print("Calendar demo.") 103 | Screen.change(BaseScreen) # A class is passed here, not an instance. 104 | 105 | 106 | test() 107 | -------------------------------------------------------------------------------- /gui/demos/checkbox.py: -------------------------------------------------------------------------------- 1 | # checkbox.py Minimal micropython-touch demo showing a Checkbox updating an LED. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import CloseButton, Checkbox, LED 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | 21 | super().__init__() 22 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 23 | col = 50 24 | row = 50 25 | self.cb = Checkbox(wri, row, col, callback=self.cbcb) 26 | col += 40 27 | self.led = LED(wri, row, col, color=YELLOW, bdcolor=GREEN) 28 | CloseButton(wri) 29 | 30 | def cbcb(self, cb): 31 | self.led.value(cb.value()) 32 | 33 | 34 | def test(): 35 | print("Checkbox demo.") 36 | Screen.change(BaseScreen) 37 | 38 | 39 | test() 40 | -------------------------------------------------------------------------------- /gui/demos/date.py: -------------------------------------------------------------------------------- 1 | # date.py Minimal Date class for micropython-touch 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023-2024 Peter Hinch 5 | 6 | from time import mktime, localtime 7 | 8 | _SECS_PER_DAY = const(86400) 9 | 10 | 11 | def leap(year): 12 | return bool((not year % 4) ^ (not year % 100)) 13 | 14 | 15 | class Date: 16 | def __init__(self, lt=None): 17 | self.callback = lambda: None # No callback until set 18 | self.now(lt) 19 | 20 | def now(self, lt=None): 21 | self._lt = list(localtime()) if lt is None else list(lt) 22 | self._update() 23 | 24 | def _update(self, ltmod=True): # If ltmod is False ._cur has been changed 25 | if ltmod: # Otherwise ._lt has been modified 26 | self._lt[3] = 6 27 | self._cur = mktime(self._lt) // _SECS_PER_DAY 28 | self._lt = list(localtime(self._cur * _SECS_PER_DAY)) 29 | self.callback() 30 | 31 | def _mlen(self, d=bytearray((31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31))): 32 | days = d[self._lt[1] - 1] 33 | return days if days else (29 if leap(self._lt[0]) else 28) 34 | 35 | @property 36 | def year(self): 37 | return self._lt[0] 38 | 39 | @year.setter 40 | def year(self, v): 41 | if self.mday == 29 and self.month == 2 and not leap(v): 42 | self.mday = 28 # Ensure it doesn't skip a month 43 | self._lt[0] = v 44 | self._update() 45 | 46 | @property 47 | def month(self): 48 | return self._lt[1] 49 | 50 | # Can write d.month = 4 or d.month += 15 51 | @month.setter 52 | def month(self, v): 53 | y, m = divmod(v - 1, 12) 54 | self._lt[0] += y 55 | self._lt[1] = m + 1 56 | self._lt[2] = min(self._lt[2], self._mlen()) 57 | self._update() 58 | 59 | @property 60 | def mday(self): 61 | return self._lt[2] 62 | 63 | @mday.setter 64 | def mday(self, v): 65 | if not 0 < v <= self._mlen(): 66 | raise ValueError(f"mday {v} is out of range") 67 | self._lt[2] = v 68 | self._update() 69 | 70 | @property 71 | def day(self): # Days since epoch. 72 | return self._cur 73 | 74 | @day.setter 75 | def day(self, v): # Usage: d.day += 7 or date_1.day = d.day. 76 | self._cur = v 77 | self._update(False) # Flag _cur change 78 | 79 | # Read-only properties 80 | 81 | @property 82 | def wday(self): 83 | return self._lt[6] 84 | 85 | # Date comparisons 86 | 87 | def __lt__(self, other): 88 | return self.day < other.day 89 | 90 | def __le__(self, other): 91 | return self.day <= other.day 92 | 93 | def __eq__(self, other): 94 | return self.day == other.day 95 | 96 | def __ne__(self, other): 97 | return self.day != other.day 98 | 99 | def __gt__(self, other): 100 | return self.day > other.day 101 | 102 | def __ge__(self, other): 103 | return self.day >= other.day 104 | 105 | def __str__(self): 106 | return f"{self.year}/{self.month}/{self.mday}" 107 | 108 | 109 | class DateCal(Date): 110 | days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") 111 | months = ( 112 | "January", 113 | "February", 114 | "March", 115 | "April", 116 | "May", 117 | "June", 118 | "July", 119 | "August", 120 | "September", 121 | "October", 122 | "November", 123 | "December", 124 | ) 125 | 126 | def __init__(self, lt=None): 127 | super().__init__(lt) 128 | 129 | @property 130 | def month_length(self): 131 | return self._mlen() 132 | 133 | @property 134 | def day_str(self): 135 | return self.days[self.wday] 136 | 137 | @property 138 | def month_str(self): 139 | return self.months[self.month - 1] 140 | 141 | def wday_n(self, mday=1): 142 | return (self._lt[6] - self._lt[2] + mday) % 7 143 | 144 | def mday_list(self, wday): 145 | ml = self._mlen() # 1 + ((wday - wday1) % 7) 146 | d0 = 1 + ((wday - (self._lt[6] - self._lt[2] + 1)) % 7) 147 | return [d for d in range(d0, ml + 1, 7)] 148 | 149 | # Optional: return UK DST offset in hours. Can pass hr to ensure that time change occurs 150 | # at 1am UTC otherwise it occurs on date change (0:0 UTC) 151 | # offs is offset by month 152 | def time_offset(self, hr=6, offs=bytearray((0, 0, 3, 1, 1, 1, 1, 1, 1, 10, 0, 0))): 153 | ml = self._mlen() 154 | wdayld = self.wday_n(ml) # Weekday of last day of month 155 | mday_sun = self.mday_list(6)[-1] # Month day of last Sunday 156 | m = offs[self._lt[1] - 1] 157 | if m < 3: 158 | return m # Deduce time offset from month alone 159 | return int( 160 | ((self._lt[2] < mday_sun) or (self._lt[2] == mday_sun and hr <= 1)) ^ (m == 3) 161 | ) # Months where offset changes 162 | 163 | def __str__(self): 164 | return f"{self.day_str} {self.mday} {self.month_str} {self.year}" 165 | -------------------------------------------------------------------------------- /gui/demos/dialog.py: -------------------------------------------------------------------------------- 1 | # dialog.py micropython-touch demo of the DialogBox class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd 9 | 10 | from gui.widgets import Label, Button, CloseButton, DialogBox 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | super().__init__() 21 | # Callback for Button 22 | def fwd(button, my_kwargs): 23 | Screen.change(DialogBox, kwargs=my_kwargs) 24 | 25 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 26 | 27 | row = 20 28 | col = 20 29 | # Trailing spaces ensure Label is wide enough to show results 30 | self.lbl = Label(wri, row, col, "Dialog box test ") 31 | # DialogBox constructor arguments. Here we pass all as keyword wargs. 32 | kwargs = { 33 | "writer": wri, 34 | "row": 20, 35 | "col": 20, 36 | "elements": (("Yes", GREEN), ("No", RED), ("Foo", YELLOW)), 37 | "label": "Test dialog", 38 | } 39 | row = 70 40 | Button( 41 | wri, 42 | row, 43 | col, 44 | text="Dialog", 45 | bgcolor=RED, 46 | textcolor=WHITE, 47 | callback=fwd, 48 | height=30, 49 | args=(kwargs,), 50 | ) 51 | CloseButton(wri) # Quit the application 52 | 53 | # Refresh the label after DialogBox has closed (but not when 54 | # the screen first opens). 55 | def after_open(self): 56 | if (v := Window.value()) is not None: 57 | self.lbl.value(f"Result: {v}") 58 | 59 | 60 | def test(): 61 | print("DialogBox demo.") 62 | Screen.change(BaseScreen) 63 | 64 | 65 | test() 66 | -------------------------------------------------------------------------------- /gui/demos/dropdown.py: -------------------------------------------------------------------------------- 1 | # dropdown.py micropython-touch demo of Dropdown class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd 9 | 10 | from gui.widgets import Label, CloseButton, Dropdown 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | 21 | super().__init__() 22 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 23 | 24 | col = 50 25 | row = 20 26 | els = ("hydrogen", "helium", "neon", "argon", "krypton", "xenon", "radon") 27 | self.dd = Dropdown( 28 | wri, 29 | row, 30 | col, 31 | elements=els, 32 | dlines=5, # Show 5 lines 33 | bdcolor=GREEN, 34 | callback=self.ddcb, 35 | ) 36 | row += 50 37 | self.lbl = Label(wri, row, col, self.dd.width, bdcolor=RED) 38 | CloseButton(wri) # Quit the application 39 | 40 | def after_open(self): 41 | self.lbl.value(self.dd.textvalue()) 42 | 43 | def ddcb(self, dd): 44 | if hasattr(self, "lbl"): 45 | self.lbl.value(dd.textvalue()) 46 | 47 | 48 | def test(): 49 | print("Dropdown demo.") 50 | Screen.change(BaseScreen) 51 | 52 | 53 | test() 54 | -------------------------------------------------------------------------------- /gui/demos/dropdown_var.py: -------------------------------------------------------------------------------- 1 | # dropdown_var.py micropython-touch demo of Dropdown widget with changeable elements 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd 9 | 10 | from gui.widgets import Label, Button, CloseButton, Dropdown 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.font10 as font 15 | from gui.core.colors import * 16 | 17 | 18 | def newtext(): # Create new listbox entries 19 | strings = ("Iron", "Copper", "Lead", "Zinc") 20 | n = 0 21 | while True: 22 | yield strings[n] 23 | n = (n + 1) % len(strings) 24 | 25 | 26 | ntxt = newtext() # Instantiate the generator 27 | 28 | 29 | class BaseScreen(Screen): 30 | def __init__(self): 31 | 32 | super().__init__() 33 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 34 | 35 | col = 2 36 | row = 2 37 | self.els = ["Hydrogen", "Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon"] 38 | self.dd = Dropdown( 39 | wri, 40 | row, 41 | col, 42 | elements=self.els, 43 | dlines=5, # Show 5 lines 44 | bdcolor=GREEN, 45 | callback=self.ddcb, 46 | ) 47 | row += 30 48 | self.lbl = Label(wri, row, col, self.dd.width, bdcolor=RED) 49 | # Common button args 50 | tbl = { 51 | "litcolor": WHITE, 52 | "height": 25, 53 | } 54 | b = Button(wri, 2, 120, text="del", callback=self.delcb, **tbl) 55 | b = Button(wri, b.mrow + 2, 120, text="add", callback=self.addcb, **tbl) 56 | b = Button(wri, b.mrow + 10, 120, text="h2", callback=self.gocb, args=("Hydrogen",), **tbl) 57 | b = Button(wri, b.mrow + 2, 120, text="fe", callback=self.gocb, args=("Iron",), **tbl) 58 | CloseButton(wri) 59 | 60 | def gocb(self, _, txt): # Go button callback: Move currency to specified entry 61 | self.dd.textvalue(txt) 62 | 63 | def addcb(self, _): # Add button callback 64 | self.els.append(next(ntxt)) # Append a new entry 65 | self.dd.update() 66 | 67 | def delcb(self, _): # Delete button callback 68 | del self.els[self.dd.value()] # Delete current entry 69 | self.dd.update() 70 | self.lbl.value(self.dd.textvalue()) # Cause value change callack to run 71 | 72 | def ddcb(self, dd): 73 | if hasattr(self, "lbl"): 74 | self.lbl.value(dd.textvalue()) 75 | 76 | def after_open(self): 77 | self.lbl.value(self.dd.textvalue()) 78 | 79 | 80 | def test(): 81 | print("Dropdown demo.") 82 | Screen.change(BaseScreen) 83 | 84 | 85 | test() 86 | -------------------------------------------------------------------------------- /gui/demos/dropdown_var_tuple.py: -------------------------------------------------------------------------------- 1 | # dropdown_var_tuple.py micropython-touch demo of Dropdown widget with changeable elements 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd 9 | 10 | from gui.widgets import Label, Button, CloseButton, Dropdown 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.font10 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | 21 | super().__init__() 22 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 23 | 24 | # Create new dropdown entries 25 | tuples = ( 26 | ("Iron", self.mcb, ("Fe",)), 27 | ("Copper", self.mcb, ("Cu",)), 28 | ("Lead", self.mcb, ("Pb",)), 29 | ("Zinc", self.mcb, ("Zn",)), 30 | ) 31 | 32 | def newtup(): 33 | n = 0 34 | while True: 35 | yield tuples[n] 36 | n = (n + 1) % len(tuples) 37 | 38 | self.ntup = newtup() # Instantiate the generator 39 | 40 | col = 2 41 | row = 2 42 | self.els = [ 43 | ("Hydrogen", self.cb, ("H",)), 44 | ("Helium", self.cb, ("He",)), 45 | ("Neon", self.cb, ("Ne",)), 46 | ("Xenon", self.cb, ("Xe",)), 47 | ("Radon", self.cb_radon, ("Ra",)), 48 | ("Uranium", self.cb_radon, ("U",)), 49 | ("Plutonium", self.cb_radon, ("Pu",)), 50 | ("Actinium", self.cb_radon, ("Ac",)), 51 | ] 52 | self.dd = Dropdown( 53 | wri, 54 | row, 55 | col, 56 | elements=self.els, 57 | dlines=5, # Show 5 lines 58 | bdcolor=GREEN, 59 | ) 60 | row += 30 61 | self.lbl = Label(wri, row, col, self.dd.width, bdcolor=RED) 62 | # Common button args 63 | tbl = { 64 | "litcolor": WHITE, 65 | "height": 25, 66 | } 67 | b = Button(wri, 2, 120, text="del", callback=self.delcb, **tbl) 68 | b = Button(wri, b.mrow + 2, 120, text="add", callback=self.addcb, **tbl) 69 | b = Button(wri, b.mrow + 10, 120, text="h2", callback=self.gocb, args=("Hydrogen",), **tbl) 70 | b = Button(wri, b.mrow + 2, 120, text="fe", callback=self.gocb, args=("Iron",), **tbl) 71 | CloseButton(wri) 72 | 73 | def gocb(self, _, txt): # Go button callback: Move currency to specified entry 74 | self.dd.textvalue(txt) 75 | 76 | def addcb(self, _): # Add button callback 77 | self.els.append(next(self.ntup)) # Append a new entry 78 | self.dd.update() 79 | 80 | def delcb(self, _): # Delete button callback 81 | del self.els[self.dd.value()] # Delete current entry 82 | self.dd.update() 83 | self.lbl.value(self.dd.textvalue()) # Cause value change callack to run 84 | 85 | def cb(self, dd, s): 86 | self.lbl.value(self.dd.textvalue()) 87 | print("Gas", s) 88 | 89 | def mcb(self, dd, s): 90 | self.lbl.value(self.dd.textvalue()) 91 | print("Metal", s) 92 | 93 | def cb_radon(self, dd, s): # Yeah, Radon is a gas too... 94 | self.lbl.value(self.dd.textvalue()) 95 | print("Radioactive", s) 96 | 97 | def after_open(self): 98 | self.lbl.value(self.dd.textvalue()) 99 | 100 | 101 | def test(): 102 | print("Dropdown demo.") 103 | Screen.change(BaseScreen) 104 | 105 | 106 | test() 107 | -------------------------------------------------------------------------------- /gui/demos/keyboard.py: -------------------------------------------------------------------------------- 1 | # keyboard.py Test Grid class. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import Grid, CloseButton, Label, Button, Pad, Dropdown 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.font10 as font 15 | import gui.fonts.font14 as font1 16 | from gui.core.colors import * 17 | 18 | 19 | def populate(g, level): 20 | if level == 0: 21 | g[0, 0:10] = iter("1234567890") 22 | g[1, 0:10] = iter("qwertyuiop") 23 | g[2, 0:10] = iter("asdfghjkl;") 24 | g[3, 0:10] = iter("zxcvbnm,./") 25 | elif level == 1: 26 | g[0, 0:10] = iter("1234567890") 27 | g[1, 0:10] = iter("QWERTYUIOP") 28 | g[2, 0:10] = iter("ASDFGHJKL;") 29 | g[3, 0:9] = iter("ZXCVBNM,.") 30 | g[3, 9] = "sp" # Special char: space 31 | else: 32 | g[0, 0:10] = iter('!"£$%^&*()') 33 | g[1, 0:10] = iter(";:@'#<>?/\\") 34 | g[2, 0:10] = iter(",.-_+=[]{}") 35 | g[3, 0:10] = iter("°μπωϕθαβγδ") 36 | 37 | 38 | class BaseScreen(Screen): 39 | def __init__(self): 40 | 41 | super().__init__() 42 | wri = CWriter(ssd, font, GREEN, BLACK, False) 43 | wri1 = CWriter(ssd, font1, WHITE, BLACK, False) 44 | col = 2 45 | row = 20 46 | self.rows = 4 # Grid dimensions in cells 47 | self.cols = 10 48 | colwidth = 25 # Column width 49 | self.grid = Grid(wri, row, col, colwidth, self.rows, self.cols, justify=Label.CENTRE) 50 | populate(self.grid, 0) 51 | self.grid[0, 0] = {"bgcolor": RED} # Initial grid currency 52 | self.last = (0, 0) # Last cell touched 53 | ch = round((gh := self.grid.height) / self.rows) # Height & width of a cell 54 | cw = round((gw := self.grid.width) / self.cols) 55 | self.pad = Pad(wri, row, col, height=gh, width=gw, callback=self.adjust, args=(ch, cw)) 56 | row = self.grid.mrow + 5 57 | els = ("lower", "upper", "symbols") 58 | dd = Dropdown(wri, row, col, elements=els, callback=self.ddcb, bdcolor=YELLOW) 59 | b = Button(wri, row, dd.mcol + 5, text="Space", callback=self.space) 60 | b = Button(wri, row, b.mcol + 5, text="bsp", callback=self.bsp) 61 | row = dd.mrow + 5 62 | self.lbltxt = Label(wri1, row, col, text=ssd.width - 4, fgcolor=WHITE) 63 | self.lbltxt.value(">_") 64 | CloseButton(wri) # Quit the application 65 | 66 | def adjust(self, pad, ch, cw): 67 | g = self.grid 68 | crl, ccl = self.last # Remove highlight from previous currency 69 | g[crl, ccl] = {"bgcolor": BLACK} 70 | 71 | cr = pad.rr // ch # Get grid coordinates of current touch 72 | cc = pad.rc // cw 73 | cl = next(g[cr, cc]) # Touched Label 74 | self.lbltxt.value("".join((self.lbltxt.value()[:-1], cl(), "_"))) 75 | g[cr, cc] = {"bgcolor": RED} # Highlight 76 | self.last = (cr, cc) 77 | 78 | def ddcb(self, dd): 79 | populate(self.grid, dd.value()) 80 | 81 | def space(self, _): 82 | self.lbltxt.value("".join((self.lbltxt.value()[:-1], " _"))) 83 | 84 | def bsp(self, _): 85 | v = self.lbltxt.value() 86 | if len(v) > 2: 87 | self.lbltxt.value("".join((v[:-2], "_"))) 88 | 89 | 90 | def test(): 91 | print("Keyboard demo.") 92 | Screen.change(BaseScreen) # A class is passed here, not an instance. 93 | 94 | 95 | test() 96 | -------------------------------------------------------------------------------- /gui/demos/linked_sliders.py: -------------------------------------------------------------------------------- 1 | # linked_sliders.py Minimal micropython-touch demo one Slider controlling two others. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import CloseButton, Slider 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | args = { 21 | "bdcolor": RED, 22 | "slotcolor": BLUE, 23 | "legends": ("0", "5", "10"), 24 | "value": 0.5, 25 | "width": 30, 26 | "height": 180, 27 | } 28 | super().__init__() 29 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 30 | col = 50 31 | row = 20 32 | # Note: callback runs now, but other sliders have not yet been instantiated. 33 | self.s0 = Slider(wri, row, col, callback=self.slider_cb, **args) 34 | col = self.s0.mcol + 20 35 | self.s1 = Slider(wri, row, col, **args) 36 | col = self.s1.mcol + 20 37 | self.s2 = Slider(wri, row, col, **args) 38 | CloseButton(wri) 39 | 40 | def slider_cb(self, s): 41 | v = s.value() 42 | if hasattr(self, "s1"): # If s1 & s2 have been instantiated 43 | self.s1.value(v) 44 | self.s2.value(v) 45 | 46 | 47 | def test(): 48 | print("Linked sliders. Leftmost one controls others.") 49 | Screen.change(BaseScreen) 50 | 51 | 52 | test() 53 | -------------------------------------------------------------------------------- /gui/demos/listbox.py: -------------------------------------------------------------------------------- 1 | # listbox.py micropython-touch demo of Listbox class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | from touch_setup import ssd # Create a display instance 8 | from gui.core.tgui import Screen 9 | from gui.core.writer import CWriter 10 | from gui.core.colors import * 11 | 12 | from gui.widgets import Listbox, CloseButton 13 | import gui.fonts.freesans20 as font 14 | 15 | 16 | class BaseScreen(Screen): 17 | def __init__(self): 18 | def cb(lb, s): 19 | print("Gas", s) 20 | 21 | def cb_radon(lb, s): # Yeah, Radon is a gas too... 22 | print("Radioactive", s) 23 | 24 | super().__init__() 25 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 26 | els = ( 27 | ("Hydrogen", cb, ("H",)), 28 | ("Helium", cb, ("He",)), 29 | ("Neon", cb, ("Ne",)), 30 | ("Xenon", cb, ("Xe",)), 31 | ("Radon", cb_radon, ("Ra",)), 32 | ("Uranium", cb_radon, ("U",)), 33 | ("Plutonium", cb_radon, ("Pu",)), 34 | ("Actinium", cb_radon, ("Ac",)), 35 | ) 36 | Listbox(wri, 2, 2, elements=els, dlines=5, bdcolor=RED, value=1) 37 | # bdcolor = RED, fgcolor=RED, fontcolor = YELLOW, select_color=BLUE, value=1) 38 | CloseButton(wri) 39 | 40 | 41 | Screen.change(BaseScreen) 42 | -------------------------------------------------------------------------------- /gui/demos/listbox_var.py: -------------------------------------------------------------------------------- 1 | # listbox_var.py micropython-touch demo of Listbox class with variable elements 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | from touch_setup import ssd # Create a display instance 8 | from gui.core.tgui import Screen 9 | from gui.core.writer import CWriter 10 | from gui.core.colors import * 11 | 12 | from gui.widgets import Listbox, Button, CloseButton 13 | import gui.fonts.freesans20 as font 14 | 15 | 16 | def newtext(): # Create new listbox entries 17 | strings = ("Iron", "Copper", "Lead", "Zinc") 18 | n = 0 19 | while True: 20 | yield strings[n] 21 | n = (n + 1) % len(strings) 22 | 23 | 24 | ntxt = newtext() # Instantiate the generator 25 | 26 | 27 | class BaseScreen(Screen): 28 | def __init__(self): 29 | 30 | super().__init__() 31 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 32 | self.els = [ 33 | "Hydrogen", 34 | "Helium", 35 | "Neon", 36 | "Xenon", 37 | "Radon", 38 | "Uranium", 39 | "Plutonium", 40 | "Actinium", 41 | ] 42 | self.lb = Listbox( 43 | wri, 44 | 2, 45 | 2, 46 | elements=self.els, 47 | dlines=5, 48 | bdcolor=RED, 49 | value=1, 50 | callback=self.lbcb, 51 | ) 52 | # Common button args 53 | tbl = { 54 | "litcolor": WHITE, 55 | "height": 25, 56 | } 57 | 58 | Button(wri, 2, 120, text="del", callback=self.delcb, **tbl) 59 | Button(wri, 32, 120, text="add", callback=self.addcb, **tbl) 60 | Button(wri, 62, 120, text="h2", callback=self.gocb, args=("Hydrogen",), **tbl) 61 | Button(wri, 92, 120, text="fe", callback=self.gocb, args=("Iron",), **tbl) 62 | CloseButton(wri) 63 | 64 | def lbcb(self, lb): # Listbox callback 65 | print(lb.textvalue()) 66 | 67 | def gocb(self, _, txt): # Go button callback: Move currency to specified entry 68 | self.lb.textvalue(txt) 69 | 70 | def addcb(self, _): # Add button callback 71 | self.els.append(next(ntxt)) # Append a new entry 72 | self.lb.update() 73 | 74 | def delcb(self, _): # Delete button callback 75 | del self.els[self.lb.value()] # Delete current entry 76 | self.lb.update() 77 | 78 | 79 | Screen.change(BaseScreen) 80 | -------------------------------------------------------------------------------- /gui/demos/menu.py: -------------------------------------------------------------------------------- 1 | # menu.py micropython-touch demo of Menu class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | import touch_setup # Create a display instance 7 | from gui.core.tgui import Screen, ssd 8 | import gui.fonts.freesans20 as font 9 | from gui.core.writer import CWriter 10 | 11 | from gui.widgets import Menu, CloseButton 12 | from gui.core.colors import * 13 | 14 | 15 | class BaseScreen(Screen): 16 | def __init__(self): 17 | def cb(button, n): 18 | print("Help callback", n) 19 | 20 | def cb_sm(lb, n): 21 | print("Submenu callback", lb.value(), lb.textvalue(), n) 22 | 23 | super().__init__() 24 | metals2 = ( 25 | ("Gold", cb_sm, (6,)), 26 | ("Silver", cb_sm, (7,)), 27 | ("Iron", cb_sm, (8,)), 28 | ("Zinc", cb_sm, (9,)), 29 | ("Copper", cb_sm, (10,)), 30 | ) 31 | 32 | gases = ( 33 | ("Helium", cb_sm, (0,)), 34 | ("Neon", cb_sm, (1,)), 35 | ("Argon", cb_sm, (2,)), 36 | ("Krypton", cb_sm, (3,)), 37 | ("Xenon", cb_sm, (4,)), 38 | ("Radon", cb_sm, (5,)), 39 | ) 40 | 41 | metals = ( 42 | ("Lithium", cb_sm, (6,)), 43 | ("Sodium", cb_sm, (7,)), 44 | ("Potassium", cb_sm, (8,)), 45 | ("Rubidium", cb_sm, (9,)), 46 | ("More", metals2), 47 | ) 48 | 49 | mnu = (("Gas", gases), ("Metal", metals), ("Help", cb, (2,))) 50 | 51 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 52 | Menu(wri, bgcolor=BLUE, textcolor=WHITE, fgcolor=RED, args=mnu) 53 | CloseButton(wri) 54 | 55 | 56 | Screen.change(BaseScreen) 57 | -------------------------------------------------------------------------------- /gui/demos/primitives.py: -------------------------------------------------------------------------------- 1 | # primitives.py micropython-touch demo of use of graphics primitives 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd, display 9 | 10 | from gui.widgets import Label, CloseButton, Pad 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.font10 as font 15 | from gui.core.colors import * 16 | 17 | 18 | def cb(obj, txt): 19 | print(f"Pad callback: text {txt}") 20 | 21 | 22 | class BaseScreen(Screen): 23 | def __init__(self): 24 | 25 | super().__init__() 26 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 27 | 28 | col = 2 29 | row = 2 30 | Label(wri, row, col, "Primitives") 31 | text = "Touch this text." 32 | row = 135 33 | Label(wri, row, col, text, fgcolor=YELLOW) 34 | Label(wri, row + 20, col, "Or try long touch.") 35 | sl = wri.stringlen(text) 36 | Pad( 37 | wri, 38 | row, 39 | col, 40 | width=sl, 41 | callback=cb, 42 | args=("release",), 43 | lp_callback=cb, 44 | lp_args=("long",), 45 | ) 46 | CloseButton(wri) # Quit the application 47 | 48 | def after_open(self): 49 | display.usegrey(False) 50 | # Coordinates are x, y as per framebuf 51 | # circle method is in Display class only 52 | display.circle(70, 70, 30, RED) 53 | # These methods exist in framebuf, so also in SSD and Display 54 | ssd.hline(0, 127, 128, BLUE) 55 | ssd.vline(127, 0, 128, BLUE) 56 | 57 | 58 | def test(): 59 | print("Primitives demo.") 60 | Screen.change(BaseScreen) 61 | 62 | 63 | test() 64 | -------------------------------------------------------------------------------- /gui/demos/qrcode.py: -------------------------------------------------------------------------------- 1 | # qrcode.py Minimal mmicropython-touch demo. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2022-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | from gui.widgets import Label, Button, CloseButton, QRMap 10 | 11 | scale = 2 # Magnification of graphic 12 | version = 4 13 | # qr_buf = QRMap.make_buffer(version, scale) 14 | from gui.core.writer import CWriter 15 | import gui.fonts.freesans20 as font 16 | from gui.core.colors import * 17 | 18 | 19 | class BaseScreen(Screen): 20 | def __init__(self): 21 | def my_callback(button, graphic): 22 | graphic("https://en.wikipedia.org/wiki/QR_code") 23 | 24 | super().__init__() 25 | wri = CWriter(ssd, font, GREEN, BLACK) 26 | col = 2 27 | row = 2 28 | Label(wri, row, col, "QR code Demo.") 29 | row = 25 30 | graphic = QRMap(wri, row, col, version, scale) 31 | graphic("uQR rocks!") 32 | col = 120 33 | Button( 34 | wri, 35 | row, 36 | col, 37 | height=25, 38 | text="URL", 39 | litcolor=WHITE, 40 | callback=my_callback, 41 | args=(graphic,), 42 | ) 43 | CloseButton(wri) # Quit the application 44 | 45 | 46 | def test(): 47 | print("QR code demo.") 48 | Screen.change(BaseScreen) # A class is passed here, not an instance. 49 | 50 | 51 | test() 52 | -------------------------------------------------------------------------------- /gui/demos/refresh_lock.py: -------------------------------------------------------------------------------- 1 | # refresh_lock.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # This demo assumes a large display whose drive supports segmented refresh. 7 | 8 | import touch_setup # Create a display instance 9 | 10 | try: 11 | from gui.core.tgui import Screen, ssd 12 | except ImportError: # Running under micro-gui 13 | from gui.core.ugui import Screen, ssd 14 | 15 | from gui.widgets import Label, Button, ButtonList, CloseButton, LED 16 | from gui.core.writer import CWriter 17 | import gui.fonts.font10 as font 18 | from gui.core.colors import * 19 | import asyncio 20 | from machine import Pin 21 | 22 | 23 | class BaseScreen(Screen): 24 | def __init__(self): 25 | 26 | table = [ 27 | {"fgcolor": YELLOW, "shape": RECTANGLE, "text": "Fast", "args": [True]}, 28 | {"fgcolor": CYAN, "shape": RECTANGLE, "text": "Slow", "args": [False]}, 29 | ] 30 | super().__init__() 31 | fixed_speed = not hasattr(ssd, "short_lock") 32 | if fixed_speed: 33 | print("Display does not support short_lock method.") 34 | self.task_count = 0 35 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 36 | col = 2 37 | row = 2 38 | lb = Label(wri, row, col, "Refresh test") 39 | self.led = LED(wri, row, lb.mcol + 20) 40 | row = 30 41 | txt = "Pause 5 secs" 42 | b = Button(wri, row, col, fgcolor=RED, shape=RECTANGLE, text=txt, callback=self.cb) 43 | row = 60 44 | bl = ButtonList(self.cbspeed) 45 | bl.greyed_out(fixed_speed) 46 | for t in table: # Buttons overlay each other at same location 47 | bl.add_button(wri, row, col, **t) 48 | row = 90 49 | lb = Label(wri, row, col, "Scheduling rate:") 50 | self.lblrate = Label(wri, row, lb.mcol + 4, "000", bdcolor=RED, justify=Label.RIGHT) 51 | Label(wri, row, self.lblrate.mcol + 4, "Hz") 52 | self.reg_task(self.flash()) # Flash the LED 53 | self.reg_task(self.toggle()) # Run a task which measures its scheduling rate 54 | self.reg_task(self.report()) 55 | CloseButton(wri) # Quit 56 | 57 | def cb(self, _): # Pause refresh 58 | asyncio.create_task(self.dopb()) 59 | 60 | async def dopb(self): 61 | self.lblrate.value("0") 62 | async with Screen.rfsh_lock: 63 | await asyncio.sleep(5) 64 | 65 | def cbspeed(self, _, v): # Fast-slow pushbutton callback 66 | ssd.short_lock(v) 67 | 68 | # Proof of stopped refresh: task keeps running but change not visible 69 | async def flash(self): 70 | while True: 71 | self.led.value(not self.led.value()) 72 | await asyncio.sleep_ms(300) 73 | 74 | async def report(self): 75 | while True: 76 | await asyncio.sleep(1) 77 | self.lblrate.value(f"{self.task_count}") 78 | self.task_count = 0 79 | 80 | # Measure the scheduling rate of a minimal task 81 | async def toggle(self): 82 | while True: 83 | async with Screen.rfsh_lock: 84 | self.task_count += 1 85 | await asyncio.sleep_ms(0) 86 | 87 | 88 | def test(): 89 | print("Refresh test.") 90 | Screen.change(BaseScreen) 91 | 92 | 93 | test() 94 | -------------------------------------------------------------------------------- /gui/demos/round.py: -------------------------------------------------------------------------------- 1 | # round.py Test/demo program for micropython-touch plot. Cross-patform, 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Create SSD instance. Must be done first because of RAM use. 7 | import touch_setup 8 | 9 | from cmath import rect, pi 10 | import math 11 | import asyncio 12 | 13 | from gui.core.writer import CWriter 14 | from gui.core.tgui import Screen, ssd 15 | from gui.widgets.graph import PolarGraph, PolarCurve 16 | from gui.widgets import Pad, Label 17 | 18 | # Fonts & colors 19 | import gui.fonts.font10 as font 20 | from gui.core.colors import * 21 | 22 | 23 | # ***** Define some generators to populate polar curves ***** 24 | def f1(k, nmax): 25 | def f(theta, k): 26 | return rect(math.sin(k * theta), theta) # complex 27 | 28 | for n in range(nmax + 1): 29 | yield f(2 * pi * n / nmax, k) # complex z 30 | 31 | 32 | def f2(k, l, nmax): 33 | v1 = 1 - l + 0j 34 | v2 = l + 0j 35 | rot = rect(1, 2 * pi / nmax) 36 | for n in range(nmax + 1): 37 | yield v1 + v2 38 | v1 *= rot 39 | v2 *= rot ** k 40 | 41 | 42 | def f3(c, r, nmax): 43 | rot = rect(1 - r * c / nmax, c * 2 * pi / nmax) 44 | v = 1 + 0j 45 | for n in range(nmax + 1): 46 | yield v 47 | v *= rot 48 | 49 | 50 | # Instantiate generators with args 51 | generators = [ 52 | [f1(3, 150), YELLOW], 53 | [f1(5, 150), RED], 54 | [f1(7, 150), CYAN], 55 | [f2(5, 0.3, 150), YELLOW], 56 | [f2(6, 0.4, 150), MAGENTA], 57 | [f3(6, 0.9, 150), WHITE], 58 | ] 59 | 60 | # ***** GUI code ***** 61 | 62 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 63 | 64 | 65 | class PolarScreen(Screen): 66 | def __init__(self, n): 67 | super().__init__() 68 | Pad(wri, 0, 0, height=239, width=239, callback=self.cb) 69 | self.n = n # Current screen number 70 | self.g = PolarGraph( 71 | wri, 2, 2, height=236, bdcolor=False, fgcolor=GREEN, gridcolor=LIGHTGREEN 72 | ) 73 | 74 | def cb(self, _): # Pad touch callback 75 | self.n += 1 # Point to next config entry 76 | if self.n < len(generators): 77 | Screen.change(PolarScreen, mode=Screen.REPLACE, args=(self.n,)) 78 | else: 79 | Screen.back() # Quit: there is no parent Screen instance. 80 | 81 | def after_open(self): # After graph has been drawn 82 | gen, color = generators[self.n] # Retrieve generator and color from config 83 | PolarCurve(self.g, color, gen) # populate graph. 84 | 85 | 86 | class BaseScreen(Screen): 87 | def __init__(self): 88 | super().__init__() 89 | Pad(wri, 0, 0, height=239, width=239, callback=self.cb) 90 | col = 40 91 | l = Label(wri, 100, col, "Round screen demo.", fgcolor=RED) 92 | l = Label(wri, l.mrow + 2, col, "Touch screen to change") 93 | Label(wri, l.mrow + 2, col, "function plotted.") 94 | 95 | def cb(self, _): # Change to screen 0 96 | Screen.change(PolarScreen, mode=Screen.REPLACE, args=(0,)) 97 | 98 | 99 | def test(): 100 | print("Plot module. Touch to change function to plot.") 101 | Screen.change(BaseScreen) 102 | 103 | 104 | test() 105 | -------------------------------------------------------------------------------- /gui/demos/screen_change.py: -------------------------------------------------------------------------------- 1 | # screen_change.py Minimal micropython-touch demo showing a Button causing a screen change. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import Button, CloseButton, Label 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | # Defining a button in this way enables it to be re-used on 18 | # multiple Screen instances. Note that a Screen class is 19 | # passed, not an instance. 20 | def fwdbutton(wri, row, col, cls_screen, text="Next"): 21 | def fwd(button): 22 | Screen.change(cls_screen) # Callback 23 | 24 | Button(wri, row, col, callback=fwd, text=text, height=50, width=80) 25 | 26 | 27 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 28 | 29 | # This screen overlays BaseScreen. 30 | class BackScreen(Screen): 31 | def __init__(self): 32 | super().__init__() 33 | Label(wri, 2, 2, "New screen.") 34 | CloseButton(wri) 35 | 36 | 37 | class BaseScreen(Screen): 38 | def __init__(self): 39 | 40 | super().__init__() 41 | Label(wri, 2, 2, "Base screen.") 42 | fwdbutton(wri, 40, 40, BackScreen) 43 | CloseButton(wri) 44 | 45 | 46 | def test(): 47 | print("Screen change demo.") 48 | Screen.change(BaseScreen) # Pass class, not instance! 49 | 50 | 51 | test() 52 | -------------------------------------------------------------------------------- /gui/demos/screen_replace.py: -------------------------------------------------------------------------------- 1 | # screen_replace.py micropython-touch demo showing non-stacked screens 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import Button, CloseButton, Label 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 18 | 19 | # Defining a button in this way enables it to be re-used on 20 | # multiple Screen instances. Note that a Screen class is 21 | # passed, not an instance. 22 | def fwdbutton(wri, row, col, cls_screen, text="Next", args=()): 23 | def fwd(button): 24 | Screen.change(cls_screen, args=args) # Callback 25 | 26 | Button(wri, row, col, height=50, width=80, callback=fwd, text=text) 27 | 28 | 29 | def navbutton(wri, row, col, gen, delta, text): 30 | def nav(button): 31 | cls_screen, num = gen(delta) # gen.send(delta) 32 | Screen.change(cls_screen, Screen.REPLACE, args=(num,)) # Callback 33 | 34 | Button(wri, row, col, height=30, callback=nav, fgcolor=BLACK, bgcolor=YELLOW, text=text) 35 | 36 | 37 | class RingScreen(Screen): 38 | def __init__(self, num): 39 | super().__init__() 40 | Label(wri, 2, 2, f"Ring screen no. {num}.", fgcolor=YELLOW) 41 | navbutton(wri, 40, 80, nav, 1, "Right") 42 | navbutton(wri, 40, 2, nav, -1, "Left") 43 | CloseButton(wri) 44 | 45 | 46 | # Create a tuple of Screen subclasses (in this case all are identical). 47 | ring = ((RingScreen, 0), (RingScreen, 1), (RingScreen, 2)) 48 | 49 | # Define a means of navigating between these classes 50 | def navigator(): 51 | x = 0 52 | 53 | def nav(delta): 54 | nonlocal x 55 | x = (x + delta) % len(ring) 56 | return ring[x] 57 | 58 | return nav 59 | 60 | 61 | nav = navigator() 62 | 63 | # This screen overlays BaseScreen. 64 | class StackScreen(Screen): 65 | def __init__(self): 66 | super().__init__() 67 | Label(wri, 2, 2, "Stacked screen.", fgcolor=CYAN) 68 | fwdbutton(wri, 40, 40, RingScreen, args=(0,)) 69 | CloseButton(wri) 70 | 71 | 72 | class BaseScreen(Screen): 73 | def __init__(self): 74 | 75 | super().__init__() 76 | Label(wri, 2, 2, "Base screen.") 77 | fwdbutton(wri, 40, 40, StackScreen) 78 | CloseButton(wri) 79 | 80 | 81 | s = """ 82 | Demo of screen replace. Screen hierarchy: 83 | 84 | Base Screen 85 | | 86 | Stacked Screen 87 | | 88 | <- Ring Screen 0 <-> Ring Screen 1 <-> Ring Screen 2 -> 89 | """ 90 | 91 | 92 | def test(): 93 | print(s) 94 | Screen.change(BaseScreen) # Pass class, not instance! 95 | 96 | 97 | test() 98 | -------------------------------------------------------------------------------- /gui/demos/screens.py: -------------------------------------------------------------------------------- 1 | # screens.py micropython-touch demo of multiple screens, dropdowns etc 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, Window, ssd 9 | 10 | from gui.widgets import Button, RadioButtons, CloseButton, Listbox, Dropdown, DialogBox, Label 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | # Note that litcolor is defeated by design, because the callback's action 18 | # is to change the screen currency. This tests the bugfix. 19 | def fwdbutton(writer, row, col, cls_screen, text, color, *args, **kwargs): 20 | def fwd(button): 21 | Screen.change(cls_screen, args=args, kwargs=kwargs) 22 | 23 | Button( 24 | writer, 25 | row, 26 | col, 27 | callback=fwd, 28 | bgcolor=color, 29 | litcolor=YELLOW, 30 | text=text, 31 | textcolor=WHITE, 32 | shape=CLIPPED_RECT, 33 | height=30, 34 | width=70, 35 | ) 36 | 37 | 38 | # Demo of creating a dialog manually 39 | class UserDialogBox(Window): 40 | def __init__(self, writer, callback, args): 41 | def back(button, text): 42 | Window.value(text) 43 | callback(Window, *args) 44 | Screen.back() 45 | 46 | def close_cb(button, text): 47 | Window.value(text) 48 | callback(Window, *args) 49 | 50 | height = 100 51 | width = 150 52 | super().__init__(20, 20, height, width, bgcolor=DARKGREEN) 53 | row = self.height - 50 54 | # .locn converts Window relative row, col to absolute row, col 55 | Button( 56 | writer, 57 | *self.locn(row, 20), 58 | height=30, 59 | width=50, 60 | textcolor=BLACK, 61 | bgcolor=RED, 62 | text="Cat", 63 | callback=back, 64 | args=("Cat",), 65 | ) 66 | Button( 67 | writer, 68 | *self.locn(row, 80), 69 | height=30, 70 | width=50, 71 | textcolor=BLACK, 72 | bgcolor=GREEN, 73 | text="Dog", 74 | callback=back, 75 | args=("Dog",), 76 | ) 77 | CloseButton(writer, callback=close_cb, args=("Close",)) 78 | 79 | 80 | # Minimal screen change example 81 | class Overlay(Screen): 82 | def __init__(self): 83 | super().__init__() 84 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 85 | Label(wri, 20, 20, "Screen overlays base") 86 | CloseButton(wri) 87 | 88 | 89 | class BaseScreen(Screen): 90 | def __init__(self): 91 | def lbcb(lb): 92 | print("Listbox", lb.textvalue()) 93 | 94 | def ddcb(dd): 95 | print("Dropdown", tv := dd.textvalue()) 96 | if tv == "new screen": 97 | Screen.change(Overlay) 98 | 99 | def dbcb(window, label): 100 | label.value("Auto Dialog: {}".format(window.value())) 101 | 102 | def udbcb(window, label): 103 | label.value("User Dialog: {}".format(window.value())) 104 | 105 | super().__init__() 106 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 107 | 108 | col = 20 109 | row = 20 110 | lb = Listbox( 111 | wri, 112 | row, 113 | col, 114 | callback=lbcb, 115 | elements=("cat", "dog", "aardvark", "goat", "pig", "mouse"), 116 | bdcolor=GREEN, 117 | bgcolor=DARKGREEN, 118 | ) 119 | col = lb.mcol + 10 120 | Dropdown( 121 | wri, 122 | row, 123 | col, 124 | callback=ddcb, 125 | elements=("hydrogen", "helium", "neon", "xenon", "new screen"), 126 | bdcolor=GREEN, 127 | bgcolor=DARKGREEN, 128 | ) 129 | row += 50 130 | lbl = Label(wri, row, col, "Result from dialog box.") 131 | row += 40 132 | dialog_elements = (("Yes", GREEN), ("No", RED), ("Foo", YELLOW)) 133 | # 1st 6 args are for fwdbutton 134 | fwdbutton( 135 | wri, 136 | row, 137 | col, 138 | DialogBox, 139 | "Dialog", 140 | RED, 141 | # Args for DialogBox constructor 142 | wri, 143 | elements=dialog_elements, 144 | label="Test dialog", 145 | callback=dbcb, 146 | args=(lbl,), 147 | ) 148 | col += 80 149 | fwdbutton( 150 | wri, 151 | row, 152 | col, 153 | UserDialogBox, 154 | "User", 155 | BLUE, 156 | # Args for UserDialogBox constructor 157 | wri, 158 | udbcb, 159 | (lbl,), 160 | ) 161 | CloseButton(wri) # Quit the application 162 | 163 | 164 | def test(): 165 | Screen.change(BaseScreen) 166 | 167 | 168 | test() 169 | -------------------------------------------------------------------------------- /gui/demos/simple.py: -------------------------------------------------------------------------------- 1 | # simple.py Minimal micropython-touch demo. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import Label, Button, CloseButton 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | def my_callback(button, arg): 21 | print("Button pressed", arg) 22 | 23 | super().__init__() 24 | tbl = {"litcolor": WHITE, "height": 60, "width": 80, "callback": my_callback} 25 | wri = CWriter(ssd, font, GREEN, BLACK) # Verbose 26 | col = 2 27 | row = 2 28 | Label(wri, row, col, "Simple Demo") 29 | row = 150 30 | Button(wri, row, col, text="Yes", args=("Yes",), **tbl) 31 | col += 100 32 | Button(wri, row, col, text="No", args=("No",), **tbl) 33 | CloseButton(wri, 30) # Quit the application 34 | 35 | 36 | def test(): 37 | print("Simple demo: button presses print to REPL.") 38 | Screen.change(BaseScreen) # A class is passed here, not an instance. 39 | 40 | 41 | test() 42 | -------------------------------------------------------------------------------- /gui/demos/slider.py: -------------------------------------------------------------------------------- 1 | # slider.py Minimal micropython-touch demo showing a Slider with variable color. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import CloseButton, Slider 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | 21 | super().__init__() 22 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 23 | col = 100 24 | row = 20 25 | Slider( 26 | wri, 27 | row, 28 | col, 29 | width=30, 30 | height=180, 31 | callback=self.slider_cb, 32 | bdcolor=RED, 33 | slotcolor=BLUE, 34 | legends=("0.0", "0.5", "1.0"), 35 | value=0.5, 36 | ) 37 | CloseButton(wri) 38 | 39 | def slider_cb(self, s): 40 | v = s.value() 41 | if v < 0.2: 42 | s.color(BLUE) 43 | elif v > 0.8: 44 | s.color(RED) 45 | else: 46 | s.color(GREEN) 47 | 48 | 49 | def test(): 50 | print("Slider demo.") 51 | Screen.change(BaseScreen) 52 | 53 | 54 | test() 55 | -------------------------------------------------------------------------------- /gui/demos/slider_label.py: -------------------------------------------------------------------------------- 1 | # slider_label.py Minimal micropython-touch demo showing a Slider controlling a Label. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | from gui.core.tgui import Screen, ssd 9 | 10 | from gui.widgets import CloseButton, Slider, Label 11 | from gui.core.writer import CWriter 12 | 13 | # Font for CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | 18 | class BaseScreen(Screen): 19 | def __init__(self): 20 | 21 | super().__init__() 22 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 23 | col = 2 24 | row = 2 25 | self.lbl = Label(wri, row, col, "0.500", bdcolor=RED, bgcolor=DARKGREEN) 26 | # Instantiate Label first, because Slider callback will run now. 27 | # See linked_sliders.py for another approach. 28 | row += 30 29 | col = 100 30 | Slider( 31 | wri, 32 | row, 33 | col, 34 | width=30, 35 | height=180, 36 | callback=self.slider_cb, 37 | bdcolor=RED, 38 | slotcolor=BLUE, 39 | legends=("0.0", "0.5", "1.0"), 40 | value=0.5, 41 | ) 42 | CloseButton(wri) 43 | 44 | def slider_cb(self, s): 45 | v = s.value() 46 | self.lbl.value("{:5.3f}".format(v)) 47 | 48 | 49 | def test(): 50 | print("Slider Label demo.") 51 | Screen.change(BaseScreen) 52 | 53 | 54 | test() 55 | -------------------------------------------------------------------------------- /gui/demos/tbox.py: -------------------------------------------------------------------------------- 1 | # tbox.py Test/demo of Textbox widget for micropython-touch 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Usage: 7 | # import gui.demos.tbox 8 | 9 | # Initialise hardware and framebuf before importing modules. 10 | import touch_setup # Create a display instance 11 | 12 | from gui.core.tgui import Screen, ssd 13 | from gui.core.writer import CWriter 14 | 15 | import asyncio 16 | from gui.core.colors import * 17 | import gui.fonts.freesans20 as font 18 | from gui.widgets import Label, Textbox, Button, CloseButton 19 | 20 | wri = CWriter(ssd, font) # verbose = True 21 | 22 | btnargs = {"height": 30, "width": 80, "fgcolor": BLACK} 23 | 24 | 25 | def fwdbutton(wri, row, col, cls_screen, text, bgc): 26 | def fwd(button): 27 | Screen.change(cls_screen) 28 | 29 | b = Button(wri, row, col, callback=fwd, bgcolor=bgc, text=text, **btnargs) 30 | return b.mrow 31 | 32 | 33 | async def wrap(tb): 34 | s = """The textbox displays multiple lines of text in a field of fixed dimensions. \ 35 | Text may be clipped to the width of the control or may be word-wrapped. If the number \ 36 | of lines of text exceeds the height available, scrolling may be performed \ 37 | by calling a method. 38 | """ 39 | tb.clear() 40 | tb.append(s, ntrim=100, line=0) 41 | while True: 42 | await asyncio.sleep(1) 43 | if not tb.scroll(1): 44 | break 45 | 46 | 47 | async def clip(tb): 48 | ss = ( 49 | "clip demo", 50 | "short", 51 | "longer line", 52 | "much longer line with spaces", 53 | "antidisestablishmentarianism", 54 | "line with\nline break", 55 | "Done", 56 | ) 57 | tb.clear() 58 | for s in ss: 59 | tb.append(s, ntrim=100) # Default line=None scrolls to show most recent 60 | await asyncio.sleep(1) 61 | 62 | 63 | # Args for textboxes 64 | # Positional 65 | pargs = (2, 2, 180, 7) # Row, Col, Width, nlines 66 | 67 | # Keyword 68 | tbargs = { 69 | "fgcolor": YELLOW, 70 | "bdcolor": RED, 71 | "bgcolor": BLACK, 72 | } 73 | 74 | 75 | class TBCScreen(Screen): 76 | def __init__(self): 77 | super().__init__() 78 | self.tb = Textbox(wri, *pargs, clip=True, **tbargs) 79 | CloseButton(wri) 80 | asyncio.create_task(self.main()) 81 | 82 | async def main(self): 83 | await clip(self.tb) 84 | 85 | 86 | class TBWScreen(Screen): 87 | def __init__(self): 88 | super().__init__() 89 | self.tb = Textbox(wri, *pargs, clip=False, **tbargs) 90 | CloseButton(wri) 91 | asyncio.create_task(self.main()) 92 | 93 | async def main(self): 94 | await wrap(self.tb) 95 | 96 | 97 | user_str = """The textbox displays multiple lines of text in a field of fixed dimensions. \ 98 | Text may be clipped to the width of the control or may be word-wrapped. If the number \ 99 | of lines of text exceeds the height available, scrolling may be performed \ 100 | by calling a method. 101 | 102 | Please touch the screen to scroll this text. 103 | """ 104 | 105 | 106 | class TBUScreen(Screen): 107 | def __init__(self): 108 | super().__init__() 109 | tb = Textbox(wri, *pargs, clip=False, active=True, **tbargs) 110 | tb.append(user_str, ntrim=100) 111 | CloseButton(wri) 112 | 113 | 114 | class MainScreen(Screen): 115 | def __init__(self): 116 | super().__init__() 117 | Label(wri, 2, 2, "Select test to run") 118 | col = 2 119 | row = 20 120 | row = fwdbutton(wri, row, col, TBWScreen, "Wrap", YELLOW) + 2 121 | row = fwdbutton(wri, row, col, TBCScreen, "Clip", BLUE) + 2 122 | fwdbutton(wri, row, col, TBUScreen, "Scroll", GREEN) 123 | CloseButton(wri) 124 | 125 | 126 | def test(): 127 | print("Textbox demo.") 128 | Screen.change(MainScreen) 129 | 130 | 131 | test() 132 | -------------------------------------------------------------------------------- /gui/demos/tstat.py: -------------------------------------------------------------------------------- 1 | # tstat.py micropython-touch demo for the Tstat class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Usage: 7 | # import gui.demos.tstat 8 | 9 | # touch_setup must be imported before other modules because of RAM use. 10 | import touch_setup # Create a display instance 11 | from gui.core.tgui import Screen, ssd 12 | 13 | from gui.widgets import Button, CloseButton, Slider, Label, Meter, Region, LED 14 | from gui.core.writer import CWriter 15 | 16 | # Font for CWriter 17 | import gui.fonts.freesans20 as font 18 | from gui.core.colors import * 19 | 20 | 21 | class BaseScreen(Screen): 22 | def __init__(self): 23 | def btncb(btn, reg, low, high): # Button callbck 24 | reg.adjust(low, high) 25 | 26 | def delete_alarm(btn, ts, reg): 27 | ts.del_region(reg) 28 | 29 | super().__init__() 30 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 31 | col = 20 32 | row = 20 33 | sl = Slider( 34 | wri, 35 | row, 36 | col, 37 | width=20, 38 | callback=self.slider_cb, 39 | bdcolor=RED, 40 | slotcolor=BLUE, 41 | legends=("0.0", "0.5", "1.0"), 42 | ) 43 | self.ts = Meter( 44 | wri, 45 | row, 46 | sl.mcol + 10, 47 | ptcolor=YELLOW, 48 | height=100, 49 | width=15, 50 | style=Meter.BAR, 51 | legends=("0.0", "0.5", "1.0"), 52 | ) 53 | reg = Region(self.ts, 0.4, 0.55, MAGENTA, self.ts_cb) 54 | al = Region(self.ts, 0.9, 1.0, RED, self.al_cb) 55 | col = self.ts.mcol + 10 56 | row = 10 57 | self.alm = LED(wri, row, col, height=20, color=RED, bdcolor=BLACK) 58 | self.led = LED(wri, self.alm.mrow + 5, col, height=20, color=YELLOW, bdcolor=BLACK) 59 | self.grn = LED(wri, self.led.mrow + 5, col, height=20, color=GREEN, bdcolor=BLACK) 60 | col = self.led.mcol + 30 61 | row = 2 62 | btn = Button( 63 | wri, 64 | row, 65 | col, 66 | width=0, 67 | height=30, 68 | text="down", 69 | litcolor=RED, 70 | callback=btncb, 71 | args=(reg, 0.2, 0.3), 72 | ) 73 | btn1 = Button( 74 | wri, 75 | btn.mrow + 5, 76 | col, 77 | width=btn.width, 78 | height=30, 79 | text="up", 80 | litcolor=RED, 81 | callback=btncb, 82 | args=(reg, 0.5, 0.6), 83 | ) 84 | Button( 85 | wri, 86 | btn1.mrow + 5, 87 | col, 88 | width=btn.width, 89 | height=30, 90 | text="del", 91 | litcolor=RED, 92 | callback=delete_alarm, 93 | args=(self.ts, al), 94 | ) 95 | row = sl.mrow + 10 96 | col = 2 97 | self.lbl = Label(wri, row, col, "0.000 ", bdcolor=RED, bgcolor=BLACK) 98 | CloseButton(wri) 99 | 100 | def after_open(self): 101 | self.ts.value(0) # Trigger callback 102 | 103 | def slider_cb(self, s): 104 | if hasattr(self, "lbl"): 105 | v = s() 106 | self.lbl("{:5.3f}".format(v)) 107 | self.ts(v) 108 | 109 | def ts_cb(self, reg, reason): 110 | # Hysteresis 111 | if reason == reg.EX_WA_IB or reason == reg.T_IB: 112 | self.led(False) 113 | self.grn(True) 114 | elif reason == reg.EX_WB_IA or reason == reg.T_IA: 115 | self.led(True) 116 | self.grn(False) 117 | 118 | def al_cb(self, reg, reason): 119 | if reason == reg.EN_WB or reason == reg.T_IA: # Logical OR 120 | self.alm(True) 121 | elif reason & (reg.EX_WB_IB | reg.EX_WA_IB | reg.T_IB): # Bitwise OR alternative 122 | self.alm(False) 123 | 124 | 125 | def test(): 126 | print("Tstat demo.") 127 | Screen.change(BaseScreen) 128 | 129 | 130 | test() 131 | -------------------------------------------------------------------------------- /gui/demos/vtest.py: -------------------------------------------------------------------------------- 1 | # vtest.py Test/demo of Dial for micropython-touch 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # Create SSD instance. Must be done first because of RAM use. 7 | import touch_setup 8 | 9 | import urandom 10 | import time 11 | from cmath import rect, pi 12 | import uasyncio as asyncio 13 | 14 | from gui.core.tgui import Screen, ssd 15 | from gui.core.writer import CWriter 16 | from gui.fonts import font10 17 | from gui.core.colors import * 18 | 19 | # Widgets 20 | from gui.widgets import Label, Button, CloseButton, Pointer, Dial 21 | 22 | 23 | def fwdbutton(wri, row, col, cls_screen, text="Next"): 24 | def fwd(button): 25 | Screen.change(cls_screen) 26 | 27 | Button( 28 | wri, 29 | row, 30 | col, 31 | height=30, 32 | callback=fwd, 33 | fgcolor=BLACK, 34 | bgcolor=GREEN, 35 | text=text, 36 | width=100, 37 | ) 38 | 39 | 40 | class BackScreen(Screen): 41 | def __init__(self): 42 | super().__init__() 43 | wri = CWriter(ssd, font10, GREEN, BLACK, verbose=False) 44 | Label(wri, 2, 2, "Ensure back refreshes properly") 45 | CloseButton(wri) 46 | 47 | 48 | # Update a pointer with a vector. Change pointer color dependent on magnitude. 49 | def vset(ptr, v): 50 | mag = abs(v) 51 | if mag < 0.3: 52 | ptr.value(v, BLUE) 53 | elif mag < 0.7: 54 | ptr.value(v, GREEN) 55 | else: 56 | ptr.value(v, RED) 57 | 58 | 59 | def get_vec(): # Return a random vector 60 | mag = urandom.getrandbits(16) / 2 ** 16 # 0..1 (well, almost 1) 61 | phi = pi * urandom.getrandbits(16) / 2 ** 15 # 0..2*pi 62 | return rect(mag, phi) 63 | 64 | 65 | async def ptr_test(dial): 66 | ptr0 = Pointer(dial) 67 | ptr1 = Pointer(dial) 68 | steps = 20 # No. of interpolation steps 69 | while True: 70 | dv0 = (get_vec() - ptr0.value()) / steps # Interpolation vectors 71 | dv1 = (get_vec() - ptr1.value()) / steps 72 | for _ in range(steps): 73 | vset(ptr0, ptr0.value() + dv0) 74 | vset(ptr1, ptr1.value() + dv1) 75 | await asyncio.sleep_ms(200) 76 | 77 | 78 | # Analog clock demo. 79 | async def aclock(dial, lbldate, lbltim): 80 | uv = lambda phi: rect(1, phi) # Return a unit vector of phase phi 81 | days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") 82 | months = ( 83 | "January", 84 | "February", 85 | "March", 86 | "April", 87 | "May", 88 | "June", 89 | "July", 90 | "August", 91 | "September", 92 | "October", 93 | "November", 94 | "December", 95 | ) 96 | 97 | hrs = Pointer(dial) 98 | mins = Pointer(dial) 99 | secs = Pointer(dial) 100 | 101 | hstart = 0 + 0.7j # Pointer lengths. Position at top. 102 | mstart = 0 + 1j 103 | sstart = 0 + 1j 104 | 105 | while True: 106 | t = time.localtime() 107 | # Maths rotates vectors counterclockwise, hence - signs. 108 | hrs.value(hstart * uv(-t[3] * pi / 6 - t[4] * pi / 360), CYAN) 109 | mins.value(mstart * uv(-t[4] * pi / 30), CYAN) 110 | secs.value(sstart * uv(-t[5] * pi / 30), RED) 111 | lbltim.value("{:02d}.{:02d}.{:02d}".format(t[3], t[4], t[5])) 112 | lbldate.value("{} {} {} {}".format(days[t[6]], t[2], months[t[1] - 1], t[0])) 113 | await asyncio.sleep(1) 114 | 115 | 116 | class VScreen(Screen): 117 | def __init__(self): 118 | super().__init__() 119 | labels = { 120 | "bdcolor": RED, 121 | "fgcolor": WHITE, 122 | "bgcolor": DARKGREEN, 123 | } 124 | 125 | wri = CWriter(ssd, font10, GREEN, BLACK, verbose=False) 126 | 127 | fwdbutton(wri, 200, 2, BackScreen, "Forward") 128 | CloseButton(wri) 129 | # Set up random vector display with two pointers 130 | dial = Dial(wri, 2, 2, height=100, ticks=12, fgcolor=YELLOW, style=Dial.COMPASS) 131 | self.reg_task(ptr_test(dial)) 132 | # Set up clock display: instantiate labels 133 | gap = 4 134 | lbldate = Label(wri, dial.mrow + gap, 2, 200, **labels) 135 | lbltim = Label(wri, lbldate.mrow + gap, 2, 80, **labels) 136 | dial = Dial(wri, 2, 120, height=100, ticks=12, fgcolor=GREEN, pip=GREEN) 137 | self.reg_task(aclock(dial, lbldate, lbltim)) 138 | 139 | 140 | def test(): 141 | if ssd.height < 240 or ssd.width < 320: 142 | print(" This test requires a display of at least 320x240 pixels.") 143 | else: 144 | print("Testing micropython-touch...") 145 | Screen.change(VScreen) 146 | 147 | 148 | test() 149 | -------------------------------------------------------------------------------- /gui/fonts/icons.py: -------------------------------------------------------------------------------- 1 | # Code generated by font_to_py.py. 2 | # Font: icofont.ttf Char set: ABCDEF 3 | # Cmd: /mnt/qnap2/data/Projects/MicroPython/micropython-font-to-py/font_to_py.py icofont.ttf 12 -x -c ABCDEF icons1.py 4 | version = '0.33' 5 | 6 | def height(): 7 | return 12 8 | 9 | def baseline(): 10 | return 12 11 | 12 | def max_width(): 13 | return 15 14 | 15 | def hmap(): 16 | return True 17 | 18 | def reverse(): 19 | return False 20 | 21 | def monospaced(): 22 | return False 23 | 24 | def min_ch(): 25 | return 65 26 | 27 | def max_ch(): 28 | return 70 29 | 30 | _font =\ 31 | b'\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ 32 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x3f\xe0'\ 33 | b'\x3f\xe0\x3f\xe0\x3f\xe0\x3f\xe0\x3f\xe0\x3f\xe0\x3f\xe0\x3f\xe0'\ 34 | b'\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x61\x80\x61\x80\x61\x80'\ 35 | b'\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x00\x00\x0f\x00'\ 36 | b'\x00\x00\x70\x00\x7f\x00\x0f\xf0\x00\xfc\x00\x7c\x01\xf0\x0f\x80'\ 37 | b'\x7e\x00\x70\x00\x40\x00\x00\x00\x0f\x00\x00\x06\x00\x3e\x01\xf8'\ 38 | b'\x0f\xc0\x3e\x00\x78\x00\x7e\x00\x1f\xc0\x01\xfc\x00\x1e\x00\x00'\ 39 | b'\x00\x00\x0f\x00\x00\x7c\x3f\xfc\x3f\xfc\x3c\x0c\x30\x0c\x30\x0c'\ 40 | b'\x30\x0c\x30\x1c\x38\x1c\x38\x1c\x38\x00\x00\x00\x0f\x00\x00\x00'\ 41 | b'\x70\x00\x7c\x00\x7f\x80\x7f\xe0\x7f\xfc\x7f\xf0\x7f\xc0\x7f\x00'\ 42 | b'\x78\x00\x60\x00\x00\x00' 43 | 44 | _index =\ 45 | b'\x00\x00\x1a\x00\x34\x00\x4e\x00\x68\x00\x82\x00\x9c\x00\xb6\x00'\ 46 | 47 | _mvfont = memoryview(_font) 48 | _mvi = memoryview(_index) 49 | ifb = lambda l : l[0] | (l[1] << 8) 50 | 51 | def get_ch(ch): 52 | oc = ord(ch) 53 | ioff = 2 * (oc - 65 + 1) if oc >= 65 and oc <= 70 else 0 54 | doff = ifb(_mvi[ioff : ]) 55 | width = ifb(_mvfont[doff : ]) 56 | 57 | next_offs = doff + 2 + ((width - 1)//8 + 1) * 12 58 | return _mvfont[doff + 2:next_offs], 12, width 59 | 60 | -------------------------------------------------------------------------------- /gui/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | _attrs = { 2 | "Button": "buttons", 3 | "CloseButton": "buttons", 4 | "ButtonList": "buttons", 5 | "RadioButtons": "buttons", 6 | "Checkbox": "checkbox", 7 | "Dial": "dial", 8 | "Pointer": "dial", 9 | "DialogBox": "dialog", 10 | "Dropdown": "dropdown", 11 | "Knob": "knob", 12 | "Label": "label", 13 | "LED": "led", 14 | "Listbox": "listbox", 15 | "SubMenu": "menu", 16 | "Menu": "menu", 17 | "Meter": "meter", 18 | "Region": "region", 19 | "Pad": "pad", 20 | "ScaleLog": "scale_log", 21 | "Scale": "scale", 22 | "Slider": "sliders", 23 | "HorizSlider": "sliders", 24 | "Textbox": "textbox", 25 | "BitMap": "bitmap", 26 | "QRMap": "qrcode", 27 | "Grid": "grid", 28 | } 29 | 30 | # Lazy loader, effectively does: 31 | # global attr 32 | # from .mod import attr 33 | # Filched from uasyncio.__init__.py 34 | 35 | 36 | def __getattr__(attr): 37 | mod = _attrs.get(attr, None) 38 | if mod is None: 39 | raise AttributeError(attr) 40 | value = getattr(__import__(mod, None, None, True, 1), attr) 41 | globals()[attr] = value 42 | return value 43 | -------------------------------------------------------------------------------- /gui/widgets/bitmap.py: -------------------------------------------------------------------------------- 1 | # bitmap.py Provides the BMG (bitmapped graphics) class 2 | # Released under the MIT License (MIT). See LICENSE. 3 | # Copyright (c) 2022 Peter Hinch 4 | 5 | # Graphics are files created by Linux bitmap utility. 6 | # Widget writes data direct to the FrameBuffer. 7 | # There is no scaling: declared size of the widget must exactly 8 | # match the size of the bitmap. 9 | 10 | from gui.core.tgui import Widget 11 | from gui.core.colors import * 12 | from gui.core.tgui import ssd 13 | 14 | 15 | class BitMap(Widget): 16 | def __init__( 17 | self, writer, row, col, height, width, *, fgcolor=None, bgcolor=None, bdcolor=RED 18 | ): 19 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 20 | 21 | def show(self): 22 | if not super().show(True): # Draw or erase border 23 | return 24 | if self._value is None: 25 | return 26 | with open(self._value, "r") as f: 27 | g = self._gen_bytes(f) 28 | bit = 1 29 | wrap = False 30 | for row in range(self.height): 31 | if not wrap: 32 | byte = next(g) # Each row starts on a new byte 33 | bit = 1 34 | for col in range(self.width): 35 | c = self.fgcolor if byte & bit else self.bgcolor 36 | ssd.pixel(self.col + col, self.row + row, c) 37 | wrap = (bit := bit << 1) == 0x100 38 | if wrap: 39 | byte = next(g) 40 | bit = 1 41 | 42 | def _gen_bytes(self, f): # Yield data bytes from file stream 43 | f.readline() 44 | f.readline() # Advance file pointer to data start 45 | s = f.readline() 46 | if not s.startswith("static"): 47 | raise ValueError("Bad file format.") 48 | while s := f.readline(): 49 | if (lb := s.find("}")) != -1: 50 | s = s[:lb] # Strip trailing }; 51 | p = s.strip().split(",") 52 | for x in p: 53 | if x: 54 | yield int(x, 16) 55 | 56 | # Get height/width dimension from file stream. 57 | def _get_dim(self, f, name): 58 | s = f.readline() 59 | elements = s.split(" ") 60 | if not elements[1].endswith(name): 61 | raise ValueError("Bad file format.") 62 | return int(elements[2]) 63 | 64 | def _validate(self, fn): 65 | if not isinstance(fn, str): 66 | raise ValueError("Value must be a filename.") 67 | with open(fn, "r") as f: 68 | wd = self._get_dim(f, "width") 69 | ht = self._get_dim(f, "height") 70 | if not (wd == self.width and ht == self.height): 71 | raise ValueError( 72 | f"Object dimensions {ht}x{wd} do not match widget {self.height}x{self.width}" 73 | ) 74 | 75 | def value(self, fn): 76 | self._validate(fn) # Throws on failure 77 | super().value(fn) 78 | 79 | def color(self, fgcolor=None, bgcolor=None): 80 | if fgcolor is not None: 81 | self.fgcolor = fgcolor 82 | if bgcolor is not None: 83 | self.bgcolor = bgcolor 84 | self.draw = True 85 | -------------------------------------------------------------------------------- /gui/widgets/checkbox.py: -------------------------------------------------------------------------------- 1 | # checkbox.py Extension to ugui providing the Checkbox class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2019-2021 Peter Hinch 5 | 6 | from gui.core.tgui import Widget, display 7 | 8 | dolittle = lambda *_: None 9 | 10 | 11 | class Checkbox(Widget): 12 | def __init__( 13 | self, 14 | writer, 15 | row, 16 | col, 17 | *, 18 | height=30, 19 | fillcolor=None, 20 | fgcolor=None, 21 | bgcolor=None, 22 | bdcolor=False, 23 | callback=dolittle, 24 | args=[], 25 | value=False, 26 | active=True 27 | ): 28 | super().__init__( 29 | writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active 30 | ) 31 | super()._set_callbacks(callback, args) 32 | self.fillcolor = fillcolor 33 | 34 | def show(self): 35 | if super().show(): 36 | x = self.col 37 | y = self.row 38 | ht = self.height 39 | x1 = x + ht - 1 40 | y1 = y + ht - 1 41 | if self._value: 42 | if self.fillcolor is not None: 43 | display.fill_rect(x, y, ht, ht, self.fillcolor) 44 | else: 45 | display.fill_rect(x, y, ht, ht, self.bgcolor) 46 | display.rect(x, y, ht, ht, self.fgcolor) 47 | if self.fillcolor is None and self._value: 48 | display.line(x, y, x1, y1, self.fgcolor) 49 | display.line(x, y1, x1, y, self.fgcolor) 50 | 51 | def _touched(self, rr, rc): 52 | self.value(not self._value) # Upddate and refresh 53 | -------------------------------------------------------------------------------- /gui/widgets/dial.py: -------------------------------------------------------------------------------- 1 | # dial.py Dial and Pointer classes for micro-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | import cmath 7 | from gui.core.tgui import Widget, display 8 | from gui.widgets.label import Label 9 | 10 | # Line defined by polar coords; origin and line are complex 11 | def polar(display, origin, line, color): 12 | xs, ys = origin.real, origin.imag 13 | theta = cmath.polar(line)[1] 14 | display.line(round(xs), round(ys), round(xs + line.real), round(ys - line.imag), color) 15 | 16 | 17 | def conj(v): # complex conjugate 18 | return v.real - v.imag * 1j 19 | 20 | 21 | # Draw an arrow; origin and vec are complex, scalar lc defines length of chevron. 22 | # cw and ccw are unit vectors of +-3pi/4 radians for chevrons (precompiled) 23 | def arrow( 24 | display, 25 | origin, 26 | vec, 27 | lc, 28 | color, 29 | ccw=cmath.exp(3j * cmath.pi / 4), 30 | cw=cmath.exp(-3j * cmath.pi / 4), 31 | ): 32 | length, theta = cmath.polar(vec) 33 | uv = cmath.rect(1, theta) # Unit rotation vector 34 | start = -vec 35 | if length > 3 * lc: # If line is long 36 | ds = cmath.rect(lc, theta) 37 | start += ds # shorten to allow for length of tail chevrons 38 | chev = lc + 0j 39 | polar(display, origin, vec, color) # Origin to tip 40 | polar(display, origin, start, color) # Origin to tail 41 | polar(display, origin + conj(vec), chev * ccw * uv, color) # Tip chevron 42 | polar(display, origin + conj(vec), chev * cw * uv, color) 43 | if length > lc: # Confusing appearance of very short vectors with tail chevron 44 | polar(display, origin + conj(start), chev * ccw * uv, color) # Tail chevron 45 | polar(display, origin + conj(start), chev * cw * uv, color) 46 | 47 | 48 | class Pointer: 49 | def __init__(self, dial): 50 | self.dial = dial 51 | dial.vectors.add(self) 52 | self.val = 0 + 0j 53 | self.color = None 54 | 55 | def value(self, v=None, color=None): 56 | if color is not None: 57 | self.color = color 58 | if v is not None: 59 | if isinstance(v, complex): 60 | l = cmath.polar(v)[0] 61 | if l > 1: 62 | self.val = v / l 63 | else: 64 | self.val = v 65 | else: 66 | raise ValueError("Pointer value must be complex.") 67 | self.dial.draw = True 68 | return self.val 69 | 70 | 71 | class Dial(Widget): 72 | CLOCK = 0 73 | COMPASS = 1 74 | 75 | def __init__( 76 | self, 77 | writer, 78 | row, 79 | col, 80 | *, 81 | height=100, 82 | fgcolor=None, 83 | bgcolor=None, 84 | bdcolor=False, 85 | ticks=4, 86 | label=None, 87 | style=0, 88 | pip=None 89 | ): 90 | super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) 91 | self.style = style 92 | self.pip = self.fgcolor if pip is None else pip 93 | if label is not None: 94 | self.label = Label(writer, row + height + 3, col, label) 95 | # Adjust metrics 96 | self.mrow = self.label.mrow - 2 # Label never has border 97 | self.mcol = max(self.mcol, self.label.mcol - 2) 98 | radius = int(height / 2) 99 | self.radius = radius 100 | self.ticks = ticks 101 | self.xorigin = col + radius 102 | self.yorigin = row + radius 103 | self.vectors = set() 104 | 105 | def show(self): 106 | if super().show(): 107 | # cache bound variables 108 | ticks = self.ticks 109 | radius = self.radius 110 | xo = self.xorigin 111 | yo = self.yorigin 112 | # vectors (complex) 113 | vor = xo + 1j * yo 114 | vtstart = 0.9 * radius + 0j # start of tick 115 | vtick = 0.1 * radius + 0j # tick 116 | vrot = cmath.exp(2j * cmath.pi / ticks) # unit rotation 117 | for _ in range(ticks): 118 | polar(display, vor + conj(vtstart), vtick, self.fgcolor) 119 | vtick *= vrot 120 | vtstart *= vrot 121 | display.circle(xo, yo, radius, self.fgcolor) 122 | vshort = 1000 # Length of shortest vector 123 | for v in self.vectors: 124 | color = self.fgcolor if v.color is None else v.color 125 | val = v.value() * radius # val is complex 126 | vshort = min(vshort, cmath.polar(val)[0]) 127 | if self.style == Dial.CLOCK: 128 | polar(display, vor, val, color) 129 | else: 130 | arrow(display, vor, val, 5, color) 131 | if isinstance(self.pip, int) and vshort > 5: 132 | display.fillcircle(xo, yo, 2, self.pip) 133 | -------------------------------------------------------------------------------- /gui/widgets/dialog.py: -------------------------------------------------------------------------------- 1 | # dialog.py Extension to ugui providing the DialogBox class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | from gui.core.tgui import display, Window, Screen 7 | from gui.core.colors import * 8 | from gui.widgets.label import Label 9 | from gui.widgets.buttons import Button, CloseButton 10 | 11 | dolittle = lambda *_: None 12 | 13 | 14 | class DialogBox(Window): 15 | def __init__( 16 | self, 17 | writer, 18 | row=20, 19 | col=20, 20 | *, 21 | elements, 22 | label=None, 23 | bgcolor=DARKGREEN, 24 | buttonwidth=25, 25 | closebutton=True, 26 | callback=dolittle, 27 | args=[] 28 | ): 29 | def back(button, text): # Callback for normal buttons 30 | Window.value(text) 31 | callback(Window, *args) 32 | Screen.back() 33 | 34 | def backbutton(button, text): 35 | Window.value(text) 36 | callback(Window, *args) 37 | 38 | height = 80 39 | spacing = 5 40 | buttonwidth = max(max(writer.stringlen(e[0]) for e in elements) + 14, buttonwidth) 41 | buttonheight = max(writer.height, 15) 42 | nelements = len(elements) 43 | width = spacing + (buttonwidth + spacing) * nelements 44 | if label is not None: 45 | width = max(width, writer.stringlen(label) + 2 * spacing) 46 | super().__init__(row, col, height, width, bgcolor=bgcolor) 47 | 48 | col = spacing # Coordinates relative to window 49 | row = self.height - buttonheight - 10 50 | gap = 0 51 | if nelements > 1: 52 | gap = ((width - 2 * spacing) - nelements * buttonwidth) // (nelements - 1) 53 | if label is not None: 54 | r, c = self.locn(10, col) 55 | Label(writer, r, c, label, bgcolor=bgcolor) 56 | for text, color in elements: 57 | Button( 58 | writer, 59 | *self.locn(row, col), 60 | height=buttonheight, 61 | width=buttonwidth, 62 | textcolor=BLACK, 63 | bgcolor=color, 64 | fgcolor=color, 65 | bdcolor=color, 66 | text=text, 67 | shape=RECTANGLE, 68 | callback=back, 69 | args=(text,) 70 | ) 71 | col += buttonwidth + gap 72 | 73 | if closebutton: 74 | CloseButton(writer, callback=backbutton, args=("Close",)) 75 | -------------------------------------------------------------------------------- /gui/widgets/grid.py: -------------------------------------------------------------------------------- 1 | # grid.py micro-gui widget providing the Grid class: a 2d array of Label instances. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023 Peter Hinch 5 | 6 | from gui.core.tgui import Widget, display 7 | from gui.core.writer import Writer 8 | from gui.core.colors import * 9 | from gui.widgets import Label 10 | from .parse2d import do_args 11 | 12 | # Given a slice and a maximum address return start and stop addresses (or None on error) 13 | # Step value must be 1, hence does not support start > stop (used with step < 0) 14 | def _do_slice(sli, nbytes): 15 | if not (sli.step is None or sli.step == 1): 16 | raise NotImplementedError("only slices with step=1 (or None) are supported") 17 | start = sli.start if sli.start is not None else 0 18 | stop = sli.stop if sli.stop is not None else nbytes 19 | start = min(start if start >= 0 else max(nbytes + start, 0), nbytes) 20 | stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes) 21 | return (start, stop) if start < stop else None # Caller should check 22 | 23 | 24 | # lwidth may be integer Label width in pixels or a tuple/list of widths 25 | class Grid(Widget): 26 | def __init__( 27 | self, 28 | writer, 29 | row, 30 | col, 31 | lwidth, 32 | nrows, 33 | ncols, 34 | invert=False, 35 | fgcolor=None, 36 | bgcolor=BLACK, 37 | bdcolor=None, 38 | justify=0, 39 | ): 40 | self.nrows = nrows 41 | self.ncols = ncols 42 | self.ncells = nrows * ncols 43 | self.cheight = writer.height + 4 # Cell height including borders 44 | # Build column width list. Column width is Label width + 4. 45 | if isinstance(lwidth, int): 46 | self.cwidth = [lwidth + 4] * ncols 47 | else: # Ensure len(.cwidth) == ncols 48 | self.cwidth = [w + 4 for w in lwidth][:ncols] 49 | self.cwidth.extend([lwidth[-1] + 4] * (ncols - len(lwidth))) 50 | width = sum(self.cwidth) - 4 # Dimensions of widget interior 51 | height = nrows * self.cheight - 4 52 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 53 | self.cells = [] 54 | r = row 55 | c = col 56 | for _ in range(self.nrows): 57 | for cw in self.cwidth: 58 | self.cells.append( 59 | Label(writer, r, c, cw - 4, invert, fgcolor, bgcolor, False, justify) 60 | ) # No border 61 | c += cw 62 | r += self.cheight 63 | c = col 64 | 65 | def __call__(self, row, col=None): # Return a single Label 66 | return self.cells[row if col is None else col + row * self.ncols] 67 | 68 | def __getitem__(self, *args): 69 | indices = do_args(args, self.nrows, self.ncols) 70 | for i in indices: 71 | yield self.cells[i] 72 | 73 | # allow grid[[r, c]] = "foo" or kwargs for Label: 74 | # grid[[r, c]] = {"text": str(n), "fgcolor" : RED} 75 | def __setitem__(self, *args): 76 | x = args[1] # Value 77 | indices = do_args(args[:-1], self.nrows, self.ncols) 78 | for i in indices: 79 | try: 80 | z = next(x) # May be a generator 81 | except StopIteration: 82 | pass # Repeat last value 83 | except TypeError: 84 | z = x 85 | v = self.cells[i].value # method of Label 86 | _ = v(**z) if isinstance(x, dict) else v(z) 87 | 88 | def show(self): 89 | super().show() # Draw border 90 | if self.has_border: # Draw grid 91 | color = self.bdcolor 92 | x = self.col - 2 # Border top left corner 93 | y = self.row - 2 94 | dy = self.cheight 95 | for row in range(1, self.nrows): 96 | display.hline(x, y + row * dy, self.width + 4, color) 97 | for cw in self.cwidth[:-1]: 98 | x += cw 99 | display.vline(x, y, self.height + 4, color) 100 | -------------------------------------------------------------------------------- /gui/widgets/knob.py: -------------------------------------------------------------------------------- 1 | # knob.py Extension to microgui providing a control knob (rotary potentiometer) widget 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | from gui.core.tgui import LinearIO, display 7 | import math 8 | 9 | TWOPI = 2 * math.pi 10 | # Null function 11 | dolittle = lambda *_: None 12 | 13 | # *********** CONTROL KNOB CLASS *********** 14 | 15 | 16 | class Knob(LinearIO): 17 | def __init__( 18 | self, 19 | writer, 20 | row, 21 | col, 22 | *, 23 | height=70, 24 | arc=TWOPI, 25 | ticks=9, 26 | value=0.0, 27 | fgcolor=None, 28 | bgcolor=None, 29 | color=None, 30 | bdcolor=None, 31 | callback=dolittle, 32 | args=[], 33 | active=True 34 | ): 35 | super().__init__( 36 | writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active, 0.1 37 | ) 38 | super()._set_callbacks(callback, args) 39 | radius = height / 2 40 | self.arc = min(max(arc, 0), TWOPI) # Usable angle of control 41 | self.radius = radius 42 | self.xorigin = col + radius 43 | self.yorigin = row + radius 44 | self.ticklen = 0.1 * radius 45 | self.pointerlen = radius - self.ticklen - 5 46 | self.ticks = max(ticks, 2) # start and end of travel 47 | self.color = color 48 | self.draw = True # Ensure a redraw on next refresh 49 | # Run callback (e.g. to set dynamic colors) 50 | self.callback(self, *self.args) 51 | 52 | def show(self): 53 | if super().show(False): # Honour bgcolor 54 | arc = self.arc 55 | ticks = self.ticks 56 | radius = self.radius 57 | ticklen = self.ticklen 58 | for tick in range(ticks): 59 | theta = (tick / (ticks - 1)) * arc - arc / 2 60 | x_start = int(self.xorigin + radius * math.sin(theta)) 61 | y_start = int(self.yorigin - radius * math.cos(theta)) 62 | x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta)) 63 | y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta)) 64 | display.line(x_start, y_start, x_end, y_end, self.fgcolor) 65 | if self.color is not None: 66 | display.fillcircle(self.xorigin, self.yorigin, radius - ticklen, self.color) 67 | display.circle(self.xorigin, self.yorigin, radius - ticklen, self.fgcolor) 68 | display.circle(self.xorigin, self.yorigin, radius - ticklen - 3, self.fgcolor) 69 | 70 | self._drawpointer(self._value, self.fgcolor) # draw new 71 | 72 | def _drawpointer(self, value, color): 73 | arc = self.arc 74 | length = self.pointerlen 75 | angle = value * arc - arc / 2 76 | x_end = int(self.xorigin + length * math.sin(angle)) 77 | y_end = int(self.yorigin - length * math.cos(angle)) 78 | display.line(int(self.xorigin), int(self.yorigin), x_end, y_end, color) 79 | -------------------------------------------------------------------------------- /gui/widgets/label.py: -------------------------------------------------------------------------------- 1 | # label.py Extension to micro-gui providing the Label class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | from gui.core.tgui import Widget, display 6 | from gui.core.writer import Writer 7 | from gui.core.colors import * 8 | 9 | # text: str display string int save width 10 | class Label(Widget): 11 | LEFT = 0 12 | CENTRE = 1 13 | RIGHT = 2 14 | 15 | def __init__( 16 | self, 17 | writer, 18 | row, 19 | col, 20 | text, 21 | invert=False, 22 | fgcolor=None, 23 | bgcolor=BLACK, 24 | bdcolor=False, 25 | justify=0, 26 | ): 27 | self.writer = writer 28 | self.justify = justify 29 | # Determine width of object 30 | if isinstance(text, int): 31 | width = text 32 | text = None 33 | else: 34 | width = writer.stringlen(text) 35 | height = writer.height 36 | self.invert = invert 37 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 38 | self.tcol = col 39 | if text is not None: 40 | self.value(text, invert) 41 | 42 | def value( 43 | self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None, justify=None 44 | ): 45 | if text is not None: 46 | sl = self.writer.stringlen(text) 47 | if justify is None: 48 | justify = self.justify 49 | self.tcol = self.col # Default is left justify 50 | if sl > self.width: # Clip 51 | font = self.writer.font 52 | pos = 0 53 | n = 0 54 | for ch in text: 55 | pos += font.get_ch(ch)[2] # width of current char 56 | if pos > self.width: 57 | break 58 | n += 1 59 | text = text[:n] 60 | elif justify == 1: # Centre 61 | self.tcol = self.col + (self.width - sl) // 2 62 | elif justify == 2: # Right 63 | self.tcol = self.col + self.width - sl 64 | 65 | txt = super().value(text) 66 | self.draw = True # Redraw unconditionally: colors may have changed. 67 | self.invert = invert 68 | self.fgcolor = self.def_fgcolor if fgcolor is None else fgcolor 69 | self.bgcolor = self.def_bgcolor if bgcolor is None else bgcolor 70 | if bdcolor is False: 71 | self.def_bdcolor = False 72 | self.bdcolor = self.def_bdcolor if bdcolor is None else bdcolor 73 | return txt 74 | 75 | def show(self): # Passive: no need to test show return value. 76 | super().show(False) # Honour background. Draw or erase border 77 | if isinstance(txt := super().value(), str): 78 | display.print_left( 79 | self.writer, self.tcol, self.row, txt, self.fgcolor, self.bgcolor, self.invert 80 | ) 81 | -------------------------------------------------------------------------------- /gui/widgets/led.py: -------------------------------------------------------------------------------- 1 | # led.py Extension to ugui providing the LED class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | from gui.core.tgui import Widget, display 7 | from gui.core.colors import * 8 | 9 | 10 | class LED(Widget): 11 | def __init__( 12 | self, writer, row, col, *, height=30, fgcolor=None, bgcolor=None, bdcolor=False, color=RED 13 | ): 14 | super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, False) 15 | self._value = False 16 | self._color = color 17 | self.radius = self.height // 2 18 | self.x = col + self.radius 19 | self.y = row + self.radius 20 | 21 | def show(self): 22 | if super().show(): # Draw or erase border 23 | color = self._color if self._value else BLACK 24 | display.fillcircle(int(self.x), int(self.y), int(self.radius), color) 25 | display.circle(int(self.x), int(self.y), int(self.radius), self.fgcolor) 26 | 27 | def color(self, color): 28 | self._color = color 29 | self.draw = True 30 | -------------------------------------------------------------------------------- /gui/widgets/menu.py: -------------------------------------------------------------------------------- 1 | # menu.py Extension to micro-gui providing the Menu class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | # Usage: 7 | # from gui.widgets.menu import Menu 8 | 9 | from gui.core.tgui import Window, Screen, display 10 | from gui.widgets.buttons import Button 11 | from gui.widgets.listbox import Listbox 12 | from gui.core.colors import * 13 | 14 | # A SubMenu is a Window containing a Listbox 15 | # Next and Prev close the listbox without running the callback. This is 16 | # handled by Screen .move bound method 17 | class SubMenu(Window): 18 | def __init__(self, menu, button, elements): # menu is parent Menu 19 | self.menu = menu 20 | self.button = button 21 | wri = menu.writer 22 | row = button.height + 2 23 | col = button.col # Drop down below top level menu button 24 | # Need to determine Window dimensions from size of Listbox, which 25 | # depends on number and length of elements. 26 | te = [x[0] for x in elements] # Text part 27 | self.elements = elements 28 | entry_height, lb_height, _, textwidth = Listbox.dimensions(wri, te, None) 29 | lb_width = textwidth + 2 30 | # Calculate Window dimensions 31 | ap_height = lb_height + 6 # Allow for listbox border 32 | ap_width = lb_width + 6 33 | super().__init__(row, col, ap_height, ap_width, draw_border=False) 34 | Listbox( 35 | wri, 36 | row + 3, 37 | col + 3, 38 | elements=te, 39 | width=lb_width, 40 | fgcolor=button.fgcolor, 41 | bgcolor=button.bgcolor, 42 | bdcolor=False, 43 | fontcolor=button.textcolor, 44 | select_color=menu.select_color, 45 | callback=self.callback, 46 | ) 47 | 48 | def callback(self, lbox): 49 | Screen.back() 50 | el = self.elements[lbox.value()] # (text, cb, args) 51 | if len(el) == 2: # Recurse into submenu 52 | args = (self.menu, self.button, el[1]) 53 | Screen.change(SubMenu, args=args) 54 | else: 55 | el[1](lbox, *el[2]) 56 | 57 | 58 | # A Menu is a set of Button objects at the top of the screen. On press, Buttons either run the 59 | # user callback or instantiate a SubMenu 60 | # args is a list comprising items which may be a mixture of two types 61 | # Single items: (top_text, cb, (args, ...)) 62 | # Submenus: (top_text, ((mnu_text, cb, (args, ...)),(mnu_text, cb, (args, ...)),...) 63 | class Menu: 64 | def __init__( 65 | self, 66 | writer, 67 | *, 68 | height=25, 69 | bgcolor=None, 70 | fgcolor=None, 71 | textcolor=None, 72 | select_color=DARKBLUE, 73 | args 74 | ): 75 | self.writer = writer 76 | self.select_color = select_color 77 | row = 2 78 | col = 2 79 | btn = { 80 | "bgcolor": bgcolor, 81 | "fgcolor": fgcolor, 82 | "height": height, 83 | "textcolor": textcolor, 84 | } 85 | for arg in args: 86 | if len(arg) == 2: # Handle submenu 87 | # txt, ((element, cb, (cbargs,)),(element,cb, (cbargs,)), ..) = arg 88 | b = Button(writer, row, col, text=arg[0], callback=self.cb, args=arg, **btn) 89 | else: 90 | txt, cb, cbargs = arg 91 | b = Button(writer, row, col, text=txt, callback=cb, args=cbargs, **btn) 92 | col = b.mcol 93 | 94 | def cb(self, button, txt, elements): # Button pushed which calls submenu 95 | args = (self, button, elements) 96 | Screen.change(SubMenu, args=args) 97 | -------------------------------------------------------------------------------- /gui/widgets/meter.py: -------------------------------------------------------------------------------- 1 | # meter.py Extension to ugui providing a linear "meter" widget. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | from gui.core.tgui import Widget, display 7 | from gui.widgets.label import Label 8 | from gui.core.colors import * 9 | 10 | # Null function 11 | dolittle = lambda *_: None 12 | 13 | 14 | class Meter(Widget): 15 | BAR = 1 16 | LINE = 0 17 | 18 | def __init__( 19 | self, 20 | writer, 21 | row, 22 | col, 23 | *, 24 | height=50, 25 | width=10, 26 | fgcolor=None, 27 | bgcolor=BLACK, 28 | ptcolor=None, 29 | bdcolor=None, 30 | divisions=5, 31 | label=None, 32 | style=0, 33 | legends=None, 34 | value=0 35 | ): 36 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 37 | self.divisions = divisions 38 | if label is not None: 39 | # Ensure bottom legend has space 40 | Label(writer, row + height + writer.height // 2, col, label) 41 | self.style = style 42 | self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor 43 | if legends is not None: # Legends are static 44 | mcol = 0 45 | x = col + width + 4 46 | y = row + height 47 | dy = 0 if len(legends) <= 1 else height / (len(legends) - 1) 48 | yl = y - writer.height / 2 # Start at bottom 49 | for legend in legends: 50 | l = Label(writer, round(yl), x, legend) 51 | yl -= dy 52 | mcol = max(mcol, l.mcol) 53 | self.mcol = mcol - 2 # For metrics. Legends never have border. 54 | self.regions = set() 55 | self.value(value) 56 | 57 | def value(self, n=None, color=None): 58 | if n is None: 59 | return super().value() 60 | n = super().value(min(1, max(0, n))) 61 | if color is not None: 62 | self.ptcolor = color 63 | for r in self.regions: 64 | r.check(n) 65 | return n 66 | 67 | def show(self): 68 | if super().show(): # Draw or erase border 69 | val = super().value() 70 | wri = self.writer 71 | width = self.width 72 | height = self.height 73 | x0 = self.col 74 | x1 = self.col + width 75 | y0 = self.row 76 | y1 = self.row + height 77 | for r in self.regions: 78 | ht = round(height * (r.vhi - r.vlo)) 79 | yr = y1 - round(height * r.vhi) 80 | display.fill_rect(x0, yr, width, ht, r.color) 81 | if self.divisions > 0: 82 | dy = height / (self.divisions) # Tick marks 83 | for tick in range(self.divisions + 1): 84 | ypos = int(y0 + dy * tick) 85 | display.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) 86 | 87 | y = int(y1 - val * height) # y position of slider 88 | if self.style == self.LINE: 89 | display.hline(x0, y, width, self.ptcolor) # Draw pointer 90 | else: 91 | w = width / 2 92 | display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) 93 | 94 | def del_region(self, reg): 95 | self.regions.discard(reg) 96 | self.draw = True 97 | -------------------------------------------------------------------------------- /gui/widgets/pad.py: -------------------------------------------------------------------------------- 1 | # buttons.py Extension to ugui providing pushbutton classes 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | import asyncio 7 | from gui.core.tgui import Screen, Widget, display 8 | from gui.core.colors import * 9 | 10 | dolittle = lambda *_: None 11 | 12 | 13 | class Pad(Widget): 14 | long_press_time = 1000 15 | 16 | def __init__( 17 | self, 18 | writer, 19 | row, 20 | col, 21 | *, 22 | height=20, 23 | width=50, 24 | onrelease=True, 25 | callback=None, 26 | args=[], 27 | lp_callback=None, 28 | lp_args=[] 29 | ): 30 | super().__init__(writer, row, col, height, width, None, None, None, False, True) 31 | self.callback = (lambda *_: None) if callback is None else callback 32 | self.callback_args = args 33 | self.onrelease = onrelease 34 | self.lp_callback = lp_callback 35 | self.lp_args = lp_args 36 | self.lp_task = None # Long press not in progress 37 | 38 | def show(self): 39 | pass 40 | 41 | def _touched(self, rr, rc): # Process touch 42 | self.rr = rr # Save coordinates of last touch (in pixels relative to Pad origin) 43 | self.rc = rc 44 | if self.lp_callback is not None: 45 | self.lp_task = asyncio.create_task(self.longpress()) 46 | if not self.onrelease: 47 | self.callback(self, *self.callback_args) # Callback not a bound method so pass self 48 | 49 | def _untouched(self): 50 | if self.lp_task is not None: 51 | self.lp_task.cancel() 52 | self.lp_task = None 53 | if self.onrelease: 54 | self.callback(self, *self.callback_args) # Callback not a bound method so pass self 55 | 56 | async def longpress(self): 57 | await asyncio.sleep_ms(Pad.long_press_time) 58 | self.lp_callback(self, *self.lp_args) 59 | -------------------------------------------------------------------------------- /gui/widgets/parse2d.py: -------------------------------------------------------------------------------- 1 | # parse2d.py Parse args for item access dunder methods for a 2D array. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2023 Peter Hinch 5 | 6 | 7 | # Called from __getitem__ or __setitem__ args is a 1-tuple. The single item may be an int or a 8 | # slice for 1D access. Or it may be a 2-tuple for 2D access. Items in the 2-tuple may be ints 9 | # or slices in any combination. 10 | # As a generator it returns offsets into the underlying 1D array or list. 11 | def do_args(args, nrows, ncols): 12 | # Given a slice and a maximum address return start and stop addresses (or None on error) 13 | # Step value must be 1, hence does not support start > stop (used with step < 0) 14 | def do_slice(sli, nbytes): 15 | step = sli.step if sli.step is not None else 1 16 | start = sli.start if sli.start is not None else 0 17 | stop = sli.stop if sli.stop is not None else nbytes 18 | start = min(start if start >= 0 else max(nbytes + start, 0), nbytes) 19 | stop = min(stop if stop >= 0 else max(nbytes + stop, 0), nbytes) 20 | ok = (start < stop and step > 0) or (start > stop and step < 0) 21 | return (start, stop, step) if ok else None # Caller should check 22 | 23 | def ivalid(n, nmax): # Validate an integer arg, handle -ve args 24 | n = n if n >= 0 else nmax + n 25 | if n < 0 or n > nmax - 1: 26 | raise IndexError("Index out of range") 27 | return n 28 | 29 | def fail(n): 30 | raise IndexError("Invalid index", n) 31 | 32 | ncells = nrows * ncols 33 | n = args[0] 34 | if isinstance(n, int): # Index into 1D array 35 | yield ivalid(n, ncells) 36 | elif isinstance(n, slice): # Slice of 1D array 37 | cells = do_slice(n, ncells) 38 | if cells is not None: 39 | for cell in range(*cells): 40 | yield cell 41 | elif isinstance(n, tuple) or isinstance(n, list): # list allows for old [[]] syntax 42 | if len(n) != 2: 43 | fail(n) 44 | row = n[0] # May be slice 45 | if isinstance(row, int): 46 | row = ivalid(row, nrows) 47 | col = n[1] 48 | if isinstance(col, int): 49 | col = ivalid(col, ncols) 50 | if isinstance(row, int) and isinstance(col, int): 51 | yield row * ncols + col 52 | elif isinstance(row, slice) and isinstance(col, int): 53 | rows = do_slice(row, nrows) 54 | if rows is not None: 55 | for row in range(*rows): 56 | yield row * ncols + col 57 | elif isinstance(row, int) and isinstance(col, slice): 58 | cols = do_slice(col, ncols) 59 | if cols is not None: 60 | for col in range(*cols): 61 | yield row * ncols + col 62 | elif isinstance(row, slice) and isinstance(col, slice): 63 | rows = do_slice(row, nrows) 64 | cols = do_slice(col, ncols) 65 | if cols is not None and rows is not None: 66 | for row in range(*rows): 67 | for col in range(*cols): 68 | yield row * ncols + col 69 | else: 70 | fail(n) 71 | else: 72 | fail(n) 73 | -------------------------------------------------------------------------------- /gui/widgets/qrcode.py: -------------------------------------------------------------------------------- 1 | # qrcode.py Provides the QRMap widget to display the output of uQR library. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2022-2024 Peter Hinch 5 | from framebuf import FrameBuffer, MONO_HLSB 6 | from gui.core.tgui import Widget, ssd 7 | from gui.core.colors import * 8 | from optional.py.uQR import QRCode 9 | 10 | from utime import ticks_diff, ticks_ms 11 | 12 | 13 | class QRMap(Widget): 14 | @staticmethod 15 | def len_side(version): 16 | return 4 * version + 17 17 | 18 | @staticmethod 19 | def make_buffer(version, scale): 20 | side = QRMap.len_side(version) * scale 21 | width = (side >> 3) + int(side & 7 > 0) # Width in bytes 22 | return bytearray(side * width) 23 | 24 | def __init__(self, writer, row, col, version=4, scale=1, *, bdcolor=RED, buf=None): 25 | self._version = version 26 | self._scale = scale 27 | self._iside = self.len_side(version) # Dimension of unscaled QR image less border 28 | side = self._iside * scale 29 | # Widget allows 4 * scale border around each edge 30 | border = 4 * scale 31 | wside = side + 2 * border # Widget dimension 32 | super().__init__(writer, row, col, wside, wside, BLACK, WHITE, bdcolor, False) 33 | super()._set_callbacks(self._update, ()) 34 | if buf is None: 35 | buf = QRMap.make_buffer(version, scale) 36 | self._fb = FrameBuffer(buf, side, side, MONO_HLSB) 37 | self._irow = row + border 38 | self._icol = col + border 39 | self._qr = QRCode(version, border=0) 40 | 41 | def show(self): 42 | if super().show(False): # Show white border 43 | palette = ssd.palette 44 | palette.bg(self.bgcolor) 45 | palette.fg(self.fgcolor) 46 | ssd.blit(self._fb, self._icol, self._irow, -1, palette) 47 | 48 | def _update(self, _): # Runs when value changes 49 | t = ticks_ms() 50 | qr = self._qr 51 | qr.clear() 52 | qr.add_data(self._value) 53 | matrix = qr.get_matrix() # 750ms. Rest of the routine adds 50ms 54 | if qr.version != self._version: 55 | raise ValueError("Text too long for QR version.") 56 | wd = self._iside 57 | s = self._scale 58 | for row in range(wd): 59 | for col in range(wd): 60 | v = matrix[row][col] 61 | for nc in range(s): 62 | for nr in range(s): 63 | self._fb.pixel(col * s + nc, row * s + nr, v) 64 | -------------------------------------------------------------------------------- /gui/widgets/region.py: -------------------------------------------------------------------------------- 1 | # region.py Extension to nanogui providing the Region class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021 Peter Hinch 5 | 6 | # Usage: 7 | # from gui.widgets.tstat import Tstat 8 | 9 | from gui.core.tgui import display 10 | from gui.core.colors import * 11 | from gui.widgets.meter import Meter 12 | 13 | 14 | class Region: 15 | # Callback reasons 16 | EX_WB_IA = 1 # Exit region. Was below. Is above. 17 | EX_WB_IB = 2 # Exit, was below, is below 18 | EX_WA_IA = 4 # Exit, was above, is above. 19 | EX_WA_IB = 8 # Exit, was above, is below 20 | T_IA = 16 # Transit, is above 21 | T_IB = 32 # Transit, is below 22 | EN_WA = 64 # Entry, was above 23 | EN_WB = 128 # Entry, was below 24 | 25 | def __init__(self, tstat, vlo, vhi, color, callback, args=()): 26 | tstat.regions.add(self) 27 | tstat.draw = True 28 | self.tstat = tstat 29 | if vlo >= vhi: 30 | raise ValueError("TStat Region: vlo must be < vhi") 31 | self.vlo = vlo 32 | self.vhi = vhi 33 | self.color = color 34 | self.cb = callback 35 | self.args = args 36 | v = self.tstat.value() # Get current value 37 | self.is_in = vlo <= v <= vhi # Is initial value in region 38 | # .wa: was above. Value prior to any entry to region. 39 | self.wa = None # None indicates unknown 40 | 41 | # Where prior state is unknown because instantiation occurred with a value 42 | # in the region (.wa is None) we make the assumption that, on exit, it is 43 | # leaving from the opposite side from purported entry. 44 | def do_check(self, v): 45 | cb = self.cb 46 | args = self.args 47 | if v < self.vlo: 48 | if not self.is_in: 49 | if self.wa is None or self.wa: # Low going transit 50 | cb(self, self.T_IB, *args) 51 | return # Was and is outside: no action. 52 | # Was in the region, find direction of exit 53 | reason = self.EX_WA_IB if (self.wa is None or self.wa) else self.EX_WB_IB 54 | cb(self, reason, *args) 55 | elif v > self.vhi: 56 | if not self.is_in: 57 | if self.wa is None or not self.wa: 58 | cb(self, self.T_IA, *args) 59 | return 60 | # Was already in region 61 | reason = self.EX_WB_IA if (self.wa is None or not self.wa) else self.EX_WA_IA 62 | cb(self, reason, *args) 63 | else: # v is in range 64 | if self.is_in: 65 | return # Nothing to do 66 | if self.wa is None or self.wa: 67 | cb(self, self.EN_WA, *args) 68 | else: 69 | cb(self, self.EN_WB, *args) 70 | 71 | def check(self, v): 72 | self.do_check(v) 73 | self.is_in = self.vlo <= v <= self.vhi 74 | if not self.is_in: # Current value is outside 75 | self.wa = v > self.vhi # Save state for next check 76 | 77 | def adjust(self, vlo, vhi): 78 | if vlo >= vhi: 79 | raise ValueError("TStat Region: vlo must be < vhi") 80 | old_vlo = self.vlo 81 | old_vhi = self.vhi 82 | self.vlo = vlo 83 | self.vhi = vhi 84 | v = self.tstat.value() 85 | self.tstat.draw = True 86 | # Despatch cases where there is nothing to do. 87 | # Outside both regions on same side 88 | if v < vlo and v < old_vlo: 89 | return 90 | if v > vhi and v > old_vhi: 91 | return 92 | is_in = vlo <= v <= vhi # Currently inside 93 | if is_in and self.is_in: # Regions overlapped 94 | return # Still inside so no action 95 | 96 | if is_in: # Inside new region but not in old 97 | self.check(v) # Treat as if entering new region from previous value 98 | else: # Outside new region 99 | if not self.is_in: # Also outside old region. Hence it lay 100 | # between old and new regions. Force a traverse of new. 101 | self.wa = v < vlo 102 | # If it was in old region treat as if leaving it 103 | self.check(v) 104 | -------------------------------------------------------------------------------- /images/bernoulli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/bernoulli.png -------------------------------------------------------------------------------- /images/big_display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/big_display.jpg -------------------------------------------------------------------------------- /images/bitmap.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/bitmap.JPG -------------------------------------------------------------------------------- /images/calendar.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/calendar.JPG -------------------------------------------------------------------------------- /images/cartesian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/cartesian.png -------------------------------------------------------------------------------- /images/checkbox.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/checkbox.JPG -------------------------------------------------------------------------------- /images/chess.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/chess.JPG -------------------------------------------------------------------------------- /images/closebutton.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/closebutton.JPG -------------------------------------------------------------------------------- /images/dd_closed.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/dd_closed.JPG -------------------------------------------------------------------------------- /images/dd_open.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/dd_open.JPG -------------------------------------------------------------------------------- /images/dial.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/dial.JPG -------------------------------------------------------------------------------- /images/dial1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/dial1.JPG -------------------------------------------------------------------------------- /images/dialog.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/dialog.JPG -------------------------------------------------------------------------------- /images/filesystem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/filesystem.png -------------------------------------------------------------------------------- /images/grid.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/grid.JPG -------------------------------------------------------------------------------- /images/iconbuttons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/iconbuttons.jpg -------------------------------------------------------------------------------- /images/keyboard.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/keyboard.JPG -------------------------------------------------------------------------------- /images/knob.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/knob.JPG -------------------------------------------------------------------------------- /images/label.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/label.JPG -------------------------------------------------------------------------------- /images/led.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/led.JPG -------------------------------------------------------------------------------- /images/lissajous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/lissajous.png -------------------------------------------------------------------------------- /images/listbox.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/listbox.JPG -------------------------------------------------------------------------------- /images/log_scale.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/log_scale.JPG -------------------------------------------------------------------------------- /images/me.ppm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/me.ppm -------------------------------------------------------------------------------- /images/menu.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/menu.JPG -------------------------------------------------------------------------------- /images/meter.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/meter.JPG -------------------------------------------------------------------------------- /images/polar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/polar.png -------------------------------------------------------------------------------- /images/pushbuttons.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/pushbuttons.JPG -------------------------------------------------------------------------------- /images/qrcode.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/qrcode.JPG -------------------------------------------------------------------------------- /images/radiobuttons.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/radiobuttons.JPG -------------------------------------------------------------------------------- /images/rp2_test_fixture.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/rp2_test_fixture.JPG -------------------------------------------------------------------------------- /images/rp2_tsc2007.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/rp2_tsc2007.JPG -------------------------------------------------------------------------------- /images/scale.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/scale.JPG -------------------------------------------------------------------------------- /images/sine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/sine.png -------------------------------------------------------------------------------- /images/sliders.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/sliders.JPG -------------------------------------------------------------------------------- /images/textbox.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/textbox.JPG -------------------------------------------------------------------------------- /images/tstat.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhinch/micropython-touch/e0015adac72e4786ee42a85a9f74250d5c7e9d51/images/tstat.JPG -------------------------------------------------------------------------------- /optional/chess/CHESS.md: -------------------------------------------------------------------------------- 1 | # The chess game demo 2 | 3 | This uses this fork of the 4 | [Sunfish chess engine](https://github.com/jacklinquan/micropython-sunfish) 5 | adapted for MicroPython. It may be installed with 6 | ```bash 7 | $ mpremote mip install github:jacklinquan/micropython-sunfish 8 | ``` 9 | The chess engine is released under the GPL V3.0 licence. 10 | 11 | The demo requires a display with at least 320x240 pixels. The host should have 12 | plenty of RAM: a Raspberry Pico 2 works well. To run on a version 1 pico would 13 | require the use of frozen bytecode. This option is untested. 14 | 15 | Gameplay is by touching the piece to be moved, followed by a touch of the 16 | destination square. Illegal moves are ignored. Castling is done by moving the 17 | King: the Rook will move automatically. Pawn promotion to Queen is automatic. 18 | _en passant_ works as expected. 19 | 20 | The engine has limitations described in 21 | [its README file](https://github.com/jacklinquan/micropython-sunfish). Notably: 22 | * You get no warning if it places you in check. If you fail to deal with this, it 23 | will end the game on its next move declaring itself the winner. 24 | * It seems unaware of being in checkmate. You need actually to take its King 25 | before it acknowledges its situation. 26 | 27 | The easy way to run the demo is from the PC. In a clone of this repo, move to the 28 | `micropython-touch` directory and issue 29 | ```bash 30 | $ mpremote exec "import optional.chess.chess_game" 31 | ``` 32 | 33 | # Note for developers 34 | 35 | The Sunfish engine has an API which is intended to be portable. If you have a 36 | chess engine which is an improvement on Sunfish it should be straightforward to 37 | bolt on the API, in which case `chess_game.py` would work. Similarly if you have 38 | a preferred GUI, coding against the API would enable engines to be interchanged. 39 | 40 | The API is documented in the Sunfish repo. 41 | -------------------------------------------------------------------------------- /optional/mqtt/MQTT_DEMO.md: -------------------------------------------------------------------------------- 1 | # The MQTT demo 2 | 3 | This demo displays incoming MQTT messages as they arrive. Two `Button` widgets 4 | each cause a message to be published. The demo `mqtt.py` will need to be edited 5 | for local conditions. As a minimum the following lines will need to be adapted: 6 | ```Python 7 | config["server"] = "192.168.0.10" # Change to suit 8 | # config['server'] = 'test.mosquitto.org' 9 | 10 | config["ssid"] = "WIFI_SSID" 11 | config["wifi_pw"] = "WIFI PASSWORD" 12 | ``` 13 | The demo has high RAM requirements: the ideal host is an ESP32 with SPIRAM. On 14 | such a host the files `mqtt.py` and `mqtt_as.py` should be copied to the root 15 | directory of the host. On a Pico W the demo has run without using frozen 16 | bytecode but `mqtt_as.py` was precompiled with `mqtt_as.mpy` being copied to the 17 | host. Ideally the GUI and `mqtt_as.py` should be frozen, with `mqtt.py` put in 18 | the root directory for easy modification. 19 | 20 | # Resources 21 | 22 | Documentation on [mqtt_as](https://github.com/peterhinch/micropython-mqtt/blob/master/mqtt_as/README.md). 23 | Latest [mqtt_as.py](https://github.com/peterhinch/micropython-mqtt/blob/master/mqtt_as/mqtt_as.py). 24 | [Mosquitto](https://mosquitto.org/) An excellent broker, plus test utilities 25 | `mosquitto_pub` and `mosquitto_sub`. 26 | 27 | # Running the demo 28 | 29 | Ensure that the touch screen has been correctly configured and can run at least 30 | one of the standard demos. 31 | 32 | The demo is started with 33 | ```Python 34 | >>> import mqtt 35 | ``` 36 | The script publishes to the topic "shed": a status report every 60s plus a 37 | message every time the "Yes" or "No" button is pressed. Buttons are disabled if 38 | connectivity to the broker is unavailable. Publications can be checked with: 39 | ```bash 40 | $ mosquitto_sub -h 192.168.0.10 -t "shed" 41 | ``` 42 | The demo subscribes to two topics, "foo_topic" and "bar_topic". Repetitive 43 | publications to these topics can be triggered with the `pubtest` bash script: 44 | ```bash 45 | $ ./pubtest 46 | ``` 47 | Note that this script assumes a broker on `192.168.0.10`: edit as necessary. 48 | Individual messages may be sent with (for example) 49 | ```bash 50 | $ mosquitto_pub -h 192.168.0.10 -t bar_topic -m "bar message" -q 1 51 | ``` 52 | The "Network" LED widget shows red on a WiFi or broker outage, green normally. 53 | The "Message" LED pulses blue each time a message is received. 54 | 55 | # Connectivity 56 | 57 | If `mqtt_as` is unable to establish an initial connection to the broker the 58 | application quits with an error message. After successful connection, subsequent 59 | outages are handled automatically. This is by design: failure to establish an 60 | initial connection is usually because configuration values such as WiFi 61 | credentials or broker address are incorrect. 62 | -------------------------------------------------------------------------------- /optional/mqtt/mqtt.py: -------------------------------------------------------------------------------- 1 | # mqtt.py touch-gui MQTT demo. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # touch_setup must be imported before other modules because of RAM use. 7 | import touch_setup # Create a display instance 8 | import asyncio 9 | import gc 10 | 11 | from gui.core.tgui import Screen, ssd 12 | from gui.widgets import Label, Button, CloseButton, LED 13 | from gui.core.writer import CWriter 14 | import gui.fonts.freesans20 as font 15 | from gui.core.colors import * 16 | 17 | # MQTT stuff 18 | from mqtt_as import MQTTClient, config 19 | 20 | TOPIC = "shed" # For demo publication and last will use same topic 21 | 22 | # **** Start of local configuration **** 23 | config["server"] = "192.168.0.10" # Change to suit 24 | # config['server'] = 'test.mosquitto.org' 25 | 26 | config["ssid"] = "WIFI_SSID" 27 | config["wifi_pw"] = "WIFI PASSWORD" 28 | # **** End of local configuration **** 29 | config["will"] = (TOPIC, "Goodbye cruel world!", False, 0) 30 | config["keepalive"] = 120 31 | config["queue_len"] = 1 # Use event interface with default queue 32 | 33 | # Set up client. Enable optional debug messages at the REPL. 34 | MQTTClient.DEBUG = True 35 | mqtt_started = False # Tasks are not yet running 36 | 37 | 38 | class BaseScreen(Screen): 39 | def __init__(self): 40 | super().__init__() 41 | wri = CWriter(ssd, font, GREEN, BLACK) # Verbose 42 | gc.collect() 43 | self.outages = 0 44 | col = 2 45 | row = 2 46 | dc = 90 47 | Label(wri, row, col, "Topic") 48 | self.lbltopic = Label(wri, row, col + dc, 160, bdcolor=GREEN) 49 | self.lbltopic.value("Waiting...") 50 | row = 30 51 | Label(wri, row, col, "Message") 52 | self.lblmsg = Label(wri, row, col + dc, 160, bdcolor=GREEN) 53 | self.lblmsg.value("Waiting...") 54 | row = 60 55 | Label(wri, row + 10, col, "Network") 56 | self.wifi_led = LED(wri, row, col + dc, color=RED, bdcolor=WHITE) 57 | self.wifi_led.value(True) 58 | row = 100 59 | Label(wri, row + 10, col, "Message") 60 | self.msg_led = LED(wri, row, col + dc, color=BLUE, bdcolor=WHITE) 61 | row = 180 62 | Label(wri, row, col, "Publish") 63 | col += dc 64 | btn = {"height": 25, "litcolor": WHITE, "callback": self.pub} 65 | self.btnyes = Button(wri, row, col, text="Yes", args=("Yes",), **btn) 66 | col += 60 67 | self.btnno = Button(wri, row, col, text="No", args=("No",), **btn) 68 | self.wifi_state(False) 69 | CloseButton(wri, 30) # Quit the application 70 | asyncio.create_task(self.start()) 71 | 72 | # Callback for publish buttons 73 | def pub(self, button, msg): 74 | asyncio.create_task(self.client.publish(TOPIC, msg, qos=1)) 75 | 76 | # Visual response to a change in WiFi state 77 | def wifi_state(self, up): 78 | self.wifi_led.color(GREEN if up else RED) 79 | self.btnyes.greyed_out(not up) 80 | self.btnno.greyed_out(not up) 81 | 82 | # MQTT tasks 83 | async def start(self): 84 | global mqtt_started # Ensure that this can only run once 85 | if not mqtt_started: 86 | self.client = MQTTClient(config) 87 | gc.collect() 88 | try: 89 | await self.client.connect() 90 | except OSError: 91 | print("Connection failed.") 92 | Screen.back() # Abort: probable MQTT or WiFi config error 93 | mqtt_started = True # Next line runs forever 94 | await asyncio.gather(self.up(), self.down(), self.messages(), self.report()) 95 | 96 | # Publish status report once per minute 97 | async def report(self): 98 | n = 0 99 | while True: 100 | await asyncio.sleep(60) 101 | n += 1 102 | msg = f"{n} repubs: {self.client.REPUB_COUNT} outages: {self.outages}" 103 | # If WiFi is down the following will pause for the duration. 104 | await self.client.publish(TOPIC, msg, qos=1) 105 | 106 | # Handle incoming messages 107 | async def messages(self): 108 | async for topic, msg, retained in self.client.queue: 109 | self.lbltopic.value(topic.decode()) # Show topic and message 110 | self.lblmsg.value(msg.decode()) 111 | self.msg_led(True) 112 | await asyncio.sleep(1) # Flash for 1s 113 | self.msg_led(False) 114 | 115 | # Starty of a WiFi outage 116 | async def down(self): 117 | while True: 118 | await self.client.down.wait() # Pause until connectivity changes 119 | self.client.down.clear() 120 | self.wifi_state(False) 121 | self.outages += 1 122 | 123 | # Initial up and end of a WiFi/broker outage 124 | async def up(self): 125 | while True: 126 | await self.client.up.wait() 127 | self.client.up.clear() 128 | self.wifi_state(True) 129 | # Must re-subscribe to all topics 130 | await self.client.subscribe("foo_topic", 1) 131 | await self.client.subscribe("bar_topic", 1) 132 | 133 | 134 | print("MQTT demo.") 135 | Screen.change(BaseScreen) # A class is passed here, not an instance. 136 | -------------------------------------------------------------------------------- /optional/mqtt/pubtest: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # mosquitto_sub -h 192.168.0.10 -t shed 3 | BROKER='192.168.0.10' 4 | while : 5 | do 6 | mosquitto_pub -h $BROKER -t foo_topic -m "gordon bennett" -q 1 7 | sleep 5 8 | mosquitto_pub -h $BROKER -t foo_topic -m "pete was here" -q 1 9 | sleep 5 10 | mosquitto_pub -h $BROKER -t bar_topic -m "bar message" -q 1 11 | sleep 5 12 | done 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["drivers/ili93xx/ili9341.py", "github:peterhinch/micropython-nano-gui/drivers/ili93xx/ili9341.py"], 4 | ["drivers/boolpalette.py", "github:peterhinch/micropython-nano-gui/drivers/boolpalette.py"], 5 | ["gui/core/colors.py", "github:peterhinch/micropython-touch/gui/core/colors.py"], 6 | ["gui/core/tgui.py", "github:peterhinch/micropython-touch/gui/core/tgui.py"], 7 | ["gui/core/writer.py", "github:peterhinch/micropython-touch/gui/core/writer.py"], 8 | ["gui/demos/simple.py", "github:peterhinch/micropython-touch/gui/demos/simple.py"], 9 | ["gui/fonts/arial10.py", "github:peterhinch/micropython-touch/gui/fonts/arial10.py"], 10 | ["gui/fonts/font10.py", "github:peterhinch/micropython-touch/gui/fonts/font10.py"], 11 | ["gui/widgets/__init__.py", "github:peterhinch/micropython-touch/gui/widgets/__init__.py"], 12 | ["gui/widgets/buttons.py", "github:peterhinch/micropython-touch/gui/widgets/buttons.py"], 13 | ["gui/widgets/label.py", "github:peterhinch/micropython-touch/gui/widgets/label.py"], 14 | ["touch/touch.py", "github:peterhinch/micropython-touch/touch/touch.py"], 15 | ["touch/tsc2007.py", "github:peterhinch/micropython-touch/touch/tsc2007.py"], 16 | ["touch/xpt2046.py", "github:peterhinch/micropython-touch/touch/xpt2046.py"], 17 | ["touch/check.py", "github:peterhinch/micropython-touch/touch/check.py"], 18 | ["touch/setup.py", "github:peterhinch/micropython-touch/touch/setup.py"] 19 | ], 20 | "version": "0.1" 21 | } 22 | -------------------------------------------------------------------------------- /setup_examples/gc9a01_ws_rp2040_touch.py: -------------------------------------------------------------------------------- 1 | # gc9a01_ws_rp2040_touch.py 2 | # Driver for https://www.waveshare.com/wiki/RP2040-Touch-LCD-1.28 3 | # Also https://www.waveshare.com/RP2350-LCD-1.28.htm tested by Adam Knowles 4 | 5 | # Released under the MIT License (MIT). See LICENSE. 6 | # Copyright (c) 2024 Peter Hinch 7 | 8 | # Pinout (from Waveshare schematic) 9 | # Touch Controller 10 | # I2C SDA 6 11 | # I2C CLK 7 12 | # TP RST 22 13 | # TP INT 21 14 | 15 | # LCD 16 | # DC 8 17 | # CS 9 18 | # SCK 10 19 | # MOSI 11 20 | # MISO 12 21 | # RST 13 22 | # BL 25 23 | 24 | 25 | import gc 26 | from machine import Pin, SPI, I2C 27 | from drivers.gc9a01.gc9a01 import GC9A01 as SSD 28 | 29 | # May use either driver. 30 | # from drivers.gc9a01.gc9a01_8_bit import GC9A01 as SSD 31 | 32 | pdc = Pin(8, Pin.OUT, value=0) 33 | pcs = Pin(9, Pin.OUT, value=1) 34 | prst = Pin(13, Pin.OUT, value=1) 35 | pbl = Pin(25, Pin.OUT, value=1) 36 | 37 | gc.collect() # Precaution before instantiating framebuf 38 | 39 | # Define the display 40 | # gc9a01 datasheet allows <= 100MHz 41 | spi = SPI(1, 33_000_000, sck=Pin(10), mosi=Pin(11), miso=Pin(12)) 42 | ssd = SSD(spi, pcs, pdc, prst) # Bool options lscape, usd, mirror 43 | from gui.core.tgui import Display, quiet 44 | 45 | quiet() # Comment this out for periodic free RAM messages 46 | 47 | # Touch configuration. 48 | from touch.cst816s import CST816S 49 | 50 | pint = Pin(21, Pin.IN) # Touch interrupt 51 | ptrst = Pin(22, Pin.OUT, value=1) # Touch reset 52 | i2c = I2C(1, scl=Pin(7), sda=Pin(6), freq=100_000) 53 | tpad = CST816S(i2c, ptrst, pint, ssd) 54 | # To create a tpad.init line for your displays please read SETUP.md 55 | # The following is consistent with the SSD constructor args above. 56 | tpad.init(240, 240, 0, 0, 240, 240, False, True, True) 57 | display = Display(ssd, tpad) 58 | -------------------------------------------------------------------------------- /setup_examples/ili9341_cst820_esp32_2432S024c.py: -------------------------------------------------------------------------------- 1 | # hardware_setup.py for CYD_ESP32-2432S024C --- ili9341_CST820_ESP32 2 | # 2.4" Cheap Yellow Display 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2021-2024 Peter Hinch 6 | # 17-dec-2024 ZolAnd 7 | # Schematic 8 | # https://github.com/jtobinart/Micropython_CYDc_ESP32-2432S024C/blob/main/resources/5-Schematic/2432S024-2-V1.0.png 9 | # also in schematics 10 | 11 | # This 2.4" Cheap Yellow Display comes in resistive and capacitive versions. 12 | # Both use a vanilla ESP32 with an ili9341 240x320 display. 13 | # Resistive version uses XPT2046 on same SPI bus as display, cs/ on GPIO33 14 | # This setup is for the capacitive version with CST820 controller on I2C. 15 | 16 | """ 17 | Pin Reference (display and touch) 18 | 19 | D 0 Digital Boot Button 20 | D 2 Digital Display - Display: TFT_RS / TFT_DC 21 | 12 Digital Display - Display: TFT_SDO / TFT_MISO [HSPI] 22 | D 13 Digital Display - Display: TFT_SDI / TFT_MOSI [HSPI] 23 | D 14 Digital Display - Display: TFT_SCK [HSPI] 24 | D 15 Digital Display - Display: TFT_CS [HSPI] 25 | T 21 Digital Touch, Connector P3 & CN1 - Touch CST820: CTP_INT / I2C SDA 26 | T 25 Digital Touch CST920 - Touch CST820: CTP_RST 27 | D 27 Digital Display - Display: TFT_BL (BackLight) 28 | T 32 Digital Touch CST820 - Touch CST820: CTP_SCL 29 | T 33 Digital Touch CST820 - Touch CST820: CTP_SDA 30 | 31 | Full pin reference. 32 | Source https://github.com/jtobinart/Micropython_CYDc_ESP32-2432S024C/blob/main/cydc.py 33 | 34 | IO Pins 35 | 0 Digital Boot Button 36 | 1 Digital Connector P1 & USB Chip - TX 37 | 2 Digital Display - TFT_RS / TFT_DC 38 | 3 Digital Connector P1 & USB Chip - RX 39 | 4 Digital RGB LED - Red 40 | 5 Digital SD Card - TF_CS [VSPI] 41 | 6 Digital Not Connected 42 | 7 Digital Not Connected 43 | 8 Digital Not Connected 44 | 9 Digital Not Connected 45 | 10 Digital Not Connected 46 | 11 Digital Not Connected 47 | 12 Digital Display - TFT_SDO / TFT_MISO [HSPI] 48 | 13 Digital Display - TFT_SDI / TFT_MOSI [HSPI] 49 | 14 Digital Display - TFT_SCK [HSPI] 50 | 15 Digital Display - TFT_CS [HSPI] 51 | 16 Digital RGB LED - Green 52 | 17 Digital RGB LED - Blue 53 | 18 Digital SD Card - SCK [VSPI] 54 | 19 Digital SD Card - MISO [VSPI] 55 | 21 Digital Touch, Connector P3 & CN1 - CTP_INT / I2C SDA 56 | 22 Digital Connector P3 & CN1 - I2C SCL 57 | 23 Digital SD Card - MOSI [VSPI] 58 | 25 Digital Touch CST920 - CTP_RST 59 | 26 Analog Speaker - !!!Speaker ONLY! Connected to Amp!!! 60 | 27 Digital Display - TFT_BL (BackLight) 61 | 32 Digital Touch CST820 - CTP_SCL 62 | 33 Digital Touch CST820 - CTP_SDA 63 | 34 Analog LDR Light Sensor - !!!Input ONLY!!! 64 | 35 Digital P3 Connector - !!!Input ONLY w/ NO pull-ups!!! 65 | 36 Digital Not Connected 66 | 39 Digital Not Connected 67 | """ 68 | 69 | from machine import Pin, SPI, SoftI2C 70 | import gc 71 | from drivers.ili93xx.ili9341 import ILI9341 as SSD 72 | 73 | # Display setup 74 | prst = Pin(0, Pin.OUT, value=1) 75 | pdc = Pin(2, Pin.OUT, value=0) 76 | pcs = Pin(15, Pin.OUT, value=1) 77 | 78 | # Use hardSPI (bus 1) 79 | spi = SPI(1, sck=Pin(14), mosi=Pin(13), baudrate=40_000_000) 80 | # Precaution before instantiating framebuf 81 | gc.collect() 82 | ssd = SSD(spi, height=240, width=320, dc=pdc, cs=pcs, rst=prst, usd=True) # 240x320 default 83 | 84 | # Backlight 85 | tft_bl = Pin(27, Pin.OUT, value=1) # Turn on backlight 86 | # Touchpad 87 | from touch.cst820 import CST820 88 | 89 | # Touch interrupt is unused: in testing it was never asserted by hardware. 90 | # pint = Pin(21, Pin.IN) 91 | ptrst = Pin(25, Pin.OUT, value=1) # Touch reset 92 | i2c = SoftI2C(scl=Pin(32), sda=Pin(33), freq=400_000) 93 | tpad = CST820(i2c, ptrst, ssd) 94 | # Assign orientation and calibration values. 95 | # xpix, ypix, xmin, ymin, xmax, ymax, trans, rr, rc): 96 | tpad.init(240, 320, 3, 8, 239, 325, True, False, True) 97 | 98 | # GUI configuration 99 | from gui.core.tgui import Display 100 | 101 | display = Display(ssd, tpad) 102 | -------------------------------------------------------------------------------- /setup_examples/ili9341_ft6206_pico.py: -------------------------------------------------------------------------------- 1 | # ili9341_ft6206_pico.py Customise for your hardware config 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # As written, supports: 7 | # ili9341 240x320 displays on Pi Pico. 8 | # FT6206 touch controller. 9 | # https://www.adafruit.com/product/1947 10 | # Edit the driver import for other displays. 11 | 12 | # Demo of initialisation procedure designed to minimise risk of memory fail 13 | # when instantiating the frame buffer. The aim is to do this as early as 14 | # possible before importing other modules. 15 | 16 | # WIRING 17 | # Pico Display 18 | # GPIO Pin 19 | # 3v3 36 Vin 20 | # IO6 9 CLK Hardware SPI0 21 | # IO7 10 DATA (AKA SI MOSI) 22 | # IO8 11 Rst 23 | # IO9 12 DC 24 | # Gnd 13 Gnd 25 | # IO10 14 CS 26 | # IO26 31 Touch SDA 27 | # IO27 32 Touch SCL 28 | 29 | from machine import Pin, I2C, SPI, freq 30 | import gc 31 | from drivers.ili93xx.ili9341 import ILI9341 as SSD 32 | 33 | freq(250_000_000) # RP2 overclock 34 | # Create and export an SSD instance 35 | prst = Pin(7, Pin.OUT, value=1) 36 | pdc = Pin(15, Pin.OUT, value=0) # Arbitrary pins 37 | pcs = Pin(17, Pin.OUT, value=1) 38 | spi = SPI(0, sck=Pin(18), mosi=Pin(19), miso=Pin(16), baudrate=30_000_000) 39 | gc.collect() # Precaution before instantiating framebuf 40 | ssd = SSD(spi, pcs, pdc, prst, height=240, width=320, usd=True) # 240x320 default 41 | from gui.core.tgui import Display, quiet 42 | 43 | quiet() # Comment this out for periodic free RAM messages 44 | # Touch configuration 45 | from touch.ft6206 import FT6206 46 | 47 | i2c = I2C(1, scl=Pin(27), sda=Pin(26), freq=100_000) 48 | tpad = FT6206(i2c, ssd) 49 | # To create a tpad.init line for your displays please read SETUP.md 50 | # The FT6206 is pre-calibrated: the purpose of running touch.setup is to match 51 | # the chosen screenorientation. Numeric args may be as below. 52 | # tpad.init(240, 320, 0, 0, 240, 320, True, True, False) 53 | 54 | display = Display(ssd, tpad) 55 | -------------------------------------------------------------------------------- /setup_examples/ili9341_tsc2007_pico.py: -------------------------------------------------------------------------------- 1 | # ili9341_tsc2007_pico.py Customise for your hardware config 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # As written, supports: 7 | # ili9341 240x320 displays on Pi Pico. 8 | # TSC2007 touch controller. 9 | # Edit the driver import for other displays. 10 | 11 | # Demo of initialisation procedure designed to minimise risk of memory fail 12 | # when instantiating the frame buffer. The aim is to do this as early as 13 | # possible before importing other modules. 14 | 15 | # WIRING 16 | # Pico Display 17 | # GPIO Pin 18 | # 3v3 36 Vin 19 | # IO6 9 CLK Hardware SPI0 20 | # IO7 10 DATA (AKA SI MOSI) 21 | # IO8 11 Rst 22 | # IO9 12 DC 23 | # Gnd 13 Gnd 24 | # IO10 14 CS 25 | # IO26 31 Touch SDA 26 | # IO27 32 Touch SCL 27 | 28 | from machine import Pin, I2C, SPI, freq 29 | import gc 30 | from drivers.ili93xx.ili9341 import ILI9341 as SSD 31 | 32 | freq(250_000_000) # RP2 overclock 33 | # Create and export an SSD instance 34 | prst = Pin(8, Pin.OUT, value=1) 35 | pdc = Pin(9, Pin.OUT, value=0) # Arbitrary pins 36 | pcs = Pin(10, Pin.OUT, value=1) 37 | spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000) 38 | gc.collect() # Precaution before instantiating framebuf 39 | ssd = SSD(spi, pcs, pdc, prst, height=240, width=320, usd=True) # 240x320 default 40 | from gui.core.tgui import Display, quiet 41 | 42 | quiet() # Comment this out for periodic free RAM messages 43 | 44 | # Touch configuration 45 | from touch.tsc2007 import TSC2007 46 | 47 | i2c = I2C(1, scl=Pin(27), sda=Pin(26), freq=100_000) 48 | tpad = TSC2007(i2c, ssd) 49 | # To create a tpad.init line for your displays please read SETUP.md 50 | # tpad.init(240, 320, 201, 304, 3901, 3873, True, True, False) 51 | 52 | display = Display(ssd, tpad) 53 | -------------------------------------------------------------------------------- /setup_examples/ili9341_xpt2046_esp32.py: -------------------------------------------------------------------------------- 1 | # ili9341_xpt2046_esp32.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # Tested on eBay sourced display 7 | 8 | # Demo of initialisation procedure designed to minimise risk of memory fail 9 | # when instantiating the frame buffer. The aim is to do this as early as 10 | # possible before importing other modules. 11 | 12 | # WIRING for Feather S3 13 | # Attempt to use a shared SPI bus to save pins failed with a single touch registering 14 | # as multiple. Perhaps bus clock rate switching only works on bare metal hosts? 15 | 16 | # LCD picoW (GPIO) 17 | # VCC Vin 18 | # GND Gnd 19 | # LCD_DC 9 20 | # LCD_CS 38 21 | # LCD_CLK 36 22 | # MOSI 35 23 | # MISO 37 24 | # BackLight 3V3 25 | # LCD_RST 33 26 | # (Touch) 27 | # TP_CS 8 28 | # MOSI 11 29 | # MISO 10 30 | # SCK 7 31 | 32 | from machine import Pin, SPI, SoftSPI 33 | import gc 34 | from drivers.ili93xx.ili9341 import ILI9341 as SSD 35 | 36 | 37 | # Screen configuration 38 | # (Create and export an SSD instance) 39 | prst = Pin(33, Pin.OUT, value=1) 40 | pdc = Pin(9, Pin.OUT, value=0) 41 | pcs = Pin(38, Pin.OUT, value=1) 42 | 43 | # Use hardSPI (bus 1) 44 | spi = SPI(1, 33_000_000, sck=Pin(36), mosi=Pin(35), miso=Pin(37)) 45 | # Precaution before instantiating framebuf 46 | gc.collect() 47 | ssd = SSD(spi, height=240, width=320, dc=pdc, cs=pcs, rst=prst, usd=True) 48 | from gui.core.tgui import Display, quiet 49 | 50 | # quiet() # Comment this out for periodic free RAM messages 51 | from touch.xpt2046 import XPT2046 52 | 53 | # Touch configuration 54 | sspi = SoftSPI(mosi=Pin(11), miso=Pin(10), sck=Pin(7)) # 2.5MHz max 55 | 56 | tpad = XPT2046(sspi, Pin(8, Pin.OUT, value=1), ssd) 57 | # To create a tpad.init line for your displays please read SETUP.md 58 | # tpad.init(240, 320, 151, 151, 4095, 4095, True, True, True) 59 | 60 | # instantiate a Display 61 | display = Display(ssd, tpad) 62 | -------------------------------------------------------------------------------- /setup_examples/ili9341_xpt2046_pico.py: -------------------------------------------------------------------------------- 1 | # ili9341_xpt2046_pico.py Customise for your hardware config 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2021-2024 Peter Hinch 5 | 6 | # As written, supports: 7 | # ili9341 240x320 displays on Pi Pico. 8 | # XPT2046 touch controller. 9 | # Edit the driver import for other displays. 10 | 11 | # Demo of initialisation procedure designed to minimise risk of memory fail 12 | # when instantiating the frame buffer. The aim is to do this as early as 13 | # possible before importing other modules. 14 | 15 | # WIRING 16 | # Pico Display 17 | # GPIO Pin 18 | # 3v3 36 Vin 19 | # IO6 9 CLK Hardware SPI0 20 | # IO7 10 DATA (AKA SI MOSI) 21 | # IO8 11 Rst 22 | # IO9 12 DC 23 | # Gnd 13 Gnd 24 | # IO10 14 CS 25 | # IO26 31 Touch SDA 26 | # IO27 32 Touch SCL 27 | 28 | from machine import Pin, SoftSPI, SPI, freq 29 | import gc 30 | from drivers.ili93xx.ili9341 import ILI9341 as SSD 31 | 32 | freq(250_000_000) # RP2 overclock 33 | # Create and export an SSD instance 34 | prst = Pin(8, Pin.OUT, value=1) 35 | pdc = Pin(9, Pin.OUT, value=0) # Arbitrary pins 36 | pcs = Pin(10, Pin.OUT, value=1) 37 | spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000) 38 | gc.collect() # Precaution before instantiating framebuf 39 | ssd = SSD(spi, pcs, pdc, prst, height=240, width=320, usd=True) # 240x320 default 40 | from gui.core.tgui import Display, quiet 41 | 42 | quiet() # Comment this out for periodic free RAM messages 43 | 44 | # Touch configuration 45 | from touch.xpt2046 import XPT2046 46 | 47 | spi = SoftSPI(mosi=Pin(1), miso=Pin(2), sck=Pin(3)) # 2.5MHz max 48 | tpad = XPT2046(spi, Pin(0, Pin.OUT, value=1), ssd) 49 | # To create a tpad.init line for your displays please read SETUP.md 50 | # tpad.init(240, 320, 157, 150, 3863, 4095, True, True, True) 51 | display = Display(ssd, tpad) 52 | -------------------------------------------------------------------------------- /setup_examples/ili9488_ws_pico_res_touch_pico.py: -------------------------------------------------------------------------------- 1 | # ili9488_ws_pico_res_touch.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # Original source https://github.com/peterhinch/micropython-touch/issues/2 7 | # Contributor @beetlegigg. 8 | 9 | # Supported display TFT 480x320 10 | # https://www.waveshare.com/pico-restouch-lcd-3.5.htm 11 | 12 | # Demo of initialisation procedure designed to minimise risk of memory fail 13 | # when instantiating the frame buffer. The aim is to do this as early as 14 | # possible before importing other modules. 15 | 16 | # WIRING for rpi pico/w 17 | 18 | # Using waveshare LCD 3.5 19 | # LCD picoW (GPIO) 20 | # VCC Vin 21 | # GND Gnd 22 | # LCD_DC 8 23 | # LCD_CS 9 24 | # LCD_CLK 10 25 | # MOSI 11 26 | # MISO 12 27 | # BackLight 13 28 | # LCD_RST 15 29 | # (Touch) 30 | # TP_CS 16 31 | # TP_IRQ 17 (unused) 32 | 33 | # (GPIO used for SD card) 34 | # 5 SDIO_CLK 35 | # 18 SDIO_CMD 36 | # 19 SDIO_DO 37 | # 20 SDIO_D1 38 | # 21 SDIO_D2 39 | # 22 SDIO CS/D3 40 | 41 | from machine import Pin, SPI, freq 42 | import gc 43 | from drivers.ili94xx.ili9486 import ILI9486 as SSD 44 | 45 | SSD.COLOR_INVERT = 0xFFFF # Fix color inversion 46 | 47 | # RP2 overclock 48 | freq(250_000_000) 49 | 50 | # Screen configuration 51 | # (Create and export an SSD instance) 52 | prst = Pin(15, Pin.OUT, value=1) 53 | pdc = Pin(8, Pin.OUT, value=0) 54 | pcs = Pin(9, Pin.OUT, value=1) 55 | 56 | # Use hardSPI (bus 1) 57 | spi = SPI(1, 2_500_000, sck=Pin(10), mosi=Pin(11), miso=Pin(12)) 58 | # Precaution before instantiating framebuf 59 | gc.collect() 60 | ssd = SSD(spi, height=320, width=480, dc=pdc, cs=pcs, rst=prst, usd=True) 61 | from gui.core.tgui import Display, quiet 62 | 63 | quiet() # Comment this out for periodic free RAM messages 64 | from touch.xpt2046 import XPT2046 65 | 66 | # Touch configuration 67 | tpad = XPT2046(spi, Pin(16, Pin.OUT, value=1), ssd) 68 | # To create a tpad.init line for your displays please read SETUP.md 69 | # tpad.init(320, 480, 202, 206, 3898, 3999, True, False, True) 70 | 71 | # instantiate a Display 72 | # Bus arbitration: pass (spi, display baud, touch baud) 73 | display = Display(ssd, tpad, (spi, 33_000_000, 2_500_000)) 74 | -------------------------------------------------------------------------------- /setup_examples/st7789_ws_pico_res_touch_pico.py: -------------------------------------------------------------------------------- 1 | # st7789_ws_pico_res_touch.py 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2022-2024 Peter Hinch 5 | # With help from Tim Wermer. 6 | 7 | # Supported display 240x320 with XPT2046 8 | # https://www.waveshare.com/Pico-ResTouch-LCD-2.8.htm 9 | # WIRING for rpi pico/w 10 | 11 | # Using waveshare LCD 2.8 12 | # LCD picoW (GPIO) 13 | # VCC Vin 14 | # GND Gnd 15 | # LCD_DC 8 16 | # LCD_CS 9 17 | # LCD_CLK 10 18 | # MOSI 11 19 | # MISO 12 20 | # BackLight 13 21 | # LCD_RST 15 22 | # (Touch) 23 | # TP_CS 16 24 | # TP_IRQ 17 (unused) 25 | 26 | # (GPIO used for SD card) 27 | # 5 SDIO_CLK 28 | # 18 SDIO_CMD 29 | # 19 SDIO_DO 30 | # 20 SDIO_D1 31 | # 21 SDIO_D2 32 | # 22 SDIO CS/D3 33 | 34 | import gc 35 | from machine import Pin, SPI 36 | from drivers.st7789.st7789_4bit import * 37 | 38 | SSD = ST7789 39 | 40 | pdc = Pin(8, Pin.OUT, value=0) 41 | pcs = Pin(9, Pin.OUT, value=1) 42 | prst = Pin(15, Pin.OUT, value=1) 43 | pbl = Pin(13, Pin.OUT, value=1) 44 | 45 | gc.collect() # Precaution before instantiating framebuf 46 | # Max baudrate produced by Pico is 31_250_000. ST7789 datasheet allows <= 62.5MHz. 47 | 48 | # In shared bus devices must set baudrate to that supported by touch controller. 49 | # This is to enable touch.setup and touch.check to work: these don't support arbitration. 50 | spi = SPI(1, 2_500_000, sck=Pin(10), mosi=Pin(11), miso=Pin(12)) 51 | 52 | # Define the display 53 | # For portrait mode: 54 | # ssd = SSD(spi, height=320, width=240, dc=pdc, cs=pcs, rst=prst) 55 | # For landscape mode: 56 | ssd = SSD(spi, height=240, width=320, disp_mode=PORTRAIT, dc=pdc, cs=pcs, rst=prst) 57 | from gui.core.tgui import Display, quiet 58 | 59 | quiet() # Comment this out for periodic free RAM messages 60 | from touch.xpt2046 import XPT2046 61 | 62 | # Touch configuration. 63 | tpad = XPT2046(spi, Pin(16, Pin.OUT, value=1), ssd) 64 | # To create a tpad.init line for your displays please read SETUP.md 65 | # tpad.init(240, 320, 157, 150, 3863, 4095, True, True, False) 66 | # Bus arbitration: pass (spi, display baud, touch baud) 67 | display = Display(ssd, tpad, (spi, 33_000_000, 2_500_000)) 68 | -------------------------------------------------------------------------------- /touch/check.py: -------------------------------------------------------------------------------- 1 | # check.py Set up touch panel 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # The touch driver returns x and y values. The first stage of setup is to determine 7 | # whether x or y corresponds to the long axis of the display. This cannot be determined 8 | # programmatically. The user supplies portrait/lanscape status and the code figures 9 | # out horizontal and vertical axes. 10 | 11 | import touch_setup # Create a display instance 12 | import time 13 | from gui.core.tgui import ssd, display, touch 14 | from gui.core.writer import CWriter 15 | import gui.fonts.font10 as font 16 | from gui.core.colors import * 17 | 18 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 19 | 20 | 21 | def main(): 22 | rw = 50 # rect width 23 | rh = 30 # height 24 | rcol = ssd.width // 2 - rw // 2 25 | rrow = ssd.height // 2 - rh // 2 26 | ssd.rect(rcol, rrow, rw, rh, GREEN) 27 | display.print_centred(wri, ssd.width // 2, ssd.height // 2, "Quit") 28 | display.print_left(wri, 2, 2, f"Touch points, check row & col.") 29 | display.print_left(wri, 2, 30, f"row = ", YELLOW) 30 | display.print_left(wri, 2, 60, f"col = ", YELLOW) 31 | display.print_left(wri, 2, 90, f"x = ", CYAN) 32 | display.print_left(wri, 2, 120, f"y = ", CYAN) 33 | ssd.show() 34 | while True: 35 | if touch.poll(): 36 | tr = touch.row 37 | tc = touch.col 38 | tx = touch._x 39 | ty = touch._y 40 | display.print_left(wri, 40, 30, f"{tr:04d}", YELLOW) 41 | display.print_left(wri, 40, 60, f"{tc:04d}", YELLOW) 42 | display.print_left(wri, 40, 90, f"{tx:04d}", CYAN) 43 | display.print_left(wri, 40, 120, f"{ty:04d}", CYAN) 44 | ssd.show() 45 | if (rcol < tc < rcol + rw) and (rrow < tr < (rrow + rh)): 46 | break 47 | time.sleep_ms(100) 48 | display.clr_scr() 49 | ssd.show() 50 | 51 | 52 | s = """ 53 | For each point touched, display row and column values. These represent display coordinates. 54 | The raw x and y values from the touch panel are also displayed. These should each cover an 55 | approximate range of 0-4095 for 12-bit touch panels. 56 | """ 57 | print(s) 58 | main() 59 | -------------------------------------------------------------------------------- /touch/cst816s.py: -------------------------------------------------------------------------------- 1 | # cst816s.py Capacitive touch driver 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # It is minimal, providing only the required functionality for the touh GUI. 7 | # Sources: datasheet, Adafruit driver 8 | # Arduino library: https://github.com/fbiego/CST816S/tree/main 9 | # Espressif driver: https://github.com/espressif/esp-bsp/blob/master/components/lcd_touch/esp_lcd_touch_cst816s/esp_lcd_touch_cst816s.c 10 | # According to 11 | # https://github.com/espressif/esp-bsp/tree/master/components/lcd_touch/esp_lcd_touch_cst816s 12 | # chip does not respond to I2C until after a touch. This is deeply weird but true. 13 | # This means that version information in .version is null until first touched. 14 | # It is not verified but is available for debug purposes. 15 | # The only datasheet is almost useless with no register details. 16 | 17 | # I2C clock rate 10KHz - 400KHz 18 | 19 | # 0 <= x < 240 20 | # 0 <= y < 240 21 | 22 | from time import sleep_ms 23 | from machine import Pin 24 | from .touch import ABCTouch 25 | 26 | 27 | class CST816S(ABCTouch): 28 | def __init__(self, i2c, rst, pint, ssd, addr=0x15): 29 | super().__init__(ssd, None) 30 | self.i2c = i2c 31 | self.addr = addr 32 | self.version = bytearray(3) 33 | rst(0) 34 | sleep_ms(5) 35 | rst(1) 36 | sleep_ms(50) 37 | self.trig = False 38 | self.doid = True # Read ID on 1st touch 39 | self.touched = False 40 | pint.irq(self.isr, trigger=Pin.IRQ_RISING) 41 | 42 | def isr(self, _): 43 | self.trig = True 44 | 45 | def acquire(self, buf=bytearray(6)): 46 | if self.trig: 47 | self.trig = False 48 | if self.doid: # Read version info 49 | self.i2c.readfrom_mem_into(self.addr, 0xA7, self.version) 50 | self.doid = False 51 | self.touched = True # A touch is in progress. 52 | # Save state because polling can occur in the absence of an interrupt. 53 | self.i2c.readfrom_mem_into(self.addr, 0x01, buf) 54 | self._x = ((buf[2] & 0xF) << 8) + buf[3] 55 | self._y = ((buf[4] & 0xF) << 8) + buf[5] 56 | if buf[0] == 5 or (buf[2] & 0x40): # Gesture == 5 or event == 1: touch released 57 | self.touched = False 58 | return self.touched 59 | 60 | # Debug version 61 | # def acquire(self, buf=bytearray(6)): 62 | # if self.trig: 63 | # self.trig = False 64 | # self.touched = True # A touch is in progress. 65 | # # Save state because polling can occur in the absence of an interrupt. 66 | # if self.doid: 67 | # self.i2c.readfrom_mem_into(self.addr, 0xA7, self.version) # Read version info 68 | # self.doid = False 69 | # self.i2c.readfrom_mem_into(self.addr, 0x01, buf) 70 | # gesture = buf[0] 71 | # points = buf[1] 72 | # event = buf[2] >> 6 73 | # self._x = ((buf[2] & 0xF) << 8) + buf[3] 74 | # self._y = ((buf[4] & 0xF) << 8) + buf[5] 75 | # print(f"x {self._x} y {self._y} points {points} event {event} gesture {gesture}") 76 | # if gesture == 5 or event == 1: # Touch released 77 | # self.touched = False 78 | # return self.touched 79 | -------------------------------------------------------------------------------- /touch/cst820.py: -------------------------------------------------------------------------------- 1 | # cst820.py Capacitive touch driver 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # It is minimal, providing only the required functionality for the touh GUI. 7 | # Sources 8 | # CTS816s: 9 | # datasheet, Adafruit driver 10 | # Arduino library: https://github.com/fbiego/CST816S/tree/main 11 | # Espressif driver: https://github.com/espressif/esp-bsp/blob/master/components/lcd_touch/esp_lcd_touch_cst816s/esp_lcd_touch_cst816s.c 12 | # CST820: 13 | # LilyGo driver: https://github.com/Xinyuan-LilyGO/LilyGO-T-A76XX/blob/92e43a7aaee0b4ad08a3ee67d3b93818fa70b068/lib/SensorLib/src/touch/TouchClassCST816.cpp#L251 14 | # Schematic https://github.com/jtobinart/Micropython_CYDc_ESP32-2432S024C/blob/main/resources/5-Schematic/2432S024-2-V1.0.png 15 | 16 | # I2C clock rate 10KHz - 400KHz 17 | 18 | from time import sleep_ms 19 | from .touch import ABCTouch 20 | 21 | 22 | class CST820(ABCTouch): 23 | def __init__(self, i2c, rst, ssd, addr=0x15): 24 | super().__init__(ssd, None) 25 | self.i2c = i2c 26 | self.addr = addr 27 | rst(0) 28 | sleep_ms(5) 29 | rst(1) 30 | sleep_ms(50) 31 | if (v := self.version()) != 0xB7: 32 | print(f"WARNING: unexpected touch chip version: {v:02X}") 33 | # i2c.writeto_mem(addr, 0xFE, b"\xff") # prohibit automatic switching to low-power mode 34 | 35 | def acquire(self, buf=bytearray(6)): 36 | self.i2c.readfrom_mem_into(self.addr, 0x01, buf) 37 | self._x = ((buf[2] & 0xF) << 8) + buf[3] 38 | self._y = ((buf[4] & 0xF) << 8) + buf[5] 39 | return buf[1] == 1 # Touched if fingers==1 40 | 41 | def version(self): 42 | return self.i2c.readfrom_mem(self.addr, 0xA7, 1)[0] 43 | -------------------------------------------------------------------------------- /touch/ft6206.py: -------------------------------------------------------------------------------- 1 | # ft6206.py Capacitive touch driver 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # It is minimal, providing only the required functionality for the touh GUI. 7 | # Sources: datasheet, Adafruit driver 8 | # https://github.com/adafruit/Adafruit_FT6206_Library/tree/master 9 | 10 | # I2C clock rate 10KHz - 400KHz 11 | # Can read multiple bytes (page 20) 12 | # Section 3.1.3 Number of touch points: 1-2 is valid (as per Adafruit) 13 | 14 | # 0 <= x <= 240 15 | # 0 <= y <= 320 16 | 17 | from .touch import ABCTouch 18 | 19 | 20 | class FT6206(ABCTouch): 21 | def __init__(self, i2c, ssd, addr=0x38, thresh=128): 22 | super().__init__(ssd, None) # No preprocessor required 23 | self.i2c = i2c 24 | self.addr = addr 25 | buf = bytearray(1) 26 | buf[0] = thresh 27 | self.i2c.writeto_mem(addr, 0x80, buf) # Set touch threshold 28 | self.i2c.readfrom_mem_into(addr, 0xA8, buf) 29 | if ok := (buf[0] == 0x11): 30 | self.i2c.readfrom_mem_into(addr, 0xA3, buf) 31 | ok = buf[0] in (0x06, 0x36, 0x64) 32 | if not ok: 33 | raise OSError("Invalid FT6206 chip type.") 34 | 35 | def _value(self, offs, buf=bytearray(1)): 36 | i2c.readfrom_mem_into(addr, offs, buf) # b0..b3 = MSB 37 | ev = buf[0] & 0xC0 # Event: 0 == touch, 0x40 release. Consider this ?? 38 | v = (buf[0] & 0x0F) << 8 39 | i2c.readfrom_mem_into(addr, offs + 1, buf) # LSB 40 | return v | buf[0] 41 | 42 | # If touched, populate ._x and ._y with raw data. Return True. ** returns 0 <= y <= 320, 0 <= x <= 240 43 | # If not touched return False. 44 | def acquire(self, buf=bytearray(11)): 45 | addr = self.addr 46 | i2c = self.i2c 47 | i2c.readfrom_mem_into(addr, 0x02, buf) 48 | t = buf[0] 49 | if t == 1: # One touch 50 | if buf[1] & 0x40: 51 | return False # The only touch was a release 52 | if t > 2 or t == 0: # No touch or invalid 53 | return False 54 | # We have at least one valid touch 55 | if (t == 2) and ((buf[7] & 0x40) == 0): # Touch 2 was a press 56 | self._x = ((buf[7] & 0x0F) << 8) | buf[8] # Save 2nd touch 57 | self._y = ((buf[9] & 0x0F) << 8) | buf[10] # Don't care about touch 1 58 | else: # Either t == 1 with a valid 1st touch or touch 2 was a release 59 | if buf[1] & 0x40: # Both touches were releases (is this possible?) 60 | return False 61 | self._x = ((buf[1] & 0x0F) << 8) | buf[2] # Save 1st touch 62 | self._y = ((buf[3] & 0x0F) << 8) | buf[4] 63 | return True 64 | -------------------------------------------------------------------------------- /touch/setup.py: -------------------------------------------------------------------------------- 1 | # setup.py Set up touch panel 2 | 3 | # TODO get touch.row, touch.col for key points. 4 | # If transpose is required, mimic transposition. 5 | # Check for reflection. 6 | # Use low level routines rather than GUI? 7 | 8 | 9 | # Released under the MIT License (MIT). See LICENSE. 10 | # Copyright (c) 2024 Peter Hinch 11 | 12 | # The touch driver returns x and y values. The first stage of setup is to determine 13 | # whether x or y corresponds to the long axis of the display. This cannot be determined 14 | # programmatically. The user supplies portrait/lanscape status and the code figures 15 | # out horizontal and vertical axes. 16 | 17 | # touch_setup must be imported before other modules because of RAM use. 18 | import touch_setup # Create a display instance 19 | import asyncio 20 | from array import array 21 | from gui.core.tgui import ssd, display, touch 22 | from gui.core.writer import CWriter 23 | import gui.fonts.font10 as font 24 | from gui.core.colors import * 25 | 26 | 27 | def cross(row, col, length, color): 28 | display.hline(col - length // 2, row, length, color) 29 | display.vline(col, row - length // 2, length, color) 30 | 31 | 32 | landscape = ssd.width > ssd.height 33 | ax = array("I", (0 for _ in range(4))) # x 34 | ay = array("I", (0 for _ in range(4))) # y 35 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 36 | 37 | 38 | async def touch_state(s): 39 | while True: # Wait for touch state to match passed value 40 | if touch.poll() == s: 41 | break 42 | # Polling at too high a rate led to bad ._x and ._y values on touch release. 43 | await asyncio.sleep_ms(100) 44 | 45 | 46 | async def do_touch(n): 47 | ssd.show() 48 | await touch_state(True) # Wait for a touch: may continue for a while 49 | await touch_state(False) # Wait for release 50 | display.print_left(wri, 2, 50 + 30 * n, f"x = {touch._x:04d} y = {touch._y:04d}", YELLOW) 51 | ax[n] = touch._x 52 | ay[n] = touch._y 53 | await asyncio.sleep(1) # Ensure touch has completed before drawing next cross 54 | 55 | 56 | async def main(): 57 | display.print_left(wri, 2, 2, "Touch each cross.") 58 | ssd.show() 59 | h = ssd.height 60 | w = ssd.width 61 | dh = h // 8 62 | dw = w // 8 63 | cross(dh, dw, 10, GREEN) 64 | await do_touch(0) 65 | 66 | cross(dh, w - dw, 10, GREEN) 67 | await do_touch(1) 68 | 69 | cross(h - dh, w - dw, 10, GREEN) 70 | await do_touch(2) 71 | 72 | cross(h - dh, dw, 10, GREEN) 73 | await do_touch(3) 74 | ssd.show() 75 | 76 | # Extrapolate to boundary: calculate margin size 77 | dx = (max(ax) - min(ax)) // 6 78 | dy = (max(ay) - min(ay)) // 6 79 | # Extrapolate but constrain to 12-bit range 80 | xmin = max(min(ax) - dx, 0) 81 | xmax = min(max(ax) + dx, 4095) 82 | ymin = max(min(ay) - dy, 0) 83 | ymax = min(max(ay) + dy, 4095) 84 | # Assign long axis pixel count to physically long axis. 85 | s = min(h, w) # Short axis in pixels 86 | l = max(h, w) # Long axis in pixels 87 | # Crosses 0 and 1 are horizontally aligned. Determine whether this is x or y. 88 | x_horizontal = abs(ax[0] - ax[1]) > abs(ay[0] - ay[1]) 89 | if x_horizontal: # Horizontal axis is x 90 | xpx = l if landscape else s 91 | ypx = s if landscape else l 92 | else: # Horizontal axis is y 93 | xpx = s if landscape else l 94 | ypx = l if landscape else s 95 | 96 | # print(f"Args: {xpx}, {ypx}, {xmin}, {ymin}, {xmax}, {ymax}") 97 | # At this stage it doesn't matter if the correct max pixel values have been assigned 98 | # to x and y: looking only at relative values 99 | # First two crosses should be on a similar row 100 | # Rows and cols should increase away from first cross (array index 0) 101 | xpose = not x_horizontal 102 | if xpose: # rows are x, cols are y but touch.py reflects before transposing 103 | crefl = ax[0] > ax[3] 104 | rrefl = ay[0] > ay[1] 105 | else: # rows are y, cols are x 106 | rrefl = ay[0] > ay[3] 107 | crefl = ax[0] > ax[1] 108 | 109 | if (not touch.precal) and (max(xmin, ymin) > 1000 or min(xmax, ymax) < 3000): 110 | print("WARNING: touches may not have been propoerly recorded. Please repeat setup.") 111 | print("Please check the following (see TOUCHPAD.md):") 112 | print(f"tpad.init({xpx}, {ypx}, {xmin}, {ymin}, {xmax}, {ymax}, {xpose}, {rrefl}, {crefl})") 113 | print("Then paste it into touch_setup.py, replacing the initial tpad.init line.") 114 | 115 | 116 | asyncio.run(main()) 117 | -------------------------------------------------------------------------------- /touch/touch.py: -------------------------------------------------------------------------------- 1 | # touch.py ABC for touch devices. 2 | # Subclass implements acquisition, base class scales and transforms 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2024 Peter Hinch 6 | from array import array 7 | from micropython import const 8 | 9 | _SCALE = const(18) # 12 bits ADC -> 30 bit small int. Subclasses must be limited to 12 bits. 10 | 11 | # Separate preprocessor allows for other designs for future touch panels 12 | # tpad subclass has method .acquire: returns True if touched and populates 13 | # ._x and ._y with raw data, otherwise returns False 14 | # .get acquires a set of samples and modifies ._x and ._y to provide mean 15 | # values. 16 | class PreProcess: 17 | def __init__(self, tpad, alen): 18 | self.tpad = tpad 19 | # Arrays for means 20 | self.ax = array("H", (0 for _ in range(alen))) 21 | self.ay = array("H", (0 for _ in range(alen))) 22 | self.alen = alen 23 | 24 | def get(self): 25 | tpad = self.tpad 26 | alen = self.alen 27 | x = tpad._x 28 | y = tpad._y 29 | ok = False 30 | if tpad.acquire(): # If touched, get and discard first (noisy) reading. 31 | for idx in range(alen): # Populate arrays 32 | if not tpad.acquire(): 33 | break # No or bad touch 34 | self.ax[idx] = tpad._x 35 | self.ay[idx] = tpad._y 36 | else: 37 | ok = True 38 | tpad._x = sum(self.ax) // alen if ok else x 39 | tpad._y = sum(self.ay) // alen if ok else y 40 | return ok 41 | 42 | 43 | # Class is instantiated with a configured preprocessor. 44 | class ABCTouch: 45 | def __init__(self, ssd, prep): 46 | self.get = self.acquire if prep is None else prep.get 47 | self.precal = prep is None 48 | self.init(ssd.height, ssd.width, 0, 0, 4095, 4095, False, False, False) 49 | 50 | # Assign orientation and calibration values. 51 | def init(self, xpix, ypix, xmin, ymin, xmax, ymax, trans, rr, rc): 52 | self._xpix = xpix # No of pixels on x axis 53 | self._ypix = ypix # Pixels on y axis 54 | if not self.precal: # Scaling 55 | self._x0 = xmin # Returned value for row 0 56 | self._y0 = ymin # Returned value for col 0 57 | self._xl = (xpix << _SCALE) // (xmax - xmin) 58 | self._yl = (ypix << _SCALE) // (ymax - ymin) 59 | # Mapping 60 | self._rr = rr # Row reflect 61 | self._rc = rc # Col reflect 62 | self._trans = trans # Transposition 63 | # Raw coordinates from subclass. 64 | self._x = 0 65 | self._y = 0 66 | # Screen referenced coordinates 67 | self.row = 0 68 | self.col = 0 69 | 70 | # API: GUI calls poll which returns True if touched. .row, .col hold Screen 71 | # referenced coordinates. 72 | # Preprocessor .get() calls touch subclass .acquire to get values 73 | def poll(self): 74 | if res := self.get(): 75 | if self.precal: 76 | col = self._x # This is not the true mapping of FT6206 but setup 77 | row = self._y # will set ._trans 78 | else: 79 | xpx = ((self._x - self._x0) * self._xl) >> _SCALE # Convert to pixels 80 | ypx = ((self._y - self._y0) * self._yl) >> _SCALE 81 | xpx = max(0, min(xpx, self._xpix - 1)) 82 | ypx = max(0, min(ypx, self._ypix - 1)) 83 | col = xpx 84 | row = ypx 85 | if self._rr: # Reflection 86 | row = self._ypix - row 87 | if self._rc: 88 | col = self._xpix - col 89 | if self._trans: # Transposition 90 | self.col = row 91 | self.row = col 92 | else: 93 | self.col = col 94 | self.row = row 95 | return res 96 | -------------------------------------------------------------------------------- /touch/tsc2007.py: -------------------------------------------------------------------------------- 1 | # tsc2007.py MicroPython driver for TSC2007 resistive touch controller. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # Works with the Adafruit TSC2007 resistive touch driver. 7 | # Adafruit product reference http://www.adafruit.com/products/5423 8 | 9 | # Reference sources: the TSC2007 datasheet plus Adafruit driver 10 | # https://github.com/adafruit/Adafruit_CircuitPython_TSC2007 11 | # The following were studied for noise reduction approach, # mainly implemented 12 | # in base class: 13 | # https://github.com/dmquirozc/XPT2046_driver_STM32/blob/main/xpt2046.c 14 | # https://github.com/PaulStoffregen/XPT2046_Touchscreen/blob/master/XPT2046_Touchscreen.cpp 15 | # https://github.com/robert-hh/micropython-ili9341/blob/master/xpt2046.py 16 | # It is minimal, providing only the required functionality for the touh GUI. See 17 | # the Adafruit driver for a more full-featured driver. 18 | 19 | from .touch import ABCTouch, PreProcess 20 | 21 | 22 | class TSC2007(ABCTouch): 23 | def __init__(self, i2c, ssd, addr=0x48, *, alen=10): 24 | # Instantiate a preprocessor 25 | super().__init__(ssd, PreProcess(self, alen)) 26 | self._i2c = i2c 27 | self._addr = addr 28 | i2c.writeto(addr, b"\x00") # Low power/read temp 29 | 30 | def _value(self, buf=bytearray(2)): 31 | self._i2c.readfrom_into(self._addr, buf) 32 | return (buf[0] << 4) | (buf[1] >> 4) 33 | 34 | # If touched, populate ._x and ._y with raw data. Return True. 35 | # If not touched return False. 36 | def acquire(self): 37 | addr = self._addr 38 | self._i2c.writeto(addr, b"\xE4") # Z 39 | if t := (self._value() > 100): # Touched 40 | self._i2c.writeto(addr, b"\xC4") # X 41 | self._x = self._value() 42 | self._i2c.writeto(addr, b"\xD4") # Y 43 | self._y = self._value() 44 | self._i2c.writeto(addr, b"\x00") # Low power/read temp 45 | return t 46 | -------------------------------------------------------------------------------- /touch/xpt2046.py: -------------------------------------------------------------------------------- 1 | # xpt2046.py Touch driver for XPT2046 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2024 Peter Hinch 5 | 6 | # It is minimal, providing only the required functionality for the touh GUI. 7 | # Sources: datasheet. 8 | # The following were studied for noise reduction approach, # mainly implemented 9 | # in base class: 10 | # https://github.com/dmquirozc/XPT2046_driver_STM32/blob/main/xpt2046.c 11 | # https://github.com/PaulStoffregen/XPT2046_Touchscreen/blob/master/XPT2046_Touchscreen.cpp 12 | # https://github.com/robert-hh/micropython-ili9341/blob/master/xpt2046.py 13 | 14 | # SPI clock rate 2.5MHz max 15 | 16 | from .touch import ABCTouch, PreProcess 17 | 18 | 19 | class XPT2046(ABCTouch): 20 | def __init__(self, spi, cspin, ssd, *, alen=10): 21 | # Instantiate a preprocessor 22 | pp = PreProcess(self, alen) 23 | super().__init__(ssd, pp) 24 | self.csn = cspin 25 | self.spi = spi 26 | self.wbuf = bytearray(3) 27 | self.rbuf = bytearray(3) 28 | 29 | def _value(self, chan): 30 | # PD0, PD1 == 1 See table 8: always powered, penIRQ off. Start bit == 1 31 | self.wbuf[0] = 0x83 | (chan << 4) # 12 bit differential mode 32 | self.spi.write_readinto(self.wbuf, self.rbuf) 33 | return (int.from_bytes(self.rbuf, "big") >> 3) & 0xFFF 34 | 35 | def acquire(self) -> bool: 36 | self.csn(0) 37 | if t := (self._value(3) > 100): # Get Z1 38 | self._x = self._value(5) 39 | self._y = self._value(1) 40 | self.csn(1) 41 | return t 42 | --------------------------------------------------------------------------------