From ca99637fb12ac20089161c310a1e00cb07dc113f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 29 Aug 2018 18:16:13 +0100 Subject: [PATCH] Plot module added. Release 0.1 --- README.md | 477 ++++++++++++++++++++++++++++- aclock.py | 104 +++++++ alevel.py | 91 ++++++ arial10.py | 139 +++++++++ color15.py | 191 ++++++++++++ color96.py | 133 ++++++++ courier20.py | 308 +++++++++++++++++++ drivers/ADAFRUIT.md | 100 ++++++ drivers/ssd1331/ssd1331.py | 92 ++++++ drivers/ssd1351/README.md | 20 ++ drivers/ssd1351/ssd1351.py | 160 ++++++++++ drivers/ssd1351/ssd1351_generic.py | 141 +++++++++ drivers/ssd1351/test128_row.py | 18 ++ drivers/ssd1351/test96_row.py | 18 ++ font6.py | 176 +++++++++++ freesans20.py | 288 +++++++++++++++++ images/IMG_2885.png | Bin 0 -> 51652 bytes images/IMG_2887.png | Bin 0 -> 45273 bytes mono_test.py | 116 +++++++ nanogui.py | 385 +++++++++++++++++++++++ plot/FPLOT.md | 267 ++++++++++++++++ plot/fplot.py | 272 ++++++++++++++++ plot/fpt.py | 214 +++++++++++++ 23 files changed, 3708 insertions(+), 2 deletions(-) create mode 100644 aclock.py create mode 100644 alevel.py create mode 100644 arial10.py create mode 100644 color15.py create mode 100644 color96.py create mode 100644 courier20.py create mode 100644 drivers/ADAFRUIT.md create mode 100644 drivers/ssd1331/ssd1331.py create mode 100644 drivers/ssd1351/README.md create mode 100644 drivers/ssd1351/ssd1351.py create mode 100644 drivers/ssd1351/ssd1351_generic.py create mode 100644 drivers/ssd1351/test128_row.py create mode 100644 drivers/ssd1351/test96_row.py create mode 100644 font6.py create mode 100644 freesans20.py create mode 100644 images/IMG_2885.png create mode 100644 images/IMG_2887.png create mode 100644 mono_test.py create mode 100644 nanogui.py create mode 100644 plot/FPLOT.md create mode 100644 plot/fplot.py create mode 100644 plot/fpt.py diff --git a/README.md b/README.md index b2c6411..c468afc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,475 @@ -# micropython-nano-gui -A lightweight MicroPython GUI library for display drivers based on framebuf class +A lightweight and minimal MicroPython GUI library for display drivers based on +the `framebuf` class. With the exception of the Nokia 5110, such drivers are +currently for color and monochrome OLED displays. This is coincidental. + +These images don't do justice to the OLED displays which are visually +impressive with bright colors and extreme contrast. For some reason they are +quite hard to photograph. +![Image](images/IMG_2885.png) +One of the demos running on an Adafruit 1.27 inch OLED. The colors change +dynamically with low values showing green, intermediate yellow and high red. In +reality the colors are vivid: the right hand meter bar and LED are a brilliant +yellow and the centre one is vivid green. + +![Image](images/IMG_2887.png) +The Dial object. + +There is an optional [graph plotting module](./plot/FPLOT.md) for basic +Cartesian and polar plots, also realtime plotting including time series. + +Notes on [Adafruit and other OLED displays](./drivers/ADAFRUIT.md) including +wiring details, pin names and hardware issues. + +**NOTE** 20 Sep 2018 Under development. I am now reasonably happy with the API +but I can't yet promise no changes. There may, of course, be bugs... + +# Contents + + 1. [Introduction](./README.md#1-introduction) + 2. [Files and Dependencies](./README.md#2-files-and-dependencies) + 2.1 [Dependencies](./README.md#21-dependencies) + 2.2.1 [Monochrome use](./README.md#211-monochrome-use) + 2.2.2 [Color use](./README.md#222-color-use) + 3. [The nanogui module](./README.md#3-the-nanogui-module) + 3.1 [Initialisation](./README.md#31-initialisation) Initial setup and refresh method. + 3.2 [Label class](./README.md#32-label-class) Dynamic text at any screen location. + 3.3 [Meter class](./README.md#33-meter-class) A vertical panel meter. + 3.4 [LED class](./README.md#34-led-class) Virtual LED of any color. + 3.5 [Dial and Pointer classes](./README.md#35-dial-and-pointer-classes) Clock + or compass style display of one or more pointers. + 4. [Device drivers](./README.md#4-device-drivers) Device driver compatibility + requirements (these are minimal). + +# 1. Introduction + +This library provides a limited set of GUI objects (widgets) for displays whose +display driver is subclassed from the `framebuf` class. Examples are: + + * The official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). + * The [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git). + * The [Adafruit 0.96 inch color OLED](https://www.adafruit.com/product/684) + with [this driver](https://github.com/peterhinch/micropython-nano-gui/tree/master/drivers/ssd1331). + * A driver for [Adafruit 1.5 inch OLED](https://www.adafruit.com/product/1431) + and [Adafruit 1.27 inch OLED](https://www.adafruit.com/product/1673) may be + found [here](./drivers/ssd1351/README.md). + +Widgets are intended for the display of data from physical devices such as +sensors. The GUI is display-only: there is no provision for user input. This +is because there are no `frmebuf`- based display drivers for screens with a +touch overlay. Authors of applications requiring input should consider my touch +GUI's for the official lcd160cr or for SSD1963 based displays. + +Widgets are drawn using graphics primitives rather than icons to minimise RAM +usage. It also enables them to be effciently rendered at arbitrary scale on +devices with restricted processing power. + +Owing to RAM requirements and limitations on communication speed, `framebuf` +based display drivers are intended for physically small displays with limited +numbers of pixels. The widgets are designed for displays as small as 0.96 +inches: this involves some compromises. They aim to maximise the information +on screen by offering the option of dynamically changing colors. + +Copying the contents of the frame buffer to the display is relatively slow. The +time depends on the size of the frame buffer and the interface speed, but the +latency may be too high for applications such as games. For example the time to +update a 128*128*8 color ssd1351 display on a Pyboard 1.0 is 41ms. + +Drivers based on `framebuf` must allocate contiguous RAM for the buffer. To +avoid 'out of memory' errors it is best to instantiate the display early, +possibly before importing many other modules. The `aclock.py` and `alevel.py` +demos illustrate this. + +# 2. Files and Dependencies + +## 2.1 Files + + * `nanogui.py` The library. + * `mono_test.py` Tests/demos using the official SSD1306 library for a + monochrome 128*64 OLED display. + * `color96.py` Tests/demos for the Adafruit 0.96 inch color OLED. + * `color15.py` Similar for Adafruit 1.27 inch and 1.5 inch color OLEDs. Edit + the `height = 96` line as per the comment for the larger display. + +Demos for Adafruit 1.27 inch and 1.5 inch color OLEDs. Edit the `height = 96` +line as per the code comment for the larger display. + * `aclock.py` Analog clock demo. + * `alevel.py` Spirit level using Pyboard accelerometer. + +Sample fonts created by [font_to_py.py](https://github.com/peterhinch/micropython-font-to-py.git): + * `arial10.py` + * `courier20.py` + * `font6.py` + * `freesans20.py` + +## 2.2 Dependencies + +All applicatons require a device driver for the display in use plus any Python +font files in use. The following is required by all applications: + + * [writer.py](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/writer.py) + Provides text rendering. + +### 2.2.1 Monochrome use + +OLED displays using the SSD1306 chip require: + * [ssd1306_setup.py](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/ssd1306_setup.py) + Contains wiring information. + * The official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). + +Displays based on the PCD8544 chip require: + * [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git) + +### 2.2.2 Color use + +Supported displays amd their drivers are listed below: + + * [Adafruit 0.96 inch color OLED](https://github.com/peterhinch/micropython-nano-gui/tree/master/drivers/ssd1331). + Driver for SSD1331 controller. + * [Adafruit 1.5 and 1.27 inch color OLEDs](./drivers/ssd1351/README.md) + Driver for SSD1351 controller. + +Test script for Adafruit 1.5 and 1.27 inch color OLED displays. It's a good +idea to paste this at the REPL to ensure the display is working before +progressing to the GUI. Remember to change `height` if using the 1.5 inch +display. +```python +import machine +from ssd1351 import SSD1351 as SSD + +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +ssd = SSD(spi, pcs, pdc, prst, height=96) # Ensure height is correct (96/128) +ssd.fill(0) +ssd.line(0, 0, 127, 95, ssd.rgb(0, 255, 0)) # Green diagonal corner-to-corner +ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) # Red square at top left +ssd.show() +``` + +###### [Contents](./README.md#contents) + +# 3. The nanogui module + +This supports widgets whose text components are drawn using the `Writer` +(monochrome) or `CWriter` (colour) classes. Upside down rendering is not +supported: attempts to specify it will produce unexpected results. + +Widgets are drawn at specific locations on screen and are incompatible with the +display of scrolling text: they are therefore not intended for use with the +`Writer.printstring` method. The coordinates of a widget are those of its top +left corner. If a border is specified, this is drawn outside of the limits of +the widgets with a margin of 2 pixels. If the widget is placed at [row, col] +the top left hand corner of the border is at [row-2, col-2]. + +When a widget is drawn or updated (typically with its `value` method) it is not +immediately displayed. To update the display `nanogui.refresh` is called: this +ensures that the `framebuf` contents are updated before copying the contents to +the display. This postponement is for performance reasons and to provide the +appearance of a rapid update. + +## 3.1 Initialisation + +The GUI is initialised in the following stages. The aim is to allocate the +`framebuf` before importing other modules. This is intended to reduce the risk +of memory failures when instantiating a large framebuf in an application which +imports multiple modules. + +Firstly set the display height and import the driver: +```python +height = 96 # 1.27 inch 96*128 (rows*cols) display. Set to 128 for 1.5 inch +import machine +import gc +from ssd1351 import SSD1351 as SSD # Import the display driver +``` +Then set up the bus (SPI or I2C) and instantiate the display. At this point the +framebuffer is created: +```python +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance +``` +Finally import `nanogui` modules and initialise the display. Import any other +modules required by the application. For each font to be used import the +Python font and create a `CWriter` instance (for monochrome displays a `Writer` +is used): +```python +from nanogui import Label, Dial, Pointer, refresh # Whatever you need +refresh(ssd) # Initialise and clear display. + +from writer import CWriter # Import other modules +import arial10 # Font +GREEN = SSD.rgb(0, 255, 0) # Define colors +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 + +CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it + # Instantiate any CWriters to be used (one for each font) +wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) +wri.set_clip(True, True, False) +``` + +The `nanogui.refresh` method takes two args: + 1. `device` The display instance (supports multiple displays). + 2. `clear=False` If set `True` the display will be blanked; it is also + blanked when a device is refreshed for the first time. + +It should be called after instantiating the display, and again whenever the +physical display is to be updated. + +###### [Contents](./README.md#contents) + +## 3.2 Label class + +This supports applications where text is to be rendered at specific screen +locations. + +Text can be static or dynamic. In the case of dynamic text the background is +cleared to ensure that short strings cleanly replace longer ones. + +Labels can be displayed with an optional single pixel border. + +Colors are handled flexibly. By default the colors used are those of the +`Writer` instance, however they can be changed dynamically; this might be used +to warn of overrange or underrange values. + +Constructor args: + 1. `writer` The `Writer` instance (font and screen) to use. + 2. `row` Location on screen. + 3. `col` + 4. `text` If a string is passed it is displayed: typically used for static + text. If an integer is passed it is interpreted as the maximum text length + in pixels; typically obtained from `writer.stringlen('-99.99')`. Nothing is + dsplayed until `.value()` is called. Intended for dynamic text fields. + 5. `invert=False` Display in inverted or normal style. + 6. `fgcolor=None` Optionally override the `Writer` colors. + 7. `bgcolor=None` + 8. `bdcolor=False` If `False` no border is displayed. If `None` a border is + shown in the `Writer` forgeround color. If a color is passed, it is used. + +The constructor displays the string at the required location. + +Methods: + 1. `value` Redraws the label. This takes the following args: + * `text=None` The text to display. If `None` displays last value. + * ` invert=False` If true, show inverse text. + * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + * `bgcolor=None` Background color, as per foreground. + * `bdcolor=None` Border color. As per above except that if `False` is + passed, no border is displayed. This clears a previously drawn border. + Returns the current text string. + 2. `show` No args. (Re)draws the label. Primarily for internal use by GUI. + +If populating a label would cause it to extend beyond the screen boundary a +warning is printed at the console. The label may appear at an unexpected place. +The following is a complete "Hello world" script. +```python +height = 96 # 1.27 inch 96*128 (rows*cols) display. Set to 128 for 1.5 inch +import machine +import gc +from ssd1351 import SSD1351 as SSD # Import the display driver +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance +from nanogui import Label, refresh +refresh(ssd) # Initialise and clear display. +from writer import CWriter # Import other modules +import freesans20 # Font +GREEN = SSD.rgb(0, 255, 0) # Define colors +BLACK = 0 +CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it +wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False) +wri.set_clip(True, True, False) + # End of boilerplate code. This is our application: +Label(wri, 2, 2, 'Hello world!') +refresh(ssd) +``` + +###### [Contents](./README.md#contents) + +## 3.3 Meter class + +This provides a vertical linear meter display of values scaled between 0.0 and +1.0. + +Constructor positional args: + 1. `writer` The `Writer` instance (font and screen) to use. + 2. `row` Location on screen. + 3. `col` +Keyword only args: + 4. `height=50` Height of meter. + 5. `width=10` Width. + 6. `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + 7. `bgcolor=None` Background color, as per foreground. + 8. `ptcolor=None` Color of meter pointer or bar. Default is foreground color. + 9. `bdcolor=False` If `False` no border is displayed. If `None` a border is + shown in the `Writer` forgeround color. If a color is passed, it is used. + 10. `divisions=5` No. of gradutions to show. + 11. `label=None` A text string will cause a `Label` to be drawn below the + meter. An integer will create a `Label` of that width for later use. + 12. `style=Meter.LINE` The pointer is a horizontal line. `Meter.BAR` causes a + vertical bar to be displayed. + 13. `legends=None` If a tuple of strings is passed, `Label` instances will be + displayed to the right hand side of the meter, starting at the bottom. E.G. + `('0.0', '0.5', '1.0')` + 14. `value=None` Initial value. If `None` the meter will not be drawn until + its `value()` method is called. + +Methods: + 1. `value` Args: `n=None, color=None`. + * `n` should be a float in range 0 to 1.0. Causes the meter to be updated. + Out of range values are constrained. If `None` is passed the meter is not + updated. + * `color` Updates the color of the bar or line if a value is also passed. + `None` causes no change. + Returns the current value. + 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: + * `text=None` The text to display. If `None` displays last value. + * ` invert=False` If true, show inverse text. + * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + * `bgcolor=None` Background color, as per foreground. + * `bdcolor=None` Border color. As per above except that if `False` is + passed, no border is displayed. This clears a previously drawn border. + 3. `show` No args. (Re)draws the meter. Primarily for internal use by GUI. + +###### [Contents](./README.md#contents) + +## 3.4 LED class + +This is a virtual LED whose color may be altered dynamically. + +Constructor positional args: + 1. `writer` The `Writer` instance (font and screen) to use. + 2. `row` Location on screen. + 3. `col` +Keyword only args: + 4. `height=12` Height of LED. + 5. `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + 6. `bgcolor=None` Background color, as per foreground. + 7. `bdcolor=False` If `False` no border is displayed. If `None` a border is + shown in the `Writer` forgeround color. If a color is passed, it is used. + 8. `label=None` A text string will cause a `Label` to be drawn below the + LED. An integer will create a `Label` of that width for later use. + +Methods: + 1. `color` arg `c=None` Change the LED color to `c`. If `c` is `None` the LED + is turned off (rendered in the background color). + 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: + * `text=None` The text to display. If `None` displays last value. + * ` invert=False` If true, show inverse text. + * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + * `bgcolor=None` Background color, as per foreground. + * `bdcolor=None` Border color. As per above except that if `False` is + passed, no border is displayed. This clears a previously drawn border. + 3. `show` No args. (Re)draws the LED. Primarily for internal use by GUI. + +###### [Contents](./README.md#contents) + +## 3.5 Dial and Pointer classes + +A dial is a circular analogue clock style display showing a set of pointers. To +use, the `Dial` is instantiated then one or more `Pointer` objects are +instantiated and assigned to it. The `Pointer.value` method enables the `Dial` +to be updated, with the length, angle and color being dynamically variable. +Pointer values are complex numbers. + +Constructor positional args: + 1. `writer` The `Writer` instance (font and screen) to use. + 2. `row` Location on screen. + 3. `col` +Keyword only args: + 4. `height=50` Height and width of dial. + 5. `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + 6. `bgcolor=None` Background color, as per foreground. + 7. `bdcolor=False` If `False` no border is displayed. If `None` a border is + shown in the `Writer` forgeround color. If a color is passed, it is used. + 8. `ticks=4` No. of gradutions to show. + 9. `label=None` A text string will cause a `Label` to be drawn below the + meter. An integer will create a `Label` of that width for later use. + 10. `style=Dial.CLOCK` Pointers are drawn from the centre of the circle as per + the hands of a clock. `Dial.COMPASS` causes pointers to be drawn as arrows + centred on the control's centre. Arrow tail chevrons are suppressed for very + short pointers. + 11. `pip=None` Draws a central dot. A color may be passed, otherwise the + foreground color will be used. If `False` is passed, no pip will be drawn. The + pip is suppressed if the shortest pointer would be hard to see. + +When a `Pointer` is instantiated it is assigned to the `Dial` by the `Pointer` +constructor. + +The `Pointer` class: + +Constructor arg: + 1. `dial` The `Dial` instance on which it is to be dsplayed. + +Methods: + 1. `value` Args: + * `v=None` The value is a complex number. If its magnitude exceeds unity it + is reduced (preserving phase) to constrain it to the boundary of the unit + circle. + * `color=None` By default the pointer is rendered in the foreground color + of the parent `Dial`. Otherwise the passed color is used. + 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: + * `text=None` The text to display. If `None` displays last value. + * ` invert=False` If true, show inverse text. + * `fgcolor=None` Foreground color: if `None` the `Writer` default is used. + * `bgcolor=None` Background color, as per foreground. + * `bdcolor=None` Border color. As per above except that if `False` is + passed, no border is displayed. This clears a previously drawn border. + 3. `show` No args. (Re)draws the control. Primarily for internal use by GUI. + +Typical usage (`ssd` is the device and `wri` is the current `Writer`): +```python +def clock(ssd, wri): + # Border in Writer foreground color: + dial = Dial(wri, 5, 5, ticks = 12, bdcolor=None) + hrs = Pointer(dial) + mins = Pointer(dial) + hrs.value(0 + 0.7j, RED) + mins.value(0 + 0.9j, YELLOW) + dm = cmath.exp(-1j * cmath.pi / 30) # Rotate by 1 minute + dh = cmath.exp(-1j * cmath.pi / 1800) # Rotate hours by 1 minute + # Twiddle the hands: see clock.py for an actual clock + for _ in range(80): + utime.sleep_ms(200) + mins.value(mins.value() * dm, RED) + hrs.value(hrs.value() * dh, YELLOW) + refresh(ssd) +``` + +# 4. Device drivers + +For a driver to support `nanogui` it must be subclassed from `framebuf` and +provide `height` and `width` bound variables defining the display size in +pixels. This is all that is required for monochrome drivers. + +For color drivers, to conserve RAM it is suggested that 8-bit color is used +for the `framebuf`. If the hardware does not support this, conversion to the +supported color space needs to be done "on the fly" as per the SSD1351 driver. +Since this is likely to be slow, consider using native, viper or assembler. + +Color drivers should have a static method converting rgb(255, 255, 255) to a +form acceptable to the driver. For 8-bit rrrgggbb this can be: +```python + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) +``` +This should be amended if the hardware uses a different 8-bit format. + +The `Writer` (monochrome) or `CWriter` (color) classes and the `nanogui` module +should then work automatically. + +If a display uses I2C note that owing to +[this issue](https://github.com/micropython/micropython/pull/4020) soft I2C +may be required, depending on the detailed specification of the chip. + +###### [Contents](./README.md#contents) diff --git a/aclock.py b/aclock.py new file mode 100644 index 0000000..76a0db0 --- /dev/null +++ b/aclock.py @@ -0,0 +1,104 @@ +# aclock.py Test/demo program for Adafruit ssd1351-based OLED displays +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# WIRING +# Pyb SSD +# 3v3 Vin +# Gnd Gnd +# X1 DC +# X2 CS +# X3 Rst +# X6 CLK +# X8 DATA + +height = 96 # 1.27 inch 96*128 (rows*cols) display +# height = 128 # 1.5 inch 128*128 display + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +import machine +import gc +from ssd1351 import SSD1351 as SSD + +# Initialise hardware +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance +from nanogui import Dial, Pointer, refresh, Label +refresh(ssd) # Initialise and clear display. + +# Now import other modules +import cmath +import utime +from writer import CWriter + +# Font for CWriter +import arial10 + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 + +def aclock(): + uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi + pi = cmath.pi + days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', + 'Sunday') + months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', + 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') + # Instantiate CWriter + CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + wri.set_clip(True, True, False) + + # Instantiate displayable objects + dial = Dial(wri, 2, 2, height = 75, ticks = 12, bdcolor=None, label=120, pip=False) # Border in fg color + lbltim = Label(wri, 5, 85, 35) + hrs = Pointer(dial) + mins = Pointer(dial) + secs = Pointer(dial) + + hstart = 0 + 0.7j # Pointer lengths and position at top + mstart = 0 + 0.92j + sstart = 0 + 0.92j + while True: + t = utime.localtime() + hrs.value(hstart * uv(-t[3]*pi/6 - t[4]*pi/360), YELLOW) + mins.value(mstart * uv(-t[4] * pi/30), YELLOW) + secs.value(sstart * uv(-t[5] * pi/30), RED) + lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) + dial.text('{} {} {} {}'.format(days[t[6]], t[2], months[t[1] - 1], t[0])) + refresh(ssd) + utime.sleep(1) + +aclock() diff --git a/alevel.py b/alevel.py new file mode 100644 index 0000000..dc622f1 --- /dev/null +++ b/alevel.py @@ -0,0 +1,91 @@ +# alevel.py Test/demo program for Adafruit ssd1351-based OLED displays +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# WIRING +# Pyb SSD +# 3v3 Vin +# Gnd Gnd +# X1 DC +# X2 CS +# X3 Rst +# X6 CLK +# X8 DATA + +height = 96 # 1.27 inch 96*128 (rows*cols) display +# height = 128 # 1.5 inch 128*128 display + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +import machine +import gc +from ssd1351 import SSD1351 as SSD + +# Initialise hardware +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution befor instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance +from nanogui import Dial, Pointer, refresh +refresh(ssd) # Initialise and clear display. + +# Now import other modules + +import utime +import pyb +from writer import CWriter +import arial10 # Font + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 + + +def main(): + print('alevel test is running.') + CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + wri.set_clip(True, True, False) + acc = pyb.Accel() + dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, + label='Tilt Pyboard', style = Dial.COMPASS, pip=YELLOW) # Border in fg color + ptr = Pointer(dial) + scale = 1/40 + while True: + x, y, z = acc.filtered_xyz() + # Depending on relative alignment of display and Pyboard this line may + # need changing: swap x and y or change signs so arrow points in direction + # board is tilted. + ptr.value(-y*scale + 1j*x*scale, YELLOW) + refresh(ssd) + utime.sleep_ms(200) + +main() diff --git a/arial10.py b/arial10.py new file mode 100644 index 0000000..0a28777 --- /dev/null +++ b/arial10.py @@ -0,0 +1,139 @@ +# Code generated by font-to-py.py. +# Font: Arial.ttf +version = '0.25' + +def height(): + return 10 + +def max_width(): + return 11 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 126 + +_font =\ +b'\x06\x00\x70\x88\x08\x10\x20\x20\x00\x20\x00\x00\x03\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x80\x80\x80\x80\x80\x80'\ +b'\x00\x80\x00\x00\x04\x00\xa0\xa0\xa0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x06\x00\x28\x28\xf8\x50\x50\xf8\xa0\xa0\x00\x00\x06\x00\x70\xa8'\ +b'\xa0\x70\x28\x28\xa8\x70\x20\x00\x0a\x00\x62\x00\x94\x00\x94\x00'\ +b'\x68\x00\x0b\x00\x14\x80\x14\x80\x23\x00\x00\x00\x00\x00\x07\x00'\ +b'\x30\x48\x48\x30\x50\x8c\x88\x74\x00\x00\x02\x00\x80\x80\x80\x00'\ +b'\x00\x00\x00\x00\x00\x00\x04\x00\x20\x40\x80\x80\x80\x80\x80\x80'\ +b'\x40\x20\x04\x00\x80\x40\x20\x20\x20\x20\x20\x20\x40\x80\x04\x00'\ +b'\x40\xe0\x40\xa0\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x20\x20'\ +b'\xf8\x20\x20\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x80'\ +b'\x80\x80\x04\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x03\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x03\x00\x20\x20\x40\x40'\ +b'\x40\x40\x80\x80\x00\x00\x06\x00\x70\x88\x88\x88\x88\x88\x88\x70'\ +b'\x00\x00\x06\x00\x20\x60\xa0\x20\x20\x20\x20\x20\x00\x00\x06\x00'\ +b'\x70\x88\x08\x08\x10\x20\x40\xf8\x00\x00\x06\x00\x70\x88\x08\x30'\ +b'\x08\x08\x88\x70\x00\x00\x06\x00\x10\x30\x50\x50\x90\xf8\x10\x10'\ +b'\x00\x00\x06\x00\x78\x40\x80\xf0\x08\x08\x88\x70\x00\x00\x06\x00'\ +b'\x70\x88\x80\xf0\x88\x88\x88\x70\x00\x00\x06\x00\xf8\x10\x10\x20'\ +b'\x20\x40\x40\x40\x00\x00\x06\x00\x70\x88\x88\x70\x88\x88\x88\x70'\ +b'\x00\x00\x06\x00\x70\x88\x88\x88\x78\x08\x88\x70\x00\x00\x03\x00'\ +b'\x00\x00\x80\x00\x00\x00\x00\x80\x00\x00\x03\x00\x00\x00\x80\x00'\ +b'\x00\x00\x00\x80\x80\x80\x06\x00\x00\x00\x08\x70\x80\x70\x08\x00'\ +b'\x00\x00\x06\x00\x00\x00\x00\xf8\x00\xf8\x00\x00\x00\x00\x06\x00'\ +b'\x00\x00\x80\x70\x08\x70\x80\x00\x00\x00\x06\x00\x70\x88\x08\x10'\ +b'\x20\x20\x00\x20\x00\x00\x0b\x00\x1f\x00\x60\x80\x4d\x40\x93\x40'\ +b'\xa2\x40\xa2\x40\xa6\x80\x9b\x00\x40\x40\x3f\x80\x08\x00\x10\x28'\ +b'\x28\x28\x44\x7c\x82\x82\x00\x00\x07\x00\xf8\x84\x84\xfc\x84\x84'\ +b'\x84\xf8\x00\x00\x07\x00\x38\x44\x80\x80\x80\x80\x44\x38\x00\x00'\ +b'\x07\x00\xf0\x88\x84\x84\x84\x84\x88\xf0\x00\x00\x06\x00\xf8\x80'\ +b'\x80\xf8\x80\x80\x80\xf8\x00\x00\x06\x00\xf8\x80\x80\xf0\x80\x80'\ +b'\x80\x80\x00\x00\x08\x00\x38\x44\x82\x80\x8e\x82\x44\x38\x00\x00'\ +b'\x07\x00\x84\x84\x84\xfc\x84\x84\x84\x84\x00\x00\x02\x00\x80\x80'\ +b'\x80\x80\x80\x80\x80\x80\x00\x00\x05\x00\x10\x10\x10\x10\x10\x90'\ +b'\x90\x60\x00\x00\x07\x00\x84\x88\x90\xb0\xd0\x88\x88\x84\x00\x00'\ +b'\x06\x00\x80\x80\x80\x80\x80\x80\x80\xf8\x00\x00\x08\x00\x82\xc6'\ +b'\xc6\xaa\xaa\xaa\x92\x92\x00\x00\x07\x00\x84\xc4\xa4\xa4\x94\x94'\ +b'\x8c\x84\x00\x00\x08\x00\x38\x44\x82\x82\x82\x82\x44\x38\x00\x00'\ +b'\x06\x00\xf0\x88\x88\x88\xf0\x80\x80\x80\x00\x00\x08\x00\x38\x44'\ +b'\x82\x82\x82\x9a\x44\x3e\x00\x00\x07\x00\xf8\x84\x84\xf8\x90\x88'\ +b'\x88\x84\x00\x00\x07\x00\x78\x84\x80\x60\x18\x04\x84\x78\x00\x00'\ +b'\x06\x00\xf8\x20\x20\x20\x20\x20\x20\x20\x00\x00\x07\x00\x84\x84'\ +b'\x84\x84\x84\x84\x84\x78\x00\x00\x08\x00\x82\x82\x44\x44\x28\x28'\ +b'\x10\x10\x00\x00\x0b\x00\x84\x20\x8a\x20\x4a\x40\x4a\x40\x51\x40'\ +b'\x51\x40\x20\x80\x20\x80\x00\x00\x00\x00\x07\x00\x84\x48\x48\x30'\ +b'\x30\x48\x48\x84\x00\x00\x08\x00\x82\x44\x44\x28\x10\x10\x10\x10'\ +b'\x00\x00\x07\x00\x7c\x08\x10\x10\x20\x20\x40\xfc\x00\x00\x03\x00'\ +b'\xc0\x80\x80\x80\x80\x80\x80\x80\x80\xc0\x03\x00\x80\x80\x40\x40'\ +b'\x40\x40\x20\x20\x00\x00\x03\x00\xc0\x40\x40\x40\x40\x40\x40\x40'\ +b'\x40\xc0\x05\x00\x20\x50\x50\x88\x00\x00\x00\x00\x00\x00\x06\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xfc\x00\x04\x00\x80\x40\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x70\x88\x78\x88\x98\xe8'\ +b'\x00\x00\x06\x00\x80\x80\xb0\xc8\x88\x88\xc8\xb0\x00\x00\x06\x00'\ +b'\x00\x00\x70\x88\x80\x80\x88\x70\x00\x00\x06\x00\x08\x08\x68\x98'\ +b'\x88\x88\x98\x68\x00\x00\x06\x00\x00\x00\x70\x88\xf8\x80\x88\x70'\ +b'\x00\x00\x04\x00\x20\x40\xe0\x40\x40\x40\x40\x40\x00\x00\x06\x00'\ +b'\x00\x00\x68\x98\x88\x88\x98\x68\x08\xf0\x06\x00\x80\x80\xb0\xc8'\ +b'\x88\x88\x88\x88\x00\x00\x02\x00\x80\x00\x80\x80\x80\x80\x80\x80'\ +b'\x00\x00\x02\x00\x40\x00\x40\x40\x40\x40\x40\x40\x40\x80\x05\x00'\ +b'\x80\x80\x90\xa0\xc0\xe0\xa0\x90\x00\x00\x02\x00\x80\x80\x80\x80'\ +b'\x80\x80\x80\x80\x00\x00\x08\x00\x00\x00\xbc\xd2\x92\x92\x92\x92'\ +b'\x00\x00\x06\x00\x00\x00\xf0\x88\x88\x88\x88\x88\x00\x00\x06\x00'\ +b'\x00\x00\x70\x88\x88\x88\x88\x70\x00\x00\x06\x00\x00\x00\xb0\xc8'\ +b'\x88\x88\xc8\xb0\x80\x80\x06\x00\x00\x00\x68\x98\x88\x88\x98\x68'\ +b'\x08\x08\x04\x00\x00\x00\xa0\xc0\x80\x80\x80\x80\x00\x00\x06\x00'\ +b'\x00\x00\x70\x88\x60\x10\x88\x70\x00\x00\x03\x00\x40\x40\xe0\x40'\ +b'\x40\x40\x40\x60\x00\x00\x06\x00\x00\x00\x88\x88\x88\x88\x98\x68'\ +b'\x00\x00\x06\x00\x00\x00\x88\x88\x50\x50\x20\x20\x00\x00\x0a\x00'\ +b'\x00\x00\x00\x00\x88\x80\x94\x80\x55\x00\x55\x00\x22\x00\x22\x00'\ +b'\x00\x00\x00\x00\x06\x00\x00\x00\x88\x50\x20\x20\x50\x88\x00\x00'\ +b'\x06\x00\x00\x00\x88\x88\x50\x50\x20\x20\x20\x40\x06\x00\x00\x00'\ +b'\xf8\x10\x20\x20\x40\xf8\x00\x00\x04\x00\x20\x40\x40\x40\x80\x40'\ +b'\x40\x40\x40\x20\x02\x00\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80'\ +b'\x04\x00\x80\x40\x40\x40\x20\x40\x40\x40\x40\x80\x06\x00\x00\x00'\ +b'\x00\xe8\xb0\x00\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\x0c\x00\x0c\x00\x18\x00\x18\x00\x24\x00\x24\x00\x30\x00'\ +b'\x30\x00\x3c\x00\x3c\x00\x48\x00\x48\x00\x5e\x00\x5e\x00\x6a\x00'\ +b'\x6a\x00\x76\x00\x76\x00\x82\x00\x82\x00\x8e\x00\x8e\x00\x9a\x00'\ +b'\x9a\x00\xa6\x00\xa6\x00\xb2\x00\xb2\x00\xbe\x00\xbe\x00\xca\x00'\ +b'\xca\x00\xd6\x00\xd6\x00\xe2\x00\xe2\x00\xee\x00\xee\x00\xfa\x00'\ +b'\xfa\x00\x06\x01\x06\x01\x12\x01\x12\x01\x1e\x01\x1e\x01\x2a\x01'\ +b'\x2a\x01\x36\x01\x36\x01\x42\x01\x42\x01\x4e\x01\x4e\x01\x5a\x01'\ +b'\x5a\x01\x66\x01\x66\x01\x72\x01\x72\x01\x7e\x01\x7e\x01\x8a\x01'\ +b'\x8a\x01\x96\x01\x96\x01\xac\x01\xac\x01\xb8\x01\xb8\x01\xc4\x01'\ +b'\xc4\x01\xd0\x01\xd0\x01\xdc\x01\xdc\x01\xe8\x01\xe8\x01\xf4\x01'\ +b'\xf4\x01\x00\x02\x00\x02\x0c\x02\x0c\x02\x18\x02\x18\x02\x24\x02'\ +b'\x24\x02\x30\x02\x30\x02\x3c\x02\x3c\x02\x48\x02\x48\x02\x54\x02'\ +b'\x54\x02\x60\x02\x60\x02\x6c\x02\x6c\x02\x78\x02\x78\x02\x84\x02'\ +b'\x84\x02\x90\x02\x90\x02\x9c\x02\x9c\x02\xa8\x02\xa8\x02\xb4\x02'\ +b'\xb4\x02\xca\x02\xca\x02\xd6\x02\xd6\x02\xe2\x02\xe2\x02\xee\x02'\ +b'\xee\x02\xfa\x02\xfa\x02\x06\x03\x06\x03\x12\x03\x12\x03\x1e\x03'\ +b'\x1e\x03\x2a\x03\x2a\x03\x36\x03\x36\x03\x42\x03\x42\x03\x4e\x03'\ +b'\x4e\x03\x5a\x03\x5a\x03\x66\x03\x66\x03\x72\x03\x72\x03\x7e\x03'\ +b'\x7e\x03\x8a\x03\x8a\x03\x96\x03\x96\x03\xa2\x03\xa2\x03\xae\x03'\ +b'\xae\x03\xba\x03\xba\x03\xc6\x03\xc6\x03\xd2\x03\xd2\x03\xde\x03'\ +b'\xde\x03\xea\x03\xea\x03\xf6\x03\xf6\x03\x02\x04\x02\x04\x0e\x04'\ +b'\x0e\x04\x1a\x04\x1a\x04\x26\x04\x26\x04\x32\x04\x32\x04\x3e\x04'\ +b'\x3e\x04\x54\x04\x54\x04\x60\x04\x60\x04\x6c\x04\x6c\x04\x78\x04'\ +b'\x78\x04\x84\x04\x84\x04\x90\x04\x90\x04\x9c\x04\x9c\x04\xa8\x04'\ + +_mvfont = memoryview(_font) + +def get_ch(ch): + ordch = ord(ch) + ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 + idx_offs = 4 * (ordch - 32) + offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little') + next_offs = int.from_bytes(_index[idx_offs + 2 : idx_offs + 4], 'little') + width = int.from_bytes(_font[offset:offset + 2], 'little') + return _mvfont[offset + 2:next_offs], 10, width + diff --git a/color15.py b/color15.py new file mode 100644 index 0000000..330100f --- /dev/null +++ b/color15.py @@ -0,0 +1,191 @@ +# color15.py Test/demo program for Adafruit ssd1351-based OLED displays +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 +# For wiring details see drivers/ADAFRUIT.md in this repo. + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +height = 96 # 1.27 inch 96*128 (rows*cols) display +# height = 128 # 1.5 inch 128*128 display + +import machine +import gc +from ssd1351 import SSD1351 as SSD + +# Initialise hardware and framebuf before importing modules +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution befor instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance + +import cmath +import utime +import uos +from writer import Writer, CWriter +from nanogui import Label, Meter, LED, Dial, Pointer, refresh + +# Fonts +import arial10, freesans20 + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 +CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it +wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) +wri.set_clip(True, True, False) + +def meter(): + print('Meter test.') + refresh(ssd, True) # Clear any prior image + color = lambda v : RED if v > 0.7 else YELLOW if v > 0.5 else GREEN + txt = lambda v : 'ovr' if v > 0.7 else 'high' if v > 0.5 else 'ok' + m0 = Meter(wri, 5, 2, divisions = 4, ptcolor=YELLOW, + label='left', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) + l0 = LED(wri, ssd.height - 16 - wri.height, 2, bdcolor=YELLOW, label ='over') + m1 = Meter(wri, 5, 50, divisions = 4, ptcolor=YELLOW, + label='right', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) + l1 = LED(wri, ssd.height - 16 - wri.height, 50, bdcolor=YELLOW, label ='over') + m2 = Meter(wri, 5, 98, divisions = 4, ptcolor=YELLOW, + label='bass', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) + l2 = LED(wri, ssd.height - 16 - wri.height, 98, bdcolor=YELLOW, label ='over') + steps = 10 + for n in range(steps): + v = int.from_bytes(uos.urandom(3),'little')/16777216 + m0.value(v, color(v)) + l0.color(color(v)) + l0.text(txt(v), fgcolor=color(v)) + v = n/steps + m1.value(v, color(v)) + l1.color(color(v)) + l1.text(txt(v), fgcolor=color(v)) + v = 1 - n/steps + m2.value(v, color(v)) + l2.color(color(v)) + l2.text(txt(v), fgcolor=color(v)) + refresh(ssd) + utime.sleep(1) + + +def multi_fields(t): + print('Dynamic labels.') + refresh(ssd, True) # Clear any prior image + nfields = [] + dy = wri.height + 6 + y = 2 + col = 15 + width = wri.stringlen('99.99') + for txt in ('X:', 'Y:', 'Z:'): + Label(wri, y, 0, txt) # Use wri default colors + nfields.append(Label(wri, y, col, width, bdcolor=None)) # Specify a border, color TBD + y += dy + + end = utime.ticks_add(utime.ticks_ms(), t * 1000) + while utime.ticks_diff(end, utime.ticks_ms()) > 0: + for field in nfields: + value = int.from_bytes(uos.urandom(3),'little')/167772 + overrange = None if value < 70 else YELLOW if value < 90 else RED + field.value('{:5.2f}'.format(value), fgcolor = overrange, bdcolor = overrange) + refresh(ssd) + utime.sleep(1) + Label(wri, 0, 64, ' OK ', True, fgcolor = RED) + refresh(ssd) + utime.sleep(1) + +def vari_fields(): + print('Variable label styles.') + refresh(ssd, True) # Clear any prior image + wri_large = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False) + wri_large.set_clip(True, True, False) + Label(wri_large, 0, 0, 'Text') + Label(wri_large, 20, 0, 'Border') + width = wri_large.stringlen('Yellow') + lbl_text = Label(wri_large, 0, 65, width) + lbl_bord = Label(wri_large, 20, 65, width) + lbl_text.value('Red') + lbl_bord.value('Red') + lbl_var = Label(wri_large, 50, 2, '25.46', fgcolor=RED, bdcolor=RED) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Red') + lbl_bord.value('Yellow') + lbl_var.value(bdcolor=YELLOW) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Red') + lbl_bord.value('None') + lbl_var.value(bdcolor=False) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Yellow') + lbl_bord.value('None') + lbl_var.value(fgcolor=YELLOW) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Blue') + lbl_bord.value('Green') + lbl_var.value('18.99', fgcolor=BLUE, bdcolor=GREEN) + Label(wri, ssd.height - wri.height - 2, 0, 'Done', fgcolor=RED) + refresh(ssd) + +def clock(x): + print('Clock test.') + refresh(ssd, True) # Clear any prior image + lbl = Label(wri, 5, 85, 'Clock') + dial = Dial(wri, 5, 5, height = 75, ticks = 12, bdcolor=None, label=50) # Border in fg color + hrs = Pointer(dial) + mins = Pointer(dial) + hrs.value(0 + 0.7j, RED) + mins.value(0 + 0.9j, YELLOW) + dm = cmath.rect(1, -cmath.pi/30) # Rotate by 1 minute (CW) + dh = cmath.rect(1, -cmath.pi/1800) # Rotate hours by 1 minute + for n in range(x): + refresh(ssd) + utime.sleep_ms(200) + mins.value(mins.value() * dm, YELLOW) + hrs.value(hrs.value() * dh, RED) + dial.text('ticks: {}'.format(n)) + lbl.value('Done') + +def compass(x): + print('Compass test.') + refresh(ssd, True) # Clear any prior image + dial = Dial(wri, 5, 5, height = 75, bdcolor=None, label=50, style = Dial.COMPASS) + bearing = Pointer(dial) + bearing.value(0 + 1j, RED) + dh = cmath.rect(1, -cmath.pi/30) # Rotate by 6 degrees CW + for n in range(x): + utime.sleep_ms(200) + bearing.value(bearing.value() * dh, RED) + refresh(ssd) + +print('Color display test is running.') +clock(70) +compass(70) +meter() +multi_fields(t = 10) +vari_fields() +print('Test complete.') diff --git a/color96.py b/color96.py new file mode 100644 index 0000000..3264ecf --- /dev/null +++ b/color96.py @@ -0,0 +1,133 @@ +# color96.py Test/demo program for ssd1331 Adafruit 0.96" OLED display +# https://www.adafruit.com/product/684 +# For wiring details see drivers/ADAFRUIT.md in this repo. + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import machine +import gc +from ssd1331 import SSD1331 as SSD +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() +ssd = SSD(spi, pcs, pdc, prst) # Create a display instance + +from nanogui import Label, Meter, LED, refresh +refresh(ssd) +# Fonts +import arial10 +from writer import Writer, CWriter +import utime +import uos + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +BLACK = 0 + +CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it +wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) +wri.set_clip(True, True, False) + +def meter(): + print('meter') + refresh(ssd, True) # Clear any prior image + m = Meter(wri, 5, 2, height = 45, divisions = 4, ptcolor=YELLOW, + label='level', style=Meter.BAR, legends=('0.0', '0.5', '1.0')) + l = LED(wri, 5, 40, bdcolor=YELLOW, label ='over') + steps = 10 + for _ in range(steps): + v = int.from_bytes(uos.urandom(3),'little')/16777216 + m.value(v) + l.color(GREEN if v < 0.5 else RED) + refresh(ssd) + utime.sleep(1) + refresh(ssd) + + +def multi_fields(t): + print('multi_fields') + refresh(ssd, True) # Clear any prior image + nfields = [] + dy = wri.height + 6 + y = 2 + col = 15 + width = wri.stringlen('99.99') + for txt in ('X:', 'Y:', 'Z:'): + Label(wri, y, 0, txt) # Use wri default colors + nfields.append(Label(wri, y, col, width, bdcolor=None)) # Specify a border, color TBD + y += dy + + end = utime.ticks_add(utime.ticks_ms(), t * 1000) + while utime.ticks_diff(end, utime.ticks_ms()) > 0: + for field in nfields: + value = int.from_bytes(uos.urandom(3),'little')/167772 + overrange = None if value < 70 else YELLOW if value < 90 else RED + field.value('{:5.2f}'.format(value), fgcolor = overrange, bdcolor = overrange) + refresh(ssd) + utime.sleep(1) + Label(wri, 0, 64, ' OK ', True, fgcolor = RED) + refresh(ssd) + utime.sleep(1) + +def vari_fields(): + print('vari_fields') + refresh(ssd, True) # Clear any prior image + Label(wri, 0, 0, 'Text:') + Label(wri, 20, 0, 'Border:') + width = wri.stringlen('Yellow') + lbl_text = Label(wri, 0, 40, width) + lbl_bord = Label(wri, 20, 40, width) + lbl_text.value('Red') + lbl_bord.value('Red') + lbl_var = Label(wri, 40, 2, '25.46', fgcolor=RED, bdcolor=RED) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Red') + lbl_bord.value('Yellow') + lbl_var.value(bdcolor=YELLOW) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Red') + lbl_bord.value('None') + lbl_var.value(bdcolor=False) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Yellow') + lbl_bord.value('None') + lbl_var.value(fgcolor=YELLOW) + refresh(ssd) + utime.sleep(2) + lbl_text.value('Blue') + lbl_bord.value('Green') + lbl_var.value('18.99', fgcolor=BLUE, bdcolor=GREEN) + refresh(ssd) + +print('Color display test is running.') +meter() +multi_fields(t = 10) +vari_fields() +print('Test complete.') diff --git a/courier20.py b/courier20.py new file mode 100644 index 0000000..258c09b --- /dev/null +++ b/courier20.py @@ -0,0 +1,308 @@ +# Code generated by font-to-py.py. +# Font: Courier Prime.ttf +version = '0.2' + +def height(): + return 20 + +def max_width(): + return 14 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return True + +def min_ch(): + return 32 + +def max_ch(): + return 126 + +_font =\ +b'\x0e\x00\x00\x00\x00\x00\x7c\x00\xfe\x00\xc7\x00\xc3\x00\x03\x00'\ +b'\x07\x00\x1e\x00\x18\x00\x18\x00\x18\x00\x3c\x00\x3c\x00\x18\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x60\x00\x60\x00\x60\x00'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x00\x00\x60\x00\xf0\x00'\ +b'\xf0\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\xe6\x00\xe6\x00\x66\x00\x66\x00\x66\x00\x66\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x03\x30\x02\x20'\ +b'\x02\x20\x06\x60\x3f\xf8\x3f\xf8\x0c\xc0\x08\x80\x7f\xf0\xff\xf0'\ +b'\x19\x80\x11\x00\x33\x00\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x0c\x00\x0c\x00\x3d\x80\x7f\x80\xcd\x80\xcc\x80'\ +b'\xec\x00\x7f\x00\x0f\x80\x0c\xc0\xcc\xc0\xcd\xc0\xff\x80\xcf\x00'\ +b'\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x38\x00\xfe\x18\xc6\x30\xc6\x60\xfe\xc0\x39\x80\x03\x00'\ +b'\x06\x70\x1d\xfc\x39\x8c\x71\x8c\x61\xfc\x00\x70\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x1e\x00\x3f\x00'\ +b'\x63\x00\x63\x00\x60\x00\x30\x00\x31\xc0\x49\xc0\xc7\x00\xc3\x00'\ +b'\xe3\x00\x7f\xc0\x39\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00'\ +b'\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x04\x00\x0e\x00'\ +b'\x18\x00\x30\x00\x30\x00\x60\x00\x60\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\x60\x00\x60\x00\x30\x00\x38\x00\x1c\x00'\ +b'\x0e\x00\x04\x00\x0e\x00\x40\x00\xe0\x00\x30\x00\x18\x00\x18\x00'\ +b'\x0c\x00\x0c\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00'\ +b'\x0c\x00\x0c\x00\x18\x00\x38\x00\x70\x00\xe0\x00\x80\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x0c\x00\x0c\x00\x0c\x00\xed\xc0\x7f\x80\x0c\x00'\ +b'\x1e\x00\x33\x00\x23\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\xff\xc0\xff\xc0\x0c\x00\x0c\x00'\ +b'\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x78\x00\x70\x00'\ +b'\x60\x00\x60\x00\xc0\x00\xc0\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0\xff\xc0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x00'\ +b'\xf0\x00\xf0\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\xc0\x01\x80\x01\x80\x03\x00\x03\x00\x02\x00\x06\x00'\ +b'\x04\x00\x0c\x00\x0c\x00\x18\x00\x18\x00\x30\x00\x30\x00\x20\x00'\ +b'\x60\x00\x40\x00\xc0\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x1e\x00\x3f\x00\x61\x80\xe1\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xe1\xc0\x61\x80\x3f\x00\x1e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x0c\x00\x7c\x00\xec\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\xff\xc0\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x3e\x00\xff\x00\xc3\x80\xc1\x80\x01\x80\x01\x00'\ +b'\x02\x00\x04\x00\x08\x00\x11\x80\x21\x80\xff\x80\xff\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x3e\x00'\ +b'\x7f\x00\x61\x80\x61\x80\x01\x80\x1f\x00\x1f\x00\x03\x80\x01\x80'\ +b'\x01\x80\xc3\x80\xff\x00\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\x03\x00\x07\x00\x0b\x00\x1b\x00'\ +b'\x13\x00\x23\x00\x63\x00\xff\xc0\xff\xe0\x03\x00\x03\x00\x0f\xc0'\ +b'\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x7f\x80\x7f\x80\x60\x00\x60\x00\x7e\x00\x7f\x00\x63\x80'\ +b'\x01\x80\x01\x80\x01\x80\xc3\x80\xff\x00\x3c\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x07\x80\x1f\x80'\ +b'\x3c\x00\x70\x00\x60\x00\xcf\x00\xff\x80\xe1\xc0\xc0\xc0\xc0\xc0'\ +b'\x61\xc0\x7f\x80\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\xff\x80\xff\x80\xc1\x80\xc1\x00\x03\x00'\ +b'\x02\x00\x06\x00\x06\x00\x0c\x00\x0c\x00\x08\x00\x18\x00\x10\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x1e\x00\x3f\x00\x61\x80\x61\x80\x73\x80\x3f\x00\x7f\x00\xe3\x80'\ +b'\xc1\x80\xc1\x80\xe3\x80\x7f\x00\x3e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x3e\x00\x7f\x80\xe1\x80'\ +b'\xc0\xc0\xc0\xc0\xe1\xc0\x7f\xc0\x3c\xc0\x01\x80\x03\x80\x0f\x00'\ +b'\x7e\x00\x78\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x00\xf0\x00\xf0\x00'\ +b'\x60\x00\x00\x00\x00\x00\x60\x00\xf0\x00\xf0\x00\x60\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x30\x00\x78\x00\x78\x00\x30\x00\x00\x00\x00\x00'\ +b'\x00\x00\x78\x00\x70\x00\x60\x00\x60\x00\xc0\x00\xc0\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x60\x01\xe0\x07\x80'\ +b'\x1e\x00\x78\x00\xe0\x00\x78\x00\x0e\x00\x03\x80\x00\xe0\x00\x40'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0\xff\xc0\x00\x00\x00\x00'\ +b'\x00\x00\xff\xc0\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x80\x00'\ +b'\xe0\x00\x38\x00\x0e\x00\x03\xc0\x00\xc0\x03\x80\x0e\x00\x38\x00'\ +b'\xe0\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x7c\x00\xfe\x00\xc7\x00\xc3\x00\x03\x00'\ +b'\x07\x00\x1e\x00\x18\x00\x18\x00\x18\x00\x3c\x00\x3c\x00\x18\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0f\x80\x1f\xc0\x38\xe0\x66\xf0\x6f\xb0\xcd\x30\xd9\x30'\ +b'\xd9\x30\xdb\x70\xdf\xe0\x6c\xc0\x70\x00\x3f\xc0\x0f\x80\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x3f\x00\x3f\x00\x07\x80'\ +b'\x0c\x80\x0c\x80\x18\xc0\x18\x40\x3f\xe0\x3f\xe0\x30\x60\x60\x30'\ +b'\xf8\xf8\xf8\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\xff\x00\xff\xc0\x30\xc0\x30\xc0\x30\xc0\x3f\x80'\ +b'\x3f\xc0\x30\xe0\x30\x60\x30\x60\x30\xe0\xff\xc0\xff\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x1f\x60'\ +b'\x3f\xe0\x70\xe0\x60\x60\xc0\x60\xc0\x40\xc0\x00\xc0\x00\xc0\x00'\ +b'\x60\x00\x70\x60\x3f\xe0\x1f\x80\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\xff\x00\xff\x80\x31\xc0\x30\xe0'\ +b'\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\xe0\x31\xc0\xff\x80'\ +b'\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\xff\xe0\xff\xe0\x30\x60\x33\x60\x33\x00\x3f\x00\x3f\x00'\ +b'\x33\x00\x33\x00\x30\x60\x30\x60\xff\xe0\xff\xe0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xff\xe0\xff\xe0'\ +b'\x30\x60\x33\x60\x33\x00\x3f\x00\x3f\x00\x33\x00\x33\x00\x30\x00'\ +b'\x30\x00\xfe\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x1e\xc0\x3f\xc0\x71\xc0\x60\xc0\xc0\xc0'\ +b'\xc0\x00\xc0\x00\xc7\xf0\xc7\xf0\x60\xc0\x70\xc0\x3f\xc0\x1f\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\xfd\xf8\xfd\xf8\x30\x60\x30\x60\x30\x60\x3f\xe0\x3f\xe0\x30\x60'\ +b'\x30\x60\x30\x60\x30\x60\xfd\xf8\xfd\xf8\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xff\xc0\xff\xc0\x0c\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\xff\xc0\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x3f\xf8\x3f\xf8\x01\x80\x01\x80\x01\x80\x01\x80'\ +b'\x81\x80\xc1\x80\xc1\x80\xc1\x80\xc3\x80\x7f\x00\x3e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xfc\xf0'\ +b'\xfc\xf0\x30\x40\x31\x80\x33\x00\x34\x00\x3f\x00\x31\x80\x30\x80'\ +b'\x30\xc0\x30\x40\xfc\x70\xfc\x30\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\x7f\x00\xff\x00\x18\x00\x18\x00'\ +b'\x18\x00\x18\x00\x18\x00\x18\x30\x18\x30\x18\x30\x18\x30\x7f\xf0'\ +b'\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\xf0\xf0\xf0\xf0\x70\xe0\x79\xe0\x69\x60\x69\x60\x6f\x60'\ +b'\x66\x60\x66\x60\x66\x60\x60\x60\xf9\xf0\xf9\xf0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xf0\xf8\xf8\xf8'\ +b'\x3c\x30\x34\x30\x32\x30\x32\x30\x31\x30\x31\x30\x30\xb0\x30\x70'\ +b'\x30\x70\x7c\x30\x7c\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x1f\x80\x3f\xc0\x70\xe0\x60\x60\xc0\x30'\ +b'\xc0\x30\xc0\x30\xc0\x30\xc0\x30\x60\x60\x70\xe0\x3f\xc0\x1f\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x7f\xc0\xff\xe0\x18\x70\x18\x30\x18\x30\x18\x70\x1f\xe0\x1f\xc0'\ +b'\x18\x00\x18\x00\x18\x00\x7f\x00\xff\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x1f\x80\x3f\xc0\x70\xe0'\ +b'\x60\x60\xc0\x30\xc0\x30\xc0\x30\xc0\x30\xc0\x30\x60\x60\x70\xe0'\ +b'\x3f\xc0\x0f\x80\x18\x20\x3f\xe0\x3f\xc0\x20\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\xff\x80\xff\xc0\x30\xe0\x30\x60\x30\xe0\x3f\xc0'\ +b'\x3f\x00\x31\x80\x30\xc0\x30\xc0\x30\x60\xfc\x78\xfc\x38\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x3c\xc0'\ +b'\x7f\xc0\xe1\xc0\xc0\xc0\xc0\x00\x70\x00\x1f\x00\x01\x80\x00\xc0'\ +b'\xc0\xc0\xe1\xc0\xff\x80\xdf\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\xff\xc0\xff\xc0\xcc\xc0\xcc\xc0'\ +b'\xcc\xc0\xcc\xc0\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x7f\x80'\ +b'\x7f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\xfc\xfc\xfc\xfc\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x38\x70\x1f\xe0\x0f\xc0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xfc\x7c\xfc\x7c'\ +b'\x30\x30\x30\x30\x10\x20\x18\x60\x18\x40\x0c\xc0\x0c\xc0\x04\x80'\ +b'\x07\x80\x07\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\xf8\x7c\xf8\x7c\x60\x18\x63\x18\x23\x10'\ +b'\x23\x90\x37\x90\x37\xb0\x34\xb0\x3c\xf0\x1c\xe0\x18\x60\x18\x60'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\xf9\xf0\xf9\xf0\x30\xc0\x19\x80\x1f\x80\x0f\x00\x06\x00\x0f\x00'\ +b'\x19\x80\x30\xc0\x60\x60\xf9\xf0\xf9\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\xf9\xf0\xf9\xf0\x30\xc0'\ +b'\x30\xc0\x19\x80\x0f\x00\x0f\x00\x06\x00\x06\x00\x06\x00\x06\x00'\ +b'\x3f\xc0\x3f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\xff\xc0\xff\xc0\xc1\x80\xc3\x00\x02\x00\x04\x00'\ +b'\x0c\x00\x18\x00\x10\xc0\x20\xc0\x40\xc0\xff\xc0\xff\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\xfc\x00\xfc\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xfc\x00\xfc\x00'\ +b'\x00\x00\x0e\x00\xc0\x00\x40\x00\x60\x00\x20\x00\x30\x00\x30\x00'\ +b'\x18\x00\x18\x00\x0c\x00\x0c\x00\x04\x00\x06\x00\x02\x00\x03\x00'\ +b'\x03\x00\x01\x80\x01\x80\x00\xc0\x00\x00\x00\x00\x0e\x00\xfc\x00'\ +b'\xfc\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\xfc\x00\xfc\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x18\x00\x18\x00'\ +b'\x3c\x00\x24\x00\x66\x00\xc6\x00\xc3\x00\x83\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xff\xfc\xff\xfc\x00\x00\x00\x00\x00\x00\x0e\x00\xe0\x00\xf8\x00'\ +b'\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3e\x00\xff\x00\xc1\x80\x3f\x80\x7f\x80\xc1\x80\xc1\x80\xc3\x80'\ +b'\xff\x80\x7c\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\xf0\x00\xf0\x00\x30\x00\x30\x00\x30\x00\x37\xc0\x3f\xe0\x38\x60'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x38\x60\xff\xe0\xf3\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1f\x60\x7f\xe0\x70\xe0\xc0\x60\xc0\x00\xc0\x00'\ +b'\xc0\x00\x70\x60\x7f\xc0\x1f\x80\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x03\xc0\x03\xc0\x00\xc0\x00\xc0\x00\xc0\x3e\xc0'\ +b'\x7f\xc0\x61\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x61\xc0\x7f\xf0'\ +b'\x3c\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x7f\x80\x60\xc0\xff\xc0'\ +b'\xff\xc0\xc0\x00\xc0\x00\x60\xc0\x7f\xc0\x1f\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x0f\x80\x1f\xc0\x38\x40\x30\x00'\ +b'\x30\x00\xff\x80\xff\x80\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00'\ +b'\x30\x00\xff\x80\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x78\x7f\xf8'\ +b'\x60\xe0\xc0\x60\xc0\x60\xc0\x60\xc0\x60\x60\xe0\x7f\xe0\x1e\x60'\ +b'\x00\x60\x20\xe0\x3f\xc0\x1f\x80\x00\x00\x0e\x00\xf0\x00\xf0\x00'\ +b'\x30\x00\x30\x00\x30\x00\x37\xc0\x3f\xe0\x3c\x60\x38\x60\x30\x60'\ +b'\x30\x60\x30\x60\x30\x60\xfd\xf8\xfd\xf8\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x0c\x00\x0c\x00\x0c\x00\x00\x00\x00\x00'\ +b'\x7c\x00\x7c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\xff\xc0\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x03\x00\x03\x00\x03\x00\x00\x00\x00\x00\x7f\x00\x7f\x00\x03\x00'\ +b'\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00'\ +b'\xc3\x00\xfe\x00\x3c\x00\x00\x00\x0e\x00\xf0\x00\xf0\x00\x30\x00'\ +b'\x30\x00\x30\x00\x33\xe0\x33\xe0\x31\x80\x33\x00\x34\x00\x3a\x00'\ +b'\x31\x80\x30\xc0\xfd\xf0\xfd\xf0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x7c\x00\x7c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\xff\xc0'\ +b'\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x70\xff\xf8\x3b\x98\x33\x18'\ +b'\x33\x18\x33\x18\x33\x18\x33\x18\xfb\x9c\xfb\x9c\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf3\xc0\xf7\xe0\x38\x60\x38\x60\x30\x60\x30\x60\x30\x60'\ +b'\x30\x60\xfd\xf8\xfd\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x80\x3f\xc0'\ +b'\x70\xe0\xc0\x30\xc0\x30\xc0\x30\xc0\x30\x70\xe0\x3f\xc0\x1f\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\xf7\xc0\xff\xe0\x38\x60\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x38\x60\x3f\xe0\x37\xc0\x30\x00\x30\x00\xfc\x00'\ +b'\xfc\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3e\xf0\x7f\xf0\x61\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x61\xc0'\ +b'\x7f\xc0\x3c\xc0\x00\xc0\x00\xc0\x03\xf0\x03\xf0\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\xc0\xf7\xe0\x3e\x40'\ +b'\x38\x00\x30\x00\x30\x00\x30\x00\x30\x00\xff\x00\xff\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x7d\x80\xff\x80\xc1\x80\xc0\x80\x7c\x00\x03\x80'\ +b'\xc0\xc0\xe1\xc0\xff\xc0\xdf\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x30\x00\x30\x00\x30\x00\x30\x00\xff\x80'\ +b'\xff\x80\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\xc0\x1f\xc0'\ +b'\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xf1\xe0\xf1\xe0\x30\x60\x30\x60'\ +b'\x30\x60\x30\x60\x30\xe0\x30\xe0\x3f\xf8\x1e\x78\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xf8\xf0\xf9\xf0\x20\x60\x30\xc0\x10\xc0\x18\x80\x09\x80'\ +b'\x0d\x00\x0f\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x3c\xf8\x3c'\ +b'\x63\x18\x63\x18\x27\x90\x37\xb0\x34\xb0\x1c\xe0\x1c\xe0\x18\x60'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\xf9\xf0\xf9\xf0\x30\xc0\x19\x80\x0f\x00'\ +b'\x0f\x00\x19\x80\x30\xc0\xf9\xf0\xf9\xf0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xf8\xf0\xf9\xf0\x30\x60\x30\xc0\x18\xc0\x19\x80\x0d\x80\x07\x00'\ +b'\x07\x00\x06\x00\x0c\x00\x1c\x00\x78\x00\x70\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0\xff\x80\xc1\x00'\ +b'\xc2\x00\x04\x00\x18\x00\x30\xc0\x60\xc0\xff\xc0\xff\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x06\x00\x0e\x00\x18\x00'\ +b'\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x38\x00\xf0\x00\xf0\x00'\ +b'\x38\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x1e\x00\x0e\x00'\ +b'\x00\x00\x0e\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x0e\x00\xc0\x00'\ +b'\xe0\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x38\x00'\ +b'\x1e\x00\x1e\x00\x38\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00'\ +b'\xf0\x00\xe0\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x78\x40\xff\xc0\x87\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ + +_index =\ +b'\x00\x00\x2a\x00\x54\x00\x7e\x00\xa8\x00\xd2\x00\xfc\x00\x26\x01'\ +b'\x50\x01\x7a\x01\xa4\x01\xce\x01\xf8\x01\x22\x02\x4c\x02\x76\x02'\ +b'\xa0\x02\xca\x02\xf4\x02\x1e\x03\x48\x03\x72\x03\x9c\x03\xc6\x03'\ +b'\xf0\x03\x1a\x04\x44\x04\x6e\x04\x98\x04\xc2\x04\xec\x04\x16\x05'\ +b'\x40\x05\x6a\x05\x94\x05\xbe\x05\xe8\x05\x12\x06\x3c\x06\x66\x06'\ +b'\x90\x06\xba\x06\xe4\x06\x0e\x07\x38\x07\x62\x07\x8c\x07\xb6\x07'\ +b'\xe0\x07\x0a\x08\x34\x08\x5e\x08\x88\x08\xb2\x08\xdc\x08\x06\x09'\ +b'\x30\x09\x5a\x09\x84\x09\xae\x09\xd8\x09\x02\x0a\x2c\x0a\x56\x0a'\ +b'\x80\x0a\xaa\x0a\xd4\x0a\xfe\x0a\x28\x0b\x52\x0b\x7c\x0b\xa6\x0b'\ +b'\xd0\x0b\xfa\x0b\x24\x0c\x4e\x0c\x78\x0c\xa2\x0c\xcc\x0c\xf6\x0c'\ +b'\x20\x0d\x4a\x0d\x74\x0d\x9e\x0d\xc8\x0d\xf2\x0d\x1c\x0e\x46\x0e'\ +b'\x70\x0e\x9a\x0e\xc4\x0e\xee\x0e\x18\x0f\x42\x0f\x6c\x0f\x96\x0f'\ +b'\xc0\x0f' + +_mvfont = memoryview(_font) + +def _chr_addr(ordch): + offset = 2 * (ordch - 32) + return int.from_bytes(_index[offset:offset + 2], 'little') + +def get_ch(ch): + ordch = ord(ch) + ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 + offset = _chr_addr(ordch) + width = int.from_bytes(_font[offset:offset + 2], 'little') + next_offs = _chr_addr(ordch +1) + return _mvfont[offset + 2:next_offs], 20, width + diff --git a/drivers/ADAFRUIT.md b/drivers/ADAFRUIT.md new file mode 100644 index 0000000..7f8ff5e --- /dev/null +++ b/drivers/ADAFRUIT.md @@ -0,0 +1,100 @@ +# Adafruit and other OLED displays + +###### [Main README](../README.md) + +# SPI Pin names and wiring + +The names used on the Pyboard are the correct names for SPI signals. Some OLED +displays use different names. Adafruit use abbreviated names where space is at +a premium. The following table shows the correct names followed by others I +have seen. The column labelled "Adafruit" references pin numbers on the 1.27 +and 1.5 inch displays. Pin numbering on the 0.96 inch display differs: pin +names are as below (SCK is CLK on this unit). + +Pyboard pins are for SPI(1). Adapt for SPI(2) or other hardware. + +| Pin | Pyboard | Display | Adafruit | Alternative names | +|:---:|:-------:|:-------:|:--------:|:---------:| +| 3V3 | 3V3 | | Vin (10) | | +| Gnd | Gnd | | Gnd (11) | | +| X1 | X1 | | DC (3) | | +| X2 | X2 | | CS (5) | OC OLEDCS | +| X3 | X3 | | Rst (4) | R RESET | +| X6 | SCK | SCK | CL (2) | SCK CLK | +| X8 | MOSI | MOSI | SI (1) | DATA SI | +| X7 | MISO | MISO | SO (7) | MISO (see below) | +| X21 | X21 | | SC (6) | SDCS (see below) | + +The last two pins above are specific to Adafruit 1.27 and 1.5 inch displays and +only need to be connected if the SD card is to be used. The pin labelled CD on +those displays is a card detect signal; it can be ignored. The pin labelled 3Vo +is an output: these displays can be powered from +5V. + +Pyboard pins are arbitrary with the exception of MOSI, SCK and MISO. These can +be changed if software SPI is used. + +# I2C pin names and wiring + +I2C is generally only available on monochrome displays. Monochrome OLED panels +typically use the SSD1306 chip which is +[officially supported](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). +At the time of writing (Sept 2018) this works only with software SPI. See +[this issue](https://github.com/micropython/micropython/pull/4020). Wiring +details: + +| Pin | Pyboard | Display | +|:---:|:-------:|:-------:| +| 3V3 | 3V3 | Vin | +| Gnd | Gnd | Gnd | +| Y9 | SCL | CLK | +| Y10 | SDA | DATA | + +Typical initialisation on a Pyboard: +```python +pscl = machine.Pin('Y9', machine.Pin.OPEN_DRAIN) +psda = machine.Pin('Y10', machine.Pin.OPEN_DRAIN) +i2c = machine.I2C(scl=pscl, sda=psda) +``` + +# Adafruit - use of the onboard SD card + +If the SD card is to be used, the official `scdard.py` driver should be +employed. This may be found +[here](https://github.com/micropython/micropython/tree/master/drivers/sdcard). +Note that `sdtest.py` initialises the SPI bus before accessing the SD card. +This is necessary because the display drivers use a high baudrate unsupported +by SD cards. Ensure applications do this before the first SD card access and +before subsequent ones if the display has been refreshed. + +# Hardware note: SPI clock rate + +For performance reasons the drivers for the Adafruit color displays run the SPI +bus at a high rate (currently 10.5MHz). Leads should be short and direct. An +attempt to use 21MHz failed. The datasheet limit is 20MHz. Whether a 5% +overclock caused this is moot: with very short leads or a PCB this might well +work. Note that the Pyboard hardware SPI supports only 10.5MHz and 21MHz. + +In practice the 41ms update time is visually fast for most purposes except some +games. + +# Notes on OLED displays + +## Power consumption + +The power consumption of OLED displays is roughly proportional to the number +and brightness of illuminated pixels. I tested a 1.27 inch Adafruit display +running the `clock.py` demo. It consumed 19.7mA. Initial current with screen +blank was 3.3mA. + +## Wearout + +OLED displays suffer gradual loss of luminosity over long periods of +illumination. Wikipedia refers to 15,000 hours for significant loss, which +equates to 1.7 years of 24/7 usage. However it also refers to fabrication +techniques which ameliorate this which implies the likelihood of better +figures. I have not seen figures for the Adafruit displays. + +Options are to blank the display when not required, or to design screens where +the elements are occasionally moved slightly to preserve individual pixels. + +###### [Main README](../README.md) diff --git a/drivers/ssd1331/ssd1331.py b/drivers/ssd1331/ssd1331.py new file mode 100644 index 0000000..c3603c0 --- /dev/null +++ b/drivers/ssd1331/ssd1331.py @@ -0,0 +1,92 @@ +# SSD1331.py MicroPython driver for Adafruit 0.96" OLED display +# https://www.adafruit.com/product/684 + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Show command +# 0x15, 0, 0x5f, 0x75, 0, 0x3f Col 0-95 row 0-63 + +# Initialisation command +# 0xae display off (sleep mode) +# 0xa0, 0x32 256 color RGB, horizontal RAM increment +# 0xa1, 0x00 Startline row 0 +# 0xa2, 0x00 Vertical offset 0 +# 0xa4 Normal display +# 0xa8, 0x3f Set multiplex ratio +# 0xad, 0x8e Ext supply +# 0xb0, 0x0b Disable power save mode +# 0xb1, 0x31 Phase period +# 0xb3, 0xf0 Oscillator frequency +# 0x8a, 0x64, 0x8b, 0x78, 0x8c, 0x64, # Precharge +# 0xbb, 0x3a Precharge voltge +# 0xbe, 0x3e COM deselect level +# 0x87, 0x06 master current attenuation factor +# 0x81, 0x91 contrast for all color "A" segment +# 0x82, 0x50 contrast for all color "B" segment +# 0x83, 0x7d contrast for all color "C" segment +# 0xaf Display on + +import framebuf +import utime +import gc + +class SSD1331(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # acceptable to hardware: rrrgggbb + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) + + def __init__(self, spi, pincs, pindc, pinrs, height=64, width=96): + self.spi = spi + self.rate = 6660000 # Data sheet: 150ns min clock period + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + # Save color mode for use by writer_gui (blit) + self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + gc.collect() + self.buffer = bytearray(self.height * self.width) + super().__init__(self.buffer, self.width, self.height, self.mode) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + self._write(b'\xae\xa0\x32\xa1\x00\xa2\x00\xa4\xa8\x3f\xad\x8e\xb0'\ + b'\x0b\xb1\x31\xb3\xf0\x8a\x64\x8b\x78\x8c\x64\xbb\x3a\xbe\x3e\x87'\ + b'\x06\x81\x91\x82\x50\x83\x7d\xaf', 0) + gc.collect() + self.show() + + def _write(self, buf, dc): + self.spi.init(baudrate=self.rate, polarity=1, phase=1) + self.pincs(1) + self.pindc(dc) + self.pincs(0) + self.spi.write(buf) + self.pincs(1) + + def show(self, _cmd=b'\x15\x00\x5f\x75\x00\x3f'): # Pre-allocate + self._write(_cmd, 0) + self._write(self.buffer, 1) diff --git a/drivers/ssd1351/README.md b/drivers/ssd1351/README.md new file mode 100644 index 0000000..96b9b3a --- /dev/null +++ b/drivers/ssd1351/README.md @@ -0,0 +1,20 @@ +# Drivers for SSD1351 + +There are two versions. + * `ssd1351.py` This is optimised for STM (e.g. Pyboard) platforms. + * `ssd1351_generic.py` Cross-platform version. + +The cross-platform version includes the `micropythn.viper` decorator. If your +platform does not support this, comment it out and remove the type annotations. +You may be able to use the native decorator. + +If the platform supports the viper emitter performance should still be good: on +a Pyboard V1 this driver perorms a refresh of a 128*128 color display in 47ms. +The STM version is faster but not by a large margin: a refresh takes 41ms. 32ms +of these figures is consumed by the data transfer over the SPI interface. + +If the viper and native decorators are unsupported a screen redraw takes 272ms +(on Pyboard 1.0) which is visibly slow. + +This driver was tested on official Adafruit 1.5 and 1.27 inch displays, also a +Chinese 1.5 inch unit. diff --git a/drivers/ssd1351/ssd1351.py b/drivers/ssd1351/ssd1351.py new file mode 100644 index 0000000..e3f9fad --- /dev/null +++ b/drivers/ssd1351/ssd1351.py @@ -0,0 +1,160 @@ +# SSD1351.py MicroPython driver for Adafruit color OLED displays. +# STM (Pyboard etc) version. Display refresh takes 41ms on Pyboard V1.0 + +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 +# For wiring details see drivers/ADAFRUIT.md in this repo. + +# This driver is based on the Adafruit C++ library for Arduino +# https://github.com/adafruit/Adafruit-SSD1351-library.git + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import framebuf +import utime +import gc +import micropython +from uctypes import addressof + +# Timings with standard emitter +# 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms +# Buffer transfer time = 272-240 = 32ms which accords with expected: +# 128*128*2/10500000 = 31.2ms (2 bytes/pixel, baudrate = 10.5MHz) +# With assembler .show() takes 41ms + +# Copy a buffer with 8 bit rrrgggbb pixels to a buffer of 16 bit pixels. +@micropython.asm_thumb +def _lcopy(r0, r1, r2): # r0 dest, r1 source, r2 no. of bytes + label(LOOP) + ldrb(r3, [r1, 0]) # Get source byte to r3, r5, r6 + mov(r5, r3) + mov(r6, r3) + mov(r4, 3) + and_(r3, r4) + mov(r4, 6) + lsl(r3, r4) + mov(r4, 0x1c) + and_(r5, r4) + mov(r4, 2) + lsr(r5, r4) + orr(r3, r5) + strb(r3, [r0, 0]) + mov(r4, 0xe0) + and_(r6, r4) + mov(r4, 2) + lsr(r6, r4) + strb(r6, [r0, 1]) + add(r0, 2) + add(r1, 1) + sub(r2, 1) + bne(LOOP) + +# Initialisation commands in cmd_init: +# 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode +# 0xae, # display off (sleep mode) +# 0xb3, 0xf1, # clock div +# 0xca, 0x7f, # mux ratio +# 0xa0, 0x74, # setremap 0x74 +# 0x15, 0, 0x7f, # setcolumn +# 0x75, 0, 0x7f, # setrow +# 0xa1, 0, # set display start line +# 0xa2, 0, # displayoffset +# 0xb5, 0, # setgpio +# 0xab, 1, # functionselect: serial interface, internal Vdd regulator +# 0xb1, 0x32, # Precharge +# 0xbe, 0x05, # vcommh +# 0xa6, # normaldisplay +# 0xc1, 0xc8, 0x80, 0xc8, # contrast abc +# 0xc7, 0x0f, # Master contrast +# 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) +# 0xb6, 1, # Precharge 2 +# 0xaf, # Display on + +# SPI baudrate: Pyboard can produce 10.5MHz or 21MHz. Datasheet gives max of 20MHz. +# Attempt to use 21MHz failed but might work on a PCB or with very short leads. +class SSD1351(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # acceptable to hardware: rrrgggbb + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) + + def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.rate = 11000000 # See baudrate note above. + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + # Save color mode for use by writer_gui (blit) + self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + gc.collect() + self.buffer = bytearray(self.height * self.width) + super().__init__(self.buffer, self.width, self.height, self.mode) + self.linebuf = bytearray(self.width * 2) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + # See above comment to explain this allocation-saving gibberish. + self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ + b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ + b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ + b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) + self.show() + gc.collect() + + def _write(self, buf, dc): + self.spi.init(baudrate=self.rate, polarity=1, phase=1) + self.pincs(1) + self.pindc(dc) + self.pincs(0) + self.spi.write(buf) + self.pincs(1) + + # Write lines from the framebuf out of order to match the mapping of the + # SSD1351 RAM to the OLED device. + def show(self): + lb = self.linebuf + buf = self.buffer + self._write(b'\x5c', 0) # Enable data write + if self.height == 128: + for l in range(128): + l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126 .. 96 + start = l0 * self.width + _lcopy(lb, addressof(buf) + start, self.width) + self._write(lb, 1) # Send a line + else: + for l in range(128): + if l < 64: + start = (63 -l) * self.width # 63 62 .. 1 0 + _lcopy(lb, addressof(buf) + start, self.width) + self._write(lb, 1) # Send a line + elif l < 96: # This is daft but I can't get setrow to work + self._write(lb, 1) # Let RAM counter increase + else: + start = (191 - l) * self.width # 127 126 .. 95 + _lcopy(lb, addressof(buf) + start, self.width) + self._write(lb, 1) # Send a line diff --git a/drivers/ssd1351/ssd1351_generic.py b/drivers/ssd1351/ssd1351_generic.py new file mode 100644 index 0000000..2f9d09c --- /dev/null +++ b/drivers/ssd1351/ssd1351_generic.py @@ -0,0 +1,141 @@ +# SSD1351_generic.py MicroPython driver for Adafruit color OLED displays. +# This is cross-platform. It lacks STM optimisations and is slower than the +# standard version. + +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 +# For wiring details see drivers/ADAFRUIT.md in this repo. + +# This driver is based on the Adafruit C++ library for Arduino +# https://github.com/adafruit/Adafruit-SSD1351-library.git +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import framebuf +import utime +import gc +import micropython +from uctypes import addressof + +# Timings with standard emitter +# 1.86ms * 128 lines = 240ms. copy dominates: show() took 272ms +# Buffer transfer time = 272-240 = 32ms which accords with expected: +# 128*128*2/10500000 = 31.2ms (2 bytes/pixel, baudrate = 10.5MHz) +# With viper emitter show() takes 47ms vs 41ms for assembler. + +@micropython.viper +def _lcopy(dest:ptr8, source:ptr8, length:int): + n = 0 + for x in range(length): + c = source[x] + dest[n] = ((c & 3) << 6) | ((c & 0x1c) >> 2) # Blue green + n += 1 + dest[n] = (c & 0xe0) >> 3 # Red + n += 1 + +# Initialisation commands in cmd_init: +# 0xfd, 0x12, 0xfd, 0xb1, # Unlock command mode +# 0xae, # display off (sleep mode) +# 0xb3, 0xf1, # clock div +# 0xca, 0x7f, # mux ratio +# 0xa0, 0x74, # setremap 0x74 +# 0x15, 0, 0x7f, # setcolumn +# 0x75, 0, 0x7f, # setrow +# 0xa1, 0, # set display start line +# 0xa2, 0, # displayoffset +# 0xb5, 0, # setgpio +# 0xab, 1, # functionselect: serial interface, internal Vdd regulator +# 0xb1, 0x32, # Precharge +# 0xbe, 0x05, # vcommh +# 0xa6, # normaldisplay +# 0xc1, 0xc8, 0x80, 0xc8, # contrast abc +# 0xc7, 0x0f, # Master contrast +# 0xb4, 0xa0, 0xb5, 0x55, # set vsl (see datasheet re ext circuit) +# 0xb6, 1, # Precharge 2 +# 0xaf, # Display on + +class SSD1351(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # acceptable to hardware: rrrgggbb + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) + + def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.rate = 20000000 # Data sheet: should support 20MHz + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + # Save color mode for use by writer_gui (blit) + self.mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + gc.collect() + self.buffer = bytearray(self.height * self.width) + super().__init__(self.buffer, self.width, self.height, self.mode) + self.linebuf = bytearray(self.width * 2) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + # See above comment to explain this allocation-saving gibberish. + self._write(b'\xfd\x12\xfd\xb1\xae\xb3\xf1\xca\x7f\xa0\x74'\ + b'\x15\x00\x7f\x75\x00\x7f\xa1\x00\xa2\x00\xb5\x00\xab\x01'\ + b'\xb1\x32\xbe\x05\xa6\xc1\xc8\x80\xc8\xc7\x0f'\ + b'\xb4\xa0\xb5\x55\xb6\x01\xaf', 0) + gc.collect() + self.show() + + def _write(self, buf, dc): + self.spi.init(baudrate=self.rate, polarity=1, phase=1) + self.pincs(1) + self.pindc(dc) + self.pincs(0) + self.spi.write(buf) + self.pincs(1) + + # Write lines from the framebuf out of order to match the mapping of the + # SSD1351 RAM to the OLED device. + def show(self): + lb = self.linebuf + buf = memoryview(self.buffer) + self._write(b'\x5c', 0) # Enable data write + if self.height == 128: + for l in range(128): + l0 = (95 - l) % 128 # 95 94 .. 1 0 127 126... + start = l0 * self.width + _lcopy(lb, buf[start : start + self.width], self.width) + self._write(lb, 1) # Send a line + else: + for l in range(128): + if l < 64: + start = (63 -l) * self.width + _lcopy(lb, buf[start : start + self.width], self.width) + self._write(lb, 1) # Send a line + elif l < 96: # This is daft but I can't get setrow to work + self._write(lb, 1) # Let RAM counter increase + else: + start = (191 - l) * self.width + _lcopy(lb, buf[start : start + self.width], self.width) + self._write(lb, 1) # Send a line diff --git a/drivers/ssd1351/test128_row.py b/drivers/ssd1351/test128_row.py new file mode 100644 index 0000000..5bc67af --- /dev/null +++ b/drivers/ssd1351/test128_row.py @@ -0,0 +1,18 @@ +# test128_row.py Test for device driver on 96 row display +import machine +from ssd1351 import SSD1351 as SSD + +# Initialise hardware +def setup(): + pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) + pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) + prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) + spi = machine.SPI(1) + ssd = SSD(spi, pcs, pdc, prst) # Create a display instance + return ssd + +ssd = setup() +ssd.fill(0) +ssd.line(0, 0, 127, 127, ssd.rgb(0, 255, 0)) +ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) +ssd.show() diff --git a/drivers/ssd1351/test96_row.py b/drivers/ssd1351/test96_row.py new file mode 100644 index 0000000..2e37f73 --- /dev/null +++ b/drivers/ssd1351/test96_row.py @@ -0,0 +1,18 @@ +# test96.py Test for device driver on 96 row display +import machine +from ssd1351 import SSD1351 as SSD + +# Initialise hardware +def setup(): + pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) + pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) + prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) + spi = machine.SPI(1) + ssd = SSD(spi, pcs, pdc, prst, height=96) # Create a display instance + return ssd + +ssd = setup() +ssd.fill(0) +ssd.line(0, 0, 127, 95, ssd.rgb(0, 255, 0)) +ssd.rect(0, 0, 15, 15, ssd.rgb(255, 0, 0)) +ssd.show() diff --git a/font6.py b/font6.py new file mode 100644 index 0000000..0adc524 --- /dev/null +++ b/font6.py @@ -0,0 +1,176 @@ +# Code generated by font-to-py.py. +# Font: FreeSans.ttf +version = '0.2' + +def height(): + return 14 + +def max_width(): + return 14 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 126 + +_font =\ +b'\x08\x00\x00\x78\x8c\x84\x04\x18\x30\x20\x20\x00\x20\x00\x00\x00'\ +b'\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x05\x00\x00\x80\x80\x80\x80\x80\x80\x80\x80\x00\x80\x00\x00\x00'\ +b'\x05\x00\x00\xa0\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x08\x00\x00\x00\x12\x14\x7f\x24\x24\xfe\x28\x48\x48\x00\x00\x00'\ +b'\x08\x00\x20\x78\xac\xa4\xa0\xa0\x78\x2c\xa4\xac\x78\x20\x00\x00'\ +b'\x0c\x00\x00\x00\x70\x80\x89\x00\x89\x00\x8a\x00\x72\x00\x04\xe0'\ +b'\x05\x10\x09\x10\x09\x10\x10\xe0\x00\x00\x00\x00\x00\x00\x09\x00'\ +b'\x00\x00\x30\x00\x48\x00\x48\x00\x78\x00\x20\x00\x52\x00\x9e\x00'\ +b'\x8c\x00\x8e\x00\x73\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x80'\ +b'\x80\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20'\ +b'\x40\x40\x80\x80\x80\x80\x80\x80\x80\x40\x40\x20\x05\x00\x00\x80'\ +b'\x40\x40\x20\x20\x20\x20\x20\x20\x20\x40\x40\x80\x05\x00\x00\x20'\ +b'\xf8\x20\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\x00\x00\x20\x20\xf8\x20\x20\x20\x00\x00\x00\x04\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x80\x80\x80\x00\x05\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x04\x00\x00\x10'\ +b'\x10\x20\x20\x20\x40\x40\x40\x80\x80\x00\x00\x00\x08\x00\x00\x78'\ +b'\x48\x84\x84\x84\x84\x84\x84\x48\x78\x00\x00\x00\x08\x00\x00\x20'\ +b'\x60\xe0\x20\x20\x20\x20\x20\x20\x20\x00\x00\x00\x08\x00\x00\x78'\ +b'\xcc\x84\x04\x0c\x18\x60\x40\x80\xfc\x00\x00\x00\x08\x00\x00\x78'\ +b'\xc4\x84\x04\x38\x04\x04\x84\xcc\x78\x00\x00\x00\x08\x00\x00\x08'\ +b'\x18\x38\x28\x48\x88\xfc\x08\x08\x08\x00\x00\x00\x08\x00\x00\x7c'\ +b'\x80\x80\xb8\xcc\x04\x04\x04\x88\x78\x00\x00\x00\x08\x00\x00\x38'\ +b'\x48\x84\x80\xf8\xcc\x84\x84\x4c\x78\x00\x00\x00\x08\x00\x00\xfc'\ +b'\x0c\x08\x10\x10\x20\x20\x20\x40\x40\x00\x00\x00\x08\x00\x00\x78'\ +b'\x84\x84\x84\x78\xcc\x84\x84\xcc\x78\x00\x00\x00\x08\x00\x00\x78'\ +b'\xc8\x84\x84\xcc\x74\x04\x04\x88\x70\x00\x00\x00\x04\x00\x00\x00'\ +b'\x00\x80\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00'\ +b'\x00\x00\x80\x00\x00\x00\x00\x00\x80\x80\x80\x00\x08\x00\x00\x00'\ +b'\x00\x00\x00\x1c\x70\x80\x60\x1c\x04\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\x00\x00\x00\xfc\x00\xfc\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\x00\x00\xe0\x38\x06\x1c\x60\x80\x00\x00\x00\x08\x00\x00\x78'\ +b'\x8c\x84\x04\x18\x30\x20\x20\x00\x20\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x07\xc0\x18\x60\x20\x10\x43\x48\x84\xc8\x88\xc8\x88\x88\x89\x90'\ +b'\xc6\xe0\x60\x00\x30\x00\x0f\xc0\x00\x00\x09\x00\x00\x00\x0c\x00'\ +b'\x1c\x00\x14\x00\x16\x00\x32\x00\x22\x00\x7f\x00\x41\x00\x41\x80'\ +b'\xc1\x80\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\xfc\x00\x82\x00'\ +b'\x82\x00\x82\x00\xfc\x00\x86\x00\x82\x00\x82\x00\x86\x00\xfc\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x3c\x00\x42\x00\x41\x00'\ +b'\x80\x00\x80\x00\x80\x00\x81\x00\xc1\x00\x62\x00\x3c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0a\x00\x00\x00\xfc\x00\x82\x00\x83\x00\x81\x00'\ +b'\x81\x00\x81\x00\x81\x00\x83\x00\x82\x00\xfc\x00\x00\x00\x00\x00'\ +b'\x00\x00\x09\x00\x00\x00\xfe\x00\x80\x00\x80\x00\x80\x00\xfc\x00'\ +b'\x80\x00\x80\x00\x80\x00\x80\x00\xfe\x00\x00\x00\x00\x00\x00\x00'\ +b'\x08\x00\x00\xfc\x80\x80\x80\xfc\x80\x80\x80\x80\x80\x00\x00\x00'\ +b'\x0b\x00\x00\x00\x1e\x00\x61\x00\x40\x80\x80\x00\x80\x00\x87\x80'\ +b'\x80\x80\xc0\x80\x61\x80\x3e\x80\x00\x00\x00\x00\x00\x00\x0a\x00'\ +b'\x00\x00\x81\x00\x81\x00\x81\x00\x81\x00\xff\x00\x81\x00\x81\x00'\ +b'\x81\x00\x81\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x80'\ +b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x07\x00\x00\x04'\ +b'\x04\x04\x04\x04\x04\x04\x84\x84\x78\x00\x00\x00\x09\x00\x00\x00'\ +b'\x82\x00\x84\x00\x88\x00\x90\x00\xb0\x00\xd8\x00\x88\x00\x84\x00'\ +b'\x86\x00\x82\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x80\x80\x80'\ +b'\x80\x80\x80\x80\x80\x80\xfc\x00\x00\x00\x0c\x00\x00\x00\xc1\x80'\ +b'\xc1\x80\xc1\x80\xa2\x80\xa2\x80\xa2\x80\x94\x80\x94\x80\x94\x80'\ +b'\x88\x80\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\xc1\x00\xc1\x00'\ +b'\xe1\x00\xb1\x00\x91\x00\x89\x00\x8d\x00\x87\x00\x83\x00\x83\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x3e\x00\x63\x00\xc1\x00'\ +b'\x80\x80\x80\x80\x80\x80\x80\x80\xc1\x00\x63\x00\x3e\x00\x00\x00'\ +b'\x00\x00\x00\x00\x09\x00\x00\x00\xfc\x00\x86\x00\x82\x00\x82\x00'\ +b'\x86\x00\xfc\x00\x80\x00\x80\x00\x80\x00\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0b\x00\x00\x00\x3e\x00\x63\x00\xc1\x00\x80\x80\x80\x80'\ +b'\x80\x80\x80\x80\xc5\x80\x63\x00\x3f\x00\x00\x80\x00\x00\x00\x00'\ +b'\x0a\x00\x00\x00\xfc\x00\x82\x00\x82\x00\x82\x00\x82\x00\xfc\x00'\ +b'\x82\x00\x82\x00\x82\x00\x83\x00\x00\x00\x00\x00\x00\x00\x09\x00'\ +b'\x00\x00\x7c\x00\xc6\x00\x82\x00\xc0\x00\x78\x00\x0e\x00\x02\x00'\ +b'\x82\x00\xc6\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00'\ +b'\xfe\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00\x10\x00'\ +b'\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x81\x00'\ +b'\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\x81\x00\xc3\x00'\ +b'\x3c\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\xc1\x80\x41\x00'\ +b'\x41\x00\x63\x00\x22\x00\x32\x00\x16\x00\x14\x00\x1c\x00\x08\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\xc2\x18\x45\x18\x45\x10'\ +b'\x65\x10\x65\xb0\x28\xa0\x28\xa0\x38\xa0\x38\xe0\x10\x40\x00\x00'\ +b'\x00\x00\x00\x00\x09\x00\x00\x00\x41\x00\x63\x00\x32\x00\x14\x00'\ +b'\x0c\x00\x1c\x00\x16\x00\x22\x00\x63\x00\x41\x80\x00\x00\x00\x00'\ +b'\x00\x00\x09\x00\x00\x00\xc1\x80\x63\x00\x22\x00\x36\x00\x14\x00'\ +b'\x08\x00\x08\x00\x08\x00\x08\x00\x08\x00\x00\x00\x00\x00\x00\x00'\ +b'\x09\x00\x00\x00\x7f\x00\x03\x00\x06\x00\x04\x00\x0c\x00\x18\x00'\ +b'\x30\x00\x20\x00\x40\x00\xff\x00\x00\x00\x00\x00\x00\x00\x04\x00'\ +b'\x00\xc0\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\xc0\x04\x00'\ +b'\x00\x80\x80\x40\x40\x40\x20\x20\x20\x10\x10\x00\x00\x00\x04\x00'\ +b'\x00\xc0\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xc0\x07\x00'\ +b'\x00\x20\x60\x50\x90\x88\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x04\x00'\ +b'\x00\x40\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00'\ +b'\x00\x00\x00\x78\x84\x04\x04\x7c\x84\x8c\x76\x00\x00\x00\x08\x00'\ +b'\x00\x80\x80\xb8\xcc\x84\x84\x84\x84\xc8\xb8\x00\x00\x00\x07\x00'\ +b'\x00\x00\x00\x78\x44\x80\x80\x80\x80\x44\x78\x00\x00\x00\x08\x00'\ +b'\x00\x02\x02\x3a\x46\x82\x82\x82\x82\x46\x3a\x00\x00\x00\x07\x00'\ +b'\x00\x00\x00\x3c\x44\x82\xfe\x80\x80\x46\x3c\x00\x00\x00\x04\x00'\ +b'\x00\x60\x40\xe0\x40\x40\x40\x40\x40\x40\x40\x00\x00\x00\x08\x00'\ +b'\x00\x00\x00\x3a\x46\x82\x82\x82\x82\x46\x7a\x02\x84\x7c\x08\x00'\ +b'\x00\x80\x80\xb0\xc8\x88\x88\x88\x88\x88\x88\x00\x00\x00\x03\x00'\ +b'\x00\x80\x00\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x03\x00'\ +b'\x00\x40\x00\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\xc0\x07\x00'\ +b'\x00\x80\x80\x88\x90\xa0\xe0\x90\x98\x88\x8c\x00\x00\x00\x03\x00'\ +b'\x00\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x00\x00\x00\x0b\x00'\ +b'\x00\x00\x00\x00\x00\x00\xb7\x00\xcc\x80\x88\x80\x88\x80\x88\x80'\ +b'\x88\x80\x88\x80\x88\x80\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\xb8\xc4\x84\x84\x84\x84\x84\x84\x00\x00\x00\x07\x00\x00\x00'\ +b'\x00\x38\x44\x82\x82\x82\x82\x44\x38\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\xb8\xc8\x84\x84\x84\x84\xc8\xb8\x80\x80\x00\x08\x00\x00\x00'\ +b'\x00\x3a\x46\x82\x82\x82\x82\x46\x7a\x02\x02\x00\x05\x00\x00\x00'\ +b'\x00\xa0\xc0\x80\x80\x80\x80\x80\x80\x00\x00\x00\x07\x00\x00\x00'\ +b'\x00\x70\x88\x80\xc0\x70\x08\x88\x70\x00\x00\x00\x04\x00\x00\x00'\ +b'\x40\xe0\x40\x40\x40\x40\x40\x40\x60\x00\x00\x00\x08\x00\x00\x00'\ +b'\x00\x84\x84\x84\x84\x84\x84\x8c\x74\x00\x00\x00\x07\x00\x00\x00'\ +b'\x00\xc6\x44\x44\x6c\x28\x28\x38\x10\x00\x00\x00\x0a\x00\x00\x00'\ +b'\x00\x00\x00\x00\x8c\x40\xcc\xc0\x4c\x80\x5c\x80\x52\x80\x73\x80'\ +b'\x33\x00\x33\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x44'\ +b'\x68\x28\x30\x30\x28\x4c\xc4\x00\x00\x00\x07\x00\x00\x00\x00\xc6'\ +b'\x44\x44\x6c\x28\x28\x30\x10\x10\x20\x60\x07\x00\x00\x00\x00\x7c'\ +b'\x0c\x08\x10\x30\x60\x40\xfc\x00\x00\x00\x05\x00\x00\x60\x40\x40'\ +b'\x40\x40\x40\x80\x40\x40\x40\x40\x40\x60\x04\x00\x00\x80\x80\x80'\ +b'\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x05\x00\x00\xc0\x40\x40'\ +b'\x40\x40\x40\x20\x40\x40\x40\x40\x40\xc0\x07\x00\x00\x00\x00\x00'\ +b'\x00\x62\x9e\x00\x00\x00\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\x10\x00\x20\x00\x30\x00\x40\x00\x50\x00\x60\x00\x7e\x00'\ +b'\x9c\x00\xac\x00\xbc\x00\xcc\x00\xdc\x00\xec\x00\xfc\x00\x0c\x01'\ +b'\x1c\x01\x2c\x01\x3c\x01\x4c\x01\x5c\x01\x6c\x01\x7c\x01\x8c\x01'\ +b'\x9c\x01\xac\x01\xbc\x01\xcc\x01\xdc\x01\xec\x01\xfc\x01\x0c\x02'\ +b'\x1c\x02\x2c\x02\x4a\x02\x68\x02\x86\x02\xa4\x02\xc2\x02\xe0\x02'\ +b'\xf0\x02\x0e\x03\x2c\x03\x3c\x03\x4c\x03\x6a\x03\x7a\x03\x98\x03'\ +b'\xb6\x03\xd4\x03\xf2\x03\x10\x04\x2e\x04\x4c\x04\x6a\x04\x88\x04'\ +b'\xa6\x04\xc4\x04\xe2\x04\x00\x05\x1e\x05\x2e\x05\x3e\x05\x4e\x05'\ +b'\x5e\x05\x6e\x05\x7e\x05\x8e\x05\x9e\x05\xae\x05\xbe\x05\xce\x05'\ +b'\xde\x05\xee\x05\xfe\x05\x0e\x06\x1e\x06\x2e\x06\x3e\x06\x5c\x06'\ +b'\x6c\x06\x7c\x06\x8c\x06\x9c\x06\xac\x06\xbc\x06\xcc\x06\xdc\x06'\ +b'\xec\x06\x0a\x07\x1a\x07\x2a\x07\x3a\x07\x4a\x07\x5a\x07\x6a\x07'\ +b'\x7a\x07' + +_mvfont = memoryview(_font) + +def _chr_addr(ordch): + offset = 2 * (ordch - 32) + return int.from_bytes(_index[offset:offset + 2], 'little') + +def get_ch(ch): + ordch = ord(ch) + ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 + offset = _chr_addr(ordch) + width = int.from_bytes(_font[offset:offset + 2], 'little') + next_offs = _chr_addr(ordch +1) + return _mvfont[offset + 2:next_offs], 14, width + diff --git a/freesans20.py b/freesans20.py new file mode 100644 index 0000000..1f6da29 --- /dev/null +++ b/freesans20.py @@ -0,0 +1,288 @@ +# Code generated by font-to-py.py. +# Font: FreeSans.ttf +version = '0.25' + +def height(): + return 20 + +def max_width(): + return 20 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 126 + +_font =\ +b'\x0b\x00\x00\x00\x3c\x00\x7e\x00\xc7\x00\xc3\x00\x03\x00\x03\x00'\ +b'\x06\x00\x0c\x00\x08\x00\x18\x00\x18\x00\x00\x00\x00\x00\x18\x00'\ +b'\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x07\x00\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00'\ +b'\xc0\xc0\x00\x00\x00\x00\x07\x00\x00\x00\xd8\xd8\xd8\xd8\x90\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x0c\xc0\x08\x80\x08\x80\x7f\xe0\x7f\xe0\x19\x80\x11\x00'\ +b'\x11\x00\xff\xc0\xff\xc0\x33\x00\x33\x00\x22\x00\x22\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0b\x00\x08\x00\x3e\x00\x7f\x80\xe9\xc0'\ +b'\xc8\xc0\xc8\xc0\xc8\x00\xe8\x00\x7c\x00\x1f\x80\x09\xc0\x08\xc0'\ +b'\xc8\xc0\xe9\xc0\x7f\x80\x3e\x00\x08\x00\x00\x00\x00\x00\x00\x00'\ +b'\x12\x00\x00\x00\x00\x00\x00\x00\x38\x10\x00\x7c\x10\x00\xc6\x20'\ +b'\x00\xc6\x20\x00\xc6\x40\x00\x7c\xc0\x00\x38\x80\x00\x01\x1e\x00'\ +b'\x01\x3f\x00\x02\x73\x80\x02\x61\x80\x04\x73\x80\x04\x3f\x00\x08'\ +b'\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00'\ +b'\x00\x00\x00\x00\x0e\x00\x1f\x00\x31\x80\x31\x80\x31\x80\x1f\x00'\ +b'\x1c\x00\x76\x60\xe3\x60\xc1\xc0\xc0\xc0\xe1\xc0\x7f\x60\x3e\x30'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\xc0\xc0\xc0\xc0'\ +b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00'\ +b'\x00\x10\x10\x20\x20\x60\x40\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x40\x60'\ +b'\x20\x30\x10\x18\x07\x00\x00\x40\x40\x20\x20\x30\x10\x18\x18\x18'\ +b'\x18\x18\x18\x18\x10\x30\x20\x60\x40\xc0\x08\x00\x00\x20\x20\xf8'\ +b'\x20\x50\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x18\x00\x18\x00\x18\x00\xff\x00\xff\x00\x18\x00\x18\x00\x18\x00'\ +b'\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xc0\x40\x40\x80\x00'\ +b'\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\xf8\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\xc0\xc0\x00\x00\x00\x00\x06\x00\x00\x04'\ +b'\x0c\x08\x08\x18\x10\x10\x30\x20\x20\x60\x40\x40\xc0\x80\x00\x00'\ +b'\x00\x00\x0b\x00\x00\x00\x00\x00\x3e\x00\x7f\x00\x63\x00\xe3\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xe3\x80\x63\x00'\ +b'\x7f\x00\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x10\x00\x30\x00\xf0\x00\xf0\x00\x30\x00\x30\x00\x30\x00'\ +b'\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x3e\x00\x7f\x00'\ +b'\xe3\x80\xc1\x80\x01\x80\x01\x80\x03\x00\x0e\x00\x1c\x00\x30\x00'\ +b'\x60\x00\xc0\x00\xff\x80\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0b\x00\x00\x00\x00\x00\x3e\x00\x7f\x00\xe3\x80\xc1\x80\x01\x80'\ +b'\x0f\x00\x0f\x00\x03\x80\x01\x80\x01\x80\xc1\x80\xe3\x80\x7f\x00'\ +b'\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00'\ +b'\x06\x00\x06\x00\x0e\x00\x1e\x00\x16\x00\x26\x00\x46\x00\x46\x00'\ +b'\x86\x00\xff\x00\xff\x00\x06\x00\x06\x00\x06\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x7f\x00\x7f\x00\x60\x00'\ +b'\x60\x00\xde\x00\xff\x00\xe3\x80\x01\x80\x01\x80\x01\x80\x01\x80'\ +b'\xc3\x00\x7f\x00\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00'\ +b'\x00\x00\x00\x00\x1e\x00\x3f\x00\x63\x00\x61\x80\xc0\x00\xde\x00'\ +b'\xff\x00\xe3\x80\xc1\x80\xc1\x80\xc1\x80\x63\x80\x7f\x00\x3e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\xff\x80'\ +b'\xff\x80\x01\x00\x03\x00\x02\x00\x06\x00\x04\x00\x0c\x00\x08\x00'\ +b'\x18\x00\x18\x00\x10\x00\x30\x00\x30\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0b\x00\x00\x00\x00\x00\x1c\x00\x3e\x00\x63\x00\x63\x00'\ +b'\x63\x00\x3e\x00\x3e\x00\x63\x00\xc1\x80\xc1\x80\xc1\x80\x63\x00'\ +b'\x7f\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x3e\x00\x7f\x00\xe3\x00\xc1\x80\xc1\x80\xc1\x80\xe3\x80'\ +b'\x7f\x80\x3d\x80\x01\x80\x03\x00\xe3\x00\x7e\x00\x3c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\xc0\xc0\x00'\ +b'\x00\x00\x00\x00\x00\x00\xc0\xc0\x00\x00\x00\x00\x05\x00\x00\x00'\ +b'\x00\x00\x00\x00\xc0\xc0\x00\x00\x00\x00\x00\x00\xc0\xc0\x40\x40'\ +b'\x80\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x40\x01\xc0\x07\x00\x3c\x00\xe0\x00\xe0\x00\x78\x00\x0f\x00'\ +b'\x03\xc0\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc0'\ +b'\xff\xc0\x00\x00\x00\x00\xff\xc0\xff\xc0\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\xe0\x00\x78\x00\x0e\x00\x03\xc0\x01\xc0'\ +b'\x07\x00\x3c\x00\xf0\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0b\x00\x00\x00\x3c\x00\x7e\x00\xc7\x00\xc3\x00\x03\x00\x03\x00'\ +b'\x06\x00\x0c\x00\x08\x00\x18\x00\x18\x00\x00\x00\x00\x00\x18\x00'\ +b'\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x03'\ +b'\xf0\x00\x0f\xfc\x00\x1e\x0f\x00\x38\x03\x80\x71\xe1\x80\x63\xe9'\ +b'\xc0\x67\x18\xc0\xce\x18\xc0\xcc\x18\xc0\xcc\x10\xc0\xcc\x31\x80'\ +b'\xce\x73\x80\x67\xff\x00\x63\x9e\x00\x30\x00\x00\x3c\x00\x00\x0f'\ +b'\xf8\x00\x03\xf0\x00\x00\x00\x00\x0d\x00\x00\x00\x07\x00\x07\x00'\ +b'\x07\x80\x0d\x80\x0d\x80\x08\xc0\x18\xc0\x18\xc0\x10\x60\x3f\xe0'\ +b'\x3f\xe0\x30\x30\x60\x30\x60\x38\xc0\x18\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0d\x00\x00\x00\xff\x00\xff\x80\xc1\xc0\xc0\xc0\xc0\xc0'\ +b'\xc1\xc0\xff\x00\xff\x80\xc0\xc0\xc0\x60\xc0\x60\xc0\x60\xc0\xe0'\ +b'\xff\xc0\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x0f\x80\x3f\xe0\x70\x60\x60\x30\xe0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xe0\x30\x60\x70\x70\x60\x3f\xe0\x0f\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\xff\x00\xff\x80\xc1\xc0'\ +b'\xc0\xc0\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60'\ +b'\xc0\xc0\xc1\xc0\xff\x80\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0d\x00\x00\x00\xff\xc0\xff\xc0\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xff\x80\xff\x80\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xff\xc0'\ +b'\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\xff\x80'\ +b'\xff\x80\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xff\x00\xff\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0f\x00\x00\x00\x0f\xc0\x3f\xf0\x38\x30\x60\x18'\ +b'\x60\x00\xc0\x00\xc0\x00\xc1\xf8\xc1\xf8\xc0\x18\xe0\x18\x60\x38'\ +b'\x78\x78\x3f\xd8\x0f\x88\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xff\xe0'\ +b'\xff\xe0\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x0b\x00'\ +b'\x00\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00'\ +b'\x03\x00\x03\x00\x03\x00\xc3\x00\xc3\x00\xe7\x00\x7e\x00\x3c\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\xc0\x60\xc0\xc0'\ +b'\xc1\x80\xc3\x00\xc6\x00\xcc\x00\xdc\x00\xf6\x00\xe6\x00\xc3\x00'\ +b'\xc1\x80\xc1\x80\xc0\xc0\xc0\x60\xc0\x60\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0b\x00\x00\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xff\x80\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00'\ +b'\x00\xe0\x1c\x00\xe0\x1c\x00\xf0\x3c\x00\xf0\x3c\x00\xd0\x2c\x00'\ +b'\xd8\x6c\x00\xd8\x6c\x00\xc8\x4c\x00\xcc\xcc\x00\xcc\xcc\x00\xc4'\ +b'\x8c\x00\xc6\x8c\x00\xc7\x8c\x00\xc3\x0c\x00\xc3\x0c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\xe0\x60'\ +b'\xe0\x60\xf0\x60\xf0\x60\xd8\x60\xd8\x60\xcc\x60\xc4\x60\xc6\x60'\ +b'\xc2\x60\xc3\x60\xc1\xe0\xc1\xe0\xc0\xe0\xc0\xe0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x10\x00\x00\x00\x0f\xc0\x1f\xe0\x38\x70\x60\x18'\ +b'\x60\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\x60\x1c\x60\x18'\ +b'\x38\x70\x1f\xe0\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00'\ +b'\x00\x00\xff\x00\xff\x80\xc1\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc1\xc0'\ +b'\xff\x80\xff\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x0f\xc0\x1f\xe0'\ +b'\x38\x70\x60\x18\x60\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c'\ +b'\x60\x18\x60\xd8\x38\x70\x1f\xf8\x0f\x98\x00\x08\x00\x00\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\xff\x80\xff\xc0\xc0\xe0\xc0\x60\xc0\x60'\ +b'\xc0\x60\xc0\xc0\xff\x80\xff\xc0\xc0\xe0\xc0\x60\xc0\x60\xc0\x60'\ +b'\xc0\x60\xc0\x70\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00'\ +b'\x1f\x80\x7f\xe0\xe0\x70\xc0\x30\xc0\x00\xe0\x00\x78\x00\x3f\x80'\ +b'\x03\xe0\x00\x70\xc0\x30\xc0\x30\x70\x60\x7f\xe0\x1f\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\xff\xc0\xff\xc0\x0c\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60'\ +b'\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\xc0\x60\x60\xc0\x7f\xc0'\ +b'\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\xc0\x30'\ +b'\x60\x30\x60\x30\x20\x20\x30\x60\x30\x60\x10\x40\x18\xc0\x18\xc0'\ +b'\x08\x80\x0d\x80\x0d\x80\x07\x00\x07\x00\x07\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x13\x00\x00\x00\x00\xc0\xc0\xc0\x60\xe0\xc0\x60'\ +b'\xe0\xc0\x61\xe0\xc0\x61\xb1\x80\x31\xb1\x80\x31\xb1\x80\x33\x11'\ +b'\x80\x33\x19\x00\x13\x1b\x00\x1f\x1b\x00\x1e\x0b\x00\x1e\x0e\x00'\ +b'\x0e\x0e\x00\x0c\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0d\x00\x00\x00\x60\x30\x30\x70\x30\x60\x18\xc0\x0c\xc0'\ +b'\x0d\x80\x07\x00\x07\x00\x07\x00\x0d\x80\x18\xc0\x18\xe0\x30\x60'\ +b'\x70\x30\x60\x38\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00'\ +b'\x60\x18\x70\x38\x30\x30\x18\x60\x18\x60\x0c\xc0\x0f\xc0\x07\x80'\ +b'\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\xff\xe0\xff\xe0\x00\xc0'\ +b'\x01\x80\x03\x80\x03\x00\x06\x00\x0c\x00\x1c\x00\x38\x00\x30\x00'\ +b'\x60\x00\xc0\x00\xff\xe0\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x06\x00\x00\xe0\xe0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\xe0\xe0\x06\x00\x00\x80\xc0\x40\x40\x60\x20\x20'\ +b'\x30\x10\x10\x18\x08\x08\x0c\x04\x00\x00\x00\x00\x06\x00\x00\xe0'\ +b'\xe0\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\xe0\xe0\x09\x00\x00\x00\x00\x00\x18\x00\x38\x00\x28\x00\x2c\x00'\ +b'\x64\x00\x46\x00\xc2\x00\x82\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xf0'\ +b'\x00\x00\x00\x00\x00\x00\x05\x00\x00\xc0\x60\x30\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x3e\x00\xff\x80\xc1\x80\x01\x80'\ +b'\x01\x80\x3f\x80\xf1\x80\xc1\x80\xc3\x80\xff\xc0\x78\xc0\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc0\x00\xdf\x00\xff\x80\xe1\x80\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xe1\x80\xff\x80\xde\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x7f\x00'\ +b'\x61\x80\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xc1\x80\x63\x80\x7f\x00'\ +b'\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x01\x80'\ +b'\x01\x80\x01\x80\x01\x80\x3d\x80\x7f\x80\x63\x80\xc1\x80\xc1\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\x63\x80\x7f\x80\x3d\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3e\x00\x7f\x00\x63\x00\xc1\x80\xff\x80\xff\x80\xc0\x00\xc0\x00'\ +b'\x63\x80\x7f\x00\x3e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00'\ +b'\x00\x30\x70\x60\x60\xf0\xf0\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3d\x80\x7f\x80\x63\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80'\ +b'\x63\x80\x7f\x80\x3d\x80\x01\x80\xc3\x80\x7f\x00\x3e\x00\x0b\x00'\ +b'\x00\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00\xdf\x00\xdf\x80\xe3\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\xc0\xc0\x00\x00\xc0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x05\x00'\ +b'\x00\x30\x30\x00\x00\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\xf0\xe0\x0a\x00\x00\x00\xc0\x00\xc0\x00\xc0\x00\xc0\x00'\ +b'\xc3\x00\xc6\x00\xcc\x00\xd8\x00\xf8\x00\xec\x00\xce\x00\xc6\x00'\ +b'\xc3\x00\xc3\x00\xc1\x80\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00'\ +b'\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xde\x78\xfe\xfc\xe3\x8c\xc3\x0c\xc3\x0c\xc3\x0c\xc3\x0c\xc3\x0c'\ +b'\xc3\x0c\xc3\x0c\xc3\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcf\x00\xdf\x80\xe3\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xc1\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x3e\x00\x7f\x00\x63\x00\xc1\x80\xc1\x80\xc1\x80'\ +b'\xc1\x80\xc1\x80\x63\x00\x7f\x00\x3e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xde\x00'\ +b'\xff\x80\xe1\x80\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xe1\x80'\ +b'\xff\x80\xde\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x3d\x80\x7f\x80\x63\x80\xc1\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\xc1\x80\x63\x80\x7f\x80\x3d\x80\x01\x80'\ +b'\x01\x80\x01\x80\x00\x00\x07\x00\x00\x00\x00\x00\x00\xd8\xf8\xe0'\ +b'\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x0a\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x00\x7f\x00\xc3\x00\xc0\x00'\ +b'\xf0\x00\x7e\x00\x0f\x00\x03\x00\xc3\x00\xfe\x00\x7c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x60\x60\xf0\xf0\x60'\ +b'\x60\x60\x60\x60\x60\x60\x70\x70\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\xc1\x80\xc1\x80\xc1\x80\xc1\x80'\ +b'\xc1\x80\xc1\x80\xc1\x80\xc1\x80\xe3\x80\xfd\x80\x79\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xc0\xc0\x61\x80\x61\x80\x61\x00\x23\x00\x33\x00\x32\x00'\ +b'\x16\x00\x1e\x00\x1c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x0c\xc3\x8c'\ +b'\x63\x8c\x67\x88\x66\x98\x24\xd8\x34\xd0\x3c\xd0\x3c\x70\x18\x70'\ +b'\x18\x60\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x61\x80\x63\x00\x33\x00\x1e\x00\x1c\x00'\ +b'\x0c\x00\x1c\x00\x16\x00\x33\x00\x63\x00\x41\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xc0\x80\x41\x80\x61\x80\x61\x00\x23\x00\x33\x00\x32\x00\x16\x00'\ +b'\x1c\x00\x1c\x00\x0c\x00\x08\x00\x18\x00\x78\x00\x70\x00\x0a\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\x00\x06\x00'\ +b'\x06\x00\x0c\x00\x18\x00\x30\x00\x60\x00\xc0\x00\xff\x00\xff\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x18\x38\x30\x30\x30'\ +b'\x30\x30\x30\x70\xc0\x70\x30\x30\x30\x30\x30\x30\x38\x18\x05\x00'\ +b'\x00\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0\xc0'\ +b'\xc0\xc0\xc0\xc0\x07\x00\x00\xc0\xe0\x60\x60\x60\x60\x60\x60\x70'\ +b'\x18\x70\x60\x60\x60\x60\x60\x60\xe0\xc0\x0a\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x00\xf1\x00\x9f\x00'\ +b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\x2a\x00\x2a\x00\x40\x00\x40\x00\x56\x00\x56\x00\x6c\x00'\ +b'\x6c\x00\x96\x00\x96\x00\xc0\x00\xc0\x00\xfe\x00\xfe\x00\x28\x01'\ +b'\x28\x01\x3e\x01\x3e\x01\x54\x01\x54\x01\x6a\x01\x6a\x01\x80\x01'\ +b'\x80\x01\xaa\x01\xaa\x01\xc0\x01\xc0\x01\xd6\x01\xd6\x01\xec\x01'\ +b'\xec\x01\x02\x02\x02\x02\x2c\x02\x2c\x02\x56\x02\x56\x02\x80\x02'\ +b'\x80\x02\xaa\x02\xaa\x02\xd4\x02\xd4\x02\xfe\x02\xfe\x02\x28\x03'\ +b'\x28\x03\x52\x03\x52\x03\x7c\x03\x7c\x03\xa6\x03\xa6\x03\xbc\x03'\ +b'\xbc\x03\xd2\x03\xd2\x03\xfc\x03\xfc\x03\x26\x04\x26\x04\x50\x04'\ +b'\x50\x04\x7a\x04\x7a\x04\xb8\x04\xb8\x04\xe2\x04\xe2\x04\x0c\x05'\ +b'\x0c\x05\x36\x05\x36\x05\x60\x05\x60\x05\x8a\x05\x8a\x05\xb4\x05'\ +b'\xb4\x05\xde\x05\xde\x05\x08\x06\x08\x06\x1e\x06\x1e\x06\x48\x06'\ +b'\x48\x06\x72\x06\x72\x06\x9c\x06\x9c\x06\xda\x06\xda\x06\x04\x07'\ +b'\x04\x07\x2e\x07\x2e\x07\x58\x07\x58\x07\x82\x07\x82\x07\xac\x07'\ +b'\xac\x07\xd6\x07\xd6\x07\x00\x08\x00\x08\x2a\x08\x2a\x08\x54\x08'\ +b'\x54\x08\x92\x08\x92\x08\xbc\x08\xbc\x08\xe6\x08\xe6\x08\x10\x09'\ +b'\x10\x09\x26\x09\x26\x09\x3c\x09\x3c\x09\x52\x09\x52\x09\x7c\x09'\ +b'\x7c\x09\xa6\x09\xa6\x09\xbc\x09\xbc\x09\xe6\x09\xe6\x09\x10\x0a'\ +b'\x10\x0a\x3a\x0a\x3a\x0a\x64\x0a\x64\x0a\x8e\x0a\x8e\x0a\xa4\x0a'\ +b'\xa4\x0a\xce\x0a\xce\x0a\xf8\x0a\xf8\x0a\x0e\x0b\x0e\x0b\x24\x0b'\ +b'\x24\x0b\x4e\x0b\x4e\x0b\x64\x0b\x64\x0b\x8e\x0b\x8e\x0b\xb8\x0b'\ +b'\xb8\x0b\xe2\x0b\xe2\x0b\x0c\x0c\x0c\x0c\x36\x0c\x36\x0c\x4c\x0c'\ +b'\x4c\x0c\x76\x0c\x76\x0c\x8c\x0c\x8c\x0c\xb6\x0c\xb6\x0c\xe0\x0c'\ +b'\xe0\x0c\x0a\x0d\x0a\x0d\x34\x0d\x34\x0d\x5e\x0d\x5e\x0d\x88\x0d'\ +b'\x88\x0d\x9e\x0d\x9e\x0d\xb4\x0d\xb4\x0d\xca\x0d\xca\x0d\xf4\x0d'\ + +_mvfont = memoryview(_font) + +def get_ch(ch): + ordch = ord(ch) + ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32 + idx_offs = 4 * (ordch - 32) + offset = int.from_bytes(_index[idx_offs : idx_offs + 2], 'little') + next_offs = int.from_bytes(_index[idx_offs + 2 : idx_offs + 4], 'little') + width = int.from_bytes(_font[offset:offset + 2], 'little') + return _mvfont[offset + 2:next_offs], 20, width + diff --git a/images/IMG_2885.png b/images/IMG_2885.png new file mode 100644 index 0000000000000000000000000000000000000000..86f3e0a5fbccd27a67a5a014e68732fd35a6f22e GIT binary patch literal 51652 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 T8}BWo@bP2LAN|?LX^;FLkwTIN literal 0 HcmV?d00001 diff --git a/images/IMG_2887.png b/images/IMG_2887.png new file mode 100644 index 0000000000000000000000000000000000000000..f108bdc9ec881894dce9762e64bd987b4102a55e GIT binary patch literal 45273 zcmd6Q2Y^=9mH+$tbY_?th9U?EN>NY{6hjnLP!u(aQHecb+?be6;+k$ubhFXK#5HBJ z8 zedPSJsw+lUSe8}2QshH;b*O^`T{@DzXgE7F^i2Y z^|3k(O~{i!hnH(Cy0yu#6C^OG*tltpAOl22>ZZbs4I)(xGS%G;#xmsG@^9Serk>A4 zkqyoww+zD*K(KW;-DsuiheG4y$AXJm3y<~^CbvTuPQ`FIw_QF|qjuh^2nrw{VY6?nh2f^Lv)I}27T$DReXIcYf0CkqA zm|lY;5NW!%#At}cF~dvfg`1YCc(J`L7#XlFn}Mj?QuE>rsl_&u>kKDTSNwId10i7B zHV9-(!ig}i_&E`w;%|%kOhqt3RlOR|Pp<`NCb}(qP346aX^HYhIu8%Hu$C^~Kqy!a zO6XAePy^ABRS||a13?NF{R?u8K!MX|$|ZB*$|$jYxLge>I)J`NqcglQJZm%OVBP zEU&KW@8~YCt?Aw07Op4wtLEOE-(=~Noy1PC^Kj6s>R z)9Gv|6v(EsIUb|Yx=Je)4q}kmb~=%!XeHAaVA(Wrz-WY_>3F>Foqa@|NZP?*rfUF$ z%uZ%7No0GIkYi`SH*4o|kZ5NhH<1s^(1dF!KyVX*u*KY%$yph4nH3F0rq>0cVT7}0 z_Sh*)PpCU`WFnQWD2pXCnTl8$Mr<~lN@p@bL>8mavin-v-dw#qcc9-22eRD*J@x--=iB;ReDAP|H+I zSBZ*326u!|HJ~pkMqK;5QN9M|b_kP$-FkRip1=X5(HgRF6; z;&?_$jfoD9AVM=-Ia~nHo_g3P9C$)wAc1g%43y6tUOuI9>Xq~A$B*vo=-t14-`>Yw z0q0a_Ps{(V&u#BjT4LN@-k1CEl>HCC)AyIH%Ey+~{mAA6XKt_}(Of(^e9naVH=bKw zSJSbxdBe0f6I=JUKeSZ@;-fWpT$@-l^r5zUT(gl?#)ffBT$)9ccE1|s9DDvoWMa`BvDjdi;b zxwl_Ub@v~5a6@`aC&xU1>C_<(1~Q3kZ%1b?i8+^;(iRfhQ{BJcEcO1UcDAkDfKXM> z9e>(q&hBgLe0%2W_CUsN@7e#W4Wu2c=F}1V5?d2R9GER+sBZpGtow77;dYs0~T1iL>S ze||4cwhp}7^5>T}51V_`S)V_zX~YqoU9C@6Jh0~nYl28Sux0C^7Fl?(p@mZ#oVDWI zhjYer0%%k;6=vzR*BJi3D`pgm01k#uWYNH_2U*F9r_~{QIT(b628@DXDP$^%m9h)@U##Fewgj+#7ie0BAPH#Ri?`IUWtT%X$58Ng_k^9zXKqZHGS<0K!z zBiEMfeh^croXwWfFDa6v|3j>6csE-|l!J5V`Qq6+Q99%zruy2rWN;9U~hj zt_TciENY>nc)^iau6IZcR>k0wNG`G?*Jwz|Sc+V~P$oCL3sn50kWjQj!py~zs;;K6 zQ&`xETj2uuirp>DG=kbIdsIX*WCK?0xZ3E1;YVLEy}7OHh)}S*xqbKDFIWi)H?b0A zk*A~zH|VM=iY;dej|tO31}P*CPar(Hy85_=O`F!O{Mdih%ow}imb2F%*^p`J*m=*Z zbQHj4hE5Ql$Hkk%4d*%-yL6KQ{0X1-qML8)g?d{a)WThbp)NuRp#e}LFikPSk~twq zs4x!ka$aZHN`-0JI}{SQhbPC%CF73!^upSvhV5%MZ~yf(`_7s&ZSl;W3#NBGuz{3e z@0%F-I2LSrkv$5d3+=MmV^2J9{{83Q9opR2w`JpV-HG}4UXdH<3szUW_l@UpcZMz* z6>GicrfGmlHkqM2J#x~jQ^c*0N3gr&aMw);MItH5DSzm4ke8T5aUskG8>_*WrV|*; zLxK3CUUlAtX;+`z+nH>B^|gKXzm?w9(;kY3m&^*6#ehqgE(ThsV$NZ7lSui51LV*Ffh{g z5ko-okqanJxb9NS5H}9$2SGC-6f(lj*T%)tQA`-a)ZlpXWLfWH-iMIuK|}CJ*d=;ilg${>jGzp~$8$KSlRW zGq_Eo_)v76%wS+HS00Kf$=;nHYnV~qRCJHWC2k$5L(J61GPi6l z9EgZ0r6#2s6+zQ(G|HQbU=4X&jNXJ$IP&D+2_k7i10t_5M+c4wq5+#jbW@)$6N%9H zfaWOn2$FQ=}SSH5CIOB09Klf;-2E20a*{!eH8KUsyF4Pd@RJXZCc*-~QU4 zv+s5h6&FS(-e`G6sFvagjN+wWwi$Fc2P1;Cv|5DQJqWg2u=(orhS_JpJSpLU$k8V~#<7WYyOAMmYg+69W*LNs&}o z1T|S|+ruxPHv5yOx9;nD=c|uq)**g;!b6OOo1)^K;!h}pF1~CdzQ*ZZCdHWMaAMZn z`?rgJS~X(S?E5aE*#(ciP}~GiEuF)~k&HqwW3-g!OyeeVcjXZsDgeqE6Ga%BKB>ic z1I@uCt?@q)2uH(sPD45Abh00Vm_Mh?{^Y`r&i-|`KAPkEFW`Uxv6o%gN}z(cvs-#f z4>g~%XVO`Jy$R1-prI7S4XR}_l&&Z=i};v%+9sZi zgf1_@M>_N-l;CmaP!7pt7a1ak$bU7ai)-Ie-URiZu_14Um9;|PWvq6W} zsX1ymutC=jhkFzc48B}@-yHL+j@Si4p-^S?xDQQV^TJzguWy-q$@~+~n)`77FS`GLhpsfw7MZaf8HTq55=GtZ zqIFTjC#4`uQpX(zESKup)782C!1_BLvl4Wf`>laT9uM71*5Myr=-9N5CK|N*SqhVj zMQ#DgmwpZ1D5lQ5UvwxBhIfNsS+%<{6tPc43T$B*c{ z6VG4rGccudF|5|H?8hH@?1GgyKRoX)tfQj`qaiDvN?H+^%u^0?6Aq`IIn>Y{NoiTJ(qq$2JznuKuUr zoN)As#G9_2=qMP&BCU+c?|e3d%i4-$2RgaQUkX5q3U<-vlaT^6Hw>l}q{jIS zF7wF{6x_10Qknm``zKetbbGb}jEOTYIxrS-Aq>|QVnH|zFVtBeSxHcS$tU(fWE6_6 z>|Mnp-*}{(8|bcshOQ%^Fd~Sx?wJfu*R1H6>a%XXaLqGs?tgeK%{?yF%24dXT`6f~Ns3h#U# za|=YjGRsi9yLG)}r$&-dNZK-2C~OXFetOqv1GUew=mdn;++tO3I-X)~!Rp%D?sxaw z9VuU$YGPm+DWZbbOYJqH4?L*e!ZanIq)owrD_ZatD^$(YnH)%3IO;1MWdNv#rcg~0 zYD{%|&x7Gtu5hP|4-p`QRA>gI(-xW{B>VX2;zL$VJsi=LKjuI;eZdbmF+wz23MZH2q8vGeu@$;(HFP-_V~GDLPRp9()k%gDk| zkg2W$Pj`N8thTmBS1!5bl9zt*Z1sFxYF4vt2aVhMjVy!MArOc*m!SmY9EdgDM`cW@p)xo!e? z-HoO#V^}dHH6k?~IXr)2@ewm)zzmoxv;i@l+R$qi;XrwHbz<#4ERV<89rba*RsL#u zX75qfK)K?JC}2K_)#}Opj-;4F}Sh{=F+^r0WOyTtG#bHn8+E3Pgb#J-q0+DczEDSeQ1dV zs7O#Q@ty(P4W{7f$;OydNNUN>LIiGD3F7<^Ld~K2lVnL6;!aI07PDi4v6mgcuVr7N zvsd&emZXOwi8&rJq3CmHTS~-a_K)}e^3n&tVnr=0nG2TFT77^erAlg5vh!dHl^{k4 zO9Z$8;}Y2Df-zXK7aCr1!9RcW$$KB|ePIV~919mVvIMnX*1^VE38Vb14qF?CLo2ZB zfi>et9cAIwN8GMR(+o?7yYk+fuR}2T(DXzIJ-&cIuLgd$9&B$X9Y%3ygI^?zmRH2L zby_r|a-mEGX)$VJr>Ow77%~_-Xo~+(cA8l42SE#;kOfp|o9vHc-y8487|c&K#x%^8 z6{gKh4(BFsBBhf^5pYnUK&1j3BenTlPw%j)Lxs?*KxoE4_)Lp27-1Q%!Se( z5Bd0Yep*V1+tWo$Va`R-RfkZ~DLNj-ATY2+4(%b_#fU`8lDMgX??fo(#UghwDF^Ro z=N3gXS2kv@mkR*sr3M+35|cbeE(CQ%fMqnX;B}K=+B)_#r=^y4B8Hp3{hZwvCq51v zn2m;yB{ii(wnX9}DHwKy#XBs4u!FhlCJwK?=aTO&{ZEFvv?5F?K0z9MkiAgxw;r;& zGrzy$h?(Peyt1kBn9-f>?a(2az)M-I$3gh0Ze@VBu{e=I^rIB)Fd@|LQywVj2yg^i zK|Ugk)5e$R)lQ-2F;~o|a?j5nTjowTa*^o}j}OZ5 zT@m}E6NfLo`;t>K_TS3s+mU#Fzz>2P<|T9$jR&=5XKZ@(mA!9m?%LkE@o(8B%P+&R z5F-+cOp=oAkOak?IoHEj)G%t9iU-MQ7%w>>sTCwNAp{P)4@#iwieR1>3?HWlQIT;$ z0kr7T7k140)s^21yw!@^H1q8k-!NpLATl8&7D2jSYK83Wy`eT- zRe&GK5aw*7u@gj6H%78HjE@c>VcrN3Mi{y1h|H;+kdIwr_(cc|t|L!Gs-*)r} zIy0mTP?7_lz8RH33>Q|i&4-v?i2+j?7=;IS3hA;5+!C1(zHF4iS0Zo;#Wp#JkflTB z0_Mln)#THE_!j z5mOm zT})ZPUjlb9X-WVLbO>Q0g_F@NK9xitb)y0kfWSv;V_!b!e27pFervQ660*XCkQ!P; zH%E7l5xt8C4HIdK0cg;&6CA0McmpV4CDOQR;`?iiMc(CM=w`3$;(GQ1`2t}1kF)t@ zM>k&gwX+*W*FDgcxcBt$Sy6oWi6&=!9}hT$mdg!Dx{w@krP52Y&)*9|K0F`73=Np% z;j3XOToK|+MWHer4B2n2v-XF=_U4meQK|y=gV#)OO$d$6ZaZhNHcgIudbTA`U5R#XWTBOVn%a959!(I>~ZdB>7g^H=trD1w6k_1D;@QF1X z-0ABhxpgfM-*DfpE5Ez;?q{t5oZ6U?fdR%TqHjZohR#^K;f%v#Y zYn~FhV3NP-#b2Nk7Rm)Qaw&>vveiOLO3>i4tt%$g&AWB!jA*EPSk0eywSVh|pVHmP zP>>RV1q&bAf)zlZln<9GPORJ59cK=+EN3l%kP@;aFgsLd9Y>1`5#)h{vC-zCd0=IBN?dL zgDO!y(GtZp;~{J0BXG==QycJg>8g>9H0t9&LDT*IJ#3`w0XdV|()p)r|7-b^xBvK- z$LK4(PO1_W$>-qV4CdS;Yd`swv!Y`gA8hUT!RdDb5qGfo%dYCVBiuqGPz8x}VNbDA z97CL5;1L(&@KB|>*66O36cbdV79wEpZUlT|laO?W87vWG-%)U|lz3gz%OLK0`nqeqG>V zV4gdn{?Z%g*H3I3X$Kz($G&s*@51!iXM~8KSaHzc4B^0e?dZ)3K#b>z;VmFSCy@hxj+ht;JR{=!+%IJZ0k+;Vl@EzpCU z-_RuaJr0CIs9DOiNJi^GDxRdVbTc|Naqft1D3qZuFw;m$wD)d)Z}Yq#U%L8J zzo)A@u%Pc`5WkNc-Ei?ur;QlfSRc$i9*ur|$&W%&Jg~xN(TTq}g7_1z>GRBPt=#w}>cUqDm1sVs;h;cvadv_nH@KI5_Npfydpy*KiH3>N zIeZsaj(!GBJL;eUOCSubi$du526hxG1=Vt>gaqyc9@esP2(j(m+_vUl{_xq~e+wa` zivwI5Sk~poHqHC^$G|cv^bGi!gmDm$x$)ld>|}Z zBbXQGAq7&bbBm$K;XWub1d)+wF~_D+qxyHYhjRGDJIxb`-Z&mAii*@P`ui0oB|p$& zCel(;XfWtf(mQw>OE;o`3v+;(>58x4zj@LvXKelMGi6goeEx=07muo{8aeFYeZ6-q z{{ij_gmH6@ZdMb2CwMH&*VOVIO)%pU6#~s}hBzrDHPlG1z1up)0i!TyU-7jYUR(Xj z=2dI4Y7ID(KYZNA4QuIM6tx&JVBur_D%U7}LzV1O;(b(22z}yxO-F*`LI>v5@YG2r zQur1lFY$Kk6CeA@wJ&_{xyki*{jfQs>z~?o;M+@n6wKJ6EX{j7GX24G*cGp_!|0m6 z4_t7?!I+|SX$|vP7Bh9*-kp1z_gwzP6)!#eeCL*y#&JzsURf8;g|W<8f|ZOXDsjJ5 z&RvMhP$lQ)XH>MoCY9i%Gfz8ipAYR>SDn91HlDyF@HZ*pXR~}AtXsg znyv_PK@vgtbipiyfe-zmOdPkNbip|(?wz)8uIUJ^Uh!M}as}QB!gnlV;*cH2v;*yF>?;ysp#|%` zV(i-~pZlPtX?)0NVonnh9zyy3g&~TBJGDe&L??2|1h0BRA9^^KdtTZ7*BgI!;#Za) zb<64W_KYUuA*JyMs9BkWVocx0WE>}-9j0G_q47xn4^x3+R6@9R&s}`;wLOWR=l=W@ zj)EQy@l4Im;!22=b`&3y28T%Ili`d)$vfHlkhodYLwX#;68cdleL1i7!$ZDw7QbQe z=9eFyfBq7%LYOJEDs2n1)x-r9p{!u1NzUN(H;S8jfRvyqz@`*PIf3WQsS_u+uW#w! z*Nx>fEJcgKRZ>?ngpUWiisK3w@Pn?>g9e@ST;qf;Bvq@P*GGeohX6u@v(QwWFDl$< za7oTiB?h*(Y@M`XKE1bw>j_SDOjHI@m>4gRHie5KAxwI_Y$xRMg3N#(e!6^!<|>$e zeC9$#$d$?SroFsj4Si=_V~LXBfC_%I3LpDc+}Hz@RsZlp3wu~S$R_cefJ4?p(iUWR z^_SN{IW{Yc0)bsC*RTHK{ij}c?#T0JAfj*q8?1bToo1q>YbCVvT&~ewtS`Qjri=iX z4y-Ojs^&v9f?yha6@H2y;+L}*Ty|D47JU9!D{)6ub9MNWFnnzm*M%IsL!AT)Sf|x1 zy>|%7dOCmNw1N}iS*No!6Wt4ZAtFLvnn2u)XUMNQSiAe4z3UI7mK--_#c4Rq$oL?M zJQ%ShLid*3<6^`Tje5cqvt62Q4bc4RM`irMY*;91Dbk_vlvAdc53ktt;ySE4#DM@1 z6liDEf#dKg%rJh?(SQac$&aH{Hb(jTxm~E&v6T~;m4k{fK)hX$Q86Hx8SChw1mD^g z32e4Qht%Hr@SAJz`OExkmeehrM7=s;STqp{i6$h*rHXC0WiuUdK2v1WLe_d1NZxor zS||Y#BilaZqJ_AH^u*5}#tA0{NHO)EFvC|_{A?eVnI2T8Pg;RyX){arQV7|P2F+Cb zl8R}{a2^ui6b$0~5H3tIn}!m{0wF{LUvd5Go%fHuVAiy&KP(@75s`=@6^W{1>em5n zpMG!0Q;+TJVmS5qdh_YcQ1f}7MB`q3&YpJKjGCspHIJ>r4;*6rEqddPdo4@-MgYuE zNu}hdCS4<1^qZWJPX@Rgkgvf92?^Xg^D}Sp%u0Ha8mwVaeTuou4X5W9eBYb6k@BM<8klU9;{};w zv1KOkSUT7I*LU|m{?5z`<_|x8@|d%ZLm)$SWwo=9Y;WD~mQ+wDj3rW+@iJ>2}KuM%dRR{qWIHBO*e*XBVhl`Mc z!B-@RR$4n}j~X*1oyplsB{G1Slg=cZw)$9WYK*e8yJh(FQ6ok*PB>=LQ@?yPi63^x zWdpO)>XTg{5^af>)I^B+c|D}7x{HWz*2h6q1YYlHE{C{xD?%*Pa6yf1H9C5@6Vw$y zvVcw+Cb0*P3 zH{gp;v_9?@gwV~qNMORrQ_0zAvRvMZViY`XvQz2gg@j_po-p^ARgXP4{iGQSE;wW5 zkN=qNO_7>RL{BA!S`^lzrBhlY2ctPkU)CiuHk`XO7adH&_j=D~lNK=8y;e@+d+zx` z8*;SnbA`%)q~C?YJ@5@cFf14grTg)6GnQRY2f@i7Jk*@Dz6o2g6GDx(vsU6xT$vS*)ic z8-+@v(Ow+jai{KfanT^xo?Xlv20Aqtj=PEwl5wno(9$R=PKz$!n=~avBIC)5UW$)} zL)m1YGHmtm&yo-}`UQ=06-6Mb!CkZgW85~k0~?-v&3eM_TX6glvyPs7;@pSYem!Z% z)a|dX?`!VB8HD<_G+1y>P~=0wNOhFolqWSzh7$tr;;>C%K6CM$=H0uG75$e=DSWW{IDRdMxY?U6|UyRmo6OoD8ScRR);iAG$(nSKE-q1wH(QyQpsNv9v zH<>UE+rh0btlj#@SKfZ&#fIa@U--{g)g3qH=p}O^BdR7Xo>eiX7OtlsUBImp46m|q zb|6Kg45x@;JUT%KzFFnYNaFb&d7$fq_VM8Gc%Es_BqK&s7SViH7CXnt=Mbbxd5j#^iu>^(E zjTNU2%&NlgYf1-qnyy2y44Dr?h>Rn1SnI-uv9_X=HiYyi<+(5r^mH%+D@>umV3@`M zeP++jg|OlmM~*O765)0RJ^^9{67*>h@)uT!@v|s|7Xt~d<+m!JO%Tt|@zXna0|#fh z*-Yln>%WRu{_a_IdnS;*`TMs&`}pH0pZ4L6>(-5(Ft)3;bN=~fltrSQecj{6jM?<= z=Gh;bweHQe-}&4fpV$YXc;kn)PbFQ8^?_5Xltt*-7paEE3d2W z-`g1sV?}-_J&=k8BLn^YH4U|$`&#QqG`6-JXlfeSvUl(3(WCe5ZXPq~$d+AuMvNTQ z+OqH0cm24xyL-pFZOMW7TeTJ0Ui#oP48WS*NGP02B-g*NroVq+;@l~%JNDPt5ASGg zt81w5*xyz^y0N3BZTRShj=k-5qwBj`J8B#1x?4MIM%4DU_Eb04^mX=E)>Xv2`par6 zlgU)1JYrQvvORG-iKSjhL?jxb9?s=5(ekqBurhk~L{X-os!>WlDN%f0^)*Wn7VW~} z1e3mWs{=q$+>#yeAr3HAlSZ6&?KznvYtFxZS!a!X)fcYck#1jc>rLDGTCTh8rd_?Q zAN$hvZONXc*I$|mTeHtTwf?A4<4>A8b>7U%$&IHjUp$abUvTxsdph=C^|^oAxNFNN zZ~NSv8{Yoxzuxl3TW{QS#~0q+zJA3m|J>ZVbNP)QPiFegyy%>oQNxa(Gi%(G@#CjW zo_Wgg^<$e(J^Qp+UB!9VElcH6=YM=ze|F%~o38BW?Y#2KE84r;uDtCN9o-$5fA#wA zzOKu@at(ed<>D`0fp2u3_xVdhvC!gAf25+eYT+l&88)hZ-jxfRCLS?s*}Nl6j0^6A^SmNYW^&R7eLQ z+?7PcDPZF=sT@Dkk*S>0)Zg4uF=qI{?hbrhfnM*z8zp$n69^eA+EkqxNYK+_$db>u zLS|Li!ZJ}TtH6N}2TOXuNb@K@tU+6>S_y~h#y0hLc9)fx_3Uc5%7XZ{L8UMh5h^H+ znOY`4Z1I~RgriLnea#lfX@1{achhW6D!~b$6*M_)3kN!`(}HH&f-_L?*_{L(S!EvL zlNw=c zq6IAM!_Al3=boBMXHxt70x29VaZ?V#4}|c20{n_Zj=ljN#BU4$4(HQ+8(G7@mx*v7e(0m$JIDN|w&;~QdhkM{ zffFB!P%z8fNG=or1grG8jwU8-yoro&ec-s6!kdSb0j{tevh#-MYcD#Eg{tVRlZdLg z>Q1xIhbxGrA>jc~7A&tQ!@1()KmS8|cMpyg;mWd#hT4IyUfeoM_oV{(j0GJ+>~LiS ztB2EF1A+2L#qgR$f1D0!-SKEeERE9-UYCRMh$GyQM^7wks=WQm&%)7r*X=&zqO+g* z?MnK=b_`XFU~!~A#ge^(DIe3+f^#b#+5oAUh9JlvI*ils<1KkYx_Lk$0wdUd;FYfqh?RK z_5L3OMwC|{Q9t2~88`m&_NmKGo^!?GKn2bVvRHQ$tcaYv?2My6GOJ>ERpYcVpZ$+p zCoi6H#?|KstHLKQIkj%W2%I8{FX5tWE}KfHqILvV1sJ304840#h0ZZp2cFFYCR+w@ zv}i*G@M0BoN|)KY&=su`EZN2{d09N5%)Ergpedi!Fo<=@4N;=dOD= zIk6>Tg@&tz?T@^+Yt5!DkG{D1#W(Hvz_L$Wx#g9$-@Ekl>+XL(lCmosheblsSVh^i z1t;#>y#3wZtvYGZhsrA}zH`%U8=hE`*xyrss~O>8(NJ|X zS`~{#aZuHLi~tp6K#;$=XpOY3Oi+BI^mifTLtLGns^bX{SU2{6dFcYns-3^&%!LtIOujIq8|*D{&XxBN6uwQrVt8 zJ-FD%Z_{pnW9y9BGa()l-5P3rysCK^9kii&c>R!1=HT%p#%dV3?#_JD!wiv8hyW<5 zrczLjJG=7Lq@e&PBgJDG1TKS>-&vgBWw4$$HnJu>to)2me59hOCK`*bdhW$-Yd6HI z%J3%iIaeULXv#hcL$K7Bof^iL5 zN?TH;v}y&d@c^Ct^MD7{&_?IGWKiVq6G<*G+)UiQaKi^xr&Am*v**nXoHPR0#LqtT z*v8fG%vo^CnU^lD8Bw=q^NxM%cZ``d?!b<{@4dG1L#Hq3>}lKh^4eHc>A`>hEzb4nM%e#n<$byD zkZds$$@%Jta!8EKI1oOyQ=8(MMAfkBjyLy0MzpaqS`)*4%S=}SUxqGgtVs2za`8;% z=wbZ_dguc}vEcB@Bir8FS6NxnzrUw^L`}S-mp*PI2RU#@i3Vz?jT+e76&+U5v#FJC z1#konPC>H%colZ_6t21r^chu{z{Q_vSvSKZpMyYX z%Qb^wDkMb=^vd%f7@=Y~`GynSGb4e*(^mY1k<-Jprs*LP?p{5H(2{{A{9%$D^Mt1n zQ)uv+PM@rNcs|5Of@W!)(!KfI4jFPVL2h3I7e%(Z>S{5_wefes*CvC&*2S4HO&4*{ zeFS7SjP54k4lTi!UyGn^-Uhe?bM7O`{Jg}oCS wpEx~9`oJH2GvCYkkkVTC@#%ik2U)sS>^foW*T21)WGq>9{+SP*e$DOw4+z)|NdN!< literal 0 HcmV?d00001 diff --git a/mono_test.py b/mono_test.py new file mode 100644 index 0000000..6f89578 --- /dev/null +++ b/mono_test.py @@ -0,0 +1,116 @@ +# mono_test.py Demo program for nano_gui on an SSD1306 OLED display. + +# The MIT License (MIT) +# +# Copyright (c) 2018 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +# https://learn.adafruit.com/monochrome-oled-breakouts/wiring-128x32-spi-oled-display +# https://www.proto-pic.co.uk/monochrome-128x32-oled-graphic-display.html + +# V0.31 9th Sep 2018 + +import utime +import uos +from ssd1306_setup import WIDTH, HEIGHT, setup +from writer import Writer, CWriter +from nanogui import Label, Meter, refresh + +# Fonts +import courier20 as fixed +import font6 as small +import arial10 + + +def fields(use_spi=False, soft=True): + ssd = setup(use_spi, soft) # Create a display instance + Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = Writer(ssd, fixed, verbose=False) + wri.set_clip(False, False, False) + textfield = Label(wri, 0, 2, wri.stringlen('longer')) + numfield = Label(wri, 25, 2, wri.stringlen('99.99'), bdcolor=None) + countfield = Label(wri, 0, 90, wri.stringlen('1')) + n = 1 + for s in ('short', 'longer', '1', ''): + textfield.value(s) + numfield.value('{:5.2f}'.format(int.from_bytes(uos.urandom(2),'little')/1000)) + countfield.value('{:1d}'.format(n)) + n += 1 + refresh(ssd) + utime.sleep(2) + textfield.value('Done', True) + refresh(ssd) + +def multi_fields(use_spi=False, soft=True): + ssd = setup(use_spi, soft) # Create a display instance + Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it + wri = Writer(ssd, small, verbose=False) + wri.set_clip(False, False, False) + + nfields = [] + dy = small.height() + 6 + y = 2 + col = 15 + width = wri.stringlen('99.99') + for txt in ('X:', 'Y:', 'Z:'): + Label(wri, y, 0, txt) + nfields.append(Label(wri, y, col, width, bdcolor=None)) # Draw border + y += dy + + for _ in range(10): + for field in nfields: + value = int.from_bytes(uos.urandom(3),'little')/167772 + field.value('{:5.2f}'.format(value)) + refresh(ssd) + utime.sleep(1) + Label(wri, 0, 64, ' DONE ', True) + refresh(ssd) + +def meter(use_spi=False, soft=True): + ssd = setup(use_spi, soft) + wri = Writer(ssd, arial10, verbose=False) + ssd.fill(0) + refresh(ssd) + m0 = Meter(wri, 5, 2, height = 50, divisions = 4, legends=('0.0', '0.5', '1.0')) + m1 = Meter(wri, 5, 44, height = 50, divisions = 4, legends=('-1', '0', '+1')) + m2 = Meter(wri, 5, 86, height = 50, divisions = 4, legends=('-1', '0', '+1')) + steps = 10 + for n in range(steps + 1): + m0.value(int.from_bytes(uos.urandom(3),'little')/16777216) + m1.value(n/steps) + m2.value(1 - n/steps) + refresh(ssd) + utime.sleep(1) + + +tstr = '''Test assumes a 128*64 (w*h) display. Edit WIDTH and HEIGHT in ssd1306_setup.py for others. +Device pinouts are comments in ssd1306_setup.py. +All tests take two boolean args: +use_spi = False. Set True for SPI connected device +soft=True set False to use hardware I2C/SPI. Hardware I2C option currently fails with official SSD1306 driver. + +Available tests: +fields() Label test with dynamic data. +multi_fields() More Labels. +meter() Demo of Meter object. +''' + +print(tstr) diff --git a/nanogui.py b/nanogui.py new file mode 100644 index 0000000..0ec58e6 --- /dev/null +++ b/nanogui.py @@ -0,0 +1,385 @@ +# nanogui.py Displayable objects based on the Writer and CWriter classes +# V0.3 Peter Hinch 26th Aug 2018 + +# The MIT License (MIT) +# +# Copyright (c) 2018 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Base class for a displayable object. Subclasses must implement .show() and .value() +# Has position, colors and border definition. +# border: False no border None use bgcolor, int: treat as color + +import cmath +from writer import Writer +import framebuf +import gc + +def _circle(dev, x0, y0, r, color): # Single pixel circle + x = -r + y = 0 + err = 2 -2*r + while x <= 0: + dev.pixel(x0 -x, y0 +y, color) + dev.pixel(x0 +x, y0 +y, color) + dev.pixel(x0 +x, y0 -y, color) + dev.pixel(x0 -x, y0 -y, color) + e2 = err + if (e2 <= y): + y += 1 + err += y*2 +1 + if (-x == y and e2 <= x): + e2 = 0 + if (e2 > x): + x += 1 + err += x*2 +1 + +def circle(dev, x0, y0, r, color, width =1): # Draw circle + x0, y0, r = int(x0), int(y0), int(r) + for r in range(r, r -width, -1): + _circle(dev, x0, y0, r, color) + +def fillcircle(dev, x0, y0, r, color): # Draw filled circle + x0, y0, r = int(x0), int(y0), int(r) + x = -r + y = 0 + err = 2 -2*r + while x <= 0: + dev.line(x0 -x, y0 -y, x0 -x, y0 +y, color) + dev.line(x0 +x, y0 -y, x0 +x, y0 +y, color) + e2 = err + if (e2 <= y): + y +=1 + err += y*2 +1 + if (-x == y and e2 <= x): + e2 = 0 + if (e2 > x): + x += 1 + err += x*2 +1 + +# Line defined by polar coords; origin and line are complex +def polar(dev, origin, line, color): + xs, ys = origin.real, origin.imag + theta = cmath.polar(line)[1] + dev.line(round(xs), round(ys), round(xs + line.real), round(ys - line.imag), color) + +def conj(v): # complex conjugate + return v.real - v.imag * 1j + +# Draw an arrow; origin and vec are complex, scalar lc defines length of chevron. +# cw and ccw are unit vectors of +-3pi/4 radians for chevrons (precompiled) +def arrow(dev, origin, vec, lc, color, ccw=cmath.exp(3j * cmath.pi/4), cw=cmath.exp(-3j * cmath.pi/4)): + length, theta = cmath.polar(vec) + uv = cmath.rect(1, theta) # Unit rotation vector + start = -vec + if length > 3 * lc: # If line is long + ds = cmath.rect(lc, theta) + start += ds # shorten to allow for length of tail chevrons + chev = lc + 0j + polar(dev, origin, vec, color) # Origin to tip + polar(dev, origin, start, color) # Origin to tail + polar(dev, origin + conj(vec), chev*ccw*uv, color) # Tip chevron + polar(dev, origin + conj(vec), chev*cw*uv, color) + if length > lc: # Confusing appearance of very short vectors with tail chevron + polar(dev, origin + conj(start), chev*ccw*uv, color) # Tail chevron + polar(dev, origin + conj(start), chev*cw*uv, color) + +# If a (framebuf based) device is passed to refresh, the screen is cleared. +# None causes pending widgets to be drawn and the result to be copied to hardware. +# The pend mechanism enables a displayable object to postpone its renedering +# until it is complete: efficient for e.g. Dial which may have multiple Pointers +def refresh(device, clear=False): + if not isinstance(device, framebuf.FrameBuffer): + raise ValueError('Device must be derived from FrameBuffer.') + if device not in DObject.devices: + DObject.devices[device] = set() + device.fill(0) + else: + if clear: + DObject.devices[device].clear() # Clear the pending set + device.fill(0) + else: + for obj in DObject.devices[device]: + obj.show() + DObject.devices[device].clear() + device.show() + +# Displayable object: effectively an ABC for all GUI objects. +class DObject(): + devices = {} # Index device instance, value is a set of pending objects + + @classmethod + def _set_pend(cls, obj): + cls.devices[obj.device].add(obj) + + def __init__(self, writer, row, col, height, width, fgcolor, bgcolor, bdcolor): + writer.set_clip(True, True, False) # Disable scrolling text + self.writer = writer + device = writer.device + self.device = device + if row < 0: + row = 0 + self.warning() + elif row + height >= device.height: + row = device.height - height - 1 + self.warning() + if col < 0: + col = 0 + self.warning() + elif col + width >= device.width: + row = device.width - width - 1 + self.warning() + self.row = row + self.col = col + self.width = width + self.height = height + self._value = None # Type depends on context but None means don't display. + # Current colors + if fgcolor is None: + fgcolor = writer.fgcolor + if bgcolor is None: + bgcolor = writer.bgcolor + if bdcolor is None: + bdcolor = fgcolor + self.fgcolor = fgcolor + self.bgcolor = bgcolor + # bdcolor is False if no border is to be drawn + self.bdcolor = bdcolor + # Default colors allow restoration after dynamic change + self.def_fgcolor = fgcolor + self.def_bgcolor = bgcolor + self.def_bdcolor = bdcolor + # has_border is True if a border was drawn + self.has_border = False + + def warning(self): + print('Warning: attempt to create {} outside screen dimensions.'.format(self.__class__.__name__)) + + # Blank working area + # Draw a border if .bdcolor specifies a color. If False, erase an existing border + def show(self): + wri = self.writer + dev = self.device + dev.fill_rect(self.col, self.row, self.width, self.height, self.bgcolor) + if isinstance(self.bdcolor, bool): # No border + if self.has_border: # Border exists: erase it + dev.rect(self.col - 2, self.row - 2, self.width + 4, self.height + 4, self.bgcolor) + self.has_border = False + elif self.bdcolor: # Border is required + dev.rect(self.col - 2, self.row - 2, self.width + 4, self.height + 4, self.bdcolor) + self.has_border = True + + def value(self, v=None): + if v is not None: + self._value = v + return self._value + + def text(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): + if hasattr(self, 'label'): + self.label.value(text, invert, fgcolor, bgcolor, bdcolor) + else: + raise ValueError('Attempt to update nonexistent label.') + +# text: str display string int save width +class Label(DObject): + def __init__(self, writer, row, col, text, invert=False, fgcolor=None, bgcolor=None, bdcolor=False): + # Determine width of object + if isinstance(text, int): + width = text + text = None + else: + width = writer.stringlen(text) + height = writer.height + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + if text is not None: + self.value(text, invert) + + def value(self, text=None, invert=False, fgcolor=None, bgcolor=None, bdcolor=None): + txt = super().value(text) + # Redraw even if no text supplied: colors may have changed. + self.invert = invert + self.fgcolor = self.def_fgcolor if fgcolor is None else fgcolor + self.bgcolor = self.def_bgcolor if bgcolor is None else bgcolor + if bdcolor is False: + self.def_bdcolor = False + self.bdcolor = self.def_bdcolor if bdcolor is None else bdcolor + self.show() + return txt + + def show(self): + txt = super().value() + if txt is None: # No content to draw. Future use. + return + super().show() # Draw or erase border + wri = self.writer + dev = self.device + wri.setcolor(self.fgcolor, self.bgcolor) + Writer.set_textpos(dev, self.row, self.col) + wri.setcolor(self.fgcolor, self.bgcolor) + wri.printstring(txt, self.invert) + wri.setcolor() # Restore defaults + +class Meter(DObject): + BAR = 1 + LINE = 0 + def __init__(self, writer, row, col, *, height=50, width=10, + fgcolor=None, bgcolor=None, ptcolor=None, bdcolor=None, + divisions=5, label=None, style=0, legends=None, value=None): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + self.divisions = divisions + if label is not None: + Label(writer, row + height + 3, col, label) + self.style = style + self.legends = legends + self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor + self.value(value) + + def value(self, n=None, color=None): + if n is None: + return super().value() + n = super().value(min(1, max(0, n))) + if color is not None: + self.ptcolor = color + self.show() + return n + + def show(self): + super().show() # Draw or erase border + val = super().value() + wri = self.writer + dev = self.device + width = self.width + height = self.height + legends = self.legends + x0 = self.col + x1 = self.col + width + y0 = self.row + y1 = self.row + height + if self.divisions > 0: + dy = height / (self.divisions) # Tick marks + for tick in range(self.divisions + 1): + ypos = int(y0 + dy * tick) + dev.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) + + if legends is not None: # Legends + dy = 0 if len(legends) <= 1 else height / (len(legends) -1) + yl = y1 - wri.height / 2 # Start at bottom + for legend in legends: + Label(wri, int(yl), x1 + 4, legend) + yl -= dy + y = int(y1 - val * height) # y position of slider + if self.style == self.LINE: + dev.hline(x0, y, width, self.ptcolor) # Draw pointer + else: + w = width / 2 + dev.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) + + +class LED(DObject): + def __init__(self, writer, row, col, *, height=12, + fgcolor=None, bgcolor=None, bdcolor=None, label=None): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) + if label is not None: + self.label = Label(writer, row + height + 3, col, label) + self.radius = self.height // 2 + + def color(self, c=None): + self.fgcolor = self.bgcolor if c is None else c + self.show() + + def show(self): + super().show() + wri = self.writer + dev = self.device + r = self.radius + fillcircle(dev, self.col + r, self.row + r, r, self.fgcolor) + if isinstance(self.bdcolor, int): + circle(dev, self.col + r, self.row + r, r, self.bdcolor) + + +class Pointer(): + def __init__(self, dial): + self.dial = dial + self.val = 0 + 0j + self.color = None + + def value(self, v=None, color=None): + self.color = color + if v is not None: + if isinstance(v, complex): + l = cmath.polar(v)[0] + if l > 1: + self.val = v/l + else: + self.val = v + else: + raise ValueError('Pointer value must be complex.') + self.dial.vectors.add(self) + self.dial._set_pend(self.dial) # avoid redrawing for each vector + return self.val + +class Dial(DObject): + CLOCK = 0 + COMPASS = 1 + def __init__(self, writer, row, col, *, height=50, + fgcolor=None, bgcolor=None, bdcolor=False, ticks=4, + label=None, style=0, pip=None): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor) + self.style = style + self.pip = self.fgcolor if pip is None else pip + if label is not None: + self.label = Label(writer, row + height + 3, col, label) + radius = int(height / 2) + self.radius = radius + self.ticks = ticks + self.xorigin = col + radius + self.yorigin = row + radius + self.vectors = set() + + def show(self): + super().show() + # cache bound variables + dev = self.device + ticks = self.ticks + radius = self.radius + xo = self.xorigin + yo = self.yorigin + # vectors (complex) + vor = xo + 1j * yo + vtstart = 0.9 * radius + 0j # start of tick + vtick = 0.1 * radius + 0j # tick + vrot = cmath.exp(2j * cmath.pi/ticks) # unit rotation + for _ in range(ticks): + polar(dev, vor + conj(vtstart), vtick, self.fgcolor) + vtick *= vrot + vtstart *= vrot + circle(dev, xo, yo, radius, self.fgcolor) + vshort = 1000 # Length of shortest vector + for v in self.vectors: + color = self.fgcolor if v.color is None else v.color + val = v.value() * radius # val is complex + vshort = min(vshort, cmath.polar(val)[0]) + if self.style == Dial.CLOCK: + polar(dev, vor, val, color) + else: + arrow(dev, vor, val, 5, color) + if isinstance(self.pip, int) and vshort > 5: + fillcircle(dev, xo, yo, 2, self.pip) + diff --git a/plot/FPLOT.md b/plot/FPLOT.md new file mode 100644 index 0000000..f6a1bc4 --- /dev/null +++ b/plot/FPLOT.md @@ -0,0 +1,267 @@ +# fplot module + +This provides a rudimentary means of displaying two dimensional Cartesian (xy) +and polar graphs on `framebuf` based displays. It is an optional extension to +the MicroPython [nano-gui](https://github.com/peterhinch/micropython-nano-gui) +library: this should be installed, configured and tested before use. + +This was ported from the +[lcd160cr-gui library](https://github.com/peterhinch/micropython-lcd160cr-gui). +Like `nanogui.py` it uses synchronous code. + +# Contents + + 1. [Python files](./FPLOT.md#1-python-files) + 2. [Concepts](./FPLOT.md#2-concepts) + 2.1 [Graph classes](./FPLOT.md#21-graph-classes) + 2.2 [Curve clsses](./FPLOT.md#22-curve-classes) + 2.3 [Coordinates](./FPLOT.md#23-coordinates) + 3. [Graph classes](./FPLOT.md#3-graph-classes) + 3.1 [Class CartesianGraph](./FPLOT.md#31-class-cartesiangraph) + 3.2 [Class PolarGraph](./FPLOT.md#32-class-polargraph) + 4. [Curve classes](./FPLOT.md#4-curve-classes) + 4.1 [class Curve](./FPLOT.md#41-class-curve) + 4.1.1 [Scaling](./FPLOT.md#411-scaling) Optional scaling of data values. + 4.2 [class PolarCurve](./FPLOT.md#42-class-polarcurve) + 4.2.1 [Scaling](./FPLOT.md#421-scaling) Required scaling of complex points. + 4.3 [class TSequence](./FPLOT.md#43-class-tsequence) Plot Y values on time axis. + +###### [Main README](../README.md) + +# 1. Python files + +These are located in the `plot` directory. + + 1. `fplot.py` The plot library + 2. `fpt.py` Test program. Usage examples. + +# 2. Concepts + +Data for Cartesian graphs constitutes a set of x, y pairs, for polar graphs +it is a set of complex `z` values. The module supports three common cases: + 1. The dataset is complete at the outset. + 2. Arbitrary data arrives gradually and needs to be plotted as it arrives. + 3. One or more `y` values arrive gradually. The `X` axis represents time. This + is a simplifying case of 2. + +## 2.1 Graph classes + +A user program first instantiates a graph object (`PolarGraph` or +`CartesianGraph`). This creates an empty graph image upon which one or more +curves may be plotted. + +## 2.2 Curve classes + +The user program then instantiates one or more curves (`Curve` or +`PolarCurve`) as appropriate to the graph. Curves may be assigned colors to +distinguish them. + +A curve is plotted by means of a user defined `populate` generator. This +assigns points to the curve in the order in which they are to be plotted. The +curve will be displayed on the graph as a sequence of straight line segments +between successive points. + +Where it is required to plot realtime data as it arrives, this is achieved +via calls to the curve's `point` method. + +## 2.3 Coordinates + +Graph objects are sized and positioned in terms of TFT screen pixel +coordinates, with (0, 0) being the top left corner of the display, with x +increasing to the right and y increasing downwards. The coordinate system +within a graph conforms to normal mathematical conventions. + +Scaling is provided on Cartesian curves enabling user defined ranges for x and +y values. Points lying outside of the defined range will produce lines which +are clipped at the graph boundary. + +Points on polar curves are defined as Python `complex` types and should lie +within the unit circle. Points which are out of range may be plotted beyond the +unit circle but will be clipped to the rectangular graph boundary. + +###### [Contents](./FPLOT.md#contents) + +# 3. Graph classes + +## 3.1 Class CartesianGraph + +Constructor. +Mandatory positional arguments: + 1. `writer` A `CWriter` instance. + 2. `row` Position of the graph in screen coordinates. + 3. `col` + +Keyword only arguments (all optional): + * `height=90` Dimension of the bounding box. + * `width=110` Dimension of the bounding box. + * `fgcolor=None` Color of the axis lines. Defaults to Writer forgeround color. + * `bgcolor=None` Background color of graph. Defaults to Writer background. + * `bdcolor=None` Border color. If `False` no border is displayed. If `None` a + border is shown in the `Writer` forgeround color. If a color is passed, it is + used. + * `gridcolor=None` Color of grid. Default: Writer forgeround color. + * `xdivs=10` Number of divisions (grid lines) on x axis. + * `ydivs=10` Number of divisions on y axis. + * `xorigin=5` Location of origin in terms of grid divisions. + * `yorigin=5` As `xorigin`. The default of 5, 5 with 10 grid lines on each + axis puts the origin at the centre of the graph. Settings of 0, 0 would be + used to plot positive values only. + +Methods: + 1. `clear` No args. Clears all curves from the graph. + 2. `show` No args. Redraws the graph. For future/subclass use. + +## 3.2 Class PolarGraph + +Constructor. +Mandatory positional arguments: + 1. `writer` A `CWriter` instance. + 2. `row` Position of the graph in screen coordinates. + 3. `col` + +Keyword only arguments (all optional): + * `height=90` Dimension of the square bounding box. + * `fgcolor=None` Color of the axis lines. Defaults to Writer forgeround color. + * `bgcolor=None` Background color of graph. Defaults to Writer background. + * `bdcolor=None` Border color. If `False` no border is displayed. If `None` a + border is shown in the `Writer` forgeround color. If a color is passed, it is + used. + * `gridcolor=None` Color of grid. Default: Writer forgeround color. + * `adivs=3` Number of angle divisions per quadrant. + * `rdivs=4` Number radius divisions. + +Methods: + 1. `clear` No args. Clears all curves from the graph. + 2. `show` No args. Redraws the graph. For future/subclass use. + +###### [Contents](./FPLOT.md#contents) + +# 4. Curve classes + +## 4.1 class Curve + +The Cartesian curve constructor takes the following positional arguments: + +Mandatory arguments: + 1. `graph` The `CartesianGraph` instance. + 2. `color` + +Optional arguments: + 3. `populate=None` A generator to populate the curve. See below. + 4. `origin=(0,0)` 2-tuple containing x and y values for the origin. Provides + for an optional shift of the data's origin. + 5. `excursion=(1,1)` 2-tuple containing scaling values for x and y. + +Methods: + * `point` Arguments x, y. Defaults `None`. Adds a point to the curve. If a + prior point exists a line will be drawn between it and the current point. If a + point is out of range or if either arg is `None` no line will be drawn. + Passing no args enables discontinuous curves to be plotted. This method is + normally used for real time plotting. + +The `populate` generator may take zero or more positional arguments. It should +repeatedly yield `x, y` values before returning. Where a curve is discontinuous +`None, None` may be yielded: this causes the line to stop. It is resumed when +the next valid `x, y` pair is yielded. + +If `populate` is not provided the curve may be plotted by successive calls to +the `point` method. This may be of use where data points are acquired in real +time, and realtime plotting is required. See function `rt_rect` in `fpt.py`. + +### 4.1.1 Scaling + +By default, with symmetrical axes, x and y values are assumed to lie between -1 +and +1. + +To plot x values from 1000 to 4000 we would set the `origin` x value to 1000 +and the `excursion` x value to 3000. The `excursion` values scale the plotted +values to fit the corresponding axis. + +## 4.2 class PolarCurve + +The constructor takes the following positional arguments: + +Mandatory arguments: + 1. `graph` The `PolarGraph` instance. + 2. `color` + +Optional arguments: + 3. `populate=None` A generator to populate the curve. See below. + +Methods: + * `point` Argument `z=None`. Normally a `complex`. Adds a point + to the curve. If a prior point exists a line will be drawn between it and the + current point. If the arg is `None` no line will be drawn. Passing no args + enables discontinuous curves to be plotted. Lines are clipped at the square + region bounded by (-1, -1) to (+1, +1). + +The `populate` generator may take zero or more positional arguments. It should +yield a complex `z` value for each point before returning. Where a curve is +discontinuous a value of `None` may be yielded: this causes plotting to stop. +It is resumed when the next valid `z` point is yielded. + +If `populate` is not provided the curve may be plotted by successive calls to +the `point` method. This may be of use where data points are acquired in real +time, and realtime plotting is required. + +### 4.2.1 Scaling + +Complex points should lie within the unit circle to be drawn within the grid. + +###### [Contents](./FPLOT.md#contents) + +## 4.3 class TSequence + +A common task is the acquisition and plotting of real time data against time, +such as hourly temperature and air pressure readings. This class facilitates +this. Time is on the x-axis with the most recent data on the right. Older +points are plotted to the left until they reach the left hand edge when they +are discarded. This is akin to old fashioned pen plotters where the pen was at +the rightmost edge (corresponding to time now) with old values scrolling to the +left with the time axis in the conventional direction. + +The user instantiates a graph with the X origin at the right hand side and then +instantiates one or more `TSequence` objects. As each set of data arrives it is +appended to its `TSequence` using the `add` method. See the example below. + +The constructor takes the following args: + +Mandatory arguments: + 1. `graph` The `PolarGraph` instance. + 2. `color` + 3. `size` Integer. The number of time samples to be plotted. See below. + +Optional arguments: + 4. `yorigin=0` These args provide scaling of Y axis values as per the `Curve` + class. + 5 `yexc=1` + +Method: + 1. `add` Arg `v` the value to be plotted. This should lie between -1 and +1 + unless scaling is applied. + +Note that there is little point in setting the `size` argument to a value +greater than the number of X-axis pixels on the graph. It will work but RAM +and execution time will be wasted: the constructor instantiates an array of +floats of this size. + +Each time a data set arrives the graph should be cleared, a data value should +be added to each `TSequence` instance, and the display instance should be +refreshed. The following example assumes that `ssd` is the display device and +`wri` is a `Writer` or `CWriter` instance. + +```python +def foo(): + refresh(ssd, True) # Clear any prior image + g = CartesianGraph(wri, 2, 2, xorigin = 10, fgcolor=WHITE, gridcolor=LIGHTGREEN) + tsy = TSequence(g, YELLOW, 50) + tsr = TSequence(g, RED, 50) + for t in range(100): + g.clear() + tsy.add(0.9*math.sin(t/10)) + tsr.add(0.4*math.cos(t/10)) + refresh(ssd) + utime.sleep_ms(100) +``` + +###### [Contents](./FPLOT.md#contents) diff --git a/plot/fplot.py b/plot/fplot.py new file mode 100644 index 0000000..ce3e609 --- /dev/null +++ b/plot/fplot.py @@ -0,0 +1,272 @@ +# fplot.py Graph plotting extension for nanogui +# Now clips out of range lines + +# The MIT License (MIT) +# +# Copyright (c) 2018 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from nanogui import DObject, circle +from cmath import rect, pi +from micropython import const +from array import array + +type_gen = type((lambda: (yield))()) + +# Line clipping outcode bits +_TOP = const(1) +_BOTTOM = const(2) +_LEFT = const(4) +_RIGHT = const(8) +# Bounding box for line clipping +_XMAX = const(1) +_XMIN = const(-1) +_YMAX = const(1) +_YMIN = const(-1) + + +class Curve(): + @staticmethod + def _outcode(x, y): + oc = _TOP if y > 1 else 0 + oc |= _BOTTOM if y < -1 else 0 + oc |= _RIGHT if x > 1 else 0 + oc |= _LEFT if x < -1 else 0 + return oc + + def __init__(self, graph, color, populate=None, origin=(0, 0), excursion=(1, 1)): + self.graph = graph + self.origin = origin + self.excursion = excursion + self.color = color + self.lastpoint = None + self.newpoint = None + if populate is not None and self._valid(populate): + for x, y in populate: + self.point(x, y) + + def _valid(self, populate): + if not isinstance(populate, type_gen): + raise ValueError('populate must be a generator.') + return True + + def point(self, x=None, y=None): + if x is None or y is None: + self.newpoint = None + self.lastpoint = None + return + + self.newpoint = self._scale(x, y) # In-range points scaled to +-1 bounding box + if self.lastpoint is None: # Nothing to plot. Save for next line. + self.lastpoint = self.newpoint + return + + res = self._clip(*(self.lastpoint + self.newpoint)) # Clip to +-1 box + if res is not None: # Ignore lines which don't intersect + self.graph.line(res[0:2], res[2:], self.color) + self.lastpoint = self.newpoint # Scaled but not clipped + + # Cohen–Sutherland line clipping algorithm + # If self.newpoint and self.lastpoint are valid clip them so that both lie + # in +-1 range. If both are outside the box return None. + def _clip(self, x0, y0, x1, y1): + oc1 = self._outcode(x0, y0) + oc2 = self._outcode(x1, y1) + while True: + if not oc1 | oc2: # OK to plot + return x0, y0, x1, y1 + if oc1 & oc2: # Nothing to do + return + oc = oc1 if oc1 else oc2 + if oc & _TOP: + x = x0 + (_YMAX - y0)*(x1 - x0)/(y1 - y0) + y = _YMAX + elif oc & _BOTTOM: + x = x0 + (_YMIN - y0)*(x1 - x0)/(y1 - y0) + y = _YMIN + elif oc & _RIGHT: + y = y0 + (_XMAX - x0)*(y1 - y0)/(x1 - x0) + x = _XMAX + elif oc & _LEFT: + y = y0 + (_XMIN - x0)*(y1 - y0)/(x1 - x0) + x = _XMIN + if oc is oc1: + x0, y0 = x, y + oc1 = self._outcode(x0, y0) + else: + x1, y1 = x, y + oc2 = self._outcode(x1, y1) + + def _scale(self, x, y): # Scale to +-1.0 + x0, y0 = self.origin + xr, yr = self.excursion + xs = (x - x0) / xr + ys = (y - y0) / yr + return xs, ys + +class PolarCurve(Curve): # Points are complex + def __init__(self, graph, color, populate=None): + super().__init__(graph, color) + if populate is not None and self._valid(populate): + for z in populate: + self.point(z) + + def point(self, z=None): + if z is None: + self.newpoint = None + self.lastpoint = None + return + + self.newpoint = self._scale(z.real, z.imag) # In-range points scaled to +-1 bounding box + if self.lastpoint is None: # Nothing to plot. Save for next line. + self.lastpoint = self.newpoint + return + + res = self._clip(*(self.lastpoint + self.newpoint)) # Clip to +-1 box + if res is not None: # At least part of line was in box + start = res[0] + 1j*res[1] + end = res[2] + 1j*res[3] + self.graph.cline(start, end, self.color) + self.lastpoint = self.newpoint # Scaled but not clipped + + +class TSequence(Curve): + def __init__(self, graph, color, size, yorigin=0, yexc=1): + super().__init__(graph, color, origin=(0, yorigin), excursion=(1, yexc)) + self.data = array('f', (0 for _ in range(size))) + self.cur = 0 + self.size = size + self.count = 0 + + def add(self, v): + p = self.cur + size = self.size + self.data[self.cur] = v + self.cur += 1 + self.cur %= size + if self.count < size: + self.count += 1 + x = 0 + dx = 1/size + for _ in range(self.count): + self.point(x, self.data[p]) + x -= dx + p -= 1 + p %= size + self.point() + + +class Graph(DObject): + def __init__(self, writer, row, col, height, width, fgcolor, bgcolor, bdcolor, gridcolor): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + super().show() # Draw border + self.x0 = col + self.x1 = col + width + self.y0 = row + self.y1 = row + height + if gridcolor is None: + gridcolor = self.fgcolor + self.gridcolor = gridcolor + + def clear(self): + self.show() # Clear working area + +class CartesianGraph(Graph): + def __init__(self, writer, row, col, *, height=90, width = 120, fgcolor=None, bgcolor=None, bdcolor=None, + gridcolor=None, xdivs=10, ydivs=10, xorigin=5, yorigin=5): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, gridcolor) + self.xdivs = xdivs + self.ydivs = ydivs + self.x_axis_len = max(xorigin, xdivs - xorigin) * width / xdivs # Max distance from origin in pixels + self.y_axis_len = max(yorigin, ydivs - yorigin) * height / ydivs + self.xp_origin = self.x0 + xorigin * width / xdivs # Origin in pixels + self.yp_origin = self.y0 + (ydivs - yorigin) * height / ydivs + self.xorigin = xorigin + self.yorigin = yorigin + self.show() + + def show(self): + super().show() # Clear working area + ssd = self.device + x0 = self.x0 + x1 = self.x1 + y0 = self.y0 + y1 = self.y1 + if self.ydivs > 0: + dy = self.height / (self.ydivs) # Y grid line + for line in range(self.ydivs + 1): + color = self.fgcolor if line == self.yorigin else self.gridcolor + ypos = round(self.y1 - dy * line) + ssd.hline(x0, ypos, x1 - x0, color) + if self.xdivs > 0: + width = x1 - x0 + dx = width / (self.xdivs) # X grid line + for line in range(self.xdivs + 1): + color = self.fgcolor if line == self.xorigin else self.gridcolor + xpos = round(x0 + dx * line) + ssd.vline(xpos, y0, y1 - y0, color) + + # Called by Curve + def line(self, start, end, color): # start and end relative to origin and scaled -1 .. 0 .. +1 + xs = round(self.xp_origin + start[0] * self.x_axis_len) + ys = round(self.yp_origin - start[1] * self.y_axis_len) + xe = round(self.xp_origin + end[0] * self.x_axis_len) + ye = round(self.yp_origin - end[1] * self.y_axis_len) + self.device.line(xs, ys, xe, ye, color) + +class PolarGraph(Graph): + def __init__(self, writer, row, col, *, height=90, fgcolor=None, bgcolor=None, bdcolor=None, + gridcolor=None, adivs=3, rdivs=4): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, gridcolor) + self.adivs = adivs * 2 # No. of divisions of Pi radians + self.rdivs = rdivs + self.radius = round(height / 2) # Unit: pixels + self.xp_origin = self.x0 + self.radius # Origin in pixels + self.yp_origin = self.y0 + self.radius + self.show() + + def show(self): + super().show() # Clear working area + ssd = self.device + x0 = self.x0 + y0 = self.y0 + radius = self.radius + adivs = self.adivs + rdivs = self.rdivs + diam = 2 * radius + if rdivs > 0: + for r in range(1, rdivs + 1): + circle(ssd, self.xp_origin, self.yp_origin, round(radius * r / rdivs), self.gridcolor) + if adivs > 0: + v = complex(1) + m = rect(1, pi / adivs) + for _ in range(adivs): + self.cline(-v, v, self.gridcolor) + v *= m + ssd.vline(x0 + radius, y0, diam, self.fgcolor) + ssd.hline(x0, y0 + radius, diam, self.fgcolor) + + def cline(self, start, end, color): # start and end are complex, 0 <= magnitude <= 1 + height = self.radius # Unit: pixels + xs = round(self.xp_origin + start.real * height) + ys = round(self.yp_origin - start.imag * height) + xe = round(self.xp_origin + end.real * height) + ye = round(self.yp_origin - end.imag * height) + self.device.line(xs, ys, xe, ye, color) diff --git a/plot/fpt.py b/plot/fpt.py new file mode 100644 index 0000000..eebc674 --- /dev/null +++ b/plot/fpt.py @@ -0,0 +1,214 @@ +# fpt.py Test/demo program for framebuf plot +# Uses Adafruit ssd1351-based OLED displays (change height to suit) +# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431 +# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673 + +# The MIT License (MIT) + +# Copyright (c) 2018 Peter Hinch + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# WIRING (Adafruit pin nos and names) +# Pyb SSD +# 3v3 Vin (10) +# Gnd Gnd (11) +# X1 DC (3 DC) +# X2 CS (5 OC OLEDCS) +# X3 Rst (4 R RESET) +# X6 CLK (2 CL SCK) +# X8 DATA (1 SI MOSI) + +height = 96 # 1.27 inch 96*128 (rows*cols) display +# height = 128 # 1.5 inch 128*128 display + +import machine +import gc +from ssd1351 import SSD1351 as SSD + +# Initialise hardware and framebuf before importing modules +pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(1) +gc.collect() # Precaution befor instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance + +import cmath +import math +import utime +import uos +from writer import Writer, CWriter +from fplot import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence +from nanogui import Label, refresh +refresh(ssd) + +# Fonts +import arial10, freesans20 + +GREEN = SSD.rgb(0, 255, 0) +RED = SSD.rgb(255, 0, 0) +BLUE = SSD.rgb(0, 0, 255) +YELLOW = SSD.rgb(255, 255, 0) +WHITE = SSD.rgb(255, 255, 255) +BLACK = 0 +LIGHTGREEN = SSD.rgb(0, 100, 0) + +CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it +wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) +wri.set_clip(True, True, False) + +def cart(): + print('Cartesian data test.') + def populate_1(func): + x = -1 + while x < 1.01: + yield x, func(x) # x, y + x += 0.1 + + def populate_2(): + x = -1 + while x < 1.01: + yield x, x**2 # x, y + x += 0.1 + + refresh(ssd, True) # Clear any prior image + g = CartesianGraph(wri, 2, 2, yorigin = 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) # Asymmetric y axis + curve1 = Curve(g, YELLOW, populate_1(lambda x : x**3 + x**2 -x,)) # args demo + curve2 = Curve(g, RED, populate_2()) + refresh(ssd) + +def polar(): + print('Polar data test.') + def populate(): + def f(theta): + return cmath.rect(math.sin(3 * theta), theta) # complex + nmax = 150 + for n in range(nmax + 1): + yield f(2 * cmath.pi * n / nmax) # complex z + refresh(ssd, True) # Clear any prior image + g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curve = PolarCurve(g, YELLOW, populate()) + refresh(ssd) + +def polar_clip(): + print('Test of polar data clipping.') + def populate(rot): + f = lambda theta : cmath.rect(1.15 * math.sin(5 * theta), theta) * rot # complex + nmax = 150 + for n in range(nmax + 1): + yield f(2 * cmath.pi * n / nmax) # complex z + refresh(ssd, True) # Clear any prior image + g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curve = PolarCurve(g, YELLOW, populate(1)) + curve1 = PolarCurve(g, RED, populate(cmath.rect(1, cmath.pi/5),)) + refresh(ssd) + +def rt_polar(): + print('Simulate realtime polar data acquisition.') + refresh(ssd, True) # Clear any prior image + g = PolarGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curvey = PolarCurve(g, YELLOW) + curver = PolarCurve(g, RED) + for x in range(100): + curvey.point(cmath.rect(x/100, -x * cmath.pi/30)) + curver.point(cmath.rect((100 - x)/100, -x * cmath.pi/30)) + utime.sleep_ms(60) + refresh(ssd) + +def rt_rect(): + print('Simulate realtime data acquisition of discontinuous data.') + refresh(ssd, True) # Clear any prior image + g = CartesianGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curve = Curve(g, RED) + x = -1 + for _ in range(40): + y = 0.1/x if abs(x) > 0.05 else None # Discontinuity + curve.point(x, y) + utime.sleep_ms(100) + refresh(ssd) + x += 0.05 + g.clear() + curve = Curve(g, YELLOW) + x = -1 + for _ in range(40): + y = -0.1/x if abs(x) > 0.05 else None # Discontinuity + curve.point(x, y) + utime.sleep_ms(100) + refresh(ssd) + x += 0.05 + + +def lem(): + print('Lemniscate of Bernoulli.') + def populate(): + t = -math.pi + while t <= math.pi + 0.1: + x = 0.5*math.sqrt(2)*math.cos(t)/(math.sin(t)**2 + 1) + y = math.sqrt(2)*math.cos(t)*math.sin(t)/(math.sin(t)**2 + 1) + yield x, y + t += 0.1 + refresh(ssd, True) # Clear any prior image + Label(wri, 82, 2, 'To infinity and beyond...') + g = CartesianGraph(wri, 2, 2, height = 75, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curve = Curve(g, YELLOW, populate()) + refresh(ssd) + +def liss(): + print('Lissajous figure.') + def populate(): + t = -math.pi + while t <= math.pi: + yield math.sin(t), math.cos(3*t) # x, y + t += 0.1 + refresh(ssd, True) # Clear any prior image + g = CartesianGraph(wri, 2, 2, fgcolor=WHITE, gridcolor=LIGHTGREEN) + curve = Curve(g, YELLOW, populate()) + refresh(ssd) + +def seq(): + print('Time sequence test - sine and cosine.') + refresh(ssd, True) # Clear any prior image + # y axis at t==now, no border + g = CartesianGraph(wri, 2, 2, xorigin = 10, fgcolor=WHITE, + gridcolor=LIGHTGREEN, bdcolor=False) + tsy = TSequence(g, YELLOW, 50) + tsr = TSequence(g, RED, 50) + for t in range(100): + g.clear() + tsy.add(0.9*math.sin(t/10)) + tsr.add(0.4*math.cos(t/10)) + refresh(ssd) + utime.sleep_ms(100) + +seq() +utime.sleep(1.5) +liss() +utime.sleep(1.5) +rt_rect() +utime.sleep(1.5) +rt_polar() +utime.sleep(1.5) +polar() +utime.sleep(1.5) +cart() +utime.sleep(1.5) +polar_clip() +utime.sleep(1.5) +lem()