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 0000000..86f3e0a Binary files /dev/null and b/images/IMG_2885.png differ diff --git a/images/IMG_2887.png b/images/IMG_2887.png new file mode 100644 index 0000000..f108bdc Binary files /dev/null and b/images/IMG_2887.png differ 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()