├── LICENSE ├── README.md ├── Splooshtest.jpg ├── barebones.py ├── color_setup.py ├── drivers ├── boolpalette.py └── ssd1351 │ ├── __init__.py │ ├── ssd1351.py │ ├── ssd1351_16bit.py │ ├── ssd1351_4bit.py │ └── ssd1351_generic.py ├── gui ├── core │ ├── __init__.py │ ├── colors.py │ ├── fplot.py │ ├── nanogui.py │ └── writer.py ├── demos │ ├── aclock.py │ ├── aclock_large.py │ ├── aclock_ttgo.py │ ├── alevel.py │ ├── asnano.py │ ├── asnano_sync.py │ ├── clock_batt.py │ ├── clocktest.py │ ├── color15.py │ ├── color96.py │ ├── epd29_async.py │ ├── epd29_lowpower.py │ ├── epd29_sync.py │ ├── fpt.py │ ├── mono_test.py │ ├── scale.py │ ├── sharptest.py │ ├── tbox.py │ └── waveshare_test.py ├── fonts │ ├── arial10.py │ ├── arial35.py │ ├── arial_50.py │ ├── courier20.py │ ├── font10.py │ ├── font6.py │ ├── freesans20.py │ └── quantico40.py └── widgets │ ├── __init__.py │ ├── dial.py │ ├── label.py │ ├── led.py │ ├── meter.py │ ├── scale.py │ └── textbox.py ├── lib ├── ds18x20.py └── onewire.py ├── main.py ├── primitives ├── __init__.py ├── aadc.py ├── barrier.py ├── condition.py ├── delay_ms.py ├── encoder.py ├── message.py ├── pushbutton.py ├── queue.py ├── semaphore.py ├── switch.py └── tests │ ├── __init__.py │ ├── adctest.py │ ├── asyntest.py │ ├── delay_test.py │ ├── encoder_stop.py │ ├── encoder_test.py │ └── switches.py └── sploosh.jpg /README.md: -------------------------------------------------------------------------------- 1 | ![sploosh](/Splooshtest.jpg) 2 | 3 | [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCz5BOU9J9pB_O0B8-rDjCWQ?style=flat&logo=youtube&logoColor=red&labelColor=white&color=ffed53)](https://www.youtube.com/channel/UCz5BOU9J9pB_O0B8-rDjCWQ) [![Instagram](https://img.shields.io/github/stars/veebch?style=flat&logo=github&logoColor=black&labelColor=white&color=ffed53)](https://www.instagram.com/v_e_e_b/) 4 | 5 | 6 | # Clive Moss (the Window-Box Boss) 7 | 8 | A [proportional integral derivative](https://en.wikipedia.org/wiki/PID_controller) (PID) controller that will be used to run a plant waterer. PID is a fancy way of saying that the code plays a game of 'Warmer', 'Colder' to get something to a particular value (in our example, a particular moistness). The internet is littered with examples of these things, so it is mostly a learning exercise that will use a few bits of code we've previously developed, and hopefully it will make us a little smarter along the way. This is a very lightly tweaked version of the code we used for [cooking](https://github.com/veebch/heat-o-matic). 9 | 10 | (There's also a bare-bones version in the repository that doesn't use a screen or encoder, and has a target moisture level that can be adjusted by setting a paramater in the code. This lower-power version can be ran from a battery for extended periods of time. This is the version used in the video) 11 | 12 | 13 | # Hardware 14 | 15 | - Raspberry Pi Pico 16 | - SSD1351 Waveshare OLED (not needed if you're making the bare bones version) 17 | - WGCD KY-040 Rotary Encoder (not needed if you're making the bare bones version) 18 | - [A **capacitive** soil-moisture sensor](https://www.az-delivery.de/en/products/bodenfeuchte-sensor-modul-v1-2P) 19 | - A relay switch 20 | - A plug socket for the water pump 21 | - A cheap fish tank water pump 22 | 23 | **Warning: Don't pump water using something that dislikes being power-cycled a lot. This is GPL code, ie NO WARRANTY** 24 | 25 | ## Wiring 26 | 27 | All of the pins are listed in main.py. 28 | GPIO to peripherals as follows: 29 | 30 | | [Pico GPIO](https://www.elektronik-kompendium.de/sites/raspberry-pi/bilder/raspberry-pi-pico-gpio.png) | OLED | 31 | |-----------|------| 32 | | 19 | DIN/MOSI | 33 | | 18 | CLK/SCK | 34 | | 17 | CS | 35 | | 20 | DC | 36 | | 21 | RST | 37 | 38 | (not needed if you're making the bare bones version) 39 | 40 | 41 | | [Pico GPIO](https://www.elektronik-kompendium.de/sites/raspberry-pi/bilder/raspberry-pi-pico-gpio.png) | Rotary Encoder | 42 | |-----------|----------------| 43 | | 2 | CLK | 44 | | 3 | DT | 45 | | 4 | SW | 46 | 47 | (not needed if you're making the bare bones version) 48 | 49 | 50 | | [Pico GPIO](https://www.elektronik-kompendium.de/sites/raspberry-pi/bilder/raspberry-pi-pico-gpio.png) | Relay | 51 | |-----------|----------------| 52 | | 15 | Signal | 53 | 54 | 55 | | [Pico GPIO](https://www.elektronik-kompendium.de/sites/raspberry-pi/bilder/raspberry-pi-pico-gpio.png) | Soil Sensor | 56 | |-----------|----------------| 57 | | 26 | A0 | 58 | 59 | ## Enclosure 60 | 61 | The stl files for an enclosure are on thingiverse [here](https://www.thingiverse.com/thing:6125748) (Thanks Ryan!). 62 | 63 | # Firmware 64 | 65 | ## Installing onto a Pico 66 | 67 | First flash the board with the latest version of micropython. 68 | 69 | Then clone this repository onto your computer 70 | 71 | git clone https://github.com/veebch/sploosh 72 | 73 | and move into the repository directory 74 | 75 | cd sploosh 76 | 77 | If (**and only if**) you are using the bare-bones version (no screen or rotary encoder) 78 | 79 | mv main.py withscreen.py 80 | mv barebones.py main.py 81 | 82 | There are a few files to copy to the pico, [ampy](https://learn.adafruit.com/micropython-basics-load-files-and-run-code/install-ampy) is a good way to do it. 83 | 84 | ampy -p /dev/ttyACM0 put ./ 85 | 86 | substitute the device name to whatever the pico is on your system. 87 | 88 | # Running 89 | 90 | Plug it in, pop the soil probe into the medium you are going to water, plug the watering device into the plug socket, pick a setpoint using the dial. That's it! 91 | 92 | # Video 93 | 94 | Here's a video outlining how it works and a timelapse of it looking after a plant for a week: 95 | 96 | [![Mod demo](http://img.youtube.com/vi/WVijoh-hqkw/0.jpg)](http://www.youtube.com/watch?v=WVijoh-hqkw "Video Title") 97 | 98 | # Contributing to the code 99 | 100 | If you look at this and feel like you can make it better, please fork the repository and use a feature branch. Pull requests are welcome and encouraged. 101 | 102 | # Licence 103 | GPL 3.0 104 | -------------------------------------------------------------------------------- /Splooshtest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/Splooshtest.jpg -------------------------------------------------------------------------------- /barebones.py: -------------------------------------------------------------------------------- 1 | # Headless Bare-Bones version of sploosh. No screen, no dials, uses battery 2 | # List of pins 3 | # 4 | # ADC: 26 (for moisuture reading) 5 | # Relaypin: 15 6 | # LED: 25 (onboard) 7 | 8 | from machine import Pin,I2C,ADC 9 | import utime 10 | import gc 11 | 12 | # The Tweakable values that will help tune for our use case 13 | calibratewet=20000 # ADC value for a very wet thing 14 | calibratedry=50000 # ADC value for a very dry thing 15 | checkin = 5 # Time interval (seconds) for each cycle of monitoring loop 16 | # PID Parameters 17 | # Description Stolen From Reddit: In terms of steering a ship: 18 | # Kp is steering harder the further off course you are, 19 | # Ki is steering into the wind to targetact a drift 20 | # Kd is slowing the turn as you approach your course 21 | Kp=2 # Proportional term - Basic steering (This is the first parameter you should tune for a particular setup) 22 | Ki=0 # Integral term - Compensate for heat loss by vessel 23 | Kd=0 # Derivative term - to prevent overshoot due to inertia - if it is zooming towards setpoint this 24 | # will cancel out the proportional term due to the large negative gradient 25 | target=6 # Target Wetness For Plant 26 | 27 | # Initialise Pins 28 | led=Pin(25,Pin.OUT) # Blink onboard LED as proof-of-life 29 | relaypin = Pin(15, mode = Pin.OUT) 30 | wetness = machine.ADC(26) 31 | # Blink LED as proof of life 32 | led.value(1) 33 | utime.sleep(.2) 34 | led.value(0) 35 | # Initialise 36 | gc.enable() # Garbage collection enabled 37 | integral = 0 38 | lastupdate = utime.time() 39 | lasterror = 0 40 | output=0 41 | offstate=True 42 | # PID infinite loop 43 | while True: 44 | try: 45 | # Get wetness 46 | imwet=wetness.read_u16() 47 | howdry = min(10,max(0,10*(imwet-calibratedry)/(calibratewet-calibratedry))) # linear relationship between ADC and wetness, clamped between 0, 10 48 | now = utime.time() 49 | dt= now-lastupdate 50 | if offstate == False and dt > checkin * round(output)/100 : 51 | relaypin = Pin(15, mode = Pin.OUT, value =0 ) 52 | offstate= True 53 | utime.sleep(.1) 54 | if dt > checkin: 55 | print("WETNESS",howdry) 56 | error=target-howdry 57 | integral = integral + dt * error 58 | derivative = (error - lasterror)/dt 59 | output = Kp * error + Ki * integral + Kd * derivative 60 | print(str(output)+"= Kp term: "+str(Kp*error)+" + Ki term:" + str(Ki*integral) + "+ Kd term: " + str(Kd*derivative)) 61 | output = max(min(100, output), 0) # Clamp output between 0 and 100 62 | print("OUTPUT ",output) 63 | if output>0: 64 | relaypin.value(1) 65 | offstate = False 66 | else: 67 | relaypin.value(0) 68 | offstate = True 69 | utime.sleep(.1) 70 | lastupdate = now 71 | lasterror = error 72 | except Exception as e: 73 | print('error encountered:'+str(e)) 74 | utime.sleep(checkin) 75 | -------------------------------------------------------------------------------- /color_setup.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import gc 3 | from drivers.ssd1351.ssd1351 import SSD1351 as SSD 4 | 5 | height = 128 # height = 128 # 1.5 inch 128*128 display 6 | 7 | pdc = machine.Pin(20, machine.Pin.OUT, value=0) 8 | pcs = machine.Pin(17, machine.Pin.OUT, value=1) 9 | prst = machine.Pin(21, machine.Pin.OUT, value=1) 10 | spi = machine.SPI(0, 11 | baudrate=10000000, 12 | polarity=1, 13 | phase=1, 14 | bits=8, 15 | firstbit=machine.SPI.MSB, 16 | sck=machine.Pin(18), 17 | mosi=machine.Pin(19), 18 | miso=machine.Pin(16)) 19 | gc.collect() # Precaution before instantiating framebuf 20 | ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance 21 | -------------------------------------------------------------------------------- /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/ssd1351/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/drivers/ssd1351/__init__.py -------------------------------------------------------------------------------- /drivers/ssd1351/ssd1351.py: -------------------------------------------------------------------------------- 1 | # SSD1351.py MicroPython driver for Adafruit color OLED displays. 2 | # STM (Pyboard etc) version. Display refresh takes 41ms on Pyboard V1.0 3 | 4 | # Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 5 | # Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 6 | # For wiring details see drivers/ADAFRUIT.md in this repo. 7 | 8 | # This driver is based on the Adafruit C++ library for Arduino 9 | # https://github.com/adafruit/Adafruit-SSD1351-library.git 10 | 11 | # Copyright (c) Peter Hinch 2018-2020 12 | # Released under the MIT license see LICENSE 13 | 14 | import framebuf 15 | import utime 16 | import gc 17 | import micropython 18 | from uctypes import addressof 19 | from drivers.boolpalette import BoolPalette 20 | 21 | # Timings with standard emitter 22 | # 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms 23 | # Buffer transfer time = 272-240 = 32ms which accords with expected: 24 | # 128*128*2/10500000 = 31.2ms (2 bytes/pixel, baudrate = 10.5MHz) 25 | # With assembler .show() takes 41ms 26 | 27 | # Copy a buffer with 8 bit rrrgggbb pixels to a buffer of 16 bit pixels. 28 | @micropython.asm_thumb 29 | def _lcopy(r0, r1, r2): # r0 dest, r1 source, r2 no. of bytes 30 | label(LOOP) 31 | ldrb(r3, [r1, 0]) # Get source byte to r3, r5, r6 32 | mov(r5, r3) 33 | mov(r6, r3) 34 | mov(r4, 3) 35 | and_(r3, r4) 36 | mov(r4, 6) 37 | lsl(r3, r4) 38 | mov(r4, 0x1c) 39 | and_(r5, r4) 40 | mov(r4, 2) 41 | lsr(r5, r4) 42 | orr(r3, r5) 43 | strb(r3, [r0, 0]) 44 | mov(r4, 0xe0) 45 | and_(r6, r4) 46 | mov(r4, 3) 47 | lsr(r6, r4) 48 | strb(r6, [r0, 1]) 49 | add(r0, 2) 50 | add(r1, 1) 51 | sub(r2, 1) 52 | bne(LOOP) 53 | 54 | # Initialisation commands in cmd_init: 55 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 56 | # 0xae, # display off (sleep mode) 57 | # 0xb3, 0xf1, # clock div 58 | # 0xca, 0x7f, # mux ratio 59 | # 0xa0, 0x74, # setremap 0x74 60 | # 0x15, 0, 0x7f, # setcolumn 61 | # 0x75, 0, 0x7f, # setrow 62 | # 0xa1, 0, # set display start line 63 | # 0xa2, 0, # displayoffset 64 | # 0xb5, 0, # setgpio 65 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 66 | # 0xb1, 0x32, # Precharge 67 | # 0xbe, 0x05, # vcommh 68 | # 0xa6, # normaldisplay 69 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 70 | # 0xc7, 0x0f, # Master contrast 71 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 72 | # 0xb6, 1, # Precharge 2 73 | # 0xaf, # Display on 74 | 75 | # SPI baudrate: Pyboard can produce 10.5MHz or 21MHz. Datasheet gives max of 20MHz. 76 | # Attempt to use 21MHz failed but might work on a PCB or with very short leads. 77 | class SSD1351(framebuf.FrameBuffer): 78 | # Convert r, g, b in range 0-255 to an 8 bit colour value 79 | # acceptable to hardware: rrrgggbb 80 | @staticmethod 81 | def rgb(r, g, b): 82 | return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) 83 | 84 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 85 | if height not in (96, 128): 86 | raise ValueError('Unsupported height {}'.format(height)) 87 | self.spi = spi 88 | self.spi_init = init_spi 89 | self.pincs = pincs 90 | self.pindc = pindc # 1 = data 0 = cmd 91 | self.height = height # Required by Writer class 92 | self.width = width 93 | mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. 94 | self.palette = BoolPalette(mode) 95 | gc.collect() 96 | self.buffer = bytearray(self.height * self.width) 97 | super().__init__(self.buffer, self.width, self.height, mode) 98 | self.linebuf = bytearray(self.width * 2) 99 | pinrs(0) # Pulse the reset line 100 | utime.sleep_ms(1) 101 | pinrs(1) 102 | utime.sleep_ms(1) 103 | if self.spi_init: # A callback was passed 104 | self.spi_init(spi) # Bus may be shared 105 | # See above comment to explain this allocation-saving gibberish. 106 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 107 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 108 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 109 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 110 | self.show() 111 | gc.collect() 112 | 113 | def _write(self, buf, dc): 114 | self.pincs(1) 115 | self.pindc(dc) 116 | self.pincs(0) 117 | self.spi.write(buf) 118 | self.pincs(1) 119 | 120 | # Write lines from the framebuf out of order to match the mapping of the 121 | # SSD1351 RAM to the OLED device. 122 | def show(self): 123 | lb = self.linebuf 124 | buf = self.buffer 125 | if self.spi_init: # A callback was passed 126 | self.spi_init(self.spi) # Bus may be shared 127 | self._write(b'\x5c', 0) # Enable data write 128 | if self.height == 128: 129 | for l in range(128): 130 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126 .. 96 131 | start = l0 * self.width 132 | _lcopy(lb, addressof(buf) + start, self.width) 133 | self._write(lb, 1) # Send a line 134 | else: 135 | for l in range(128): 136 | if l < 64: 137 | start = (63 -l) * self.width # 63 62 .. 1 0 138 | _lcopy(lb, addressof(buf) + start, self.width) 139 | self._write(lb, 1) # Send a line 140 | elif l < 96: # This is daft but I can't get setrow to work 141 | self._write(lb, 1) # Let RAM counter increase 142 | else: 143 | start = (191 - l) * self.width # 127 126 .. 95 144 | _lcopy(lb, addressof(buf) + start, self.width) 145 | self._write(lb, 1) # Send a line 146 | -------------------------------------------------------------------------------- /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 uctypes import addressof 18 | from drivers.boolpalette import BoolPalette 19 | 20 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 21 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 22 | # Now using 0,0 on STM and ESP32 23 | 24 | # Initialisation commands in cmd_init: 25 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 26 | # 0xae, # display off (sleep mode) 27 | # 0xb3, 0xf1, # clock div 28 | # 0xca, 0x7f, # mux ratio 29 | # 0xa0, 0x74, # setremap 0x74 30 | # 0x15, 0, 0x7f, # setcolumn 31 | # 0x75, 0, 0x7f, # setrow 32 | # 0xa1, 0, # set display start line 33 | # 0xa2, 0, # displayoffset 34 | # 0xb5, 0, # setgpio 35 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 36 | # 0xb1, 0x32, # Precharge 37 | # 0xbe, 0x05, # vcommh 38 | # 0xa6, # normaldisplay 39 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 40 | # 0xc7, 0x0f, # Master contrast 41 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 42 | # 0xb6, 1, # Precharge 2 43 | # 0xaf, # Display on 44 | 45 | 46 | class SSD1351(framebuf.FrameBuffer): 47 | # Convert r, g, b in range 0-255 to a 16 bit colour value RGB565 48 | # acceptable to hardware: rrrrrggggggbbbbb 49 | @staticmethod 50 | def rgb(r, g, b): 51 | return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5) 52 | 53 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 54 | if height not in (96, 128): 55 | raise ValueError('Unsupported height {}'.format(height)) 56 | self.spi = spi 57 | self.spi_init = init_spi 58 | self.pincs = pincs 59 | self.pindc = pindc # 1 = data 0 = cmd 60 | self.height = height # Required by Writer class 61 | self.width = width 62 | mode = framebuf.RGB565 63 | self.palette = BoolPalette(mode) 64 | gc.collect() 65 | self.buffer = bytearray(self.height * self.width * 2) 66 | super().__init__(self.buffer, self.width, self.height, mode) 67 | self.mvb = memoryview(self.buffer) 68 | pinrs(0) # Pulse the reset line 69 | utime.sleep_ms(1) 70 | pinrs(1) 71 | utime.sleep_ms(1) 72 | if self.spi_init: # A callback was passed 73 | self.spi_init(spi) # Bus may be shared 74 | # See above comment to explain this allocation-saving gibberish. 75 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 76 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 77 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 78 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 79 | self.show() 80 | gc.collect() 81 | 82 | def _write(self, mv, dc): 83 | self.pincs(1) 84 | self.pindc(dc) 85 | self.pincs(0) 86 | self.spi.write(bytes(mv)) 87 | self.pincs(1) 88 | 89 | # Write lines from the framebuf out of order to match the mapping of the 90 | # SSD1351 RAM to the OLED device. 91 | def show(self): 92 | mvb = self.mvb 93 | bw = self.width * 2 # Width in bytes 94 | if self.spi_init: # A callback was passed 95 | self.spi_init(self.spi) # Bus may be shared 96 | self._write(b'\x5c', 0) # Enable data write 97 | if self.height == 128: 98 | for l in range(128): 99 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126 .. 96 100 | start = l0 * self.width * 2 101 | self._write(mvb[start : start + bw], 1) # Send a line 102 | else: 103 | for l in range(128): 104 | if l < 64: 105 | start = (63 -l) * self.width * 2 # 63 62 .. 1 0 106 | elif l < 96: 107 | start = 0 108 | else: 109 | start = (191 - l) * self.width * 2 # 127 126 .. 95 110 | self._write(mvb[start : start + bw], 1) # Send a line 111 | -------------------------------------------------------------------------------- /drivers/ssd1351/ssd1351_4bit.py: -------------------------------------------------------------------------------- 1 | # SSD1351_4bit.py MicroPython driver for Adafruit color OLED displays. 2 | # This is cross-platform and uses 4 bit color for minimum RAM usage. 3 | 4 | # Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 5 | # Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 6 | # For wiring details see drivers/ADAFRUIT.md in this repo. 7 | 8 | # This driver is based on the Adafruit C++ library for Arduino 9 | # https://github.com/adafruit/Adafruit-SSD1351-library.git 10 | 11 | # Copyright (c) Peter Hinch 2020 12 | # Released under the MIT license see LICENSE 13 | 14 | import framebuf 15 | import utime 16 | import gc 17 | import micropython 18 | from uctypes import addressof 19 | from drivers.boolpalette import BoolPalette 20 | 21 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 22 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 23 | # Now using 0,0 on STM and ESP32 24 | 25 | # ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz 26 | # OLED datasheet: should support 20MHz 27 | def spi_init(spi): 28 | spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz 29 | 30 | @micropython.viper 31 | def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): 32 | n = 0 33 | for x in range(length): 34 | c = source[x] 35 | d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color) 36 | e = (c & 0x0f) << 1 37 | dest[n] = lut[d] 38 | n += 1 39 | dest[n] = lut[d + 1] 40 | n += 1 41 | dest[n] = lut[e] 42 | n += 1 43 | dest[n] = lut[e + 1] 44 | n += 1 45 | 46 | 47 | # Initialisation commands in cmd_init: 48 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 49 | # 0xae, # display off (sleep mode) 50 | # 0xb3, 0xf1, # clock div 51 | # 0xca, 0x7f, # mux ratio 52 | # 0xa0, 0x74, # setremap 0x74 53 | # 0x15, 0, 0x7f, # setcolumn 54 | # 0x75, 0, 0x7f, # setrow 55 | # 0xa1, 0, # set display start line 56 | # 0xa2, 0, # displayoffset 57 | # 0xb5, 0, # setgpio 58 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 59 | # 0xb1, 0x32, # Precharge 60 | # 0xbe, 0x05, # vcommh 61 | # 0xa6, # normaldisplay 62 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 63 | # 0xc7, 0x0f, # Master contrast 64 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 65 | # 0xb6, 1, # Precharge 2 66 | # 0xaf, # Display on 67 | 68 | 69 | class SSD1351(framebuf.FrameBuffer): 70 | 71 | lut = bytearray(32) 72 | 73 | # Convert r, g, b in range 0-255 to a 16 bit colour value 74 | # LS byte goes into LUT offset 0, MS byte into offset 1 75 | # Same mapping in linebuf so LS byte is shifted out 1st 76 | # Note pretty colors in datasheet don't match actual colors. See doc. 77 | @staticmethod 78 | def rgb(r, g, b): 79 | return (r & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (b & 0xf8) 80 | 81 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 82 | if height not in (96, 128): 83 | raise ValueError('Unsupported height {}'.format(height)) 84 | self.spi = spi 85 | self.pincs = pincs 86 | self.pindc = pindc # 1 = data 0 = cmd 87 | self.height = height # Required by Writer class 88 | self.width = width 89 | self.spi_init = init_spi 90 | mode = framebuf.GS4_HMSB # Use 4bit greyscale. 91 | self.palette = BoolPalette(mode) 92 | gc.collect() 93 | self.buffer = bytearray(self.height * self.width // 2) 94 | super().__init__(self.buffer, self.width, self.height, mode) 95 | self.linebuf = bytearray(self.width * 2) 96 | pinrs(0) # Pulse the reset line 97 | utime.sleep_ms(1) 98 | pinrs(1) 99 | utime.sleep_ms(1) 100 | if self.spi_init: # A callback was passed 101 | self.spi_init(spi) # Bus may be shared 102 | # See above comment to explain this allocation-saving gibberish. 103 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 104 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 105 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 106 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 107 | gc.collect() 108 | self.show() 109 | 110 | def _write(self, buf, dc): 111 | self.pincs(1) 112 | self.pindc(dc) 113 | self.pincs(0) 114 | self.spi.write(buf) 115 | self.pincs(1) 116 | 117 | # Write lines from the framebuf out of order to match the mapping of the 118 | # SSD1351 RAM to the OLED device. 119 | def show(self): # 44ms on Pyboard 1.x 120 | clut = SSD1351.lut 121 | lb = self.linebuf 122 | wd = self.width // 2 123 | buf = memoryview(self.buffer) 124 | if self.spi_init: # A callback was passed 125 | self.spi_init(self.spi) # Bus may be shared 126 | self._write(b'\x5c', 0) # Enable data write 127 | if self.height == 128: 128 | for l in range(128): 129 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126... 130 | start = l0 * wd 131 | _lcopy(lb, buf[start : start + wd], clut, wd) 132 | self._write(lb, 1) # Send a line 133 | else: 134 | for l in range(128): 135 | if l < 64: 136 | start = (63 -l) * wd 137 | _lcopy(lb, buf[start : start + wd], clut, wd) 138 | elif l < 96: # This is daft but I can't get setrow to work 139 | pass # Let RAM counter increase 140 | else: 141 | start = (191 - l) * wd 142 | _lcopy(lb, buf[start : start + wd], clut, wd) 143 | self._write(lb, 1) # Send a line 144 | 145 | -------------------------------------------------------------------------------- /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 uctypes import addressof 20 | from drivers.boolpalette import BoolPalette 21 | 22 | import sys 23 | # https://github.com/peterhinch/micropython-nano-gui/issues/2 24 | # The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. 25 | # Now using 0,0 on STM and ESP32 26 | 27 | # Timings with standard emitter 28 | # 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms 29 | # Buffer transfer time = 272-240 = 32ms which accords with expected: 30 | # 128*128*2/10500000 = 31.2ms (2 bytes/pixel, baudrate = 10.5MHz) 31 | # With viper emitter show() takes 47ms vs 41ms for assembler. 32 | 33 | @micropython.viper 34 | def _lcopy(dest:ptr8, source:ptr8, length:int): 35 | n = 0 36 | for x in range(length): 37 | c = source[x] 38 | dest[n] = ((c & 3) << 6) | ((c & 0x1c) >> 2) # Blue green 39 | n += 1 40 | dest[n] = (c & 0xe0) >> 3 # Red 41 | n += 1 42 | 43 | # Initialisation commands in cmd_init: 44 | # 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode 45 | # 0xae, # display off (sleep mode) 46 | # 0xb3, 0xf1, # clock div 47 | # 0xca, 0x7f, # mux ratio 48 | # 0xa0, 0x74, # setremap 0x74 49 | # 0x15, 0, 0x7f, # setcolumn 50 | # 0x75, 0, 0x7f, # setrow 51 | # 0xa1, 0, # set display start line 52 | # 0xa2, 0, # displayoffset 53 | # 0xb5, 0, # setgpio 54 | # 0xab, 1, # functionselect: serial interface, internal Vdd regulator 55 | # 0xb1, 0x32, # Precharge 56 | # 0xbe, 0x05, # vcommh 57 | # 0xa6, # normaldisplay 58 | # 0xc1, 0xc8, 0x80, 0xc8, # contrast abc 59 | # 0xc7, 0x0f, # Master contrast 60 | # 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) 61 | # 0xb6, 1, # Precharge 2 62 | # 0xaf, # Display on 63 | 64 | class SSD1351(framebuf.FrameBuffer): 65 | # Convert r, g, b in range 0-255 to an 8 bit colour value 66 | # acceptable to hardware: rrrgggbb 67 | @staticmethod 68 | def rgb(r, g, b): 69 | return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) 70 | 71 | def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): 72 | if height not in (96, 128): 73 | raise ValueError('Unsupported height {}'.format(height)) 74 | self.spi = spi 75 | self.spi_init = init_spi 76 | self.pincs = pincs 77 | self.pindc = pindc # 1 = data 0 = cmd 78 | self.height = height # Required by Writer class 79 | self.width = width 80 | mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. 81 | self.palette = BoolPalette(mode) 82 | gc.collect() 83 | self.buffer = bytearray(self.height * self.width) 84 | super().__init__(self.buffer, self.width, self.height, mode) 85 | self.linebuf = bytearray(self.width * 2) 86 | pinrs(0) # Pulse the reset line 87 | utime.sleep_ms(1) 88 | pinrs(1) 89 | utime.sleep_ms(1) 90 | if self.spi_init: # A callback was passed 91 | self.spi_init(spi) # Bus may be shared 92 | # See above comment to explain this allocation-saving gibberish. 93 | self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ 94 | b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ 95 | b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ 96 | b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) 97 | gc.collect() 98 | self.show() 99 | 100 | def _write(self, buf, dc): 101 | self.pincs(1) 102 | self.pindc(dc) 103 | self.pincs(0) 104 | self.spi.write(buf) 105 | self.pincs(1) 106 | 107 | # Write lines from the framebuf out of order to match the mapping of the 108 | # SSD1351 RAM to the OLED device. 109 | def show(self): 110 | lb = self.linebuf 111 | buf = memoryview(self.buffer) 112 | if self.spi_init: # A callback was passed 113 | self.spi_init(self.spi) # Bus may be shared 114 | self._write(b'\x5c', 0) # Enable data write 115 | if self.height == 128: 116 | for l in range(128): 117 | l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126... 118 | start = l0 * self.width 119 | _lcopy(lb, buf[start : start + self.width], self.width) 120 | self._write(lb, 1) # Send a line 121 | else: 122 | for l in range(128): 123 | if l < 64: 124 | start = (63 -l) * self.width 125 | _lcopy(lb, buf[start : start + self.width], self.width) 126 | self._write(lb, 1) # Send a line 127 | elif l < 96: # This is daft but I can't get setrow to work 128 | self._write(lb, 1) # Let RAM counter increase 129 | else: 130 | start = (191 - l) * self.width 131 | _lcopy(lb, buf[start : start + self.width], self.width) 132 | self._write(lb, 1) # Send a line 133 | 134 | -------------------------------------------------------------------------------- /gui/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/gui/core/__init__.py -------------------------------------------------------------------------------- /gui/core/colors.py: -------------------------------------------------------------------------------- 1 | # colors.py Standard color constants for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | from color_setup import SSD 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 | c = SSD.rgb(r, g, b) 11 | if not hasattr(SSD, 'lut'): 12 | return c 13 | if not 0 <= idx <= 15: 14 | raise ValueError('Color nos must be 0..15') 15 | x = idx << 1 16 | SSD.lut[x] = c & 0xff 17 | SSD.lut[x + 1] = c >> 8 18 | return idx 19 | 20 | if hasattr(SSD, 'lut'): # Colors defined by LUT 21 | BLACK = create_color(0, 0, 0, 0) 22 | GREEN = create_color(1, 0, 255, 0) 23 | RED = create_color(2, 255, 0, 0) 24 | LIGHTRED = create_color(3, 140, 0, 0) 25 | BLUE = create_color(4, 0, 0, 255) 26 | YELLOW = create_color(5, 255, 255, 0) 27 | GREY = create_color(6, 100, 100, 100) 28 | MAGENTA = create_color(7, 255, 0, 255) 29 | CYAN = create_color(8, 0, 255, 255) 30 | LIGHTGREEN = create_color(9, 0, 100, 0) 31 | DARKGREEN = create_color(10, 0, 80, 0) 32 | DARKBLUE = create_color(11, 0, 0, 90) 33 | # 12, 13, 14 free for user definition 34 | WHITE = create_color(15, 255, 255, 255) 35 | else: 36 | BLACK = SSD.rgb(0, 0, 0) 37 | GREEN = SSD.rgb(0, 255, 0) 38 | RED = SSD.rgb(255, 0, 0) 39 | LIGHTRED = SSD.rgb(140, 0, 0) 40 | BLUE = SSD.rgb(0, 0, 255) 41 | YELLOW = SSD.rgb(255, 255, 0) 42 | GREY = SSD.rgb(100, 100, 100) 43 | MAGENTA = SSD.rgb(255, 0, 255) 44 | CYAN = SSD.rgb(0, 255, 255) 45 | LIGHTGREEN = SSD.rgb(0, 100, 0) 46 | DARKGREEN = SSD.rgb(0, 80, 0) 47 | DARKBLUE = SSD.rgb(0, 0, 90) 48 | WHITE = SSD.rgb(255, 255, 255) 49 | -------------------------------------------------------------------------------- /gui/core/fplot.py: -------------------------------------------------------------------------------- 1 | # fplot.py Graph plotting extension for nanogui 2 | # Now clips out of range lines 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2018 Peter Hinch 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | 26 | from gui.core.nanogui import DObject, circle 27 | from cmath import rect, pi 28 | from micropython import const 29 | from array import array 30 | 31 | type_gen = type((lambda: (yield))()) 32 | 33 | # Line clipping outcode bits 34 | _TOP = const(1) 35 | _BOTTOM = const(2) 36 | _LEFT = const(4) 37 | _RIGHT = const(8) 38 | # Bounding box for line clipping 39 | _XMAX = const(1) 40 | _XMIN = const(-1) 41 | _YMAX = const(1) 42 | _YMIN = const(-1) 43 | 44 | 45 | class Curve(): 46 | @staticmethod 47 | def _outcode(x, y): 48 | oc = _TOP if y > 1 else 0 49 | oc |= _BOTTOM if y < -1 else 0 50 | oc |= _RIGHT if x > 1 else 0 51 | oc |= _LEFT if x < -1 else 0 52 | return oc 53 | 54 | def __init__(self, graph, color, populate=None, origin=(0, 0), excursion=(1, 1)): 55 | if not isinstance(self, PolarCurve): # Check not done in subclass 56 | if isinstance(graph, PolarGraph) or not isinstance(graph, CartesianGraph): 57 | raise ValueError('Curve must use a CartesianGraph instance.') 58 | self.graph = graph 59 | self.origin = origin 60 | self.excursion = excursion 61 | self.color = color if color is not None else graph.fgcolor 62 | self.lastpoint = None 63 | self.newpoint = None 64 | if populate is not None and self._valid(populate): 65 | for x, y in populate: 66 | self.point(x, y) 67 | 68 | def _valid(self, populate): 69 | if not isinstance(populate, type_gen): 70 | raise ValueError('populate must be a generator.') 71 | return True 72 | 73 | def point(self, x=None, y=None): 74 | if x is None or y is None: 75 | self.newpoint = None 76 | self.lastpoint = None 77 | return 78 | 79 | self.newpoint = self._scale(x, y) # In-range points scaled to +-1 bounding box 80 | if self.lastpoint is None: # Nothing to plot. Save for next line. 81 | self.lastpoint = self.newpoint 82 | return 83 | 84 | res = self._clip(*(self.lastpoint + self.newpoint)) # Clip to +-1 box 85 | if res is not None: # Ignore lines which don't intersect 86 | self.graph.line(res[0:2], res[2:], self.color) 87 | self.lastpoint = self.newpoint # Scaled but not clipped 88 | 89 | # Cohen–Sutherland line clipping algorithm 90 | # If self.newpoint and self.lastpoint are valid clip them so that both lie 91 | # in +-1 range. If both are outside the box return None. 92 | def _clip(self, x0, y0, x1, y1): 93 | oc1 = self._outcode(x0, y0) 94 | oc2 = self._outcode(x1, y1) 95 | while True: 96 | if not oc1 | oc2: # OK to plot 97 | return x0, y0, x1, y1 98 | if oc1 & oc2: # Nothing to do 99 | return 100 | oc = oc1 if oc1 else oc2 101 | if oc & _TOP: 102 | x = x0 + (_YMAX - y0)*(x1 - x0)/(y1 - y0) 103 | y = _YMAX 104 | elif oc & _BOTTOM: 105 | x = x0 + (_YMIN - y0)*(x1 - x0)/(y1 - y0) 106 | y = _YMIN 107 | elif oc & _RIGHT: 108 | y = y0 + (_XMAX - x0)*(y1 - y0)/(x1 - x0) 109 | x = _XMAX 110 | elif oc & _LEFT: 111 | y = y0 + (_XMIN - x0)*(y1 - y0)/(x1 - x0) 112 | x = _XMIN 113 | if oc is oc1: 114 | x0, y0 = x, y 115 | oc1 = self._outcode(x0, y0) 116 | else: 117 | x1, y1 = x, y 118 | oc2 = self._outcode(x1, y1) 119 | 120 | def _scale(self, x, y): # Scale to +-1.0 121 | x0, y0 = self.origin 122 | xr, yr = self.excursion 123 | xs = (x - x0) / xr 124 | ys = (y - y0) / yr 125 | return xs, ys 126 | 127 | class PolarCurve(Curve): # Points are complex 128 | def __init__(self, graph, color, populate=None): 129 | if not isinstance(graph, PolarGraph): 130 | raise ValueError('PolarCurve must use a PolarGraph instance.') 131 | super().__init__(graph, color) 132 | if populate is not None and self._valid(populate): 133 | for z in populate: 134 | self.point(z) 135 | 136 | def point(self, z=None): 137 | if z is None: 138 | self.newpoint = None 139 | self.lastpoint = None 140 | return 141 | 142 | self.newpoint = self._scale(z.real, z.imag) # In-range points scaled to +-1 bounding box 143 | if self.lastpoint is None: # Nothing to plot. Save for next line. 144 | self.lastpoint = self.newpoint 145 | return 146 | 147 | res = self._clip(*(self.lastpoint + self.newpoint)) # Clip to +-1 box 148 | if res is not None: # At least part of line was in box 149 | start = res[0] + 1j*res[1] 150 | end = res[2] + 1j*res[3] 151 | self.graph.cline(start, end, self.color) 152 | self.lastpoint = self.newpoint # Scaled but not clipped 153 | 154 | 155 | class TSequence(Curve): 156 | def __init__(self, graph, color, size, yorigin=0, yexc=1): 157 | super().__init__(graph, color, origin=(0, yorigin), excursion=(1, yexc)) 158 | self.data = array('f', (0 for _ in range(size))) 159 | self.cur = 0 160 | self.size = size 161 | self.count = 0 162 | 163 | def add(self, v): 164 | p = self.cur 165 | size = self.size 166 | self.data[self.cur] = v 167 | self.cur += 1 168 | self.cur %= size 169 | if self.count < size: 170 | self.count += 1 171 | x = 0 172 | dx = 1/size 173 | for _ in range(self.count): 174 | self.point(x, self.data[p]) 175 | x -= dx 176 | p -= 1 177 | p %= size 178 | self.point() 179 | 180 | 181 | class Graph(DObject): 182 | def __init__(self, writer, row, col, height, width, fgcolor, bgcolor, bdcolor, gridcolor): 183 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 184 | super().show() # Draw border 185 | self.x0 = col 186 | self.x1 = col + width 187 | self.y0 = row 188 | self.y1 = row + height 189 | if gridcolor is None: 190 | gridcolor = self.fgcolor 191 | self.gridcolor = gridcolor 192 | 193 | def clear(self): 194 | self.show() # Clear working area 195 | 196 | class CartesianGraph(Graph): 197 | def __init__(self, writer, row, col, *, height=90, width = 120, fgcolor=None, bgcolor=None, bdcolor=None, 198 | gridcolor=None, xdivs=10, ydivs=10, xorigin=5, yorigin=5): 199 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, gridcolor) 200 | self.xdivs = xdivs 201 | self.ydivs = ydivs 202 | self.x_axis_len = max(xorigin, xdivs - xorigin) * width / xdivs # Max distance from origin in pixels 203 | self.y_axis_len = max(yorigin, ydivs - yorigin) * height / ydivs 204 | self.xp_origin = self.x0 + xorigin * width / xdivs # Origin in pixels 205 | self.yp_origin = self.y0 + (ydivs - yorigin) * height / ydivs 206 | self.xorigin = xorigin 207 | self.yorigin = yorigin 208 | self.show() 209 | 210 | def show(self): 211 | super().show() # Clear working area 212 | ssd = self.device 213 | x0 = self.x0 214 | x1 = self.x1 215 | y0 = self.y0 216 | y1 = self.y1 217 | if self.ydivs > 0: 218 | dy = self.height / (self.ydivs) # Y grid line 219 | for line in range(self.ydivs + 1): 220 | color = self.fgcolor if line == self.yorigin else self.gridcolor 221 | ypos = round(self.y1 - dy * line) 222 | ssd.hline(x0, ypos, x1 - x0, color) 223 | if self.xdivs > 0: 224 | width = x1 - x0 225 | dx = width / (self.xdivs) # X grid line 226 | for line in range(self.xdivs + 1): 227 | color = self.fgcolor if line == self.xorigin else self.gridcolor 228 | xpos = round(x0 + dx * line) 229 | ssd.vline(xpos, y0, y1 - y0, color) 230 | 231 | # Called by Curve 232 | def line(self, start, end, color): # start and end relative to origin and scaled -1 .. 0 .. +1 233 | xs = round(self.xp_origin + start[0] * self.x_axis_len) 234 | ys = round(self.yp_origin - start[1] * self.y_axis_len) 235 | xe = round(self.xp_origin + end[0] * self.x_axis_len) 236 | ye = round(self.yp_origin - end[1] * self.y_axis_len) 237 | self.device.line(xs, ys, xe, ye, color) 238 | 239 | class PolarGraph(Graph): 240 | def __init__(self, writer, row, col, *, height=90, fgcolor=None, bgcolor=None, bdcolor=None, 241 | gridcolor=None, adivs=3, rdivs=4): 242 | super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, gridcolor) 243 | self.adivs = adivs * 2 # No. of divisions of Pi radians 244 | self.rdivs = rdivs 245 | self.radius = round(height / 2) # Unit: pixels 246 | self.xp_origin = self.x0 + self.radius # Origin in pixels 247 | self.yp_origin = self.y0 + self.radius 248 | self.show() 249 | 250 | def show(self): 251 | super().show() # Clear working area 252 | ssd = self.device 253 | x0 = self.x0 254 | y0 = self.y0 255 | radius = self.radius 256 | adivs = self.adivs 257 | rdivs = self.rdivs 258 | diam = 2 * radius 259 | if rdivs > 0: 260 | for r in range(1, rdivs + 1): 261 | circle(ssd, self.xp_origin, self.yp_origin, round(radius * r / rdivs), self.gridcolor) 262 | if adivs > 0: 263 | v = complex(1) 264 | m = rect(1, pi / adivs) 265 | for _ in range(adivs): 266 | self.cline(-v, v, self.gridcolor) 267 | v *= m 268 | ssd.vline(x0 + radius, y0, diam, self.fgcolor) 269 | ssd.hline(x0, y0 + radius, diam, self.fgcolor) 270 | 271 | def cline(self, start, end, color): # start and end are complex, 0 <= magnitude <= 1 272 | height = self.radius # Unit: pixels 273 | xs = round(self.xp_origin + start.real * height) 274 | ys = round(self.yp_origin - start.imag * height) 275 | xe = round(self.xp_origin + end.real * height) 276 | ye = round(self.yp_origin - end.imag * height) 277 | self.device.line(xs, ys, xe, ye, color) 278 | -------------------------------------------------------------------------------- /gui/core/nanogui.py: -------------------------------------------------------------------------------- 1 | # nanogui.py Displayable objects based on the Writer and CWriter classes 2 | # V0.41 Peter Hinch 16th Nov 2020 3 | # Move cmath dependency to widgets/dial 4 | 5 | # Released under the MIT License (MIT). See LICENSE. 6 | # Copyright (c) 2018-2021 Peter Hinch 7 | 8 | # Base class for a displayable object. Subclasses must implement .show() and .value() 9 | # Has position, colors and border definition. 10 | # border: False no border None use bgcolor, int: treat as color 11 | 12 | from gui.core.colors import * # Populate color LUT before use. 13 | from gui.core.writer import Writer 14 | import framebuf 15 | import gc 16 | 17 | def _circle(dev, x0, y0, r, color): # Single pixel circle 18 | x = -r 19 | y = 0 20 | err = 2 -2*r 21 | while x <= 0: 22 | dev.pixel(x0 -x, y0 +y, color) 23 | dev.pixel(x0 +x, y0 +y, color) 24 | dev.pixel(x0 +x, y0 -y, color) 25 | dev.pixel(x0 -x, y0 -y, color) 26 | e2 = err 27 | if (e2 <= y): 28 | y += 1 29 | err += y*2 +1 30 | if (-x == y and e2 <= x): 31 | e2 = 0 32 | if (e2 > x): 33 | x += 1 34 | err += x*2 +1 35 | 36 | def circle(dev, x0, y0, r, color, width =1): # Draw circle 37 | x0, y0, r = int(x0), int(y0), int(r) 38 | for r in range(r, r -width, -1): 39 | _circle(dev, x0, y0, r, color) 40 | 41 | def fillcircle(dev, x0, y0, r, color): # Draw filled circle 42 | x0, y0, r = int(x0), int(y0), int(r) 43 | x = -r 44 | y = 0 45 | err = 2 -2*r 46 | while x <= 0: 47 | dev.line(x0 -x, y0 -y, x0 -x, y0 +y, color) 48 | dev.line(x0 +x, y0 -y, x0 +x, y0 +y, color) 49 | e2 = err 50 | if (e2 <= y): 51 | y +=1 52 | err += y*2 +1 53 | if (-x == y and e2 <= x): 54 | e2 = 0 55 | if (e2 > x): 56 | x += 1 57 | err += x*2 +1 58 | 59 | # If a (framebuf based) device is passed to refresh, the screen is cleared. 60 | # None causes pending widgets to be drawn and the result to be copied to hardware. 61 | # The pend mechanism enables a displayable object to postpone its renedering 62 | # until it is complete: efficient for e.g. Dial which may have multiple Pointers 63 | def refresh(device, clear=False): 64 | if not isinstance(device, framebuf.FrameBuffer): 65 | raise ValueError('Device must be derived from FrameBuffer.') 66 | if device not in DObject.devices: 67 | DObject.devices[device] = set() 68 | device.fill(0) 69 | else: 70 | if clear: 71 | DObject.devices[device].clear() # Clear the pending set 72 | device.fill(0) 73 | else: 74 | for obj in DObject.devices[device]: 75 | obj.show() 76 | DObject.devices[device].clear() 77 | device.show() 78 | 79 | # Displayable object: effectively an ABC for all GUI objects. 80 | class DObject(): 81 | devices = {} # Index device instance, value is a set of pending objects 82 | 83 | @classmethod 84 | def _set_pend(cls, obj): 85 | cls.devices[obj.device].add(obj) 86 | 87 | def __init__(self, writer, row, col, height, width, fgcolor, bgcolor, bdcolor): 88 | writer.set_clip(True, True, False) # Disable scrolling text 89 | self.writer = writer 90 | device = writer.device 91 | self.device = device 92 | # The following assumes that the widget is mal-positioned, not oversize. 93 | if row < 0: 94 | row = 0 95 | self.warning() 96 | elif row + height >= device.height: 97 | row = device.height - height - 1 98 | self.warning() 99 | if col < 0: 100 | col = 0 101 | self.warning() 102 | elif col + width >= device.width: 103 | col = device.width - width - 1 104 | self.warning() 105 | self.row = row 106 | self.col = col 107 | self.width = width 108 | self.height = height 109 | self._value = None # Type depends on context but None means don't display. 110 | # Current colors 111 | if fgcolor is None: 112 | fgcolor = writer.fgcolor 113 | if bgcolor is None: 114 | bgcolor = writer.bgcolor 115 | if bdcolor is None: 116 | bdcolor = fgcolor 117 | self.fgcolor = fgcolor 118 | self.bgcolor = bgcolor 119 | # bdcolor is False if no border is to be drawn 120 | self.bdcolor = bdcolor 121 | # Default colors allow restoration after dynamic change 122 | self.def_fgcolor = fgcolor 123 | self.def_bgcolor = bgcolor 124 | self.def_bdcolor = bdcolor 125 | # has_border is True if a border was drawn 126 | self.has_border = False 127 | 128 | def warning(self): 129 | print('Warning: attempt to create {} outside screen dimensions.'.format(self.__class__.__name__)) 130 | 131 | # Blank working area 132 | # Draw a border if .bdcolor specifies a color. If False, erase an existing border 133 | def show(self): 134 | wri = self.writer 135 | dev = self.device 136 | dev.fill_rect(self.col, self.row, self.width, self.height, self.bgcolor) 137 | if isinstance(self.bdcolor, bool): # No border 138 | if self.has_border: # Border exists: erase it 139 | dev.rect(self.col - 2, self.row - 2, self.width + 4, self.height + 4, self.bgcolor) 140 | self.has_border = False 141 | elif self.bdcolor: # Border is required 142 | dev.rect(self.col - 2, self.row - 2, self.width + 4, self.height + 4, self.bdcolor) 143 | self.has_border = True 144 | 145 | def value(self, v=None): 146 | if v is not None: 147 | self._value = v 148 | return self._value 149 | 150 | def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): 151 | if hasattr(self, 'label'): 152 | self.label.value(text, invert, fgcolor, bgcolor, bdcolor) 153 | else: 154 | raise ValueError('Attempt to update nonexistent label.') 155 | -------------------------------------------------------------------------------- /gui/core/writer.py: -------------------------------------------------------------------------------- 1 | # writer.py Implements the Writer class. 2 | # Handles colour, word wrap and tab stops 3 | 4 | # V0.5.0 Sep 2021 Color now requires firmware >= 1.17. 5 | # V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682). 6 | # V0.4.0 Jan 2021 Improved handling of word wrap and line clip. Upside-down 7 | # rendering no longer supported: delegate to device driver. 8 | # V0.3.5 Sept 2020 Fast rendering option for color displays 9 | 10 | # Released under the MIT License (MIT). See LICENSE. 11 | # Copyright (c) 2019-2021 Peter Hinch 12 | 13 | # A Writer supports rendering text to a Display instance in a given font. 14 | # Multiple Writer instances may be created, each rendering a font to the 15 | # same Display object. 16 | 17 | # Timings were run on a pyboard D SF6W comparing slow and fast rendering 18 | # and averaging over multiple characters. Proportional fonts were used. 19 | # 20 pixel high font, timings were 5.44ms/467μs, gain 11.7 (freesans20). 20 | # 10 pixel high font, timings were 1.76ms/396μs, gain 4.36 (arial10). 21 | 22 | 23 | import framebuf 24 | from uctypes import bytearray_at, addressof 25 | from sys import implementation 26 | import os 27 | 28 | __version__ = (0, 5, 0) 29 | 30 | fast_mode = True # Does nothing. Kept to avoid breaking code. 31 | 32 | class DisplayState(): 33 | def __init__(self): 34 | self.text_row = 0 35 | self.text_col = 0 36 | 37 | def _get_id(device): 38 | if not isinstance(device, framebuf.FrameBuffer): 39 | raise ValueError('Device must be derived from FrameBuffer.') 40 | return id(device) 41 | 42 | # Basic Writer class for monochrome displays 43 | class Writer(): 44 | 45 | state = {} # Holds a display state for each device 46 | 47 | @staticmethod 48 | def set_textpos(device, row=None, col=None): 49 | devid = _get_id(device) 50 | if devid not in Writer.state: 51 | Writer.state[devid] = DisplayState() 52 | s = Writer.state[devid] # Current state 53 | if row is not None: 54 | if row < 0 or row >= device.height: 55 | raise ValueError('row is out of range') 56 | s.text_row = row 57 | if col is not None: 58 | if col < 0 or col >= device.width: 59 | raise ValueError('col is out of range') 60 | s.text_col = col 61 | return s.text_row, s.text_col 62 | 63 | def __init__(self, device, font, verbose=True): 64 | self.devid = _get_id(device) 65 | self.device = device 66 | if self.devid not in Writer.state: 67 | Writer.state[self.devid] = DisplayState() 68 | self.font = font 69 | if font.height() >= device.height or font.max_width() >= device.width: 70 | raise ValueError('Font too large for screen') 71 | # Allow to work with reverse or normal font mapping 72 | if font.hmap(): 73 | self.map = framebuf.MONO_HMSB if font.reverse() else framebuf.MONO_HLSB 74 | else: 75 | raise ValueError('Font must be horizontally mapped.') 76 | if verbose: 77 | fstr = 'Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}.' 78 | print(fstr.format(font.reverse(), device.width, device.height)) 79 | print('Start row = {} col = {}'.format(self._getstate().text_row, self._getstate().text_col)) 80 | self.screenwidth = device.width # In pixels 81 | self.screenheight = device.height 82 | self.bgcolor = 0 # Monochrome background and foreground colors 83 | self.fgcolor = 1 84 | self.row_clip = False # Clip or scroll when screen fullt 85 | self.col_clip = False # Clip or new line when row is full 86 | self.wrap = True # Word wrap 87 | self.cpos = 0 88 | self.tab = 4 89 | 90 | self.glyph = None # Current char 91 | self.char_height = 0 92 | self.char_width = 0 93 | self.clip_width = 0 94 | 95 | def _getstate(self): 96 | return Writer.state[self.devid] 97 | 98 | def _newline(self): 99 | s = self._getstate() 100 | height = self.font.height() 101 | s.text_row += height 102 | s.text_col = 0 103 | margin = self.screenheight - (s.text_row + height) 104 | y = self.screenheight + margin 105 | if margin < 0: 106 | if not self.row_clip: 107 | self.device.scroll(0, margin) 108 | self.device.fill_rect(0, y, self.screenwidth, abs(margin), self.bgcolor) 109 | s.text_row += margin 110 | 111 | def set_clip(self, row_clip=None, col_clip=None, wrap=None): 112 | if row_clip is not None: 113 | self.row_clip = row_clip 114 | if col_clip is not None: 115 | self.col_clip = col_clip 116 | if wrap is not None: 117 | self.wrap = wrap 118 | return self.row_clip, self.col_clip, self.wrap 119 | 120 | @property 121 | def height(self): # Property for consistency with device 122 | return self.font.height() 123 | 124 | def printstring(self, string, invert=False): 125 | # word wrapping. Assumes words separated by single space. 126 | q = string.split('\n') 127 | last = len(q) - 1 128 | for n, s in enumerate(q): 129 | if s: 130 | self._printline(s, invert) 131 | if n != last: 132 | self._printchar('\n') 133 | 134 | def _printline(self, string, invert): 135 | rstr = None 136 | if self.wrap and self.stringlen(string, True): # Length > self.screenwidth 137 | pos = 0 138 | lstr = string[:] 139 | while self.stringlen(lstr, True): # Length > self.screenwidth 140 | pos = lstr.rfind(' ') 141 | lstr = lstr[:pos].rstrip() 142 | if pos > 0: 143 | rstr = string[pos + 1:] 144 | string = lstr 145 | 146 | for char in string: 147 | self._printchar(char, invert) 148 | if rstr is not None: 149 | self._printchar('\n') 150 | self._printline(rstr, invert) # Recurse 151 | 152 | def stringlen(self, string, oh=False): 153 | if not len(string): 154 | return 0 155 | sc = self._getstate().text_col # Start column 156 | wd = self.screenwidth 157 | l = 0 158 | for char in string[:-1]: 159 | _, _, char_width = self.font.get_ch(char) 160 | l += char_width 161 | if oh and l + sc > wd: 162 | return True # All done. Save time. 163 | char = string[-1] 164 | _, _, char_width = self.font.get_ch(char) 165 | if oh and l + sc + char_width > wd: 166 | l += self._truelen(char) # Last char might have blank cols on RHS 167 | else: 168 | l += char_width # Public method. Return same value as old code. 169 | return l + sc > wd if oh else l 170 | 171 | # Return the printable width of a glyph less any blank columns on RHS 172 | def _truelen(self, char): 173 | glyph, ht, wd = self.font.get_ch(char) 174 | div, mod = divmod(wd, 8) 175 | gbytes = div + 1 if mod else div # No. of bytes per row of glyph 176 | mc = 0 # Max non-blank column 177 | data = glyph[(wd - 1) // 8] # Last byte of row 0 178 | for row in range(ht): # Glyph row 179 | for col in range(wd -1, -1, -1): # Glyph column 180 | gbyte, gbit = divmod(col, 8) 181 | if gbit == 0: # Next glyph byte 182 | data = glyph[row * gbytes + gbyte] 183 | if col <= mc: 184 | break 185 | if data & (1 << (7 - gbit)): # Pixel is lit (1) 186 | mc = col # Eventually gives rightmost lit pixel 187 | break 188 | if mc + 1 == wd: 189 | break # All done: no trailing space 190 | # print('Truelen', char, wd, mc + 1) # TEST 191 | return mc + 1 192 | 193 | def _get_char(self, char, recurse): 194 | if not recurse: # Handle tabs 195 | if char == '\n': 196 | self.cpos = 0 197 | elif char == '\t': 198 | nspaces = self.tab - (self.cpos % self.tab) 199 | if nspaces == 0: 200 | nspaces = self.tab 201 | while nspaces: 202 | nspaces -= 1 203 | self._printchar(' ', recurse=True) 204 | self.glyph = None # All done 205 | return 206 | 207 | self.glyph = None # Assume all done 208 | if char == '\n': 209 | self._newline() 210 | return 211 | glyph, char_height, char_width = self.font.get_ch(char) 212 | s = self._getstate() 213 | np = None # Allow restriction on printable columns 214 | if s.text_row + char_height > self.screenheight: 215 | if self.row_clip: 216 | return 217 | self._newline() 218 | oh = s.text_col + char_width - self.screenwidth # Overhang (+ve) 219 | if oh > 0: 220 | if self.col_clip or self.wrap: 221 | np = char_width - oh # No. of printable columns 222 | if np <= 0: 223 | return 224 | else: 225 | self._newline() 226 | self.glyph = glyph 227 | self.char_height = char_height 228 | self.char_width = char_width 229 | self.clip_width = char_width if np is None else np 230 | 231 | # Method using blitting. Efficient rendering for monochrome displays. 232 | # Tested on SSD1306. Invert is for black-on-white rendering. 233 | def _printchar(self, char, invert=False, recurse=False): 234 | s = self._getstate() 235 | self._get_char(char, recurse) 236 | if self.glyph is None: 237 | return # All done 238 | buf = bytearray(self.glyph) 239 | if invert: 240 | for i, v in enumerate(buf): 241 | buf[i] = 0xFF & ~ v 242 | fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map) 243 | self.device.blit(fbc, s.text_col, s.text_row) 244 | s.text_col += self.char_width 245 | self.cpos += 1 246 | 247 | def tabsize(self, value=None): 248 | if value is not None: 249 | self.tab = value 250 | return self.tab 251 | 252 | def setcolor(self, *_): 253 | return self.fgcolor, self.bgcolor 254 | 255 | # Writer for colour displays. 256 | class CWriter(Writer): 257 | 258 | 259 | def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True): 260 | if not hasattr(device, 'palette'): 261 | raise OSError('Incompatible device driver.') 262 | if implementation[1] < (1, 17, 0): 263 | raise OSError('Firmware must be >= 1.17.') 264 | 265 | super().__init__(device, font, verbose) 266 | if bgcolor is not None: # Assume monochrome. 267 | self.bgcolor = bgcolor 268 | if fgcolor is not None: 269 | self.fgcolor = fgcolor 270 | self.def_bgcolor = self.bgcolor 271 | self.def_fgcolor = self.fgcolor 272 | 273 | def _printchar(self, char, invert=False, recurse=False): 274 | s = self._getstate() 275 | self._get_char(char, recurse) 276 | if self.glyph is None: 277 | return # All done 278 | buf = bytearray_at(addressof(self.glyph), len(self.glyph)) 279 | fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map) 280 | palette = self.device.palette 281 | palette.bg(self.fgcolor if invert else self.bgcolor) 282 | palette.fg(self.bgcolor if invert else self.fgcolor) 283 | self.device.blit(fbc, s.text_col, s.text_row, -1, palette) 284 | s.text_col += self.char_width 285 | self.cpos += 1 286 | 287 | def setcolor(self, fgcolor=None, bgcolor=None): 288 | if fgcolor is None and bgcolor is None: 289 | self.fgcolor = self.def_fgcolor 290 | self.bgcolor = self.def_bgcolor 291 | else: 292 | if fgcolor is not None: 293 | self.fgcolor = fgcolor 294 | if bgcolor is not None: 295 | self.bgcolor = bgcolor 296 | return self.fgcolor, self.bgcolor 297 | -------------------------------------------------------------------------------- /gui/demos/aclock.py: -------------------------------------------------------------------------------- 1 | # aclock.py Test/demo program for nanogui 2 | # Orinally for ssd1351-based OLED displays but runs on most displays 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 | 6 | # Released under the MIT License (MIT). See LICENSE. 7 | # Copyright (c) 2018-2020 Peter Hinch 8 | 9 | # Initialise hardware and framebuf before importing modules. 10 | from color_setup import ssd # Create a display instance 11 | from gui.core.nanogui import refresh # Color LUT is updated now. 12 | from gui.widgets.label import Label 13 | from gui.widgets.dial import Dial, Pointer 14 | refresh(ssd, True) # Initialise and clear display. 15 | 16 | # Now import other modules 17 | import cmath 18 | import utime 19 | from gui.core.writer import CWriter 20 | 21 | # Font for CWriter 22 | import gui.fonts.arial10 as arial10 23 | from gui.core.colors import * 24 | 25 | def aclock(): 26 | uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi 27 | pi = cmath.pi 28 | days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 29 | 'Sunday') 30 | months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 31 | 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') 32 | # Instantiate CWriter 33 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 34 | wri = CWriter(ssd, arial10, GREEN, BLACK) # Report on fast mode. Or use verbose=False 35 | wri.set_clip(True, True, False) 36 | 37 | # Instantiate displayable objects 38 | dial = Dial(wri, 2, 2, height = 75, ticks = 12, bdcolor=None, label=120, pip=False) # Border in fg color 39 | lbltim = Label(wri, 5, 85, 35) 40 | hrs = Pointer(dial) 41 | mins = Pointer(dial) 42 | secs = Pointer(dial) 43 | 44 | hstart = 0 + 0.7j # Pointer lengths and position at top 45 | mstart = 0 + 0.92j 46 | sstart = 0 + 0.92j 47 | while True: 48 | t = utime.localtime() 49 | hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360), YELLOW) 50 | mins.value(mstart * uv(-t[4] * pi/30), YELLOW) 51 | secs.value(sstart * uv(-t[5] * pi/30), RED) 52 | lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) 53 | dial.text('{} {} {} {}'.format(days[t[6]], t[2], months[t[1] - 1], t[0])) 54 | refresh(ssd) 55 | utime.sleep(1) 56 | 57 | aclock() 58 | -------------------------------------------------------------------------------- /gui/demos/aclock_large.py: -------------------------------------------------------------------------------- 1 | # aclock_large.py Test/demo program for displays of 240x240 pixels or larger 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2021 Peter Hinch 5 | 6 | # Initialise hardware and framebuf before importing modules. 7 | from color_setup import ssd # Create a display instance 8 | from gui.core.nanogui import refresh 9 | refresh(ssd, True) # Initialise and clear display. 10 | 11 | # Now import other modules 12 | from gui.widgets.label import Label 13 | from gui.widgets.dial import Dial, Pointer 14 | import cmath 15 | import utime 16 | from gui.core.writer import CWriter 17 | 18 | # Font for CWriter 19 | import gui.fonts.freesans20 as font 20 | from gui.core.colors import * 21 | 22 | def aclock(): 23 | uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi 24 | pi = cmath.pi 25 | days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 26 | 'Sunday') 27 | months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 28 | 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') 29 | # Instantiate CWriter 30 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 31 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 32 | wri.set_clip(True, True, False) 33 | 34 | # Instantiate displayable objects 35 | dial = Dial(wri, 2, 2, height = 150, ticks = 12, bdcolor=None, label=240, pip=False) # Border in fg color 36 | lbltim = Label(wri, 200, 2, 35) 37 | hrs = Pointer(dial) 38 | mins = Pointer(dial) 39 | secs = Pointer(dial) 40 | 41 | hstart = 0 + 0.7j # Pointer lengths and position at top 42 | mstart = 0 + 0.92j 43 | sstart = 0 + 0.92j 44 | while True: 45 | t = utime.localtime() 46 | hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360), YELLOW) 47 | mins.value(mstart * uv(-t[4] * pi/30), YELLOW) 48 | secs.value(sstart * uv(-t[5] * pi/30), RED) 49 | lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) 50 | dial.text('{} {} {} {}'.format(days[t[6]], t[2], months[t[1] - 1], t[0])) 51 | refresh(ssd) 52 | utime.sleep(1) 53 | 54 | aclock() 55 | -------------------------------------------------------------------------------- /gui/demos/aclock_ttgo.py: -------------------------------------------------------------------------------- 1 | # aclock.py Test/demo program for nanogui 2 | # Orinally for ssd1351-based OLED displays but runs on most displays 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 | 6 | # Released under the MIT License (MIT). See LICENSE. 7 | # Copyright (c) 2021 Peter Hinch 8 | 9 | # UK daylight saving code adapted from 10 | # https://forum.micropython.org/viewtopic.php?f=2&t=4034 11 | # Winter UTC Summer (BST) is UTC+1H 12 | # Changes happen last Sundays of March (BST) and October (UTC) at 01:00 UTC 13 | # Ref. formulas : http://www.webexhibits.org/daylightsaving/i.html 14 | # Since 1996, valid through 2099 15 | 16 | 17 | # Initialise hardware and framebuf before importing modules. 18 | from color_setup import ssd # Create a display instance 19 | from gui.core.nanogui import refresh 20 | from gui.widgets.label import Label 21 | from gui.widgets.dial import Dial, Pointer 22 | refresh(ssd, True) # Initialise and clear display. 23 | 24 | # Now import other modules 25 | import cmath 26 | import time 27 | from gui.core.writer import CWriter 28 | from machine import RTC 29 | import uasyncio as asyncio 30 | import ntptime 31 | import do_connect # WiFi connction script 32 | 33 | # Font for CWriter 34 | import gc 35 | gc.collect() 36 | import gui.fonts.freesans20 as font 37 | from gui.core.colors import * 38 | 39 | bst = False 40 | def gbtime(now): 41 | global bst 42 | bst = False 43 | year = time.localtime(now)[0] # get current year 44 | # Time of March change to BST 45 | t_march = time.mktime((year, 3, (31 - (int(5*year/4 + 4)) % 7), 1, 0, 0, 0, 0, 0)) 46 | # Time of October change to UTC 47 | t_october = time.mktime((year, 10, (31 - (int(5*year/4 + 1)) % 7), 1, 0, 0, 0, 0, 0)) 48 | if now < t_march: # we are before last sunday of march 49 | gbt = time.localtime(now) # UTC 50 | elif now < t_october: # we are before last sunday of october 51 | gbt = time.localtime(now + 3600) # BST: UTC+1H 52 | bst = True 53 | else: # we are after last sunday of october 54 | gbt = time.localtime(now) # UTC 55 | return(gbt) 56 | 57 | async def set_rtc(): 58 | rtc = RTC() 59 | while True: 60 | t = -1 61 | while t < 0: 62 | try: 63 | t = ntptime.time() 64 | except OSError: 65 | print('ntp timeout') 66 | await asyncio.sleep(5) 67 | 68 | s = gbtime(t) # Convert UTC to local (GB) time 69 | t0 = time.time() 70 | rtc.datetime(s[0:3] + (0,) + s[3:6] + (0,)) 71 | print('RTC was set, delta =', time.time() - t0) 72 | await asyncio.sleep(600) 73 | 74 | async def ramcheck(): 75 | while True: 76 | gc.collect() 77 | print('Free RAM:',gc.mem_free()) 78 | await asyncio.sleep(600) 79 | 80 | async def aclock(): 81 | do_connect.do_connect() 82 | asyncio.create_task(set_rtc()) 83 | asyncio.create_task(ramcheck()) 84 | uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi 85 | pi = cmath.pi 86 | days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 87 | 'Sunday') 88 | months = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 89 | 'August', 'September', 'October', 'November', 'December') 90 | # Instantiate CWriter 91 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 92 | wri = CWriter(ssd, font, GREEN, BLACK, verbose=False) 93 | wri.set_clip(True, True, False) 94 | 95 | # Instantiate displayable objects 96 | dial = Dial(wri, 2, 2, height = 130, ticks = 12, bdcolor=None) # Border in fg color 97 | lbltim = Label(wri, 140, 2, 130) 98 | lblday = Label(wri, 170, 2, 130) 99 | lblmonth = Label(wri, 190, 2, 130) 100 | lblyr = Label(wri, 210, 2, 130) 101 | hrs = Pointer(dial) 102 | mins = Pointer(dial) 103 | secs = Pointer(dial) 104 | 105 | hstart = 0 + 0.7j # Pointer lengths and position at top 106 | mstart = 0 + 0.92j 107 | sstart = 0 + 0.92j 108 | t = time.localtime() 109 | while True: 110 | hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360), YELLOW) 111 | mins.value(mstart * uv(-t[4] * pi/30 - t[5] * pi/1800), YELLOW) 112 | secs.value(sstart * uv(-t[5] * pi/30), RED) 113 | lbltim.value('{:02d}.{:02d}.{:02d} {}'.format(t[3], t[4], t[5], 'BST' if bst else 'UTC')) 114 | lblday.value('{}'.format(days[t[6]])) 115 | lblmonth.value('{} {}'.format(t[2], months[t[1] - 1])) 116 | lblyr.value('{}'.format(t[0])) 117 | refresh(ssd) 118 | st = t 119 | while st == t: 120 | await asyncio.sleep_ms(100) 121 | t = time.localtime() 122 | 123 | 124 | try: 125 | asyncio.run(aclock()) 126 | finally: 127 | _ = asyncio.new_event_loop() 128 | 129 | -------------------------------------------------------------------------------- /gui/demos/alevel.py: -------------------------------------------------------------------------------- 1 | # alevel.py Test/demo "spirit level" program. 2 | # Requires Pyboard for accelerometer. 3 | # Tested with Adafruit ssd1351 OLED display. 4 | 5 | # Released under the MIT License (MIT). See LICENSE. 6 | # Copyright (c) 2018-2020 Peter Hinch 7 | 8 | # Initialise hardware and framebuf before importing modules. 9 | from color_setup import ssd # Create a display instance 10 | 11 | from gui.core.nanogui import refresh 12 | from gui.widgets.dial import Dial, Pointer 13 | refresh(ssd, True) # Initialise and clear display. 14 | 15 | # Now import other modules 16 | 17 | import utime 18 | import pyb 19 | from gui.core.writer import CWriter 20 | import gui.fonts.arial10 as arial10 21 | from gui.core.colors import * 22 | 23 | def main(): 24 | print('alevel test is running.') 25 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 26 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 27 | wri.set_clip(True, True, False) 28 | acc = pyb.Accel() 29 | dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, 30 | label='Tilt Pyboard', style = Dial.COMPASS, pip=YELLOW) # Border in fg color 31 | ptr = Pointer(dial) 32 | scale = 1/40 33 | while True: 34 | x, y, z = acc.filtered_xyz() 35 | # Depending on relative alignment of display and Pyboard this line may 36 | # need changing: swap x and y or change signs so arrow points in direction 37 | # board is tilted. 38 | ptr.value(-y*scale + 1j*x*scale, YELLOW) 39 | refresh(ssd) 40 | utime.sleep_ms(200) 41 | 42 | main() 43 | -------------------------------------------------------------------------------- /gui/demos/asnano.py: -------------------------------------------------------------------------------- 1 | # asnano.py Test/demo program for use of nanogui with uasyncio 2 | # Uses Adafruit ssd1351-based OLED displays (change height to suit) 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 | 6 | # Copyright (c) 2020 Peter Hinch 7 | # Released under the MIT License (MIT) - see LICENSE file 8 | 9 | # Initialise hardware and framebuf before importing modules. 10 | from color_setup import ssd # Create a display instance 11 | 12 | import uasyncio as asyncio 13 | import pyb 14 | import uos 15 | from gui.core.writer import CWriter 16 | from gui.core.nanogui import refresh 17 | from gui.widgets.led import LED 18 | from gui.widgets.meter import Meter 19 | 20 | refresh(ssd) 21 | 22 | # Fonts 23 | import gui.fonts.arial10 as arial10 24 | import gui.fonts.freesans20 as freesans20 25 | 26 | from gui.core.colors import * 27 | 28 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 29 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 30 | wri.set_clip(True, True, False) 31 | 32 | color = lambda v : RED if v > 0.7 else YELLOW if v > 0.5 else GREEN 33 | txt = lambda v : 'ovr' if v > 0.7 else 'high' if v > 0.5 else 'ok' 34 | 35 | async def meter(n, x, text, t): 36 | print('Meter {} test.'.format(n)) 37 | m = Meter(wri, 5, x, divisions = 4, ptcolor=YELLOW, 38 | label=text, style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 39 | l = LED(wri, ssd.height - 16 - wri.height, x, bdcolor=YELLOW, label ='over') 40 | while True: 41 | v = int.from_bytes(uos.urandom(3),'little')/16777216 42 | m.value(v, color(v)) 43 | l.color(color(v)) 44 | l.text(txt(v), fgcolor=color(v)) 45 | refresh(ssd) 46 | await asyncio.sleep_ms(t) 47 | 48 | async def flash(n, t): 49 | led = pyb.LED(n) 50 | while True: 51 | led.toggle() 52 | await asyncio.sleep_ms(t) 53 | 54 | async def killer(tasks): 55 | sw = pyb.Switch() 56 | while not sw(): 57 | await asyncio.sleep_ms(100) 58 | for task in tasks: 59 | task.cancel() 60 | 61 | async def main(): 62 | tasks = [] 63 | tasks.append(asyncio.create_task(meter(1, 2, 'left', 700))) 64 | tasks.append(asyncio.create_task(meter(2, 50, 'right', 1000))) 65 | tasks.append(asyncio.create_task(meter(3, 98, 'bass', 1500))) 66 | tasks.append(asyncio.create_task(flash(1, 200))) 67 | tasks.append(asyncio.create_task(flash(2, 233))) 68 | await killer(tasks) 69 | 70 | print('Press Pyboard usr button to stop test.') 71 | try: 72 | asyncio.run(main()) 73 | finally: # Reset uasyncio case of KeyboardInterrupt 74 | asyncio.new_event_loop() 75 | -------------------------------------------------------------------------------- /gui/demos/asnano_sync.py: -------------------------------------------------------------------------------- 1 | # asnano_sync.py Test/demo program for use of nanogui with uasyncio 2 | # Requires Pyboard for switch and LEDs. 3 | # Tested with Adafruit ssd1351 OLED display. 4 | 5 | # Copyright (c) 2020 Peter Hinch 6 | # Released under the MIT License (MIT) - see LICENSE file 7 | 8 | # Initialise hardware and framebuf before importing modules 9 | from color_setup import ssd # Create a display instance 10 | 11 | import uasyncio as asyncio 12 | import pyb 13 | import uos 14 | from gui.core.writer import CWriter 15 | from gui.core.nanogui import refresh 16 | from gui.widgets.led import LED 17 | from gui.widgets.meter import Meter 18 | 19 | refresh(ssd, True) 20 | 21 | # Fonts 22 | import gui.fonts.arial10 as arial10 23 | import gui.fonts.freesans20 as freesans20 24 | 25 | from gui.core.colors import * 26 | 27 | color = lambda v : RED if v > 0.7 else YELLOW if v > 0.5 else GREEN 28 | txt = lambda v : 'ovr' if v > 0.7 else 'high' if v > 0.5 else 'ok' 29 | 30 | class MyMeter(Meter): 31 | def __init__(self, x, text): 32 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 33 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 34 | wri.set_clip(True, True, False) 35 | super().__init__(wri, 5, x, divisions = 4, ptcolor=YELLOW, label=text, 36 | style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 37 | self.led = LED(wri, ssd.height - 16 - wri.height, x, bdcolor=YELLOW, label ='over') 38 | self.task = asyncio.create_task(self._run()) 39 | 40 | async def _run(self): 41 | while True: 42 | v = int.from_bytes(uos.urandom(3),'little')/16777216 43 | self.value(v, color(v)) 44 | self.led.color(color(v)) 45 | self.led.text(txt(v), fgcolor=color(v)) 46 | # Slow asynchronous data acquisition might occur here. Note 47 | # that meters update themselves asynchronously (in a real 48 | # application as data becomes available). 49 | await asyncio.sleep(v) # Demo variable times 50 | 51 | async def flash(n, t): 52 | led = pyb.LED(n) 53 | try: 54 | while True: 55 | led.toggle() 56 | await asyncio.sleep_ms(t) 57 | except asyncio.CancelledError: 58 | led.off() # Demo tidying up on cancellation. 59 | 60 | class Killer: 61 | def __init__(self): 62 | self.sw = pyb.Switch() 63 | 64 | async def wait(self, t): 65 | while t >= 0: 66 | if self.sw(): 67 | return True 68 | await asyncio.sleep_ms(50) 69 | t -= 50 70 | return False 71 | 72 | # The main task instantiates other tasks then does the display update process. 73 | async def main(): 74 | print('Press Pyboard usr button to stop test.') 75 | # Asynchronously flash Pyboard LED's. Because we can. 76 | leds = [asyncio.create_task(flash(1, 200)), asyncio.create_task(flash(2, 233))] 77 | # Task for each meter and GUI LED 78 | mtasks =[MyMeter(2, 'left').task, MyMeter(50, 'right').task, MyMeter(98, 'bass').task] 79 | k = Killer() 80 | while True: 81 | if await k.wait(800): # Switch was pressed 82 | break 83 | refresh(ssd) 84 | for task in mtasks + leds: 85 | task.cancel() 86 | await asyncio.sleep_ms(0) 87 | ssd.fill(0) # Clear display at end. 88 | refresh(ssd) 89 | 90 | def test(): 91 | try: 92 | asyncio.run(main()) 93 | finally: # Reset uasyncio case of KeyboardInterrupt 94 | asyncio.new_event_loop() 95 | print('asnano_sync.test() to re-run test.') 96 | 97 | test() 98 | -------------------------------------------------------------------------------- /gui/demos/clock_batt.py: -------------------------------------------------------------------------------- 1 | # clock_batt.py Battery powered clock demo for Pyboard/Adafruit sharp 2.7" display 2 | 3 | # Copyright (c) 2020 Peter Hinch 4 | # Released under the MIT license. See LICENSE 5 | 6 | # HARDWARE 7 | # This assumes a Pybaord D in WBUS-DIP28 and powered by a LiPo cell 8 | # WIRING 9 | # Pyb SSD 10 | # Vin Vin Pyboard D: Vin on DIP28 is an output when powered by LiPo 11 | # Gnd Gnd 12 | # Y8 DI 13 | # Y6 CLK 14 | # Y5 CS 15 | 16 | 17 | # Demo of initialisation procedure designed to minimise risk of memory fail 18 | # when instantiating the frame buffer. The aim is to do this as early as 19 | # possible before importing other modules. 20 | 21 | from color_setup import ssd # Create a display instance 22 | 23 | import upower 24 | from gui.core.nanogui import refresh 25 | from gui.widgets.label import Label 26 | from gui.widgets.dial import Dial, Pointer 27 | 28 | import pyb 29 | import cmath 30 | 31 | from gui.core.writer import Writer 32 | 33 | # Fonts for Writer 34 | import gui.fonts.freesans20 as font_small 35 | import gui.fonts.arial35 as font_large 36 | 37 | refresh(ssd) # Initialise display. 38 | 39 | def aclock(): 40 | rtc = pyb.RTC() 41 | uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi 42 | pi = cmath.pi 43 | days = ('Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun') 44 | months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 45 | 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') 46 | # Instantiate Writer 47 | Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it 48 | wri = Writer(ssd, font_small, verbose=False) 49 | wri.set_clip(True, True, False) 50 | wri_tim = Writer(ssd, font_large, verbose=False) 51 | wri_tim.set_clip(True, True, False) 52 | 53 | # Instantiate displayable objects 54 | dial = Dial(wri, 2, 2, height = 215, ticks = 12, bdcolor=None, pip=True) 55 | lbltim = Label(wri_tim, 50, 230, '00.00.00') 56 | lbldat = Label(wri, 100, 230, 100) 57 | hrs = Pointer(dial) 58 | mins = Pointer(dial) 59 | 60 | hstart = 0 + 0.7j # Pointer lengths and position at top 61 | mstart = 0 + 0.92j 62 | while True: 63 | t = rtc.datetime() # (year, month, day, weekday, hours, minutes, seconds, subseconds) 64 | hang = -t[4]*pi/6 - t[5]*pi/360 # Angles of hands in radians 65 | mang = -t[5] * pi/30 66 | if abs(hang - mang) < pi/360: # Avoid visually confusing overlap of hands 67 | hang += pi/30 # by making hr hand lag slightly 68 | hrs.value(hstart * uv(hang)) 69 | mins.value(mstart * uv(mang)) 70 | lbltim.value('{:02d}.{:02d}'.format(t[4], t[5])) 71 | lbldat.value('{} {} {} {}'.format(days[t[3] - 1], t[2], months[t[1] - 1], t[0])) 72 | refresh(ssd) 73 | # Power saving: only refresh every 30s 74 | for _ in range(30): 75 | upower.lpdelay(1000) 76 | ssd.update() # Toggle VCOM 77 | 78 | aclock() 79 | -------------------------------------------------------------------------------- /gui/demos/clocktest.py: -------------------------------------------------------------------------------- 1 | # clocktest.py Test/demo program for Adafruit sharp 2.7" display 2 | 3 | # Copyright (c) 2020 Peter Hinch 4 | # Released under the MIT license. See LICENSE 5 | 6 | # WIRING 7 | # Pyb SSD 8 | # Vin Vin Pyboard: Vin is an output when powered by USB 9 | # Gnd Gnd 10 | # Y8 DI 11 | # Y6 CLK 12 | # Y5 CS 13 | 14 | 15 | # Demo of initialisation procedure designed to minimise risk of memory fail 16 | # when instantiating the frame buffer. The aim is to do this as early as 17 | # possible before importing other modules. 18 | 19 | from color_setup import ssd # Create a display instance 20 | 21 | from gui.core.nanogui import refresh 22 | from gui.widgets.label import Label 23 | from gui.widgets.dial import Dial, Pointer 24 | 25 | import cmath 26 | import utime 27 | 28 | from gui.core.writer import Writer 29 | 30 | # Fonts for Writer 31 | import gui.fonts.freesans20 as font_small 32 | import gui.fonts.arial35 as font_large 33 | 34 | refresh(ssd) # Initialise display. 35 | 36 | def aclock(): 37 | uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi 38 | pi = cmath.pi 39 | days = ('Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun') 40 | months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', 41 | 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') 42 | # Instantiate Writer 43 | Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it 44 | wri = Writer(ssd, font_small, verbose=False) 45 | wri.set_clip(True, True, False) 46 | wri_tim = Writer(ssd, font_large, verbose=False) 47 | wri_tim.set_clip(True, True, False) 48 | 49 | # Instantiate displayable objects 50 | dial = Dial(wri, 2, 2, height = 215, ticks = 12, bdcolor=None, pip=True) 51 | lbltim = Label(wri_tim, 50, 230, '00.00.00') 52 | lbldat = Label(wri, 100, 230, 100) 53 | hrs = Pointer(dial) 54 | mins = Pointer(dial) 55 | secs = Pointer(dial) 56 | 57 | hstart = 0 + 0.7j # Pointer lengths and position at top 58 | mstart = 0 + 0.92j 59 | sstart = 0 + 0.92j 60 | while True: 61 | t = utime.localtime() 62 | hang = -t[3]*pi/6 - t[4]*pi/360 # Angles of hour and minute hands 63 | mang = -t[4] * pi/30 64 | sang = -t[5] * pi/30 65 | if abs(hang - mang) < pi/360: # Avoid overlap of hr and min hands 66 | hang += pi/30 # which is visually confusing. Add slight lag to hrs 67 | hrs.value(hstart * uv(hang)) 68 | mins.value(mstart * uv(mang)) 69 | secs.value(sstart * uv(sang)) 70 | lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) 71 | lbldat.value('{} {} {} {}'.format(days[t[6]], t[2], months[t[1] - 1], t[0])) 72 | refresh(ssd) 73 | utime.sleep(1) 74 | 75 | aclock() 76 | -------------------------------------------------------------------------------- /gui/demos/color15.py: -------------------------------------------------------------------------------- 1 | # color15.py Test/demo program for larger displays. Cross-platform. 2 | # Tested on Adafruit ssd1351-based OLED displays: 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 | # Released under the MIT License (MIT). See LICENSE. 8 | # Copyright (c) 2018-2020 Peter Hinch 9 | 10 | # Initialise hardware and framebuf before importing modules. 11 | from color_setup import ssd # Create a display instance 12 | 13 | import cmath 14 | import utime 15 | import uos 16 | from gui.core.writer import Writer, CWriter 17 | from gui.core.nanogui import refresh 18 | from gui.widgets.led import LED 19 | from gui.widgets.meter import Meter 20 | from gui.widgets.label import Label 21 | from gui.widgets.dial import Dial, Pointer 22 | 23 | # Fonts 24 | import gui.fonts.arial10 as arial10 25 | import gui.fonts.freesans20 as freesans20 26 | 27 | from gui.core.colors import * 28 | 29 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 30 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 31 | wri.set_clip(True, True, False) 32 | 33 | def meter(): 34 | print('Meter test.') 35 | refresh(ssd, True) # Clear any prior image 36 | color = lambda v : RED if v > 0.7 else YELLOW if v > 0.5 else GREEN 37 | txt = lambda v : 'ovr' if v > 0.7 else 'high' if v > 0.5 else 'ok' 38 | m0 = Meter(wri, 5, 2, divisions = 4, ptcolor=YELLOW, 39 | label='left', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 40 | l0 = LED(wri, ssd.height - 16 - wri.height, 2, bdcolor=YELLOW, label ='over') 41 | m1 = Meter(wri, 5, 50, divisions = 4, ptcolor=YELLOW, 42 | label='right', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 43 | l1 = LED(wri, ssd.height - 16 - wri.height, 50, bdcolor=YELLOW, label ='over') 44 | m2 = Meter(wri, 5, 98, divisions = 4, ptcolor=YELLOW, 45 | label='bass', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 46 | l2 = LED(wri, ssd.height - 16 - wri.height, 98, bdcolor=YELLOW, label ='over') 47 | steps = 10 48 | for n in range(steps): 49 | v = int.from_bytes(uos.urandom(3),'little')/16777216 50 | m0.value(v, color(v)) 51 | l0.color(color(v)) 52 | l0.text(txt(v), fgcolor=color(v)) 53 | v = n/steps 54 | m1.value(v, color(v)) 55 | l1.color(color(v)) 56 | l1.text(txt(v), fgcolor=color(v)) 57 | v = 1 - n/steps 58 | m2.value(v, color(v)) 59 | l2.color(color(v)) 60 | l2.text(txt(v), fgcolor=color(v)) 61 | refresh(ssd) 62 | utime.sleep(1) 63 | 64 | 65 | def multi_fields(t): 66 | print('Dynamic labels.') 67 | refresh(ssd, True) # Clear any prior image 68 | nfields = [] 69 | dy = wri.height + 6 70 | y = 2 71 | col = 15 72 | width = wri.stringlen('99.99') 73 | for txt in ('X:', 'Y:', 'Z:'): 74 | Label(wri, y, 0, txt) # Use wri default colors 75 | nfields.append(Label(wri, y, col, width, bdcolor=None)) # Specify a border, color TBD 76 | y += dy 77 | 78 | end = utime.ticks_add(utime.ticks_ms(), t * 1000) 79 | while utime.ticks_diff(end, utime.ticks_ms()) > 0: 80 | for field in nfields: 81 | value = int.from_bytes(uos.urandom(3),'little')/167772 82 | overrange = None if value < 70 else YELLOW if value < 90 else RED 83 | field.value('{:5.2f}'.format(value), fgcolor = overrange, bdcolor = overrange) 84 | refresh(ssd) 85 | utime.sleep(1) 86 | Label(wri, 0, 64, ' OK ', True, fgcolor = RED) 87 | refresh(ssd) 88 | utime.sleep(1) 89 | 90 | def vari_fields(): 91 | print('Variable label styles.') 92 | VIOLET = create_color(12, 255, 0, 255) # Custom color 93 | refresh(ssd, True) # Clear any prior image 94 | wri_large = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False) 95 | wri_large.set_clip(True, True, False) 96 | Label(wri_large, 0, 0, 'Text') 97 | Label(wri_large, 20, 0, 'Border') 98 | width = wri_large.stringlen('Yellow') 99 | lbl_text = Label(wri_large, 0, 65, width) 100 | lbl_bord = Label(wri_large, 20, 65, width) 101 | lbl_text.value('Red') 102 | lbl_bord.value('Red') 103 | lbl_var = Label(wri_large, 50, 2, '25.46', fgcolor=RED, bdcolor=RED) 104 | refresh(ssd) 105 | utime.sleep(2) 106 | lbl_text.value('Red') 107 | lbl_bord.value('Yellow') 108 | lbl_var.value(bdcolor=YELLOW) 109 | refresh(ssd) 110 | utime.sleep(2) 111 | lbl_text.value('Red') 112 | lbl_bord.value('None') 113 | lbl_var.value(bdcolor=False) 114 | refresh(ssd) 115 | utime.sleep(2) 116 | lbl_text.value('Yellow') 117 | lbl_bord.value('None') 118 | lbl_var.value(fgcolor=YELLOW) 119 | refresh(ssd) 120 | utime.sleep(2) 121 | lbl_text.value('Violet') 122 | lbl_bord.value('None') 123 | lbl_var.value(fgcolor=VIOLET) 124 | refresh(ssd) 125 | utime.sleep(2) 126 | lbl_text.value('Blue') 127 | lbl_bord.value('Green') 128 | lbl_var.value('18.99', fgcolor=BLUE, bdcolor=GREEN) 129 | Label(wri, ssd.height - wri.height - 2, 0, 'Done', fgcolor=RED) 130 | refresh(ssd) 131 | 132 | def clock(x): 133 | print('Clock test.') 134 | refresh(ssd, True) # Clear any prior image 135 | lbl = Label(wri, 5, 85, 'Clock') 136 | dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, label=50) # Border in fg color 137 | hrs = Pointer(dial) 138 | mins = Pointer(dial) 139 | hrs.value(0 + 0.7j, RED) 140 | mins.value(0 + 0.9j, YELLOW) 141 | dm = cmath.rect(1, -cmath.pi/30) # Rotate by 1 minute (CW) 142 | dh = cmath.rect(1, -cmath.pi/1800) # Rotate hours by 1 minute 143 | for n in range(x): 144 | refresh(ssd) 145 | utime.sleep_ms(200) 146 | mins.value(mins.value() * dm, YELLOW) 147 | hrs.value(hrs.value() * dh, RED) 148 | dial.text('ticks: {}'.format(n)) 149 | lbl.value('Done') 150 | 151 | def compass(x): 152 | print('Compass test.') 153 | refresh(ssd, True) # Clear any prior image 154 | dial = Dial(wri, 5, 5, height = 75, bdcolor=None, label=50, style = Dial.COMPASS) 155 | bearing = Pointer(dial) 156 | bearing.value(0 + 1j, RED) 157 | dh = cmath.rect(1, -cmath.pi/30) # Rotate by 6 degrees CW 158 | for n in range(x): 159 | utime.sleep_ms(200) 160 | bearing.value(bearing.value() * dh, RED) 161 | refresh(ssd) 162 | 163 | print('Color display test is running.') 164 | print('Test runs to completion: ~65 secs.') 165 | clock(70) 166 | compass(70) 167 | meter() 168 | multi_fields(t = 10) 169 | vari_fields() 170 | print('Test complete.') 171 | -------------------------------------------------------------------------------- /gui/demos/color96.py: -------------------------------------------------------------------------------- 1 | # color96.py Test/demo program for ssd1331 Adafruit 0.96" OLED display. 2 | # Cross-platfom. 3 | # Works on larger displays, but only occupies the top left region. 4 | # https://www.adafruit.com/product/684 5 | # For wiring details see drivers/ADAFRUIT.md in this repo. 6 | 7 | # Released under the MIT License (MIT). See LICENSE. 8 | # Copyright (c) 2018-2020 Peter Hinch 9 | 10 | # Initialise hardware and framebuf before importing modules. 11 | from color_setup import ssd # Create a display instance 12 | 13 | from gui.core.nanogui import refresh 14 | from gui.widgets.led import LED 15 | from gui.widgets.meter import Meter 16 | from gui.widgets.label import Label 17 | 18 | refresh(ssd, True) 19 | # Fonts 20 | import gui.fonts.arial10 as arial10 21 | 22 | from gui.core.writer import Writer, CWriter 23 | import utime 24 | import uos 25 | from gui.core.colors import * 26 | 27 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 28 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 29 | wri.set_clip(True, True, False) 30 | 31 | def meter(): 32 | print('meter') 33 | refresh(ssd, True) # Clear any prior image 34 | m = Meter(wri, 5, 2, height = 45, divisions = 4, ptcolor=YELLOW, 35 | label='level', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) 36 | l = LED(wri, 5, 40, bdcolor=YELLOW, label ='over') 37 | steps = 10 38 | for _ in range(steps): 39 | v = int.from_bytes(uos.urandom(3),'little')/16777216 40 | m.value(v) 41 | l.color(GREEN if v < 0.5 else RED) 42 | refresh(ssd) 43 | utime.sleep(1) 44 | refresh(ssd) 45 | 46 | 47 | def multi_fields(t): 48 | print('multi_fields') 49 | refresh(ssd, True) # Clear any prior image 50 | nfields = [] 51 | dy = wri.height + 6 52 | y = 2 53 | col = 15 54 | width = wri.stringlen('99.99') 55 | for txt in ('X:', 'Y:', 'Z:'): 56 | Label(wri, y, 0, txt) # Use wri default colors 57 | nfields.append(Label(wri, y, col, width, bdcolor=None)) # Specify a border, color TBD 58 | y += dy 59 | 60 | end = utime.ticks_add(utime.ticks_ms(), t * 1000) 61 | while utime.ticks_diff(end, utime.ticks_ms()) > 0: 62 | for field in nfields: 63 | value = int.from_bytes(uos.urandom(3),'little')/167772 64 | overrange = None if value < 70 else YELLOW if value < 90 else RED 65 | field.value('{:5.2f}'.format(value), fgcolor = overrange, bdcolor = overrange) 66 | refresh(ssd) 67 | utime.sleep(1) 68 | Label(wri, 0, 64, ' OK ', True, fgcolor = RED) 69 | refresh(ssd) 70 | utime.sleep(1) 71 | 72 | def vari_fields(): 73 | print('vari_fields') 74 | refresh(ssd, True) # Clear any prior image 75 | Label(wri, 0, 0, 'Text:') 76 | Label(wri, 20, 0, 'Border:') 77 | width = wri.stringlen('Yellow') 78 | lbl_text = Label(wri, 0, 40, width) 79 | lbl_bord = Label(wri, 20, 40, width) 80 | lbl_text.value('Red') 81 | lbl_bord.value('Red') 82 | lbl_var = Label(wri, 40, 2, '25.46', fgcolor=RED, bdcolor=RED) 83 | refresh(ssd) 84 | utime.sleep(2) 85 | lbl_text.value('Red') 86 | lbl_bord.value('Yellow') 87 | lbl_var.value(bdcolor=YELLOW) 88 | refresh(ssd) 89 | utime.sleep(2) 90 | lbl_text.value('Red') 91 | lbl_bord.value('None') 92 | lbl_var.value(bdcolor=False) 93 | refresh(ssd) 94 | utime.sleep(2) 95 | lbl_text.value('Yellow') 96 | lbl_bord.value('None') 97 | lbl_var.value(fgcolor=YELLOW) 98 | refresh(ssd) 99 | utime.sleep(2) 100 | lbl_text.value('Blue') 101 | lbl_bord.value('Green') 102 | lbl_var.value('18.99', fgcolor=BLUE, bdcolor=GREEN) 103 | refresh(ssd) 104 | 105 | print('Color display test is running.') 106 | print('Test runs to completion.') 107 | meter() 108 | multi_fields(t = 10) 109 | vari_fields() 110 | print('Test complete.') 111 | -------------------------------------------------------------------------------- /gui/demos/epd29_async.py: -------------------------------------------------------------------------------- 1 | # epd29_async.py Demo program for nano_gui on an Adafruit 2.9" flexible ePaper screen 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # color_setup must set landcsape True, asyn True and must not set demo_mode 7 | from cmath import exp, pi 8 | import uasyncio as asyncio 9 | from color_setup import ssd 10 | # On a monochrome display Writer is more efficient than CWriter. 11 | from gui.core.writer import Writer 12 | from gui.core.nanogui import refresh 13 | from gui.widgets.meter import Meter 14 | from gui.widgets.label import Label 15 | from gui.widgets.dial import Dial, Pointer 16 | 17 | # Fonts 18 | import gui.fonts.arial10 as arial10 19 | import gui.fonts.font6 as small 20 | 21 | ssd._asyn = True # HACK to make it config agnostic 22 | # Some ports don't support uos.urandom. 23 | # See https://github.com/peterhinch/micropython-samples/tree/master/random 24 | def xorshift64star(modulo, seed = 0xf9ac6ba4): 25 | x = seed 26 | def func(): 27 | nonlocal x 28 | x ^= x >> 12 29 | x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 30 | x ^= x >> 27 31 | return (x * 0x2545F4914F6CDD1D) % modulo 32 | return func 33 | 34 | async def compass(evt): 35 | wri = Writer(ssd, arial10, verbose=False) 36 | wri.set_clip(False, False, False) 37 | v1 = 0 + 0.9j 38 | v2 = exp(0 - (pi / 6) * 1j) 39 | dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, 40 | label='Direction', style = Dial.COMPASS) 41 | ptr = Pointer(dial) 42 | while True: 43 | ptr.value(v1) 44 | v1 *= v2 45 | await evt.wait() 46 | 47 | 48 | async def multi_fields(evt): 49 | wri = Writer(ssd, small, verbose=False) 50 | wri.set_clip(False, False, False) 51 | 52 | nfields = [] 53 | dy = small.height() + 10 54 | row = 2 55 | col = 100 56 | width = wri.stringlen('99.990') 57 | for txt in ('X:', 'Y:', 'Z:'): 58 | Label(wri, row, col, txt) 59 | nfields.append(Label(wri, row, col, width, bdcolor=None)) # Draw border 60 | row += dy 61 | 62 | random = xorshift64star(2**24 - 1) 63 | while True: 64 | for _ in range(10): 65 | for field in nfields: 66 | value = random() / 167772 67 | field.value('{:5.2f}'.format(value)) 68 | await evt.wait() 69 | 70 | async def meter(evt): 71 | wri = Writer(ssd, arial10, verbose=False) 72 | wri.set_clip(False, False, False) 73 | row = 10 74 | col = 170 75 | args = {'height' : 80, 76 | 'width' : 15, 77 | 'divisions' : 4, 78 | 'style' : Meter.BAR} 79 | m0 = Meter(wri, row, col, legends=('0.0', '0.5', '1.0'), **args) 80 | m1 = Meter(wri, row, col + 40, legends=('-1', '0', '+1'), **args) 81 | m2 = Meter(wri, row, col + 80, legends=('-1', '0', '+1'), **args) 82 | random = xorshift64star(2**24 - 1) 83 | while True: 84 | steps = 10 85 | for n in range(steps + 1): 86 | m0.value(random() / 16777216) 87 | m1.value(n/steps) 88 | m2.value(1 - n/steps) 89 | await evt.wait() 90 | 91 | async def main(): 92 | refresh(ssd, True) # Clear display 93 | await ssd.wait() 94 | print('Ready') 95 | evt = asyncio.Event() 96 | asyncio.create_task(meter(evt)) 97 | asyncio.create_task(multi_fields(evt)) 98 | asyncio.create_task(compass(evt)) 99 | while True: 100 | # Normal procedure before refresh, but 10s sleep should mean it always returns immediately 101 | await ssd.wait() 102 | refresh(ssd) # Launches ._as_show() 103 | await ssd.updated() 104 | # Content has now been shifted out so coros can update 105 | # framebuffer in background 106 | evt.set() 107 | evt.clear() 108 | await asyncio.sleep(10) # Allow for slow refresh 109 | 110 | 111 | tstr = '''Test of asynchronous code updating the EPD. This should 112 | not be run for long periods as the EPD should not be updated more 113 | frequently than every 180s. 114 | ''' 115 | 116 | print(tstr) 117 | 118 | try: 119 | asyncio.run(main()) 120 | except KeyboardInterrupt: 121 | # Defensive code: avoid leaving EPD hardware in an undefined state. 122 | print('Waiting for display to become idle') 123 | ssd.sleep() # Synchronous code. May block for 5s if display is updating. 124 | finally: 125 | _ = asyncio.new_event_loop() 126 | -------------------------------------------------------------------------------- /gui/demos/epd29_lowpower.py: -------------------------------------------------------------------------------- 1 | # epd29_sync.py Demo of synchronous code on 2.9" EPD display 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # color_setup must set landcsape True, asyn False and must not set demo_mode 7 | 8 | from math import pi, sin 9 | import upower 10 | import machine 11 | import pyb 12 | pon = machine.Pin('Y5', machine.Pin.OUT_PP, value=1) # Power on before instantiating display 13 | upower.lpdelay(1000) # Give the valves (tubes) time to warm up :) 14 | from color_setup import ssd # Instantiate 15 | from gui.core.writer import Writer 16 | from gui.core.nanogui import refresh 17 | from gui.core.fplot import CartesianGraph, Curve 18 | from gui.widgets.meter import Meter 19 | from gui.widgets.label import Label 20 | from gui.widgets.dial import Dial, Pointer 21 | 22 | # Fonts 23 | import gui.fonts.arial10 as arial10 24 | import gui.fonts.freesans20 as large 25 | 26 | wri = Writer(ssd, arial10, verbose=False) 27 | wri.set_clip(False, False, False) 28 | 29 | wri_large = Writer(ssd, large, verbose=False) 30 | wri_large.set_clip(False, False, False) 31 | 32 | def graph(): 33 | row, col, ht, wd = 5, 140, 75, 150 34 | def populate(): 35 | x = -0.998 36 | while x < 1.01: 37 | z = 6 * pi * x 38 | y = sin(z) / z 39 | yield x, y 40 | x += 0.05 41 | 42 | g = CartesianGraph(wri, row, col, height = ht, width = wd, bdcolor=False) 43 | curve2 = Curve(g, None, populate()) 44 | Label(wri, row + ht + 5, col - 10, '-2.0 t: secs') 45 | Label(wri, row + ht + 5, col - 8 + int(wd//2), '0.0') 46 | Label(wri, row + ht + 5, col - 10 + wd, '2.0') 47 | 48 | def compass(): 49 | dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, 50 | label='Direction', style = Dial.COMPASS) 51 | ptr = Pointer(dial) 52 | ptr.value(1 + 1j) 53 | 54 | def meter(): 55 | m = Meter(wri, 5, 100, height = 75, divisions = 4, 56 | label='Peak', style=Meter.BAR, legends=('0', '50', '100')) 57 | m.value(0.72) 58 | 59 | def labels(): 60 | row = 100 61 | col = 0 62 | Label(wri_large, row, col, 'Seismograph') 63 | col = 140 64 | Label(wri, row, col + 0, 'Event time') 65 | Label(wri, row, col + 60, '01:35', bdcolor=None) 66 | Label(wri, row, col + 95, 'UTC') 67 | row = 115 68 | Label(wri, row, col + 0, 'Event date') 69 | Label(wri, row, col + 60, '6th Jan 2021', bdcolor=None) 70 | 71 | 72 | # Populate the display - GUI and Writer code goes here 73 | def populate(): 74 | graph() 75 | compass() 76 | meter() 77 | labels() 78 | 79 | # Initialise GUI clearing display. Populate frame buffer. Update diplay and 80 | # leave in power down state ready for phsyical loss of power 81 | def show(): 82 | # Low power version of .wait_until_ready() 83 | def wait_ready(): 84 | while not ssd.ready(): 85 | upower.lpdelay(1000) 86 | 87 | refresh(ssd, True) # Init and clear. busy will go True for ~5s 88 | populate() 89 | wait_ready() # wait for display ready (seconds) 90 | refresh(ssd) 91 | wait_ready() 92 | ssd.sleep() # Put into "off" state 93 | 94 | # Handle initial power up and subsequent wakeup. 95 | rtc = pyb.RTC() 96 | # If we have a backup battery clear down any setting from a previously running program 97 | rtc.wakeup(None) 98 | reason = machine.reset_cause() # Why have we woken? 99 | red = pyb.LED(1) 100 | if reason in (machine.PWRON_RESET, machine.HARD_RESET, machine.SOFT_RESET): 101 | # Code to run when the application is first started 102 | aa = upower.Alarm('a') 103 | aa.timeset(second = 39) 104 | ab = upower.Alarm('b') 105 | ab.timeset(second = 9) 106 | elif reason == machine.DEEPSLEEP_RESET: 107 | # Display on. Pin is pulled down by 2K2 so hi-z turns display off. 108 | red.on() 109 | show() 110 | pon(0) # Physically power down display 111 | red.off() 112 | 113 | pyb.standby() 114 | -------------------------------------------------------------------------------- /gui/demos/epd29_sync.py: -------------------------------------------------------------------------------- 1 | # epd29_sync.py Demo of synchronous code on 2.9" EPD display 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # color_setup must set landcsape True, asyn False and must not set demo_mode 7 | 8 | from math import pi, sin 9 | from color_setup import ssd 10 | from gui.core.writer import Writer 11 | from gui.core.nanogui import refresh 12 | from gui.core.fplot import CartesianGraph, Curve 13 | from gui.widgets.meter import Meter 14 | from gui.widgets.label import Label 15 | from gui.widgets.dial import Dial, Pointer 16 | 17 | # Fonts 18 | import gui.fonts.arial10 as arial10 19 | import gui.fonts.freesans20 as large 20 | 21 | wri = Writer(ssd, arial10, verbose=False) 22 | wri.set_clip(False, False, False) 23 | 24 | wri_large = Writer(ssd, large, verbose=False) 25 | wri_large.set_clip(False, False, False) 26 | 27 | # 296*128 28 | def graph(): 29 | row, col, ht, wd = 5, 140, 75, 150 30 | def populate(): 31 | x = -0.998 32 | while x < 1.01: 33 | z = 6 * pi * x 34 | y = sin(z) / z 35 | yield x, y 36 | x += 0.05 37 | 38 | g = CartesianGraph(wri, row, col, height = ht, width = wd, bdcolor=False) 39 | curve2 = Curve(g, None, populate()) 40 | Label(wri, row + ht + 5, col - 10, '-2.0 t: secs') 41 | Label(wri, row + ht + 5, col - 8 + int(wd//2), '0.0') 42 | Label(wri, row + ht + 5, col - 10 + wd, '2.0') 43 | 44 | def compass(): 45 | dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, 46 | label='Direction', style = Dial.COMPASS) 47 | ptr = Pointer(dial) 48 | ptr.value(1 + 1j) 49 | 50 | def meter(): 51 | m = Meter(wri, 5, 100, height = 75, divisions = 4, 52 | label='Peak', style=Meter.BAR, legends=('0', '50', '100')) 53 | m.value(0.72) 54 | 55 | def labels(): 56 | row = 100 57 | col = 0 58 | Label(wri_large, row, col, 'Seismograph') 59 | col = 140 60 | Label(wri, row, col + 0, 'Event time') 61 | Label(wri, row, col + 60, '01:35', bdcolor=None) 62 | Label(wri, row, col + 95, 'UTC') 63 | row = 115 64 | Label(wri, row, col + 0, 'Event date') 65 | Label(wri, row, col + 60, '6th Jan 2021', bdcolor=None) 66 | 67 | def main(): 68 | refresh(ssd, True) 69 | graph() 70 | compass() 71 | meter() 72 | labels() 73 | ssd.wait_until_ready() 74 | refresh(ssd) 75 | print('Waiting for display update') 76 | ssd.wait_until_ready() 77 | 78 | main() 79 | 80 | -------------------------------------------------------------------------------- /gui/demos/fpt.py: -------------------------------------------------------------------------------- 1 | # fpt.py Test/demo program for framebuf plot. Cross-patform, 2 | # but requires a large enough display. 3 | # Tested on Adafruit ssd1351-based OLED displays: 4 | # Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 5 | # Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 6 | 7 | # Released under the MIT License (MIT). See LICENSE. 8 | # Copyright (c) 2018-2020 Peter Hinch 9 | 10 | # Initialise hardware and framebuf before importing modules. 11 | from color_setup import ssd # Create a display instance 12 | 13 | import cmath 14 | import math 15 | import utime 16 | import uos 17 | from gui.core.writer import Writer, CWriter 18 | from gui.core.fplot import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence 19 | from gui.core.nanogui import refresh 20 | from gui.widgets.label import Label 21 | 22 | refresh(ssd, True) 23 | 24 | # Fonts 25 | import gui.fonts.arial10 as arial10 26 | import gui.fonts.freesans20 as freesans20 27 | 28 | from gui.core.colors import * 29 | 30 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 31 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 32 | wri.set_clip(True, True, False) 33 | 34 | def cart(): 35 | print('Cartesian data test.') 36 | def populate_1(func): 37 | x = -1 38 | while x < 1.01: 39 | yield x, func(x) # x, y 40 | x += 0.1 41 | 42 | def populate_2(): 43 | x = -1 44 | while x < 1.01: 45 | yield x, x**2 # x, y 46 | x += 0.1 47 | 48 | refresh(ssd, True) # Clear any prior image 49 | g = CartesianGraph(wri, 2, 2, yorigin = 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) # Asymmetric y axis 50 | curve1 = Curve(g, YELLOW, populate_1(lambda x : x**3 + x**2 -x,)) # args demo 51 | curve2 = Curve(g, RED, populate_2()) 52 | refresh(ssd) 53 | 54 | def polar(): 55 | print('Polar data test.') 56 | def populate(): 57 | def f(theta): 58 | return cmath.rect(math.sin(3 * theta), theta) # complex 59 | nmax = 150 60 | for n in range(nmax + 1): 61 | yield f(2 * cmath.pi * n / nmax) # complex z 62 | refresh(ssd, True) # Clear any prior image 63 | g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) 64 | curve = PolarCurve(g, YELLOW, populate()) 65 | refresh(ssd) 66 | 67 | def polar_clip(): 68 | print('Test of polar data clipping.') 69 | def populate(rot): 70 | f = lambda theta : cmath.rect(1.15 * math.sin(5 * theta), theta) * rot # complex 71 | nmax = 150 72 | for n in range(nmax + 1): 73 | yield f(2 * cmath.pi * n / nmax) # complex z 74 | refresh(ssd, True) # Clear any prior image 75 | g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) 76 | curve = PolarCurve(g, YELLOW, populate(1)) 77 | curve1 = PolarCurve(g, RED, populate(cmath.rect(1, cmath.pi/5),)) 78 | refresh(ssd) 79 | 80 | def rt_polar(): 81 | print('Simulate realtime polar data acquisition.') 82 | refresh(ssd, True) # Clear any prior image 83 | g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) 84 | curvey = PolarCurve(g, YELLOW) 85 | curver = PolarCurve(g, RED) 86 | for x in range(100): 87 | curvey.point(cmath.rect(x/100, -x * cmath.pi/30)) 88 | curver.point(cmath.rect((100 - x)/100, -x * cmath.pi/30)) 89 | utime.sleep_ms(60) 90 | refresh(ssd) 91 | 92 | def rt_rect(): 93 | print('Simulate realtime data acquisition of discontinuous data.') 94 | refresh(ssd, True) # Clear any prior image 95 | g = CartesianGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) 96 | curve = Curve(g, RED) 97 | x = -1 98 | for _ in range(40): 99 | y = 0.1/x if abs(x) > 0.05 else None # Discontinuity 100 | curve.point(x, y) 101 | utime.sleep_ms(100) 102 | refresh(ssd) 103 | x += 0.05 104 | g.clear() 105 | curve = Curve(g, YELLOW) 106 | x = -1 107 | for _ in range(40): 108 | y = -0.1/x if abs(x) > 0.05 else None # Discontinuity 109 | curve.point(x, y) 110 | utime.sleep_ms(100) 111 | refresh(ssd) 112 | x += 0.05 113 | 114 | 115 | def lem(): 116 | print('Lemniscate of Bernoulli.') 117 | def populate(): 118 | t = -math.pi 119 | while t <= math.pi + 0.1: 120 | x = 0.5*math.sqrt(2)*math.cos(t)/(math.sin(t)**2 + 1) 121 | y = math.sqrt(2)*math.cos(t)*math.sin(t)/(math.sin(t)**2 + 1) 122 | yield x, y 123 | t += 0.1 124 | refresh(ssd, True) # Clear any prior image 125 | Label(wri, 82, 2, 'To infinity and beyond...') 126 | g = CartesianGraph(wri, 2, 2, height = 75, fgcolor=WHITE, gridcolor=LIGHTGREEN) 127 | curve = Curve(g, YELLOW, populate()) 128 | refresh(ssd) 129 | 130 | def liss(): 131 | print('Lissajous figure.') 132 | def populate(): 133 | t = -math.pi 134 | while t <= math.pi: 135 | yield math.sin(t), math.cos(3*t) # x, y 136 | t += 0.1 137 | refresh(ssd, True) # Clear any prior image 138 | g = CartesianGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) 139 | curve = Curve(g, YELLOW, populate()) 140 | refresh(ssd) 141 | 142 | def seq(): 143 | print('Time sequence test - sine and cosine.') 144 | refresh(ssd, True) # Clear any prior image 145 | # y axis at t==now, no border 146 | g = CartesianGraph(wri, 2, 2, xorigin = 10, fgcolor=WHITE, 147 | gridcolor=LIGHTGREEN, bdcolor=False) 148 | tsy = TSequence(g, YELLOW, 50) 149 | tsr = TSequence(g, RED, 50) 150 | for t in range(100): 151 | g.clear() 152 | tsy.add(0.9*math.sin(t/10)) 153 | tsr.add(0.4*math.cos(t/10)) 154 | refresh(ssd) 155 | utime.sleep_ms(100) 156 | 157 | print('Test runs to completion.') 158 | seq() 159 | utime.sleep(1.5) 160 | liss() 161 | utime.sleep(1.5) 162 | rt_rect() 163 | utime.sleep(1.5) 164 | rt_polar() 165 | utime.sleep(1.5) 166 | polar() 167 | utime.sleep(1.5) 168 | cart() 169 | utime.sleep(1.5) 170 | polar_clip() 171 | utime.sleep(1.5) 172 | lem() 173 | -------------------------------------------------------------------------------- /gui/demos/mono_test.py: -------------------------------------------------------------------------------- 1 | # mono_test.py Demo program for nano_gui on an SSD1306 OLED display. 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2021 Peter Hinch 5 | 6 | # https://learn.adafruit.com/monochrome-oled-breakouts/wiring-128x32-spi-oled-display 7 | # https://www.proto-pic.co.uk/monochrome-128x32-oled-graphic-display.html 8 | 9 | # V0.33 16th Jan 2021 Hardware configuration is now defined in color_setup to be 10 | # consistent with other displays. 11 | # V0.32 5th Nov 2020 Replace uos.urandom for minimal ports 12 | 13 | import utime 14 | # import uos 15 | from color_setup import ssd 16 | # On a monochrome display Writer is more efficient than CWriter. 17 | from gui.core.writer import Writer 18 | from gui.core.nanogui import refresh 19 | from gui.widgets.meter import Meter 20 | from gui.widgets.label import Label 21 | 22 | # Fonts 23 | import gui.fonts.arial10 as arial10 24 | import gui.fonts.courier20 as fixed 25 | import gui.fonts.font6 as small 26 | 27 | # Some ports don't support uos.urandom. 28 | # See https://github.com/peterhinch/micropython-samples/tree/master/random 29 | def xorshift64star(modulo, seed = 0xf9ac6ba4): 30 | x = seed 31 | def func(): 32 | nonlocal x 33 | x ^= x >> 12 34 | x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 35 | x ^= x >> 27 36 | return (x * 0x2545F4914F6CDD1D) % modulo 37 | return func 38 | 39 | def fields(): 40 | ssd.fill(0) 41 | refresh(ssd) 42 | Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it 43 | wri = Writer(ssd, fixed, verbose=False) 44 | wri.set_clip(False, False, False) 45 | textfield = Label(wri, 0, 2, wri.stringlen('longer')) 46 | numfield = Label(wri, 25, 2, wri.stringlen('99.99'), bdcolor=None) 47 | countfield = Label(wri, 0, 90, wri.stringlen('1')) 48 | n = 1 49 | random = xorshift64star(65535) 50 | for s in ('short', 'longer', '1', ''): 51 | textfield.value(s) 52 | numfield.value('{:5.2f}'.format(random() /1000)) 53 | countfield.value('{:1d}'.format(n)) 54 | n += 1 55 | refresh(ssd) 56 | utime.sleep(2) 57 | textfield.value('Done', True) 58 | refresh(ssd) 59 | 60 | def multi_fields(): 61 | ssd.fill(0) 62 | refresh(ssd) 63 | Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it 64 | wri = Writer(ssd, small, verbose=False) 65 | wri.set_clip(False, False, False) 66 | 67 | nfields = [] 68 | dy = small.height() + 6 69 | y = 2 70 | col = 15 71 | width = wri.stringlen('99.99') 72 | for txt in ('X:', 'Y:', 'Z:'): 73 | Label(wri, y, 0, txt) 74 | nfields.append(Label(wri, y, col, width, bdcolor=None)) # Draw border 75 | y += dy 76 | 77 | random = xorshift64star(2**24 - 1) 78 | for _ in range(10): 79 | for field in nfields: 80 | value = random() / 167772 81 | field.value('{:5.2f}'.format(value)) 82 | refresh(ssd) 83 | utime.sleep(1) 84 | Label(wri, 0, 64, ' DONE ', True) 85 | refresh(ssd) 86 | 87 | def meter(): 88 | ssd.fill(0) 89 | refresh(ssd) 90 | wri = Writer(ssd, arial10, verbose=False) 91 | m0 = Meter(wri, 5, 2, height = 50, divisions = 4, legends=('0.0', '0.5', '1.0')) 92 | m1 = Meter(wri, 5, 44, height = 50, divisions = 4, legends=('-1', '0', '+1')) 93 | m2 = Meter(wri, 5, 86, height = 50, divisions = 4, legends=('-1', '0', '+1')) 94 | steps = 10 95 | random = xorshift64star(2**24 - 1) 96 | for n in range(steps + 1): 97 | m0.value(random() / 16777216) 98 | m1.value(n/steps) 99 | m2.value(1 - n/steps) 100 | refresh(ssd) 101 | utime.sleep(1) 102 | 103 | 104 | tstr = '''Test assumes a 128*64 (w*h) display. Edit WIDTH and HEIGHT in ssd1306_setup.py for others. 105 | Device pinouts are comments in ssd1306_setup.py. 106 | 107 | Test runs to completion. 108 | ''' 109 | 110 | print(tstr) 111 | print('Basic test of fields.') 112 | fields() 113 | print('More fields.') 114 | multi_fields() 115 | print('Meters.') 116 | meter() 117 | print('Done.') 118 | -------------------------------------------------------------------------------- /gui/demos/scale.py: -------------------------------------------------------------------------------- 1 | # scale.py Test/demo of scale widget for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020-2021 Peter Hinch 5 | 6 | # Usage: 7 | # import gui.demos.scale 8 | 9 | # Initialise hardware and framebuf before importing modules. 10 | # Uses uasyncio and also the asynchronous do_refresh method if the driver 11 | # supports it. 12 | 13 | from color_setup import ssd # Create a display instance 14 | 15 | from gui.core.nanogui import refresh 16 | from gui.core.writer import CWriter 17 | 18 | import uasyncio as asyncio 19 | from gui.core.colors import * 20 | import gui.fonts.arial10 as arial10 21 | from gui.widgets.label import Label 22 | from gui.widgets.scale import Scale 23 | 24 | # COROUTINES 25 | async def radio(scale): 26 | cv = 88.0 # Current value 27 | val = 108.0 # Target value 28 | while True: 29 | v1, v2 = val, cv 30 | steps = 200 31 | delta = (val - cv) / steps 32 | for _ in range(steps): 33 | cv += delta 34 | # Map user variable to -1.0..+1.0 35 | scale.value(2 * (cv - 88)/(108 - 88) - 1) 36 | await asyncio.sleep_ms(200) 37 | val, cv = v2, v1 38 | 39 | async def default(scale, lbl): 40 | cv = -1.0 # Current 41 | val = 1.0 42 | while True: 43 | v1, v2 = val, cv 44 | steps = 400 45 | delta = (val - cv) / steps 46 | for _ in range(steps): 47 | cv += delta 48 | scale.value(cv) 49 | lbl.value('{:4.3f}'.format(cv)) 50 | if hasattr(ssd, 'do_refresh'): 51 | # Option to reduce uasyncio latency 52 | await ssd.do_refresh() 53 | else: 54 | # Normal synchronous call 55 | refresh(ssd) 56 | await asyncio.sleep_ms(250) 57 | val, cv = v2, v1 58 | 59 | 60 | def test(): 61 | def tickcb(f, c): 62 | if f > 0.8: 63 | return RED 64 | if f < -0.8: 65 | return BLUE 66 | return c 67 | def legendcb(f): 68 | return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88)) 69 | refresh(ssd, True) # Initialise and clear display. 70 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 71 | wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) 72 | wri.set_clip(True, True, False) 73 | scale1 = Scale(wri, 2, 2, width = 124, legendcb = legendcb, 74 | pointercolor=RED, fontcolor=YELLOW) 75 | asyncio.create_task(radio(scale1)) 76 | 77 | lbl = Label(wri, ssd.height - wri.height - 2, 2, 50, 78 | bgcolor = DARKGREEN, bdcolor = RED, fgcolor=WHITE) 79 | # do_refresh is called with arg 4. In landscape mode this splits screen 80 | # into segments of 240/4=60 lines. Here we ensure a scale straddles 81 | # this boundary 82 | scale = Scale(wri, 55, 2, width = 124, tickcb = tickcb, 83 | pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN) 84 | asyncio.run(default(scale, lbl)) 85 | 86 | test() 87 | -------------------------------------------------------------------------------- /gui/demos/sharptest.py: -------------------------------------------------------------------------------- 1 | # sharptest.py Test script for monochrome sharp displays 2 | # Tested on 3 | # https://www.adafruit.com/product/4694 2.7 inch 400x240 Monochrome 4 | 5 | # Copyright (c) Peter Hinch 2020 6 | # Released under the MIT license see LICENSE 7 | 8 | # WIRING 9 | # Pyb SSD 10 | # Vin Vin Pyboard: Vin is an output when powered by USB 11 | # Gnd Gnd 12 | # Y8 DI 13 | # Y6 CLK 14 | # Y5 CS 15 | 16 | from color_setup import ssd # Create a display instance 17 | # Fonts for Writer 18 | import gui.fonts.freesans20 as freesans20 19 | import gui.fonts.arial_50 as arial_50 20 | 21 | from gui.core.writer import Writer 22 | import time 23 | 24 | def test(): 25 | rhs = ssd.width -1 26 | ssd.line(rhs - 80, 0, rhs, 80, 1) 27 | square_side = 40 28 | ssd.fill_rect(rhs - square_side, 0, square_side, square_side, 1) 29 | 30 | wri = Writer(ssd, freesans20) 31 | Writer.set_textpos(ssd, 0, 0) # verbose = False to suppress console output 32 | wri.printstring('Sunday\n') 33 | wri.printstring('12 Aug 2018\n') 34 | wri.printstring('10.30am') 35 | 36 | wri = Writer(ssd, arial_50) 37 | Writer.set_textpos(ssd, 0, 120) 38 | wri.printstring('10:30') 39 | ssd.show() 40 | 41 | test() 42 | -------------------------------------------------------------------------------- /gui/demos/tbox.py: -------------------------------------------------------------------------------- 1 | # tbox.py Test/demo of Textbox widget for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # Usage: 7 | # import gui.demos.tbox 8 | 9 | # Initialise hardware and framebuf before importing modules. 10 | from color_setup import ssd # Create a display instance 11 | 12 | from gui.core.nanogui import refresh 13 | from gui.core.writer import CWriter 14 | 15 | import uasyncio as asyncio 16 | from gui.core.colors import * 17 | import gui.fonts.arial10 as arial10 18 | from gui.widgets.label import Label 19 | from gui.widgets.textbox import Textbox 20 | 21 | # Args common to both Textbox instances 22 | # Positional 23 | pargs = (2, 2, 124, 7) # Row, Col, Width, nlines 24 | 25 | # Keyword 26 | tbargs = {'fgcolor' : YELLOW, 27 | 'bdcolor' : RED, 28 | 'bgcolor' : DARKGREEN, 29 | } 30 | 31 | async def wrap(wri): 32 | s = '''The textbox displays multiple lines of text in a field of fixed dimensions. \ 33 | Text may be clipped to the width of the control or may be word-wrapped. If the number \ 34 | of lines of text exceeds the height available, scrolling may be performed \ 35 | by calling a method. 36 | ''' 37 | tb = Textbox(wri, *pargs, clip=False, **tbargs) 38 | tb.append(s, ntrim = 100, line = 0) 39 | refresh(ssd) 40 | while True: 41 | await asyncio.sleep(1) 42 | if not tb.scroll(1): 43 | break 44 | refresh(ssd) 45 | 46 | async def clip(wri): 47 | ss = ('clip demo', 'short', 'longer line', 'much longer line with spaces', 48 | 'antidisestablishmentarianism', 'line with\nline break', 'Done') 49 | tb = Textbox(wri, *pargs, clip=True, **tbargs) 50 | for s in ss: 51 | tb.append(s, ntrim = 100) # Default line=None scrolls to show most recent 52 | refresh(ssd) 53 | await asyncio.sleep(1) 54 | 55 | 56 | async def main(wri): 57 | await wrap(wri) 58 | await clip(wri) 59 | 60 | def test(): 61 | refresh(ssd, True) # Initialise and clear display. 62 | CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it 63 | wri = CWriter(ssd, arial10, verbose=False) 64 | wri.set_clip(True, True, False) 65 | asyncio.run(main(wri)) 66 | 67 | test() 68 | -------------------------------------------------------------------------------- /gui/demos/waveshare_test.py: -------------------------------------------------------------------------------- 1 | # waveshare_test.py Demo program for nano_gui on an Waveshare ePaper screen 2 | # https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT 3 | 4 | # Released under the MIT License (MIT). See LICENSE. 5 | # Copyright (c) 2020 Peter Hinch 6 | 7 | # color_setup must set landcsape False, asyn True and must not set demo_mode 8 | import uasyncio as asyncio 9 | from color_setup import ssd 10 | from gui.core.writer import Writer 11 | from gui.core.nanogui import refresh 12 | from gui.widgets.meter import Meter 13 | from gui.widgets.label import Label 14 | 15 | # Fonts 16 | import gui.fonts.arial10 as arial10 17 | import gui.fonts.courier20 as fixed 18 | import gui.fonts.font6 as small 19 | 20 | # Some ports don't support uos.urandom. 21 | # See https://github.com/peterhinch/micropython-samples/tree/master/random 22 | def xorshift64star(modulo, seed = 0xf9ac6ba4): 23 | x = seed 24 | def func(): 25 | nonlocal x 26 | x ^= x >> 12 27 | x ^= ((x << 25) & 0xffffffffffffffff) # modulo 2**64 28 | x ^= x >> 27 29 | return (x * 0x2545F4914F6CDD1D) % modulo 30 | return func 31 | 32 | async def fields(evt): 33 | wri = Writer(ssd, fixed, verbose=False) 34 | wri.set_clip(False, False, False) 35 | textfield = Label(wri, 0, 2, wri.stringlen('longer')) 36 | numfield = Label(wri, 25, 2, wri.stringlen('99.990'), bdcolor=None) 37 | countfield = Label(wri, 0, 90, wri.stringlen('1')) 38 | n = 1 39 | random = xorshift64star(65535) 40 | while True: 41 | for s in ('short', 'longer', '1', ''): 42 | textfield.value(s) 43 | numfield.value('{:5.2f}'.format(random() /1000)) 44 | countfield.value('{:1d}'.format(n)) 45 | n += 1 46 | await evt.wait() 47 | 48 | async def multi_fields(evt): 49 | wri = Writer(ssd, small, verbose=False) 50 | wri.set_clip(False, False, False) 51 | 52 | nfields = [] 53 | dy = small.height() + 10 54 | y = 80 55 | col = 20 56 | width = wri.stringlen('99.990') 57 | for txt in ('X:', 'Y:', 'Z:'): 58 | Label(wri, y, 0, txt) 59 | nfields.append(Label(wri, y, col, width, bdcolor=None)) # Draw border 60 | y += dy 61 | 62 | random = xorshift64star(2**24 - 1) 63 | while True: 64 | for _ in range(10): 65 | for field in nfields: 66 | value = random() / 167772 67 | field.value('{:5.2f}'.format(value)) 68 | await evt.wait() 69 | 70 | async def meter(evt): 71 | wri = Writer(ssd, arial10, verbose=False) 72 | args = {'height' : 80, 73 | 'width' : 15, 74 | 'divisions' : 4, 75 | 'style' : Meter.BAR} 76 | m0 = Meter(wri, 165, 2, legends=('0.0', '0.5', '1.0'), **args) 77 | m1 = Meter(wri, 165, 62, legends=('-1', '0', '+1'), **args) 78 | m2 = Meter(wri, 165, 122, legends=('-1', '0', '+1'), **args) 79 | random = xorshift64star(2**24 - 1) 80 | while True: 81 | steps = 10 82 | for n in range(steps + 1): 83 | m0.value(random() / 16777216) 84 | m1.value(n/steps) 85 | m2.value(1 - n/steps) 86 | await evt.wait() 87 | 88 | async def main(): 89 | # ssd.fill(1) 90 | # ssd.show() 91 | # await ssd.wait() 92 | refresh(ssd, True) # Clear display 93 | await ssd.wait() 94 | print('Ready') 95 | evt = asyncio.Event() 96 | asyncio.create_task(meter(evt)) 97 | asyncio.create_task(multi_fields(evt)) 98 | asyncio.create_task(fields(evt)) 99 | while True: 100 | # Normal procedure before refresh, but 10s sleep should mean it always returns immediately 101 | await ssd.wait() 102 | refresh(ssd) # Launches ._as_show() 103 | await ssd.updated() 104 | # Content has now been shifted out so coros can update 105 | # framebuffer in background 106 | evt.set() 107 | evt.clear() 108 | await asyncio.sleep(9) # Allow for slow refresh 109 | 110 | 111 | tstr = '''Runs the following tests, updates every 10s 112 | fields() Label test with dynamic data. 113 | multi_fields() More Labels. 114 | meter() Demo of Meter object. 115 | ''' 116 | 117 | print(tstr) 118 | 119 | try: 120 | asyncio.run(main()) 121 | except KeyboardInterrupt: 122 | print('Waiting for display to become idle') 123 | ssd.wait_until_ready() # Synchronous code 124 | finally: 125 | _ = asyncio.new_event_loop() 126 | -------------------------------------------------------------------------------- /gui/fonts/arial10.py: -------------------------------------------------------------------------------- 1 | # Code generated by font-to-py.py. 2 | # Font: Arial.ttf 3 | version = '0.25' 4 | 5 | def height(): 6 | return 10 7 | 8 | def max_width(): 9 | return 11 10 | 11 | def hmap(): 12 | return True 13 | 14 | def reverse(): 15 | return False 16 | 17 | def monospaced(): 18 | return False 19 | 20 | def min_ch(): 21 | return 32 22 | 23 | def max_ch(): 24 | return 126 25 | 26 | _font =\ 27 | b'\x06\x00\x70\x88\x08\x10\x20\x20\x00\x20\x00\x00\x03\x00\x00\x00'\ 28 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x80\x80\x80\x80\x80\x80'\ 29 | b'\x00\x80\x00\x00\x04\x00\xa0\xa0\xa0\x00\x00\x00\x00\x00\x00\x00'\ 30 | b'\x06\x00\x28\x28\xf8\x50\x50\xf8\xa0\xa0\x00\x00\x06\x00\x70\xa8'\ 31 | b'\xa0\x70\x28\x28\xa8\x70\x20\x00\x0a\x00\x62\x00\x94\x00\x94\x00'\ 32 | b'\x68\x00\x0b\x00\x14\x80\x14\x80\x23\x00\x00\x00\x00\x00\x07\x00'\ 33 | b'\x30\x48\x48\x30\x50\x8c\x88\x74\x00\x00\x02\x00\x80\x80\x80\x00'\ 34 | b'\x00\x00\x00\x00\x00\x00\x04\x00\x20\x40\x80\x80\x80\x80\x80\x80'\ 35 | b'\x40\x20\x04\x00\x80\x40\x20\x20\x20\x20\x20\x20\x40\x80\x04\x00'\ 36 | b'\x40\xe0\x40\xa0\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x20\x20'\ 37 | b'\xf8\x20\x20\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x80'\ 38 | b'\x80\x80\x04\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x03\x00'\ 39 | b'\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x03\x00\x20\x20\x40\x40'\ 40 | b'\x40\x40\x80\x80\x00\x00\x06\x00\x70\x88\x88\x88\x88\x88\x88\x70'\ 41 | b'\x00\x00\x06\x00\x20\x60\xa0\x20\x20\x20\x20\x20\x00\x00\x06\x00'\ 42 | b'\x70\x88\x08\x08\x10\x20\x40\xf8\x00\x00\x06\x00\x70\x88\x08\x30'\ 43 | b'\x08\x08\x88\x70\x00\x00\x06\x00\x10\x30\x50\x50\x90\xf8\x10\x10'\ 44 | b'\x00\x00\x06\x00\x78\x40\x80\xf0\x08\x08\x88\x70\x00\x00\x06\x00'\ 45 | b'\x70\x88\x80\xf0\x88\x88\x88\x70\x00\x00\x06\x00\xf8\x10\x10\x20'\ 46 | b'\x20\x40\x40\x40\x00\x00\x06\x00\x70\x88\x88\x70\x88\x88\x88\x70'\ 47 | b'\x00\x00\x06\x00\x70\x88\x88\x88\x78\x08\x88\x70\x00\x00\x03\x00'\ 48 | b'\x00\x00\x80\x00\x00\x00\x00\x80\x00\x00\x03\x00\x00\x00\x80\x00'\ 49 | b'\x00\x00\x00\x80\x80\x80\x06\x00\x00\x00\x08\x70\x80\x70\x08\x00'\ 50 | b'\x00\x00\x06\x00\x00\x00\x00\xf8\x00\xf8\x00\x00\x00\x00\x06\x00'\ 51 | b'\x00\x00\x80\x70\x08\x70\x80\x00\x00\x00\x06\x00\x70\x88\x08\x10'\ 52 | b'\x20\x20\x00\x20\x00\x00\x0b\x00\x1f\x00\x60\x80\x4d\x40\x93\x40'\ 53 | b'\xa2\x40\xa2\x40\xa6\x80\x9b\x00\x40\x40\x3f\x80\x08\x00\x10\x28'\ 54 | b'\x28\x28\x44\x7c\x82\x82\x00\x00\x07\x00\xf8\x84\x84\xfc\x84\x84'\ 55 | b'\x84\xf8\x00\x00\x07\x00\x38\x44\x80\x80\x80\x80\x44\x38\x00\x00'\ 56 | b'\x07\x00\xf0\x88\x84\x84\x84\x84\x88\xf0\x00\x00\x06\x00\xf8\x80'\ 57 | b'\x80\xf8\x80\x80\x80\xf8\x00\x00\x06\x00\xf8\x80\x80\xf0\x80\x80'\ 58 | b'\x80\x80\x00\x00\x08\x00\x38\x44\x82\x80\x8e\x82\x44\x38\x00\x00'\ 59 | b'\x07\x00\x84\x84\x84\xfc\x84\x84\x84\x84\x00\x00\x02\x00\x80\x80'\ 60 | b'\x80\x80\x80\x80\x80\x80\x00\x00\x05\x00\x10\x10\x10\x10\x10\x90'\ 61 | b'\x90\x60\x00\x00\x07\x00\x84\x88\x90\xb0\xd0\x88\x88\x84\x00\x00'\ 62 | b'\x06\x00\x80\x80\x80\x80\x80\x80\x80\xf8\x00\x00\x08\x00\x82\xc6'\ 63 | b'\xc6\xaa\xaa\xaa\x92\x92\x00\x00\x07\x00\x84\xc4\xa4\xa4\x94\x94'\ 64 | b'\x8c\x84\x00\x00\x08\x00\x38\x44\x82\x82\x82\x82\x44\x38\x00\x00'\ 65 | b'\x06\x00\xf0\x88\x88\x88\xf0\x80\x80\x80\x00\x00\x08\x00\x38\x44'\ 66 | b'\x82\x82\x82\x9a\x44\x3e\x00\x00\x07\x00\xf8\x84\x84\xf8\x90\x88'\ 67 | b'\x88\x84\x00\x00\x07\x00\x78\x84\x80\x60\x18\x04\x84\x78\x00\x00'\ 68 | b'\x06\x00\xf8\x20\x20\x20\x20\x20\x20\x20\x00\x00\x07\x00\x84\x84'\ 69 | b'\x84\x84\x84\x84\x84\x78\x00\x00\x08\x00\x82\x82\x44\x44\x28\x28'\ 70 | b'\x10\x10\x00\x00\x0b\x00\x84\x20\x8a\x20\x4a\x40\x4a\x40\x51\x40'\ 71 | b'\x51\x40\x20\x80\x20\x80\x00\x00\x00\x00\x07\x00\x84\x48\x48\x30'\ 72 | b'\x30\x48\x48\x84\x00\x00\x08\x00\x82\x44\x44\x28\x10\x10\x10\x10'\ 73 | b'\x00\x00\x07\x00\x7c\x08\x10\x10\x20\x20\x40\xfc\x00\x00\x03\x00'\ 74 | b'\xc0\x80\x80\x80\x80\x80\x80\x80\x80\xc0\x03\x00\x80\x80\x40\x40'\ 75 | b'\x40\x40\x20\x20\x00\x00\x03\x00\xc0\x40\x40\x40\x40\x40\x40\x40'\ 76 | b'\x40\xc0\x05\x00\x20\x50\x50\x88\x00\x00\x00\x00\x00\x00\x06\x00'\ 77 | b'\x00\x00\x00\x00\x00\x00\x00\x00\xfc\x00\x04\x00\x80\x40\x00\x00'\ 78 | b'\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x70\x88\x78\x88\x98\xe8'\ 79 | b'\x00\x00\x06\x00\x80\x80\xb0\xc8\x88\x88\xc8\xb0\x00\x00\x06\x00'\ 80 | b'\x00\x00\x70\x88\x80\x80\x88\x70\x00\x00\x06\x00\x08\x08\x68\x98'\ 81 | b'\x88\x88\x98\x68\x00\x00\x06\x00\x00\x00\x70\x88\xf8\x80\x88\x70'\ 82 | b'\x00\x00\x04\x00\x20\x40\xe0\x40\x40\x40\x40\x40\x00\x00\x06\x00'\ 83 | b'\x00\x00\x68\x98\x88\x88\x98\x68\x08\xf0\x06\x00\x80\x80\xb0\xc8'\ 84 | b'\x88\x88\x88\x88\x00\x00\x02\x00\x80\x00\x80\x80\x80\x80\x80\x80'\ 85 | b'\x00\x00\x02\x00\x40\x00\x40\x40\x40\x40\x40\x40\x40\x80\x05\x00'\ 86 | b'\x80\x80\x90\xa0\xc0\xe0\xa0\x90\x00\x00\x02\x00\x80\x80\x80\x80'\ 87 | b'\x80\x80\x80\x80\x00\x00\x08\x00\x00\x00\xbc\xd2\x92\x92\x92\x92'\ 88 | b'\x00\x00\x06\x00\x00\x00\xf0\x88\x88\x88\x88\x88\x00\x00\x06\x00'\ 89 | b'\x00\x00\x70\x88\x88\x88\x88\x70\x00\x00\x06\x00\x00\x00\xb0\xc8'\ 90 | b'\x88\x88\xc8\xb0\x80\x80\x06\x00\x00\x00\x68\x98\x88\x88\x98\x68'\ 91 | b'\x08\x08\x04\x00\x00\x00\xa0\xc0\x80\x80\x80\x80\x00\x00\x06\x00'\ 92 | b'\x00\x00\x70\x88\x60\x10\x88\x70\x00\x00\x03\x00\x40\x40\xe0\x40'\ 93 | b'\x40\x40\x40\x60\x00\x00\x06\x00\x00\x00\x88\x88\x88\x88\x98\x68'\ 94 | b'\x00\x00\x06\x00\x00\x00\x88\x88\x50\x50\x20\x20\x00\x00\x0a\x00'\ 95 | b'\x00\x00\x00\x00\x88\x80\x94\x80\x55\x00\x55\x00\x22\x00\x22\x00'\ 96 | b'\x00\x00\x00\x00\x06\x00\x00\x00\x88\x50\x20\x20\x50\x88\x00\x00'\ 97 | b'\x06\x00\x00\x00\x88\x88\x50\x50\x20\x20\x20\x40\x06\x00\x00\x00'\ 98 | b'\xf8\x10\x20\x20\x40\xf8\x00\x00\x04\x00\x20\x40\x40\x40\x80\x40'\ 99 | b'\x40\x40\x40\x20\x02\x00\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80'\ 100 | b'\x04\x00\x80\x40\x40\x40\x20\x40\x40\x40\x40\x80\x06\x00\x00\x00'\ 101 | b'\x00\xe8\xb0\x00\x00\x00\x00\x00' 102 | 103 | _index =\ 104 | b'\x00\x00\x0c\x00\x0c\x00\x18\x00\x18\x00\x24\x00\x24\x00\x30\x00'\ 105 | b'\x30\x00\x3c\x00\x3c\x00\x48\x00\x48\x00\x5e\x00\x5e\x00\x6a\x00'\ 106 | b'\x6a\x00\x76\x00\x76\x00\x82\x00\x82\x00\x8e\x00\x8e\x00\x9a\x00'\ 107 | b'\x9a\x00\xa6\x00\xa6\x00\xb2\x00\xb2\x00\xbe\x00\xbe\x00\xca\x00'\ 108 | b'\xca\x00\xd6\x00\xd6\x00\xe2\x00\xe2\x00\xee\x00\xee\x00\xfa\x00'\ 109 | b'\xfa\x00\x06\x01\x06\x01\x12\x01\x12\x01\x1e\x01\x1e\x01\x2a\x01'\ 110 | b'\x2a\x01\x36\x01\x36\x01\x42\x01\x42\x01\x4e\x01\x4e\x01\x5a\x01'\ 111 | b'\x5a\x01\x66\x01\x66\x01\x72\x01\x72\x01\x7e\x01\x7e\x01\x8a\x01'\ 112 | b'\x8a\x01\x96\x01\x96\x01\xac\x01\xac\x01\xb8\x01\xb8\x01\xc4\x01'\ 113 | b'\xc4\x01\xd0\x01\xd0\x01\xdc\x01\xdc\x01\xe8\x01\xe8\x01\xf4\x01'\ 114 | b'\xf4\x01\x00\x02\x00\x02\x0c\x02\x0c\x02\x18\x02\x18\x02\x24\x02'\ 115 | b'\x24\x02\x30\x02\x30\x02\x3c\x02\x3c\x02\x48\x02\x48\x02\x54\x02'\ 116 | b'\x54\x02\x60\x02\x60\x02\x6c\x02\x6c\x02\x78\x02\x78\x02\x84\x02'\ 117 | b'\x84\x02\x90\x02\x90\x02\x9c\x02\x9c\x02\xa8\x02\xa8\x02\xb4\x02'\ 118 | b'\xb4\x02\xca\x02\xca\x02\xd6\x02\xd6\x02\xe2\x02\xe2\x02\xee\x02'\ 119 | b'\xee\x02\xfa\x02\xfa\x02\x06\x03\x06\x03\x12\x03\x12\x03\x1e\x03'\ 120 | b'\x1e\x03\x2a\x03\x2a\x03\x36\x03\x36\x03\x42\x03\x42\x03\x4e\x03'\ 121 | b'\x4e\x03\x5a\x03\x5a\x03\x66\x03\x66\x03\x72\x03\x72\x03\x7e\x03'\ 122 | b'\x7e\x03\x8a\x03\x8a\x03\x96\x03\x96\x03\xa2\x03\xa2\x03\xae\x03'\ 123 | b'\xae\x03\xba\x03\xba\x03\xc6\x03\xc6\x03\xd2\x03\xd2\x03\xde\x03'\ 124 | b'\xde\x03\xea\x03\xea\x03\xf6\x03\xf6\x03\x02\x04\x02\x04\x0e\x04'\ 125 | b'\x0e\x04\x1a\x04\x1a\x04\x26\x04\x26\x04\x32\x04\x32\x04\x3e\x04'\ 126 | b'\x3e\x04\x54\x04\x54\x04\x60\x04\x60\x04\x6c\x04\x6c\x04\x78\x04'\ 127 | b'\x78\x04\x84\x04\x84\x04\x90\x04\x90\x04\x9c\x04\x9c\x04\xa8\x04'\ 128 | 129 | _mvfont = memoryview(_font) 130 | 131 | def get_ch(ch): 132 | ordch = ord(ch) 133 | ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 134 | idx_offs = 4 * (ordch - 32) 135 | offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little') 136 | next_offs = int.from_bytes(_index[idx_offs + 2 : idx_offs + 4], 'little') 137 | width = int.from_bytes(_font[offset:offset + 2], 'little') 138 | return _mvfont[offset + 2:next_offs], 10, width 139 | 140 | -------------------------------------------------------------------------------- /gui/fonts/font6.py: -------------------------------------------------------------------------------- 1 | # Code generated by font-to-py.py. 2 | # Font: FreeSans.ttf 3 | version = '0.2' 4 | 5 | def height(): 6 | return 14 7 | 8 | def max_width(): 9 | return 14 10 | 11 | def hmap(): 12 | return True 13 | 14 | def reverse(): 15 | return False 16 | 17 | def monospaced(): 18 | return False 19 | 20 | def min_ch(): 21 | return 32 22 | 23 | def max_ch(): 24 | return 126 25 | 26 | _font =\ 27 | b'\x08\x00\x00\x78\x8c\x84\x04\x18\x30\x20\x20\x00\x20\x00\x00\x00'\ 28 | b'\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ 29 | b'\x05\x00\x00\x80\x80\x80\x80\x80\x80\x80\x80\x00\x80\x00\x00\x00'\ 30 | b'\x05\x00\x00\xa0\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ 31 | b'\x08\x00\x00\x00\x12\x14\x7f\x24\x24\xfe\x28\x48\x48\x00\x00\x00'\ 32 | b'\x08\x00\x20\x78\xac\xa4\xa0\xa0\x78\x2c\xa4\xac\x78\x20\x00\x00'\ 33 | b'\x0c\x00\x00\x00\x70\x80\x89\x00\x89\x00\x8a\x00\x72\x00\x04\xe0'\ 34 | b'\x05\x10\x09\x10\x09\x10\x10\xe0\x00\x00\x00\x00\x00\x00\x09\x00'\ 35 | b'\x00\x00\x30\x00\x48\x00\x48\x00\x78\x00\x20\x00\x52\x00\x9e\x00'\ 36 | b'\x8c\x00\x8e\x00\x73\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x80'\ 37 | b'\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20'\ 38 | b'\x40\x40\x80\x80\x80\x80\x80\x80\x80\x40\x40\x20\x05\x00\x00\x80'\ 39 | b'\x40\x40\x20\x20\x20\x20\x20\x20\x20\x40\x40\x80\x05\x00\x00\x20'\ 40 | b'\xf8\x20\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ 41 | b'\x00\x00\x00\x20\x20\xf8\x20\x20\x20\x00\x00\x00\x04\x00\x00\x00'\ 42 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x80\x80\x80\x00\x05\x00\x00\x00'\ 43 | b'\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00'\ 44 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x04\x00\x00\x10'\ 45 | b'\x10\x20\x20\x20\x40\x40\x40\x80\x80\x00\x00\x00\x08\x00\x00\x78'\ 46 | b'\x48\x84\x84\x84\x84\x84\x84\x48\x78\x00\x00\x00\x08\x00\x00\x20'\ 47 | b'\x60\xe0\x20\x20\x20\x20\x20\x20\x20\x00\x00\x00\x08\x00\x00\x78'\ 48 | b'\xcc\x84\x04\x0c\x18\x60\x40\x80\xfc\x00\x00\x00\x08\x00\x00\x78'\ 49 | b'\xc4\x84\x04\x38\x04\x04\x84\xcc\x78\x00\x00\x00\x08\x00\x00\x08'\ 50 | b'\x18\x38\x28\x48\x88\xfc\x08\x08\x08\x00\x00\x00\x08\x00\x00\x7c'\ 51 | b'\x80\x80\xb8\xcc\x04\x04\x04\x88\x78\x00\x00\x00\x08\x00\x00\x38'\ 52 | b'\x48\x84\x80\xf8\xcc\x84\x84\x4c\x78\x00\x00\x00\x08\x00\x00\xfc'\ 53 | b'\x0c\x08\x10\x10\x20\x20\x20\x40\x40\x00\x00\x00\x08\x00\x00\x78'\ 54 | b'\x84\x84\x84\x78\xcc\x84\x84\xcc\x78\x00\x00\x00\x08\x00\x00\x78'\ 55 | b'\xc8\x84\x84\xcc\x74\x04\x04\x88\x70\x00\x00\x00\x04\x00\x00\x00'\ 56 | b'\x00\x80\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00'\ 57 | b'\x00\x00\x80\x00\x00\x00\x00\x00\x80\x80\x80\x00\x08\x00\x00\x00'\ 58 | b'\x00\x00\x00\x1c\x70\x80\x60\x1c\x04\x00\x00\x00\x08\x00\x00\x00'\ 59 | b'\x00\x00\x00\x00\xfc\x00\xfc\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ 60 | b'\x00\x00\x00\xe0\x38\x06\x1c\x60\x80\x00\x00\x00\x08\x00\x00\x78'\ 61 | b'\x8c\x84\x04\x18\x30\x20\x20\x00\x20\x00\x00\x00\x0e\x00\x00\x00'\ 62 | b'\x07\xc0\x18\x60\x20\x10\x43\x48\x84\xc8\x88\xc8\x88\x88\x89\x90'\ 63 | b'\xc6\xe0\x60\x00\x30\x00\x0f\xc0\x00\x00\x09\x00\x00\x00\x0c\x00'\ 64 | b'\x1c\x00\x14\x00\x16\x00\x32\x00\x22\x00\x7f\x00\x41\x00\x41\x80'\ 65 | b'\xc1\x80\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\xfc\x00\x82\x00'\ 66 | b'\x82\x00\x82\x00\xfc\x00\x86\x00\x82\x00\x82\x00\x86\x00\xfc\x00'\ 67 | b'\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x3c\x00\x42\x00\x41\x00'\ 68 | b'\x80\x00\x80\x00\x80\x00\x81\x00\xc1\x00\x62\x00\x3c\x00\x00\x00'\ 69 | b'\x00\x00\x00\x00\x0a\x00\x00\x00\xfc\x00\x82\x00\x83\x00\x81\x00'\ 70 | b'\x81\x00\x81\x00\x81\x00\x83\x00\x82\x00\xfc\x00\x00\x00\x00\x00'\ 71 | b'\x00\x00\x09\x00\x00\x00\xfe\x00\x80\x00\x80\x00\x80\x00\xfc\x00'\ 72 | b'\x80\x00\x80\x00\x80\x00\x80\x00\xfe\x00\x00\x00\x00\x00\x00\x00'\ 73 | b'\x08\x00\x00\xfc\x80\x80\x80\xfc\x80\x80\x80\x80\x80\x00\x00\x00'\ 74 | b'\x0b\x00\x00\x00\x1e\x00\x61\x00\x40\x80\x80\x00\x80\x00\x87\x80'\ 75 | b'\x80\x80\xc0\x80\x61\x80\x3e\x80\x00\x00\x00\x00\x00\x00\x0a\x00'\ 76 | b'\x00\x00\x81\x00\x81\x00\x81\x00\x81\x00\xff\x00\x81\x00\x81\x00'\ 77 | b'\x81\x00\x81\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x80'\ 78 | b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x07\x00\x00\x04'\ 79 | b'\x04\x04\x04\x04\x04\x04\x84\x84\x78\x00\x00\x00\x09\x00\x00\x00'\ 80 | b'\x82\x00\x84\x00\x88\x00\x90\x00\xb0\x00\xd8\x00\x88\x00\x84\x00'\ 81 | b'\x86\x00\x82\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x80\x80\x80'\ 82 | b'\x80\x80\x80\x80\x80\x80\xfc\x00\x00\x00\x0c\x00\x00\x00\xc1\x80'\ 83 | b'\xc1\x80\xc1\x80\xa2\x80\xa2\x80\xa2\x80\x94\x80\x94\x80\x94\x80'\ 84 | b'\x88\x80\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\xc1\x00\xc1\x00'\ 85 | b'\xe1\x00\xb1\x00\x91\x00\x89\x00\x8d\x00\x87\x00\x83\x00\x83\x00'\ 86 | b'\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x3e\x00\x63\x00\xc1\x00'\ 87 | b'\x80\x80\x80\x80\x80\x80\x80\x80\xc1\x00\x63\x00\x3e\x00\x00\x00'\ 88 | b'\x00\x00\x00\x00\x09\x00\x00\x00\xfc\x00\x86\x00\x82\x00\x82\x00'\ 89 | b'\x86\x00\xfc\x00\x80\x00\x80\x00\x80\x00\x80\x00\x00\x00\x00\x00'\ 90 | b'\x00\x00\x0b\x00\x00\x00\x3e\x00\x63\x00\xc1\x00\x80\x80\x80\x80'\ 91 | b'\x80\x80\x80\x80\xc5\x80\x63\x00\x3f\x00\x00\x80\x00\x00\x00\x00'\ 92 | b'\x0a\x00\x00\x00\xfc\x00\x82\x00\x82\x00\x82\x00\x82\x00\xfc\x00'\ 93 | b'\x82\x00\x82\x00\x82\x00\x83\x00\x00\x00\x00\x00\x00\x00\x09\x00'\ 94 | b'\x00\x00\x7c\x00\xc6\x00\x82\x00\xc0\x00\x78\x00\x0e\x00\x02\x00'\ 95 | b'\x82\x00\xc6\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00'\ 96 | b'\xfe\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00'\ 97 | b'\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x81\x00'\ 98 | b'\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\xc3\x00'\ 99 | b'\x3c\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\xc1\x80\x41\x00'\ 100 | b'\x41\x00\x63\x00\x22\x00\x32\x00\x16\x00\x14\x00\x1c\x00\x08\x00'\ 101 | b'\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\xc2\x18\x45\x18\x45\x10'\ 102 | b'\x65\x10\x65\xb0\x28\xa0\x28\xa0\x38\xa0\x38\xe0\x10\x40\x00\x00'\ 103 | b'\x00\x00\x00\x00\x09\x00\x00\x00\x41\x00\x63\x00\x32\x00\x14\x00'\ 104 | b'\x0c\x00\x1c\x00\x16\x00\x22\x00\x63\x00\x41\x80\x00\x00\x00\x00'\ 105 | b'\x00\x00\x09\x00\x00\x00\xc1\x80\x63\x00\x22\x00\x36\x00\x14\x00'\ 106 | b'\x08\x00\x08\x00\x08\x00\x08\x00\x08\x00\x00\x00\x00\x00\x00\x00'\ 107 | b'\x09\x00\x00\x00\x7f\x00\x03\x00\x06\x00\x04\x00\x0c\x00\x18\x00'\ 108 | b'\x30\x00\x20\x00\x40\x00\xff\x00\x00\x00\x00\x00\x00\x00\x04\x00'\ 109 | b'\x00\xc0\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\xc0\x04\x00'\ 110 | b'\x00\x80\x80\x40\x40\x40\x20\x20\x20\x10\x10\x00\x00\x00\x04\x00'\ 111 | b'\x00\xc0\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xc0\x07\x00'\ 112 | b'\x00\x20\x60\x50\x90\x88\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00'\ 113 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x04\x00'\ 114 | b'\x00\x40\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00'\ 115 | b'\x00\x00\x00\x78\x84\x04\x04\x7c\x84\x8c\x76\x00\x00\x00\x08\x00'\ 116 | b'\x00\x80\x80\xb8\xcc\x84\x84\x84\x84\xc8\xb8\x00\x00\x00\x07\x00'\ 117 | b'\x00\x00\x00\x78\x44\x80\x80\x80\x80\x44\x78\x00\x00\x00\x08\x00'\ 118 | b'\x00\x02\x02\x3a\x46\x82\x82\x82\x82\x46\x3a\x00\x00\x00\x07\x00'\ 119 | b'\x00\x00\x00\x3c\x44\x82\xfe\x80\x80\x46\x3c\x00\x00\x00\x04\x00'\ 120 | b'\x00\x60\x40\xe0\x40\x40\x40\x40\x40\x40\x40\x00\x00\x00\x08\x00'\ 121 | b'\x00\x00\x00\x3a\x46\x82\x82\x82\x82\x46\x7a\x02\x84\x7c\x08\x00'\ 122 | b'\x00\x80\x80\xb0\xc8\x88\x88\x88\x88\x88\x88\x00\x00\x00\x03\x00'\ 123 | b'\x00\x80\x00\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x03\x00'\ 124 | b'\x00\x40\x00\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xc0\x07\x00'\ 125 | b'\x00\x80\x80\x88\x90\xa0\xe0\x90\x98\x88\x8c\x00\x00\x00\x03\x00'\ 126 | b'\x00\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x0b\x00'\ 127 | b'\x00\x00\x00\x00\x00\x00\xb7\x00\xcc\x80\x88\x80\x88\x80\x88\x80'\ 128 | b'\x88\x80\x88\x80\x88\x80\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ 129 | b'\x00\xb8\xc4\x84\x84\x84\x84\x84\x84\x00\x00\x00\x07\x00\x00\x00'\ 130 | b'\x00\x38\x44\x82\x82\x82\x82\x44\x38\x00\x00\x00\x08\x00\x00\x00'\ 131 | b'\x00\xb8\xc8\x84\x84\x84\x84\xc8\xb8\x80\x80\x00\x08\x00\x00\x00'\ 132 | b'\x00\x3a\x46\x82\x82\x82\x82\x46\x7a\x02\x02\x00\x05\x00\x00\x00'\ 133 | b'\x00\xa0\xc0\x80\x80\x80\x80\x80\x80\x00\x00\x00\x07\x00\x00\x00'\ 134 | b'\x00\x70\x88\x80\xc0\x70\x08\x88\x70\x00\x00\x00\x04\x00\x00\x00'\ 135 | b'\x40\xe0\x40\x40\x40\x40\x40\x40\x60\x00\x00\x00\x08\x00\x00\x00'\ 136 | b'\x00\x84\x84\x84\x84\x84\x84\x8c\x74\x00\x00\x00\x07\x00\x00\x00'\ 137 | b'\x00\xc6\x44\x44\x6c\x28\x28\x38\x10\x00\x00\x00\x0a\x00\x00\x00'\ 138 | b'\x00\x00\x00\x00\x8c\x40\xcc\xc0\x4c\x80\x5c\x80\x52\x80\x73\x80'\ 139 | b'\x33\x00\x33\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x44'\ 140 | b'\x68\x28\x30\x30\x28\x4c\xc4\x00\x00\x00\x07\x00\x00\x00\x00\xc6'\ 141 | b'\x44\x44\x6c\x28\x28\x30\x10\x10\x20\x60\x07\x00\x00\x00\x00\x7c'\ 142 | b'\x0c\x08\x10\x30\x60\x40\xfc\x00\x00\x00\x05\x00\x00\x60\x40\x40'\ 143 | b'\x40\x40\x40\x80\x40\x40\x40\x40\x40\x60\x04\x00\x00\x80\x80\x80'\ 144 | b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x05\x00\x00\xc0\x40\x40'\ 145 | b'\x40\x40\x40\x20\x40\x40\x40\x40\x40\xc0\x07\x00\x00\x00\x00\x00'\ 146 | b'\x00\x62\x9e\x00\x00\x00\x00\x00\x00\x00' 147 | 148 | _index =\ 149 | b'\x00\x00\x10\x00\x20\x00\x30\x00\x40\x00\x50\x00\x60\x00\x7e\x00'\ 150 | b'\x9c\x00\xac\x00\xbc\x00\xcc\x00\xdc\x00\xec\x00\xfc\x00\x0c\x01'\ 151 | b'\x1c\x01\x2c\x01\x3c\x01\x4c\x01\x5c\x01\x6c\x01\x7c\x01\x8c\x01'\ 152 | b'\x9c\x01\xac\x01\xbc\x01\xcc\x01\xdc\x01\xec\x01\xfc\x01\x0c\x02'\ 153 | b'\x1c\x02\x2c\x02\x4a\x02\x68\x02\x86\x02\xa4\x02\xc2\x02\xe0\x02'\ 154 | b'\xf0\x02\x0e\x03\x2c\x03\x3c\x03\x4c\x03\x6a\x03\x7a\x03\x98\x03'\ 155 | b'\xb6\x03\xd4\x03\xf2\x03\x10\x04\x2e\x04\x4c\x04\x6a\x04\x88\x04'\ 156 | b'\xa6\x04\xc4\x04\xe2\x04\x00\x05\x1e\x05\x2e\x05\x3e\x05\x4e\x05'\ 157 | b'\x5e\x05\x6e\x05\x7e\x05\x8e\x05\x9e\x05\xae\x05\xbe\x05\xce\x05'\ 158 | b'\xde\x05\xee\x05\xfe\x05\x0e\x06\x1e\x06\x2e\x06\x3e\x06\x5c\x06'\ 159 | b'\x6c\x06\x7c\x06\x8c\x06\x9c\x06\xac\x06\xbc\x06\xcc\x06\xdc\x06'\ 160 | b'\xec\x06\x0a\x07\x1a\x07\x2a\x07\x3a\x07\x4a\x07\x5a\x07\x6a\x07'\ 161 | b'\x7a\x07' 162 | 163 | _mvfont = memoryview(_font) 164 | 165 | def _chr_addr(ordch): 166 | offset = 2 * (ordch - 32) 167 | return int.from_bytes(_index[offset:offset + 2], 'little') 168 | 169 | def get_ch(ch): 170 | ordch = ord(ch) 171 | ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 172 | offset = _chr_addr(ordch) 173 | width = int.from_bytes(_font[offset:offset + 2], 'little') 174 | next_offs = _chr_addr(ordch +1) 175 | return _mvfont[offset + 2:next_offs], 14, width 176 | 177 | -------------------------------------------------------------------------------- /gui/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/gui/widgets/__init__.py -------------------------------------------------------------------------------- /gui/widgets/dial.py: -------------------------------------------------------------------------------- 1 | # dial.py Dial and Pointer classes for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2020 Peter Hinch 5 | 6 | import cmath 7 | from gui.core.nanogui import DObject, circle, fillcircle 8 | from gui.widgets.label import Label 9 | 10 | # Line defined by polar coords; origin and line are complex 11 | def polar(dev, origin, line, color): 12 | xs, ys = origin.real, origin.imag 13 | theta = cmath.polar(line)[1] 14 | dev.line(round(xs), round(ys), round(xs + line.real), round(ys - line.imag), color) 15 | 16 | def conj(v): # complex conjugate 17 | return v.real - v.imag * 1j 18 | 19 | # Draw an arrow; origin and vec are complex, scalar lc defines length of chevron. 20 | # cw and ccw are unit vectors of +-3pi/4 radians for chevrons (precompiled) 21 | def arrow(dev, origin, vec, lc, color, ccw=cmath.exp(3j * cmath.pi/4), cw=cmath.exp(-3j * cmath.pi/4)): 22 | length, theta = cmath.polar(vec) 23 | uv = cmath.rect(1, theta) # Unit rotation vector 24 | start = -vec 25 | if length > 3 * lc: # If line is long 26 | ds = cmath.rect(lc, theta) 27 | start += ds # shorten to allow for length of tail chevrons 28 | chev = lc + 0j 29 | polar(dev, origin, vec, color) # Origin to tip 30 | polar(dev, origin, start, color) # Origin to tail 31 | polar(dev, origin + conj(vec), chev*ccw*uv, color) # Tip chevron 32 | polar(dev, origin + conj(vec), chev*cw*uv, color) 33 | if length > lc: # Confusing appearance of very short vectors with tail chevron 34 | polar(dev, origin + conj(start), chev*ccw*uv, color) # Tail chevron 35 | polar(dev, origin + conj(start), chev*cw*uv, color) 36 | 37 | 38 | class Pointer(): 39 | def __init__(self, dial): 40 | self.dial = dial 41 | self.val = 0 + 0j 42 | self.color = None 43 | 44 | def value(self, v=None, color=None): 45 | self.color = color 46 | if v is not None: 47 | if isinstance(v, complex): 48 | l = cmath.polar(v)[0] 49 | if l > 1: 50 | self.val = v/l 51 | else: 52 | self.val = v 53 | else: 54 | raise ValueError('Pointer value must be complex.') 55 | self.dial.vectors.add(self) 56 | self.dial._set_pend(self.dial) # avoid redrawing for each vector 57 | return self.val 58 | 59 | class Dial(DObject): 60 | CLOCK = 0 61 | COMPASS = 1 62 | def __init__(self, writer, row, col, *, height=50, 63 | fgcolor=None, bgcolor=None, bdcolor=False, ticks=4, 64 | label=None, style=0, pip=None): 65 | super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) 66 | self.style = style 67 | self.pip = self.fgcolor if pip is None else pip 68 | if label is not None: 69 | self.label = Label(writer, row + height + 3, col, label) 70 | radius = int(height / 2) 71 | self.radius = radius 72 | self.ticks = ticks 73 | self.xorigin = col + radius 74 | self.yorigin = row + radius 75 | self.vectors = set() 76 | 77 | def show(self): 78 | super().show() 79 | # cache bound variables 80 | dev = self.device 81 | ticks = self.ticks 82 | radius = self.radius 83 | xo = self.xorigin 84 | yo = self.yorigin 85 | # vectors (complex) 86 | vor = xo + 1j * yo 87 | vtstart = 0.9 * radius + 0j # start of tick 88 | vtick = 0.1 * radius + 0j # tick 89 | vrot = cmath.exp(2j * cmath.pi/ticks) # unit rotation 90 | for _ in range(ticks): 91 | polar(dev, vor + conj(vtstart), vtick, self.fgcolor) 92 | vtick *= vrot 93 | vtstart *= vrot 94 | circle(dev, xo, yo, radius, self.fgcolor) 95 | vshort = 1000 # Length of shortest vector 96 | for v in self.vectors: 97 | color = self.fgcolor if v.color is None else v.color 98 | val = v.value() * radius # val is complex 99 | vshort = min(vshort, cmath.polar(val)[0]) 100 | if self.style == Dial.CLOCK: 101 | polar(dev, vor, val, color) 102 | else: 103 | arrow(dev, vor, val, 5, color) 104 | if isinstance(self.pip, int) and vshort > 5: 105 | fillcircle(dev, xo, yo, 2, self.pip) 106 | 107 | -------------------------------------------------------------------------------- /gui/widgets/label.py: -------------------------------------------------------------------------------- 1 | # label.py Label class for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2020 Peter Hinch 5 | 6 | from gui.core.nanogui import DObject 7 | from gui.core.writer import Writer 8 | 9 | # text: str display string int save width 10 | class Label(DObject): 11 | def __init__(self, writer, row, col, text, invert=False, fgcolor=None, bgcolor=None, bdcolor=False): 12 | # Determine width of object 13 | if isinstance(text, int): 14 | width = text 15 | text = None 16 | else: 17 | width = writer.stringlen(text) 18 | height = writer.height 19 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 20 | if text is not None: 21 | self.value(text, invert) 22 | 23 | def value(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): 24 | txt = super().value(text) 25 | # Redraw even if no text supplied: colors may have changed. 26 | self.invert = invert 27 | self.fgcolor = self.def_fgcolor if fgcolor is None else fgcolor 28 | self.bgcolor = self.def_bgcolor if bgcolor is None else bgcolor 29 | if bdcolor is False: 30 | self.def_bdcolor = False 31 | self.bdcolor = self.def_bdcolor if bdcolor is None else bdcolor 32 | self.show() 33 | return txt 34 | 35 | def show(self): 36 | txt = super().value() 37 | if txt is None: # No content to draw. Future use. 38 | return 39 | super().show() # Draw or erase border 40 | wri = self.writer 41 | dev = self.device 42 | Writer.set_textpos(dev, self.row, self.col) 43 | wri.setcolor(self.fgcolor, self.bgcolor) 44 | wri.printstring(txt, self.invert) 45 | wri.setcolor() # Restore defaults 46 | -------------------------------------------------------------------------------- /gui/widgets/led.py: -------------------------------------------------------------------------------- 1 | # led.py LED class for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2020 Peter Hinch 5 | 6 | from gui.core.nanogui import DObject, fillcircle, circle 7 | from gui.widgets.label import Label 8 | 9 | class LED(DObject): 10 | def __init__(self, writer, row, col, *, height=12, 11 | fgcolor=None, bgcolor=None, bdcolor=None, label=None): 12 | super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) 13 | if label is not None: 14 | self.label = Label(writer, row + height + 3, col, label) 15 | self.radius = self.height // 2 16 | 17 | def color(self, c=None): 18 | self.fgcolor = self.bgcolor if c is None else c 19 | self.show() 20 | 21 | def show(self): 22 | super().show() 23 | wri = self.writer 24 | dev = self.device 25 | r = self.radius 26 | fillcircle(dev, self.col + r, self.row + r, r, self.fgcolor) 27 | if isinstance(self.bdcolor, int): 28 | circle(dev, self.col + r, self.row + r, r, self.bdcolor) 29 | -------------------------------------------------------------------------------- /gui/widgets/meter.py: -------------------------------------------------------------------------------- 1 | # meter.py Meter class for nano-gui 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2018-2020 Peter Hinch 5 | 6 | from gui.core.nanogui import DObject 7 | from gui.widgets.label import Label 8 | 9 | 10 | class Meter(DObject): 11 | BAR = 1 12 | LINE = 0 13 | def __init__(self, writer, row, col, *, height=50, width=10, 14 | fgcolor=None, bgcolor=None, ptcolor=None, bdcolor=None, 15 | divisions=5, label=None, style=0, legends=None, value=None): 16 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 17 | self.divisions = divisions 18 | if label is not None: 19 | Label(writer, row + height + 3, col, label) 20 | self.style = style 21 | if legends is not None: # Legends 22 | x = col + width + 4 23 | y = row + height 24 | dy = 0 if len(legends) <= 1 else height / (len(legends) -1) 25 | yl = y - writer.height / 2 # Start at bottom 26 | for legend in legends: 27 | Label(writer, int(yl), x, legend) 28 | yl -= dy 29 | self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor 30 | self.value(value) 31 | 32 | def value(self, n=None, color=None): 33 | if n is None: 34 | return super().value() 35 | n = super().value(min(1, max(0, n))) 36 | if color is not None: 37 | self.ptcolor = color 38 | self.show() 39 | return n 40 | 41 | def show(self): 42 | super().show() # Draw or erase border 43 | val = super().value() 44 | wri = self.writer 45 | dev = self.device 46 | width = self.width 47 | height = self.height 48 | x0 = self.col 49 | x1 = self.col + width 50 | y0 = self.row 51 | y1 = self.row + height 52 | if self.divisions > 0: 53 | dy = height / (self.divisions) # Tick marks 54 | for tick in range(self.divisions + 1): 55 | ypos = int(y0 + dy * tick) 56 | dev.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) 57 | 58 | y = int(y1 - val * height) # y position of slider 59 | if self.style == self.LINE: 60 | dev.hline(x0, y, width, self.ptcolor) # Draw pointer 61 | else: 62 | w = width / 2 63 | dev.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) 64 | -------------------------------------------------------------------------------- /gui/widgets/scale.py: -------------------------------------------------------------------------------- 1 | # scale.py Extension to nano-gui providing the Scale class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # Usage: 7 | # from gui.widgets.scale import Scale 8 | 9 | from gui.core.nanogui import DObject 10 | from gui.core.writer import Writer 11 | from gui.core.colors import BLACK 12 | 13 | class Scale(DObject): 14 | def __init__(self, writer, row, col, *, 15 | ticks=200, legendcb=None, tickcb=None, 16 | height=0, width=100, bdcolor=None, fgcolor=None, bgcolor=None, 17 | pointercolor=None, fontcolor=None): 18 | if ticks % 2: 19 | raise ValueError('ticks arg must be divisible by 2') 20 | self.ticks = ticks 21 | self.tickcb = tickcb 22 | def lcb(f): 23 | return '{:3.1f}'.format(f) 24 | self.legendcb = legendcb if legendcb is not None else lcb 25 | bgcolor = BLACK if bgcolor is None else bgcolor 26 | text_ht = writer.font.height() 27 | ctrl_ht = 12 # Minimum height for ticks 28 | # Add 2 pixel internal border to give a little more space 29 | min_ht = text_ht + 6 # Ht of text, borders and gap between text and ticks 30 | if height < min_ht + ctrl_ht: 31 | height = min_ht + ctrl_ht # min workable height 32 | else: 33 | ctrl_ht = height - min_ht # adjust ticks for greater height 34 | width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset 35 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 36 | self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor 37 | self.x0 = col + 2 38 | self.x1 = col + self.width - 2 39 | self.y0 = row + 2 40 | self.y1 = row + self.height - 2 41 | self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor 42 | # Define tick dimensions 43 | ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap) 44 | ycl = ytop + (self.y1 - ytop) // 2 # Centre line 45 | self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick. 46 | self.sdy0 = ycl - self.sdl // 2 47 | self.mdl = round(ctrl_ht * 2 / 3) # Medium tick 48 | self.mdy0 = ycl - self.mdl // 2 49 | self.ldl = ctrl_ht # Large tick 50 | self.ldy0 = ycl - self.ldl // 2 51 | 52 | def show(self): 53 | wri = self.writer 54 | dev = self.device 55 | x0: int = self.x0 # Internal rectangle occupied by scale and text 56 | x1: int = self.x1 57 | y0: int = self.y0 58 | y1: int = self.y1 59 | dev.fill_rect(x0, y0, x1 - x0, y1 - y0, self.bgcolor) 60 | super().show() 61 | # Scale is drawn using ints. Each division is 10 units. 62 | val: int = self._value # 0..ticks*10 63 | # iv increments for each tick. Its value modulo N determines tick length 64 | iv: int # val / 10 at a tick position 65 | d: int # val % 10: offset relative to a tick position 66 | fx: int # X offset of current tick in value units 67 | if val >= 100: # Whole LHS of scale will be drawn 68 | iv, d = divmod(val - 100, 10) # Initial value 69 | fx = 10 - d 70 | iv += 1 71 | else: # Scale will scroll right 72 | iv = 0 73 | fx = 100 - val 74 | 75 | # Window shows 20 divisions, each of which corresponds to 10 units of value. 76 | # So pixels per unit value == win_width/200 77 | win_width: int = x1 - x0 78 | ticks: int = self.ticks # Total # of ticks visible and hidden 79 | while True: 80 | x: int = x0 + (fx * win_width) // 200 # Current X position 81 | ys: int # Start Y position for tick 82 | yl: int # tick length 83 | if x > x1 or iv > ticks: # Out of space or data (scroll left) 84 | break 85 | if not iv % 10: 86 | txt = self.legendcb(self._fvalue(iv * 10)) 87 | tlen = wri.stringlen(txt) 88 | Writer.set_textpos(dev, y0, min(x, x1 - tlen)) 89 | wri.setcolor(self.fontcolor, self.bgcolor) 90 | wri.printstring(txt) 91 | wri.setcolor() 92 | ys = self.ldy0 # Large tick 93 | yl = self.ldl 94 | elif not iv % 5: 95 | ys = self.mdy0 96 | yl = self.mdl 97 | else: 98 | ys = self.sdy0 99 | yl = self.sdl 100 | if self.tickcb is None: 101 | color = self.fgcolor 102 | else: 103 | color = self.tickcb(self._fvalue(iv * 10), self.fgcolor) 104 | dev.vline(x, ys, yl, color) # Draw tick 105 | fx += 10 106 | iv += 1 107 | 108 | dev.vline(x0 + (x1 - x0) // 2, y0, y1 - y0, self.ptrcolor) # Draw pointer 109 | 110 | def _to_int(self, v): 111 | return round((v + 1.0) * self.ticks * 5) # 0..self.ticks*10 112 | 113 | def _fvalue(self, v=None): 114 | return v / (5 * self.ticks) - 1.0 115 | 116 | def value(self, val=None): # User method to get or set value 117 | if val is not None: 118 | val = min(max(val, - 1.0), 1.0) 119 | v = self._to_int(val) 120 | if v != self._value: 121 | self._value = v 122 | self.show() 123 | return self._fvalue(self._value) 124 | -------------------------------------------------------------------------------- /gui/widgets/textbox.py: -------------------------------------------------------------------------------- 1 | # textbox.py Extension to nanogui providing the Textbox class 2 | 3 | # Released under the MIT License (MIT). See LICENSE. 4 | # Copyright (c) 2020 Peter Hinch 5 | 6 | # Usage: 7 | # from gui.widgets.textbox import Textbox 8 | 9 | from gui.core.nanogui import DObject 10 | from gui.core.writer import Writer 11 | 12 | # Reason for no tab support in private/reason_for_no_tabs 13 | 14 | class Textbox(DObject): 15 | def __init__(self, writer, row, col, width, nlines, *, bdcolor=None, fgcolor=None, 16 | bgcolor=None, clip=True): 17 | height = nlines * writer.height 18 | devht = writer.device.height 19 | devwd = writer.device.width 20 | if ((row + height + 2) > devht) or ((col + width + 2) > devwd): 21 | raise ValueError('Textbox extends beyond physical screen.') 22 | super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) 23 | self.nlines = nlines 24 | self.clip = clip 25 | self.lines = [] 26 | self.start = 0 # Start line for display 27 | 28 | def _add_lines(self, s): 29 | width = self.width 30 | font = self.writer.font 31 | n = -1 # Index into string 32 | newline = True 33 | while True: 34 | n += 1 35 | if newline: 36 | newline = False 37 | ls = n # Start of line being processed 38 | col = 0 # Column relative to text area 39 | if n >= len(s): # End of string 40 | if n > ls: 41 | self.lines.append(s[ls :]) 42 | return 43 | c = s[n] # Current char 44 | if c == '\n': 45 | self.lines.append(s[ls : n]) 46 | newline = True 47 | continue # Line fits window 48 | col += font.get_ch(c)[2] # width of current char 49 | if col > width: 50 | if self.clip: 51 | p = s[ls :].find('\n') # end of 1st line 52 | if p == -1: 53 | self.lines.append(s[ls : n]) # clip, discard all to right 54 | return 55 | self.lines.append(s[ls : n]) # clip, discard to 1st newline 56 | n = p # n will move to 1st char after newline 57 | elif c == ' ': # Easy word wrap 58 | self.lines.append(s[ls : n]) 59 | else: # Edge splits a word 60 | p = s.rfind(' ', ls, n + 1) 61 | if p >= 0: # spacechar in line: wrap at space 62 | assert (p > 0), 'space char in position 0' 63 | self.lines.append(s[ls : p]) 64 | n = p 65 | else: # No spacechar: wrap at end 66 | self.lines.append(s[ls : n]) 67 | n -= 1 # Don't skip current char 68 | newline = True 69 | 70 | def _print_lines(self): 71 | if len(self.lines) == 0: 72 | return 73 | 74 | dev = self.device 75 | wri = self.writer 76 | col = self.col 77 | row = self.row 78 | left = col 79 | ht = wri.height 80 | wri.setcolor(self.fgcolor, self.bgcolor) 81 | # Print the first (or last?) lines that fit widget's height 82 | #for line in self.lines[-self.nlines : ]: 83 | for line in self.lines[self.start : self.start + self.nlines]: 84 | Writer.set_textpos(dev, row, col) 85 | wri.printstring(line) 86 | row += ht 87 | col = left 88 | wri.setcolor() # Restore defaults 89 | 90 | def show(self): 91 | dev = self.device 92 | super().show() 93 | self._print_lines() 94 | 95 | def append(self, s, ntrim=None, line=None): 96 | self._add_lines(s) 97 | if ntrim is None: # Default to no. of lines that can fit 98 | ntrim = self.nlines 99 | if len(self.lines) > ntrim: 100 | self.lines = self.lines[-ntrim:] 101 | self.goto(line) 102 | 103 | def scroll(self, n): # Relative scrolling 104 | value = len(self.lines) 105 | if n == 0 or value <= self.nlines: # Nothing to do 106 | return False 107 | s = self.start 108 | self.start = max(0, min(self.start + n, value - self.nlines)) 109 | if s != self.start: 110 | self.show() 111 | return True 112 | return False 113 | 114 | def value(self): 115 | return len(self.lines) 116 | 117 | def clear(self): 118 | self.lines = [] 119 | self.show() 120 | 121 | def goto(self, line=None): # Absolute scrolling 122 | if line is None: 123 | self.start = max(0, len(self.lines) - self.nlines) 124 | else: 125 | self.start = max(0, min(line, len(self.lines) - self.nlines)) 126 | self.show() 127 | -------------------------------------------------------------------------------- /lib/ds18x20.py: -------------------------------------------------------------------------------- 1 | # DS18x20 temperature sensor driver for MicroPython. 2 | # MIT license; Copyright (c) 2016 Damien P. George 3 | 4 | from micropython import const 5 | 6 | _CONVERT = const(0x44) 7 | _RD_SCRATCH = const(0xBE) 8 | _WR_SCRATCH = const(0x4E) 9 | 10 | 11 | class DS18X20: 12 | def __init__(self, onewire): 13 | self.ow = onewire 14 | self.buf = bytearray(9) 15 | 16 | def scan(self): 17 | return [rom for rom in self.ow.scan() if rom[0] in (0x10, 0x22, 0x28)] 18 | 19 | def convert_temp(self): 20 | self.ow.reset(True) 21 | self.ow.writebyte(self.ow.SKIP_ROM) 22 | self.ow.writebyte(_CONVERT) 23 | 24 | def read_scratch(self, rom): 25 | self.ow.reset(True) 26 | self.ow.select_rom(rom) 27 | self.ow.writebyte(_RD_SCRATCH) 28 | self.ow.readinto(self.buf) 29 | if self.ow.crc8(self.buf): 30 | raise Exception("CRC error") 31 | return self.buf 32 | 33 | def write_scratch(self, rom, buf): 34 | self.ow.reset(True) 35 | self.ow.select_rom(rom) 36 | self.ow.writebyte(_WR_SCRATCH) 37 | self.ow.write(buf) 38 | 39 | def read_temp(self, rom): 40 | buf = self.read_scratch(rom) 41 | if rom[0] == 0x10: 42 | if buf[1]: 43 | t = buf[0] >> 1 | 0x80 44 | t = -((~t + 1) & 0xFF) 45 | else: 46 | t = buf[0] >> 1 47 | return t - 0.25 + (buf[7] - buf[6]) / buf[7] 48 | else: 49 | t = buf[1] << 8 | buf[0] 50 | if t & 0x8000: # sign bit set 51 | t = -((t ^ 0xFFFF) + 1) 52 | return t / 16 53 | -------------------------------------------------------------------------------- /lib/onewire.py: -------------------------------------------------------------------------------- 1 | # 1-Wire driver for MicroPython 2 | # MIT license; Copyright (c) 2016 Damien P. George 3 | 4 | import _onewire as _ow 5 | 6 | 7 | class OneWireError(Exception): 8 | pass 9 | 10 | 11 | class OneWire: 12 | SEARCH_ROM = 0xF0 13 | MATCH_ROM = 0x55 14 | SKIP_ROM = 0xCC 15 | 16 | def __init__(self, pin): 17 | self.pin = pin 18 | self.pin.init(pin.OPEN_DRAIN, pin.PULL_UP) 19 | 20 | def reset(self, required=False): 21 | reset = _ow.reset(self.pin) 22 | if required and not reset: 23 | raise OneWireError 24 | return reset 25 | 26 | def readbit(self): 27 | return _ow.readbit(self.pin) 28 | 29 | def readbyte(self): 30 | return _ow.readbyte(self.pin) 31 | 32 | def readinto(self, buf): 33 | for i in range(len(buf)): 34 | buf[i] = _ow.readbyte(self.pin) 35 | 36 | def writebit(self, value): 37 | return _ow.writebit(self.pin, value) 38 | 39 | def writebyte(self, value): 40 | return _ow.writebyte(self.pin, value) 41 | 42 | def write(self, buf): 43 | for b in buf: 44 | _ow.writebyte(self.pin, b) 45 | 46 | def select_rom(self, rom): 47 | self.reset() 48 | self.writebyte(self.MATCH_ROM) 49 | self.write(rom) 50 | 51 | def scan(self): 52 | devices = [] 53 | diff = 65 54 | rom = False 55 | for i in range(0xFF): 56 | rom, diff = self._search_rom(rom, diff) 57 | if rom: 58 | devices += [rom] 59 | if diff == 0: 60 | break 61 | return devices 62 | 63 | def _search_rom(self, l_rom, diff): 64 | if not self.reset(): 65 | return None, 0 66 | self.writebyte(self.SEARCH_ROM) 67 | if not l_rom: 68 | l_rom = bytearray(8) 69 | rom = bytearray(8) 70 | next_diff = 0 71 | i = 64 72 | for byte in range(8): 73 | r_b = 0 74 | for bit in range(8): 75 | b = self.readbit() 76 | if self.readbit(): 77 | if b: # there are no devices or there is an error on the bus 78 | return None, 0 79 | else: 80 | if not b: # collision, two devices with different bit meaning 81 | if diff > i or ((l_rom[byte] & (1 << bit)) and diff != i): 82 | b = 1 83 | next_diff = i 84 | self.writebit(b) 85 | if b: 86 | r_b |= 1 << bit 87 | i -= 1 88 | rom[byte] = r_b 89 | return rom, next_diff 90 | 91 | def crc8(self, data): 92 | return _ow.crc8(data) 93 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py - a script for making a plant watering thing, running using a Raspberry Pi Pico 3 | First prototype is using an OLED, rotary encoder and a relay switch (linked to water pump device of some sort) 4 | The display uses drivers made by Peter Hinch [link](https://github.com/peterhinch/micropython-nano-gui) 5 | 6 | Copyright (C) 2023 Veeb Projects https://veeb.ch 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see 20 | 21 | Fonts for Writer (generated using https://github.com/peterhinch/micropython-font-to-py) 22 | 23 | """ 24 | 25 | 26 | import gui.fonts.freesans20 as freesans20 27 | import gui.fonts.quantico40 as quantico40 28 | from gui.core.writer import CWriter 29 | from gui.core.nanogui import refresh 30 | import utime 31 | from machine import Pin, I2C, SPI, ADC, reset 32 | #from rp2 import PIO, StateMachine, asm_pio 33 | import sys 34 | import math 35 | import gc 36 | from drivers.ssd1351.ssd1351_16bit import SSD1351 as SSD 37 | import uasyncio as asyncio 38 | from primitives.pushbutton import Pushbutton 39 | 40 | 41 | def splash(string): 42 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 43 | 50, 50, 0), bgcolor=0, verbose=False) 44 | CWriter.set_textpos(ssd, 90, 25) 45 | wri.printstring('veeb.ch/') 46 | ssd.show() 47 | utime.sleep(.3) 48 | for x in range(10): 49 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 50 | 25*x, 25*x, 25*x), bgcolor=0, verbose=False) 51 | CWriter.set_textpos(ssd, 55, 25) 52 | wri.printstring(string) 53 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 54 | 50-x, 50-x, 0), bgcolor=0, verbose=False) 55 | CWriter.set_textpos(ssd, 90, 25) 56 | wri.printstring('veeb.ch/') 57 | ssd.show() 58 | utime.sleep(2) 59 | for x in range(10, 0, -1): 60 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 61 | 25*x, 25*x, 25*x), bgcolor=0, verbose=False) 62 | CWriter.set_textpos(ssd, 55, 25) 63 | wri.printstring(string) 64 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 65 | 50-x, 50-x, 0), bgcolor=0, verbose=False) 66 | CWriter.set_textpos(ssd, 90, 25) 67 | wri.printstring('veeb.ch/') 68 | ssd.show() 69 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 70 | 50, 50, 0), bgcolor=0, verbose=False) 71 | CWriter.set_textpos(ssd, 90, 25) 72 | wri.printstring('veeb.ch/') 73 | ssd.show() 74 | utime.sleep(.3) 75 | return 76 | 77 | 78 | def encoder(pin): 79 | # get global variables this would all be tidier if we use the encoder primitive - fix this 80 | global counter 81 | global direction 82 | global outA_last 83 | global outA_current 84 | global outA 85 | 86 | # read the value of current state of outA pin / CLK pin 87 | try: 88 | outA_current = outA.value() 89 | except: 90 | print('outA not defined') 91 | outA_current = 0 92 | outA_last = 0 93 | # if current state is not same as the last stare , encoder has rotated 94 | if outA_current != outA_last: 95 | # read outB pin/ DT pin 96 | # if DT value is not equal to CLK value 97 | # rotation is clockwise [or Counterclockwise ---> sensor dependent] 98 | if outB.value() != outA_current: 99 | counter += .5 100 | else: 101 | counter -= .5 102 | 103 | # print the data on screen 104 | #print("Counter : ", counter, " | Direction : ",direction) 105 | # print("\n") 106 | 107 | # update the last state of outA pin / CLK pin with the current state 108 | outA_last = outA_current 109 | counter = min(9, counter) 110 | counter = max(0, counter) 111 | return(counter) 112 | 113 | 114 | # function for short button press - currently just a placeholder 115 | def button(): 116 | print('Button short press: Boop') 117 | return 118 | 119 | # function for long button press - currently just a placeholder 120 | 121 | 122 | def buttonlong(): 123 | print('Button long press: Reset') 124 | return 125 | 126 | # Screen to display on OLED during heating 127 | 128 | 129 | def displaynum(num, value): 130 | # This needs to be fast for nice responsive increments 131 | # 100 increments? 132 | ssd.fill(0) 133 | delta = num-value 134 | text = SSD.rgb(0, 255, 0) 135 | if delta >= .5: 136 | text = SSD.rgb(165, 42, 42) 137 | if delta <= -.5: 138 | text = SSD.rgb(0, 255, 255) 139 | wri = CWriter(ssd, quantico40, fgcolor=text, bgcolor=0) 140 | # verbose = False to suppress console output 141 | CWriter.set_textpos(ssd, 50, 0) 142 | wri.printstring(str("{:.0f}".format(num))) 143 | wrimem = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 144 | 255, 255, 255), bgcolor=0) 145 | CWriter.set_textpos(ssd, 100, 0) 146 | wrimem.printstring('now at: '+str("{:.0f}".format(value))+"/ 10") 147 | CWriter.set_textpos(ssd, 0, 0) 148 | wrimem = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 149 | 155, 155, 155), bgcolor=0) 150 | wrimem.printstring('moisture') 151 | CWriter.set_textpos(ssd, 20, 0) 152 | wrimem.printstring('target:') 153 | ssd.show() 154 | return 155 | 156 | 157 | def beanaproblem(string): 158 | refresh(ssd, True) # Clear any prior image 159 | wri = CWriter(ssd, freesans20, fgcolor=SSD.rgb( 160 | 250, 250, 250), bgcolor=0, verbose=False) 161 | CWriter.set_textpos(ssd, 55, 25) 162 | wri.printstring(string) 163 | ssd.show() 164 | relaypin = Pin(15, mode=Pin.OUT, value=0) 165 | utime.sleep(2) 166 | 167 | 168 | # define encoder pins 169 | btn = Pin(4, Pin.IN, Pin.PULL_UP) # Adapt for your hardware 170 | pb = Pushbutton(btn, suppress=True) 171 | outA = Pin(2, mode=Pin.IN) # Pin CLK of encoder 172 | outB = Pin(3, mode=Pin.IN) # Pin DT of encoder 173 | # Attach interrupt to Pins 174 | 175 | # attach interrupt to the outA pin ( CLK pin of encoder module ) 176 | outA.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, 177 | handler=encoder) 178 | 179 | # attach interrupt to the outB pin ( DT pin of encoder module ) 180 | outB.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, 181 | handler=encoder) 182 | 183 | 184 | height = 128 185 | pdc = Pin(20, Pin.OUT, value=0) 186 | pcs = Pin(17, Pin.OUT, value=1) 187 | prst = Pin(21, Pin.OUT, value=1) 188 | spi = SPI(0, 189 | baudrate=10000000, 190 | polarity=1, 191 | phase=1, 192 | bits=8, 193 | firstbit=SPI.MSB, 194 | sck=Pin(18), 195 | mosi=Pin(19), 196 | miso=Pin(16)) 197 | gc.collect() # Precaution before instantiating framebuf 198 | 199 | ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance 200 | 201 | splash("sploosh") 202 | 203 | # Define relay and LED pins 204 | 205 | # Onboard led on GPIO 25, not currently used, but who doesnt love a controllable led? 206 | ledPin = Pin(25, mode=Pin.OUT, value=0) 207 | 208 | # define global variables 209 | counter = 0 # counter updates when encoder rotates 210 | direction = "" # empty string for registering direction change 211 | outA_last = 0 # registers the last state of outA pin / CLK pin 212 | outA_current = 0 # registers the current state of outA pin / CLK pin 213 | 214 | # Read the last state of CLK pin in the initialisaton phase of the program 215 | outA_last = outA.value() # lastStateCLK 216 | 217 | # Main Logic 218 | 219 | 220 | async def main(): 221 | short_press = pb.release_func(button, ()) 222 | long_press = pb.long_func(buttonlong, ()) 223 | pin = 0 224 | integral = 0 225 | lastupdate = utime.time() 226 | refresh(ssd, True) # Initialise and clear display. 227 | wetness = ADC(26) 228 | lasterror = 0 229 | # The Tweakable values that will help tune for our use case. TODO: Make accessible via menu on OLED 230 | calibratewet = 20000 # ADC value for a very wet thing 231 | calibratedry = 50000 # ADC value for a very dry thing 232 | checkin = 5 233 | # Stolen From Reddit: In terms of steering a ship: 234 | # Kp is steering harder the further off course you are, 235 | # Ki is steering into the wind to counteract a drift 236 | # Kd is slowing the turn as you approach your course 237 | # Proportional term - Basic steering (This is the first parameter you should tune for a particular setup) 238 | Kp = 2 239 | Ki = 0 # Integral term - Compensate for heat loss by vessel 240 | Kd = 0 # Derivative term - to prevent overshoot due to inertia - if it is zooming towards setpoint this 241 | # will cancel out the proportional term due to the large negative gradient 242 | output = 0 243 | offstate = False 244 | # PID loop - Default behaviour 245 | powerup = True 246 | while True: 247 | if powerup: 248 | try: 249 | counter = encoder(pin) 250 | # Get wetness 251 | imwet = wetness.read_u16() 252 | # linear relationship between ADC and wetness, clamped between 0, 10 253 | howdry = min(10, max(0, 10*(imwet-calibratedry) / 254 | (calibratewet-calibratedry))) 255 | print(imwet, howdry) 256 | temp = howdry # Wetness 257 | displaynum(counter, float(temp)) 258 | now = utime.time() 259 | dt = now-lastupdate 260 | if output < 100 and offstate == False and dt > checkin * round(output)/100: 261 | relaypin = Pin(15, mode=Pin.OUT, value=0) 262 | offstate = True 263 | utime.sleep(.1) 264 | if dt > checkin: 265 | error = counter-temp 266 | integral = integral + dt * error 267 | derivative = (error - lasterror)/dt 268 | output = Kp * error + Ki * integral + Kd * derivative 269 | print(str(output)+"= Kp term: "+str(Kp*error)+" + Ki term:" + 270 | str(Ki*integral) + "+ Kd term: " + str(Kd*derivative)) 271 | # Clamp output between 0 and 100 272 | output = max(min(100, output), 0) 273 | print(output) 274 | if output > 0: 275 | relaypin = Pin(15, mode=Pin.OUT, value=1) 276 | offstate = False 277 | else: 278 | relaypin = Pin(15, mode=Pin.OUT, value=0) 279 | offstate = True 280 | utime.sleep(.1) 281 | lastupdate = now 282 | lasterror = error 283 | except Exception as e: 284 | # Put something to output to OLED screen 285 | beanaproblem('error.') 286 | print('error encountered:'+str(e)) 287 | utime.sleep(checkin) 288 | else: 289 | refresh(ssd, True) # Clear any prior image 290 | relaypin = Pin(15, mode=Pin.OUT, value=0) 291 | await asyncio.sleep(.01) 292 | 293 | asyncio.run(main()) 294 | -------------------------------------------------------------------------------- /primitives/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py Common functions for uasyncio primitives 2 | 3 | # Copyright (c) 2018-2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | try: 7 | import uasyncio as asyncio 8 | except ImportError: 9 | import asyncio 10 | 11 | 12 | async def _g(): 13 | pass 14 | type_coro = type(_g()) 15 | 16 | # If a callback is passed, run it and return. 17 | # If a coro is passed initiate it and return. 18 | # coros are passed by name i.e. not using function call syntax. 19 | def launch(func, tup_args): 20 | res = func(*tup_args) 21 | if isinstance(res, type_coro): 22 | res = asyncio.create_task(res) 23 | return res 24 | 25 | def set_global_exception(): 26 | def _handle_exception(loop, context): 27 | import sys 28 | sys.print_exception(context["exception"]) 29 | sys.exit() 30 | loop = asyncio.get_event_loop() 31 | loop.set_exception_handler(_handle_exception) 32 | 33 | _attrs = { 34 | "AADC": "aadc", 35 | "Barrier": "barrier", 36 | "Condition": "condition", 37 | "Delay_ms": "delay_ms", 38 | "Encode": "encoder_async", 39 | "Message": "message", 40 | "Pushbutton": "pushbutton", 41 | "Queue": "queue", 42 | "Semaphore": "semaphore", 43 | "BoundedSemaphore": "semaphore", 44 | "Switch": "switch", 45 | } 46 | 47 | # Copied from uasyncio.__init__.py 48 | # Lazy loader, effectively does: 49 | # global attr 50 | # from .mod import attr 51 | def __getattr__(attr): 52 | mod = _attrs.get(attr, None) 53 | if mod is None: 54 | raise AttributeError(attr) 55 | value = getattr(__import__(mod, None, None, True, 1), attr) 56 | globals()[attr] = value 57 | return value 58 | -------------------------------------------------------------------------------- /primitives/aadc.py: -------------------------------------------------------------------------------- 1 | # aadc.py AADC (asynchronous ADC) class 2 | 3 | # Copyright (c) 2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import uasyncio as asyncio 7 | import io 8 | 9 | MP_STREAM_POLL_RD = const(1) 10 | MP_STREAM_POLL = const(3) 11 | MP_STREAM_ERROR = const(-1) 12 | 13 | class AADC(io.IOBase): 14 | def __init__(self, adc): 15 | self._adc = adc 16 | self._lower = 0 17 | self._upper = 65535 18 | self._pol = True 19 | self._last = None 20 | self._sreader = asyncio.StreamReader(self) 21 | 22 | def __iter__(self): 23 | b = yield from self._sreader.read(2) 24 | return int.from_bytes(b, 'little') 25 | 26 | def _adcread(self): 27 | self._last = self._adc.read_u16() 28 | return self._last 29 | 30 | def read(self, n): # For use by StreamReader only 31 | return int.to_bytes(self._last, 2, 'little') 32 | 33 | def ioctl(self, req, arg): 34 | ret = MP_STREAM_ERROR 35 | if req == MP_STREAM_POLL: 36 | ret = 0 37 | if arg & MP_STREAM_POLL_RD: 38 | if self._pol ^ (self._lower <= self._adcread() <= self._upper): 39 | ret |= MP_STREAM_POLL_RD 40 | return ret 41 | 42 | # *** API *** 43 | 44 | # If normal will pause until ADC value is in range 45 | # Otherwise will pause until value is out of range 46 | def sense(self, normal): 47 | self._pol = normal 48 | 49 | def read_u16(self, last=False): 50 | if last: 51 | return self._last 52 | return self._adcread() 53 | 54 | # Call syntax: set limits for trigger 55 | # lower is None: leave limits unchanged. 56 | # upper is None: treat lower as relative to current value. 57 | # both have values: treat as absolute limits. 58 | def __call__(self, lower=None, upper=None): 59 | if lower is not None: 60 | if upper is None: # Relative limit 61 | r = self._adcread() if self._last is None else self._last 62 | self._lower = r - lower 63 | self._upper = r + lower 64 | else: # Absolute limits 65 | self._lower = lower 66 | self._upper = upper 67 | return self 68 | -------------------------------------------------------------------------------- /primitives/barrier.py: -------------------------------------------------------------------------------- 1 | # barrier.py 2 | # Copyright (c) 2018-2020 Peter Hinch 3 | # Released under the MIT License (MIT) - see LICENSE file 4 | 5 | # Now uses Event rather than polling. 6 | 7 | try: 8 | import uasyncio as asyncio 9 | except ImportError: 10 | import asyncio 11 | 12 | from . import launch 13 | 14 | # A Barrier synchronises N coros. Each issues await barrier. 15 | # Execution pauses until all other participant coros are waiting on it. 16 | # At that point the callback is executed. Then the barrier is 'opened' and 17 | # execution of all participants resumes. 18 | 19 | class Barrier(): 20 | def __init__(self, participants, func=None, args=()): 21 | self._participants = participants 22 | self._count = participants 23 | self._func = func 24 | self._args = args 25 | self._res = None 26 | self._evt = asyncio.Event() 27 | 28 | def __await__(self): 29 | if self.trigger(): 30 | return # Other tasks have already reached barrier 31 | await self._evt.wait() # Wait until last task reaches it 32 | 33 | __iter__ = __await__ 34 | 35 | def result(self): 36 | return self._res 37 | 38 | def trigger(self): 39 | self._count -=1 40 | if self._count < 0: 41 | raise ValueError('Too many tasks accessing Barrier') 42 | if self._count > 0: 43 | return False # At least 1 other task has not reached barrier 44 | # All other tasks are waiting 45 | if self._func is not None: 46 | self._res = launch(self._func, self._args) 47 | self._count = self._participants 48 | self._evt.set() # Release others 49 | self._evt.clear() 50 | return True 51 | 52 | def busy(self): 53 | return self._count < self._participants 54 | -------------------------------------------------------------------------------- /primitives/condition.py: -------------------------------------------------------------------------------- 1 | # condition.py 2 | 3 | # Copyright (c) 2018-2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | try: 7 | import uasyncio as asyncio 8 | except ImportError: 9 | import asyncio 10 | 11 | # Condition class 12 | # from primitives.condition import Condition 13 | 14 | class Condition(): 15 | def __init__(self, lock=None): 16 | self.lock = asyncio.Lock() if lock is None else lock 17 | self.events = [] 18 | 19 | async def acquire(self): 20 | await self.lock.acquire() 21 | 22 | # enable this syntax: 23 | # with await condition [as cond]: 24 | def __await__(self): 25 | await self.lock.acquire() 26 | return self 27 | 28 | __iter__ = __await__ 29 | 30 | def __enter__(self): 31 | return self 32 | 33 | def __exit__(self, *_): 34 | self.lock.release() 35 | 36 | def locked(self): 37 | return self.lock.locked() 38 | 39 | def release(self): 40 | self.lock.release() # Will raise RuntimeError if not locked 41 | 42 | def notify(self, n=1): # Caller controls lock 43 | if not self.lock.locked(): 44 | raise RuntimeError('Condition notify with lock not acquired.') 45 | for _ in range(min(n, len(self.events))): 46 | ev = self.events.pop() 47 | ev.set() 48 | 49 | def notify_all(self): 50 | self.notify(len(self.events)) 51 | 52 | async def wait(self): 53 | if not self.lock.locked(): 54 | raise RuntimeError('Condition wait with lock not acquired.') 55 | ev = asyncio.Event() 56 | self.events.append(ev) 57 | self.lock.release() 58 | await ev.wait() 59 | await self.lock.acquire() 60 | assert ev not in self.events, 'condition wait assertion fail' 61 | return True # CPython compatibility 62 | 63 | async def wait_for(self, predicate): 64 | result = predicate() 65 | while not result: 66 | await self.wait() 67 | result = predicate() 68 | return result 69 | -------------------------------------------------------------------------------- /primitives/delay_ms.py: -------------------------------------------------------------------------------- 1 | # delay_ms.py Now uses ThreadSafeFlag and has extra .wait() API 2 | # Usage: 3 | # from primitives.delay_ms import Delay_ms 4 | 5 | # Copyright (c) 2018-2021 Peter Hinch 6 | # Released under the MIT License (MIT) - see LICENSE file 7 | 8 | import uasyncio as asyncio 9 | from utime import ticks_add, ticks_diff, ticks_ms 10 | from . import launch 11 | 12 | class Delay_ms: 13 | 14 | class DummyTimer: # Stand-in for the timer class. Can be cancelled. 15 | def cancel(self): 16 | pass 17 | _fake = DummyTimer() 18 | 19 | def __init__(self, func=None, args=(), duration=1000): 20 | self._func = func 21 | self._args = args 22 | self._durn = duration # Default duration 23 | self._retn = None # Return value of launched callable 24 | self._tend = None # Stop time (absolute ms). 25 | self._busy = False 26 | self._trig = asyncio.ThreadSafeFlag() 27 | self._tout = asyncio.Event() # Timeout event 28 | self.wait = self._tout.wait # Allow: await wait_ms.wait() 29 | self._ttask = self._fake # Timer task 30 | self._mtask = asyncio.create_task(self._run()) #Main task 31 | 32 | async def _run(self): 33 | while True: 34 | await self._trig.wait() # Await a trigger 35 | self._ttask.cancel() # Cancel and replace 36 | await asyncio.sleep_ms(0) 37 | dt = max(ticks_diff(self._tend, ticks_ms()), 0) # Beware already elapsed. 38 | self._ttask = asyncio.create_task(self._timer(dt)) 39 | 40 | async def _timer(self, dt): 41 | await asyncio.sleep_ms(dt) 42 | self._tout.set() # Only gets here if not cancelled. 43 | self._tout.clear() 44 | self._busy = False 45 | if self._func is not None: 46 | self._retn = launch(self._func, self._args) 47 | 48 | # API 49 | # trigger may be called from hard ISR. 50 | def trigger(self, duration=0): # Update absolute end time, 0-> ctor default 51 | if self._mtask is None: 52 | raise RuntimeError("Delay_ms.deinit() has run.") 53 | self._tend = ticks_add(ticks_ms(), duration if duration > 0 else self._durn) 54 | self._retn = None # Default in case cancelled. 55 | self._busy = True 56 | self._trig.set() 57 | 58 | def stop(self): 59 | self._ttask.cancel() 60 | self._ttask = self._fake 61 | self._busy = False 62 | 63 | def __call__(self): # Current running status 64 | return self._busy 65 | 66 | running = __call__ 67 | 68 | def rvalue(self): 69 | return self._retn 70 | 71 | def callback(self, func=None, args=()): 72 | self._func = func 73 | self._args = args 74 | 75 | def deinit(self): 76 | self.stop() 77 | self._mtask.cancel() 78 | self._mtask = None 79 | -------------------------------------------------------------------------------- /primitives/encoder.py: -------------------------------------------------------------------------------- 1 | # encoder.py Asynchronous driver for incremental quadrature encoder. 2 | 3 | # Copyright (c) 2021-2022 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | # Thanks are due to @ilium007 for identifying the issue of tracking detents, 7 | # https://github.com/peterhinch/micropython-async/issues/82. 8 | # Also to Mike Teachman (@miketeachman) for design discussions and testing 9 | # against a state table design 10 | # https://github.com/miketeachman/micropython-rotary/blob/master/rotary.py 11 | 12 | import uasyncio as asyncio 13 | from machine import Pin 14 | 15 | class Encoder: 16 | 17 | def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, 18 | mod=None, callback=lambda a, b : None, args=(), delay=100): 19 | self._pin_x = pin_x 20 | self._pin_y = pin_y 21 | self._x = pin_x() 22 | self._y = pin_y() 23 | self._v = v * div # Initialise hardware value 24 | self._cv = v # Current (divided) value 25 | self.delay = delay # Pause (ms) for motion to stop/limit callback frequency 26 | 27 | if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): 28 | raise ValueError('Incompatible args: must have vmin <= v <= vmax') 29 | self._tsf = asyncio.ThreadSafeFlag() 30 | trig = Pin.IRQ_RISING | Pin.IRQ_FALLING 31 | try: 32 | xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True) 33 | yirq = pin_y.irq(trigger=trig, handler=self._y_cb, hard=True) 34 | except TypeError: # hard arg is unsupported on some hosts 35 | xirq = pin_x.irq(trigger=trig, handler=self._x_cb) 36 | yirq = pin_y.irq(trigger=trig, handler=self._y_cb) 37 | asyncio.create_task(self._run(vmin, vmax, div, mod, callback, args)) 38 | 39 | # Hardware IRQ's. Duration 36μs on Pyboard 1 ~50μs on ESP32. 40 | # IRQ latency: 2nd edge may have occured by the time ISR runs, in 41 | # which case there is no movement. 42 | def _x_cb(self, pin_x): 43 | if (x := pin_x()) != self._x: 44 | self._x = x 45 | self._v += 1 if x ^ self._pin_y() else -1 46 | self._tsf.set() 47 | 48 | def _y_cb(self, pin_y): 49 | if (y := pin_y()) != self._y: 50 | self._y = y 51 | self._v -= 1 if y ^ self._pin_x() else -1 52 | self._tsf.set() 53 | 54 | async def _run(self, vmin, vmax, div, mod, cb, args): 55 | pv = self._v # Prior hardware value 56 | pcv = self._cv # Prior divided value passed to callback 57 | lcv = pcv # Current value after limits applied 58 | plcv = pcv # Previous value after limits applied 59 | delay = self.delay 60 | while True: 61 | await self._tsf.wait() 62 | await asyncio.sleep_ms(delay) # Wait for motion to stop. 63 | hv = self._v # Sample hardware (atomic read). 64 | if hv == pv: # A change happened but was negated before 65 | continue # this got scheduled. Nothing to do. 66 | pv = hv 67 | cv = round(hv / div) # cv is divided value. 68 | if not (dv := cv - pcv): # dv is change in divided value. 69 | continue # No change 70 | lcv += dv # lcv: divided value with limits/mod applied 71 | lcv = lcv if vmax is None else min(vmax, lcv) 72 | lcv = lcv if vmin is None else max(vmin, lcv) 73 | lcv = lcv if mod is None else lcv % mod 74 | self._cv = lcv # update ._cv for .value() before CB. 75 | if lcv != plcv: 76 | cb(lcv, lcv - plcv, *args) # Run user CB in uasyncio context 77 | pcv = cv 78 | plcv = lcv 79 | 80 | def value(self): 81 | return self._cv 82 | -------------------------------------------------------------------------------- /primitives/message.py: -------------------------------------------------------------------------------- 1 | # message.py 2 | # Now uses ThreadSafeFlag for efficiency 3 | 4 | # Copyright (c) 2018-2021 Peter Hinch 5 | # Released under the MIT License (MIT) - see LICENSE file 6 | 7 | # Usage: 8 | # from primitives.message import Message 9 | 10 | try: 11 | import uasyncio as asyncio 12 | except ImportError: 13 | import asyncio 14 | 15 | # A coro waiting on a message issues await message 16 | # A coro or hard/soft ISR raising the message issues.set(payload) 17 | # .clear() should be issued by at least one waiting task and before 18 | # next event. 19 | 20 | class Message(asyncio.ThreadSafeFlag): 21 | def __init__(self, _=0): # Arg: poll interval. Compatibility with old code. 22 | self._evt = asyncio.Event() 23 | self._data = None # Message 24 | self._state = False # Ensure only one task waits on ThreadSafeFlag 25 | self._is_set = False # For .is_set() 26 | super().__init__() 27 | 28 | def clear(self): # At least one task must call clear when scheduled 29 | self._state = False 30 | self._is_set = False 31 | 32 | def __iter__(self): 33 | yield from self.wait() 34 | return self._data 35 | 36 | async def wait(self): 37 | if self._state: # A task waits on ThreadSafeFlag 38 | await self._evt.wait() # Wait on event 39 | else: # First task to wait 40 | self._state = True 41 | # Ensure other tasks see updated ._state before they wait 42 | await asyncio.sleep_ms(0) 43 | await super().wait() # Wait on ThreadSafeFlag 44 | self._evt.set() 45 | self._evt.clear() 46 | return self._data 47 | 48 | def set(self, data=None): # Can be called from a hard ISR 49 | self._data = data 50 | self._is_set = True 51 | super().set() 52 | 53 | def is_set(self): 54 | return self._is_set 55 | 56 | def value(self): 57 | return self._data 58 | -------------------------------------------------------------------------------- /primitives/pushbutton.py: -------------------------------------------------------------------------------- 1 | # pushbutton.py 2 | 3 | # Copyright (c) 2018-2022 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import uasyncio as asyncio 7 | import utime as time 8 | from . import launch 9 | from primitives.delay_ms import Delay_ms 10 | 11 | 12 | # An alternative Pushbutton solution with lower RAM use is available here 13 | # https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py 14 | class Pushbutton: 15 | debounce_ms = 50 16 | long_press_ms = 1000 17 | double_click_ms = 400 18 | def __init__(self, pin, suppress=False, sense=None): 19 | self.pin = pin # Initialise for input 20 | self._supp = suppress 21 | self._dblpend = False # Doubleclick waiting for 2nd click 22 | self._dblran = False # Doubleclick executed user function 23 | self._tf = False 24 | self._ff = False 25 | self._df = False 26 | self._ld = False # Delay_ms instance for long press 27 | self._dd = False # Ditto for doubleclick 28 | self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value 29 | self.state = self.rawstate() # Initial state 30 | self._run = asyncio.create_task(self.buttoncheck()) # Thread runs forever 31 | 32 | def press_func(self, func=False, args=()): 33 | self._tf = func 34 | self._ta = args 35 | 36 | def release_func(self, func=False, args=()): 37 | self._ff = func 38 | self._fa = args 39 | 40 | def double_func(self, func=False, args=()): 41 | self._df = func 42 | self._da = args 43 | if func: # If double timer already in place, leave it 44 | if not self._dd: 45 | self._dd = Delay_ms(self._ddto) 46 | else: 47 | self._dd = False # Clearing down double func 48 | 49 | def long_func(self, func=False, args=()): 50 | if func: 51 | if self._ld: 52 | self._ld.callback(func, args) 53 | else: 54 | self._ld = Delay_ms(func, args) 55 | else: 56 | self._ld = False 57 | 58 | # Current non-debounced logical button state: True == pressed 59 | def rawstate(self): 60 | return bool(self.pin.value() ^ self.sense) 61 | 62 | # Current debounced state of button (True == pressed) 63 | def __call__(self): 64 | return self.state 65 | 66 | def _ddto(self): # Doubleclick timeout: no doubleclick occurred 67 | self._dblpend = False 68 | if self._supp and not self.state: 69 | if not self._ld or (self._ld and not self._ld()): 70 | launch(self._ff, self._fa) 71 | 72 | async def buttoncheck(self): 73 | while True: 74 | state = self.rawstate() 75 | # State has changed: act on it now. 76 | if state != self.state: 77 | self.state = state 78 | if state: # Button pressed: launch pressed func 79 | if self._tf: 80 | launch(self._tf, self._ta) 81 | if self._ld: # There's a long func: start long press delay 82 | self._ld.trigger(Pushbutton.long_press_ms) 83 | if self._df: 84 | if self._dd(): # Second click: timer running 85 | self._dd.stop() 86 | self._dblpend = False 87 | self._dblran = True # Prevent suppressed launch on release 88 | launch(self._df, self._da) 89 | else: 90 | # First click: start doubleclick timer 91 | self._dd.trigger(Pushbutton.double_click_ms) 92 | self._dblpend = True # Prevent suppressed launch on release 93 | else: # Button release. Is there a release func? 94 | if self._ff: 95 | if self._supp: 96 | d = self._ld 97 | # If long delay exists, is running and doubleclick status is OK 98 | if not self._dblpend and not self._dblran: 99 | if (d and d()) or not d: 100 | launch(self._ff, self._fa) 101 | else: 102 | launch(self._ff, self._fa) 103 | if self._ld: 104 | self._ld.stop() # Avoid interpreting a second click as a long push 105 | self._dblran = False 106 | # Ignore state changes until switch has settled 107 | # See https://github.com/peterhinch/micropython-async/issues/69 108 | await asyncio.sleep_ms(Pushbutton.debounce_ms) 109 | 110 | def deinit(self): 111 | self._run.cancel() 112 | -------------------------------------------------------------------------------- /primitives/queue.py: -------------------------------------------------------------------------------- 1 | # queue.py: adapted from uasyncio V2 2 | 3 | # Copyright (c) 2018-2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | # Code is based on Paul Sokolovsky's work. 7 | # This is a temporary solution until uasyncio V3 gets an efficient official version 8 | 9 | import uasyncio as asyncio 10 | 11 | 12 | # Exception raised by get_nowait(). 13 | class QueueEmpty(Exception): 14 | pass 15 | 16 | 17 | # Exception raised by put_nowait(). 18 | class QueueFull(Exception): 19 | pass 20 | 21 | class Queue: 22 | 23 | def __init__(self, maxsize=0): 24 | self.maxsize = maxsize 25 | self._queue = [] 26 | self._evput = asyncio.Event() # Triggered by put, tested by get 27 | self._evget = asyncio.Event() # Triggered by get, tested by put 28 | 29 | def _get(self): 30 | self._evget.set() # Schedule all tasks waiting on get 31 | self._evget.clear() 32 | return self._queue.pop(0) 33 | 34 | async def get(self): # Usage: item = await queue.get() 35 | while self.empty(): # May be multiple tasks waiting on get() 36 | # Queue is empty, suspend task until a put occurs 37 | # 1st of N tasks gets, the rest loop again 38 | await self._evput.wait() 39 | return self._get() 40 | 41 | def get_nowait(self): # Remove and return an item from the queue. 42 | # Return an item if one is immediately available, else raise QueueEmpty. 43 | if self.empty(): 44 | raise QueueEmpty() 45 | return self._get() 46 | 47 | def _put(self, val): 48 | self._evput.set() # Schedule tasks waiting on put 49 | self._evput.clear() 50 | self._queue.append(val) 51 | 52 | async def put(self, val): # Usage: await queue.put(item) 53 | while self.full(): 54 | # Queue full 55 | await self._evget.wait() 56 | # Task(s) waiting to get from queue, schedule first Task 57 | self._put(val) 58 | 59 | def put_nowait(self, val): # Put an item into the queue without blocking. 60 | if self.full(): 61 | raise QueueFull() 62 | self._put(val) 63 | 64 | def qsize(self): # Number of items in the queue. 65 | return len(self._queue) 66 | 67 | def empty(self): # Return True if the queue is empty, False otherwise. 68 | return len(self._queue) == 0 69 | 70 | def full(self): # Return True if there are maxsize items in the queue. 71 | # Note: if the Queue was initialized with maxsize=0 (the default) or 72 | # any negative number, then full() is never True. 73 | return self.maxsize > 0 and self.qsize() >= self.maxsize 74 | -------------------------------------------------------------------------------- /primitives/semaphore.py: -------------------------------------------------------------------------------- 1 | # semaphore.py 2 | 3 | # Copyright (c) 2018-2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | try: 7 | import uasyncio as asyncio 8 | except ImportError: 9 | import asyncio 10 | 11 | # A Semaphore is typically used to limit the number of coros running a 12 | # particular piece of code at once. The number is defined in the constructor. 13 | class Semaphore(): 14 | def __init__(self, value=1): 15 | self._count = value 16 | self._event = asyncio.Event() 17 | 18 | async def __aenter__(self): 19 | await self.acquire() 20 | return self 21 | 22 | async def __aexit__(self, *args): 23 | self.release() 24 | await asyncio.sleep(0) 25 | 26 | async def acquire(self): 27 | self._event.clear() 28 | while self._count == 0: # Multiple tasks may be waiting for 29 | await self._event.wait() # a release 30 | self._event.clear() 31 | # When we yield, another task may succeed. In this case 32 | await asyncio.sleep(0) # the loop repeats 33 | self._count -= 1 34 | 35 | def release(self): 36 | self._event.set() 37 | self._count += 1 38 | 39 | class BoundedSemaphore(Semaphore): 40 | def __init__(self, value=1): 41 | super().__init__(value) 42 | self._initial_value = value 43 | 44 | def release(self): 45 | if self._count < self._initial_value: 46 | super().release() 47 | else: 48 | raise ValueError('Semaphore released more than acquired') 49 | -------------------------------------------------------------------------------- /primitives/switch.py: -------------------------------------------------------------------------------- 1 | # switch.py 2 | 3 | # Copyright (c) 2018-2022 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import uasyncio as asyncio 7 | import utime as time 8 | from . import launch 9 | 10 | class Switch: 11 | debounce_ms = 50 12 | def __init__(self, pin): 13 | self.pin = pin # Should be initialised for input with pullup 14 | self._open_func = False 15 | self._close_func = False 16 | self.switchstate = self.pin.value() # Get initial state 17 | self._run = asyncio.create_task(self.switchcheck()) # Thread runs forever 18 | 19 | def open_func(self, func, args=()): 20 | self._open_func = func 21 | self._open_args = args 22 | 23 | def close_func(self, func, args=()): 24 | self._close_func = func 25 | self._close_args = args 26 | 27 | # Return current state of switch (0 = pressed) 28 | def __call__(self): 29 | return self.switchstate 30 | 31 | async def switchcheck(self): 32 | while True: 33 | state = self.pin.value() 34 | if state != self.switchstate: 35 | # State has changed: act on it now. 36 | self.switchstate = state 37 | if state == 0 and self._close_func: 38 | launch(self._close_func, self._close_args) 39 | elif state == 1 and self._open_func: 40 | launch(self._open_func, self._open_args) 41 | # Ignore further state changes until switch has settled 42 | await asyncio.sleep_ms(Switch.debounce_ms) 43 | 44 | def deinit(self): 45 | self._run.cancel() 46 | -------------------------------------------------------------------------------- /primitives/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/primitives/tests/__init__.py -------------------------------------------------------------------------------- /primitives/tests/adctest.py: -------------------------------------------------------------------------------- 1 | # adctest.py 2 | 3 | # Copyright (c) 2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import uasyncio as asyncio 7 | from machine import ADC 8 | import pyb 9 | from primitives import AADC 10 | 11 | async def signal(): # Could use write_timed but this prints values 12 | dac = pyb.DAC(1, bits=12, buffering=True) 13 | v = 0 14 | while True: 15 | if not v & 0xf: 16 | print('write', v << 4) # Make value u16 as per ADC read 17 | dac.write(v) 18 | v += 1 19 | v %= 4096 20 | await asyncio.sleep_ms(50) 21 | 22 | async def adctest(): 23 | asyncio.create_task(signal()) 24 | adc = AADC(ADC(pyb.Pin.board.X1)) 25 | await asyncio.sleep(0) 26 | adc.sense(normal=False) # Wait until ADC gets to 5000 27 | value = await adc(5000, 10000) 28 | print('Received', value, adc.read_u16(True)) # Reduce to 12 bits 29 | adc.sense(normal=True) # Now print all changes > 2000 30 | while True: 31 | value = await adc(2000) # Trigger if value changes by 2000 32 | print('Received', value, adc.read_u16(True)) 33 | 34 | st = '''This test requires a Pyboard with pins X1 and X5 linked. 35 | A sawtooth waveform is applied to the ADC. Initially the test waits 36 | until the ADC value reaches 5000. It then reports whenever the value 37 | changes by 2000. 38 | Issue test() to start. 39 | ''' 40 | print(st) 41 | 42 | def test(): 43 | try: 44 | asyncio.run(adctest()) 45 | except KeyboardInterrupt: 46 | print('Interrupted') 47 | finally: 48 | asyncio.new_event_loop() 49 | print() 50 | print(st) 51 | -------------------------------------------------------------------------------- /primitives/tests/delay_test.py: -------------------------------------------------------------------------------- 1 | # delay_test.py Tests for Delay_ms class 2 | 3 | # Copyright (c) 2020 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | import uasyncio as asyncio 7 | import micropython 8 | from primitives.delay_ms import Delay_ms 9 | 10 | micropython.alloc_emergency_exception_buf(100) 11 | 12 | def printexp(exp, runtime=0): 13 | print('Expected output:') 14 | print('\x1b[32m') 15 | print(exp) 16 | print('\x1b[39m') 17 | if runtime: 18 | print('Running (runtime = {}s):'.format(runtime)) 19 | else: 20 | print('Running (runtime < 1s):') 21 | 22 | async def ctor_test(): # Constructor arg 23 | s = ''' 24 | Trigger 5 sec delay 25 | Retrigger 5 sec delay 26 | Callback should run 27 | cb callback 28 | Done 29 | ''' 30 | printexp(s, 12) 31 | def cb(v): 32 | print('cb', v) 33 | 34 | d = Delay_ms(cb, ('callback',), duration=5000) 35 | 36 | print('Trigger 5 sec delay') 37 | d.trigger() 38 | await asyncio.sleep(4) 39 | print('Retrigger 5 sec delay') 40 | d.trigger() 41 | await asyncio.sleep(4) 42 | print('Callback should run') 43 | await asyncio.sleep(2) 44 | print('Done') 45 | 46 | async def launch_test(): 47 | s = ''' 48 | Trigger 5 sec delay 49 | Coroutine should run: run to completion. 50 | Coroutine starts 51 | Coroutine ends 52 | Coroutine should run: test cancellation. 53 | Coroutine starts 54 | Coroutine should run: test awaiting. 55 | Coroutine starts 56 | Coroutine ends 57 | Done 58 | ''' 59 | printexp(s, 20) 60 | async def cb(v, ms): 61 | print(v, 'starts') 62 | await asyncio.sleep_ms(ms) 63 | print(v, 'ends') 64 | 65 | d = Delay_ms(cb, ('coroutine', 1000)) 66 | 67 | print('Trigger 5 sec delay') 68 | d.trigger(5000) # Test extending time 69 | await asyncio.sleep(4) 70 | print('Coroutine should run: run to completion.') 71 | await asyncio.sleep(3) 72 | d = Delay_ms(cb, ('coroutine', 3000)) 73 | d.trigger(5000) 74 | await asyncio.sleep(4) 75 | print('Coroutine should run: test cancellation.') 76 | await asyncio.sleep(2) 77 | coro = d.rvalue() 78 | coro.cancel() 79 | d.trigger(5000) 80 | await asyncio.sleep(4) 81 | print('Coroutine should run: test awaiting.') 82 | await asyncio.sleep(2) 83 | coro = d.rvalue() 84 | await coro 85 | print('Done') 86 | 87 | 88 | async def reduce_test(): # Test reducing a running delay 89 | s = ''' 90 | Trigger 5 sec delay 91 | Callback should run 92 | cb callback 93 | Callback should run 94 | cb callback 95 | Done 96 | ''' 97 | printexp(s, 11) 98 | def cb(v): 99 | print('cb', v) 100 | 101 | d = Delay_ms(cb, ('callback',)) 102 | 103 | print('Trigger 5 sec delay') 104 | d.trigger(5000) # Test extending time 105 | await asyncio.sleep(4) 106 | print('Callback should run') 107 | await asyncio.sleep(2) 108 | d.trigger(10000) 109 | await asyncio.sleep(1) 110 | d.trigger(3000) 111 | await asyncio.sleep(2) 112 | print('Callback should run') 113 | await asyncio.sleep(2) 114 | print('Done') 115 | 116 | 117 | async def stop_test(): # Test the .stop and .running methods 118 | s = ''' 119 | Trigger 5 sec delay 120 | Running 121 | Callback should run 122 | cb callback 123 | Callback returned 42 124 | Callback should not run 125 | Done 126 | ''' 127 | printexp(s, 12) 128 | def cb(v): 129 | print('cb', v) 130 | return 42 131 | 132 | d = Delay_ms(cb, ('callback',)) 133 | 134 | print('Trigger 5 sec delay') 135 | d.trigger(5000) # Test extending time 136 | await asyncio.sleep(4) 137 | if d(): 138 | print('Running') 139 | print('Callback should run') 140 | await asyncio.sleep(2) 141 | print('Callback returned', d.rvalue()) 142 | d.trigger(3000) 143 | await asyncio.sleep(1) 144 | d.stop() 145 | await asyncio.sleep(1) 146 | if d(): 147 | print('Running') 148 | print('Callback should not run') 149 | await asyncio.sleep(4) 150 | print('Done') 151 | 152 | 153 | async def isr_test(): # Test trigger from hard ISR 154 | from pyb import Timer 155 | s = ''' 156 | Timer holds off cb for 5 secs 157 | cb should now run 158 | cb callback 159 | Done 160 | ''' 161 | printexp(s, 6) 162 | def cb(v): 163 | print('cb', v) 164 | 165 | d = Delay_ms(cb, ('callback',)) 166 | 167 | def timer_cb(_): 168 | d.trigger(200) 169 | tim = Timer(1, freq=10, callback=timer_cb) 170 | 171 | print('Timer holds off cb for 5 secs') 172 | await asyncio.sleep(5) 173 | tim.deinit() 174 | print('cb should now run') 175 | await asyncio.sleep(1) 176 | print('Done') 177 | 178 | async def err_test(): # Test triggering de-initialised timer 179 | s = ''' 180 | Running (runtime = 3s): 181 | Trigger 1 sec delay 182 | cb callback 183 | Success: error was raised. 184 | Done 185 | ''' 186 | printexp(s, 3) 187 | def cb(v): 188 | print('cb', v) 189 | return 42 190 | 191 | d = Delay_ms(cb, ('callback',)) 192 | 193 | print('Trigger 1 sec delay') 194 | d.trigger(1000) 195 | await asyncio.sleep(2) 196 | d.deinit() 197 | try: 198 | d.trigger(1000) 199 | except RuntimeError: 200 | print("Success: error was raised.") 201 | print('Done') 202 | 203 | av = ''' 204 | Run a test by issuing 205 | delay_test.test(n) 206 | where n is a test number. Avaliable tests: 207 | \x1b[32m 208 | 0 Test triggering from a hard ISR (Pyboard only) 209 | 1 Test the .stop method and callback return value. 210 | 2 Test reducing the duration of a running timer 211 | 3 Test delay defined by constructor arg 212 | 4 Test triggering a Task 213 | 5 Attempt to trigger de-initialised instance 214 | \x1b[39m 215 | ''' 216 | print(av) 217 | 218 | tests = (isr_test, stop_test, reduce_test, ctor_test, launch_test, err_test) 219 | def test(n=0): 220 | try: 221 | asyncio.run(tests[n]()) 222 | finally: 223 | asyncio.new_event_loop() 224 | -------------------------------------------------------------------------------- /primitives/tests/encoder_stop.py: -------------------------------------------------------------------------------- 1 | # encoder_stop.py Demo of callback which occurs after motion has stopped. 2 | 3 | from machine import Pin 4 | import uasyncio as asyncio 5 | from primitives.encoder import Encoder 6 | from primitives.delay_ms import Delay_ms 7 | 8 | px = Pin('X1', Pin.IN, Pin.PULL_UP) 9 | py = Pin('X2', Pin.IN, Pin.PULL_UP) 10 | 11 | tim = Delay_ms(duration=400) # High value for test 12 | d = 0 13 | 14 | def tcb(pos, delta): # User callback gets args of encoder cb 15 | global d 16 | d = 0 17 | print(pos, delta) 18 | 19 | def cb(pos, delta): # Encoder callback occurs rapidly 20 | global d 21 | tim.trigger() # Postpone the user callback 22 | tim.callback(tcb, (pos, d := d + delta)) # and update its args 23 | 24 | async def main(): 25 | while True: 26 | await asyncio.sleep(1) 27 | 28 | def test(): 29 | print('Running encoder test. Press ctrl-c to teminate.') 30 | Encoder.delay = 0 # No need for this delay 31 | enc = Encoder(px, py, callback=cb) 32 | try: 33 | asyncio.run(main()) 34 | except KeyboardInterrupt: 35 | print('Interrupted') 36 | finally: 37 | asyncio.new_event_loop() 38 | 39 | test() 40 | -------------------------------------------------------------------------------- /primitives/tests/encoder_test.py: -------------------------------------------------------------------------------- 1 | # encoder_test.py Test for asynchronous driver for incremental quadrature encoder. 2 | 3 | # Copyright (c) 2021-2022 Peter Hinch 4 | # Released under the MIT License (MIT) - see LICENSE file 5 | 6 | from machine import Pin 7 | import uasyncio as asyncio 8 | from primitives.encoder import Encoder 9 | 10 | 11 | px = Pin(33, Pin.IN, Pin.PULL_UP) 12 | py = Pin(25, Pin.IN, Pin.PULL_UP) 13 | 14 | def cb(pos, delta): 15 | print(pos, delta) 16 | 17 | async def main(): 18 | while True: 19 | await asyncio.sleep(1) 20 | 21 | def test(): 22 | print('Running encoder test. Press ctrl-c to teminate.') 23 | enc = Encoder(px, py, v=0, vmin=0, vmax=100, callback=cb) 24 | try: 25 | asyncio.run(main()) 26 | except KeyboardInterrupt: 27 | print('Interrupted') 28 | finally: 29 | asyncio.new_event_loop() 30 | 31 | test() 32 | -------------------------------------------------------------------------------- /primitives/tests/switches.py: -------------------------------------------------------------------------------- 1 | # Test/demo programs for Switch and Pushbutton classes 2 | # Tested on Pyboard but should run on other microcontroller platforms 3 | # running MicroPython with uasyncio library. 4 | 5 | # Copyright (c) 2018-2022 Peter Hinch 6 | # Released under the MIT License (MIT) - see LICENSE file 7 | # Now executes .deinit() 8 | 9 | # To run: 10 | # from primitives.tests.switches import * 11 | # test_sw() # For example 12 | 13 | from machine import Pin 14 | from pyb import LED 15 | from primitives import Switch, Pushbutton 16 | import uasyncio as asyncio 17 | 18 | helptext = ''' 19 | Test using switch or pushbutton between X1 and gnd. 20 | Ground pin X2 to terminate test. 21 | 22 | ''' 23 | tests = ''' 24 | Available tests: 25 | test_sw Switch test 26 | test_swcb Switch with callback 27 | test_btn Pushutton launching coros 28 | test_btncb Pushbutton launching callbacks 29 | btn_dynamic Change coros launched at runtime. 30 | ''' 31 | print(tests) 32 | 33 | # Pulse an LED (coroutine) 34 | async def pulse(led, ms): 35 | led.on() 36 | await asyncio.sleep_ms(ms) 37 | led.off() 38 | 39 | # Toggle an LED (callback) 40 | def toggle(led): 41 | led.toggle() 42 | 43 | # Quit test by connecting X2 to ground 44 | async def killer(obj): 45 | pin = Pin('X2', Pin.IN, Pin.PULL_UP) 46 | while pin.value(): 47 | await asyncio.sleep_ms(50) 48 | obj.deinit() 49 | await asyncio.sleep_ms(0) 50 | 51 | def run(obj): 52 | try: 53 | asyncio.run(killer(obj)) 54 | except KeyboardInterrupt: 55 | print('Interrupted') 56 | finally: 57 | asyncio.new_event_loop() 58 | print(tests) 59 | 60 | 61 | # Test for the Switch class passing coros 62 | def test_sw(): 63 | s = ''' 64 | close pulses green 65 | open pulses red 66 | ''' 67 | print('Test of switch scheduling coroutines.') 68 | print(helptext) 69 | print(s) 70 | pin = Pin('X1', Pin.IN, Pin.PULL_UP) 71 | red = LED(1) 72 | green = LED(2) 73 | sw = Switch(pin) 74 | # Register coros to launch on contact close and open 75 | sw.close_func(pulse, (green, 1000)) 76 | sw.open_func(pulse, (red, 1000)) 77 | run(sw) 78 | 79 | # Test for the switch class with a callback 80 | def test_swcb(): 81 | s = ''' 82 | close toggles red 83 | open toggles green 84 | ''' 85 | print('Test of switch executing callbacks.') 86 | print(helptext) 87 | print(s) 88 | pin = Pin('X1', Pin.IN, Pin.PULL_UP) 89 | red = LED(1) 90 | green = LED(2) 91 | sw = Switch(pin) 92 | # Register a coro to launch on contact close 93 | sw.close_func(toggle, (red,)) 94 | sw.open_func(toggle, (green,)) 95 | run(sw) 96 | 97 | # Test for the Pushbutton class (coroutines) 98 | # Pass True to test suppress 99 | def test_btn(suppress=False, lf=True, df=True): 100 | s = ''' 101 | press pulses red 102 | release pulses green 103 | double click pulses yellow 104 | long press pulses blue 105 | ''' 106 | print('Test of pushbutton scheduling coroutines.') 107 | print(helptext) 108 | print(s) 109 | pin = Pin('X1', Pin.IN, Pin.PULL_UP) 110 | red = LED(1) 111 | green = LED(2) 112 | yellow = LED(3) 113 | blue = LED(4) 114 | pb = Pushbutton(pin, suppress) 115 | pb.press_func(pulse, (red, 1000)) 116 | pb.release_func(pulse, (green, 1000)) 117 | if df: 118 | print('Doubleclick enabled') 119 | pb.double_func(pulse, (yellow, 1000)) 120 | if lf: 121 | print('Long press enabled') 122 | pb.long_func(pulse, (blue, 1000)) 123 | run(pb) 124 | 125 | # Test for the Pushbutton class (callbacks) 126 | def test_btncb(): 127 | s = ''' 128 | press toggles red 129 | release toggles green 130 | double click toggles yellow 131 | long press toggles blue 132 | ''' 133 | print('Test of pushbutton executing callbacks.') 134 | print(helptext) 135 | print(s) 136 | pin = Pin('X1', Pin.IN, Pin.PULL_UP) 137 | red = LED(1) 138 | green = LED(2) 139 | yellow = LED(3) 140 | blue = LED(4) 141 | pb = Pushbutton(pin) 142 | pb.press_func(toggle, (red,)) 143 | pb.release_func(toggle, (green,)) 144 | pb.double_func(toggle, (yellow,)) 145 | pb.long_func(toggle, (blue,)) 146 | run(pb) 147 | 148 | # Test for the Pushbutton class where callback coros change dynamically 149 | def setup(pb, press, release, dbl, lng, t=1000): 150 | s = ''' 151 | Functions are changed: 152 | LED's pulse for 2 seconds 153 | press pulses blue 154 | release pulses red 155 | double click pulses green 156 | long pulses yellow 157 | ''' 158 | pb.press_func(pulse, (press, t)) 159 | pb.release_func(pulse, (release, t)) 160 | pb.double_func(pulse, (dbl, t)) 161 | if lng is not None: 162 | pb.long_func(pulse, (lng, t)) 163 | print(s) 164 | 165 | def btn_dynamic(): 166 | s = ''' 167 | press pulses red 168 | release pulses green 169 | double click pulses yellow 170 | long press changes button functions. 171 | ''' 172 | print('Test of pushbutton scheduling coroutines.') 173 | print(helptext) 174 | print(s) 175 | pin = Pin('X1', Pin.IN, Pin.PULL_UP) 176 | red = LED(1) 177 | green = LED(2) 178 | yellow = LED(3) 179 | blue = LED(4) 180 | pb = Pushbutton(pin) 181 | setup(pb, red, green, yellow, None) 182 | pb.long_func(setup, (pb, blue, red, green, yellow, 2000)) 183 | run(pb) 184 | -------------------------------------------------------------------------------- /sploosh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veebch/sploosh/583319c61616ef0ca17b61fe3ae17220b7e7c956/sploosh.jpg --------------------------------------------------------------------------------