From 64d75fe2733ce72d45af453c0857f3739a272930 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 Jun 2021 17:11:48 +0100 Subject: [PATCH] Initial commit. --- README.md | 470 +++++++++++++++++++- drivers/epaper/epaper2in7_fb.py | 303 +++++++++++++ drivers/epaper/epd29.py | 234 ++++++++++ drivers/ili93xx/ili9341.py | 166 +++++++ drivers/sharp/__init__.py | 0 drivers/sharp/sharp.py | 70 +++ drivers/ssd1306/ssd1306.py | 159 +++++++ drivers/ssd1331/__init__.py | 0 drivers/ssd1331/ssd1331.py | 81 ++++ drivers/ssd1331/ssd1331_16bit.py | 81 ++++ drivers/ssd1331/test_colors.py | 31 ++ drivers/ssd1351/__init__.py | 0 drivers/ssd1351/ssd1351.py | 143 ++++++ drivers/ssd1351/ssd1351_16bit.py | 108 +++++ drivers/ssd1351/ssd1351_4bit.py | 142 ++++++ drivers/ssd1351/ssd1351_generic.py | 131 ++++++ drivers/ssd1351/test128_row.py | 18 + drivers/ssd1351/test96_row.py | 18 + drivers/ssd1351/test_colors_96.py | 27 ++ drivers/st7735r/st7735_notes | 53 +++ drivers/st7735r/st7735r.py | 154 +++++++ drivers/st7735r/st7735r144.py | 158 +++++++ drivers/st7735r/st7735r144_4bit.py | 162 +++++++ drivers/st7735r/st7735r_4bit.py | 156 +++++++ drivers/st7789/st7789_4bit.py | 237 ++++++++++ gui/core/colors.py | 56 +++ gui/core/ugui.py | 687 +++++++++++++++++++++++++++++ gui/core/writer.py | 336 ++++++++++++++ gui/demos/active.py | 99 +++++ gui/demos/plot.py | 250 +++++++++++ gui/demos/screens.py | 109 +++++ gui/demos/simple.py | 39 ++ gui/demos/tbox.py | 120 +++++ gui/demos/various.py | 160 +++++++ gui/demos/vtest.py | 113 +++++ gui/fonts/__init__.py | 0 gui/fonts/arial10.py | 139 ++++++ gui/fonts/arial35.py | 671 ++++++++++++++++++++++++++++ gui/fonts/arial_50.py | 232 ++++++++++ gui/fonts/courier20.py | 308 +++++++++++++ gui/fonts/font10.py | 296 +++++++++++++ gui/fonts/font14.py | 395 +++++++++++++++++ gui/fonts/font6.py | 176 ++++++++ gui/fonts/freesans20.py | 288 ++++++++++++ gui/primitives/__init__.py | 31 ++ gui/primitives/delay_ms.py | 71 +++ gui/primitives/pushbutton.py | 107 +++++ gui/primitives/switch.py | 42 ++ gui/widgets/__init__.py | 0 gui/widgets/buttons.py | 218 +++++++++ gui/widgets/checkbox.py | 36 ++ gui/widgets/dial.py | 104 +++++ gui/widgets/dialog.py | 52 +++ gui/widgets/dropdown.py | 92 ++++ gui/widgets/graph.py | 259 +++++++++++ gui/widgets/knob.py | 60 +++ gui/widgets/label.py | 38 ++ gui/widgets/led.py | 27 ++ gui/widgets/listbox.py | 106 +++++ gui/widgets/meter.py | 64 +++ gui/widgets/scale.py | 132 ++++++ gui/widgets/scale_log.py | 157 +++++++ gui/widgets/sliders.py | 161 +++++++ gui/widgets/textbox.py | 140 ++++++ gui/widgets/vectors.py | 120 +++++ hardware_setup.py | 55 +++ setup_examples/ili9341_pico.py | 55 +++ setup_examples/st7789_ttgo.py | 123 ++++++ 68 files changed, 9822 insertions(+), 4 deletions(-) create mode 100644 drivers/epaper/epaper2in7_fb.py create mode 100644 drivers/epaper/epd29.py create mode 100644 drivers/ili93xx/ili9341.py create mode 100644 drivers/sharp/__init__.py create mode 100644 drivers/sharp/sharp.py create mode 100644 drivers/ssd1306/ssd1306.py create mode 100644 drivers/ssd1331/__init__.py create mode 100644 drivers/ssd1331/ssd1331.py create mode 100644 drivers/ssd1331/ssd1331_16bit.py create mode 100644 drivers/ssd1331/test_colors.py create mode 100644 drivers/ssd1351/__init__.py create mode 100644 drivers/ssd1351/ssd1351.py create mode 100644 drivers/ssd1351/ssd1351_16bit.py create mode 100644 drivers/ssd1351/ssd1351_4bit.py create mode 100644 drivers/ssd1351/ssd1351_generic.py create mode 100644 drivers/ssd1351/test128_row.py create mode 100644 drivers/ssd1351/test96_row.py create mode 100644 drivers/ssd1351/test_colors_96.py create mode 100644 drivers/st7735r/st7735_notes create mode 100644 drivers/st7735r/st7735r.py create mode 100644 drivers/st7735r/st7735r144.py create mode 100644 drivers/st7735r/st7735r144_4bit.py create mode 100644 drivers/st7735r/st7735r_4bit.py create mode 100644 drivers/st7789/st7789_4bit.py create mode 100644 gui/core/colors.py create mode 100644 gui/core/ugui.py create mode 100644 gui/core/writer.py create mode 100644 gui/demos/active.py create mode 100644 gui/demos/plot.py create mode 100644 gui/demos/screens.py create mode 100644 gui/demos/simple.py create mode 100644 gui/demos/tbox.py create mode 100644 gui/demos/various.py create mode 100644 gui/demos/vtest.py create mode 100644 gui/fonts/__init__.py create mode 100644 gui/fonts/arial10.py create mode 100644 gui/fonts/arial35.py create mode 100644 gui/fonts/arial_50.py create mode 100644 gui/fonts/courier20.py create mode 100644 gui/fonts/font10.py create mode 100644 gui/fonts/font14.py create mode 100644 gui/fonts/font6.py create mode 100644 gui/fonts/freesans20.py create mode 100644 gui/primitives/__init__.py create mode 100644 gui/primitives/delay_ms.py create mode 100644 gui/primitives/pushbutton.py create mode 100644 gui/primitives/switch.py create mode 100644 gui/widgets/__init__.py create mode 100644 gui/widgets/buttons.py create mode 100644 gui/widgets/checkbox.py create mode 100644 gui/widgets/dial.py create mode 100644 gui/widgets/dialog.py create mode 100644 gui/widgets/dropdown.py create mode 100644 gui/widgets/graph.py create mode 100644 gui/widgets/knob.py create mode 100644 gui/widgets/label.py create mode 100644 gui/widgets/led.py create mode 100644 gui/widgets/listbox.py create mode 100644 gui/widgets/meter.py create mode 100644 gui/widgets/scale.py create mode 100644 gui/widgets/scale_log.py create mode 100644 gui/widgets/sliders.py create mode 100644 gui/widgets/textbox.py create mode 100644 gui/widgets/vectors.py create mode 100644 hardware_setup.py create mode 100644 setup_examples/ili9341_pico.py create mode 100644 setup_examples/st7789_ttgo.py diff --git a/README.md b/README.md index 268a25f..2f46ed2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,470 @@ # micropython-micro-gui -Under development: will be released soon after further testing. -A lightweight MicroPython GUI library for display drivers based on framebuf, allows input via pushbuttons or via a switch joystick. +This is a lightweight, portable, MicroPython GUI library for displays with +drivers subclassed from `framebuf`. It allows input via pushbuttons or via a +switch joystick. -It is larger and more complex than nano-gui owing to the support for input. It enables switching between screens and launching modal windows. In addition to nano-gui widgets it supports listboxes, dropdown lists, various means of entering or displaying floating point values, and other widgets. +It is larger and more complex than `nano-gui` owing to the support for input. +It enables switching between screens and launching modal windows. In addition +to `nano-gui` widgets it supports listboxes, dropdown lists, various means of +entering or displaying floating point values, and other widgets. -It uses display drivers for [nano-gui](https://github.com/peterhinch/micropython-nano-gui) providing portability to a wide range of displays. It is also portable between hosts. Currently running on RP2, but I intend to test on Pyboard and ESP32/TTGo TDisplay. +It is compatible with all display drivers for +[nano-gui](https://github.com/peterhinch/micropython-nano-gui) so is portable +to a wide range of displays. It is also portable between hosts. + +## This document is seriously incomplete. +## It may be several weeks before it is usable. +## Code is in a better state. + +# 0. Contents + +**TODO** + +# 1. Basic concepts + +Internally `micro-gui` uses `uasyncio`. It presents a conventional callback +based interface; knowledge of `uasyncio` is not required for its use. Display +refresh is handled automatically. As in nano-gui, widgets are drawn using +graphics primitives rather than icons. This makes them efficiently scalable and +minimises RAM usage compared to icon-based graphics. It also facilitates the +provision of extra visual information. For example the color of all or part of +a widget may be changed programmatically, for example to highlight an overrange +condition. + +## 1.1 Coordinates + +These are defined as `row` and `col` values where `row==0` and `col==0` +corresponds to the top left most pixel. Rows increase downwards and columns +increase to the right. The graph plotting widget uses normal mathematical +conventions within graphs. + +## 1.2 Screen, Window and Widget objects + +A `Screen` is a window which occupies the entire display. A `Screen` can +overlay another, replacing all its contents. When closed, the `Screen` below is +re-displayed. + +A `Window` is a subclass of `Screen` but is smaller, with size and location +attributes. It can overlay part of an underlying `Screen` and is typically used +for modal dialog boxes. + +A `Widget` is an object capable of displaying data. Some are also capable of +data input. The latter can be capable of accepting focus, see +[navigation](./README.md#13-navigation). `Widget` objects have dimensions +defined as `height` and `width`. The space requred by them exceeds these by two +pixels all round, as a white border is drawn to show which object currently has +focus. Thus to place a `Widget` at the extreme top left, `row` and `col` values +should be 2. + +## 1.3 Fonts + +Python font files are in the `gui/fonts` directory. The easiest way to conserve +RAM is to freeze them which is highly recommended. In doing so the directory +structure must be maintained. + +To create alternatives, Python fonts may be generated from industry standard +font files with +[font_to_py.py](https://github.com/peterhinch/micropython-font-to-py.git). The +`-x` option for horizontal mapping must be specified. If fixed pitch rendering +is required `-f` is also required. Supplied examples are: + + * `arial10.py` Variable pitch Arial. 10 pixels high. + * `arial35.py` Arial 35 high. + * `arial_50.py` Arial 50 high. + * `courier20.py` Fixed pitch Courier, 20 high. + * `font6.py` FreeSans 14 high. + * `font10.py` FreeSans 17 high. + * `freesans20.py` FreeSans 20 high. + +## 1.4 Navigation + +The GUI requires from 2 to 5 pushbuttons for control. These are: + 1. `Next` Move to the next widget. + 2. `Select` Operate the currently selected widget. + 3. `Prev` Move to the previous widget. + 4. `Increase` Move within the widget. + 5. `Decrease` Move within the widget. + +Many widgets such as `Pushbutton` or `Checkbox` objects require only the +`Select` button to operate: it is possible to design an interface using only +the first two buttons. + +Widgets such as `Listbox` objects, dropdown lists (`Dropdown`), and those for +floating point data entry require the `Increase` and `Decrease` buttons to move +within the widget or to adjust the linear value. + +A `LinearIO` is a `Widget` that responds to the `increase` and `decrease` +buttons by running an `asyncio` task. These typically output floating point +values using an accelerating algorithm responding to the duration of the button +press. This enables floats with a wide dynamic range to be adjusted with +precision. + +The currently selected `Widget` is identified by a white border: the focus +moves between widgets via `Next` and `Prev`. Only `Widget` instances that can +accept input can receive the focus; such widgets are defined as `active`. Some +widgets can be declared as `active` or not in the constructor. An `active` +widget can be disabled and re-enabled at runtime. While disabled a widget is +shown "greyed-out" and cannot accept the focus. + +## 1.5 Hardware definition + +A file `hardware_setup.py` must exist in the GUI root directory. This defines +the connections to the display, the display driver, and pins used for the +pushbuttons. Example files may be found in the `setup_examples` directory. + +Display drivers are documented +[here](https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md). + +## 1.6 Installation + +The easy way to start is to use `mpremote` which allows a directory on your PC +to be mounted on the host. In this way the filesystem on the host is left +unchanged. This is at some cost in loading speed, especially on ESP32. If +adopting this approach, you will need to ensure the `hardware_setup.py` file on +the PC matches your hardware. Install `mpremote` with +```bash +$ pip3 install mpremote +``` +Clone the repo to your PC with +```bash +$ git clone https://github.com/peterhinch/micropython-micro-gui +$ cd micropython-micro-gui +``` +Edit `hardware_setup.py` then run: +```bash +$ mpremote mount . +``` +This should provide a REPL. Run the minimal demo: +```python +>>> import gui.demos.simple +``` +If installing to the device's filesystem it is necessary to maintain the +directory structure. The `drivers` and `gui` directories (with subdirectories +and contents) should be copied, along with `hardware_setup.py`. Filesystem +space may be conserved by copying only the display driver in use. Unused +widgets, fonts and demos can also be trimmed, but directory structure must be +kept. + +There is scope for speeding loading and saving RAM by using frozen bytecode. +Once again, directory structure must be maintained. + +## 1.7 Quick hardware check + +The following may be pasted at the REPL to verify correct connection to the +display. It also confirms that `hardware_setup.py` is specifying a suitable +display driver. +```python +from hardware_setup import ssd # Create a display instance +from gui.core.colors import * +ssd.fill(0) +ssd.line(0, 0, ssd.width - 1, ssd.height - 1, GREEN) # Green diagonal corner-to-corner +ssd.rect(0, 0, 15, 15, RED) # Red square at top left +ssd.rect(ssd.width -15, ssd.height -15, 15, 15, BLUE) # Blue square at bottom right +ssd.show() +``` + +## 1.8 Performance and hardware notes + +The largest supported display is a 320x240 ILI9341 unit. On a Pi Pico with no +use of frozen bytecode the demos run with over 74K of free RAM. Substantial +improvements could be achieved using frozen bytecode. + +Snappy navigation benefits from several approaches: + 1. Clocking the SPI bus as fast as possible. + 2. Clocking the host fast (`machine.freq`). + 3. Device driver support for `uasyncio`. Currently this exists on ILI9341 and + ST7789 (e.g. TTGO T-Display). I intend to extend this to other drivers. + +On ESP32 I found it necessary to use physical pullup resistors on the +pushbutton GPIO lines. + +## 1.9 Firmware and dependencies + +Firmware should be V1.15 or later. + +The source tree includes all dependencies. These are listed to enable users to +check for newer versions: + + * [writer.py](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer/writer.py) + Provides text rendering of Python font files. + +A copy of the official driver for OLED displays using the SSD1306 chip is +provided. The official file is here: + * [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). + +Displays based on the Nokia 5110 (PCD8544 chip) require this driver. It is not +in this repo but may be found here: + * [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git) + +Synchronisation primitives for `uasyncio` may be found here: + * [My async repo](https://github.com/peterhinch/micropython-async/tree/master/v3/primitives). + +## 1.10 Supported hosts and displays + +Development was done using a Raspberry Pi Pico connected to a cheap ILI9341 +320x240 display. I have also tested a TTGO T-Display (an ESP32 host) and a +Pyboard. Code is written with portability as an aim, but MicroPython configs +vary between platforms and I can't guarantee that every widget will work on +every platform. For example, some use the `cmath` module which may be absent on +some builds. + +Supported displays are as per +[the nano-gui list](https://github.com/peterhinch/micropython-nano-gui/blob/master/README.md#12-description). +In practice usage with ePaper displays is questionable because of their slow +refresh times. I haven't tested these, or the Sharp displays. + +Display drivers are documented [here](https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md). + +## 1.11 Files + +Display drivers may be found in the `drivers` directory. These are copies of +those in `nano-gui`, included for convenience. + +The system is organised as a Python package with the root being `gui`. Core +files in `gui/core` are: + * `colors.py` Constants including colors and shapes. + * `ugui.py` The main GUI code. + * `writer.py` Supports the `Writer` and `CWriter` classes. + +The `gui/primitives` directory contains the following files: + * `switch.py` Interface to physical pushbuttons. + * `delay_ms.py` A software triggerable timer. + +The `gui/demos` directory contains a variety of demos and tests, some of which +require a large (320x240) display. Demos are run by issuing (fo example): +```python +>>> import gui.demos.simple +``` + * `simple.py` Minimal demo discussed below. + * `active.py` Demonstrates `active` controls providing floating point input. + * `plot.py` Graph plotting. + * `screens.py` Listbox, dropdown and dialog boxes. + * `tbox.py` Text boxes and user-controlled scrolling. + * `various.py` Assorted widgets including the different types of pushbutton. + * `vtest.py` Clock and compass styles of vector display. + +# 2. Usage + +## 2.1 Program structure and operation + +The following is a minimal script (found in `gui.demos.simple.py`) which will +run on a minimal system with a small display and two pushbuttons. It provides +two `Button` widgets with "Yes" and "No" legends. + +It may be run by issuing +```python +>>> import gui.demos.simple +``` +at the REPL. + +Note that the import of `hardware_setup.py` is the first line of code. This is +because the frame buffer is created here, with a need for a substantial block +of contiguous RAM. +```python +from hardware_setup import ssd # Create a display instance +from gui.core.ugui import Screen + +from gui.widgets.label import Label +from gui.widgets.buttons import Button, CloseButton +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.arial10 as arial10 +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + def my_callback(button, arg): + print('Button pressed', arg) + + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + + col = 2 + row = 2 + Label(wri, row, col, 'Simple Demo') + row = 20 + Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',)) + col += 60 + Button(wri, row, col, text='No', callback=my_callback, args=('No',)) + CloseButton(wri) # Quit the application + +def test(): + print('Testing micro-gui...') + Screen.change(BaseScreen) + +test() +``` +Note how the `Next` pushbutton moves the focus between the two buttons and the +"X" close button. The focus does not move to the "Simple Demo" widget because +it is not `active`: a `Label` cannot accept user input. Pushing the `Select` +pushbutton while the focus is on a `Pushbutton` causes the callback to run. + +Applications start by performing `Screen.change()` to a user-defined Screen +object. This must be subclassed from the GUI's `Screen` class. Note that +`Screen.change` accepts a class name, not a class instance. + +The user defined `BaseScreen` class constructor instantiates all widgets to be +displayed and typically associates them with callback functions - which may be +bound methods. Screens typically have a `CloseButton` widget. This is a special +`Pushbutton` subclass which displays as an "X" at the top right corner of the +physical display and closes the current screen, showing the one below. If used +on the bottom level `Screen` (as above) it closes the application. + +The `wri` `CWriter` instance associates a widget with a font. All widgets use +a `CWriter` instance followed by `row` and `col` as positional constructor args +followed typically by a number of optional keyword args. These have (hopefully) +sensible defaults enabling you to get started easily. + +## 2.2 Callbacks + +The interface is event driven. Widgets may have optional callbacks which will +be executed when a given event occurs. A callback function receives positional +arguments. The first is a reference to the object raising the callback. +Subsequent arguments are user defined, and are specified as a tuple or list of +items. Callbacks are optional, as are the argument lists - a default null +function and empty list are provided. Callbacks may optionally be written as +bound methods - see Screens below for a reason why this can be useful. + +When writing callbacks take care to ensure that the number of arguments passed +is correct, bearing in mind the first arg listed above. Failure to do this will +result in tracebacks which implicate the GUI code rather than the buggy user +code: this is because the GUI runs the callbacks. + +## 2.3 Colors + +The file `gui/core/colors.py` defines standard color constants which may be +used with any display driver. This section describes how to change these or +to create additional colors. + +Most of the color display drivers define colors as 8-bit or larger values. +In such cases colors may be created and assigned to variables as follows: +```python +from hardware_setup import ssd +PALE_YELLOW = ssd.rgb(150, 150, 0) +``` +The GUI also provides drivers with 4-bit color to minimise RAM use. Colors are +assigned to a lookup table having 16 entries. The frame buffer stores 4-bit +color values, which are converted to the correct color depth for the hardware +when the display is refreshed. + +Of the possible 16 colors 13 are assigned in `gui/core/colors.py`, leaving +color numbers 12, 13 and 14 free. Any color can be assigned as follows: +```python +from gui.core.colors import * # Imports the create_color function +PALE_YELLOW = create_color(12, 150, 150, 0) +``` +This creates a color `rgb(150, 150, 0)` assigns it to "spare" color number 12 +then sets `PALE_YELLOW` to 12. Any color number in range `0 <= n <= 15` may be +used (implying that predefined colors may be reassigned). It is recommended +that `BLACK` (0) and `WHITE` (15) are not changed. If code is to be ported +between 4-bit and other drivers, use `create_color()` for all custom colors: +it will produce appropriate behaviour. For an example see the `nano-gui` demo +`color15.py` - in particular the `vari_fields` function. + +### 2.3.1 Monochrome displays + +Most widgets work on monochrome displays if color settings are left at default +values. If a color is specified, drivers in this repo will convert it to black +or white depending on its level of saturation. A low level will produce the +background color, a high level the foreground. + +At the bit level `1` represents the foreground. This is white on an emitting +display such as an OLED. On a Sharp display it indicates reflection. + +There is an issue regarding ePaper displays discussed +[here](https://github.com/peterhinch/micropython-nano-gui/blob/master/README.md#312-monochrome-displays). +I don't consider ePaper displays as suitable for I/O because of their slow +refresh time. + +# 3. Class details + +# 4. Screen class + +The `Screen` class presents a full-screen canvas onto which displayable +objects are rendered. Before instantiating widgets a `Screen` instance must be +created. This will be current until another is instantiated. When a widget is +instantiated it is associated with the current screen. + +All applications require the creation of at least one user screen. This is done +by subclassing the `Screen` class. Widgets are instantiated in the constructor. +Widgets may be assigned to bound variable: this facilitates communication +between them. + +## 4.1 Class methods + +In normal use the following methods only are required: + * `change` Change screen, refreshing the display. Mandatory positional + argument: the new screen class name. This must be a class subclassed from + `Screen`. The class will be instantiated and displayed. Optional keyword + arguments: `args`, `kwargs`. These enable passing positional and keyword + arguments to the constructor of the new screen. + * `back` Restore previous screen. + +These are uncommon:__ + * `shutdown` Clear the screen and shut down the GUI. Normally done by a + `CloseButton` instance. + * `show(cls, force)`. This causes the screen to be redrawn. If `force` is + `False` unchanged widgets are not refreshed. If `True`, all visible widgets + are re-drawn. Explicit calls to this should never be needed. + +See `demos/plot.py` for an example of multi-screen design. + +## 4.2 Constructor + +This takes no arguments. + +## 4.3 Callback methods + +These are null functions which may be redefined in user subclasses. + + * `on_open` Called when a screen is instantiated but prior to display. + * `after_open` Called after a screen has been displayed. + * `on_hide` Called when a screen ceases to be current. + +See `demos/plot.py` for examples of usage of `after_open`. + +## 4.4 Method + + * `reg_task` args `task`, `on_change=False`. The first arg may be a `Task` + instance or a coroutine. It is a convenience method which provides for the + automatic cancellation of tasks. If a screen runs independent coros it can opt + to register these. On shudown, any registered tasks of the base screen are + cancelled. On screen change, registered tasks with `on_change` `True` are + cancelled. For finer control applications can ignore this method and handle + cancellation explicitly in code. + +# 5. Window class + +This is a `Screen` subclass providing for modal windows. As such it has +positional and dimension information. Usage consists of writing a user class +subclassed from `Window`. Example code is in `demos/screens.py`. + +## 5.2 Constructor + +This takes the following positional args: + * `row` + * `col` + * `height` + * `width` + +Followed by keyword-only args + * `draw_border=True` + * `bgcolor=None` Background color, default black. + * `fgcolor=None` Foreground color, default white. + +## 5.3 Class method + + * `value` This accepts a single arg which cane be any Python type. It allows + widgets on a `Window` to store information in a way which can be accessed from + the calling screen. This typically occurs after the window has closed and no + longer exists as an instance. + +Another approach, demonstrated in `demos/screens.py`, is to pass one or more +callbacks to the user window constructor args. These may be called by widgets +to send data to the calling screen. Note that widgets on the screen below will +not be updated until the window has closed. diff --git a/drivers/epaper/epaper2in7_fb.py b/drivers/epaper/epaper2in7_fb.py new file mode 100644 index 0000000..69b7484 --- /dev/null +++ b/drivers/epaper/epaper2in7_fb.py @@ -0,0 +1,303 @@ +# epaper2in7_fb.py nanogui driver for ePpaper 2.7" display +# Tested with Pyboard linked to Raspberry Pi 2.7" E-Ink Display HAT +# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui. +# Optimisations to reduce allocations and RAM use. + +# Copyright (c) Peter Hinch 2020 +# Released under the MIT license see LICENSE + +# Based on the following sources: +# https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT +# MicroPython Waveshare 2.7" Black/White GDEW027W3 e-paper display driver +# https://github.com/mcauser/micropython-waveshare-epaper referred to as "mcauser" +# https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7.py ("official") + +import framebuf +import uasyncio as asyncio +from time import sleep_ms, ticks_ms, ticks_us, ticks_diff + +class EPD(framebuf.FrameBuffer): + # A monochrome approach should be used for coding this. The rgb method ensures + # nothing breaks if users specify colors. + @staticmethod + def rgb(r, g, b): + return int((r > 127) or (g > 127) or (b > 127)) + + def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False): + self._spi = spi + self._cs = cs # Pins + self._dc = dc + self._rst = rst + self._busy = busy + self._lsc = landscape + self._asyn = asyn + self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1). + self._updated = asyncio.Event() + # Dimensions in pixels. Waveshare code is portrait mode. + # Public bound variables required by nanogui. + self.width = 264 if landscape else 176 + self.height = 176 if landscape else 264 + self.demo_mode = False # Special mode enables demos to run + self._buffer = bytearray(self.height * self.width // 8) + self._mvb = memoryview(self._buffer) + mode = framebuf.MONO_VLSB if landscape else framebuf.MONO_HLSB + super().__init__(self._buffer, self.width, self.height, mode) + self.init() + + def _command(self, command, data=None): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + if data is not None: + self._dc(1) + self._cs(0) + self._spi.write(data) + self._cs(1) + + def init(self): + # Hardware reset + self._rst(1) + sleep_ms(200) + self._rst(0) + sleep_ms(200) # 5ms in Waveshare code + self._rst(1) + sleep_ms(200) + # Initialisation + cmd = self._command + cmd(b'\x01', b'\x03\x00\x2B\x2B\x09') # POWER_SETTING: VDS_EN VDG_EN, VCOM_HV VGHL_LV[1] VGHL_LV[0], VDH, VDL, VDHR + cmd(b'\x06', b'\x07\x07\x17') # BOOSTER_SOFT_START + cmd(b'\xf8', b'\x60\xA5') # POWER_OPTIMIZATION + cmd(b'\xf8', b'\x89\xA5') + cmd(b'\xf8', b'\x90\x00') + cmd(b'\xf8', b'\x93\x2A') + cmd(b'\xf8', b'\xA0\xA5') + cmd(b'\xf8', b'\xA1\x00') + cmd(b'\xf8', b'\x73\x41') + cmd(b'\x16', b'\x00') # PARTIAL_DISPLAY_REFRESH + cmd(b'\x04') # POWER_ON + self.wait_until_ready() + cmd(b'\x00', b'\xAF') # PANEL_SETTING: KW-BF, KWR-AF, BWROTP 0f + cmd(b'\x30', b'\x3A') # PLL_CONTROL: 3A 100HZ, 29 150Hz, 39 200HZ 31 171HZ + cmd(b'\x50', b'\x57') # Vcom and data interval setting (PGH) + cmd(b'\x82', b'\x12') # VCM_DC_SETTING_REGISTER + sleep_ms(2) # No delay in official code + # Set LUT. Local bytes objects reduce RAM usage. + + # Values used by mcauser + #lut_vcom_dc =\ + #b'\x00\x00\x00\x0F\x0F\x00\x00\x05\x00\x32\x32\x00\x00\x02\x00'\ + #b'\x0F\x0F\x00\x00\x05\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' + #lut_ww =\ + #b'\x50\x0F\x0F\x00\x00\x05\x60\x32\x32\x00\x00\x02\xA0\x0F\x0F'\ + #b'\x00\x00\x05\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' # R21H + #lut_bb =\ + #b'\xA0\x0F\x0F\x00\x00\x05\x60\x32\x32\x00\x00\x02\x50\x0F\x0F'\ + #b'\x00\x00\x05\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' # R24H b + + # Values from official code: + lut_vcom_dc =\ + b'\x00\x00\x00\x08\x00\x00\x00\x02\x60\x28\x28\x00\x00\x01\x00'\ + b'\x14\x00\x00\x00\x01\x00\x12\x12\x00\x00\x01\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + lut_ww =\ + b'\x40\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x40\x14\x00'\ + b'\x00\x00\x01\xA0\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + lut_bb =\ + b'\x80\x08\x00\x00\x00\x02\x90\x28\x28\x00\x00\x01\x80\x14\x00'\ + b'\x00\x00\x01\x50\x12\x12\x00\x00\x01\x00\x00\x00\x00\x00\x00'\ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + # Both agree on this: + lut_bw = lut_ww # R22H r + lut_wb = lut_bb # R23H w + cmd(b'\x20', lut_vcom_dc) # LUT_FOR_VCOM vcom + cmd(b'\x21', lut_ww) # LUT_WHITE_TO_WHITE ww -- + cmd(b'\x22', lut_bw) # LUT_BLACK_TO_WHITE bw r + cmd(b'\x23', lut_bb) # LUT_WHITE_TO_BLACK wb w + cmd(b'\x24', lut_wb) # LUT_BLACK_TO_BLACK bb b + print('Init Done.') + + def wait_until_ready(self): + sleep_ms(50) + t = ticks_ms() + while not self.ready(): + sleep_ms(100) + dt = ticks_diff(ticks_ms(), t) + print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000)) + + async def wait(self): + await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready + while not self.ready(): + await asyncio.sleep_ms(100) + + # Pause until framebuf has been copied to device. + async def updated(self): + await self._updated.wait() + + # For polling in asynchronous code. Just checks pin state. + # 0 == busy. Comment in official code is wrong. Code is correct. + def ready(self): + return not(self._as_busy or (self._busy() == 0)) # 0 == busy + + async def _as_show(self, buf1=bytearray(1)): + mvb = self._mvb + send = self._spi.write + cmd = self._command + cmd(b'\x10') # DATA_START_TRANSMISSION_1 + self._dc(1) # For some reason don't need to deassert CS here + buf1[0] = 0xff + t = ticks_ms() + for i in range(len(mvb)): + self._cs(0) # but do when copying the framebuf + send(buf1) + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + self._cs(1) + cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet + + self._dc(1) + # Necessary to deassert CS after each byte otherwise display does not + # clear down correctly + t = ticks_ms() + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for i in range(len(mvb)): + self._cs(0) + buf1[0] = mvb[idx] # INVERSION HACK ~data + send(buf1) + self._cs(1) + idx -= self.width + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + else: + for i, b in enumerate(mvb): + self._cs(0) + buf1[0] = b # INVERSION HACK ~data + send(buf1) + self._cs(1) + if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20): + await asyncio.sleep_ms(0) + t = ticks_ms() + + self._updated.set() # framebuf has now been copied to the device + self._updated.clear() + cmd(b'\x12') # DISPLAY_REFRESH + await asyncio.sleep(1) + while self._busy() == 0: + await asyncio.sleep_ms(200) # Don't release lock until update is complete + self._as_busy = False + + # draw the current frame memory. Blocking time ~180ms + def show(self, buf1=bytearray(1)): + if self._asyn: + if self._as_busy: + raise RuntimeError('Cannot refresh: display is busy.') + self._as_busy = True + asyncio.create_task(self._as_show()) + return + t = ticks_us() + mvb = self._mvb + send = self._spi.write + cmd = self._command + cmd(b'\x10') # DATA_START_TRANSMISSION_1 + self._dc(1) # For some reason don't need to deassert CS here + buf1[0] = 0xff + for i in range(len(mvb)): + self._cs(0) # but do when copying the framebuf + send(buf1) + self._cs(1) + cmd(b'\x13') # DATA_START_TRANSMISSION_2 not in datasheet + + self._dc(1) + # Necessary to deassert CS after each byte otherwise display does not + # clear down correctly + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for _ in range(len(mvb)): + self._cs(0) + buf1[0] = mvb[idx] # INVERSION HACK ~data + send(buf1) + self._cs(1) + idx -= self.width + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + else: + for b in mvb: + self._cs(0) + buf1[0] = b # INVERSION HACK ~data + send(buf1) + self._cs(1) + + cmd(b'\x12') # DISPLAY_REFRESH + te = ticks_us() + print('show time', ticks_diff(te, t)//1000, 'ms') + if not self.demo_mode: + # Immediate return to avoid blocking the whole application. + # User should wait for ready before calling refresh() + return + self.wait_until_ready() + sleep_ms(2000) # Give time for user to see result + + + # to wake call init() + def sleep(self): + self._as_busy = False + self.wait_until_ready() + cmd = self._command + cmd(b'\x50', b'\xf7') # From Waveshare code + cmd(b'\x02') # POWER_OFF + cmd(b'\x07', b'\xA5') # DEEP_SLEEP (Waveshare and mcauser) + self._rst(0) # According to schematic this turns off the power + +# Testing connections by toggling pins connected to 40-way connector and checking volts on small connector +# All OK except rst: a 1 level produced only about 1.6V as against 3.3V for all other I/O. +# Further the level on the 40-way connector read 2.9V as agains 3.3V for others. Suspect hardware problem, +# ordered a second unit from Amazon. +#import machine +#import gc + +#pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0) +#pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1) +#prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1) +#pbusy = machine.Pin('Y4', machine.Pin.IN) +## baudrate +## From https://github.com/mcauser/micropython-waveshare-epaper/blob/master/examples/2in9-hello-world/test.py 2MHz +## From https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in7.py 4MHz +#spi = machine.SPI(2, baudrate=2_000_000) +#gc.collect() # Precaution before instantiating framebuf +#epd = EPD(spi, pcs, pdc, prst, pbusy) # Create a display instance +#sleep_ms(100) +#epd.init() +#print('Initialised') +#epd.fill(1) # 1 seems to be white +#epd.show() +#sleep_ms(1000) +#epd.fill(0) +#epd.show() +#epd._rst(0) +#epd._dc(0) # Turn off power according to RPI code diff --git a/drivers/epaper/epd29.py b/drivers/epaper/epd29.py new file mode 100644 index 0000000..8241fa8 --- /dev/null +++ b/drivers/epaper/epd29.py @@ -0,0 +1,234 @@ +# epd29.py nanogui driver for Adafruit Flexible 2.9" Black and White ePaper display. +# [Interface breakout](https://www.adafruit.com/product/4224) +# [Display](https://www.adafruit.com/product/4262) + +# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui. + +# Copyright (c) Peter Hinch 2020 +# Released under the MIT license see LICENSE + +# Based on the following sources: +# [CircuitPython code](https://github.com/adafruit/Adafruit_CircuitPython_IL0373) Author: Scott Shawcroft +# [Adafruit setup guide](https://learn.adafruit.com/adafruit-eink-display-breakouts/circuitpython-code-2) +# [IL0373 datasheet](https://www.smart-prototyping.com/image/data/9_Modules/EinkDisplay/GDEW0154T8/IL0373.pdf) +# [Adafruit demo](https://github.com/adafruit/Adafruit_CircuitPython_IL0373/blob/3f4f52eb3a65173165da1908f93a95383b45a726/examples/il0373_flexible_2.9_monochrome.py) +# [eInk breakout schematic](https://learn.adafruit.com/assets/57645) + +# Physical pixels are 296w 128h. However the driver views the display as 128w * 296h with the +# Adfruit code transposing the axes. + +import framebuf +import uasyncio as asyncio +from micropython import const +from time import sleep_ms, sleep_us, ticks_ms, ticks_us, ticks_diff + +_MAX_BLOCK = const(20) # Maximum blocking time (ms) for asynchronous show. + +class EPD(framebuf.FrameBuffer): + # A monochrome approach should be used for coding this. The rgb method ensures + # nothing breaks if users specify colors. + @staticmethod + def rgb(r, g, b): + return int((r > 127) or (g > 127) or (b > 127)) + + def __init__(self, spi, cs, dc, rst, busy, landscape=True, asyn=False): + self._spi = spi + self._cs = cs # Pins + self._dc = dc + self._rst = rst # Active low. + self._busy = busy # Active low on IL0373 + self._lsc = landscape + self._asyn = asyn + # ._as_busy is set immediately on start of task. Cleared + # when busy pin is logically false (physically 1). + self._as_busy = False + self._updated = asyncio.Event() + # Public bound variables required by nanogui. + # Dimensions in pixels as seen by nanogui (landscape mode). + self.width = 296 if landscape else 128 + self.height = 128 if landscape else 296 + # Other public bound variable. + # Special mode enables demos written for generic displays to run. + self.demo_mode = False + + self._buffer = bytearray(self.height * self.width // 8) + self._mvb = memoryview(self._buffer) + mode = framebuf.MONO_VLSB if landscape else framebuf.MONO_HLSB + super().__init__(self._buffer, self.width, self.height, mode) + self.init() + + def _command(self, command, data=None): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + if data is not None: + self._data(data) + + # Datasheet P26 seems to mandate CS False after each byte. Ugh. + def _data(self, data, buf1=bytearray(1)): + self._dc(1) + for b in data: + self._cs(0) + buf1[0] = b + self._spi.write(buf1) + self._cs(1) + + def init(self): + # Hardware reset + self._rst(1) + sleep_ms(200) + self._rst(0) + sleep_ms(200) + self._rst(1) + sleep_ms(200) + # Initialisation + cmd = self._command + # Power setting. Data from Adafruit. + # Datasheet default \x03\x00\x26\x26\x03 - slightly different voltages. + cmd(b'\x01', b'\x03\x00\x2b\x2b\x09') + # Booster soft start. Matches datasheet. + cmd(b'\x06', b'\x17\x17\x17') + cmd(b'\x04') # Power on + sleep_ms(200) + # Iss https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/16 + cmd(b'\x00', b'\x9f') + # CDI: As used by Adafruit. Datasheet is confusing on this. + # See https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/11 + # With 0x37 got white border on flexible display, black on FeatherWing + # 0xf7 still produced black border on FeatherWing + cmd(b'\x50', b'\x37') + # PLL: correct for 150Hz as specified in Adafruit code + cmd(b'\x30', b'\x29') + # Resolution 128w * 296h as required by IL0373 + cmd(b'\x61', b'\x80\x01\x28') # Note hex(296) == 0x128 + # Set VCM_DC. Now clarified with Adafruit. + # https://github.com/adafruit/Adafruit_CircuitPython_IL0373/issues/17 + cmd(b'\x82', b'\x12') # Set Vcom to -1.0V + sleep_ms(50) + print('Init Done.') + + # For use in synchronous code: blocking wait on ready state. + def wait_until_ready(self): + sleep_ms(50) + while not self.ready(): + sleep_ms(100) + + # Asynchronous wait on ready state. Pause (4.9s) for physical refresh. + async def wait(self): + await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready + while not self.ready(): + await asyncio.sleep_ms(100) + + # Pause until framebuf has been copied to device. + async def updated(self): + await self._updated.wait() + + # Return immediate status. Pin state: 0 == busy. + def ready(self): + return not(self._as_busy or (self._busy() == 0)) + + async def _as_show(self, buf1=bytearray(1)): + mvb = self._mvb + cmd = self._command + dat = self._data + cmd(b'\x13') + t = ticks_ms() + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for i in range(len(mvb)): + buf1[0] = ~mvb[idx] + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): + await asyncio.sleep_ms(0) + t = ticks_ms() + else: + for i, b in enumerate(mvb): + buf1[0] = ~b + dat(buf1) + if not(i & 0x0f) and (ticks_diff(ticks_ms(), t) > _MAX_BLOCK): + await asyncio.sleep_ms(0) + t = ticks_ms() + + cmd(b'\x11') # Data stop + self._updated.set() + self._updated.clear() + sleep_us(20) # Allow for data coming back: currently ignore this + cmd(b'\x12') # DISPLAY_REFRESH + # busy goes low now, for ~4.9 seconds. + await asyncio.sleep(1) + while self._busy() == 0: + await asyncio.sleep_ms(200) + self._as_busy = False + + # draw the current frame memory. + def show(self, buf1=bytearray(1)): + if self._asyn: + if self._as_busy: + raise RuntimeError('Cannot refresh: display is busy.') + self._as_busy = True # Immediate busy flag. Pin goes low much later. + asyncio.create_task(self._as_show()) + return + + mvb = self._mvb + cmd = self._command + dat = self._data + # DATA_START_TRANSMISSION_2 Datasheet P31 indicates this sets + # busy pin low (True) and that it stays logically True until + # refresh is complete. In my testing this doesn't happen. + cmd(b'\x13') + if self._lsc: # Landscape mode + wid = self.width + tbc = self.height // 8 # Vertical bytes per column + iidx = wid * (tbc - 1) # Initial index + idx = iidx # Index into framebuf + vbc = 0 # Current vertical byte count + hpc = 0 # Horizontal pixel count + for _ in range(len(mvb)): + buf1[0] = ~mvb[idx] + dat(buf1) + idx -= wid + vbc += 1 + vbc %= tbc + if not vbc: + hpc += 1 + idx = iidx + hpc + else: + for b in mvb: + buf1[0] = ~b + dat(buf1) + + cmd(b'\x11') # Data stop + sleep_us(20) # Allow for data coming back: currently ignore this + cmd(b'\x12') # DISPLAY_REFRESH + # 258ms to get here on Pyboard D + # Checking with scope, busy goes low now. For 4.9s. + if not self.demo_mode: + # Immediate return to avoid blocking the whole application. + # User should wait for ready before calling refresh() + return + self.wait_until_ready() + sleep_ms(2000) # Give time for user to see result + + # to wake call init() + def sleep(self): + self._as_busy = False + self.wait_until_ready() + cmd = self._command + # CDI: not sure about value or why we set this here. Copying Adafruit. + cmd(b'\x50', b'\x17') + # Set VCM_DC. 0 is datasheet default. + cmd(b'\x82', b'\x00') + # POWER_OFF. User code should pull ENA low to power down the display. + cmd(b'\x02') diff --git a/drivers/ili93xx/ili9341.py b/drivers/ili93xx/ili9341.py new file mode 100644 index 0000000..3908521 --- /dev/null +++ b/drivers/ili93xx/ili9341.py @@ -0,0 +1,166 @@ +# ILI9341 nano-gui driver for ili9341 displays +# As with all nano-gui displays, touch is not supported. + +# Copyright (c) Peter Hinch 2020-2021 +# Released under the MIT license see LICENSE + +# This work is based on the following sources. +# https://github.com/rdagger/micropython-ili9341 +# Also this forum thread with ideas from @minyiky: +# https://forum.micropython.org/viewtopic.php?f=18&t=9368 + +from time import sleep_ms +import gc +import framebuf +import uasyncio as asyncio + +@micropython.viper +def _lcopy(dest:ptr16, source:ptr8, lut:ptr16, length:int): + # rgb565 - 16bit/pixel + n = 0 + for x in range(length): + c = source[x] + dest[n] = lut[c >> 4] # current pixel + n += 1 + dest[n] = lut[c & 0x0f] # next pixel + n += 1 + + +class ILI9341(framebuf.FrameBuffer): + + lut = bytearray(32) + + # Convert r, g, b in range 0-255 to a 16 bit colour value + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + # ILI9341 expects RGB order + @staticmethod + def rgb(r, g, b): + return (r & 0xf8) | (g & 0xe0) >> 5 | (g & 0x1c) << 11 | (b & 0xf8) << 5 + + # Transpose width & height for landscape mode + def __init__(self, spi, cs, dc, rst, height=240, width=320, + usd=False, init_spi=False): + self._spi = spi + self._cs = cs + self._dc = dc + self._rst = rst + self.height = height + self.width = width + self._spi_init = init_spi + mode = framebuf.GS4_HMSB + gc.collect() + buf = bytearray(self.height * self.width // 2) + self._mvb = memoryview(buf) + super().__init__(buf, self.width, self.height, mode) + self._linebuf = bytearray(self.width * 2) + # Hardware reset + self._rst(0) + sleep_ms(50) + self._rst(1) + sleep_ms(50) + if self._spi_init: # A callback was passed + self._spi_init(spi) # Bus may be shared + self._lock = asyncio.Lock() + # Send initialization commands + self._wcmd(b'\x01') # SWRESET Software reset + sleep_ms(100) + self._wcd(b'\xcf', b'\x00\xC1\x30') # PWCTRB Pwr ctrl B + self._wcd(b'\xed', b'\x64\x03\x12\x81') # POSC Pwr on seq. ctrl + self._wcd(b'\xe8', b'\x85\x00\x78') # DTCA Driver timing ctrl A + self._wcd(b'\xcb', b'\x39\x2C\x00\x34\x02') # PWCTRA Pwr ctrl A + self._wcd(b'\xf7', b'\x20') # PUMPRC Pump ratio control + self._wcd(b'\xea', b'\x00\x00') # DTCB Driver timing ctrl B + self._wcd(b'\xc0', b'\x23') # PWCTR1 Pwr ctrl 1 + self._wcd(b'\xc1', b'\x10') # PWCTR2 Pwr ctrl 2 + self._wcd(b'\xc5', b'\x3E\x28') # VMCTR1 VCOM ctrl 1 + self._wcd(b'\xc7', b'\x86') # VMCTR2 VCOM ctrl 2 + # (b'\x88', b'\xe8', b'\x48', b'\x28')[rotation // 90] + if self.height > self.width: + self._wcd(b'\x36', b'\x48' if usd else b'\x88') # MADCTL: RGB portrait mode + else: + self._wcd(b'\x36', b'\x28' if usd else b'\xe8') # MADCTL: RGB landscape mode + self._wcd(b'\x37', b'\x00') # VSCRSADD Vertical scrolling start address + self._wcd(b'\x3a', b'\x55') # PIXFMT COLMOD: Pixel format 16 bits (MCU & interface) + self._wcd(b'\xb1', b'\x00\x18') # FRMCTR1 Frame rate ctrl + self._wcd(b'\xb6', b'\x08\x82\x27') # DFUNCTR + self._wcd(b'\xf2', b'\x00') # ENABLE3G Enable 3 gamma ctrl + self._wcd(b'\x26', b'\x01') # GAMMASET Gamma curve selected + self._wcd(b'\xe0', b'\x0F\x31\x2B\x0C\x0E\x08\x4E\xF1\x37\x07\x10\x03\x0E\x09\x00') # GMCTRP1 + self._wcd(b'\xe1', b'\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F') # GMCTRN1 + self._wcmd(b'\x11') # SLPOUT Exit sleep + sleep_ms(100) + self._wcmd(b'\x29') # DISPLAY_ON + sleep_ms(100) + + # Write a command. + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, command, data): + self._dc(0) + self._cs(0) + self._spi.write(command) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(data) + self._cs(1) + +# Time (ESP32 stock freq) 196ms portrait, 185ms landscape. +# mem free on ESP32 43472 bytes (vs 110192) + @micropython.native + def show(self): + clut = ILI9341.lut + wd = self.width // 2 + ht = self.height + lb = self._linebuf + buf = self._mvb + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + # Commands needed to start data write + self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN + self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE + self._wcmd(b'\x2c') # WRITE_RAM + self._dc(1) + self._cs(0) + for start in range(0, wd*ht, wd): # For each line + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + self._spi.write(lb) + self._cs(1) + + def busy(self): + return self._lock.locked() + + async def do_refresh(self, split=4): + if self.busy(): + print('Warning: refresh paused until prior refresh completed.') + async with self._lock: + lines, mod = divmod(self.height, split) # Lines per segment + if mod: + raise ValueError('Invalid do_refresh arg.') + clut = ILI9341.lut + wd = self.width // 2 + ht = self.height + lb = self._linebuf + buf = self._mvb + # Commands needed to start data write + self._wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # SET_COLUMN + self._wcd(b'\x2b', int.to_bytes(ht, 4, 'big')) # SET_PAGE + self._wcmd(b'\x2c') # WRITE_RAM + self._dc(1) + line = 0 + for _ in range(split): # For each segment + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._cs(0) + for start in range(wd * line, wd * (line + lines), wd): # For each line + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + self._spi.write(lb) + line += lines + self._cs(1) # Allow other tasks to use bus + await asyncio.sleep_ms(0) diff --git a/drivers/sharp/__init__.py b/drivers/sharp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drivers/sharp/sharp.py b/drivers/sharp/sharp.py new file mode 100644 index 0000000..33ada6b --- /dev/null +++ b/drivers/sharp/sharp.py @@ -0,0 +1,70 @@ +# sharp.py Device driver for monochrome sharp displays + +# Tested on +# https://www.adafruit.com/product/4694 2.7 inch 400x240 Monochrome +# Should also work on +# https://www.adafruit.com/product/3502 1.3 inch 144x168 +# https://www.adafruit.com/product/1393 1.3 inch 96x96 Monochrome + +# Copyright (c) Peter Hinch 2020-2021 +# Released under the MIT license see LICENSE + +# Code checked against https://github.com/adafruit/Adafruit_CircuitPython_SharpMemoryDisplay +# Current draw on 2.7" Adafruit display ~90uA. +# 2.7" schematic https://learn.adafruit.com/assets/94077 +# Datasheet 2.7" https://cdn-learn.adafruit.com/assets/assets/000/094/215/original/LS027B7DH01_Rev_Jun_2010.pdf?1597872422 +# Datasheet 1.3" http://www.adafruit.com/datasheets/LS013B4DN04-3V_FPC-204284.pdf +import framebuf +import machine +from micropython import const + +_WRITECMD = const(1) # Command bits +_VCOM = const(2) + + +class SHARP(framebuf.FrameBuffer): + @staticmethod + def rgb(r, g, b): + return int((r > 127) or (g > 127) or (b > 127)) + + def __init__(self, spi, pincs, height=240, width=400, vcom=False): + spi.init(baudrate=2_000_000, firstbit=machine.SPI.LSB) # Data sheet: should support 2MHz + self._spi = spi + self._pincs = pincs + self.height = height # Required by Writer class and nanogui + self.width = width + self._buffer = bytearray(self.height * self.width // 8) + self._mvb = memoryview(self._buffer) + super().__init__(self._buffer, self.width, self.height, framebuf.MONO_HMSB) + self._cmd = bytearray(1) # Buffer for command. Holds current VCOM bit + self._cmd[0] = _WRITECMD | _VCOM if vcom else _WRITECMD + self._lno = bytearray(1) # Line no. + self._dummy = bytearray(1) # Dummy (0) + + # .show should be called periodically to avoid frame inversion flag + # (VCOM) retaining the same value for long periods + def show(self): + spi = self._spi + bpl = self.width // 8 # Bytes per line + self._pincs(1) # CS is active high + spi.write(self._cmd) + start = 0 + lno = self._lno + lno[0] = 1 # Gate line address (starts at 1) + for _ in range(self.height): + spi.write(lno) + spi.write(self._mvb[start : start + bpl]) + spi.write(self._dummy) + start += bpl + lno[0] += 1 # Gate line address + spi.write(self._dummy) + self._pincs(0) + self._cmd[0] ^= _VCOM # Toggle frame inversion flag + + # Toggle the VCOM bit without changing the display. Power saving method. + def update(self): + self._pincs(1) + self._lno[0] = self._cmd[0] & _VCOM + self._spi.write(self._lno) + self._cmd[0] ^= _VCOM # Toggle frame inversion flag + self._pincs(0) diff --git a/drivers/ssd1306/ssd1306.py b/drivers/ssd1306/ssd1306.py new file mode 100644 index 0000000..1364bd3 --- /dev/null +++ b/drivers/ssd1306/ssd1306.py @@ -0,0 +1,159 @@ +# MicroPython SSD1306 OLED driver, I2C and SPI interfaces + +from micropython import const +import framebuf + + +# register definitions +SET_CONTRAST = const(0x81) +SET_ENTIRE_ON = const(0xA4) +SET_NORM_INV = const(0xA6) +SET_DISP = const(0xAE) +SET_MEM_ADDR = const(0x20) +SET_COL_ADDR = const(0x21) +SET_PAGE_ADDR = const(0x22) +SET_DISP_START_LINE = const(0x40) +SET_SEG_REMAP = const(0xA0) +SET_MUX_RATIO = const(0xA8) +SET_COM_OUT_DIR = const(0xC0) +SET_DISP_OFFSET = const(0xD3) +SET_COM_PIN_CFG = const(0xDA) +SET_DISP_CLK_DIV = const(0xD5) +SET_PRECHARGE = const(0xD9) +SET_VCOM_DESEL = const(0xDB) +SET_CHARGE_PUMP = const(0x8D) + +# Subclassing FrameBuffer provides support for graphics primitives +# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html +class SSD1306(framebuf.FrameBuffer): + @staticmethod + def rgb(r, g, b): + return int((r > 127) or (g > 127) or (b > 127)) + + def __init__(self, width, height, external_vcc): + self.width = width + self.height = height + self.external_vcc = external_vcc + self.pages = self.height // 8 + self.buffer = bytearray(self.pages * self.width) + super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) + self.init_display() + + def init_display(self): + for cmd in ( + SET_DISP | 0x00, # off + # address setting + SET_MEM_ADDR, + 0x00, # horizontal + # resolution and layout + SET_DISP_START_LINE | 0x00, + SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 + SET_MUX_RATIO, + self.height - 1, + SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 + SET_DISP_OFFSET, + 0x00, + SET_COM_PIN_CFG, + 0x02 if self.width > 2 * self.height else 0x12, + # timing and driving scheme + SET_DISP_CLK_DIV, + 0x80, + SET_PRECHARGE, + 0x22 if self.external_vcc else 0xF1, + SET_VCOM_DESEL, + 0x30, # 0.83*Vcc + # display + SET_CONTRAST, + 0xFF, # maximum + SET_ENTIRE_ON, # output follows RAM contents + SET_NORM_INV, # not inverted + # charge pump + SET_CHARGE_PUMP, + 0x10 if self.external_vcc else 0x14, + SET_DISP | 0x01, + ): # on + self.write_cmd(cmd) + self.fill(0) + self.show() + + def poweroff(self): + self.write_cmd(SET_DISP | 0x00) + + def poweron(self): + self.write_cmd(SET_DISP | 0x01) + + def contrast(self, contrast): + self.write_cmd(SET_CONTRAST) + self.write_cmd(contrast) + + def invert(self, invert): + self.write_cmd(SET_NORM_INV | (invert & 1)) + + def show(self): + x0 = 0 + x1 = self.width - 1 + if self.width == 64: + # displays with width of 64 pixels are shifted by 32 + x0 += 32 + x1 += 32 + self.write_cmd(SET_COL_ADDR) + self.write_cmd(x0) + self.write_cmd(x1) + self.write_cmd(SET_PAGE_ADDR) + self.write_cmd(0) + self.write_cmd(self.pages - 1) + self.write_data(self.buffer) + + +class SSD1306_I2C(SSD1306): + def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False): + self.i2c = i2c + self.addr = addr + self.temp = bytearray(2) + self.write_list = [b"\x40", None] # Co=0, D/C#=1 + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.temp[0] = 0x80 # Co=1, D/C#=0 + self.temp[1] = cmd + self.i2c.writeto(self.addr, self.temp) + + def write_data(self, buf): + self.write_list[1] = buf + self.i2c.writevto(self.addr, self.write_list) + + +class SSD1306_SPI(SSD1306): + def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): + self.rate = 10 * 1024 * 1024 + dc.init(dc.OUT, value=0) + res.init(res.OUT, value=0) + cs.init(cs.OUT, value=1) + self.spi = spi + self.dc = dc + self.res = res + self.cs = cs + import time + + self.res(1) + time.sleep_ms(1) + self.res(0) + time.sleep_ms(10) + self.res(1) + super().__init__(width, height, external_vcc) + + def write_cmd(self, cmd): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(0) + self.cs(0) + self.spi.write(bytearray([cmd])) + self.cs(1) + + def write_data(self, buf): + self.spi.init(baudrate=self.rate, polarity=0, phase=0) + self.cs(1) + self.dc(1) + self.cs(0) + self.spi.write(buf) + self.cs(1) diff --git a/drivers/ssd1331/__init__.py b/drivers/ssd1331/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drivers/ssd1331/ssd1331.py b/drivers/ssd1331/ssd1331.py new file mode 100644 index 0000000..088a661 --- /dev/null +++ b/drivers/ssd1331/ssd1331.py @@ -0,0 +1,81 @@ +# SSD1331.py MicroPython driver for Adafruit 0.96" OLED display +# https://www.adafruit.com/product/684 + +# The MIT License (MIT) + +# Copyright (c) Peter Hinch 2018-2020 +# Released under the MIT license see LICENSE + +# 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 +import sys +# https://github.com/peterhinch/micropython-nano-gui/issues/2 +# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. +# Mode 0, 0 works on ESP and STM + +# Data sheet SPI spec: 150ns min clock period 6.66MHz + +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, init_spi=False): + self._spi = spi + self._pincs = pincs + self._pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + 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, 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._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 + if self._spi_init: # A callback was passed + self._spi_init(spi) # Bus may be shared + self._write(_cmd, 0) + self._write(self.buffer, 1) diff --git a/drivers/ssd1331/ssd1331_16bit.py b/drivers/ssd1331/ssd1331_16bit.py new file mode 100644 index 0000000..02109a8 --- /dev/null +++ b/drivers/ssd1331/ssd1331_16bit.py @@ -0,0 +1,81 @@ +# SSD1331.py MicroPython driver for Adafruit 0.96" OLED display +# https://www.adafruit.com/product/684 + +# Copyright (c) Peter Hinch 2019-2020 +# Released under the MIT license see LICENSE + +# Show command +# 0x15, 0, 0x5f, 0x75, 0, 0x3f Col 0-95 row 0-63 + +# Initialisation command +# 0xae display off (sleep mode) +# 0xa0, 0x72 16 bit 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 + +# https://github.com/peterhinch/micropython-nano-gui/issues/2 +# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. +# Mode 0, 0 works on ESP and STM + +# Data sheet SPI spec: 150ns min clock period 6.66MHz +class SSD1331(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to a 16 bit colour value RGB565 + # acceptable to hardware: rrrrrggggggbbbbb + # LS byte of 16 bit result is shifted out 1st + @staticmethod + def rgb(r, g, b): + return ((b & 0xf8) << 5) | ((g & 0x1c) << 11) | (r & 0xf8) | ((g & 0xe0) >> 5) + + def __init__(self, spi, pincs, pindc, pinrs, height=64, width=96, init_spi=False): + self._spi = spi + self._pincs = pincs + self._pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + mode = framebuf.RGB565 + gc.collect() + self.buffer = bytearray(self.height * self.width * 2) + super().__init__(self.buffer, self.width, self.height, mode) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + if self._spi_init: # A callback was passed + self._spi_init(spi) # Bus may be shared + self._write(b'\xae\xa0\x72\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._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 + if self._spi_init: # A callback was passed + self._spi_init(spi) # Bus may be shared + self._write(_cmd, 0) + self._write(self.buffer, 1) diff --git a/drivers/ssd1331/test_colors.py b/drivers/ssd1331/test_colors.py new file mode 100644 index 0000000..f816be0 --- /dev/null +++ b/drivers/ssd1331/test_colors.py @@ -0,0 +1,31 @@ +# test_colors.py Test color rendition on SSD1331 (Adafruit 0.96" OLED). +# Author Peter Hinch +# Released under the MIT licence. + +import machine +# Can test either driver +# from ssd1331 import SSD1331 as SSD +from ssd1331_16bit import SSD1331 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) +# Operate in landscape mode +x = 0 +for y in range(96): + ssd.line(y, x, y, x+20, ssd.rgb(round(255*y/96), 0, 0)) +x += 20 +for y in range(96): + ssd.line(y, x, y, x+20, ssd.rgb(0, round(255*y/96), 0)) +x += 20 +for y in range(96): + ssd.line(y, x, y, x+20, ssd.rgb(0, 0, round(255*y/96))) +ssd.show() diff --git a/drivers/ssd1351/__init__.py b/drivers/ssd1351/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drivers/ssd1351/ssd1351.py b/drivers/ssd1351/ssd1351.py new file mode 100644 index 0000000..baae425 --- /dev/null +++ b/drivers/ssd1351/ssd1351.py @@ -0,0 +1,143 @@ +# 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 + +# Copyright (c) Peter Hinch 2018-2020 +# Released under the MIT license see LICENSE + +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, 3) + 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, init_spi=False): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.spi_init = init_spi + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + 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, mode) + self.linebuf = bytearray(self.width * 2) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + if self.spi_init: # A callback was passed + self.spi_init(spi) # Bus may be shared + # 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.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 + if self.spi_init: # A callback was passed + self.spi_init(self.spi) # Bus may be shared + 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_16bit.py b/drivers/ssd1351/ssd1351_16bit.py new file mode 100644 index 0000000..68609eb --- /dev/null +++ b/drivers/ssd1351/ssd1351_16bit.py @@ -0,0 +1,108 @@ +# SSD1351_16bit.py MicroPython driver for Adafruit color 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. + +# This driver is based on the Adafruit C++ library for Arduino +# https://github.com/adafruit/Adafruit-SSD1351-library.git + +# Copyright (c) Peter Hinch 2019-2020 +# Released under the MIT license see LICENSE + +import framebuf +import utime +import gc +import micropython +from uctypes import addressof + +# https://github.com/peterhinch/micropython-nano-gui/issues/2 +# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. +# Now using 0,0 on STM and ESP32 + +# 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 a 16 bit colour value RGB565 + # acceptable to hardware: rrrrrggggggbbbbb + @staticmethod + def rgb(r, g, b): + return ((r & 0xf8) << 5) | ((g & 0x1c) << 11) | (b & 0xf8) | ((g & 0xe0) >> 5) + + def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.spi_init = init_spi + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + mode = framebuf.RGB565 + gc.collect() + self.buffer = bytearray(self.height * self.width * 2) + super().__init__(self.buffer, self.width, self.height, mode) + self.mvb = memoryview(self.buffer) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + if self.spi_init: # A callback was passed + self.spi_init(spi) # Bus may be shared + # 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, mv, dc): + self.pincs(1) + self.pindc(dc) + self.pincs(0) + self.spi.write(bytes(mv)) + 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): + mvb = self.mvb + bw = self.width * 2 # Width in bytes + if self.spi_init: # A callback was passed + self.spi_init(self.spi) # Bus may be shared + 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 * 2 + self._write(mvb[start : start + bw], 1) # Send a line + else: + for l in range(128): + if l < 64: + start = (63 -l) * self.width * 2 # 63 62 .. 1 0 + elif l < 96: + start = 0 + else: + start = (191 - l) * self.width * 2 # 127 126 .. 95 + self._write(mvb[start : start + bw], 1) # Send a line diff --git a/drivers/ssd1351/ssd1351_4bit.py b/drivers/ssd1351/ssd1351_4bit.py new file mode 100644 index 0000000..ba67b18 --- /dev/null +++ b/drivers/ssd1351/ssd1351_4bit.py @@ -0,0 +1,142 @@ +# SSD1351_4bit.py MicroPython driver for Adafruit color OLED displays. +# This is cross-platform and uses 4 bit color for minimum RAM usage. + +# 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 + +# Copyright (c) Peter Hinch 2020 +# Released under the MIT license see LICENSE + +import framebuf +import utime +import gc +import micropython +from uctypes import addressof + +# https://github.com/peterhinch/micropython-nano-gui/issues/2 +# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. +# Now using 0,0 on STM and ESP32 + +# ESP32 produces 20MHz, Pyboard D SF2W: 15MHz, SF6W: 18MHz, Pyboard 1.1: 10.5MHz +# OLED datasheet: should support 20MHz +def spi_init(spi): + spi.init(baudrate=20_000_000) # Data sheet: should support 20MHz + +@micropython.viper +def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): + n = 0 + for x in range(length): + c = source[x] + d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color) + e = (c & 0x0f) << 1 + dest[n] = lut[d] + n += 1 + dest[n] = lut[d + 1] + n += 1 + dest[n] = lut[e] + n += 1 + dest[n] = lut[e + 1] + 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): + + lut = bytearray(32) + + # Convert r, g, b in range 0-255 to a 16 bit colour value + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + # Note pretty colors in datasheet don't match actual colors. See doc. + @staticmethod + def rgb(r, g, b): + return (r & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (b & 0xf8) + + def __init__(self, spi, pincs, pindc, pinrs, height=128, width=128, init_spi=False): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + self.spi_init = init_spi + mode = framebuf.GS4_HMSB # Use 4bit greyscale. + gc.collect() + self.buffer = bytearray(self.height * self.width // 2) + super().__init__(self.buffer, self.width, self.height, mode) + self.linebuf = bytearray(self.width * 2) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + if self.spi_init: # A callback was passed + self.spi_init(spi) # Bus may be shared + # 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.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): # 44ms on Pyboard 1.x + clut = SSD1351.lut + lb = self.linebuf + wd = self.width // 2 + buf = memoryview(self.buffer) + if self.spi_init: # A callback was passed + self.spi_init(self.spi) # Bus may be shared + 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 * wd + _lcopy(lb, buf[start : start + wd], clut, wd) + self._write(lb, 1) # Send a line + else: + for l in range(128): + if l < 64: + start = (63 -l) * wd + _lcopy(lb, buf[start : start + wd], clut, wd) + elif l < 96: # This is daft but I can't get setrow to work + pass # Let RAM counter increase + else: + start = (191 - l) * wd + _lcopy(lb, buf[start : start + wd], clut, wd) + 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..0570fa5 --- /dev/null +++ b/drivers/ssd1351/ssd1351_generic.py @@ -0,0 +1,131 @@ +# 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 + +# Copyright (c) Peter Hinch 2018-2020 +# Released under the MIT license see LICENSE + +import framebuf +import utime +import gc +import micropython +from uctypes import addressof + +import sys +# https://github.com/peterhinch/micropython-nano-gui/issues/2 +# The ESP32 does not work reliably in SPI mode 1,1. Waveforms look correct. +# Now using 0,0 on STM and ESP32 + +# 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, init_spi=False): + if height not in (96, 128): + raise ValueError('Unsupported height {}'.format(height)) + self.spi = spi + self.spi_init = init_spi + self.pincs = pincs + self.pindc = pindc # 1 = data 0 = cmd + self.height = height # Required by Writer class + self.width = width + 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, mode) + self.linebuf = bytearray(self.width * 2) + pinrs(0) # Pulse the reset line + utime.sleep_ms(1) + pinrs(1) + utime.sleep_ms(1) + if self.spi_init: # A callback was passed + self.spi_init(spi) # Bus may be shared + # 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.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) + if self.spi_init: # A callback was passed + self.spi_init(self.spi) # Bus may be shared + 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/drivers/ssd1351/test_colors_96.py b/drivers/ssd1351/test_colors_96.py new file mode 100644 index 0000000..483edac --- /dev/null +++ b/drivers/ssd1351/test_colors_96.py @@ -0,0 +1,27 @@ +# test_colors_96.py +# Check color mapping. Runs on 96 row display (change height for 128 row) + +import machine +from ssd1351_16bit 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) +x = 0 +for y in range(128): + ssd.line(y, x, y, x+20, ssd.rgb(round(255*y/128), 0, 0)) +x += 20 +for y in range(128): + ssd.line(y, x, y, x+20, ssd.rgb(0, round(255*y/128), 0)) +x += 20 +for y in range(128): + ssd.line(y, x, y, x+20, ssd.rgb(0, 0, round(255*y/128))) +ssd.show() diff --git a/drivers/st7735r/st7735_notes b/drivers/st7735r/st7735_notes new file mode 100644 index 0000000..4daee06 --- /dev/null +++ b/drivers/st7735r/st7735_notes @@ -0,0 +1,53 @@ +https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage + +# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R +# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R + + +# disp = st7735.ST7735R(spi, rotation=90, bgr=True, # 0.96" MiniTFT ST7735R +# disp = ssd1351.SSD1351(spi, rotation=180, + + + +import machine +import gc +from time import sleep_ms +from drivers.st7735r.st7735r144 import ST7735R as SSD +height = 128 +width = 128 # 160 + +pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0) +pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1) +prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1) +spi = machine.SPI(2, baudrate=12_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, height, width) # Create a display instance +ssd.fill(0) +ssd.show() +sleep_ms(1000) +ssd.line(0, 0, width - 1, height - 1, 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() +sleep_ms(2000) +ssd.fill(0) +ssd.show() +ssd.line(0, 0, width - 1, height - 1, ssd.rgb(0, 255, 255)) +ssd.rect(0, 0, 40, 40, ssd.rgb(0, 0, 255)) +ssd.rect(width - 41, height - 41, 40, 40, ssd.rgb(0, 0, 255)) +ssd.show() + +Attempt to rotate 1.44" unit fell foul of x and y offsets with 1-2 pixel errors. +Rotation passed to ctor + def __init__(self, spi, cs, dc, rst, height=128, width=128, init_spi=False, rotation=0): + ... + quad, mod = divmod(rotation, 90) # Get quadrant + if mod: + raise ValueError('Rotation should be divisible by 90') + ... + + self._init(quad) + + def _init(self, quad): + ... + rval = (b'\x20', b'\x40', b'\xe0', b'\x80')[quad] + wcd(b'\x36', rval) diff --git a/drivers/st7735r/st7735r.py b/drivers/st7735r/st7735r.py new file mode 100644 index 0000000..4394e6a --- /dev/null +++ b/drivers/st7735r/st7735r.py @@ -0,0 +1,154 @@ +# st7735r.py Driver for 1.8" 128*160 ST7735R LCD displays for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +# Supported display +# Adfruit 1.8' Color TFT LCD display with MicroSD Card Breakout: +# https://www.adafruit.com/product/358 + +# Based on +# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py +# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py +# https://github.com/boochow/MicroPython-ST7735 + +# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage +# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R +# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R + +from time import sleep_ms +import framebuf +import gc +import micropython + +# Datasheet para 8.4 scl write cycle 66ns == 15MHz + +# _lcopy: copy a line in 8 bit format to one in 12 bit RGB444. para 9.8.20. +# 2 bytes become 3 in destination. Source format: +# < D7 D6 D5 D4 D3 D2 D1 D0> +# +# dest: +# + +@micropython.viper +def _lcopy(dest:ptr8, source:ptr8, length:int): + n = 0 + for x in range(0, length, 2): + c = source[x] + d = source[x + 1] + dest[n] = (c & 0xe0) | ((c & 0x1c) >> 1) # R0 G0 + n += 1 + dest[n] = ((c & 3) << 6) | ((d & 0xe0) >> 4) # B0 R1 + n += 1 + dest[n] = ((d & 0x1c) << 3) | ((d & 3) << 2) # G1 B1 + n += 1 + +class ST7735R(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # rrrgggbb. Converted to 12 bit on the fly. + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) + + # rst and cs are active low, SPI is mode 0 + def __init__(self, spi, cs, dc, rst, height=128, width=160, usd=False, init_spi=False): + self._spi = spi + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + gc.collect() + buf = bytearray(height * width) + self._mvb = memoryview(buf) + super().__init__(buf, width, height, mode) + self._linebuf = bytearray(int(width * 3 // 2)) # 12 bit color out + self._init(usd) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 500ms. + def _init(self, usd): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(b'\x01') # SW reset datasheet specifies > 120ms + sleep_ms(150) + cmd(b'\x11') # SLPOUT + sleep_ms(256) # Adafruit delay (datsheet 120ms) + wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1 + wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2 + wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3 + wcd(b'\xb4', b'\x07') # INVCTR line inversion + + wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA + wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V + wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency + wcd(b'\xc3', b'\x8a\x2a') # PWCTR4 + wcd(b'\xc4', b'\x8a\xee') # PWCTR5 + wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V + + cmd(b'\x20') # INVOFF + # d7..d5 of MADCTL determine rotation/orientation + if self.height > self.width: + wcd(b'\x36', b'\x80' if usd else b'\x40') # MADCTL: RGB portrait mode + else: + wcd(b'\x36', b'\xe0' if usd else b'\x20') # MADCTL: RGB landscape mode + wcd(b'\x3a', b'\x03') # COLMOD 12 bit + wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma + wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1 + + wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # CASET column address 0 start, 160 end + wcd(b'\x2b', int.to_bytes(self.height, 4, 'big')) # RASET + + cmd(b'\x13') # NORON + sleep_ms(10) + cmd(b'\x29') # DISPON + sleep_ms(100) + + def show(self): # Blocks 36ms on Pyboard D at stock frequency (160*128) + wd = self.width + ht = self.height + lb = self._linebuf + buf = self._mvb + self._dc(0) + self._cs(0) + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._spi.write(b'\x2c') # RAMWR + self._dc(1) + for start in range(wd * (ht - 1), -1, - wd): # For each line + _lcopy(lb, buf[start :], wd) # Copy and map colors (68us) + self._spi.write(lb) + self._cs(1) diff --git a/drivers/st7735r/st7735r144.py b/drivers/st7735r/st7735r144.py new file mode 100644 index 0000000..cc98797 --- /dev/null +++ b/drivers/st7735r/st7735r144.py @@ -0,0 +1,158 @@ +# st7735r144.py Driver for ST7735R 1.44" LCD display for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +# Supported display +# Adafruit 1.44' Color TFT LCD Display with MicroSD Card breakout: +# https://www.adafruit.com/product/2088 + +# Based on +# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py +# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py +# https://github.com/boochow/MicroPython-ST7735 + +# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage +# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R +# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R + +from time import sleep_ms +import framebuf +import gc +import micropython + +# Datasheet para 8.4 scl write cycle 66ns == 15MHz + +# _lcopy: copy a line in 8 bit format to one in 16 bit RGB565. +# 1 bytes becomes 2 in destination. Source format: +# < D7 D6 D5 D4 D3 D2 D1 D0> +# +# dest: +# <0 0 0 R02 R01 R00 0 0> + +@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 + + +class ST7735R(framebuf.FrameBuffer): + # Convert r, g, b in range 0-255 to an 8 bit colour value + # rrrgggbb. Converted to 16 bit on the fly. + @staticmethod + def rgb(r, g, b): + return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6) + + # rst and cs are active low, SPI is mode 0 + def __init__(self, spi, cs, dc, rst, height=128, width=128, rotation=0, init_spi=False): + self._spi = spi + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color. + gc.collect() + buf = bytearray(self.height * self.width) + self._mvb = memoryview(buf) + super().__init__(buf, self.width, self.height, mode) + self._linebuf = bytearray(self.width * 2) # 16 bit color out + quad, mod = divmod(rotation, 90) # Get quadrant + if mod or quad > 3: + quad %= 4 + print('Warning: rotation adjusted to', quad * 90) + self._init(quad) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 500ms. + def _init(self, quad): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(b'\x01') # SW reset datasheet specifies > 120ms + sleep_ms(150) + cmd(b'\x11') # SLPOUT + sleep_ms(256) # Adafruit delay (datsheet 120ms) + wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1 + wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2 + wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3 + wcd(b'\xb4', b'\x07') # INVCTR line inversion + + wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA + wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V + wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency + wcd(b'\xc3', b'\x8a\x2a') # PWCTR4 + wcd(b'\xc4', b'\x8a\xee') # PWCTR5 + wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V + + cmd(b'\x20') # INVOFF + # d7..d5 of MADCTL determine rotation/orientation + # (MADCTL_DATA, ColumnOffset, RowOffset) + rval, co, ro = ((b'\x20', 1, 2), + (b'\x40', 2, 1), + (b'\xe0', 3, 2), + (b'\x80', 2, 3))[quad] + wcd(b'\x36', rval) # MADCTL: rotation mode for 1.44" display + wcd(b'\x3a', b'\x05') # COLMOD 16 bit + wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma + wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1 + + wcd(b'\x2a', int.to_bytes((co << 16) + self.width + co - 1, 4, 'big')) # CASET + wcd(b'\x2b', int.to_bytes((ro << 16) + self.height + ro - 1, 4, 'big')) # RASET + + cmd(b'\x13') # NORON + sleep_ms(10) + cmd(b'\x29') # DISPON + sleep_ms(100) + + def show(self): # Blocks 38.6ms on Pyboard D at stock frequency + wd = self.width + ht = self.height + lb = self._linebuf + buf = self._mvb + self._dc(0) + self._cs(0) + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._spi.write(b'\x2c') # RAMWR + self._dc(1) + for start in range(wd * (ht - 1), -1, - wd): # For each line + _lcopy(lb, buf[start :], wd) # Copy and map colors (68us) + self._spi.write(lb) + self._cs(1) diff --git a/drivers/st7735r/st7735r144_4bit.py b/drivers/st7735r/st7735r144_4bit.py new file mode 100644 index 0000000..fa359f7 --- /dev/null +++ b/drivers/st7735r/st7735r144_4bit.py @@ -0,0 +1,162 @@ +# st7735r144.py Driver for ST7735R 1.44" LCD display for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +# Supported display +# Adafruit 1.44' Color TFT LCD Display with MicroSD Card breakout: +# https://www.adafruit.com/product/2088 + +# Based on +# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py +# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py +# https://github.com/boochow/MicroPython-ST7735 + +# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage +# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R +# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R + +from time import sleep_ms +import framebuf +import gc +import micropython + +# Datasheet para 8.4 scl write cycle 66ns == 15MHz + +@micropython.viper +def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): + n = 0 + for x in range(length): + c = source[x] + d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color) + e = (c & 0x0f) << 1 + dest[n] = lut[d] + n += 1 + dest[n] = lut[d + 1] + n += 1 + dest[n] = lut[e] + n += 1 + dest[n] = lut[e + 1] + n += 1 + + +class ST7735R(framebuf.FrameBuffer): + + lut = bytearray(32) + + # Convert r, g, b in range 0-255 to a 16 bit colour value + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + @staticmethod + def rgb(r, g, b): + return (r & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (b & 0xf8) + + # rst and cs are active low, SPI is mode 0 + def __init__(self, spi, cs, dc, rst, height=128, width=128, rotation=0, init_spi=False): + self._spi = spi + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + mode = framebuf.GS4_HMSB # Use 4bit greyscale. + gc.collect() + buf = bytearray(self.height * self.width // 2) + self._mvb = memoryview(buf) + super().__init__(buf, self.width, self.height, mode) + self._linebuf = bytearray(self.width * 2) # 16 bit color out + quad, mod = divmod(rotation, 90) # Get quadrant + if mod or quad > 3: + quad %= 4 + print('Warning: rotation adjusted to', quad * 90) + self._init(quad) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 500ms. + def _init(self, quad): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(b'\x01') # SW reset datasheet specifies > 120ms + sleep_ms(150) + cmd(b'\x11') # SLPOUT + sleep_ms(256) # Adafruit delay (datsheet 120ms) + wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1 + wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2 + wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3 + wcd(b'\xb4', b'\x07') # INVCTR line inversion + + wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA + wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V + wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency + wcd(b'\xc3', b'\x8a\x2a') # PWCTR4 + wcd(b'\xc4', b'\x8a\xee') # PWCTR5 + wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V + + cmd(b'\x20') # INVOFF + # d7..d5 of MADCTL determine rotation/orientation + # (MADCTL_DATA, ColumnOffset, RowOffset) + rval, co, ro = ((b'\x20', 1, 2), + (b'\x40', 2, 1), + (b'\xe0', 3, 2), + (b'\x80', 2, 3))[quad] + wcd(b'\x36', rval) # MADCTL: rotation mode for 1.44" display + wcd(b'\x3a', b'\x05') # COLMOD 16 bit + wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma + wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1 + + wcd(b'\x2a', int.to_bytes((co << 16) + self.width + co - 1, 4, 'big')) # CASET + wcd(b'\x2b', int.to_bytes((ro << 16) + self.height + ro - 1, 4, 'big')) # RASET + + cmd(b'\x13') # NORON + sleep_ms(10) + cmd(b'\x29') # DISPON + sleep_ms(100) + + def show(self): # Blocks 38.6ms on Pyboard D at stock frequency + clut = ST7735R.lut + wd = self.width // 2 + ht = self.height + lb = self._linebuf + buf = self._mvb + self._dc(0) + self._cs(0) + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._spi.write(b'\x2c') # RAMWR + self._dc(1) + for start in range(wd * (ht - 1), -1, - wd): # For each line + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors (68us) + self._spi.write(lb) + self._cs(1) diff --git a/drivers/st7735r/st7735r_4bit.py b/drivers/st7735r/st7735r_4bit.py new file mode 100644 index 0000000..1a0f355 --- /dev/null +++ b/drivers/st7735r/st7735r_4bit.py @@ -0,0 +1,156 @@ +# st7735r.py Driver for 1.8" 128*160 ST7735R LCD displays for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2018-2020 Peter Hinch + +# Supported display +# Adfruit 1.8' Color TFT LCD display with MicroSD Card Breakout: +# https://www.adafruit.com/product/358 + +# Based on +# https://github.com/adafruit/Adafruit_CircuitPython_ST7735R/blob/master/adafruit_st7735r.py +# https://github.com/GuyCarver/MicroPython/blob/master/lib/ST7735.py +# https://github.com/boochow/MicroPython-ST7735 + +# https://learn.adafruit.com/adafruit-1-44-color-tft-with-micro-sd-socket/python-usage +# disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R +# disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R + +from time import sleep_ms +import framebuf +import gc +import micropython + +# Datasheet para 8.4 scl write cycle 66ns == 15MHz + + +@micropython.viper +def _lcopy(dest:ptr8, source:ptr8, lut:ptr8, length:int): + n = 0 + for x in range(length): + c = source[x] + d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color) + e = (c & 0x0f) << 1 + dest[n] = lut[d] + n += 1 + dest[n] = lut[d + 1] + n += 1 + dest[n] = lut[e] + n += 1 + dest[n] = lut[e + 1] + n += 1 + +class ST7735R(framebuf.FrameBuffer): + + lut = bytearray(32) + + # Convert r, g, b in range 0-255 to a 16 bit colour value + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + @staticmethod + def rgb(r, g, b): + return (b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8) + + # rst and cs are active low, SPI is mode 0 + def __init__(self, spi, cs, dc, rst, height=128, width=160, usd=False, init_spi=False): + self._spi = spi + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._spi_init = init_spi + mode = framebuf.GS4_HMSB # Use 4bit greyscale. + gc.collect() + buf = bytearray(height * width // 2) + self._mvb = memoryview(buf) + super().__init__(buf, width, height, mode) + self._linebuf = bytearray(self.width * 2) # 16 bit color out + self._init(usd) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 500ms. + def _init(self, usd): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(b'\x01') # SW reset datasheet specifies > 120ms + sleep_ms(150) + cmd(b'\x11') # SLPOUT + sleep_ms(256) # Adafruit delay (datsheet 120ms) + wcd(b'\xb1', b'\x01\x2C\x2D') # FRMCTRL1 + wcd(b'\xb2', b'\x01\x2C\x2D') # FRMCTRL2 + wcd(b'\xb3', b'\x01\x2C\x2D\x01\x2C\x2D') # FRMCTRL3 + wcd(b'\xb4', b'\x07') # INVCTR line inversion + + wcd(b'\xc0', b'\xa2\x02\x84') # PWCTR1 GVDD = 4.7V, 1.0uA + wcd(b'\xc1', b'\xc5') # PWCTR2 VGH=14.7V, VGL=-7.35V + wcd(b'\xc2', b'\x0a\x00') # PWCTR3 Opamp current small, Boost frequency + wcd(b'\xc3', b'\x8a\x2a') # PWCTR4 + wcd(b'\xc4', b'\x8a\xee') # PWCTR5 + wcd(b'\xc5', b'\x0e') # VMCTR1 VCOMH = 4V, VOML = -1.1V NOTE I make VCOM == -0.775V + + cmd(b'\x20') # INVOFF + # d7..d5 of MADCTL determine rotation/orientation + if self.height > self.width: + wcd(b'\x36', b'\x80' if usd else b'\x40') # MADCTL: RGB portrait mode + else: + wcd(b'\x36', b'\xe0' if usd else b'\x20') # MADCTL: RGB landscape mode + wcd(b'\x3a', b'\x05') # COLMOD 16 bit + wcd(b'\xe0', b'\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2B\x39\x00\x01\x03\x10') # GMCTRP1 Gamma + wcd(b'\xe1', b'\x03\x1d\x07\x06\x2E\x2C\x29\x2D\x2E\x2E\x37\x3F\x00\x00\x02\x10') # GMCTRN1 + + wcd(b'\x2a', int.to_bytes(self.width, 4, 'big')) # CASET column address 0 start, 160 end + wcd(b'\x2b', int.to_bytes(self.height, 4, 'big')) # RASET + + cmd(b'\x13') # NORON + sleep_ms(10) + cmd(b'\x29') # DISPON + sleep_ms(100) + + def show(self): # Blocks 36ms on Pyboard D at stock frequency (160*128) + clut = ST7735R.lut + wd = self.width // 2 + ht = self.height + lb = self._linebuf + buf = self._mvb + self._dc(0) + self._cs(0) + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._spi.write(b'\x2c') # RAMWR + self._dc(1) + for start in range(wd * (ht - 1), -1, - wd): # For each line + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + self._spi.write(lb) + self._cs(1) diff --git a/drivers/st7789/st7789_4bit.py b/drivers/st7789/st7789_4bit.py new file mode 100644 index 0000000..1aaacf1 --- /dev/null +++ b/drivers/st7789/st7789_4bit.py @@ -0,0 +1,237 @@ +# st7789.py Driver for ST7789 LCD displays for nano-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa + +# Tested displays: +# Adafruit 1.3" 240x240 Wide Angle TFT LCD Display with MicroSD - ST7789 +# https://www.adafruit.com/product/4313 +# TTGO T-Display +# http://www.lilygo.cn/prod_view.aspx?TypeId=50044&Id=1126 + +# Based on +# Adfruit https://github.com/adafruit/Adafruit_CircuitPython_ST7789/blob/master/adafruit_st7789.py +# Also see st7735r_4bit.py for other source acknowledgements + +# SPI bus: default mode. Driver performs no read cycles. +# Datasheet table 6 p44 scl write cycle 16ns == 62.5MHz + +from time import sleep_ms #, ticks_us, ticks_diff +import framebuf +import gc +import micropython +import uasyncio as asyncio + +# User orientation constants +LANDSCAPE = 0 # Default +REFLECT = 1 +USD = 2 +PORTRAIT = 4 +# Display types +GENERIC = (0, 0, 0) +TDISPLAY = (52, 40, 1) + + +@micropython.viper +def _lcopy(dest:ptr16, source:ptr8, lut:ptr16, length:int): + # rgb565 - 16bit/pixel + n = 0 + for x in range(length): + c = source[x] + dest[n] = lut[c >> 4] # current pixel + n += 1 + dest[n] = lut[c & 0x0f] # next pixel + n += 1 + +class ST7789(framebuf.FrameBuffer): + + lut = bytearray(0xFF for _ in range(32)) # set all colors to BLACK + + # Convert r, g, b in range 0-255 to a 16 bit colour value rgb565. + # LS byte goes into LUT offset 0, MS byte into offset 1 + # Same mapping in linebuf so LS byte is shifted out 1st + # For some reason color must be inverted on this controller. + @staticmethod + def rgb(r, g, b): + return ((b & 0xf8) << 5 | (g & 0x1c) << 11 | (g & 0xe0) >> 5 | (r & 0xf8)) ^ 0xffff + + # rst and cs are active low, SPI is mode 0 + def __init__(self, spi, cs, dc, rst, height=240, width=240, + disp_mode=LANDSCAPE, init_spi=False, display=GENERIC): + if not 0 <= disp_mode <= 7: + raise ValueError('Invalid display mode:', disp_mode) + if not display in (GENERIC, TDISPLAY): + raise ValueError('Invalid display type.') + self._spi = spi # Clock cycle time for write 16ns 62.5MHz max (read is 150ns) + self._rst = rst # Pins + self._dc = dc + self._cs = cs + self.height = height # Required by Writer class + self.width = width + self._offset = display[:2] # display arg is (x, y, orientation) + orientation = display[2] # where x, y is the RAM offset + self._spi_init = init_spi # Possible user callback + self._lock = asyncio.Lock() + mode = framebuf.GS4_HMSB # Use 4bit greyscale. + gc.collect() + buf = bytearray(height * -(-width // 2)) # Ceiling division for odd widths + self._mvb = memoryview(buf) + super().__init__(buf, width, height, mode) + self._linebuf = bytearray(self.width * 2) # 16 bit color out + self._init(disp_mode, orientation) + self.show() + + # Hardware reset + def _hwreset(self): + self._dc(0) + self._rst(1) + sleep_ms(1) + self._rst(0) + sleep_ms(1) + self._rst(1) + sleep_ms(1) + + # Write a command, a bytes instance (in practice 1 byte). + def _wcmd(self, buf): + self._dc(0) + self._cs(0) + self._spi.write(buf) + self._cs(1) + + # Write a command followed by a data arg. + def _wcd(self, c, d): + self._dc(0) + self._cs(0) + self._spi.write(c) + self._cs(1) + self._dc(1) + self._cs(0) + self._spi.write(d) + self._cs(1) + + # Initialise the hardware. Blocks 163ms. Adafruit have various sleep delays + # where I can find no requirement in the datasheet. I removed them with + # other redundant code. + def _init(self, user_mode, orientation): + self._hwreset() # Hardware reset. Blocks 3ms + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + cmd = self._wcmd + wcd = self._wcd + cmd(b'\x01') # SW reset datasheet specifies 120ms before SLPOUT + sleep_ms(150) + cmd(b'\x11') # SLPOUT: exit sleep mode + sleep_ms(10) # Adafruit delay 500ms (datsheet 5ms) + wcd(b'\x3a', b'\x55') # _COLMOD 16 bit/pixel, 65Kbit color space + cmd(b'\x20') # INVOFF Adafruit turn inversion on. This driver fixes .rgb + cmd(b'\x13') # NORON Normal display mode + + # Table maps user request onto hardware values. index values: + # 0 Normal + # 1 Reflect + # 2 USD + # 3 USD reflect + # Followed by same for LANDSCAPE + if not orientation: + user_mode ^= PORTRAIT + # Hardware mappings + # d7..d5 of MADCTL determine rotation/orientation datasheet P124, P231 + # d5 = MV row/col exchange + # d6 = MX col addr order + # d7 = MY page addr order + # LANDSCAPE = 0 + # PORTRAIT = 0x20 + # REFLECT = 0x40 + # USD = 0x80 + mode = (0x60, 0xe0, 0xa0, 0x20, 0, 0x40, 0xc0, 0x80)[user_mode] + # Set display window depending on mode, .height and .width. + self.set_window(mode) + wcd(b'\x36', int.to_bytes(mode, 1, 'little')) + cmd(b'\x29') # DISPON. Adafruit then delay 500ms. + + # Define the mapping between RAM and the display. + # Datasheet section 8.12 p124. + def set_window(self, mode): + portrait, reflect, usd = 0x20, 0x40, 0x80 + rht = 320 + rwd = 240 # RAM ht and width + wht = self.height # Window (framebuf) dimensions. + wwd = self.width # In portrait mode wht > wwd + if mode & portrait: + xoff = self._offset[1] # x and y transposed + yoff = self._offset[0] + xs = xoff + xe = wwd + xoff - 1 + ys = yoff # y start + ye = wht + yoff - 1 # y end + if mode & reflect: + ys = rwd - wht - yoff + ye = rwd - yoff - 1 + if mode & usd: + xs = rht - wwd - xoff + xe = rht - xoff - 1 + else: # LANDSCAPE + xoff = self._offset[0] + yoff = self._offset[1] + xs = xoff + xe = wwd + xoff - 1 + ys = yoff # y start + ye = wht + yoff - 1 # y end + if mode & usd: + ys = rht - wht - yoff + ye = rht - yoff - 1 + if mode & reflect: + xs = rwd - wwd - xoff + xe = rwd - xoff - 1 + + # Col address set. + self._wcd(b'\x2a', int.to_bytes((xs << 16) + xe, 4, 'big')) + # Row address set + self._wcd(b'\x2b', int.to_bytes((ys << 16) + ye, 4, 'big')) + + #@micropython.native # Made virtually no difference to timing. + def show(self): # Blocks for 83ms @60MHz SPI + # Blocks for 60ms @30MHz SPI on TTGO in PORTRAIT mode + # Blocks for 46ms @30MHz SPI on TTGO in LANDSCAPE mode + #ts = ticks_us() + clut = ST7789.lut + wd = -(-self.width // 2) # Ceiling division for odd number widths + end = self.height * wd + lb = memoryview(self._linebuf) + buf = self._mvb + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._dc(0) + self._cs(0) + self._spi.write(b'\x2c') # RAMWR + self._dc(1) + for start in range(0, end, wd): + _lcopy(lb, buf[start:], clut, wd) # Copy and map colors + self._spi.write(lb) + self._cs(1) + #print(ticks_diff(ticks_us(), ts)) + + # Asynchronous refresh with support for reducing blocking time. + async def do_refresh(self, split=5): + async with self._lock: + lines, mod = divmod(self.height, split) # Lines per segment + if mod: + raise ValueError('Invalid do_refresh arg.') + clut = ST7789.lut + wd = -(-self.width // 2) + lb = memoryview(self._linebuf) + buf = self._mvb + line = 0 + for n in range(split): + if self._spi_init: # A callback was passed + self._spi_init(self._spi) # Bus may be shared + self._dc(0) + self._cs(0) + self._spi.write(b'\x3c' if n else b'\x2c') # RAMWR/Write memory continue + self._dc(1) + for start in range(wd * line, wd * (line + lines), wd): + _lcopy(lb, buf[start :], clut, wd) # Copy and map colors + self._spi.write(lb) + line += lines + self._cs(1) + await asyncio.sleep(0) diff --git a/gui/core/colors.py b/gui/core/colors.py new file mode 100644 index 0000000..7066378 --- /dev/null +++ b/gui/core/colors.py @@ -0,0 +1,56 @@ +# colors.py Micropython GUI library for TFT displays: colors and shapes + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch +from hardware_setup import SSD + +# Code can be portable between 4-bit and other drivers by calling create_color +def create_color(idx, r, g, b): + c = SSD.rgb(r, g, b) + if not hasattr(SSD, 'lut'): + return c + if not 0 <= idx <= 15: + raise ValueError('Color nos must be 0..15') + x = idx << 1 + SSD.lut[x] = c & 0xff + SSD.lut[x + 1] = c >> 8 + return idx + +if hasattr(SSD, 'lut'): # Colors defined by LUT + BLACK = create_color(0, 0, 0, 0) + GREEN = create_color(1, 0, 255, 0) + RED = create_color(2, 255, 0, 0) + LIGHTRED = create_color(3, 140, 0, 0) + BLUE = create_color(4, 0, 0, 255) + YELLOW = create_color(5, 255, 255, 0) + GREY = create_color(6, 100, 100, 100) + MAGENTA = create_color(7, 255, 0, 255) + CYAN = create_color(8, 0, 255, 255) + LIGHTGREEN = create_color(9, 0, 100, 0) + DARKGREEN = create_color(10, 0, 80, 0) + DARKBLUE = create_color(11, 0, 0, 90) + # 12, 13, 14 free for user definition + WHITE = create_color(15, 255, 255, 255) +else: + BLACK = SSD.rgb(0, 0, 0) + GREEN = SSD.rgb(0, 255, 0) + RED = SSD.rgb(255, 0, 0) + LIGHTRED = SSD.rgb(140, 0, 0) + BLUE = SSD.rgb(0, 0, 255) + YELLOW = SSD.rgb(255, 255, 0) + GREY = SSD.rgb(100, 100, 100) + MAGENTA = SSD.rgb(255, 0, 255) + CYAN = SSD.rgb(0, 255, 255) + LIGHTGREEN = SSD.rgb(0, 100, 0) + DARKGREEN = SSD.rgb(0, 80, 0) + DARKBLUE = SSD.rgb(0, 0, 90) + WHITE = SSD.rgb(255, 255, 255) + +# Color used when clearing the screen +BGCOLOR = BLACK + +# RA8875 defines colors as 3-tuples with greying-out operating on those. +# Should we assign GREY to any color > 0? +CIRCLE = 1 +RECTANGLE = 2 +CLIPPED_RECT = 3 diff --git a/gui/core/ugui.py b/gui/core/ugui.py new file mode 100644 index 0000000..d101cb3 --- /dev/null +++ b/gui/core/ugui.py @@ -0,0 +1,687 @@ +# ugui.py Micropython GUI library + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019-2021 Peter Hinch + +# Requires uasyncio V3 + +import uasyncio as asyncio +from uasyncio import Event +from time import ticks_diff, ticks_ms +import gc + +from gui.core.colors import * +from hardware_setup import ssd + +from gui.primitives.delay_ms import Delay_ms +from gui.primitives.switch import Switch + +gc.collect() +__version__ = (0, 1, 0) + +# Null function +dolittle = lambda *_ : None + +async def _g(): + pass +type_coro = type(_g()) + +def setup(d): + global display + display = d + +_FIRST = const(0) +_NEXT = const(1) +_PREV = const(2) +_LAST = const(3) + +# Wrapper for ssd providing buttons and framebuf compatible methods +class Display: + + def __init__(self, ssd, nxt, sel, prev=None, up=None, down=None): + self._next = Switch(nxt) + self._sel = Switch(sel) + # Mandatory buttons + self._next.close_func(Screen.next_ctrl) # Call current screen bound method + self._sel.close_func(Screen.sel_ctrl) + self._sel.open_func(Screen.unsel) + + self.height = ssd.height + self.width = ssd.width + + # Optional buttons + self._prev = None + if prev is not None: + self._prev = Switch(prev) + self._prev.close_func(Screen.prev_ctrl) + # Up and down methods get the button as an arg. + self._up = None + if up is not None: + self._up = Switch(up) + self._up.close_func(self.do_up) + self._down = None + if down is not None: + self._down = Switch(down) + self._down.close_func(self.do_down) + self._is_grey = False # Not greyed-out + + def print_centred(self, writer, x, y, text, fgcolor=None, bgcolor=None): + sl = writer.stringlen(text) + writer.set_textpos(ssd, y - writer.height // 2, x - sl // 2) + writer.setcolor(fgcolor, bgcolor) + writer.printstring(text) + writer.setcolor() # Restore defaults + + def print_left(self, writer, x, y, txt, fgcolor=None, bgcolor=None, invert=False): + writer.set_textpos(ssd, y, x) + writer.setcolor(fgcolor, bgcolor) + writer.printstring(txt, invert) + writer.setcolor() # Restore defaults + + def do_up(self): + if self._down is not None and self._down(): # Interlock. TODO More? + Screen.up_ctrl(self._up) + + def do_down(self): + if self._up is not None and self._up(): + Screen.down_ctrl(self._down) + + # Greying out has only one option given limitation of 4-bit display driver + # It would be possible to do better with RGB565 but would need inverse transformation + # to (r, g, b), scale and re-convert to integer. + def _getcolor(self, color): # Takes in an integer color, bit size dependent on driver + return GREY if self._is_grey and color != BGCOLOR else color + + def usegrey(self, val): # display.usegrey(True) sets greyed-out + self._is_grey = val + return self + + # Graphics primitives: despatch to device (i.e. framebuf) or + # local function for methods not implemented by framebuf. + # These methods support greying out color overrides. + # Clear screen. + def clr_scr(self): + ssd.fill_rect(0, 0, self.width - 1, self.height - 1, BGCOLOR) + + def rect(self, x1, y1, w, h, color): + ssd.rect(x1, y1, w, h, self._getcolor(color)) + + def fill_rect(self, x1, y1, w, h, color): + ssd.fill_rect(x1, y1, w, h, self._getcolor(color)) + + def vline(self, x, y, l, color): + ssd.vline(x, y, l, self._getcolor(color)) + + def hline(self, x, y, l, color): + ssd.hline(x, y, l, self._getcolor(color)) + + def line(self, x1, y1, x2, y2, color): + ssd.line(x1, y1, x2, y2, self._getcolor(color)) + + # Private method uses physical color + def _circle(self, x0, y0, r, color): # Single pixel circle + x = -r + y = 0 + err = 2 -2*r + while x <= 0: + ssd.pixel(x0 -x, y0 +y, color) + ssd.pixel(x0 +x, y0 +y, color) + ssd.pixel(x0 +x, y0 -y, color) + ssd.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(self, x0, y0, r, color, width =1): # Draw circle (maybe grey) + color = self._getcolor(color) + x0, y0, r = int(x0), int(y0), int(r) + for r in range(r, r -width, -1): + self._circle(x0, y0, r, color) + + def fillcircle(self, x0, y0, r, color): # Draw filled circle + color = self._getcolor(color) + x0, y0, r = int(x0), int(y0), int(r) + x = -r + y = 0 + err = 2 -2*r + while x <= 0: + ssd.line(x0 -x, y0 -y, x0 -x, y0 +y, color) + ssd.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 + + def clip_rect(self, x, y, w, h, color): + color = self._getcolor(color) + c = 4 + ssd.hline(x + c, y, w - 2 * c, color) + ssd.hline(x + c, y + h, w - 2 * c, color) + ssd.vline(x, y + c, h - 2 * c, color) + ssd.vline(x + w - 1, y + c, h - 2 * c, color) + ssd.line(x + c, y, x, y + c, color) + ssd.line(x + w - c - 1, y, x + w - 1, y + c, color) + ssd.line(x, y + h - c - 1, x + c, y + h - 1, color) + ssd.line(x + w - 1, y + h - c - 1, x + w - c - 1, y + h, color) + + def fill_clip_rect(self, x, y, w, h, color): + color = self._getcolor(color) + c = 4 + ssd.fill_rect(x, y + c, w, h - 2 * c, color) + for z in range(c): + l = w - 2 * (c - z) # Line length + ssd.hline(x + c - z, y + z, l, color) + ssd.hline(x + c - z, y + h - z - 1, l, color) + + +class Screen: + current_screen = None + is_shutdown = Event() + + @classmethod + def next_ctrl(cls): + if cls.current_screen is not None: + cls.current_screen.move(_NEXT) + + @classmethod + def prev_ctrl(cls): + if cls.current_screen is not None: + cls.current_screen.move(_PREV) + + @classmethod + def sel_ctrl(cls): + if cls.current_screen is not None: + cls.current_screen.do_sel() + + + @classmethod + def unsel(cls): + if cls.current_screen is not None: + cls.current_screen.unsel_i() + + @classmethod + def up_ctrl(cls, button): + if cls.current_screen is not None: + cls.current_screen.do_up(button) + + @classmethod + def down_ctrl(cls, button): + if cls.current_screen is not None: + cls.current_screen.do_down(button) + + # Move currency to a specific widget (e.g. ButtonList) + @classmethod + def select(cls, obj): + if cls.current_screen is not None: + return cls.current_screen.move_to(obj) + + @classmethod + def show(cls, force): + for obj in cls.current_screen.displaylist: + if obj.visible: # In a buttonlist only show visible button + # pend signals an obect with changed contents + if force or obj.draw: + obj.draw_border() + obj.show() + obj.draw = False + + @classmethod + def change(cls, cls_new_screen, *, forward=True, args=[], kwargs={}): + cs_old = cls.current_screen + if cs_old is not None: # Leaving an existing screen + for entry in cls.current_screen.tasklist: + if entry[1]: # To be cancelled on screen change + entry[0].cancel() + cs_old.on_hide() # Optional method in subclass + if forward: + if isinstance(cls_new_screen, type): + # Instantiate new screen. __init__ must terminate + new_screen = cls_new_screen(*args, **kwargs) + else: + raise ValueError('Must pass Screen class or subclass (not instance)') + new_screen.parent = cs_old + cs_new = new_screen + else: + cs_new = cls_new_screen # An object, not a class + cls.current_screen = cs_new + cs_new.on_open() # Optional subclass method + cs_new._do_open(cs_old) # Clear and redraw + cs_new.after_open() # Optional subclass method + if cs_old is None: # Initialising + try: + asyncio.run(Screen.monitor()) # Starts and ends uasyncio + finally: + asyncio.new_event_loop() + + @classmethod + async def monitor(cls): + ar = asyncio.create_task(cls.auto_refresh()) + await cls.is_shutdown.wait() + cls.is_shutdown.clear() + # Task cancellation and shutdown + ar.cancel() # Refresh task + for entry in cls.current_screen.tasklist: + entry[0].cancel() + await asyncio.sleep_ms(0) # Allow subclass to cancel tasks + display.clr_scr() + ssd.show() + cls.current_screen = None # Ensure another demo can run + + @staticmethod + async def auto_refresh(): + arfsh = hasattr(ssd, 'do_refresh') # Refresh can be asynchronous + while True: + Screen.show(False) # Update stale controls. No physical refresh. + # Now perform physical refresh. + if arfsh: + await ssd.do_refresh() + else: + ssd.show() # Synchronous (blocking) refresh. + await asyncio.sleep_ms(0) + + @classmethod + def back(cls): + parent = cls.current_screen.parent + if parent is None: # Closing base screen. Quit. + cls.shutdown() + else: + cls.change(parent, forward = False) + + @classmethod + def addobject(cls, obj): + cs = cls.current_screen + if cs is None: + raise OSError('You must create a Screen instance') + # Populate list of active widgets (i.e. ones that can acquire focus). + if obj.active: + # Append to active list regrdless of disabled state which may + # change at runtime. + al = cs.lstactive + empty = al == [] or all(o.greyed_out() for o in al) + al.append(obj) + if empty and not obj.greyed_out(): + cs.selected_obj = len(al) - 1 # Index into lstactive + cs.displaylist.append(obj) # All displayable objects + + @classmethod + def shutdown(cls): + cls.is_shutdown.set() # Tell monitor() to shutdown + + def __init__(self): + self.lstactive = [] # Controls which respond to Select button + self.selected_obj = None # Index of currently selected object + self.displaylist = [] # All displayable objects + self.tasklist = [] # Allow instance to register tasks for shutdown + self.modal = False + self.height = ssd.height # Occupies entire display + self.width = ssd.width + self.row = 0 + self.col = 0 + if Screen.current_screen is None: # Initialising class and task + # Here we create singleton tasks + asyncio.create_task(self._garbage_collect()) + Screen.current_screen = self + self.parent = None + + def _do_open(self, old_screen): # Window overrides + dev = display.usegrey(False) +# If opening a Screen from an Window just blank and redraw covered area + if old_screen is not None and old_screen.modal: + x0, y0, x1, y1, w, h = old_screen._list_dims() + dev.fill_rect(x0, y0, w, h, BGCOLOR) # Blank to screen BG + for obj in [z for z in self.displaylist if z.overlaps(x0, y0, x1, y1)]: + if obj.visible: + obj.draw_border() + obj.show() +# Normally clear the screen and redraw everything + else: + dev.clr_scr() # Clear framebuf but don't update display + Screen.show(True) # Force full redraw + + # Return an active control or None + # By default returns the selected control + # else checks a given control by index into lstactive + def get_obj(self, idx=None): + so = self.selected_obj if idx is None else idx + if so is not None: + co = self.lstactive[so] + if co.visible and not co.greyed_out(): + return co + return None + + # Move currency to next enabled control. Arg is direction of move. + def move(self, to): + if to == _FIRST: + idx = -1 + up = 1 + elif to == _LAST: + idx = len(self.lstactive) + up = -1 + else: + idx = self.selected_obj + up = 1 if to == _NEXT else -1 + + lo = self.get_obj() # Old current object + done = False + while not done: + idx += up + idx %= len(self.lstactive) + co = self.get_obj(idx) + if co is not None: + if co is not lo: + self.selected_obj = idx + if lo is not None: + lo.leave() # Tell object it's losing currency. + lo.show() # Re-display with new status + co.enter() # Tell object it has currency + co.show() + elif isinstance(self, Window): + # Special case of Window with one object: leave + # without making changes (Dropdown in particular) + Screen.back() + done = True + + # Move currency to a specific control. + def move_to(self, obj): + lo = self.get_obj() # Old current object + for idx in range(len(self.lstactive)) : + co = self.get_obj(idx) + if co is obj: + self.selected_obj = idx + if lo is not None: + lo.leave() # Tell object it's losing currency. + lo.show() # Re-display with new status + co.enter() # Tell object it has currency + co.show() + return True # Success + return False + + def do_sel(self): # Direct to current control + co = self.get_obj() + if co is not None: + co.do_sel() + + def unsel_i(self): + co = self.get_obj() + if co is not None: + co.unsel() + + def do_up(self, button): + co = self.get_obj() + if co is not None and hasattr(co, 'do_up'): + co.do_up(button) # Widget handles up/down + else: + Screen.current_screen.move(_FIRST) + + def do_down(self, button): + co = self.get_obj() + if co is not None and hasattr(co, 'do_down'): + co.do_down(button) + else: + Screen.current_screen.move(_LAST) + + # Methods optionally implemented in subclass + def on_open(self): + return + + def after_open(self): + return + + def on_hide(self): + return + + def locn(self, row, col): + return self.row + row, self.col + col + + # Housekeeping methods + def reg_task(self, task, on_change=False): # May be passed a coro or a Task + if isinstance(task, type_coro): + task = asyncio.create_task(task) + self.tasklist.append([task, on_change]) + + async def _garbage_collect(self): + while True: + await asyncio.sleep_ms(500) + gc.collect() + gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) + #print(gc.mem_free()) + +# Very basic window class. Cuts a rectangular hole in a screen on which content may be drawn +class Window(Screen): + _value = None + def __init__(self, row, col, height, width, *, draw_border=True, bgcolor=None, fgcolor=None): + Screen.__init__(self) + self.row = row + self.col = col + self.height = height + self.width = width + self.draw_border = draw_border + self.modal = True + self.fgcolor = fgcolor if fgcolor is not None else WHITE + self.bgcolor = bgcolor if bgcolor is not None else BGCOLOR + + def _do_open(self, old_screen): + dev = display.usegrey(False) + x, y = self.col, self.row + dev.fill_rect(x, y, self.width, self.height, self.bgcolor) + if self.draw_border: + dev.rect(x, y, self.width, self.height, self.fgcolor) + Screen.show(True) + + def _list_dims(self): + w = self.width + h = self.height + x = self.col + y = self.row + return x, y, x + w, y + h, w, h + + @classmethod + def value(cls, val=None): # Mechanism for testing the outcome of a dialog box + if val is not None: + cls._value = val + return cls._value + + +# Base class for all displayable objects +class Widget: + + def __init__(self, writer, row, col, height, width, + fgcolor, bgcolor, bdcolor, + value=None, active=False): + self.active = active + self._greyed_out = False + Screen.addobject(self) + self.screen = Screen.current_screen + writer.set_clip(True, True, False) # Disable scrolling text + self.writer = writer + # The following assumes that the widget is mal-positioned, not oversize. + if row < 0: + row = 0 + self.warning() + elif row + height >= ssd.height: + row = ssd.height - height - 1 + self.warning() + if col < 0: + col = 0 + self.warning() + elif col + width >= ssd.width: + col = ssd.width - width - 1 + self.warning() + self.row = row + self.col = col + self.height = height + self.width = width + self.visible = True # Used by ButtonList class for invisible buttons + self.draw = True # Signals that obect must be redrawn + self._value = value + + # Current colors + if fgcolor is None: + fgcolor = writer.fgcolor + if bgcolor is None: + bgcolor = 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 + self.callback = dolittle # Value change callback + self.args = [] + + def warning(self): + print('Warning: attempt to create {} outside screen dimensions.'.format(self.__class__.__name__)) + + def greyed_out(self): + return self._greyed_out # Subclass may be greyed out + + def value(self, val=None): # User method to get or set value + if val is not None: + if type(val) is float: + val = min(max(val, 0.0), 1.0) + if val != self._value: + self._value = val + self.draw = True # Ensure a redraw on next refresh + self.callback(self, *self.args) + return self._value + + # Some widgets (e.g. Dial) have an associated Label + 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('Method {}.text does not exist.'.format(self.__class__.__name__)) + + # Called from subclass prior to populating framebuf with control + def show(self, black=True): + if self.screen != Screen.current_screen: + # Can occur if a control's action is to change screen. + return False # Subclass abandons + + self.draw = False + self.draw_border() + # Blank controls' space + if self.visible: + dev = display.usegrey(self._greyed_out) + x = self.col + y = self.row + dev.fill_rect(x, y, self.width, self.height, BGCOLOR if black else self.bgcolor) + return True + +# Called by Screen.show(). Draw background and bounding box if required. +# Border is always 2 pixels wide, outside control's bounding box + def draw_border(self): + if self.screen is Screen.current_screen: + dev = display.usegrey(self._greyed_out) + x = self.col - 2 + y = self.row - 2 + w = self.width + 4 + h = self.height + 4 + if self.has_focus(): + dev.rect(x, y, w, h, WHITE) + self.has_border = True + else: + if isinstance(self.bdcolor, bool): # No border + if self.has_border: # Border exists: erase it + dev.rect(x, y, w, h, BGCOLOR) + self.has_border = False + elif self.bdcolor: # Border is required + dev.rect(x, y, w, h, self.bdcolor) + self.has_border = True + + def overlaps(self, xa, ya, xb, yb): # Args must be sorted: xb > xa and yb > ya + x0 = self.col + y0 = self.row + x1 = x0 + self.width + y1 = y0 + self.height + if (ya <= y1 and yb >= y0) and (xa <= x1 and xb >= x0): + return True + return False + + def _set_callbacks(self, cb, args): # Runs when value changes. + self.callback = cb + self.args = args + + def has_focus(self): + if self.active: + cs = Screen.current_screen + if (cso := cs.selected_obj) is not None: + return cs.lstactive[cso] is self + return False + + def greyed_out(self, val=None): + if val is not None and self.active and self._greyed_out != val: + self._greyed_out = val + if self.screen is Screen.current_screen: + display.usegrey(val) + self.draw_border() + self.show() + return self._greyed_out + + # Button press methods. Called from Screen if object not greyed out. + # For subclassing if specific behaviour is required. + def do_sel(self): # Select button was pushed + pass + + def unsel(self): # Select button was released + pass + + def enter(self): # Control has acquired focus + pass + + def leave(self): # Control has lost focus + pass + + # Optional methods. Implement for controls which respond to up and down. + # No dummy methods as these would prevent "first" and "last" focus movement + # when current control has focus but is inactive. + # def do_up(self, button) + # def do_down(self, button) + +# A LinearIO widget uses the up and down buttons to vary a float. Such widgets +# have do_up and do_down methods which adjust the control's value in a +# time-dependent manner. +class LinearIO(Widget): + def __init__(self, writer, row, col, height, width, + fgcolor, bgcolor, bdcolor, + value=None, active=True, + min_delta=0.01, max_delta=0.1): + self.min_delta = min_delta + self.max_delta = max_delta + super().__init__(writer, row, col, height, width, + fgcolor, bgcolor, bdcolor, + value, active) + + def do_up(self, button): + asyncio.create_task(self.btnhan(button, 1)) + + def do_down(self, button): + asyncio.create_task(self.btnhan(button, -1)) + + async def btnhan(self, button, up): # Note: textbox.py, scale_log.py overrides + d = self.min_delta + self.value(self.value() + up * d) + t = ticks_ms() + while not button(): + await asyncio.sleep_ms(0) # Quit fast on button release + if ticks_diff(ticks_ms(), t) > 500: # Button was held down + d = min(self.max_delta, d * 2) + self.value(self.value() + up * d) + t = ticks_ms() diff --git a/gui/core/writer.py b/gui/core/writer.py new file mode 100644 index 0000000..3430924 --- /dev/null +++ b/gui/core/writer.py @@ -0,0 +1,336 @@ +# writer.py Implements the Writer class. +# Handles colour, word wrap and tab stops + +# V0.40 Jan 2021 Improved handling of word wrap and line clip. Upside-down +# rendering no longer supported: delegate to device driver. +# V0.35 Sept 2020 Fast rendering option for color displays + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019-2021 Peter Hinch + +# A Writer supports rendering text to a Display instance in a given font. +# Multiple Writer instances may be created, each rendering a font to the +# same Display object. + +# Timings based on a 20 pixel high proportional font, run on a pyboard V1.0. +# Using CWriter's slow rendering: _printchar 9.5ms typ, 13.5ms max. +# Using Writer's fast rendering: _printchar 115μs min 480μs typ 950μs max. + +# CWriter on Pyboard D SF2W at standard clock rate +# Fast method 500-600μs typical, up to 1.07ms on larger fonts +# Revised fast method 691μs avg, up to 2.14ms on larger fonts +# Slow method 2700μs typical, up to 11ms on larger fonts + +import framebuf +from uctypes import bytearray_at, addressof +from sys import platform + +__version__ = (0, 4, 2) + +fast_mode = platform == 'pyboard' +if fast_mode: + try: + try: + from framebuf_utils import render + except ImportError: # May be running in GUI. Try relative import. + try: + from .framebuf_utils import render + except ImportError: + fast_mode = False + except ValueError: + fast_mode = False + if not fast_mode: + print('Ignoring missing or invalid framebuf_utils.mpy.') + + +class DisplayState(): + def __init__(self): + self.text_row = 0 + self.text_col = 0 + +def _get_id(device): + if not isinstance(device, framebuf.FrameBuffer): + raise ValueError('Device must be derived from FrameBuffer.') + return id(device) + +# Basic Writer class for monochrome displays +class Writer(): + + state = {} # Holds a display state for each device + + @staticmethod + def set_textpos(device, row=None, col=None): + devid = _get_id(device) + if devid not in Writer.state: + Writer.state[devid] = DisplayState() + s = Writer.state[devid] # Current state + if row is not None: + if row < 0 or row >= device.height: + raise ValueError('row is out of range') + s.text_row = row + if col is not None: + if col < 0 or col >= device.width: + raise ValueError('col is out of range') + s.text_col = col + return s.text_row, s.text_col + + def __init__(self, device, font, verbose=True): + self.devid = _get_id(device) + self.device = device + if self.devid not in Writer.state: + Writer.state[self.devid] = DisplayState() + self.font = font + if font.height() >= device.height or font.max_width() >= device.width: + raise ValueError('Font too large for screen') + # Allow to work with reverse or normal font mapping + if font.hmap(): + self.map = framebuf.MONO_HMSB if font.reverse() else framebuf.MONO_HLSB + else: + raise ValueError('Font must be horizontally mapped.') + if verbose: + fstr = 'Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}.' + print(fstr.format(font.reverse(), device.width, device.height)) + print('Start row = {} col = {}'.format(self._getstate().text_row, self._getstate().text_col)) + self.screenwidth = device.width # In pixels + self.screenheight = device.height + self.bgcolor = 0 # Monochrome background and foreground colors + self.fgcolor = 1 + self.row_clip = False # Clip or scroll when screen fullt + self.col_clip = False # Clip or new line when row is full + self.wrap = True # Word wrap + self.cpos = 0 + self.tab = 4 + + self.glyph = None # Current char + self.char_height = 0 + self.char_width = 0 + self.clip_width = 0 + + def _getstate(self): + return Writer.state[self.devid] + + def _newline(self): + s = self._getstate() + height = self.font.height() + s.text_row += height + s.text_col = 0 + margin = self.screenheight - (s.text_row + height) + y = self.screenheight + margin + if margin < 0: + if not self.row_clip: + self.device.scroll(0, margin) + self.device.fill_rect(0, y, self.screenwidth, abs(margin), self.bgcolor) + s.text_row += margin + + def set_clip(self, row_clip=None, col_clip=None, wrap=None): + if row_clip is not None: + self.row_clip = row_clip + if col_clip is not None: + self.col_clip = col_clip + if wrap is not None: + self.wrap = wrap + return self.row_clip, self.col_clip, self.wrap + + @property + def height(self): # Property for consistency with device + return self.font.height() + + def printstring(self, string, invert=False): + # word wrapping. Assumes words separated by single space. + q = string.split('\n') + last = len(q) - 1 + for n, s in enumerate(q): + if s: + self._printline(s, invert) + if n != last: + self._printchar('\n') + + def _printline(self, string, invert): + rstr = None + if self.wrap and self.stringlen(string, True): # Length > self.screenwidth + pos = 0 + lstr = string[:] + while self.stringlen(lstr, True): # Length > self.screenwidth + pos = lstr.rfind(' ') + lstr = lstr[:pos].rstrip() + if pos > 0: + rstr = string[pos + 1:] + string = lstr + + for char in string: + self._printchar(char, invert) + if rstr is not None: + self._printchar('\n') + self._printline(rstr, invert) # Recurse + + def stringlen(self, string, oh=False): + sc = self._getstate().text_col # Start column + wd = self.screenwidth + l = 0 + for char in string[:-1]: + _, _, char_width = self.font.get_ch(char) + l += char_width + if oh and l + sc > wd: + return True # All done. Save time. + char = string[-1] + _, _, char_width = self.font.get_ch(char) + if oh and l + sc + char_width > wd: + l += self._truelen(char) # Last char might have blank cols on RHS + else: + l += char_width # Public method. Return same value as old code. + return l + sc > wd if oh else l + + # Return the printable width of a glyph less any blank columns on RHS + def _truelen(self, char): + glyph, ht, wd = self.font.get_ch(char) + div, mod = divmod(wd, 8) + gbytes = div + 1 if mod else div # No. of bytes per row of glyph + mc = 0 # Max non-blank column + data = glyph[(wd - 1) // 8] # Last byte of row 0 + for row in range(ht): # Glyph row + for col in range(wd -1, -1, -1): # Glyph column + gbyte, gbit = divmod(col, 8) + if gbit == 0: # Next glyph byte + data = glyph[row * gbytes + gbyte] + if col <= mc: + break + if data & (1 << (7 - gbit)): # Pixel is lit (1) + mc = col # Eventually gives rightmost lit pixel + break + if mc + 1 == wd: + break # All done: no trailing space + print('Truelen', char, wd, mc + 1) # TEST + return mc + 1 + + def _get_char(self, char, recurse): + if not recurse: # Handle tabs + if char == '\n': + self.cpos = 0 + elif char == '\t': + nspaces = self.tab - (self.cpos % self.tab) + if nspaces == 0: + nspaces = self.tab + while nspaces: + nspaces -= 1 + self._printchar(' ', recurse=True) + self.glyph = None # All done + return + + self.glyph = None # Assume all done + if char == '\n': + self._newline() + return + glyph, char_height, char_width = self.font.get_ch(char) + s = self._getstate() + np = None # Allow restriction on printable columns + if s.text_row + char_height > self.screenheight: + if self.row_clip: + return + self._newline() + oh = s.text_col + char_width - self.screenwidth # Overhang (+ve) + if oh > 0: + if self.col_clip or self.wrap: + np = char_width - oh # No. of printable columns + if np <= 0: + return + else: + self._newline() + self.glyph = glyph + self.char_height = char_height + self.char_width = char_width + self.clip_width = char_width if np is None else np + + # Method using blitting. Efficient rendering for monochrome displays. + # Tested on SSD1306. Invert is for black-on-white rendering. + def _printchar(self, char, invert=False, recurse=False): + s = self._getstate() + self._get_char(char, recurse) + if self.glyph is None: + return # All done + buf = bytearray(self.glyph) + if invert: + for i, v in enumerate(buf): + buf[i] = 0xFF & ~ v + fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map) + self.device.blit(fbc, s.text_col, s.text_row) + s.text_col += self.char_width + self.cpos += 1 + + def tabsize(self, value=None): + if value is not None: + self.tab = value + return self.tab + + def setcolor(self, *_): + return self.fgcolor, self.bgcolor + +# Writer for colour displays or upside down rendering +class CWriter(Writer): + + + def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True): + super().__init__(device, font, verbose) + if bgcolor is not None: # Assume monochrome. + self.bgcolor = bgcolor + if fgcolor is not None: + self.fgcolor = fgcolor + self.def_bgcolor = self.bgcolor + self.def_fgcolor = self.fgcolor + self._printchar = self._pchfast if fast_mode else self._pchslow + verbose and print('Render {} using fast mode'.format('is' if fast_mode else 'not')) + + def _pchfast(self, char, invert=False, recurse=False): + s = self._getstate() + self._get_char(char, recurse) + if self.glyph is None: + return # All done + buf = bytearray_at(addressof(self.glyph), len(self.glyph)) + fbc = framebuf.FrameBuffer(buf, self.char_width, self.char_height, self.map) + fgcolor = self.bgcolor if invert else self.fgcolor + bgcolor = self.fgcolor if invert else self.bgcolor + # render clips a glyph if outside bounds of destination + render(self.device, fbc, s.text_col, s.text_row, fgcolor, bgcolor) + s.text_col += self.char_width + self.cpos += 1 + + def _pchslow(self, char, invert=False, recurse=False): + s = self._getstate() + self._get_char(char, recurse) + if self.glyph is None: + return # All done + char_height = self.char_height + char_width = self.char_width + clip_width = self.clip_width + + div, mod = divmod(char_width, 8) + gbytes = div + 1 if mod else div # No. of bytes per row of glyph + device = self.device + fgcolor = self.bgcolor if invert else self.fgcolor + bgcolor = self.fgcolor if invert else self.bgcolor + drow = s.text_row # Destination row + wcol = s.text_col # Destination column of character start + for srow in range(char_height): # Source row + for scol in range(clip_width): # Source column + # Destination column: add writer column + dcol = wcol + scol + gbyte, gbit = divmod(scol, 8) + if gbit == 0: # Next glyph byte + data = self.glyph[srow * gbytes + gbyte] + pixel = fgcolor if data & (1 << (7 - gbit)) else bgcolor + device.pixel(dcol, drow, pixel) + drow += 1 + if drow >= self.screenheight or drow < 0: + break + s.text_col += char_width + self.cpos += 1 + + def setcolor(self, fgcolor=None, bgcolor=None): + if fgcolor is None and bgcolor is None: + self.fgcolor = self.def_fgcolor + self.bgcolor = self.def_bgcolor + else: + if fgcolor is not None: + self.fgcolor = fgcolor + if bgcolor is not None: + self.bgcolor = bgcolor + return self.fgcolor, self.bgcolor diff --git a/gui/demos/active.py b/gui/demos/active.py new file mode 100644 index 0000000..3503921 --- /dev/null +++ b/gui/demos/active.py @@ -0,0 +1,99 @@ +# active.py micro-gui demo of widgets that respond to user control + +# Import SSD and Display instances. Must be done first because of RAM use. +from hardware_setup import display, ssd # Create a display instance +from gui.core.ugui import Screen +from gui.core.writer import CWriter +import gui.fonts.arial10 as arial10 # Font for CWriter +from gui.core.colors import * +# Widgets +from gui.widgets.label import Label +from gui.widgets.scale import Scale +from gui.widgets.scale_log import ScaleLog +from gui.widgets.buttons import Button, CloseButton +from gui.widgets.sliders import Slider, HorizSlider +from gui.widgets.knob import Knob +from gui.widgets.checkbox import Checkbox + + +class BaseScreen(Screen): + def __init__(self): + + def tickcb(f, c): + if f > 0.8: + return RED + if f < -0.8: + return BLUE + return c + + def tick_log_cb(f, c): + if f > 20_000: + return RED + if f < 4: + return BLUE + return c + + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + col = 2 + row = 200 + Label(wri, row, col, 'Result') + col = 42 + self.lbl = Label(wri, row, col, 70, bdcolor=RED) + + self.vslider = Slider(wri, 2, 2, callback=self.slider_cb, + bdcolor=RED, slotcolor=BLUE, + legends=('0.0', '0.5', '1.0'), value=0.5) + + col = 80 + row = 15 + self.hslider = HorizSlider(wri, row, col, callback=self.slider_cb, + bdcolor=YELLOW, slotcolor=BLUE, + legends=('0.0', '0.5', '1.0'), value=0.7) + row += 30 + self.scale = Scale(wri, row, col, width = 150, tickcb = tickcb, + pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN, + callback=self.cb, active=True) + row += 40 + self.scale_log = ScaleLog(wri, row, col, width = 150, tickcb = tick_log_cb, + pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN, + callback=self.cb, value=10, active=True) + row = 120 + self.knob = Knob(wri, row, 2, callback = self.cb, bgcolor=DARKGREEN, color=LIGHTRED) + col = 150 + row = 185 + Checkbox(wri, row, col, callback=self.cbcb) + col += 35 + Label(wri, row, col, 'Enable/disable') + CloseButton(wri) + + + def cb(self, obj): + self.lbl.value('{:4.2f}'.format(obj.value())) + + def cbcb(self, cb): + val = cb.value() + self.vslider.greyed_out(val) + self.hslider.greyed_out(val) + self.scale.greyed_out(val) + self.scale_log.greyed_out(val) + self.knob.greyed_out(val) + + def slider_cb(self, s): + self.cb(s) + v = s.value() + if v < 0.2: + s.color(BLUE) + elif v > 0.8: + s.color(RED) + else: + s.color(GREEN) + +def test(): + if display.height < 240 or display.width < 320: + print(' This test requires a display of at least 320x240 pixels.') + else: + print('Testing micro-gui...') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/plot.py b/gui/demos/plot.py new file mode 100644 index 0000000..0117743 --- /dev/null +++ b/gui/demos/plot.py @@ -0,0 +1,250 @@ +# plot.py Test/demo program for micro-gui plot. Cross-patform, +# but requires a large enough display. +# Tested on 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 + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Initialise hardware and framebuf before importing modules. +from hardware_setup import ssd, display # Create a display instance + +import cmath +import math +import uasyncio as asyncio +from collections import OrderedDict + +from gui.core.writer import Writer, CWriter +from gui.core.ugui import Screen +from gui.widgets.graph import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence +from gui.widgets.label import Label +from gui.widgets.buttons import Button, CloseButton +from gui.widgets.listbox import Listbox + +# Fonts & colors +import gui.fonts.arial10 as arial10 +from gui.core.colors import * + +wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + +def fwdbutton(writer, row, col, cls_screen, text, color, *args, **kwargs): + def fwd(button): + Screen.change(cls_screen, args = args, kwargs = kwargs) + Button(writer, row, col, callback = fwd, bgcolor = color, + text = text, textcolor = BLACK, height = 20, width = 60) + + +class EmptyScreen(Screen): + def __init__(self): + super().__init__() + Label(wri, 2, 2, 'Test of overlay.') + Label(wri, 20, 2, 'Check redraw of underlying screen.') + CloseButton(wri) + +class CartesianScreen(Screen): + def __init__(self): + super().__init__() + self.g = CartesianGraph(wri, 2, 2, yorigin = 2, fgcolor=GREEN, + gridcolor=LIGHTGREEN) # Asymmetric y axis + Label(wri, 100, 2, 'Asymmetric axes.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + # At this point in time the Screen and graph have been constructed but + # not rendered. If we drew the curves now they would be overwritten by + # the graph + + # Now the graph has been drawn and we can overlay it with graphics primitives. + # This is not a uasyncio issue, but is the way Screen.change() is coded. + def after_open(self): + 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 + + Curve(self.g, YELLOW, populate_1(lambda x : x**3 + x**2 -x,)) # args demo + Curve(self.g, RED, populate_2()) + +class PolarScreen(Screen): + def __init__(self): + super().__init__() + self.g = PolarGraph(wri, 2, 2, fgcolor=GREEN, gridcolor=LIGHTGREEN) + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + 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 + + PolarCurve(self.g, YELLOW, populate()) + +class Lissajous(Screen): + def __init__(self): + super().__init__() + self.g = CartesianGraph(wri, 2, 2, fgcolor=GREEN, gridcolor=LIGHTGREEN) + Label(wri, 100, 2, 'Lissajous figure.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + def populate(): + t = -math.pi + while t <= math.pi: + yield math.sin(t), math.cos(3*t) # x, y + t += 0.1 + + Curve(self.g, YELLOW, populate()) + +class Lemniscate(Screen): + def __init__(self): + super().__init__() + self.g = CartesianGraph(wri, 2, 2, height = 75, fgcolor=GREEN, gridcolor=LIGHTGREEN) + Label(wri, 82, 2, 'To infinity and beyond...') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + 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 + + Curve(self.g, YELLOW, populate()) + +class PolarClip(Screen): + def __init__(self): + super().__init__() + self.g = PolarGraph(wri, 2, 2, fgcolor=GREEN, gridcolor=LIGHTGREEN) + Label(wri, 100, 2, 'Clipping of polar data.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + 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 + + PolarCurve(self.g, YELLOW, populate(1)) + PolarCurve(self.g, RED, populate(cmath.rect(1, cmath.pi/5),)) + +class RTPolar(Screen): + def __init__(self): + super().__init__() + self.g = PolarGraph(wri, 2, 2, fgcolor=GREEN, gridcolor=LIGHTGREEN) + Label(wri, 100, 2, 'Realtime polar data.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + self.reg_task(self.run(self.g), True) # Cancel on screen change + + async def run(self, g): + await asyncio.sleep_ms(0) + 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)) + await asyncio.sleep_ms(60) + +class RTRect(Screen): + def __init__(self): + super().__init__() + self.g = CartesianGraph(wri, 2, 2, fgcolor=GREEN, gridcolor=LIGHTGREEN) + Label(wri, 100, 2, 'Realtime discontinuous data.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + self.reg_task(self.run(self.g), True) # Cancel on screen change + + async def run(self, g): + await asyncio.sleep_ms(0) + 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) + await asyncio.sleep_ms(100) + 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) + await asyncio.sleep_ms(100) + x += 0.05 + +class TSeq(Screen): + def __init__(self): + super().__init__() + self.g = CartesianGraph(wri, 2, 2, xorigin = 10, fgcolor=GREEN, + gridcolor=LIGHTGREEN, bdcolor=False) + Label(wri, 100, 2, 'Time sequence.') + fwdbutton(wri, 2, 130, EmptyScreen, 'Forward', GREEN) + CloseButton(wri) + + def after_open(self): # After graph has been drawn + self.reg_task(self.run(self.g), True) # Cancel on screen change + + async def run(self, g): + await asyncio.sleep_ms(0) + tsy = TSequence(g, YELLOW, 50) + tsr = TSequence(g, RED, 50) + t = 0 + while True: + g.show() # Redraw the empty graph + tsy.add(0.9*math.sin(t/10)) + tsr.add(0.4*math.cos(t/10)) # Plot the new curves + await asyncio.sleep_ms(400) + t += 1 + + +class BaseScreen(Screen): + def __init__(self): + super().__init__() + d = OrderedDict() + d['Cartesian'] = CartesianScreen + d['Polar'] = PolarScreen + d['Lissajous'] = Lissajous + d['Lemniscate'] = Lemniscate + d['Polar clipping'] = PolarClip + d['Realtime polar'] = RTPolar + d['Realtime rect'] = RTRect + d['Time sequence'] = TSeq + + row = 2 + col = 2 + Listbox(wri, row, col, callback=self.lbcb, args=(d,), + elements = tuple(d.keys()), + bdcolor = GREEN, bgcolor = DARKGREEN) + CloseButton(wri) + + def lbcb(self, lb, d): + Screen.change(d[lb.textvalue()]) + +def test(): + if display.height < 240 or display.width < 320: + print(' This test requires a display of at least 320x240 pixels.') + else: + print('Testing micro-gui...') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/screens.py b/gui/demos/screens.py new file mode 100644 index 0000000..5bab5b3 --- /dev/null +++ b/gui/demos/screens.py @@ -0,0 +1,109 @@ +# screens.py micro-gui demo of multiple screens, dropdowns etc +# Initialise hardware and framebuf before importing modules. +# Import SSD and Display instances. Must be done first because of RAM use. +from hardware_setup import display, ssd # Create a display instance +from gui.core.ugui import Screen, Window + +from gui.widgets.label import Label +from gui.widgets.buttons import Button, RadioButtons, CloseButton +from gui.widgets.listbox import Listbox, ON_LEAVE +from gui.widgets.dropdown import Dropdown +from gui.widgets.dialog import DialogBox +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.arial10 as arial10 +from gui.core.colors import * + +# Note that litcolor is defeated by design, because the callback's action +# is to change the screen currency. This tests the bugfix. +def fwdbutton(writer, row, col, cls_screen, text, color, *args, **kwargs): + def fwd(button): + Screen.change(cls_screen, args = args, kwargs = kwargs) + Button(writer, row, col, callback = fwd, bgcolor = color, litcolor = YELLOW, + text = text, textcolor = WHITE, shape = CLIPPED_RECT) + +# Demo of creating a dialog manually +class UserDialogBox(Window): + def __init__(self, writer, callback, args): + + def back(button, text): + Window.value(text) + callback(Window, *args) + Screen.back() + + def close_cb(button, text): + Window.value(text) + callback(Window, *args) + + height = 80 + width = 150 + super().__init__(20, 20, height, width, bgcolor = DARKGREEN) + row = self.height - 30 + # .locn converts Window relative row, col to absolute row, col + Button(writer, *self.locn(row, 20), height = 20, width = 50, textcolor = BLACK, bgcolor = RED, + text = 'Cat', callback = back, args = ('Cat',)) + Button(writer, *self.locn(row, 80), height = 20, width = 50, textcolor = BLACK, bgcolor = GREEN, + text = 'Dog', callback = back, args = ('Dog',)) + CloseButton(writer, callback=close_cb, args = ('Close',)) + +# Minimal screen change example +class Overlay(Screen): + def __init__(self): + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + Label(wri, 20, 20, 'Screen overlays base') + CloseButton(wri) + +class BaseScreen(Screen): + + def __init__(self): + + def lbcb(lb): + print('Listbox', lb.textvalue()) + + def ddcb(dd): + print('Dropdown', tv := dd.textvalue()) + if tv == 'new screen': + Screen.change(Overlay) + + def dbcb(window, label): + label.value('Auto Dialog: {}'.format(window.value())) + + def udbcb(window, label): + label.value('User Dialog: {}'.format(window.value())) + + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + + col = 2 + row = 2 + Listbox(wri, row, col, callback=lbcb, + elements = ('cat', 'dog', 'aardvark', 'goat', 'pig', 'mouse'), + bdcolor = GREEN, bgcolor = DARKGREEN, also = ON_LEAVE) + col = 70 + Dropdown(wri, row, col, callback=ddcb, + elements = ('hydrogen', 'helium', 'neon', 'xenon', 'new screen'), + bdcolor = GREEN, bgcolor = DARKGREEN) + row += 30 + lbl = Label(wri, row, col, 'Result from dialog box.') + row += 20 + dialog_elements = (('Yes', GREEN), ('No', RED), ('Foo', YELLOW)) + # 1st 6 args are for fwdbutton + fwdbutton(wri, row, col, DialogBox, 'Dialog', RED, + # Args for DialogBox constructor + wri, elements = dialog_elements, label = 'Test dialog', callback = dbcb, args = (lbl,)) + col += 60 + fwdbutton(wri, row, col, UserDialogBox, 'User', BLUE, + # Args for UserDialogBox constructor + wri, udbcb, (lbl,)) + CloseButton(wri) # Quit the application + +def test(): + if display.height < 128 or display.width < 240: + print('This test requires a display of at least 240x128 pixels.') + else: + print('Testing micro-gui...') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/simple.py b/gui/demos/simple.py new file mode 100644 index 0000000..2b91597 --- /dev/null +++ b/gui/demos/simple.py @@ -0,0 +1,39 @@ +# simple.py Minimal micro-gui demo. +# Initialise hardware and framebuf before importing modules. +# Import SSD and Display instances. Must be done first because of RAM use. +from hardware_setup import ssd # Create a display instance +from gui.core.ugui import Screen + +from gui.widgets.label import Label +from gui.widgets.buttons import Button, CloseButton +from gui.core.writer import CWriter + +# Font for CWriter +import gui.fonts.arial10 as arial10 +from gui.core.colors import * + + +class BaseScreen(Screen): + + def __init__(self): + + def my_callback(button, arg): + print('Button pressed', arg) + + super().__init__() + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + + col = 2 + row = 2 + Label(wri, row, col, 'Simple Demo') + row = 20 + Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',)) + col += 60 + Button(wri, row, col, text='No', callback=my_callback, args=('No',)) + CloseButton(wri) # Quit the application + +def test(): + print('Testing micro-gui...') + Screen.change(BaseScreen) + +test() diff --git a/gui/demos/tbox.py b/gui/demos/tbox.py new file mode 100644 index 0000000..6255b6b --- /dev/null +++ b/gui/demos/tbox.py @@ -0,0 +1,120 @@ +# tbox.py Test/demo of Textbox widget for micro-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Usage: +# import gui.demos.tbox + +# Initialise hardware and framebuf before importing modules. +from hardware_setup import display, ssd # Create a display instance + +from gui.core.ugui import Screen +from gui.core.writer import CWriter + +import uasyncio as asyncio +from gui.core.colors import * +import gui.fonts.arial10 as arial10 +from gui.widgets.label import Label +from gui.widgets.textbox import Textbox +from gui.widgets.buttons import Button, CloseButton + +wri = CWriter(ssd, arial10, verbose=False) + +def fwdbutton(wri, row, col, cls_screen, width, text='Next'): + def fwd(button): + Screen.change(cls_screen) + Button(wri, row, col, height = 20, width = width, + callback = fwd, fgcolor = BLACK, bgcolor = GREEN, + text = text, shape = RECTANGLE) + return width + + +async def wrap(tb): + s = '''The textbox displays multiple lines of text in a field of fixed dimensions. \ +Text may be clipped to the width of the control or may be word-wrapped. If the number \ +of lines of text exceeds the height available, scrolling may be performed \ +by calling a method. +''' + tb.clear() + tb.append(s, ntrim = 100, line = 0) + while True: + await asyncio.sleep(1) + if not tb.scroll(1): + break + +async def clip(tb): + ss = ('clip demo', 'short', 'longer line', 'much longer line with spaces', + 'antidisestablishmentarianism', 'line with\nline break', 'Done') + tb.clear() + for s in ss: + tb.append(s, ntrim = 100) # Default line=None scrolls to show most recent + await asyncio.sleep(1) + +# Args for textboxes +# Positional +pargs = (2, 2, 124, 7) # Row, Col, Width, nlines + +# Keyword +tbargs = {'fgcolor' : YELLOW, + 'bdcolor' : RED, + 'bgcolor' : DARKGREEN, + } + + +class TBCScreen(Screen): + def __init__(self): + super().__init__() + self.tb = Textbox(wri, *pargs, clip=True, **tbargs) + CloseButton(wri) + asyncio.create_task(self.main()) + + async def main(self): + await clip(self.tb) + +class TBWScreen(Screen): + def __init__(self): + super().__init__() + self.tb = Textbox(wri, *pargs, clip=False, **tbargs) + CloseButton(wri) + asyncio.create_task(self.main()) + + async def main(self): + await wrap(self.tb) + +user_str = '''The textbox displays multiple lines of text in a field of fixed dimensions. \ +Text may be clipped to the width of the control or may be word-wrapped. If the number \ +of lines of text exceeds the height available, scrolling may be performed \ +by calling a method. + +Please use the increase and decrease buttons to scroll this text. +''' + +class TBUScreen(Screen): + def __init__(self): + super().__init__() + tb = Textbox(wri, *pargs, clip=False, active=True, **tbargs) + tb.append(user_str, ntrim=100) + CloseButton(wri) + + +class MainScreen(Screen): + def __init__(self): + super().__init__() + Label(wri, 2, 2, 'Select test to run') + col = 2 + row = 60 + col += fwdbutton(wri, row, col, TBWScreen, 50, 'Wrap') + 10 + col += fwdbutton(wri, row, col, TBCScreen, 50, 'Clip') + 10 + fwdbutton(wri, row, col, TBUScreen, 50, 'Scroll') + CloseButton(wri) + + +def test(): + if display.height < 128 or display.width < 128: + print(' This test requires a display of at least 128x128 pixels.') + else: + print('Testing micro-gui...') + Screen.change(MainScreen) + +test() diff --git a/gui/demos/various.py b/gui/demos/various.py new file mode 100644 index 0000000..82637c7 --- /dev/null +++ b/gui/demos/various.py @@ -0,0 +1,160 @@ +# various.py micro-gui demo of multiple controls on a large display + +# Initialise hardware and framebuf before importing modules. +# Import SSD and Display instances. Must be done first because of RAM use. +from hardware_setup import display, ssd # Create a display instance +from gui.core.ugui import Screen +from gui.core.writer import CWriter +import gui.fonts.arial10 as arial10 # Font for CWriter +from gui.core.colors import * +# Widgets +from gui.widgets.label import Label +from gui.widgets.dial import Dial, Pointer +from gui.widgets.meter import Meter +from gui.widgets.scale import Scale +from gui.widgets.buttons import Button, ButtonList, RadioButtons, CloseButton +from gui.widgets.checkbox import Checkbox +from gui.widgets.led import LED + +import cmath +import uasyncio as asyncio +import utime +import gc + + +class FooScreen(Screen): + def __init__(self): + buttons = [] + + # A ButtonList with two entries + table_buttonset = ( + {'fgcolor' : RED, 'text' : 'Disable', 'args' : (buttons, True)}, + {'fgcolor' : GREEN, 'text' : 'Enable', 'args' : (buttons, False)}, + ) + + table_radiobuttons = ( + {'text' : '1', 'args' : ('1',)}, + {'text' : '2', 'args' : ('2',)}, + {'text' : '3', 'args' : ('3',)}, + {'text' : '4', 'args' : ('4',)}, + ) + + def tickcb(f, c): + if f > 0.8: + return RED + if f < -0.8: + return BLUE + return c + + def bcb(b): + print('Button pressed', b) + + super().__init__() + self.rb0 = None + self.bs0 = None + wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) + lbltim = Label(wri, 50, 85, 'this is a test', bdcolor=RED) + + m0 = Meter(wri, 20, 240, divisions = 4, ptcolor=YELLOW, + label='left', style=Meter.LINE, legends=('0.0', '0.5', '1.0')) + # Instantiate displayable objects. bgcolor forces complete redraw. + dial = Dial(wri, 2, 2, height = 75, ticks = 12, bgcolor=BLACK, bdcolor=None, label=120) # Border in fg color + scale = Scale(wri, 2, 100, width = 124, tickcb = tickcb, + pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN) + + # Four Button instances + row = 120 + ht = 30 + for i, s in enumerate(('a', 'b', 'c', 'd')): + col= 2 + i * (ht + 5) + buttons.append(Button(wri, row, col, height=ht, callback=bcb, text=s, litcolor=RED, shape=CIRCLE, bgcolor=DARKGREEN)) + + # ButtonList + self.bs = ButtonList(self.callback) + self.bs0 = None + col+= 50 + for t in table_buttonset: # Buttons overlay each other at same location + button = self.bs.add_button(wri, 120, col, shape=RECTANGLE, textcolor=BLUE, height=30, **t) + if self.bs0 is None: # Save for reset button callback + self.bs0 = button + + # Reset button + col+= 60 + btn = Button(wri, row, col, height=30, callback=self.rstcb, text='reset', litcolor=RED, fgcolor=GREEN, bgcolor=DARKGREEN) + + # Radio buttons + col= 2 + self.rb = RadioButtons(BLUE, self.rbcb) # color of selected button + self.rb0 = None + for t in table_radiobuttons: + button = self.rb.add_button(wri, 160, col, textcolor = WHITE, + fgcolor = BLUE, bgcolor = DARKBLUE, shape=CIRCLE, height = 30, **t) + if self.rb0 is None: # Save for reset button callback + self.rb0 = button + col+= 35 + # Checkbox + col+= 35 + Checkbox(wri, 160, col, callback=self.cbcb) + col+= 40 + self.led = LED(wri, 160, col, color=YELLOW, bdcolor=GREEN) + CloseButton(wri) + asyncio.create_task(run(dial, lbltim, m0, scale)) + + + def callback(self, button, buttons, val): + buttons[2].greyed_out(val) + + def rbcb(self, button, val): + print('RadioButtons callback', val) + + def rstcb(self, button): + print('Reset button: init ButtonList and RadioButtons') + self.bs.value(self.bs0) # BUG This is calling ButtonList.value which calls ._update -> Screen.select which changes currency + self.rb.value(self.rb0) + + def cbcb(self, cb): + self.led.value(cb.value()) + gc.collect() + print('Free RAM:', gc.mem_free()) + +async def run(dial, lbltim, m0, scale): + days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', + 'Sunday') + months = ('Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', + 'Aug', 'Sept', 'Oct', 'Nov', 'Dec') + uv = lambda phi : cmath.rect(1, phi) # Return a unit vector of phase phi + pi = cmath.pi + 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 + + cv = -1.0 # Scale + dv = 0.005 + 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])) + m0.value(t[5]/60) + scale.value(cv) + await asyncio.sleep_ms(200) + cv += dv + if abs(cv) > 1.0: + dv = -dv + cv += dv + + +def test(): + if display.height < 240 or display.width < 320: + print(' This test requires a display of at least 320x240 pixels.') + else: + print('Testing micro-gui...') + Screen.change(FooScreen) + +test() diff --git a/gui/demos/vtest.py b/gui/demos/vtest.py new file mode 100644 index 0000000..44e3a76 --- /dev/null +++ b/gui/demos/vtest.py @@ -0,0 +1,113 @@ +# vtest.py Test/demo of VectorDial for micro-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Initialise hardware and framebuf before importing modules. +from hardware_setup import display, ssd # Create a display instance + +import urandom +import time +from cmath import rect, pi +import uasyncio as asyncio + +from gui.core.ugui import Screen +from gui.core.writer import CWriter +from gui.fonts import font10 +from gui.core.colors import * +# Widgets +from gui.widgets.label import Label +from gui.widgets.buttons import Button, CloseButton +from gui.widgets.vectors import Pointer, VectorDial + + +def fwdbutton(wri, row, col, cls_screen, text='Next'): + def fwd(button): + Screen.change(cls_screen) + Button(wri, row, col, height = 30, callback = fwd, fgcolor = BLACK, bgcolor = GREEN, + text = text, shape = RECTANGLE, width = 100) + + +class BackScreen(Screen): + def __init__(self): + super().__init__() + wri = CWriter(ssd, font10, GREEN, BLACK, verbose=False) + Label(wri, 2, 2, 'Ensure back refreshes properly') + CloseButton(wri) + +# Create a random vector. Interpolate between current vector and the new one. +# Change pointer color dependent on magnitude. +async def ptr_test(dial): + ptr = Pointer(dial) + v = 0j + steps = 20 # No. of interpolation steps + grv = lambda : urandom.getrandbits(16) / 2**15 - 1 # Random: range -1.0 to +1.0 + while True: + v1 = grv() + 1j * grv() # Random vector + dv = (v1 - v) / steps # Interpolation vector + for _ in range(steps): + v += dv + mag = abs(v) + if mag < 0.3: + ptr.value(v, BLUE) + elif mag < 0.7: + ptr.value(v, GREEN) + else: + ptr.value(v, RED) + await asyncio.sleep_ms(200) + +# Analog clock demo. Note this could also be achieved using the Dial class. +async def aclock(dial, lbldate, lbltim): + uv = lambda phi : rect(1, phi) # Return a unit vector of phase phi + days = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', + 'Sunday') + months = ('January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December') + + hrs = Pointer(dial) + mins = Pointer(dial) + secs = Pointer(dial) + + hstart = 0 + 0.7j # Pointer lengths. Position at top. + mstart = 0 + 1j + sstart = 0 + 1j + + while True: + t = time.localtime() + hrs.value(hstart * uv(-t[3] * pi/6 - t[4] * pi / 360), CYAN) + mins.value(mstart * uv(-t[4] * pi/30), CYAN) + secs.value(sstart * uv(-t[5] * pi/30), RED) + lbltim.value('{:02d}.{:02d}.{:02d}'.format(t[3], t[4], t[5])) + lbldate.value('{} {} {} {}'.format(days[t[6]], t[2], months[t[1] - 1], t[0])) + await asyncio.sleep(1) + +class VScreen(Screen): + def __init__(self): + super().__init__() + labels = {'bdcolor' : RED, + 'fgcolor' : WHITE, + 'bgcolor' : DARKGREEN, + } + + wri = CWriter(ssd, font10, GREEN, BLACK, verbose=False) + + fwdbutton(wri, 200, 2, BackScreen, 'Forward') + CloseButton(wri) + # Set up random vector display with two pointers + dial = VectorDial(wri, 2, 2, height = 100, ticks = 12, fgcolor = YELLOW, arrow = True) + self.reg_task(ptr_test(dial)) + self.reg_task(ptr_test(dial)) + # Set up clock display: instantiate labels + lbldate = Label(wri, 110, 2, 200, **labels) + lbltim = Label(wri, 150, 2, 80, **labels) + dial = VectorDial(wri, 2, 120, height = 100, ticks = 12, fgcolor = GREEN, pip = GREEN) + self.reg_task(aclock(dial, lbldate, lbltim)) + +def test(): + if display.height < 240 or display.width < 320: + print(' This test requires a display of at least 320x240 pixels.') + else: + print('Testing micro-gui...') + Screen.change(VScreen) + +test() diff --git a/gui/fonts/__init__.py b/gui/fonts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/fonts/arial10.py b/gui/fonts/arial10.py new file mode 100644 index 0000000..0a28777 --- /dev/null +++ b/gui/fonts/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/gui/fonts/arial35.py b/gui/fonts/arial35.py new file mode 100644 index 0000000..03af2b1 --- /dev/null +++ b/gui/fonts/arial35.py @@ -0,0 +1,671 @@ +# Code generated by font_to_py.py. +# Font: Arial.ttf +# Cmd: ./font_to_py.py Arial.ttf 35 arial35.py -x +version = '0.33' + +def height(): + return 35 + +def baseline(): + return 27 + +def max_width(): + return 37 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 126 + +_font =\ +b'\x14\x00\x00\x00\x00\x01\xf8\x00\x07\xfe\x00\x0f\xff\x00\x1f\x0f'\ +b'\x80\x1c\x03\xc0\x38\x01\xc0\x38\x01\xc0\x00\x01\xc0\x00\x01\xc0'\ +b'\x00\x03\x80\x00\x07\x80\x00\x0f\x00\x00\x1e\x00\x00\x3c\x00\x00'\ +b'\x78\x00\x00\x70\x00\x00\xe0\x00\x00\xe0\x00\x00\xe0\x00\x00\xe0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\xe0\x00'\ +b'\x00\xe0\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\x0a\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x00\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e'\ +b'\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e'\ +b'\x00\x04\x00\x04\x00\x04\x00\x04\x00\x04\x00\x04\x00\x04\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x70'\ +b'\xe0\x70\xe0\x70\xe0\x70\xe0\x70\xe0\x70\xe0\x70\xe0\x70\xe0\x20'\ +b'\x40\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x14\x00\x00\x00\x00\x01\xc1\xc0\x01\xc1\xc0\x01\xc3'\ +b'\xc0\x03\x83\x80\x03\x83\x80\x03\x83\x80\x03\x83\x80\xff\xff\xf0'\ +b'\xff\xff\xf0\xff\xff\xf0\x07\x07\x00\x07\x07\x00\x07\x07\x00\x0e'\ +b'\x0e\x00\x0e\x0e\x00\x0e\x0e\x00\xff\xff\xf0\xff\xff\xf0\xff\xff'\ +b'\xf0\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x3c\x38\x00'\ +b'\x38\x38\x00\x38\x38\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\x14\x00'\ +b'\x00\x60\x00\x03\xf8\x00\x0f\xfe\x00\x1f\xff\x00\x1e\x67\x00\x3c'\ +b'\x63\x80\x38\x63\x80\x38\x60\x00\x38\x60\x00\x38\x60\x00\x1c\x60'\ +b'\x00\x1f\xe0\x00\x0f\xf8\x00\x03\xfe\x00\x00\xff\x00\x00\x6f\x80'\ +b'\x00\x63\x80\x00\x61\xc0\x00\x61\xc0\x70\x61\xc0\x70\x61\xc0\x78'\ +b'\x63\xc0\x3c\x63\x80\x3e\x67\x80\x1f\xff\x00\x0f\xfe\x00\x01\xf8'\ +b'\x00\x00\x60\x00\x00\x60\x00\x00\x60\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x00\x00\x07'\ +b'\xc0\x03\x80\x0f\xe0\x07\x80\x1c\x70\x07\x00\x1c\x70\x0f\x00\x38'\ +b'\x38\x0e\x00\x38\x38\x1c\x00\x38\x38\x3c\x00\x38\x38\x38\x00\x38'\ +b'\x38\x78\x00\x38\x38\x70\x00\x18\x70\xf0\x00\x1c\x70\xe0\x00\x0f'\ +b'\xe1\xc3\xe0\x07\xc1\xc7\xf0\x00\x03\x8e\x38\x00\x07\x8e\x38\x00'\ +b'\x07\x1c\x1c\x00\x0f\x1c\x1c\x00\x0e\x1c\x1c\x00\x1e\x1c\x1c\x00'\ +b'\x1c\x1c\x1c\x00\x38\x1c\x1c\x00\x78\x0c\x38\x00\x70\x0e\x38\x00'\ +b'\xf0\x07\xf0\x00\xe0\x03\xe0\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\x00\x00\x00\x18\x00\x00\x00\x00\x00\x7c\x00\x01'\ +b'\xfe\x00\x03\xff\x00\x07\x87\x80\x07\x03\x80\x07\x03\x80\x07\x03'\ +b'\x80\x07\x83\x80\x03\x87\x00\x03\xde\x00\x01\xfc\x00\x01\xf8\x00'\ +b'\x03\xf0\x00\x0f\xb8\x00\x1e\x3c\x20\x1c\x1e\x38\x3c\x0f\x78\x38'\ +b'\x07\x70\x38\x03\xf0\x38\x03\xe0\x3c\x01\xe0\x1c\x03\xf0\x1f\x0f'\ +b'\xf8\x0f\xff\xbe\x07\xfe\x1c\x01\xf8\x08\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\x07\x00\x00\x38\x38\x38\x38\x38\x38\x38\x38\x10\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\x0c\x00\x00\x00\x00\xc0\x01\x80\x03'\ +b'\x80\x03\x00\x07\x00\x06\x00\x0e\x00\x0c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38'\ +b'\x00\x38\x00\x38\x00\x18\x00\x1c\x00\x1c\x00\x1c\x00\x0c\x00\x0e'\ +b'\x00\x06\x00\x07\x00\x03\x00\x03\x80\x01\x80\x00\xc0\x00\x00\x0c'\ +b'\x00\x00\x00\x30\x00\x18\x00\x1c\x00\x0c\x00\x0e\x00\x06\x00\x07'\ +b'\x00\x03\x00\x03\x80\x03\x80\x03\x80\x01\x80\x01\xc0\x01\xc0\x01'\ +b'\xc0\x01\xc0\x01\xc0\x01\xc0\x01\xc0\x01\xc0\x01\xc0\x01\x80\x03'\ +b'\x80\x03\x80\x03\x80\x07\x00\x07\x00\x06\x00\x0e\x00\x0c\x00\x1c'\ +b'\x00\x18\x00\x30\x00\x00\x00\x0e\x00\x00\x00\x03\x80\x03\x80\x03'\ +b'\x80\x3b\xb8\x7f\xfc\x1f\xf0\x07\xc0\x0e\xe0\x1e\xf0\x1c\x70\x08'\ +b'\x20\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00'\ +b'\x70\x00\x00\x70\x00\x00\x70\x00\x3f\xff\xe0\x3f\xff\xe0\x3f\xff'\ +b'\xe0\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00'\ +b'\x00\x70\x00\x00\x70\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\x00\x00\x00\x00\x00\x00\x0a\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\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x1c\x00'\ +b'\x1c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x18\x00\x18\x00\x00\x00'\ +b'\x00\x00\x0c\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\x7f\xe0\x7f\xe0\x7f\xe0\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\x00\x00\x00\x00\x00\x00\x0a\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\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x1c\x00'\ +b'\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0a\x00\x00\x00\x01\xc0\x01\xc0\x03\x80\x03\x80\x03\x80'\ +b'\x03\x80\x07\x00\x07\x00\x07\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00'\ +b'\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x38\x00\x38\x00\x38\x00\x78\x00'\ +b'\x70\x00\x70\x00\x70\x00\xe0\x00\xe0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x01'\ +b'\xf8\x00\x07\xfe\x00\x0f\xff\x00\x0f\x0f\x00\x1e\x07\x80\x1c\x03'\ +b'\x80\x1c\x03\x80\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38'\ +b'\x01\xc0\x38\x01\xc0\x38\x01\xc0\x1c\x03\x80\x1c\x03\x80\x1e\x07'\ +b'\x80\x0f\x0f\x00\x0f\xff\x00\x07\xfe\x00\x01\xf8\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\x14\x00\x00\x00\x00\x00\x18\x00\x00\x38\x00'\ +b'\x00\x78\x00\x00\xf8\x00\x01\xf8\x00\x03\xf8\x00\x0f\xb8\x00\x0f'\ +b'\x38\x00\x0c\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38'\ +b'\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00'\ +b'\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\ +b'\x38\x00\x00\x38\x00\x00\x38\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'\x14\x00\x00\x00\x00\x03\xf8\x00\x0f\xfe\x00\x1f\xff\x00\x3e\x0f'\ +b'\x80\x38\x03\x80\x70\x03\xc0\x70\x01\xc0\x00\x01\xc0\x00\x01\xc0'\ +b'\x00\x01\xc0\x00\x03\x80\x00\x07\x80\x00\x07\x00\x00\x0e\x00\x00'\ +b'\x1c\x00\x00\x38\x00\x00\xf0\x00\x01\xe0\x00\x03\xc0\x00\x07\x80'\ +b'\x00\x0f\x00\x00\x1c\x00\x00\x38\x00\x00\x3f\xff\xc0\x7f\xff\xc0'\ +b'\x7f\xff\xc0\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\x14\x00\x00\x00\x00'\ +b'\x01\xf8\x00\x07\xfe\x00\x0f\xff\x00\x1e\x0f\x00\x1c\x07\x80\x38'\ +b'\x03\x80\x38\x03\x80\x00\x03\x80\x00\x07\x00\x00\x0f\x00\x00\xfe'\ +b'\x00\x00\xfe\x00\x00\xff\x00\x00\x07\x80\x00\x03\x80\x00\x01\xc0'\ +b'\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x38\x01\xc0\x38\x03\xc0\x1c'\ +b'\x03\x80\x1e\x0f\x80\x0f\xff\x00\x07\xfe\x00\x01\xf8\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\x14\x00\x00\x00\x00\x00\x0e\x00\x00\x1e'\ +b'\x00\x00\x3e\x00\x00\x3e\x00\x00\x7e\x00\x00\xfe\x00\x00\xee\x00'\ +b'\x01\xee\x00\x03\xce\x00\x03\x8e\x00\x07\x8e\x00\x07\x0e\x00\x0e'\ +b'\x0e\x00\x1e\x0e\x00\x1c\x0e\x00\x38\x0e\x00\x78\x0e\x00\x7f\xff'\ +b'\xc0\x7f\xff\xc0\x7f\xff\xc0\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00'\ +b'\x00\x0e\x00\x00\x0e\x00\x00\x0e\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\x14\x00\x00\x00\x00\x07\xff\x80\x07\xff\x80\x0f\xff\x80\x0e'\ +b'\x00\x00\x0e\x00\x00\x0e\x00\x00\x1e\x00\x00\x1c\x00\x00\x1c\xf8'\ +b'\x00\x1f\xfe\x00\x1f\xff\x00\x3e\x0f\x80\x38\x03\x80\x00\x03\xc0'\ +b'\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x38'\ +b'\x01\xc0\x38\x03\x80\x1c\x03\x80\x1e\x0f\x00\x0f\xff\x00\x07\xfc'\ +b'\x00\x01\xf8\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\x14\x00\x00\x00'\ +b'\x00\x01\xf8\x00\x07\xfe\x00\x0f\xff\x00\x1f\x0f\x80\x1c\x03\x80'\ +b'\x3c\x03\xc0\x38\x01\xc0\x38\x00\x00\x70\x00\x00\x70\xf8\x00\x73'\ +b'\xfe\x00\x77\xff\x00\x7e\x0f\x80\x7c\x03\x80\x78\x03\xc0\x70\x01'\ +b'\xc0\x70\x01\xc0\x70\x01\xc0\x70\x01\xc0\x30\x01\xc0\x38\x03\x80'\ +b'\x3c\x03\x80\x1e\x0f\x80\x0f\xff\x00\x07\xfe\x00\x01\xf8\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\x14\x00\x00\x00\x00\x7f\xff\xc0\x7f'\ +b'\xff\xc0\x7f\xff\xc0\x00\x01\x80\x00\x03\x00\x00\x07\x00\x00\x0e'\ +b'\x00\x00\x1c\x00\x00\x1c\x00\x00\x38\x00\x00\x38\x00\x00\x70\x00'\ +b'\x00\x70\x00\x00\xe0\x00\x00\xe0\x00\x01\xc0\x00\x01\xc0\x00\x01'\ +b'\xc0\x00\x03\x80\x00\x03\x80\x00\x03\x80\x00\x03\x80\x00\x07\x00'\ +b'\x00\x07\x00\x00\x07\x00\x00\x07\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\x14\x00\x00\x00\x00\x01\xf8\x00\x07\xfe\x00\x0f\xff\x00'\ +b'\x0f\x0f\x00\x1e\x07\x80\x1c\x03\x80\x1c\x03\x80\x1c\x03\x80\x1e'\ +b'\x07\x80\x0f\x0f\x00\x07\xfe\x00\x03\xfc\x00\x0f\xff\x00\x1e\x07'\ +b'\x80\x1c\x03\x80\x38\x03\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x03\xc0\x1c\x03\x80\x1f\x07\x80\x0f\xff\x00\x07'\ +b'\xfe\x00\x01\xf8\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\x14\x00\x00'\ +b'\x00\x00\x01\xf0\x00\x07\xfc\x00\x0f\xfe\x00\x1f\x07\x00\x1c\x03'\ +b'\x80\x3c\x03\x80\x38\x01\x80\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x3c\x03\xc0\x1c\x03\xc0\x1e\x0f\xc0\x0f\xfd\xc0\x07'\ +b'\xf9\xc0\x01\xf1\xc0\x00\x01\xc0\x00\x03\x80\x38\x03\x80\x38\x03'\ +b'\x80\x1c\x07\x00\x1e\x0f\x00\x0f\xfe\x00\x07\xfc\x00\x03\xf0\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\x0a\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x1c\x00\x1c\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\x1c\x00\x1c\x00\x1c\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x1c\x00\x1c\x00\x1c\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\x1c\x00\x1c\x00\x1c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00'\ +b'\x18\x00\x10\x00\x00\x00\x00\x00\x15\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00'\ +b'\x01\xe0\x00\x07\xe0\x00\x3f\xc0\x00\xfe\x00\x03\xf8\x00\x1f\xc0'\ +b'\x00\x3f\x00\x00\x38\x00\x00\x3f\x00\x00\x1f\xc0\x00\x03\xf8\x00'\ +b'\x00\xfe\x00\x00\x3f\xc0\x00\x07\xe0\x00\x01\xe0\x00\x00\x20\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\x15\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'\x3f\xff\xe0\x3f\xff\xe0\x3f\xff\xe0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xff\xe0\x3f\xff\xe0\x3f\xff'\ +b'\xe0\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x20\x00\x00\x3c\x00\x00\x3f\x00\x00\x1f\xe0\x00\x03\xf8'\ +b'\x00\x00\xfe\x00\x00\x1f\xc0\x00\x07\xe0\x00\x00\xe0\x00\x07\xe0'\ +b'\x00\x1f\xc0\x00\xfe\x00\x03\xf8\x00\x1f\xe0\x00\x3f\x00\x00\x3c'\ +b'\x00\x00\x20\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\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x01\xf8'\ +b'\x00\x07\xfe\x00\x0f\xff\x00\x1f\x0f\x80\x1c\x03\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x03\x80\x00\x07\x80\x00'\ +b'\x0f\x00\x00\x1e\x00\x00\x3c\x00\x00\x78\x00\x00\x70\x00\x00\xe0'\ +b'\x00\x00\xe0\x00\x00\xe0\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\xe0\x00\x00\xe0\x00\x00\xe0\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\x25\x00\x00\x00\x00\x00\x00\x00\x01\xff\x00\x00'\ +b'\x00\x0f\xff\xc0\x00\x00\x3f\xff\xf0\x00\x00\xfe\x01\xf8\x00\x01'\ +b'\xf8\x00\x7c\x00\x03\xe0\x00\x1e\x00\x03\xc0\x00\x0f\x00\x07\x80'\ +b'\xf8\xe7\x80\x0f\x03\xfc\xe3\x80\x0e\x07\xfe\xc3\x80\x1e\x0f\x07'\ +b'\xc3\xc0\x1c\x1e\x03\xc1\xc0\x1c\x1c\x01\xc1\xc0\x3c\x38\x01\xc1'\ +b'\xc0\x38\x38\x01\x81\xc0\x38\x70\x01\x81\xc0\x38\x70\x01\x81\xc0'\ +b'\x38\x70\x01\x83\x80\x38\x70\x03\x83\x80\x38\x70\x03\x07\x80\x38'\ +b'\x70\x07\x07\x00\x38\x38\x0f\x0e\x00\x1c\x3c\x3f\x3e\x00\x1c\x1f'\ +b'\xff\xfc\x00\x1e\x0f\xe7\xf8\x00\x0e\x07\xc3\xe0\x00\x0f\x00\x00'\ +b'\x00\xe0\x07\x80\x00\x01\xc0\x07\xc0\x00\x07\x80\x03\xf0\x00\x0f'\ +b'\x00\x00\xfe\x00\x7e\x00\x00\x7f\xff\xfc\x00\x00\x1f\xff\xf0\x00'\ +b'\x00\x01\xff\x80\x00\x17\x00\x00\x00\x00\x00\x7c\x00\x00\x7c\x00'\ +b'\x00\x7c\x00\x00\xee\x00\x00\xee\x00\x01\xef\x00\x01\xc7\x00\x01'\ +b'\xc7\x00\x03\x83\x80\x03\x83\x80\x03\x83\x80\x07\x01\xc0\x07\x01'\ +b'\xc0\x0f\x01\xe0\x0e\x00\xe0\x0f\xff\xe0\x1f\xff\xf0\x1f\xff\xf0'\ +b'\x1c\x00\x70\x38\x00\x38\x38\x00\x38\x78\x00\x3c\x70\x00\x1c\x70'\ +b'\x00\x1c\xf0\x00\x1e\xe0\x00\x0e\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'\x18\x00\x00\x00\x00\x1f\xff\x00\x1f\xff\xc0\x1f\xff\xf0\x1c\x00'\ +b'\xf0\x1c\x00\x78\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38'\ +b'\x1c\x00\x70\x1c\x00\xf0\x1f\xff\xe0\x1f\xff\xe0\x1f\xff\xf0\x1c'\ +b'\x00\xf8\x1c\x00\x38\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c\x00'\ +b'\x1c\x1c\x00\x1c\x1c\x00\x38\x1c\x00\xf8\x1f\xff\xf0\x1f\xff\xe0'\ +b'\x1f\xff\x80\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\x1a\x00\x00\x00\x00'\ +b'\x00\x00\x3f\x80\x00\x00\xff\xe0\x00\x03\xff\xf0\x00\x07\xc0\xf8'\ +b'\x00\x0f\x00\x3c\x00\x0e\x00\x1c\x00\x1c\x00\x1e\x00\x1c\x00\x0e'\ +b'\x00\x3c\x00\x00\x00\x38\x00\x00\x00\x38\x00\x00\x00\x38\x00\x00'\ +b'\x00\x38\x00\x00\x00\x38\x00\x00\x00\x38\x00\x00\x00\x38\x00\x00'\ +b'\x00\x38\x00\x00\x00\x1c\x00\x07\x00\x1c\x00\x0f\x00\x1e\x00\x0e'\ +b'\x00\x0e\x00\x1e\x00\x0f\x00\x3c\x00\x07\xc0\xfc\x00\x03\xff\xf8'\ +b'\x00\x01\xff\xe0\x00\x00\x3f\x80\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\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x1f'\ +b'\xff\x80\x00\x1f\xff\xe0\x00\x1f\xff\xf0\x00\x1c\x00\xf8\x00\x1c'\ +b'\x00\x3c\x00\x1c\x00\x1c\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x0f\x00\x1c\x00\x07\x00\x1c\x00\x07\x00\x1c\x00\x07\x00\x1c'\ +b'\x00\x07\x00\x1c\x00\x07\x00\x1c\x00\x07\x00\x1c\x00\x07\x00\x1c'\ +b'\x00\x07\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x3c\x00\x1c\x00\xf8\x00\x1f\xff\xf0\x00\x1f'\ +b'\xff\xe0\x00\x1f\xff\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\x00\x00\x00\x18\x00\x00\x00\x00\x1f\xff\xf8\x1f'\ +b'\xff\xf8\x1f\xff\xf8\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00'\ +b'\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1f\xff\xf0'\ +b'\x1f\xff\xf0\x1f\xff\xf0\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c'\ +b'\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00'\ +b'\x00\x1f\xff\xfc\x1f\xff\xfc\x1f\xff\xfc\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\x16\x00\x00\x00\x00\x1f\xff\xf0\x1f\xff\xf0\x1f\xff\xf0'\ +b'\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c'\ +b'\x00\x00\x1c\x00\x00\x1c\x00\x00\x1f\xff\xc0\x1f\xff\xc0\x1f\xff'\ +b'\xc0\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00'\ +b'\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c'\ +b'\x00\x00\x1c\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\x1c\x00\x00'\ +b'\x00\x00\x00\x00\x1f\xe0\x00\x00\xff\xf8\x00\x01\xff\xfe\x00\x03'\ +b'\xe0\x3f\x00\x07\x80\x0f\x00\x0f\x00\x07\x80\x1e\x00\x03\x80\x1c'\ +b'\x00\x01\xc0\x1c\x00\x00\x00\x38\x00\x00\x00\x38\x00\x00\x00\x38'\ +b'\x00\x00\x00\x38\x00\x00\x00\x38\x01\xff\xc0\x38\x01\xff\xc0\x38'\ +b'\x01\xff\xc0\x38\x00\x01\xc0\x1c\x00\x01\xc0\x1c\x00\x01\xc0\x1e'\ +b'\x00\x01\xc0\x0f\x00\x01\xc0\x07\x80\x07\xc0\x03\xf0\x1f\xc0\x01'\ +b'\xff\xff\x00\x00\xff\xfc\x00\x00\x1f\xe0\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\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1f\xff\xfe'\ +b'\x00\x1f\xff\xfe\x00\x1f\xff\xfe\x00\x1c\x00\x0e\x00\x1c\x00\x0e'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e'\ +b'\x00\x1c\x00\x0e\x00\x1c\x00\x0e\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\x00\x00\x00\x00\x00\x09\x00\x00\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x12\x00\x00\x00\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00'\ +b'\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e'\ +b'\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00'\ +b'\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x70\x0e\x00\x70'\ +b'\x0e\x00\x70\x0e\x00\x78\x1e\x00\x3c\x3c\x00\x3f\xfc\x00\x1f\xf8'\ +b'\x00\x07\xe0\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\x18\x00\x00\x00'\ +b'\x00\x1c\x00\x1e\x1c\x00\x3c\x1c\x00\x78\x1c\x00\xf0\x1c\x01\xe0'\ +b'\x1c\x03\xc0\x1c\x07\x80\x1c\x0f\x00\x1c\x1e\x00\x1c\x3c\x00\x1c'\ +b'\x78\x00\x1c\xf8\x00\x1d\xfc\x00\x1f\xde\x00\x1f\x8e\x00\x1f\x07'\ +b'\x00\x1e\x07\x80\x1c\x03\xc0\x1c\x01\xc0\x1c\x00\xe0\x1c\x00\xf0'\ +b'\x1c\x00\x78\x1c\x00\x38\x1c\x00\x1c\x1c\x00\x1e\x1c\x00\x0f\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\x14\x00\x00\x00\x00\x1c\x00\x00\x1c'\ +b'\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00'\ +b'\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00'\ +b'\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c'\ +b'\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00'\ +b'\x00\x1f\xff\xe0\x1f\xff\xe0\x1f\xff\xe0\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\x1d\x00\x00\x00\x00\x00\x1f\x00\x07\xc0\x1f\x80\x0f\xc0'\ +b'\x1f\x80\x0f\xc0\x1f\x80\x0f\xc0\x1f\xc0\x0f\xc0\x1d\xc0\x1d\xc0'\ +b'\x1d\xc0\x1d\xc0\x1d\xc0\x1d\xc0\x1c\xe0\x39\xc0\x1c\xe0\x39\xc0'\ +b'\x1c\xe0\x39\xc0\x1c\xf0\x39\xc0\x1c\x70\x71\xc0\x1c\x70\x71\xc0'\ +b'\x1c\x70\x71\xc0\x1c\x38\xe1\xc0\x1c\x38\xe1\xc0\x1c\x38\xe1\xc0'\ +b'\x1c\x3d\xc1\xc0\x1c\x1d\xc1\xc0\x1c\x1d\xc1\xc0\x1c\x1d\xc1\xc0'\ +b'\x1c\x0f\x81\xc0\x1c\x0f\x81\xc0\x1c\x0f\x81\xc0\x1c\x0f\x01\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x1a\x00\x00\x00\x00\x00\x1e\x00\x0e\x00\x1e\x00\x0e\x00\x1f\x00'\ +b'\x0e\x00\x1f\x80\x0e\x00\x1f\x80\x0e\x00\x1f\xc0\x0e\x00\x1d\xc0'\ +b'\x0e\x00\x1c\xe0\x0e\x00\x1c\xf0\x0e\x00\x1c\x70\x0e\x00\x1c\x38'\ +b'\x0e\x00\x1c\x3c\x0e\x00\x1c\x1c\x0e\x00\x1c\x0e\x0e\x00\x1c\x0f'\ +b'\x0e\x00\x1c\x07\x0e\x00\x1c\x03\x8e\x00\x1c\x03\xce\x00\x1c\x01'\ +b'\xce\x00\x1c\x00\xee\x00\x1c\x00\xfe\x00\x1c\x00\x7e\x00\x1c\x00'\ +b'\x7e\x00\x1c\x00\x3e\x00\x1c\x00\x1e\x00\x1c\x00\x1e\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\x1c\x00'\ +b'\x00\x00\x00\x00\x00\x3f\xc0\x00\x00\xff\xf0\x00\x03\xff\xf8\x00'\ +b'\x07\xe0\x7e\x00\x07\x80\x1e\x00\x0f\x00\x0f\x00\x1e\x00\x07\x80'\ +b'\x1c\x00\x03\x80\x1c\x00\x03\x80\x38\x00\x01\xc0\x38\x00\x01\xc0'\ +b'\x38\x00\x01\xc0\x38\x00\x01\xc0\x38\x00\x01\xc0\x38\x00\x01\xc0'\ +b'\x38\x00\x01\xc0\x38\x00\x01\xc0\x1c\x00\x03\x80\x1c\x00\x03\x80'\ +b'\x1e\x00\x07\x80\x0f\x00\x0f\x00\x07\x80\x1e\x00\x07\xe0\x7e\x00'\ +b'\x01\xff\xfc\x00\x00\xff\xf0\x00\x00\x3f\xc0\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\x18\x00\x00\x00'\ +b'\x00\x1f\xff\x80\x1f\xff\xe0\x1f\xff\xf0\x1c\x00\x78\x1c\x00\x38'\ +b'\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c\x00\x1c\x1c'\ +b'\x00\x38\x1c\x00\xf8\x1f\xff\xf0\x1f\xff\xe0\x1f\xff\x80\x1c\x00'\ +b'\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00'\ +b'\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\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\x1c\x00\x00\x00\x00\x00\x00\x3f\x80'\ +b'\x00\x00\xff\xe0\x00\x03\xff\xf8\x00\x07\xe0\xfc\x00\x0f\x80\x3e'\ +b'\x00\x0e\x00\x1e\x00\x1e\x00\x0f\x00\x1c\x00\x07\x00\x1c\x00\x07'\ +b'\x00\x38\x00\x03\x80\x38\x00\x03\x80\x38\x00\x03\x80\x38\x00\x03'\ +b'\x80\x38\x00\x03\x80\x38\x00\x03\x80\x38\x00\x03\x80\x38\x00\x03'\ +b'\x80\x1c\x00\x07\x00\x1c\x02\x07\x00\x1e\x03\x8f\x00\x0e\x03\xfe'\ +b'\x00\x0f\x81\xfe\x00\x07\xe0\xfc\x00\x03\xff\xfe\x00\x00\xff\xff'\ +b'\x80\x00\x3f\xcf\xc0\x00\x00\x03\x80\x00\x00\x00\x80\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\x1a\x00\x00\x00\x00\x00\x1f\xff\xc0\x00\x1f'\ +b'\xff\xf0\x00\x1f\xff\xf8\x00\x1c\x00\x7c\x00\x1c\x00\x1e\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x1e\x00\x1c\x00\x7c\x00\x1f\xff\xf8\x00\x1f\xff\xf0\x00\x1f'\ +b'\xff\xc0\x00\x1c\x0f\x00\x00\x1c\x07\xc0\x00\x1c\x03\xc0\x00\x1c'\ +b'\x01\xe0\x00\x1c\x00\xf0\x00\x1c\x00\xf0\x00\x1c\x00\x78\x00\x1c'\ +b'\x00\x3c\x00\x1c\x00\x3c\x00\x1c\x00\x1e\x00\x1c\x00\x1e\x00\x1c'\ +b'\x00\x0f\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\x18\x00\x00\x00\x00\x00\xfe\x00\x03\xff\xc0\x07\xff'\ +b'\xe0\x0f\x81\xf0\x1e\x00\xf0\x1c\x00\x78\x1c\x00\x38\x1c\x00\x38'\ +b'\x1e\x00\x00\x0f\x00\x00\x0f\xe0\x00\x07\xfe\x00\x01\xff\xc0\x00'\ +b'\x7f\xf0\x00\x07\xf8\x00\x00\xf8\x00\x00\x3c\x38\x00\x1c\x38\x00'\ +b'\x1c\x3c\x00\x1c\x1c\x00\x3c\x1e\x00\x38\x0f\x80\xf8\x07\xff\xf0'\ +b'\x03\xff\xe0\x00\x7f\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\x15\x00'\ +b'\x00\x00\x00\x7f\xff\xf0\x7f\xff\xf0\x7f\xff\xf0\x00\x70\x00\x00'\ +b'\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70'\ +b'\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00'\ +b'\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00'\ +b'\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70'\ +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\x1a\x00\x00\x00\x00\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1c'\ +b'\x00\x0e\x00\x1c\x00\x0e\x00\x1c\x00\x0e\x00\x1e\x00\x1e\x00\x0e'\ +b'\x00\x1c\x00\x0f\x00\x3c\x00\x07\xc0\xf8\x00\x07\xff\xf0\x00\x01'\ +b'\xff\xe0\x00\x00\x7f\x80\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\x00\x00\x00\x18\x00\x00\x00\x00\x70\x00\x07\x78'\ +b'\x00\x0f\x38\x00\x0e\x38\x00\x0e\x3c\x00\x1e\x1c\x00\x1c\x1e\x00'\ +b'\x3c\x0e\x00\x38\x0e\x00\x38\x0f\x00\x78\x07\x00\x70\x07\x00\x70'\ +b'\x03\x80\xe0\x03\x80\xe0\x03\xc1\xe0\x01\xc1\xc0\x01\xc1\xc0\x01'\ +b'\xe3\xc0\x00\xe3\x80\x00\xe7\x80\x00\x77\x00\x00\x77\x00\x00\x7f'\ +b'\x00\x00\x3e\x00\x00\x3e\x00\x00\x3e\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\x24\x00\x00\x00\x00\x00\x00\xe0\x01\xf0\x00\xe0\xe0\x01'\ +b'\xf0\x00\xe0\x70\x01\xf0\x01\xc0\x70\x03\xb8\x01\xc0\x70\x03\xb8'\ +b'\x01\xc0\x70\x03\xb8\x01\xc0\x38\x07\x1c\x03\x80\x38\x07\x1c\x03'\ +b'\x80\x38\x07\x1c\x03\x80\x38\x0f\x1e\x03\x80\x1c\x0e\x0e\x07\x00'\ +b'\x1c\x0e\x0e\x07\x00\x1c\x1e\x0e\x07\x00\x1c\x1c\x07\x07\x00\x0e'\ +b'\x1c\x07\x0e\x00\x0e\x1c\x07\x0e\x00\x0e\x38\x03\x8e\x00\x0e\x38'\ +b'\x03\x8e\x00\x0f\x38\x03\x9e\x00\x07\x78\x01\xdc\x00\x07\x70\x01'\ +b'\xdc\x00\x07\x70\x01\xdc\x00\x07\xf0\x01\xfc\x00\x03\xe0\x00\xf8'\ +b'\x00\x03\xe0\x00\xf8\x00\x03\xe0\x00\xf8\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x17\x00\x00\x00\x00\x78\x00\x3c\x3c\x00\x78\x1c\x00'\ +b'\x70\x1e\x00\xf0\x0f\x01\xe0\x07\x01\xc0\x03\x83\x80\x03\xc7\x80'\ +b'\x01\xef\x00\x00\xee\x00\x00\xfe\x00\x00\x7c\x00\x00\x38\x00\x00'\ +b'\x7c\x00\x00\xfe\x00\x01\xef\x00\x01\xc7\x00\x03\xc7\x80\x07\x83'\ +b'\xc0\x07\x01\xc0\x0e\x00\xe0\x1e\x00\xf0\x3c\x00\x78\x38\x00\x38'\ +b'\x78\x00\x3c\xf0\x00\x1e\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\x17\x00'\ +b'\x00\x00\x00\xf0\x00\x1e\x70\x00\x1c\x38\x00\x38\x3c\x00\x78\x1c'\ +b'\x00\x70\x0e\x00\xe0\x0f\x01\xe0\x07\x01\xc0\x03\x83\x80\x03\xc7'\ +b'\x80\x01\xc7\x00\x00\xee\x00\x00\xfe\x00\x00\x7c\x00\x00\x38\x00'\ +b'\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\ +b'\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38'\ +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\x16\x00\x00\x00\x00\x1f\xff'\ +b'\xf0\x1f\xff\xf0\x1f\xff\xf0\x00\x00\xf0\x00\x01\xe0\x00\x03\xc0'\ +b'\x00\x03\x80\x00\x07\x80\x00\x0f\x00\x00\x1e\x00\x00\x1c\x00\x00'\ +b'\x38\x00\x00\x78\x00\x00\xf0\x00\x01\xe0\x00\x01\xc0\x00\x03\xc0'\ +b'\x00\x07\x80\x00\x0f\x00\x00\x0e\x00\x00\x1c\x00\x00\x3c\x00\x00'\ +b'\x78\x00\x00\x7f\xff\xf8\x7f\xff\xf8\x7f\xff\xf8\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\x0a\x00\x00\x00\x3f\x80\x3f\x80\x3f\x80\x38\x00'\ +b'\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00'\ +b'\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00'\ +b'\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00'\ +b'\x38\x00\x38\x00\x3f\x80\x3f\x80\x3f\x80\x00\x00\x0a\x00\x00\x00'\ +b'\xe0\x00\xe0\x00\x70\x00\x70\x00\x70\x00\x70\x00\x38\x00\x38\x00'\ +b'\x38\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x0e\x00\x0e\x00\x0e\x00'\ +b'\x0e\x00\x07\x00\x07\x00\x07\x00\x07\x80\x03\x80\x03\x80\x03\x80'\ +b'\x01\xc0\x01\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0a\x00\x00\x00\x7f\x00\x7f\x00\x7f\x00\x07\x00'\ +b'\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x07\x00\x7f\x00\x7f\x00\x7f\x00\x00\x00\x11\x00\x00\x00'\ +b'\x00\x01\xc0\x00\x03\xe0\x00\x03\xe0\x00\x07\x70\x00\x07\x70\x00'\ +b'\x06\x30\x00\x0e\x38\x00\x0e\x38\x00\x1c\x1c\x00\x1c\x1c\x00\x38'\ +b'\x0e\x00\x38\x0e\x00\x38\x0e\x00\x70\x07\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\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\x00\x00\x00\x15\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xf8\xff\xff\xf8'\ +b'\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0c\x00\x00\x00\x78\x00\x3c\x00\x1c\x00\x0c\x00\x06\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\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\x01\xfc\x00\x07\xff\x00\x0f\xff\x80\x1e\x07\xc0'\ +b'\x3c\x01\xc0\x38\x01\xc0\x00\x01\xc0\x00\x07\xc0\x01\xff\xc0\x0f'\ +b'\xff\xc0\x1f\xf9\xc0\x1e\x01\xc0\x38\x01\xc0\x38\x03\xc0\x38\x07'\ +b'\xc0\x3c\x0f\xc0\x1f\xff\xc0\x0f\xfd\xc0\x07\xf0\xe0\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\x14\x00\x00\x00\x00\x38\x00\x00\x38\x00\x00'\ +b'\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38'\ +b'\xf8\x00\x3b\xfe\x00\x3f\xff\x00\x3f\x0f\x80\x3e\x07\x80\x3c\x03'\ +b'\x80\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x01\xc0\x3c\x03\x80\x3c\x07\x80\x3f\x0f\x00\x3f'\ +b'\xff\x00\x3b\xfe\x00\x38\xf8\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'\x12\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\x01\xf0\x00\x07\xfc\x00'\ +b'\x0f\xfe\x00\x1e\x0f\x00\x3c\x07\x80\x38\x03\x80\x70\x00\x00\x70'\ +b'\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x00\x00\x70\x03'\ +b'\x80\x38\x03\x80\x3c\x07\x00\x3e\x0f\x00\x1f\xfe\x00\x0f\xfc\x00'\ +b'\x03\xf0\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\x14\x00\x00\x00\x00'\ +b'\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00'\ +b'\x01\xc0\x00\x01\xc0\x01\xf1\xc0\x07\xfd\xc0\x0f\xff\xc0\x1f\x0f'\ +b'\xc0\x1c\x07\xc0\x1c\x03\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x1c\x03\xc0\x1e'\ +b'\x07\xc0\x0f\x0f\xc0\x0f\xff\xc0\x07\xfd\xc0\x01\xf1\xc0\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\x14\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'\x01\xf8\x00\x07\xfe\x00\x0f\xff\x00\x0f\x0f\x80\x1c\x03\x80\x18'\ +b'\x01\x80\x38\x01\xc0\x3f\xff\xc0\x3f\xff\xc0\x3f\xff\xc0\x38\x00'\ +b'\x00\x38\x00\x00\x38\x00\x00\x1c\x01\xc0\x1e\x03\x80\x1f\x07\x80'\ +b'\x0f\xff\x00\x07\xfe\x00\x01\xf8\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\x0b\x00\x00\x00\x07\xe0\x0f\xe0\x1f\xe0\x1e\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\xff\xc0\xff\xc0\xff\xc0\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\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\x01\xf1\xc0\x07\xfd\xc0\x0f\xff\xc0\x0f\x0f\xc0\x1e'\ +b'\x07\xc0\x1c\x03\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01'\ +b'\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x1c\x03\xc0\x1e\x07\xc0'\ +b'\x0f\x0f\xc0\x0f\xff\xc0\x07\xfd\xc0\x01\xf9\xc0\x00\x01\xc0\x38'\ +b'\x01\xc0\x3c\x03\x80\x3e\x07\x80\x1f\xff\x00\x0f\xfe\x00\x03\xf8'\ +b'\x00\x00\x00\x00\x13\x00\x00\x00\x00\x38\x00\x00\x38\x00\x00\x38'\ +b'\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\xf8'\ +b'\x00\x3b\xfe\x00\x3f\xff\x00\x3f\x0f\x80\x3c\x07\x80\x3c\x03\x80'\ +b'\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38'\ +b'\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03'\ +b'\x80\x38\x03\x80\x38\x03\x80\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\x07'\ +b'\x00\x00\x38\x38\x38\x00\x00\x00\x00\x38\x38\x38\x38\x38\x38\x38'\ +b'\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0b\x00\x00\x00\x07\x00\x07\x00\x07\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00'\ +b'\x07\x00\x0f\x00\x7e\x00\x7e\x00\xfc\x00\x00\x00\x13\x00\x00\x00'\ +b'\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00\x1c\x00\x00'\ +b'\x1c\x00\x00\x1c\x00\x00\x1c\x03\xc0\x1c\x07\x80\x1c\x0f\x00\x1c'\ +b'\x1e\x00\x1c\x7c\x00\x1c\xf8\x00\x1d\xe0\x00\x1f\xc0\x00\x1f\xe0'\ +b'\x00\x1f\xf0\x00\x1e\x78\x00\x1c\x78\x00\x1c\x3c\x00\x1c\x1e\x00'\ +b'\x1c\x0f\x00\x1c\x0f\x00\x1c\x07\x80\x1c\x03\xc0\x1c\x01\xe0\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\x07\x00\x00\x38\x38\x38\x38\x38\x38'\ +b'\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38\x38'\ +b'\x38\x38\x38\x38\x00\x00\x00\x00\x00\x00\x00\x00\x1f\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\x1c\x7c'\ +b'\x0f\x80\x1d\xfe\x3f\xc0\x1d\xff\x7f\xe0\x1f\x87\xe0\xf0\x1e\x07'\ +b'\xc0\x70\x1e\x03\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\x1c\x03'\ +b'\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\x1c\x03'\ +b'\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\x1c\x03'\ +b'\x80\x70\x1c\x03\x80\x70\x1c\x03\x80\x70\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\x00\x00\x00\x00\x00\x00\x13\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\x38\xf8\x00\x3b\xfe\x00\x3f\xff\x00\x3f\x0f\x80'\ +b'\x3c\x07\x80\x3c\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38'\ +b'\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03'\ +b'\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\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\x14\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\x01'\ +b'\xf8\x00\x07\xfe\x00\x0f\xff\x00\x1f\x0f\x80\x1e\x07\x80\x1c\x03'\ +b'\x80\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x01\xc0\x1c\x03\x80\x1e\x07\x80\x1f\x0f\x80\x0f'\ +b'\xff\x00\x07\xfe\x00\x01\xf8\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'\x14\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\x38\xf8\x00\x3b\xfe\x00'\ +b'\x3b\xff\x00\x3f\x0f\x80\x3e\x07\x80\x3c\x03\x80\x38\x01\xc0\x38'\ +b'\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01'\ +b'\xc0\x3c\x03\x80\x3c\x07\x80\x3f\x0f\x00\x3f\xff\x00\x3b\xfc\x00'\ +b'\x38\xf8\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38'\ +b'\x00\x00\x38\x00\x00\x38\x00\x00\x00\x00\x00\x14\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\x01\xf1\xc0\x07\xfd\xc0\x0f\xfd\xc0\x1f\x0f'\ +b'\xc0\x1e\x07\xc0\x1c\x03\xc0\x38\x03\xc0\x38\x01\xc0\x38\x01\xc0'\ +b'\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x38\x01\xc0\x1c\x03\xc0\x1e'\ +b'\x07\xc0\x0f\x0f\xc0\x0f\xff\xc0\x03\xfd\xc0\x01\xf1\xc0\x00\x01'\ +b'\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0\x00\x01\xc0'\ +b'\x00\x01\xc0\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x39\xf0\x3b\xf0\x3f\xe0\x3e\x00'\ +b'\x3c\x00\x3c\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00'\ +b'\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x38\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\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\x07\xf0\x00\x1f\xfc\x00\x3f\xfe'\ +b'\x00\x78\x0f\x00\x70\x07\x00\x70\x00\x00\x78\x00\x00\x3f\x00\x00'\ +b'\x3f\xf0\x00\x0f\xfe\x00\x01\xff\x00\x00\x1f\x80\x00\x07\x80\x70'\ +b'\x03\x80\x78\x03\x80\x3c\x0f\x00\x3f\xff\x00\x0f\xfe\x00\x03\xf8'\ +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\x0a\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\xff\xc0\xff\xc0\xff'\ +b'\xc0\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1f\xc0\x0f\xc0\x07'\ +b'\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x13\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\x38\x03\x80\x38\x03'\ +b'\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80'\ +b'\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38\x03\x80\x38'\ +b'\x03\x80\x38\x07\x80\x3c\x07\x80\x3e\x1f\x80\x1f\xfb\x80\x0f\xf3'\ +b'\x80\x03\xe3\x80\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\x11\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\xe0\x03\x80\x70\x07\x00\x70\x07\x00\x70'\ +b'\x07\x00\x38\x0e\x00\x38\x0e\x00\x3c\x1e\x00\x1c\x1c\x00\x1c\x1c'\ +b'\x00\x0e\x38\x00\x0e\x38\x00\x0e\x38\x00\x07\x70\x00\x07\x70\x00'\ +b'\x07\x70\x00\x03\xe0\x00\x03\xe0\x00\x01\xc0\x00\x01\xc0\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\x1b\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\x00\x00\x00\x00\x00\x70\x0e\x01\xc0\x70\x0e\x01'\ +b'\xc0\x70\x1f\x01\xc0\x38\x1f\x03\x80\x38\x1b\x03\x80\x38\x1b\x07'\ +b'\x80\x1c\x3b\x87\x00\x1c\x39\x87\x00\x0e\x31\x8e\x00\x0e\x31\x8e'\ +b'\x00\x0e\x31\x8e\x00\x07\x71\xdc\x00\x07\x60\xdc\x00\x07\x60\xdc'\ +b'\x00\x03\x60\xd8\x00\x03\xe0\xf8\x00\x03\xc0\x70\x00\x01\xc0\x70'\ +b'\x00\x01\xc0\x70\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\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\xf0\x07\x70\x0e\x38\x1e\x3c\x1c\x1c'\ +b'\x38\x0e\x78\x0f\x70\x07\xe0\x03\xe0\x03\xc0\x03\xc0\x07\xe0\x0f'\ +b'\xf0\x0e\x70\x1e\x38\x3c\x3c\x38\x1c\x70\x0e\xf0\x0f\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\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\xe0\x03\x80\x70\x07\x80\x70\x07\x00'\ +b'\x78\x07\x00\x38\x0f\x00\x38\x0e\x00\x3c\x0e\x00\x1c\x1c\x00\x1c'\ +b'\x1c\x00\x0e\x3c\x00\x0e\x38\x00\x0f\x38\x00\x07\x78\x00\x07\x70'\ +b'\x00\x07\xf0\x00\x03\xe0\x00\x03\xe0\x00\x01\xe0\x00\x01\xc0\x00'\ +b'\x01\xc0\x00\x03\xc0\x00\x03\x80\x00\x07\x80\x00\x3f\x00\x00\x3e'\ +b'\x00\x00\x3c\x00\x00\x00\x00\x00\x11\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\x7f\xff\x00\x7f\xff\x00\x7f\xff\x00\x00\x1e\x00\x00\x3c'\ +b'\x00\x00\x78\x00\x00\x70\x00\x00\xf0\x00\x01\xe0\x00\x03\xc0\x00'\ +b'\x07\x80\x00\x0f\x00\x00\x1e\x00\x00\x3c\x00\x00\x78\x00\x00\x70'\ +b'\x00\x00\xff\xff\x00\xff\xff\x00\xff\xff\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\x0c\x00\x00\x00\x00\xf0\x03\xf0\x03\xf0\x07\x80\x07'\ +b'\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07'\ +b'\x00\x0e\x00\x1e\x00\x7c\x00\x70\x00\x7c\x00\x1e\x00\x0e\x00\x07'\ +b'\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07\x00\x07'\ +b'\x00\x07\x80\x03\xf0\x03\xf0\x00\xf0\x00\x00\x09\x00\x00\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c\x00\x1c'\ +b'\x00\x1c\x00\x0c\x00\x00\x00\xf0\x00\xfc\x00\xfc\x00\x1e\x00\x0e'\ +b'\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e'\ +b'\x00\x07\x00\x07\x80\x03\xe0\x00\xe0\x03\xe0\x07\x80\x0f\x00\x0e'\ +b'\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e\x00\x0e'\ +b'\x00\x1e\x00\xfc\x00\xfc\x00\xf0\x00\x00\x00\x15\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\x07\x80'\ +b'\x00\x1f\xf0\x10\x3f\xfc\x70\x38\x7f\xf0\x20\x1f\xe0\x00\x07\x80'\ +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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\x6b\x00\xb3\x00\xfb\x00\x43\x01\xae\x01\x19\x02\xa7\x02'\ +b'\x12\x03\x37\x03\x7f\x03\xc7\x03\x0f\x04\x7a\x04\xc2\x04\x0a\x05'\ +b'\x52\x05\x9a\x05\x05\x06\x70\x06\xdb\x06\x46\x07\xb1\x07\x1c\x08'\ +b'\x87\x08\xf2\x08\x5d\x09\xc8\x09\x10\x0a\x58\x0a\xc3\x0a\x2e\x0b'\ +b'\x99\x0b\x04\x0c\xb5\x0c\x20\x0d\x8b\x0d\x19\x0e\xa7\x0e\x12\x0f'\ +b'\x7d\x0f\x0b\x10\x99\x10\xe1\x10\x4c\x11\xb7\x11\x22\x12\xb0\x12'\ +b'\x3e\x13\xcc\x13\x37\x14\xc5\x14\x53\x15\xbe\x15\x29\x16\xb7\x16'\ +b'\x22\x17\xd3\x17\x3e\x18\xa9\x18\x14\x19\x5c\x19\xa4\x19\xec\x19'\ +b'\x57\x1a\xc2\x1a\x0a\x1b\x75\x1b\xe0\x1b\x4b\x1c\xb6\x1c\x21\x1d'\ +b'\x69\x1d\xd4\x1d\x3f\x1e\x64\x1e\xac\x1e\x17\x1f\x3c\x1f\xca\x1f'\ +b'\x35\x20\xa0\x20\x0b\x21\x76\x21\xbe\x21\x29\x22\x71\x22\xdc\x22'\ +b'\x47\x23\xd5\x23\x1d\x24\x88\x24\xf3\x24\x3b\x25\x83\x25\xcb\x25'\ +b'\x36\x26' + +_mvfont = memoryview(_font) +_mvi = memoryview(_index) +ifb = lambda l : l[0] | (l[1] << 8) + +def get_ch(ch): + oc = ord(ch) + ioff = 2 * (oc - 32 + 1) if oc >= 32 and oc <= 126 else 0 + doff = ifb(_mvi[ioff : ]) + width = ifb(_mvfont[doff : ]) + + next_offs = doff + 2 + ((width - 1)//8 + 1) * 35 + return _mvfont[doff + 2:next_offs], 35, width + diff --git a/gui/fonts/arial_50.py b/gui/fonts/arial_50.py new file mode 100644 index 0000000..8646f3b --- /dev/null +++ b/gui/fonts/arial_50.py @@ -0,0 +1,232 @@ +# Code generated by font_to_py.py. +# Font: Arial.ttf Char set: 0123456789: +# Cmd: ./font_to_py.py Arial.ttf 50 arial_50.py -x -c 0123456789: +version = '0.33' + +def height(): + return 50 + +def baseline(): + return 49 + +def max_width(): + return 37 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 48 + +def max_ch(): + return 63 + +_font =\ +b'\x25\x00\x00\x03\xfe\x00\x00\x00\x1f\xff\xc0\x00\x00\x7f\xff\xf0'\ +b'\x00\x00\xff\xff\xf8\x00\x01\xff\xff\xfc\x00\x03\xff\xff\xfe\x00'\ +b'\x07\xfe\x03\xff\x00\x07\xf8\x00\xff\x80\x0f\xf0\x00\x7f\x80\x0f'\ +b'\xe0\x00\x3f\x80\x0f\xc0\x00\x1f\xc0\x1f\xc0\x00\x1f\xc0\x1f\xc0'\ +b'\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x03\x80\x00\x0f\xc0\x00\x00\x00'\ +b'\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x1f\x80\x00\x00\x00\x3f'\ +b'\x80\x00\x00\x00\x7f\x00\x00\x00\x00\xff\x00\x00\x00\x01\xfe\x00'\ +b'\x00\x00\x03\xfc\x00\x00\x00\x07\xf8\x00\x00\x00\x0f\xf0\x00\x00'\ +b'\x00\x1f\xe0\x00\x00\x00\x3f\xc0\x00\x00\x00\x7f\x80\x00\x00\x00'\ +b'\x7f\x00\x00\x00\x00\xfe\x00\x00\x00\x00\xfc\x00\x00\x00\x01\xfc'\ +b'\x00\x00\x00\x01\xfc\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf8\x00'\ +b'\x00\x00\x01\xf8\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf8\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\x01\xf8\x00\x00\x00\x01'\ +b'\xf8\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf8'\ +b'\x00\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x00\x00\x25\x00\x00\x00'\ +b'\x00\x00\x00\x00\x03\xfe\x00\x00\x00\x0f\xff\x80\x00\x00\x3f\xff'\ +b'\xe0\x00\x00\x7f\xff\xf0\x00\x00\xff\xff\xf8\x00\x01\xff\xff\xfc'\ +b'\x00\x03\xfe\x07\xfc\x00\x03\xfc\x01\xfe\x00\x07\xf0\x00\xfe\x00'\ +b'\x07\xf0\x00\x7f\x00\x07\xe0\x00\x3f\x00\x0f\xe0\x00\x3f\x00\x0f'\ +b'\xc0\x00\x1f\x80\x0f\xc0\x00\x1f\x80\x0f\xc0\x00\x1f\x80\x0f\xc0'\ +b'\x00\x1f\x80\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00'\ +b'\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f'\ +b'\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0'\ +b'\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f'\ +b'\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80'\ +b'\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x0f\xc0\x00\x1f\x80\x0f\xc0\x00'\ +b'\x1f\x80\x0f\xc0\x00\x1f\x80\x0f\xc0\x00\x1f\x80\x0f\xe0\x00\x3f'\ +b'\x80\x07\xe0\x00\x3f\x00\x07\xf0\x00\x7f\x00\x07\xf8\x00\xff\x00'\ +b'\x03\xfc\x01\xfe\x00\x03\xff\x07\xfe\x00\x01\xff\xff\xfc\x00\x00'\ +b'\xff\xff\xf8\x00\x00\x7f\xff\xf0\x00\x00\x3f\xff\xe0\x00\x00\x0f'\ +b'\xff\x80\x00\x00\x03\xfe\x00\x00\x25\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x07\x80\x00\x00\x00\x0f\x80\x00\x00\x00\x0f\x80\x00\x00\x00'\ +b'\x1f\x80\x00\x00\x00\x3f\x80\x00\x00\x00\x7f\x80\x00\x00\x00\xff'\ +b'\x80\x00\x00\x03\xff\x80\x00\x00\x07\xff\x80\x00\x00\x0f\xff\x80'\ +b'\x00\x00\x3f\xff\x80\x00\x00\xff\xdf\x80\x00\x01\xff\x9f\x80\x00'\ +b'\x01\xfe\x1f\x80\x00\x01\xfc\x1f\x80\x00\x01\xf0\x1f\x80\x00\x01'\ +b'\xc0\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00'\ +b'\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f'\ +b'\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80'\ +b'\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00'\ +b'\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00'\ +b'\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00'\ +b'\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f'\ +b'\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80'\ +b'\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00'\ +b'\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00'\ +b'\x00\x00\x00\x00\x25\x00\x00\x00\x00\x00\x00\x00\x03\xfe\x00\x00'\ +b'\x00\x1f\xff\xc0\x00\x00\x7f\xff\xe0\x00\x01\xff\xff\xf8\x00\x03'\ +b'\xff\xff\xfc\x00\x03\xff\xff\xfc\x00\x07\xfe\x07\xfe\x00\x0f\xf0'\ +b'\x00\xff\x00\x0f\xe0\x00\x7f\x00\x0f\xc0\x00\x3f\x00\x1f\xc0\x00'\ +b'\x3f\x80\x1f\x80\x00\x1f\x80\x1f\x80\x00\x1f\x80\x03\x80\x00\x1f'\ +b'\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80'\ +b'\x00\x00\x00\x3f\x00\x00\x00\x00\x3f\x00\x00\x00\x00\x7f\x00\x00'\ +b'\x00\x00\xfe\x00\x00\x00\x01\xfe\x00\x00\x00\x03\xfc\x00\x00\x00'\ +b'\x07\xf8\x00\x00\x00\x0f\xf0\x00\x00\x00\x1f\xe0\x00\x00\x00\x3f'\ +b'\xe0\x00\x00\x00\x7f\xc0\x00\x00\x00\xff\x80\x00\x00\x01\xfe\x00'\ +b'\x00\x00\x03\xfc\x00\x00\x00\x07\xf8\x00\x00\x00\x1f\xf0\x00\x00'\ +b'\x00\x3f\xe0\x00\x00\x00\x7f\xc0\x00\x00\x00\xff\x80\x00\x00\x01'\ +b'\xfe\x00\x00\x00\x03\xfc\x00\x00\x00\x07\xf8\x00\x00\x00\x07\xf0'\ +b'\x00\x00\x00\x0f\xe0\x00\x00\x00\x0f\xe0\x00\x00\x00\x1f\xff\xff'\ +b'\xff\x80\x1f\xff\xff\xff\x80\x3f\xff\xff\xff\x80\x3f\xff\xff\xff'\ +b'\x80\x3f\xff\xff\xff\x80\x3f\xff\xff\xff\x80\x00\x00\x00\x00\x00'\ +b'\x25\x00\x00\x00\x00\x00\x00\x00\x07\xfc\x00\x00\x00\x1f\xff\x80'\ +b'\x00\x00\x7f\xff\xe0\x00\x00\xff\xff\xf0\x00\x01\xff\xff\xf8\x00'\ +b'\x03\xff\xff\xfc\x00\x07\xfc\x07\xfe\x00\x0f\xf0\x01\xfe\x00\x0f'\ +b'\xe0\x00\xfe\x00\x0f\xc0\x00\x7f\x00\x1f\xc0\x00\x3f\x00\x1f\x80'\ +b'\x00\x3f\x00\x03\x80\x00\x3f\x00\x00\x00\x00\x3f\x00\x00\x00\x00'\ +b'\x3f\x00\x00\x00\x00\x7e\x00\x00\x00\x00\xfe\x00\x00\x00\x01\xfc'\ +b'\x00\x00\x00\x0f\xf8\x00\x00\x01\xff\xf0\x00\x00\x01\xff\xe0\x00'\ +b'\x00\x01\xff\xe0\x00\x00\x01\xff\xf8\x00\x00\x01\xff\xfc\x00\x00'\ +b'\x01\x8f\xfe\x00\x00\x00\x01\xff\x00\x00\x00\x00\x7f\x00\x00\x00'\ +b'\x00\x3f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\xc0\x00\x00\x00'\ +b'\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f'\ +b'\xc0\x00\x00\x00\x0f\xc0\x03\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0'\ +b'\x1f\xc0\x00\x1f\x80\x1f\xc0\x00\x1f\x80\x0f\xe0\x00\x3f\x80\x0f'\ +b'\xf0\x00\x7f\x00\x07\xf8\x00\xff\x00\x07\xfe\x03\xfe\x00\x03\xff'\ +b'\xff\xfc\x00\x01\xff\xff\xf8\x00\x00\xff\xff\xf0\x00\x00\x7f\xff'\ +b'\xe0\x00\x00\x1f\xff\x80\x00\x00\x03\xfc\x00\x00\x25\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xf0\x00\x00\x00\x03'\ +b'\xf0\x00\x00\x00\x07\xf0\x00\x00\x00\x0f\xf0\x00\x00\x00\x0f\xf0'\ +b'\x00\x00\x00\x1f\xf0\x00\x00\x00\x3f\xf0\x00\x00\x00\x7f\xf0\x00'\ +b'\x00\x00\x7f\xf0\x00\x00\x00\xff\xf0\x00\x00\x01\xff\xf0\x00\x00'\ +b'\x01\xff\xf0\x00\x00\x03\xfb\xf0\x00\x00\x07\xf3\xf0\x00\x00\x0f'\ +b'\xf3\xf0\x00\x00\x0f\xe3\xf0\x00\x00\x1f\xc3\xf0\x00\x00\x3f\x83'\ +b'\xf0\x00\x00\x7f\x83\xf0\x00\x00\x7f\x03\xf0\x00\x00\xfe\x03\xf0'\ +b'\x00\x01\xfc\x03\xf0\x00\x03\xfc\x03\xf0\x00\x03\xf8\x03\xf0\x00'\ +b'\x07\xf0\x03\xf0\x00\x0f\xf0\x03\xf0\x00\x0f\xe0\x03\xf0\x00\x1f'\ +b'\xc0\x03\xf0\x00\x3f\x80\x03\xf0\x00\x7f\x80\x03\xf0\x00\x7f\xff'\ +b'\xff\xff\xc0\x7f\xff\xff\xff\xc0\x7f\xff\xff\xff\xc0\x7f\xff\xff'\ +b'\xff\xc0\x7f\xff\xff\xff\xc0\x7f\xff\xff\xff\xc0\x00\x00\x03\xf0'\ +b'\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00'\ +b'\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00'\ +b'\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00'\ +b'\x03\xf0\x00\x00\x00\x00\x00\x00\x25\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x7f\xff\xff\x00\x00\x7f\xff\xff\x00\x00\xff'\ +b'\xff\xff\x00\x00\xff\xff\xff\x00\x00\xff\xff\xff\x00\x00\xff\xff'\ +b'\xff\x00\x00\xfc\x00\x00\x00\x01\xfc\x00\x00\x00\x01\xf8\x00\x00'\ +b'\x00\x01\xf8\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf8\x00\x00\x00'\ +b'\x03\xf8\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00\x03'\ +b'\xf0\x7f\x00\x00\x03\xf3\xff\xc0\x00\x07\xf7\xff\xf0\x00\x07\xff'\ +b'\xff\xf8\x00\x07\xff\xff\xfc\x00\x07\xff\xff\xfe\x00\x07\xfe\x03'\ +b'\xff\x00\x0f\xf8\x00\xff\x00\x0f\xf0\x00\x7f\x80\x0f\xe0\x00\x3f'\ +b'\x80\x01\xc0\x00\x1f\x80\x00\x00\x00\x1f\xc0\x00\x00\x00\x0f\xc0'\ +b'\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00'\ +b'\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00'\ +b'\x00\x0f\xc0\x1f\x80\x00\x1f\x80\x1f\x80\x00\x1f\x80\x1f\xc0\x00'\ +b'\x1f\x80\x0f\xc0\x00\x3f\x00\x0f\xe0\x00\x7f\x00\x07\xf0\x00\xfe'\ +b'\x00\x07\xfc\x03\xfe\x00\x03\xff\xff\xfc\x00\x01\xff\xff\xf8\x00'\ +b'\x00\xff\xff\xf0\x00\x00\x7f\xff\xe0\x00\x00\x1f\xff\x80\x00\x00'\ +b'\x07\xfc\x00\x00\x25\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x00'\ +b'\x00\x0f\xff\xc0\x00\x00\x3f\xff\xf0\x00\x00\x7f\xff\xf8\x00\x00'\ +b'\xff\xff\xfc\x00\x01\xff\xff\xfe\x00\x03\xff\x03\xfe\x00\x03\xf8'\ +b'\x00\xff\x00\x07\xf0\x00\x7f\x00\x07\xf0\x00\x3f\x00\x0f\xe0\x00'\ +b'\x3f\x80\x0f\xc0\x00\x1f\x80\x0f\xc0\x00\x00\x00\x1f\x80\x00\x00'\ +b'\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00'\ +b'\x1f\x00\xff\x00\x00\x3f\x07\xff\xc0\x00\x3f\x0f\xff\xf0\x00\x3f'\ +b'\x3f\xff\xf8\x00\x3f\x7f\xff\xfc\x00\x3f\x7f\xff\xfe\x00\x3f\xfe'\ +b'\x03\xff\x00\x3f\xf0\x00\xff\x00\x3f\xe0\x00\x7f\x80\x3f\xc0\x00'\ +b'\x3f\x80\x3f\x80\x00\x1f\x80\x3f\x80\x00\x1f\xc0\x3f\x00\x00\x0f'\ +b'\xc0\x3f\x00\x00\x0f\xc0\x3f\x00\x00\x0f\xc0\x3f\x00\x00\x0f\xc0'\ +b'\x1f\x00\x00\x0f\xc0\x1f\x00\x00\x0f\xc0\x1f\x00\x00\x0f\xc0\x1f'\ +b'\x80\x00\x1f\xc0\x1f\x80\x00\x1f\x80\x0f\xc0\x00\x1f\x80\x0f\xc0'\ +b'\x00\x3f\x80\x07\xe0\x00\x7f\x00\x07\xf8\x00\xff\x00\x03\xfe\x03'\ +b'\xfe\x00\x01\xff\xff\xfc\x00\x01\xff\xff\xfc\x00\x00\x7f\xff\xf8'\ +b'\x00\x00\x3f\xff\xe0\x00\x00\x0f\xff\xc0\x00\x00\x01\xfe\x00\x00'\ +b'\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff'\ +b'\xc0\x1f\xff\xff\xff\xc0\x1f\xff\xff\xff\xc0\x1f\xff\xff\xff\xc0'\ +b'\x1f\xff\xff\xff\xc0\x1f\xff\xff\xff\x80\x00\x00\x00\x0f\x80\x00'\ +b'\x00\x00\x1f\x00\x00\x00\x00\x3e\x00\x00\x00\x00\x7c\x00\x00\x00'\ +b'\x00\xfc\x00\x00\x00\x01\xf8\x00\x00\x00\x01\xf0\x00\x00\x00\x03'\ +b'\xf0\x00\x00\x00\x07\xe0\x00\x00\x00\x07\xc0\x00\x00\x00\x0f\xc0'\ +b'\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x3f\x00\x00'\ +b'\x00\x00\x3f\x00\x00\x00\x00\x7e\x00\x00\x00\x00\x7e\x00\x00\x00'\ +b'\x00\xfc\x00\x00\x00\x00\xfc\x00\x00\x00\x01\xf8\x00\x00\x00\x01'\ +b'\xf8\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0\x00\x00\x00\x03\xf0'\ +b'\x00\x00\x00\x07\xe0\x00\x00\x00\x07\xe0\x00\x00\x00\x07\xe0\x00'\ +b'\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00\x00\x0f\xc0\x00\x00'\ +b'\x00\x0f\xc0\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00'\ +b'\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x3f'\ +b'\x00\x00\x00\x00\x3f\x00\x00\x00\x00\x3f\x00\x00\x00\x00\x3f\x00'\ +b'\x00\x00\x00\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x25\x00\x00\x00'\ +b'\x00\x00\x00\x00\x03\xfe\x00\x00\x00\x1f\xff\x80\x00\x00\x3f\xff'\ +b'\xe0\x00\x00\x7f\xff\xf0\x00\x00\xff\xff\xf8\x00\x01\xff\xff\xfc'\ +b'\x00\x03\xfe\x03\xfe\x00\x03\xf8\x00\xfe\x00\x03\xf0\x00\x7e\x00'\ +b'\x07\xf0\x00\x7f\x00\x07\xe0\x00\x3f\x00\x07\xe0\x00\x3f\x00\x07'\ +b'\xe0\x00\x3f\x00\x07\xe0\x00\x3f\x00\x07\xe0\x00\x3f\x00\x07\xf0'\ +b'\x00\x7f\x00\x03\xf0\x00\x7e\x00\x03\xf8\x00\xfe\x00\x01\xfe\x03'\ +b'\xfc\x00\x00\xff\xff\xf8\x00\x00\x7f\xff\xf0\x00\x00\x1f\xff\xc0'\ +b'\x00\x00\x3f\xff\xe0\x00\x00\xff\xff\xf8\x00\x01\xff\xff\xfc\x00'\ +b'\x03\xfe\x03\xfe\x00\x07\xf8\x00\xff\x00\x07\xe0\x00\x7f\x00\x0f'\ +b'\xe0\x00\x3f\x80\x0f\xc0\x00\x1f\x80\x1f\xc0\x00\x1f\xc0\x1f\x80'\ +b'\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00'\ +b'\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\xc0\x00\x1f'\ +b'\xc0\x0f\xc0\x00\x1f\x80\x0f\xc0\x00\x3f\x80\x0f\xe0\x00\x3f\x80'\ +b'\x07\xf8\x00\xff\x00\x07\xfe\x03\xff\x00\x03\xff\xff\xfe\x00\x01'\ +b'\xff\xff\xfc\x00\x00\xff\xff\xf8\x00\x00\x7f\xff\xf0\x00\x00\x1f'\ +b'\xff\xc0\x00\x00\x03\xfe\x00\x00\x25\x00\x00\x00\x00\x00\x00\x00'\ +b'\x03\xfc\x00\x00\x00\x1f\xff\x00\x00\x00\x7f\xff\xc0\x00\x00\xff'\ +b'\xff\xf0\x00\x01\xff\xff\xf8\x00\x03\xff\xff\xfc\x00\x03\xfe\x03'\ +b'\xfc\x00\x07\xf8\x00\xfe\x00\x0f\xf0\x00\x7e\x00\x0f\xe0\x00\x3f'\ +b'\x00\x0f\xe0\x00\x1f\x00\x1f\xc0\x00\x1f\x80\x1f\xc0\x00\x0f\x80'\ +b'\x1f\x80\x00\x0f\x80\x1f\x80\x00\x0f\x80\x1f\x80\x00\x0f\xc0\x1f'\ +b'\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80\x00\x0f\xc0\x1f\x80'\ +b'\x00\x0f\xc0\x1f\xc0\x00\x1f\xc0\x0f\xc0\x00\x1f\xc0\x0f\xe0\x00'\ +b'\x3f\xc0\x0f\xf0\x00\x7f\xc0\x07\xf8\x00\xff\xc0\x07\xfe\x03\xff'\ +b'\xc0\x03\xff\xff\xff\xc0\x01\xff\xff\xef\xc0\x00\xff\xff\xcf\xc0'\ +b'\x00\x7f\xff\x0f\xc0\x00\x1f\xfe\x0f\xc0\x00\x07\xf0\x0f\xc0\x00'\ +b'\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00\x00\x1f\x80\x00\x00'\ +b'\x00\x1f\x80\x00\x00\x00\x3f\x00\x0f\xc0\x00\x3f\x00\x0f\xc0\x00'\ +b'\x3f\x00\x0f\xe0\x00\x7e\x00\x07\xe0\x00\xfe\x00\x07\xf0\x01\xfc'\ +b'\x00\x03\xfc\x07\xfc\x00\x03\xff\xff\xf8\x00\x01\xff\xff\xf0\x00'\ +b'\x00\xff\xff\xe0\x00\x00\x7f\xff\xc0\x00\x00\x1f\xff\x00\x00\x00'\ +b'\x07\xf8\x00\x00\x12\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x03\xf0\x00\x03\xf0\x00\x03\xf0\x00\x03\xf0\x00\x03\xf0\x00\x03'\ +b'\xf0\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\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\x00\x00\x00\x03\xf0\x00\x03\xf0\x00\x03\xf0\x00'\ +b'\x03\xf0\x00\x03\xf0\x00\x03\xf0\x00\x00\x00\x00' + +_index =\ +b'\x00\x00\xfc\x00\xf8\x01\xf4\x02\xf0\x03\xec\x04\xe8\x05\xe4\x06'\ +b'\xe0\x07\xdc\x08\xd8\x09\xd4\x0a\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x6c\x0b' + +_mvfont = memoryview(_font) +_mvi = memoryview(_index) +ifb = lambda l : l[0] | (l[1] << 8) + +def get_ch(ch): + oc = ord(ch) + ioff = 2 * (oc - 48 + 1) if oc >= 48 and oc <= 63 else 0 + doff = ifb(_mvi[ioff : ]) + width = ifb(_mvfont[doff : ]) + + next_offs = doff + 2 + ((width - 1)//8 + 1) * 50 + return _mvfont[doff + 2:next_offs], 50, width + diff --git a/gui/fonts/courier20.py b/gui/fonts/courier20.py new file mode 100644 index 0000000..258c09b --- /dev/null +++ b/gui/fonts/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/gui/fonts/font10.py b/gui/fonts/font10.py new file mode 100644 index 0000000..e636f3b --- /dev/null +++ b/gui/fonts/font10.py @@ -0,0 +1,296 @@ +# Code generated by font_to_py.py. +# Font: FreeSans.ttf Char set: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~£¬°Ωαβγδθλμπωϕ +# Cmd: ./font_to_py.py -xk extended FreeSans.ttf 17 font10.py +version = '0.33' + +def height(): + return 17 + +def baseline(): + return 13 + +def max_width(): + return 17 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 981 + +_font =\ +b'\x09\x00\x1e\x00\x63\x80\x61\x80\x01\x80\x01\x80\x03\x00\x06\x00'\ +b'\x04\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x06\x00\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x20\x00\x30\x00\x00\x00\x00\x06\x00\x00\x78\x78\x78'\ +b'\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x00\x00\x19\x00\x19\x00\x13\x00\x7f\x80\x12\x00\x32\x00\x32'\ +b'\x00\xff\x80\x26\x00\x24\x00\x64\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x09\x00\x08\x00\x1e\x00\x2b\x00\x69\x80\x69\x80\x68\x00\x68'\ +b'\x00\x1e\x00\x0b\x80\x09\x80\x69\x80\x6b\x00\x3e\x00\x08\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x3c\x10\x66\x20\x66\x40\x66'\ +b'\x40\x64\x80\x18\x80\x01\x3c\x02\x66\x02\x66\x04\x66\x04\x66\x08'\ +b'\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x0f\x00\x19'\ +b'\x80\x19\x80\x19\x80\x0f\x00\x0c\x00\x3a\x60\x73\x60\x61\xc0\x60'\ +b'\xc0\x71\xc0\x1e\x20\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00'\ +b'\x60\x60\x60\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x06\x00\x08\x10\x10\x30\x20\x60\x60\x60\x60\x60\x60\x20\x30\x10'\ +b'\x18\x08\x00\x06\x00\x40\x60\x20\x30\x10\x18\x18\x18\x18\x18\x18'\ +b'\x10\x30\x20\x60\x40\x00\x07\x00\x10\x54\x38\x28\x28\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x0c\x00\x0c\x00\x3f\x00\x0c'\ +b'\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x20\x20\x40\x00'\ +b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x78\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x60\x00\x00\x00\x00\x05\x00\x08\x08\x10\x10\x10\x20\x20\x20'\ +b'\x40\x40\x40\x80\x80\x00\x00\x00\x00\x09\x00\x00\x00\x1e\x00\x33'\ +b'\x00\x21\x00\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x21'\ +b'\x00\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x04\x00\x0c\x00\x3c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c'\ +b'\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x09\x00\x00\x00\x1e\x00\x33\x00\x61\x80\x61\x80\x01\x80\x03'\ +b'\x00\x06\x00\x1c\x00\x30\x00\x20\x00\x60\x00\x7f\x80\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x09\x00\x00\x00\x3e\x00\x73\x80\x61\x80\x01'\ +b'\x80\x01\x00\x0e\x00\x03\x80\x01\x80\x01\x80\x61\x80\x73\x00\x1e'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x03\x00\x03'\ +b'\x00\x07\x00\x0b\x00\x0b\x00\x13\x00\x23\x00\x23\x00\x3f\x80\x03'\ +b'\x00\x03\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x3f\x00\x20\x00\x20\x00\x40\x00\x5e\x00\x73\x00\x01\x80\x01'\ +b'\x80\x01\x80\x61\x80\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x09\x00\x00\x00\x1e\x00\x33\x00\x21\x80\x60\x00\x60\x00\x7e'\ +b'\x00\x73\x00\x61\x80\x61\x80\x61\x80\x33\x00\x1e\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x09\x00\x00\x00\xff\x00\x03\x00\x02\x00\x06'\ +b'\x00\x04\x00\x0c\x00\x08\x00\x18\x00\x18\x00\x10\x00\x30\x00\x30'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x1e\x00\x33'\ +b'\x00\x61\x80\x61\x80\x33\x00\x1e\x00\x33\x00\x61\x80\x61\x80\x61'\ +b'\x80\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x1e\x00\x33\x00\x61\x80\x61\x80\x61\x80\x33\x80\x1d\x80\x01'\ +b'\x80\x01\x80\x61\x00\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x04\x00\x00\x00\x00\x00\x60\x00\x00\x00\x00\x00\x00\x00\x60'\ +b'\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x60\x00\x00\x00\x00'\ +b'\x00\x00\x60\x20\x20\x40\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x01\x80\x07\x00\x1c\x00\x60\x00\x70\x00\x1c\x00\x03'\ +b'\x80\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\x80\x00\x00\x00'\ +b'\x00\x7f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x00\x38\x00\x0e'\ +b'\x00\x01\x80\x03\x80\x0e\x00\x70\x00\x40\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x09\x00\x1e\x00\x63\x80\x61\x80\x01\x80\x01\x80\x03'\ +b'\x00\x06\x00\x04\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x0c\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x11\x00\x03\xf0\x00\x06\x1c\x00\x18'\ +b'\x06\x00\x10\x03\x00\x31\xdb\x80\x26\x39\x80\x66\x31\x80\x6c\x31'\ +b'\x80\x6c\x31\x80\x6c\x23\x00\x6e\x67\x00\x37\xbc\x00\x18\x00\x00'\ +b'\x0c\x00\x00\x07\xf0\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x06\x00'\ +b'\x0e\x00\x0b\x00\x1b\x00\x1b\x00\x11\x80\x31\x80\x31\x80\x3f\xc0'\ +b'\x60\xc0\x60\x40\x40\x60\xc0\x60\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0b\x00\x7f\x00\x61\xc0\x60\xc0\x60\xc0\x60\xc0\x61\x80\x7f\x00'\ +b'\x60\xc0\x60\x60\x60\x60\x60\x60\x60\xc0\x7f\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0c\x00\x0f\xc0\x18\x60\x30\x30\x20\x30\x60\x00'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x20\x30\x30\x30\x18\x60\x0f\xc0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x7f\x80\x60\xc0\x60\x60'\ +b'\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x60'\ +b'\x60\xc0\x7f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x7f\x80'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x7f\x80\x60\x00\x60\x00'\ +b'\x60\x00\x60\x00\x60\x00\x7f\x80\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0a\x00\x7f\x80\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x7f\x00'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0d\x00\x07\xe0\x18\x30\x30\x18\x30\x00\x60\x00'\ +b'\x60\x00\x60\xf8\x60\x18\x60\x18\x30\x18\x30\x38\x18\x78\x07\x88'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x7f\xe0\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00\x00\x00\x00\x09'\ +b'\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03'\ +b'\x00\x03\x00\x63\x00\x63\x00\x62\x00\x3c\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x60\x60\x60\xc0\x61\x80\x63\x00\x66\x00\x6c'\ +b'\x00\x7e\x00\x73\x00\x63\x00\x61\x80\x60\xc0\x60\xc0\x60\x60\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x60\x00\x60\x00\x60\x00\x60'\ +b'\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60'\ +b'\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x70\x1c\x70'\ +b'\x1c\x78\x3c\x78\x3c\x68\x2c\x6c\x6c\x6c\x6c\x64\x4c\x66\xcc\x66'\ +b'\xcc\x62\x8c\x63\x8c\x63\x8c\x00\x00\x00\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x70\x30\x70\x30\x78\x30\x68\x30\x6c\x30\x66\x30\x62\x30\x63'\ +b'\x30\x61\xb0\x61\xb0\x60\xf0\x60\x70\x60\x70\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0d\x00\x0f\xc0\x18\x60\x30\x30\x70\x30\x60\x18\x60'\ +b'\x18\x60\x18\x60\x18\x60\x18\x70\x30\x30\x30\x18\x60\x0f\xc0\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x7f\x80\x60\xc0\x60\x60\x60'\ +b'\x60\x60\x60\x60\xc0\x7f\x80\x60\x00\x60\x00\x60\x00\x60\x00\x60'\ +b'\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x0f\xc0\x18'\ +b'\x60\x30\x30\x70\x30\x60\x18\x60\x18\x60\x18\x60\x18\x60\x18\x70'\ +b'\xb0\x30\xf0\x18\x60\x0f\xf0\x00\x10\x00\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x7f\x80\x60\xe0\x60\x60\x60\x60\x60\x60\x60\xc0\x7f\x80\x60'\ +b'\xe0\x60\x60\x60\x60\x60\x60\x60\x60\x60\x70\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x1f\x80\x30\xc0\x60\x60\x60\x00\x60\x00\x30'\ +b'\x00\x1f\x00\x03\xc0\x00\xe0\x60\x60\x60\x60\x30\xc0\x1f\x80\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x3f\xc0\x06\x00\x06\x00\x06'\ +b'\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06'\ +b'\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x30\xc0\x1f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x0b'\ +b'\x00\xc0\x60\x40\x40\x60\xc0\x60\xc0\x20\x80\x31\x80\x31\x80\x11'\ +b'\x00\x1b\x00\x0b\x00\x0a\x00\x0e\x00\x06\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x10\x00\xc1\x83\xc1\x82\x42\x86\x62\xc6\x62\xc6\x62'\ +b'\x44\x24\x44\x24\x6c\x34\x2c\x3c\x28\x18\x38\x18\x38\x18\x18\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x60\x40\x20\xc0\x31\x80\x19'\ +b'\x00\x1b\x00\x0e\x00\x06\x00\x0e\x00\x1b\x00\x11\x80\x31\x80\x60'\ +b'\xc0\x40\x60\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x40\x60\x60'\ +b'\x60\x30\xc0\x30\xc0\x19\x80\x0d\x00\x0f\x00\x06\x00\x06\x00\x06'\ +b'\x00\x06\x00\x06\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a'\ +b'\x00\x7f\xc0\x00\xc0\x01\x80\x03\x00\x03\x00\x06\x00\x0c\x00\x0c'\ +b'\x00\x18\x00\x30\x00\x30\x00\x60\x00\x7f\xc0\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x05\x00\x70\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x70\x05\x00\x80\x80\x40\x40\x40\x20\x20\x20'\ +b'\x10\x10\x10\x08\x08\x00\x00\x00\x00\x05\x00\xe0\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\xe0\x08\x00\x00\x18'\ +b'\x18\x3c\x24\x24\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a'\ +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\xff\xc0\x00\x00\x00'\ +b'\x00\x00\x00\x04\x00\xc0\x40\x20\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x3e\x00\x63\x00\x03\x00\x03\x00\x3f\x00\x63\x00\x63\x00\x67\x00'\ +b'\x3b\x80\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x60\x00\x60\x00'\ +b'\x60\x00\x60\x00\x6f\x00\x71\x80\x60\x80\xe0\x80\xe0\x80\xe0\x80'\ +b'\xe0\x80\xf1\x80\x6f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x33\x00\x61\x80\x60\x00'\ +b'\x60\x00\x60\x00\x61\x80\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0a\x00\x01\x80\x01\x80\x01\x80\x01\x80\x1d\x80\x33\x80'\ +b'\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x33\x80\x1d\x80\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x1e\x00\x33\x00\x61\x80\x61\x80\x7f\x80\x60\x00\x61\x80\x33\x00'\ +b'\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x18\x30\x30\x30'\ +b'\x78\x30\x30\x30\x30\x30\x30\x30\x30\x00\x00\x00\x00\x09\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x1d\x80\x33\x80\x61\x80\x61\x80\x61'\ +b'\x80\x61\x80\x61\x80\x33\x80\x1d\x80\x01\x80\x01\x80\x63\x00\x3e'\ +b'\x00\x09\x00\x60\x00\x60\x00\x60\x00\x60\x00\x6f\x00\x71\x80\x61'\ +b'\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x04\x00\x60\x00\x00\x00\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x00\x00\x00\x00\x04\x00\x30\x00\x00\x00\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\xe0\x09\x00\x60\x00\x60'\ +b'\x00\x60\x00\x60\x00\x63\x00\x66\x00\x6c\x00\x7c\x00\x74\x00\x66'\ +b'\x00\x63\x00\x63\x00\x61\x80\x00\x00\x00\x00\x00\x00\x00\x00\x04'\ +b'\x00\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x00\x00'\ +b'\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x6e\xf0\x73\x98'\ +b'\x63\x18\x63\x18\x63\x18\x63\x18\x63\x18\x63\x18\x63\x18\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x6f\x00\x71\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80'\ +b'\x61\x80\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x1e\x00\x33\x00\x61\x80\x61\x80\x61\x80\x61\x80'\ +b'\x61\x80\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x6f\x00\x71\x80\x60\x80\xe0\x80'\ +b'\xe0\x80\xe0\x80\xe0\x80\xf1\x80\x6f\x00\x60\x00\x60\x00\x60\x00'\ +b'\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x80\x33\x80'\ +b'\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x33\x80\x1d\x80\x01\x80'\ +b'\x01\x80\x01\x80\x00\x00\x06\x00\x00\x00\x00\x00\x6c\x70\x60\x60'\ +b'\x60\x60\x60\x60\x60\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x3e'\ +b'\x63\x60\x60\x38\x07\x63\x63\x3e\x00\x00\x00\x00\x05\x00\x00\x00'\ +b'\x30\x30\x78\x30\x30\x30\x30\x30\x30\x30\x38\x00\x00\x00\x00\x09'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x61\x80\x61\x80\x61\x80\x61'\ +b'\x80\x61\x80\x61\x80\x61\x80\x63\x80\x3d\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x08\x00\x00\x00\x00\x00\xc3\x43\x62\x66\x26\x34\x3c'\ +b'\x18\x18\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xc6\x30\x46\x30\x47\x20\x6f\x20\x69\x60\x29\x60\x29\xc0\x39\xc0'\ +b'\x10\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00'\ +b'\x42\x66\x34\x18\x18\x1c\x24\x66\x43\x00\x00\x00\x00\x08\x00\x00'\ +b'\x00\x00\x00\xc3\x42\x42\x66\x24\x24\x3c\x18\x18\x18\x10\x30\x60'\ +b'\x08\x00\x00\x00\x00\x00\x7f\x06\x04\x0c\x18\x30\x20\x60\x7f\x00'\ +b'\x00\x00\x00\x06\x00\x18\x30\x30\x30\x30\x30\x30\x70\x60\x70\x30'\ +b'\x30\x30\x30\x30\x30\x18\x04\x00\x30\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x00\x06\x00\x60\x30\x30\x30\x30'\ +b'\x30\x30\x38\x18\x38\x30\x30\x30\x30\x30\x30\x60\x09\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\x00\x4f\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x09\x00\x1f\x00\x31\x80\x61\x80\x60\x00\x60\x00\x30\x00\xfc\x00'\ +b'\x18\x00\x18\x00\x18\x00\x30\x00\x7c\x80\x47\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x3f\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00\x0e\x00\x11\x00'\ +b'\x11\x00\x11\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x0f\xc0'\ +b'\x18\x60\x30\x30\x30\x18\x60\x18\x60\x18\x60\x18\x60\x18\x60\x18'\ +b'\x30\x30\x10\x60\x08\xc0\x7c\xf8\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x80\x33\x80\x61\x80'\ +b'\x61\x80\x61\x80\x61\x80\x61\x80\x33\x80\x1f\xc0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x09\x00\x3e\x00\x23\x00\x63\x00\x63\x00\x63\x00'\ +b'\x66\x00\x6c\x00\x63\x00\x61\x80\x61\x80\x61\x80\x73\x00\x7e\x00'\ +b'\x60\x00\x60\x00\x60\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\xc1\x80\x61\x80\x21\x00\x33\x00\x32\x00\x1e\x00\x1e\x00'\ +b'\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x0c\x00\x00\x00\x09\x00\x3f\x00'\ +b'\x30\x00\x1c\x00\x0e\x00\x1f\x00\x33\x00\x61\x80\x61\x80\x61\x80'\ +b'\x61\x80\x61\x80\x33\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x09\x00\x1e\x00\x33\x00\x21\x00\x61\x80\x61\x80\x61\x80\x7f\x80'\ +b'\x61\x80\x61\x80\x61\x80\x61\x00\x33\x00\x1e\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x09\x00\x60\x00\x10\x00\x18\x00\x18\x00\x18\x00'\ +b'\x1c\x00\x34\x00\x34\x00\x26\x00\x62\x00\x62\x00\x43\x00\xc1\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80\x61\x80'\ +b'\x73\x80\x7f\x80\x60\x00\x60\x00\x60\x00\x00\x00\x0a\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x7f\xc0\x31\x80\x31\x80\x31\x80\x31\x80'\ +b'\x31\x80\x31\x80\x31\x80\x31\xc0\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x60\x30\x30\x20\x10'\ +b'\x63\x10\xe3\x10\xe3\x10\xe3\x10\xb3\x30\x1c\xe0\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0c\x00\x00\x00\x06\x00\x06\x00\x06\x00\x1f\x80'\ +b'\x36\x60\x66\x30\x66\x30\x66\x30\x66\x30\x66\x30\x36\x60\x1f\xc0'\ +b'\x06\x00\x06\x00\x06\x00\x00\x00' + +_sparse =\ +b'\x20\x00\x24\x00\x21\x00\x37\x00\x22\x00\x4a\x00\x23\x00\x5d\x00'\ +b'\x24\x00\x81\x00\x25\x00\xa5\x00\x26\x00\xc9\x00\x27\x00\xed\x00'\ +b'\x28\x00\x00\x01\x29\x00\x13\x01\x2a\x00\x26\x01\x2b\x00\x39\x01'\ +b'\x2c\x00\x5d\x01\x2d\x00\x70\x01\x2e\x00\x83\x01\x2f\x00\x96\x01'\ +b'\x30\x00\xa9\x01\x31\x00\xcd\x01\x32\x00\xf1\x01\x33\x00\x15\x02'\ +b'\x34\x00\x39\x02\x35\x00\x5d\x02\x36\x00\x81\x02\x37\x00\xa5\x02'\ +b'\x38\x00\xc9\x02\x39\x00\xed\x02\x3a\x00\x11\x03\x3b\x00\x24\x03'\ +b'\x3c\x00\x37\x03\x3d\x00\x5b\x03\x3e\x00\x7f\x03\x3f\x00\xa3\x03'\ +b'\x40\x00\xc7\x03\x41\x00\xfc\x03\x42\x00\x20\x04\x43\x00\x44\x04'\ +b'\x44\x00\x68\x04\x45\x00\x8c\x04\x46\x00\xb0\x04\x47\x00\xd4\x04'\ +b'\x48\x00\xf8\x04\x49\x00\x1c\x05\x4a\x00\x2f\x05\x4b\x00\x53\x05'\ +b'\x4c\x00\x77\x05\x4d\x00\x9b\x05\x4e\x00\xbf\x05\x4f\x00\xe3\x05'\ +b'\x50\x00\x07\x06\x51\x00\x2b\x06\x52\x00\x4f\x06\x53\x00\x73\x06'\ +b'\x54\x00\x97\x06\x55\x00\xbb\x06\x56\x00\xdf\x06\x57\x00\x03\x07'\ +b'\x58\x00\x27\x07\x59\x00\x4b\x07\x5a\x00\x6f\x07\x5b\x00\x93\x07'\ +b'\x5c\x00\xa6\x07\x5d\x00\xb9\x07\x5e\x00\xcc\x07\x5f\x00\xdf\x07'\ +b'\x60\x00\x03\x08\x61\x00\x16\x08\x62\x00\x3a\x08\x63\x00\x5e\x08'\ +b'\x64\x00\x82\x08\x65\x00\xa6\x08\x66\x00\xca\x08\x67\x00\xdd\x08'\ +b'\x68\x00\x01\x09\x69\x00\x25\x09\x6a\x00\x38\x09\x6b\x00\x4b\x09'\ +b'\x6c\x00\x6f\x09\x6d\x00\x82\x09\x6e\x00\xa6\x09\x6f\x00\xca\x09'\ +b'\x70\x00\xee\x09\x71\x00\x12\x0a\x72\x00\x36\x0a\x73\x00\x49\x0a'\ +b'\x74\x00\x5c\x0a\x75\x00\x6f\x0a\x76\x00\x93\x0a\x77\x00\xa6\x0a'\ +b'\x78\x00\xca\x0a\x79\x00\xdd\x0a\x7a\x00\xf0\x0a\x7b\x00\x03\x0b'\ +b'\x7c\x00\x16\x0b\x7d\x00\x29\x0b\x7e\x00\x3c\x0b\xa3\x00\x60\x0b'\ +b'\xac\x00\x84\x0b\xb0\x00\xa8\x0b\xa9\x03\xcc\x0b\xb1\x03\xf0\x0b'\ +b'\xb2\x03\x14\x0c\xb3\x03\x38\x0c\xb4\x03\x5c\x0c\xb8\x03\x80\x0c'\ +b'\xbb\x03\xa4\x0c\xbc\x03\xc8\x0c\xc0\x03\xec\x0c\xc9\x03\x10\x0d'\ +b'\xd5\x03\x34\x0d' + +_mvfont = memoryview(_font) +_mvsp = memoryview(_sparse) +ifb = lambda l : l[0] | (l[1] << 8) + +def bs(lst, val): + while True: + m = (len(lst) & ~ 7) >> 1 + v = ifb(lst[m:]) + if v == val: + return ifb(lst[m + 2:]) + if not m: + return 0 + lst = lst[m:] if v < val else lst[:m] + +def get_ch(ch): + doff = bs(_mvsp, ord(ch)) + width = ifb(_mvfont[doff : ]) + + next_offs = doff + 2 + ((width - 1)//8 + 1) * 17 + return _mvfont[doff + 2:next_offs], 17, width + diff --git a/gui/fonts/font14.py b/gui/fonts/font14.py new file mode 100644 index 0000000..5cafbb5 --- /dev/null +++ b/gui/fonts/font14.py @@ -0,0 +1,395 @@ +# Code generated by font_to_py.py. +# Font: FreeSans.ttf Char set: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~£¬°Ωαβγδθλμπωϕ +# Cmd: ./font_to_py.py -x -k extended FreeSans.ttf 23 font14.py +version = '0.33' + +def height(): + return 23 + +def baseline(): + return 18 + +def max_width(): + return 23 + +def hmap(): + return True + +def reverse(): + return False + +def monospaced(): + return False + +def min_ch(): + return 32 + +def max_ch(): + return 981 + +_font =\ +b'\x0d\x00\x00\x00\x0f\xc0\x1f\xe0\x38\x70\x30\x30\x30\x30\x00\x30'\ +b'\x00\x70\x00\x60\x01\xc0\x01\x80\x03\x00\x03\x00\x03\x00\x00\x00'\ +b'\x00\x00\x03\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x06\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\x08\x00\x00\x18\x18\x18\x18'\ +b'\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00\x00\x18\x18\x00\x00\x00'\ +b'\x00\x00\x08\x00\x00\x00\x66\x66\x66\x66\x44\x44\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00'\ +b'\x00\x04\x60\x04\x60\x0c\x60\x0c\x40\x7f\xf8\x7f\xf8\x08\xc0\x18'\ +b'\xc0\x18\xc0\x18\x80\xff\xf0\xff\xf0\x31\x80\x31\x80\x31\x00\x33'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x02\x00\x0f'\ +b'\x80\x3f\xe0\x32\x60\x62\x30\x62\x30\x62\x00\x62\x00\x3a\x00\x3f'\ +b'\x00\x0f\xe0\x02\xf0\x02\x30\x62\x30\x62\x30\x72\x70\x3f\xe0\x0f'\ +b'\x80\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x02\x00\x1e\x02\x00\x3f\x04\x00\x73\x8c\x00\x61'\ +b'\x88\x00\x73\x98\x00\x3f\x10\x00\x1e\x20\x00\x00\x23\xc0\x00\x47'\ +b'\xe0\x00\xce\x70\x00\x8c\x30\x01\x8c\x30\x01\x0e\x70\x02\x07\xe0'\ +b'\x02\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x0f\x00\x00\x00\x00\x00\x07\x80\x0f\xc0\x1c\xe0\x18\x60'\ +b'\x18\x60\x0c\xc0\x0f\x80\x0f\x00\x1b\x98\x31\xd8\x60\xf8\x60\x70'\ +b'\x60\x30\x70\xf8\x3f\xd8\x1f\x0c\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x05\x00\x00\x00\x60\x60\x60\x60\x40\x40\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x02\x04'\ +b'\x0c\x08\x18\x18\x10\x30\x30\x30\x30\x30\x30\x30\x30\x18\x18\x18'\ +b'\x0c\x0c\x04\x02\x08\x00\x00\x40\x20\x30\x10\x18\x18\x08\x0c\x0c'\ +b'\x0c\x0c\x0c\x0c\x0c\x0c\x18\x18\x18\x30\x30\x20\x40\x09\x00\x00'\ +b'\x00\x08\x00\x08\x00\x6b\x00\x3e\x00\x1c\x00\x36\x00\x24\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\x0d\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x03'\ +b'\x00\x03\x00\x03\x00\x3f\xf0\x3f\xf0\x03\x00\x03\x00\x03\x00\x03'\ +b'\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30'\ +b'\x30\x10\x10\x20\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x7c\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x30\x30\x00\x00\x00\x00\x00\x06\x00\x00\x04\x0c\x08\x08\x18'\ +b'\x10\x10\x10\x30\x20\x20\x20\x60\x40\x40\xc0\x80\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x0f\x80\x1f\xc0\x38\xe0\x30\x60\x60'\ +b'\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x60\x30\x30'\ +b'\x60\x38\xe0\x1f\xc0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x01\x00\x03\x00\x07\x00\x1f\x00\x1f'\ +b'\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03'\ +b'\x00\x03\x00\x03\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x0f\x80\x3f\xe0\x30\x70\x60\x30\x60'\ +b'\x30\x00\x30\x00\x30\x00\xe0\x01\xc0\x07\x80\x0e\x00\x18\x00\x10'\ +b'\x00\x20\x00\x3f\xf0\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x0f\xc0\x3f\xe0\x30\x70\x60\x30\x60'\ +b'\x30\x00\x70\x07\xe0\x07\xe0\x00\x70\x00\x30\x00\x30\x60\x30\x60'\ +b'\x30\x30\x60\x3f\xe0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x00\xc0\x00\xc0\x01\xc0\x03\xc0\x06'\ +b'\xc0\x04\xc0\x0c\xc0\x18\xc0\x10\xc0\x20\xc0\x3f\xf0\x3f\xf0\x00'\ +b'\xc0\x00\xc0\x00\xc0\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x3f\xe0\x3f\xe0\x30\x00\x30\x00\x30'\ +b'\x00\x37\x80\x3f\xe0\x70\xe0\x00\x70\x00\x30\x00\x30\x00\x30\x60'\ +b'\x30\x70\xe0\x3f\xc0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x07\x80\x1f\xe0\x38\x60\x30\x70\x20'\ +b'\x00\x60\x00\x67\x80\x7f\xe0\x70\x60\x60\x30\x60\x30\x60\x30\x20'\ +b'\x30\x38\x60\x1f\xe0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x7f\xe0\x7f\xe0\x00\x40\x00\xc0\x00'\ +b'\x80\x01\x80\x03\x00\x03\x00\x06\x00\x06\x00\x04\x00\x0c\x00\x0c'\ +b'\x00\x08\x00\x18\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x0f\x80\x1f\xc0\x38\xe0\x30\x60\x30'\ +b'\x60\x38\xe0\x1f\xc0\x1f\xc0\x38\xe0\x60\x30\x60\x30\x60\x30\x60'\ +b'\x30\x30\x60\x1f\xc0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x00\x00\x0f\x00\x3f\xc0\x30\xc0\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x30\xe0\x3f\xe0\x1f\x60\x00\x60\x00\x60\x60'\ +b'\xc0\x31\xc0\x3f\x80\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x06\x00\x00\x00\x00\x00\x00\x00\x30\x30\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x30\x30\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00'\ +b'\x00\x00\x30\x30\x00\x00\x00\x00\x00\x00\x00\x00\x30\x30\x10\x10'\ +b'\x20\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x10\x00\xf0\x03\xc0\x0f\x00\x78\x00\x60\x00\x3c'\ +b'\x00\x0f\x00\x03\xe0\x00\x70\x00\x10\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x7f\xf0\x7f\xf0\x00\x00\x00\x00\x7f'\ +b'\xf0\x7f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x60\x00\x78\x00\x1e\x00\x07\xc0\x00\xf0\x00\x30\x00'\ +b'\xf0\x07\x80\x1e\x00\x78\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0d\x00\x00\x00\x0f\xc0\x1f\xe0\x38\x70\x30\x30\x30'\ +b'\x30\x00\x30\x00\x70\x00\x60\x01\xc0\x01\x80\x03\x00\x03\x00\x03'\ +b'\x00\x00\x00\x00\x00\x03\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x17\x00\x00\x00\x00\x00\x7f\x00\x03\xff\xc0\x07\x81'\ +b'\xe0\x0e\x00\x70\x1c\x38\x38\x38\xfd\x98\x30\xc7\x8c\x31\x83\x0c'\ +b'\x63\x83\x0c\x63\x03\x0c\x63\x02\x0c\x63\x06\x18\x63\x06\x18\x73'\ +b'\x8e\x30\x31\xff\xf0\x38\xe3\xc0\x1c\x00\x00\x0f\x00\x00\x03\xff'\ +b'\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x03\x80'\ +b'\x03\xc0\x07\xc0\x06\xc0\x06\xe0\x0e\x60\x0c\x60\x0c\x70\x18\x30'\ +b'\x18\x30\x1f\xf8\x3f\xf8\x30\x18\x30\x1c\x70\x0c\x60\x0c\x60\x0e'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x3f\xe0'\ +b'\x3f\xf0\x30\x38\x30\x18\x30\x18\x30\x18\x30\x30\x3f\xe0\x3f\xf0'\ +b'\x30\x18\x30\x0c\x30\x0c\x30\x0c\x30\x0c\x30\x18\x3f\xf8\x3f\xf0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x03\xf0'\ +b'\x0f\xf8\x1c\x1c\x38\x0e\x30\x06\x60\x00\x60\x00\x60\x00\x60\x00'\ +b'\x60\x00\x60\x00\x60\x06\x30\x06\x38\x0c\x1c\x1c\x0f\xf8\x03\xe0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x3f\xe0'\ +b'\x3f\xf8\x30\x38\x30\x0c\x30\x0c\x30\x06\x30\x06\x30\x06\x30\x06'\ +b'\x30\x06\x30\x06\x30\x06\x30\x0c\x30\x0c\x30\x38\x3f\xf0\x3f\xe0'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x3f\xf8'\ +b'\x3f\xf8\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x3f\xf8\x3f\xf8'\ +b'\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x3f\xfc\x3f\xfc'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x3f\xf8'\ +b'\x3f\xf8\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x3f\xf0\x3f\xf0'\ +b'\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x00\x03'\ +b'\xf0\x00\x0f\xfc\x00\x1c\x0e\x00\x38\x07\x00\x30\x03\x00\x70\x00'\ +b'\x00\x60\x00\x00\x60\x00\x00\x60\x3f\x00\x60\x3f\x00\x60\x03\x00'\ +b'\x70\x03\x00\x30\x03\x00\x38\x07\x00\x1c\x1f\x00\x0f\xfb\x00\x03'\ +b'\xe1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x11\x00\x00\x00\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30'\ +b'\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x3f\xfe\x00\x3f\xfe'\ +b'\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00'\ +b'\x30\x06\x00\x30\x06\x00\x30\x06\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00\x00\x00\x00'\ +b'\x00\x0c\x00\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\x60\xc0\x60'\ +b'\xc0\x71\xc0\x3f\x80\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0f\x00\x00\x00\x30\x0e\x30\x1c\x30\x38\x30\x70\x30\xe0\x31'\ +b'\xc0\x33\x80\x37\x80\x3f\x80\x3c\xc0\x38\xe0\x30\x60\x30\x30\x30'\ +b'\x38\x30\x18\x30\x0c\x30\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x0d\x00\x00\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30'\ +b'\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30'\ +b'\x00\x30\x00\x3f\xf0\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x13\x00\x00\x00\x00\x38\x01\xc0\x38\x01\xc0\x3c\x03\xc0\x3c'\ +b'\x03\xc0\x36\x02\xc0\x36\x06\xc0\x36\x06\xc0\x33\x04\xc0\x33\x0c'\ +b'\xc0\x33\x0c\xc0\x31\x98\xc0\x31\x98\xc0\x31\x98\xc0\x30\xf0\xc0'\ +b'\x30\xf0\xc0\x30\xf0\xc0\x30\x60\xc0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x38\x06\x00'\ +b'\x38\x06\x00\x3c\x06\x00\x3c\x06\x00\x36\x06\x00\x37\x06\x00\x33'\ +b'\x06\x00\x31\x86\x00\x31\xc6\x00\x30\xc6\x00\x30\x66\x00\x30\x66'\ +b'\x00\x30\x36\x00\x30\x3e\x00\x30\x1e\x00\x30\x0e\x00\x30\x0e\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12'\ +b'\x00\x00\x00\x00\x03\xf0\x00\x0f\xfc\x00\x1c\x0e\x00\x38\x07\x00'\ +b'\x30\x03\x00\x70\x03\x80\x60\x01\x80\x60\x01\x80\x60\x01\x80\x60'\ +b'\x01\x80\x60\x01\x80\x70\x03\x80\x30\x03\x00\x38\x07\x00\x1c\x0e'\ +b'\x00\x0f\xfc\x00\x03\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x3f\xe0\x3f\xf8\x30\x18'\ +b'\x30\x0c\x30\x0c\x30\x0c\x30\x0c\x30\x1c\x3f\xf8\x3f\xf0\x30\x00'\ +b'\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x00\x03\xf0\x00\x0f\xfc'\ +b'\x00\x1c\x0e\x00\x38\x07\x00\x30\x03\x00\x70\x03\x80\x60\x01\x80'\ +b'\x60\x01\x80\x60\x01\x80\x60\x01\x80\x60\x01\x80\x70\x03\x80\x30'\ +b'\x13\x00\x38\x1f\x00\x1c\x1e\x00\x0f\xff\x00\x03\xf3\x00\x00\x01'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00'\ +b'\x00\x3f\xf0\x3f\xf8\x30\x1c\x30\x0c\x30\x0c\x30\x0c\x30\x0c\x30'\ +b'\x18\x3f\xf0\x3f\xf8\x30\x1c\x30\x0c\x30\x0c\x30\x0c\x30\x0c\x30'\ +b'\x0c\x30\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00'\ +b'\x00\x07\xe0\x1f\xf8\x18\x38\x30\x0c\x30\x0c\x30\x00\x38\x00\x1f'\ +b'\x00\x0f\xf0\x00\xf8\x00\x1c\x00\x0c\x60\x0c\x60\x0c\x38\x18\x1f'\ +b'\xf8\x07\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00'\ +b'\x00\x7f\xf8\x7f\xf8\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03'\ +b'\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03\x00\x03'\ +b'\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00'\ +b'\x00\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06'\ +b'\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00\x30\x06\x00'\ +b'\x30\x06\x00\x30\x06\x00\x30\x06\x00\x38\x0e\x00\x1c\x1c\x00\x0f'\ +b'\xf8\x00\x07\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x0f\x00\x00\x00\x60\x0c\x60\x0c\x60\x1c\x30\x18'\ +b'\x30\x18\x38\x38\x18\x30\x18\x30\x1c\x70\x0c\x60\x0c\x60\x0e\xe0'\ +b'\x06\xc0\x06\xc0\x07\xc0\x03\x80\x03\x80\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x16\x00\x00\x00\x00\xe0\x70\x18\x60\x70\x38\x60'\ +b'\x78\x38\x60\x78\x30\x70\xd8\x30\x30\xd8\x30\x30\xcc\x70\x30\xcc'\ +b'\x60\x39\x8c\x60\x19\x86\x60\x19\x86\xe0\x1b\x86\xc0\x1f\x06\xc0'\ +b'\x0f\x03\xc0\x0f\x03\xc0\x0e\x03\x80\x0e\x03\x80\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x70'\ +b'\x0c\x30\x18\x38\x38\x1c\x30\x0c\x60\x0e\xe0\x06\xc0\x03\x80\x03'\ +b'\x80\x07\xc0\x06\xc0\x0e\x60\x1c\x70\x18\x30\x38\x18\x70\x1c\x60'\ +b'\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x60'\ +b'\x0e\x70\x0c\x38\x1c\x18\x18\x1c\x38\x0c\x30\x06\x60\x07\xe0\x03'\ +b'\xc0\x03\xc0\x01\x80\x01\x80\x01\x80\x01\x80\x01\x80\x01\x80\x01'\ +b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x7f'\ +b'\xf8\x7f\xf8\x00\x38\x00\x30\x00\x70\x00\xe0\x01\xc0\x01\x80\x03'\ +b'\x00\x07\x00\x0e\x00\x1c\x00\x18\x00\x38\x00\x70\x00\x7f\xf8\x7f'\ +b'\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x78\x78'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x78\x78\x06\x00\x00\x80\xc0\x40\x40\x60\x20\x20\x20\x30'\ +b'\x10\x10\x10\x18\x08\x08\x0c\x04\x00\x00\x00\x00\x00\x06\x00\x00'\ +b'\x78\x78\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18'\ +b'\x18\x18\x18\x18\x78\x78\x0b\x00\x00\x00\x00\x00\x0c\x00\x0e\x00'\ +b'\x0a\x00\x1b\x00\x13\x00\x31\x00\x31\x80\x20\x80\x60\xc0\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\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\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xff\xf8\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x06\x00\x00\x30\x18\x0c\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x80\x3f'\ +b'\xc0\x60\xe0\x00\x60\x00\x60\x0f\xe0\x3e\x60\x60\x60\x60\x60\x61'\ +b'\xe0\x3f\x70\x1e\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d'\ +b'\x00\x00\x00\x60\x00\x60\x00\x60\x00\x60\x00\x60\x00\x67\x80\x7f'\ +b'\xe0\x78\xe0\x70\x70\x60\x30\x60\x30\x60\x30\x60\x30\x70\x70\x78'\ +b'\xe0\x6f\xc0\x67\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x80\x1f'\ +b'\xc0\x30\xe0\x70\x60\x60\x00\x60\x00\x60\x00\x60\x00\x70\x60\x30'\ +b'\xe0\x3f\xc0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d'\ +b'\x00\x00\x00\x00\x30\x00\x30\x00\x30\x00\x30\x00\x30\x0f\x30\x1f'\ +b'\xf0\x38\xf0\x70\x70\x60\x30\x60\x30\x60\x30\x60\x30\x70\x70\x38'\ +b'\xf0\x1f\xb0\x0f\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x80\x1f'\ +b'\xc0\x38\x60\x70\x30\x60\x30\x7f\xf0\x7f\xf0\x60\x00\x60\x30\x30'\ +b'\x60\x1f\xe0\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06'\ +b'\x00\x00\x18\x38\x30\x30\x30\x78\x78\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x0f\x60\x3f\xe0\x30\xe0\x70\xe0\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x70\xe0\x30\xe0\x3f\x60\x0e\x60\x00\x60'\ +b'\x00\xe0\x60\xc0\x3f\xc0\x1f\x00\x0c\x00\x00\x00\x60\x00\x60\x00'\ +b'\x60\x00\x60\x00\x60\x00\x67\x80\x6f\xe0\x78\xe0\x70\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x30\x30\x00\x00\x00'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00\x00\x00\x00'\ +b'\x00\x06\x00\x00\x30\x30\x00\x00\x00\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\xf0\xe0\x0c\x00\x00\x00\x60\x00'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x60\xc0\x61\x80\x63\x00\x66\x00'\ +b'\x6e\x00\x7e\x00\x77\x00\x63\x00\x61\x80\x61\xc0\x60\xc0\x60\x60'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x00\x00'\ +b'\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x67\x1f\x00\x6f\xbf\x80\x71\xe3\x80'\ +b'\x60\xc1\x80\x60\xc1\x80\x60\xc1\x80\x60\xc1\x80\x60\xc1\x80\x60'\ +b'\xc1\x80\x60\xc1\x80\x60\xc1\x80\x60\xc1\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x67\x80\x6f\xe0\x78\xe0\x70\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x80\x1f\xc0\x38\xe0\x70\x70'\ +b'\x60\x30\x60\x30\x60\x30\x60\x30\x70\x70\x38\xe0\x1f\xc0\x0f\x80'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x67\x80\x6f\xc0\x78\xe0\x70\x70'\ +b'\x60\x30\x60\x30\x60\x30\x60\x30\x70\x70\x78\xe0\x7f\xc0\x67\x80'\ +b'\x60\x00\x60\x00\x60\x00\x60\x00\x00\x00\x0d\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x30\x1f\xf0\x38\xf0\x70\x70'\ +b'\x60\x30\x60\x30\x60\x30\x60\x30\x70\x70\x38\xf0\x3f\xf0\x0f\x30'\ +b'\x00\x30\x00\x30\x00\x30\x00\x30\x00\x00\x08\x00\x00\x00\x00\x00'\ +b'\x00\x00\x66\x6e\x78\x60\x60\x60\x60\x60\x60\x60\x60\x60\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x1f\x80\x3f\xe0\x70\x60\x60\x00\x70\x00\x3f\x00\x0f\xc0\x00'\ +b'\xe0\x60\x60\x70\xe0\x3f\xc0\x1f\x80\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x06\x00\x00\x00\x00\x00\x30\x30\x78\x78\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x38\x38\x00\x00\x00\x00\x00\x0c\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\xe0\x71\xe0\x7f\x60'\ +b'\x1e\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x60\x60\xe0\x60\xc0'\ +b'\x70\xc0\x31\x80\x31\x80\x19\x80\x1b\x00\x1b\x00\x0f\x00\x0e\x00'\ +b'\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\xc1\xc3\x00\x61\xc3\x00\x63\xc3\x00\x63\xc6\x00\x33\x66\x00\x33'\ +b'\x66\x00\x32\x66\x00\x36\x6c\x00\x1e\x3c\x00\x1e\x3c\x00\x1c\x38'\ +b'\x00\x0c\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x60\xc0\x71\xc0\x31\x80\x1b\x00\x0e\x00\x0e\x00\x0e\x00\x1b'\ +b'\x00\x1b\x00\x31\x80\x60\xc0\x60\xc0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\xc0\xe0\x60\xc0\x60\xc0\x61\x80\x31\x80\x31\x80\x33\x00\x1b'\ +b'\x00\x1b\x00\x1e\x00\x0e\x00\x0e\x00\x0c\x00\x0c\x00\x18\x00\x78'\ +b'\x00\x70\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x7f\xc0\x7f\xc0\x01\xc0\x01\x80\x03\x00\x06\x00\x0c\x00\x18'\ +b'\x00\x30\x00\x60\x00\x7f\xc0\x7f\xc0\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x08\x00\x00\x0c\x1c\x18\x18\x18\x18\x18\x18\x18\x18'\ +b'\x70\x70\x18\x18\x18\x18\x18\x18\x18\x18\x1c\x0c\x06\x00\x00\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x08\x00\x00\x30\x38\x18\x18\x18\x18\x18\x18'\ +b'\x18\x18\x0e\x0e\x18\x18\x18\x18\x18\x18\x18\x18\x38\x30\x0c\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00'\ +b'\x7c\x00\x4e\x40\x07\xc0\x01\x80\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00'\ +b'\x00\x00\x0f\xc0\x1f\xe0\x38\x60\x60\x30\x60\x30\x60\x00\x70\x00'\ +b'\x38\x00\xff\x00\xff\x00\x1c\x00\x0c\x00\x0c\x00\x18\x00\x2f\x10'\ +b'\x7f\xf0\x61\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x3f\xf8\x3f\xf8\x00\x18\x00\x18\x00\x18\x00\x18\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00'\ +b'\x00\x00\x00\x00\x07\x00\x0f\x80\x18\xc0\x10\x40\x18\xc0\x0f\x80'\ +b'\x07\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\x11\x00'\ +b'\x00\x00\x00\x03\xe0\x00\x0f\xf8\x00\x1c\x1c\x00\x38\x0e\x00\x30'\ +b'\x06\x00\x60\x03\x00\x60\x03\x00\x60\x03\x00\x60\x03\x00\x60\x03'\ +b'\x00\x60\x03\x00\x30\x06\x00\x30\x0e\x00\x18\x0c\x00\x0c\x38\x00'\ +b'\x7e\x3f\x00\x7e\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x0f\x60\x3f\xe0\x30\xe0\x60\xe0\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\xe0\x30\xe0\x3f\xf0\x0f\x70\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x1f\x80\x3f\xc0\x70\xe0\x60'\ +b'\x60\x60\x60\x60\xc0\x63\x80\x63\xc0\x60\x60\x60\x30\x60\x30\x60'\ +b'\x30\x60\x30\x60\x30\x70\x60\x7f\xe0\x6f\x80\x60\x00\x60\x00\x60'\ +b'\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\xe0\x60\xe0\x60\x70\xc0\x30\xc0\x30\xc0\x39\x80\x19'\ +b'\x80\x1b\x80\x0f\x00\x0f\x00\x06\x00\x06\x00\x06\x00\x06\x00\x06'\ +b'\x00\x06\x00\x06\x00\x0d\x00\x00\x00\x3f\xe0\x3f\xe0\x18\x00\x0e'\ +b'\x00\x03\x00\x0f\x80\x3f\xc0\x38\xe0\x70\x70\x60\x30\x60\x30\x60'\ +b'\x30\x60\x30\x70\x70\x30\xe0\x3f\xc0\x0f\x80\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x0f\x00\x1f\x80\x30\xc0\x30'\ +b'\xc0\x60\x60\x60\x60\x60\x60\x7f\xe0\x7f\xe0\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x30\xc0\x30\xc0\x1f\x80\x0f\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x70\x00\x78\x00\x0c\x00\x0c'\ +b'\x00\x06\x00\x06\x00\x0e\x00\x0f\x00\x1b\x00\x19\x00\x19\x80\x31'\ +b'\x80\x30\xc0\x30\xc0\x60\xc0\x60\x70\xe0\x70\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60\x60'\ +b'\x60\x60\x60\x60\x60\x70\xe0\x7f\xe0\x6f\x60\x60\x00\x60\x00\x60'\ +b'\x00\x60\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x7f\xf8\x7f\xf8\x30\x30\x30\x30\x30\x30\x30\x30\x30'\ +b'\x30\x30\x30\x30\x30\x30\x30\x30\x3c\x30\x1c\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x0c\x00\x30\x0e\x00\x30'\ +b'\x06\x00\x61\x87\x00\x61\x83\x00\x61\x83\x00\x61\x83\x00\x61\x83'\ +b'\x00\x61\x83\x00\x33\xc6\x00\x3f\x7e\x00\x1e\x38\x00\x00\x00\x00'\ +b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00'\ +b'\x00\x00\x01\x80\x01\x80\x01\x80\x01\x80\x0f\xf0\x1f\xf8\x39\x9c'\ +b'\x71\x8e\x61\x86\x61\x86\x61\x86\x61\x86\x71\x8e\x39\x9c\x1f\xf8'\ +b'\x0f\xf0\x01\x80\x01\x80\x01\x80\x01\x80\x00\x00' + +_sparse =\ +b'\x20\x00\x30\x00\x21\x00\x49\x00\x22\x00\x62\x00\x23\x00\x7b\x00'\ +b'\x24\x00\xab\x00\x25\x00\xdb\x00\x26\x00\x22\x01\x27\x00\x52\x01'\ +b'\x28\x00\x6b\x01\x29\x00\x84\x01\x2a\x00\x9d\x01\x2b\x00\xcd\x01'\ +b'\x2c\x00\xfd\x01\x2d\x00\x16\x02\x2e\x00\x2f\x02\x2f\x00\x48\x02'\ +b'\x30\x00\x61\x02\x31\x00\x91\x02\x32\x00\xc1\x02\x33\x00\xf1\x02'\ +b'\x34\x00\x21\x03\x35\x00\x51\x03\x36\x00\x81\x03\x37\x00\xb1\x03'\ +b'\x38\x00\xe1\x03\x39\x00\x11\x04\x3a\x00\x41\x04\x3b\x00\x5a\x04'\ +b'\x3c\x00\x73\x04\x3d\x00\xa3\x04\x3e\x00\xd3\x04\x3f\x00\x03\x05'\ +b'\x40\x00\x33\x05\x41\x00\x7a\x05\x42\x00\xaa\x05\x43\x00\xda\x05'\ +b'\x44\x00\x0a\x06\x45\x00\x3a\x06\x46\x00\x6a\x06\x47\x00\x9a\x06'\ +b'\x48\x00\xe1\x06\x49\x00\x28\x07\x4a\x00\x41\x07\x4b\x00\x71\x07'\ +b'\x4c\x00\xa1\x07\x4d\x00\xd1\x07\x4e\x00\x18\x08\x4f\x00\x5f\x08'\ +b'\x50\x00\xa6\x08\x51\x00\xd6\x08\x52\x00\x1d\x09\x53\x00\x4d\x09'\ +b'\x54\x00\x7d\x09\x55\x00\xad\x09\x56\x00\xf4\x09\x57\x00\x24\x0a'\ +b'\x58\x00\x6b\x0a\x59\x00\x9b\x0a\x5a\x00\xcb\x0a\x5b\x00\xfb\x0a'\ +b'\x5c\x00\x14\x0b\x5d\x00\x2d\x0b\x5e\x00\x46\x0b\x5f\x00\x76\x0b'\ +b'\x60\x00\xa6\x0b\x61\x00\xbf\x0b\x62\x00\xef\x0b\x63\x00\x1f\x0c'\ +b'\x64\x00\x4f\x0c\x65\x00\x7f\x0c\x66\x00\xaf\x0c\x67\x00\xc8\x0c'\ +b'\x68\x00\xf8\x0c\x69\x00\x28\x0d\x6a\x00\x41\x0d\x6b\x00\x5a\x0d'\ +b'\x6c\x00\x8a\x0d\x6d\x00\xa3\x0d\x6e\x00\xea\x0d\x6f\x00\x1a\x0e'\ +b'\x70\x00\x4a\x0e\x71\x00\x7a\x0e\x72\x00\xaa\x0e\x73\x00\xc3\x0e'\ +b'\x74\x00\xf3\x0e\x75\x00\x0c\x0f\x76\x00\x3c\x0f\x77\x00\x6c\x0f'\ +b'\x78\x00\xb3\x0f\x79\x00\xe3\x0f\x7a\x00\x13\x10\x7b\x00\x43\x10'\ +b'\x7c\x00\x5c\x10\x7d\x00\x75\x10\x7e\x00\x8e\x10\xa3\x00\xbe\x10'\ +b'\xac\x00\xee\x10\xb0\x00\x1e\x11\xa9\x03\x4e\x11\xb1\x03\x95\x11'\ +b'\xb2\x03\xc5\x11\xb3\x03\xf5\x11\xb4\x03\x25\x12\xb8\x03\x55\x12'\ +b'\xbb\x03\x85\x12\xbc\x03\xb5\x12\xc0\x03\xe5\x12\xc9\x03\x15\x13'\ +b'\xd5\x03\x5c\x13' + +_mvfont = memoryview(_font) +_mvsp = memoryview(_sparse) +ifb = lambda l : l[0] | (l[1] << 8) + +def bs(lst, val): + while True: + m = (len(lst) & ~ 7) >> 1 + v = ifb(lst[m:]) + if v == val: + return ifb(lst[m + 2:]) + if not m: + return 0 + lst = lst[m:] if v < val else lst[:m] + +def get_ch(ch): + doff = bs(_mvsp, ord(ch)) + width = ifb(_mvfont[doff : ]) + + next_offs = doff + 2 + ((width - 1)//8 + 1) * 23 + return _mvfont[doff + 2:next_offs], 23, width + diff --git a/gui/fonts/font6.py b/gui/fonts/font6.py new file mode 100644 index 0000000..0adc524 --- /dev/null +++ b/gui/fonts/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/gui/fonts/freesans20.py b/gui/fonts/freesans20.py new file mode 100644 index 0000000..1f6da29 --- /dev/null +++ b/gui/fonts/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/gui/primitives/__init__.py b/gui/primitives/__init__.py new file mode 100644 index 0000000..0274fc2 --- /dev/null +++ b/gui/primitives/__init__.py @@ -0,0 +1,31 @@ +# __init__.py Common functions for uasyncio primitives + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + + +async def _g(): + pass +type_coro = type(_g()) + +# If a callback is passed, run it and return. +# If a coro is passed initiate it and return. +# coros are passed by name i.e. not using function call syntax. +def launch(func, tup_args): + res = func(*tup_args) + if isinstance(res, type_coro): + res = asyncio.create_task(res) + return res + +def set_global_exception(): + def _handle_exception(loop, context): + import sys + sys.print_exception(context["exception"]) + sys.exit() + loop = asyncio.get_event_loop() + loop.set_exception_handler(_handle_exception) diff --git a/gui/primitives/delay_ms.py b/gui/primitives/delay_ms.py new file mode 100644 index 0000000..6fd11fb --- /dev/null +++ b/gui/primitives/delay_ms.py @@ -0,0 +1,71 @@ +# delay_ms.py Now uses ThreadSafeFlag and has extra .wait() API +# Usage: +# from primitives.delay_ms import Delay_ms + +# Copyright (c) 2018-2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +from utime import ticks_add, ticks_diff, ticks_ms +from . import launch + +class Delay_ms: + + class DummyTimer: # Stand-in for the timer class. Can be cancelled. + def cancel(self): + pass + _fake = DummyTimer() + + def __init__(self, func=None, args=(), duration=1000): + self._func = func + self._args = args + self._durn = duration # Default duration + self._retn = None # Return value of launched callable + self._tend = None # Stop time (absolute ms). + self._busy = False + self._trig = asyncio.ThreadSafeFlag() + self._tout = asyncio.Event() # Timeout event + self.wait = self._tout.wait # Allow: await wait_ms.wait() + self._ttask = self._fake # Timer task + asyncio.create_task(self._run()) + + async def _run(self): + while True: + await self._trig.wait() # Await a trigger + self._ttask.cancel() # Cancel and replace + await asyncio.sleep_ms(0) + dt = max(ticks_diff(self._tend, ticks_ms()), 0) # Beware already elapsed. + self._ttask = asyncio.create_task(self._timer(dt)) + + async def _timer(self, dt): + await asyncio.sleep_ms(dt) + self._tout.set() # Only gets here if not cancelled. + self._tout.clear() + self._busy = False + if self._func is not None: + self._retn = launch(self._func, self._args) + +# API + # trigger may be called from hard ISR. + def trigger(self, duration=0): # Update absolute end time, 0-> ctor default + self._tend = ticks_add(ticks_ms(), duration if duration > 0 else self._durn) + self._retn = None # Default in case cancelled. + self._busy = True + self._trig.set() + + def stop(self): + self._ttask.cancel() + self._ttask = self._fake + self._busy = False + + def __call__(self): # Current running status + return self._busy + + running = __call__ + + def rvalue(self): + return self._retn + + def callback(self, func=None, args=()): + self._func = func + self._args = args diff --git a/gui/primitives/pushbutton.py b/gui/primitives/pushbutton.py new file mode 100644 index 0000000..0442517 --- /dev/null +++ b/gui/primitives/pushbutton.py @@ -0,0 +1,107 @@ +# pushbutton.py + +# Copyright (c) 2018-2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import utime as time +from . import launch +from gui.primitives.delay_ms import Delay_ms + + +# An alternative Pushbutton solution with lower RAM use is available here +# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py +class Pushbutton: + debounce_ms = 50 + long_press_ms = 1000 + double_click_ms = 400 + def __init__(self, pin, suppress=False, sense=None): + self.pin = pin # Initialise for input + self._supp = suppress + self._dblpend = False # Doubleclick waiting for 2nd click + self._dblran = False # Doubleclick executed user function + self._tf = False + self._ff = False + self._df = False + self._ld = False # Delay_ms instance for long press + self._dd = False # Ditto for doubleclick + self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value + self.state = self.rawstate() # Initial state + asyncio.create_task(self.buttoncheck()) # Thread runs forever + + def press_func(self, func=False, args=()): + self._tf = func + self._ta = args + + def release_func(self, func=False, args=()): + self._ff = func + self._fa = args + + def double_func(self, func=False, args=()): + self._df = func + self._da = args + if func: # If double timer already in place, leave it + if not self._dd: + self._dd = Delay_ms(self._ddto) + else: + self._dd = False # Clearing down double func + + def long_func(self, func=False, args=()): + if func: + if self._ld: + self._ld.callback(func, args) + else: + self._ld = Delay_ms(func, args) + else: + self._ld = False + + # Current non-debounced logical button state: True == pressed + def rawstate(self): + return bool(self.pin.value() ^ self.sense) + + # Current debounced state of button (True == pressed) + def __call__(self): + return self.state + + def _ddto(self): # Doubleclick timeout: no doubleclick occurred + self._dblpend = False + if self._supp and not self.state: + if not self._ld or (self._ld and not self._ld()): + launch(self._ff, self._fa) + + async def buttoncheck(self): + while True: + state = self.rawstate() + # State has changed: act on it now. + if state != self.state: + self.state = state + if state: # Button pressed: launch pressed func + if self._tf: + launch(self._tf, self._ta) + if self._ld: # There's a long func: start long press delay + self._ld.trigger(Pushbutton.long_press_ms) + if self._df: + if self._dd(): # Second click: timer running + self._dd.stop() + self._dblpend = False + self._dblran = True # Prevent suppressed launch on release + launch(self._df, self._da) + else: + # First click: start doubleclick timer + self._dd.trigger(Pushbutton.double_click_ms) + self._dblpend = True # Prevent suppressed launch on release + else: # Button release. Is there a release func? + if self._ff: + if self._supp: + d = self._ld + # If long delay exists, is running and doubleclick status is OK + if not self._dblpend and not self._dblran: + if (d and d()) or not d: + launch(self._ff, self._fa) + else: + launch(self._ff, self._fa) + if self._ld: + self._ld.stop() # Avoid interpreting a second click as a long push + self._dblran = False + # Ignore state changes until switch has settled + await asyncio.sleep_ms(Pushbutton.debounce_ms) diff --git a/gui/primitives/switch.py b/gui/primitives/switch.py new file mode 100644 index 0000000..87ce8d5 --- /dev/null +++ b/gui/primitives/switch.py @@ -0,0 +1,42 @@ +# switch.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import utime as time +from . import launch + +class Switch: + debounce_ms = 50 + def __init__(self, pin): + self.pin = pin # Should be initialised for input with pullup + self._open_func = False + self._close_func = False + self.switchstate = self.pin.value() # Get initial state + asyncio.create_task(self.switchcheck()) # Thread runs forever + + def open_func(self, func, args=()): + self._open_func = func + self._open_args = args + + def close_func(self, func, args=()): + self._close_func = func + self._close_args = args + + # Return current state of switch (0 = pressed) + def __call__(self): + return self.switchstate + + async def switchcheck(self): + while True: + state = self.pin.value() + if state != self.switchstate: + # State has changed: act on it now. + self.switchstate = state + if state == 0 and self._close_func: + launch(self._close_func, self._close_args) + elif state == 1 and self._open_func: + launch(self._open_func, self._open_args) + # Ignore further state changes until switch has settled + await asyncio.sleep_ms(Switch.debounce_ms) diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/widgets/buttons.py b/gui/widgets/buttons.py new file mode 100644 index 0000000..40925e5 --- /dev/null +++ b/gui/widgets/buttons.py @@ -0,0 +1,218 @@ +# buttons.py Extension to ugui providing pushbutton classes + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +import uasyncio as asyncio +from gui.core.ugui import Screen, Widget, display +from gui.primitives.delay_ms import Delay_ms +from gui.core.colors import * + +dolittle = lambda *_ : None + + +class Button(Widget): + lit_time = 1000 + long_press_time = 1000 + def __init__(self, writer, row, col, *, shape=RECTANGLE, height=20, width=50, + fgcolor=None, bgcolor=None, bdcolor=False, textcolor=None, litcolor=None, text='', + callback=dolittle, args=[], onrelease=False, lp_callback=dolittle, lp_args=[]): + sl = writer.stringlen(text) + if shape == CIRCLE: # Only height need be specified + width = max(sl, height) + height = width + else: + width = max(sl, width) + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, False, True) + self.shape = shape + self.radius = height // 2 + self.fill = bgcolor is not None # Draw background if color specified + self.litcolor = litcolor + self.textcolor = self.fgcolor if textcolor is None else textcolor + self.orig_fgcolor = self.fgcolor + self.orig_bgcolor = self.bgcolor + self.text = text + self.callback = callback + self.callback_args = args + self.onrelease = onrelease + self.lp_callback = lp_callback + self.lp_args = lp_args + if self.litcolor is not None: + self.delay = Delay_ms(self.shownormal) + self.litcolor = litcolor if self.fgcolor is not None else None + + def show(self): + if self.screen is not Screen.current_screen: + return + x = self.col + y = self.row + w = self.width + h = self.height + if not self.visible: # erase the button + display.usegrey(False) + display.fill_rect(x, y, w, h, BGCOLOR) + return + super().show() # Blank rectangle containing button + if self.shape == CIRCLE: # Button coords are of top left corner of bounding box + x += self.radius + y += self.radius + if self.fill: + display.fillcircle(x, y, self.radius, self.bgcolor) + display.circle(x, y, self.radius, self.fgcolor) + if len(self.text): + display.print_centred(self.writer, x, y, self.text, self.textcolor, self.bgcolor) + else: + xc = x + w // 2 + yc = y + h // 2 + if self.shape == RECTANGLE: # rectangle + if self.fill: + display.fill_rect(x, y, w, h, self.bgcolor) + display.rect(x, y, w, h, self.fgcolor) + if len(self.text): + display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor) + elif self.shape == CLIPPED_RECT: # clipped rectangle + if self.fill: + display.fill_clip_rect(x, y, w, h, self.bgcolor) + display.clip_rect(x, y, w, h, self.fgcolor) + if len(self.text): + display.print_centred(self.writer, xc, yc, self.text, self.textcolor, self.bgcolor) + + async def shownormal(self): + # Handle case where screen changed while timer was active: delay repaint + # until screen is current. Pathological app behaviour where another + # control caused a screen change while timer running. + while self.screen is not Screen.current_screen: + await asyncio.sleep_ms(500) + self.bgcolor = self.orig_bgcolor + self.draw = True # Redisplay + + def do_sel(self): # Select was pushed + if not self.onrelease: + self.callback(self, *self.callback_args) # CB takes self as 1st arg. + if self.litcolor is not None and self.has_focus(): # CB may have changed focus + self.bgcolor = self.litcolor + self.draw = True # Redisplay + self.delay.trigger(Button.lit_time) + + def unsel(self): # Select was released + if self.onrelease: + self.callback(self, *self.callback_args) # Callback not a bound method so pass self + + def longsel(self): # Select was long pressed + self.lp_callback(self, *self.lp_args) + +# Preferred way to close a screen or dialog. Produces an X button at the top RHS. +# Note that if the bottom screen is closed, the application terminates. +class CloseButton(Button): + def __init__(self, writer, width=0, callback=dolittle, args=(), bgcolor=RED): + scr = Screen.current_screen + wd = max(writer.stringlen('X') + 6, width) + self.user_cb = callback + self.user_args = args + super().__init__(writer, *scr.locn(4, scr.width - wd - 4), + width = wd, height = wd, bgcolor = bgcolor, + callback = self.cb, text = 'X') + + def cb(self, _): + self.user_cb(self, *self.user_args) + Screen.back() + +# Group of buttons, typically at same location, where pressing one shows +# the next e.g. start/stop toggle or sequential select from short list +class ButtonList: + def __init__(self, callback=dolittle): + self.user_callback = callback + self.lstbuttons = [] + self.current = None # No current button + self._greyed_out = False + + def add_button(self, *args, **kwargs): + button = Button(*args, **kwargs) + self.lstbuttons.append(button) + active = self.current is None # 1st button added is active + button.visible = active + button.callback = self._callback + if active: + self.current = button + return button + + def value(self, button=None, new_cb=False): + if button is not None and button is not self.current: + old = self.current + new = button + self.current = new + old.visible = False + new.visible = True + new.draw = True # Redisplay without changing currency + # Args for user callback: button instance followed by any specified. + # Normal behaviour is to run cb of old button: this mimics a button press. + # Optionally programmatic value changes can run the cb of new button. + if new_cb: # Forced value change, callback is that of new button + self.user_callback(new, *new.callback_args) + else: # A button was pressed + # Callback context is button just pressed, not the new one + self.user_callback(old, *old.callback_args) + return self.current + + def greyed_out(self, val=None): + if val is not None and self._greyed_out != val: + self._greyed_out = val + for button in self.lstbuttons: + button.greyed_out(val) + self.current.draw = True + return self._greyed_out + + def _callback(self, button, *_): + old = button + old_index = self.lstbuttons.index(button) + new = self.lstbuttons[(old_index + 1) % len(self.lstbuttons)] + self.current = new + old.visible = False + new.visible = True + Screen.select(new) # Move currency and redisplay + # Callback context is button just pressed, not the new one + self.user_callback(old, *old.callback_args) + + +# Group of buttons at different locations, where pressing one shows +# only current button highlighted and oes callback from current one +class RadioButtons: + def __init__(self, highlight, callback=dolittle, selected=0): + self.user_callback = callback + self.lstbuttons = [] + self.current = None # No current button + self.highlight = highlight + self.selected = selected + self._greyed_out = False + + def add_button(self, *args, **kwargs): + button = Button(*args, **kwargs) + self.lstbuttons.append(button) + button.callback = self._callback + active = len(self.lstbuttons) == self.selected + 1 + button.bgcolor = self.highlight if active else button.orig_bgcolor + if active: + self.current = button + return button + + def value(self, button=None): + if button is not None and button is not self.current: + self._callback(button, *button.callback_args) + return self.current + + def greyed_out(self, val=None): + if val is not None and self._greyed_out != val: + self._greyed_out = val + for button in self.lstbuttons: + button.greyed_out(val) + return self._greyed_out + + def _callback(self, button, *args): + for but in self.lstbuttons: + if but is button: + but.bgcolor = self.highlight + self.current = button + else: + but.bgcolor = but.orig_bgcolor + but.draw = True + self.user_callback(button, *args) # user gets button with args they specified diff --git a/gui/widgets/checkbox.py b/gui/widgets/checkbox.py new file mode 100644 index 0000000..580100f --- /dev/null +++ b/gui/widgets/checkbox.py @@ -0,0 +1,36 @@ +# checkbox.py Extension to ugui providing the Checkbox class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019-2021 Peter Hinch + +from gui.core.ugui import Widget, display + +dolittle = lambda *_ : None + +class Checkbox(Widget): + def __init__(self, writer, row, col, *, height=30, fillcolor=None, + fgcolor=None, bgcolor=None, bdcolor=False, + callback=dolittle, args=[], value=False, active=True): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active) + super()._set_callbacks(callback, args) + self.fillcolor = fillcolor + + def show(self): + if super().show(): + x = self.col + y = self.row + ht = self.height + x1 = x + ht - 1 + y1 = y + ht - 1 + if self._value: + if self.fillcolor is not None: + display.fill_rect(x, y, ht, ht, self.fillcolor) + else: + display.fill_rect(x, y, ht, ht, self.bgcolor) + display.rect(x, y, ht, ht, self.fgcolor) + if self.fillcolor is None and self._value: + display.line(x, y, x1, y1, self.fgcolor) + display.line(x, y1, x1, y, self.fgcolor) + + def do_sel(self): # Select was pushed + self.value(not self._value) # Upddate and refresh diff --git a/gui/widgets/dial.py b/gui/widgets/dial.py new file mode 100644 index 0000000..53eabc4 --- /dev/null +++ b/gui/widgets/dial.py @@ -0,0 +1,104 @@ +# dial.py Dial and Pointer classes for micro-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +import cmath +from gui.core.ugui import Widget, display +from gui.widgets.label import Label + +# Line defined by polar coords; origin and line are complex +def polar(display, origin, line, color): + xs, ys = origin.real, origin.imag + theta = cmath.polar(line)[1] + display.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(display, 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(display, origin, vec, color) # Origin to tip + polar(display, origin, start, color) # Origin to tail + polar(display, origin + conj(vec), chev*ccw*uv, color) # Tip chevron + polar(display, origin + conj(vec), chev*cw*uv, color) + if length > lc: # Confusing appearance of very short vectors with tail chevron + polar(display, origin + conj(start), chev*ccw*uv, color) # Tail chevron + polar(display, origin + conj(start), chev*cw*uv, color) + + +class Pointer(): + def __init__(self, dial): + self.dial = dial + dial.vectors.add(self) + 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.draw = True + return self.val + +class Dial(Widget): + 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): + if super().show(): + # cache bound variables + 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(display, vor + conj(vtstart), vtick, self.fgcolor) + vtick *= vrot + vtstart *= vrot + display.circle(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(display, vor, val, color) + else: + arrow(display, vor, val, 5, color) + if isinstance(self.pip, int) and vshort > 5: + display.fillcircle(xo, yo, 2, self.pip) diff --git a/gui/widgets/dialog.py b/gui/widgets/dialog.py new file mode 100644 index 0000000..0dede75 --- /dev/null +++ b/gui/widgets/dialog.py @@ -0,0 +1,52 @@ +# dialog.py Extension to ugui providing the DialogBox class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from gui.core.ugui import display, Window, Screen +from gui.core.colors import * +from gui.widgets.label import Label +from gui.widgets.buttons import Button, CloseButton + +dolittle = lambda *_ : None + +class DialogBox(Window): + def __init__(self, writer, row=20, col=20, *, elements, label=None, + bgcolor=DARKGREEN, buttonwidth=25, closebutton=True, callback=dolittle, args=[]): + + def back(button, text): # Callback for normal buttons + Window.value(text) + callback(Window, *args) + Screen.back() + + def backbutton(button, text): + Window.value(text) + callback(Window, *args) + + height = 80 + spacing = 10 + buttonwidth = max(max(writer.stringlen(e[0]) for e in elements) + 4, buttonwidth) + buttonheight = max(writer.height, 15) + nelements = len(elements) + width = spacing + (buttonwidth + spacing) * nelements + if label is not None: + width = max(width, writer.stringlen(label) + 2 * spacing) + super().__init__(row, col, height, width, bgcolor = bgcolor) + + col = spacing # Coordinates relative to window + row = self.height - buttonheight - 10 + gap = 0 + if nelements > 1: + gap = ((width - 2 * spacing) - nelements * buttonwidth) // (nelements - 1) + if label is not None: + r, c = self.locn(10, col) + Label(writer, r, c, label, bgcolor = bgcolor) + for text, color in elements: + Button(writer, *self.locn(row, col), height = buttonheight, width = buttonwidth, + textcolor = BLACK, bgcolor = color, + text = text, shape = RECTANGLE, + callback = back, args = (text,)) + col += buttonwidth + gap + + if closebutton: + CloseButton(writer, callback = backbutton, args = ('Close',)) diff --git a/gui/widgets/dropdown.py b/gui/widgets/dropdown.py new file mode 100644 index 0000000..9cbf3ed --- /dev/null +++ b/gui/widgets/dropdown.py @@ -0,0 +1,92 @@ +# dropdown.py Extension to ugui providing the Dropdown class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from gui.core.ugui import Widget, display, Window, Screen +from gui.core.colors import * + +from gui.widgets.listbox import Listbox + +dolittle = lambda *_ : None + +# Next and Prev close the listbox without updating the Dropdown. This is +# handled by Screen .move bound method +class _ListDialog(Window): + + def __init__(self, writer, row, col, dropdown, textwidth): + dd = dropdown + elements = dd.elements + # Need to determine Window dimensions from size of Listbox, which + # depends on number and length of elements. + entry_height, lb_height, textwidth = Listbox.dimensions(writer, elements) + lb_width = textwidth + 2 + # Calculate Window dimensions + ap_height = lb_height + 6 # Allow for listbox border + ap_width = lb_width + 6 + super().__init__(row, col, ap_height, ap_width) + self.listbox = Listbox(writer, row + 3, col + 3, elements = elements, width = lb_width, + fgcolor = dd.fgcolor, bgcolor = dd.bgcolor, bdcolor=False, + fontcolor = WHITE, select_color = dd.select_color, + value = dd.value(), callback = self.callback) + self.dropdown = dd + + def callback(self, obj_listbox): + Screen.back() + self.dropdown.value(obj_listbox.value()) # Update it + + +class Dropdown(Widget): + def __init__(self, writer, row, col, *, elements, width=None, value=0, + fgcolor=None, bgcolor=None, bdcolor=False, fontcolor=None, select_color=DARKBLUE, + callback=dolittle, args=[]): + + self.entry_height = writer.height + 2 # Allow a pixel above and below text + height = self.entry_height + if width is None: # Allow for square at end for arrow + self.textwidth = max(writer.stringlen(s) for s in elements) + width = self.textwidth + 2 + height + else: + self.textwidth = width + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, True) + super()._set_callbacks(callback, args) + self.select_color = select_color + self.fontcolor = self.fgcolor if fontcolor is None else fontcolor + self.elements = elements + + def show(self): + if super().show(): + x, y = self.col, self.row + self._draw(x, y) + if self._value is not None: + display.print_left(self.writer, x, y + 1, self.elements[self._value], self.fontcolor) + + def textvalue(self, text=None): # if no arg return current text + if text is None: + return self.elements[self._value] + else: # set value by text + try: + v = self.elements.index(text) + except ValueError: + v = None + else: + if v != self._value: + self.value(v) + return v + + def _draw(self, x, y): + self.draw_border() + display.vline(x + self.width - self.height, y, self.height, self.fgcolor) + xcentre = x + self.width - self.height // 2 # Centre of triangle + ycentre = y + self.height // 2 + halflength = (self.height - 8) // 2 + length = halflength * 2 + if length > 0: + display.hline(xcentre - halflength, ycentre - halflength, length, self.fgcolor) + display.line(xcentre - halflength, ycentre - halflength, xcentre, ycentre + halflength, self.fgcolor) + display.line(xcentre + halflength, ycentre - halflength, xcentre, ycentre + halflength, self.fgcolor) + + def do_sel(self): # Select was pushed + if len(self.elements) > 1: + args = (self.writer, self.row - 2, self.col - 2, self, self.textwidth) + Screen.change(_ListDialog, args = args) diff --git a/gui/widgets/graph.py b/gui/widgets/graph.py new file mode 100644 index 0000000..35b9629 --- /dev/null +++ b/gui/widgets/graph.py @@ -0,0 +1,259 @@ +# graph.py Graph plotting widgets for micro-gui + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from hardware_setup import ssd, display # Create a display instance +from gui.core.ugui import Widget +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)): + if not isinstance(self, PolarCurve): # Check not done in subclass + if isinstance(graph, PolarGraph) or not isinstance(graph, CartesianGraph): + raise ValueError('Curve must use a CartesianGraph instance.') + self.graph = graph + self.origin = origin + self.excursion = excursion + self.color = color if color is not None else graph.fgcolor + 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): + if not isinstance(graph, PolarGraph): + raise ValueError('PolarCurve must use a PolarGraph instance.') + 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(Widget): + def __init__(self, writer, row, col, height, width, fgcolor, bgcolor, bdcolor, gridcolor): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) + 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.draw = True # Clear working area + + def show(self): + return super().show() # Draw or erase border + +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.draw = True + + def show(self): + if super().show(): # Clear working area + 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) + ssd.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.draw = True + + def show(self): + if super().show(): # Clear working area + display.usegrey(False) + 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): + display.circle(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) + ssd.line(xs, ys, xe, ye, color) diff --git a/gui/widgets/knob.py b/gui/widgets/knob.py new file mode 100644 index 0000000..482e7bd --- /dev/null +++ b/gui/widgets/knob.py @@ -0,0 +1,60 @@ +# knob.py Extension to microgui providing a control knob (rotary potentiometer) widget + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from gui.core.ugui import LinearIO, display +import math + +TWOPI = 2 * math.pi +# Null function +dolittle = lambda *_ : None + +# *********** CONTROL KNOB CLASS *********** + +class Knob(LinearIO): + def __init__(self, writer, row, col, *, height=70, arc=TWOPI, ticks=9, value=0.0, + fgcolor=None, bgcolor=None, color=None, bdcolor=None, + callback=dolittle, args=[], active=True): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, value, active) + super()._set_callbacks(callback, args) + radius = height / 2 + self.arc = min(max(arc, 0), TWOPI) # Usable angle of control + self.radius = radius + self.xorigin = col + radius + self.yorigin = row + radius + self.ticklen = 0.1 * radius + self.pointerlen = radius - self.ticklen - 5 + self.ticks = max(ticks, 2) # start and end of travel + self.color = color + self.draw = True # Ensure a redraw on next refresh + if active: # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + def show(self): + if super().show(False): # Honour bgcolor + arc = self.arc + ticks = self.ticks + radius = self.radius + ticklen = self.ticklen + for tick in range(ticks): + theta = (tick / (ticks - 1)) * arc - arc / 2 + x_start = int(self.xorigin + radius * math.sin(theta)) + y_start = int(self.yorigin - radius * math.cos(theta)) + x_end = int(self.xorigin + (radius - ticklen) * math.sin(theta)) + y_end = int(self.yorigin - (radius - ticklen) * math.cos(theta)) + display.line(x_start, y_start, x_end, y_end, self.fgcolor) + if self.color is not None: + display.fillcircle(self.xorigin, self.yorigin, radius - ticklen, self.color) + display.circle(self.xorigin, self.yorigin, radius - ticklen, self.fgcolor) + display.circle(self.xorigin, self.yorigin, radius - ticklen - 3, self.fgcolor) + + self._drawpointer(self._value, self.fgcolor) # draw new + + def _drawpointer(self, value, color): + arc = self.arc + length = self.pointerlen + angle = value * arc - arc / 2 + x_end = int(self.xorigin + length * math.sin(angle)) + y_end = int(self.yorigin - length * math.cos(angle)) + display.line(int(self.xorigin), int(self.yorigin), x_end, y_end, color) diff --git a/gui/widgets/label.py b/gui/widgets/label.py new file mode 100644 index 0000000..0e5aae2 --- /dev/null +++ b/gui/widgets/label.py @@ -0,0 +1,38 @@ +# label.py Extension to micro-gui providing the Label class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch +from gui.core.ugui import Widget, display +from gui.core.writer import Writer +from gui.core.colors import * + +# text: str display string int save width +class Label(Widget): + def __init__(self, writer, row, col, text, invert=False, fgcolor=None, bgcolor=BLACK, bdcolor=False): + # Determine width of object + if isinstance(text, int): + width = text + text = None + else: + width = writer.stringlen(text) + height = writer.height + self.invert = invert + 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) # Sets .draw ensuring refresh + # 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 + return txt + + def show(self): + if super().show(): # Draw or erase border + if isinstance(txt := super().value(), str): + display.print_left(self.writer, self.col, self.row, txt, self.fgcolor, self.bgcolor, self.invert) diff --git a/gui/widgets/led.py b/gui/widgets/led.py new file mode 100644 index 0000000..7cef19f --- /dev/null +++ b/gui/widgets/led.py @@ -0,0 +1,27 @@ +# led.py Extension to ugui providing the LED class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from gui.core.ugui import Widget, display +from gui.core.colors import * + + +class LED(Widget): + def __init__(self, writer, row, col, *, height=30, fgcolor=None, bgcolor=None, bdcolor=False, color=RED): + super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor, False) + self._value = False + self._color = color + self.radius = self.height // 2 + self.x = col + self.radius + self.y = row + self.radius + + def show(self): + if super().show(): # Draw or erase border + color = self._color if self._value else BLACK + display.fillcircle(int(self.x), int(self.y), int(self.radius), color) + display.circle(int(self.x), int(self.y), int(self.radius), self.fgcolor) + + def color(self, color): + self._color = color + self.draw = True diff --git a/gui/widgets/listbox.py b/gui/widgets/listbox.py new file mode 100644 index 0000000..07d5208 --- /dev/null +++ b/gui/widgets/listbox.py @@ -0,0 +1,106 @@ +# listbox.py Extension to ugui providing the Listbox class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch +from micropython import const +from gui.core.ugui import Widget, display +from gui.core.colors import * + +dolittle = lambda *_ : None + +# Behaviour has issues compared to touch displays because movement between +# entries is sequential. This can affect the choice in when the callback runs. +# It always runs when select is pressed. See 'also' ctor arg. +ON_MOVE = const(1) # Also run whenever the currency moves. +ON_LEAVE = const(2) # Also run on exit from the control. + +class Listbox(Widget): + @staticmethod + def dimensions(writer, elements): + entry_height = writer.height + 2 # Allow a pixel above and below text + le = len(elements) + height = entry_height * le + 2 + textwidth = max(writer.stringlen(s) for s in elements) + 4 + return entry_height, height, textwidth + + def __init__(self, writer, row, col, *, elements, width=None, value=0, + fgcolor=None, bgcolor=None, bdcolor=False, fontcolor=None, select_color=DARKBLUE, + callback=dolittle, args=[], also=0): + + self.entry_height, height, textwidth = self.dimensions(writer, elements) + self.also = also + if width is None: + width = textwidth + if not isinstance(value, int) or value >= len(elements): + value = 0 + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, True) + self.cb = callback + self.cb_args = args + self.select_color = select_color + self.fontcolor = fontcolor + fail = False + try: + self.elements = [s for s in elements if type(s) is str] + except: + fail = True + else: + fail = len(self.elements) == 0 + if fail: + raise ValueError('elements must be a list or tuple of one or more strings') + self._value = value # No callback until user selects + self.ev = value + + def show(self): + if not super().show(False): # Clear to self.bgcolor + return + + length = len(self.elements) + x = self.col + y = self.row + for n in range(length): + if n == self._value: + display.fill_rect(x, y + 1, self.width, self.entry_height - 1, self.select_color) + display.print_left(self.writer, x + 2, y + 1, self.elements[n], self.fgcolor, self.select_color) + else: + display.print_left(self.writer, x + 2, y + 1, self.elements[n], self.fgcolor, self.bgcolor) + y += self.entry_height + + def textvalue(self, text=None): # if no arg return current text + if text is None: + return self.elements[self._value] + else: # set value by text + try: + v = self.elements.index(text) + except ValueError: + v = None + else: + if v != self._value: + self.value(v) + return v + + def do_up(self, _): + if v := self._value: + self.value(v - 1) + if (self.also & ON_MOVE): # Treat as if select pressed + self.do_sel() + + def do_down(self, _): + if (v := self._value) < len(self.elements) - 1: + self.value(v + 1) + if (self.also & ON_MOVE): + self.do_sel() + + # Callback runs if select is pressed. Also (if ON_LEAVE) if user changes + # list currency and then moves off the control. Otherwise if we have a + # callback that refreshes another control, that second control does not + # track currency. + def do_sel(self): # Select was pushed + self.ev = self._value + self.cb(self, *self.cb_args) + + def enter(self): + self.ev = self._value # Value change detection + + def leave(self): + if (self.also & ON_LEAVE) and self._value != self.ev: + self.do_sel() diff --git a/gui/widgets/meter.py b/gui/widgets/meter.py new file mode 100644 index 0000000..47039ba --- /dev/null +++ b/gui/widgets/meter.py @@ -0,0 +1,64 @@ +# meter.py Extension to ugui providing a linear "meter" widget. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2019 Peter Hinch + +from gui.core.ugui import Widget, display +from gui.widgets.label import Label +from gui.core.colors import * + +# Null function +dolittle = lambda *_ : None + +class Meter(Widget): + BAR = 1 + LINE = 0 + def __init__(self, writer, row, col, *, height=50, width=10, + fgcolor=None, bgcolor=BLACK, ptcolor=None, bdcolor=None, + divisions=5, label=None, style=0, legends=None, value=0): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor) # TODO Consider active + self.divisions = divisions + if label is not None: + Label(writer, row + height + 3, col, label) + self.style = style + self.ptcolor = ptcolor if ptcolor is not None else self.fgcolor + if legends is not None: # Legends + x = col + width + 4 + y = row + height + dy = 0 if len(legends) <= 1 else height / (len(legends) -1) + yl = y - writer.height / 2 # Start at bottom + for legend in legends: + Label(writer, int(yl), x, legend) + yl -= dy + 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 + return n + + def show(self): + if super().show(): # Draw or erase border + val = super().value() + wri = self.writer + width = self.width + height = self.height + 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) + display.hline(x0 + 2, ypos, x1 - x0 - 4, self.fgcolor) + + y = int(y1 - val * height) # y position of slider + if self.style == self.LINE: + display.hline(x0, y, width, self.ptcolor) # Draw pointer + else: + w = width / 2 + display.fill_rect(int(x0 + w - 2), y, 4, y1 - y, self.ptcolor) diff --git a/gui/widgets/scale.py b/gui/widgets/scale.py new file mode 100644 index 0000000..ecca0dc --- /dev/null +++ b/gui/widgets/scale.py @@ -0,0 +1,132 @@ +# scale.py Extension to micro-gui providing the Scale class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# Usage: +# from gui.widgets.scale import Scale + +from gui.core.ugui import LinearIO, display +from hardware_setup import ssd # Display driver for Writer +from gui.core.writer import Writer +from gui.core.colors import BLACK + +dolittle = lambda *_ : None + +class Scale(LinearIO): + def __init__(self, writer, row, col, *, + ticks=200, legendcb=None, tickcb=None, + height=0, width=100, bdcolor=None, fgcolor=None, bgcolor=None, + callback=dolittle, args=[], + pointercolor=None, fontcolor=None, value=0.0, active=False): + if ticks % 2: + raise ValueError('ticks arg must be divisible by 2') + self.ticks = ticks + self.tickcb = tickcb + def lcb(f): + return '{:3.1f}'.format(f) + self.legendcb = legendcb if legendcb is not None else lcb + bgcolor = BLACK if bgcolor is None else bgcolor + text_ht = writer.font.height() + ctrl_ht = 12 # Minimum height for ticks + # Add 2 pixel internal border to give a little more space + min_ht = text_ht + 6 # Ht of text, borders and gap between text and ticks + if height < min_ht + ctrl_ht: + height = min_ht + ctrl_ht # min workable height + else: + ctrl_ht = height - min_ht # adjust ticks for greater height + width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._to_int(value), active) + if active: + super()._set_callbacks(callback, args) + self.minval = -1.0 # By default scales run from -1.0 to +1.0 + self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor + self.x0 = col + 2 + self.x1 = col + self.width - 2 + self.y0 = row + 2 + self.y1 = row + self.height - 2 + self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor + # Define tick dimensions + ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap) + ycl = ytop + (self.y1 - ytop) // 2 # Centre line + self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick. + self.sdy0 = ycl - self.sdl // 2 + self.mdl = round(ctrl_ht * 2 / 3) # Medium tick + self.mdy0 = ycl - self.mdl // 2 + self.ldl = ctrl_ht # Large tick + self.ldy0 = ycl - self.ldl // 2 + self.draw = True # Ensure a redraw on next refresh + if active: # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + def show(self): + wri = self.writer + x0: int = self.x0 # Internal rectangle occupied by scale and text + x1: int = self.x1 + y0: int = self.y0 + y1: int = self.y1 + if super().show(): + # Scale is drawn using ints. Each division is 10 units. + val: int = self._value # 0..ticks*10 + # iv increments for each tick. Its value modulo N determines tick length + iv: int # val / 10 at a tick position + d: int # val % 10: offset relative to a tick position + fx: int # X offset of current tick in value units + if val >= 100: # Whole LHS of scale will be drawn + iv, d = divmod(val - 100, 10) # Initial value + fx = 10 - d + iv += 1 + else: # Scale will scroll right + iv = 0 + fx = 100 - val + + # Window shows 20 divisions, each of which corresponds to 10 units of value. + # So pixels per unit value == win_width/200 + win_width: int = x1 - x0 + ticks: int = self.ticks # Total # of ticks visible and hidden + while True: + x: int = x0 + (fx * win_width) // 200 # Current X position + ys: int # Start Y position for tick + yl: int # tick length + if x > x1 or iv > ticks: # Out of space or data (scroll left) + break + if not iv % 10: + txt = self.legendcb(self._fvalue(iv * 10)) + tlen = wri.stringlen(txt) + Writer.set_textpos(ssd, y0, min(x, x1 - tlen)) + wri.setcolor(self.fontcolor, self.bgcolor) + wri.printstring(txt) + wri.setcolor() + ys = self.ldy0 # Large tick + yl = self.ldl + elif not iv % 5: + ys = self.mdy0 + yl = self.mdl + else: + ys = self.sdy0 + yl = self.sdl + if self.tickcb is None: + color = self.fgcolor + else: + color = self.tickcb(self._fvalue(iv * 10), self.fgcolor) + display.vline(x, ys, yl, color) # Draw tick + fx += 10 + iv += 1 + + display.vline(x0 + (x1 - x0) // 2, y0, y1 - y0, self.ptrcolor) # Draw pointer + + def _to_int(self, v): + return round((v + 1.0) * self.ticks * 5) # 0..self.ticks*10 + + def _fvalue(self, v=None): + return v / (5 * self.ticks) - 1.0 + + def value(self, val=None): # User method to get or set value + if val is not None: + val = min(max(val, - 1.0), 1.0) + v = self._to_int(val) + if v != self._value: + self._value = v + self.draw = True # Ensure a redraw on next refresh + self.callback(self, *self.args) + return self._fvalue(self._value) diff --git a/gui/widgets/scale_log.py b/gui/widgets/scale_log.py new file mode 100644 index 0000000..d9cefc2 --- /dev/null +++ b/gui/widgets/scale_log.py @@ -0,0 +1,157 @@ +# scale_log.py Extension to micro-gui providing the ScaleLog class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# A logarithmic Scale which responds to user input +# Usage: +# from gui.widgets.scale_log import ScaleLog + + +import uasyncio as asyncio +from time import ticks_ms, ticks_diff +from math import log10 + +from gui.core.ugui import LinearIO, display +from hardware_setup import ssd # Display driver for Writer +from gui.core.writer import Writer +from gui.core.colors import BLACK + +# Null function +dolittle = lambda *_ : None + + +# Start value is 1.0. User applies scaling to value and ticks callback. +class ScaleLog(LinearIO): + def __init__(self, writer, row, col, *, + decades=5, height=0, width=160, + bdcolor=None, fgcolor=None, bgcolor=None, + pointercolor=None, fontcolor=None, + legendcb=None, tickcb=None, + callback=dolittle, args=[], + value=1.0, delta=0.01, active=False): + # For correct text rendering inside control must explicitly set bgcolor + bgcolor = BLACK if bgcolor is None else bgcolor + if decades < 3: + raise ValueError('decades must be >= 3') + self.mval = 10**decades # Max value + self.tickcb = tickcb + self.delta = delta # Min multiplier = 1 + delta + def lcb(f): + return '{:<1.0f}'.format(f) + self.legendcb = legendcb if legendcb is not None else lcb + text_ht = writer.height + ctrl_ht = 12 # Minimum height for ticks + min_ht = text_ht + 8 # Ht of text and gap between text and ticks + if height < min_ht + ctrl_ht: + height = min_ht + ctrl_ht # min workable height + else: + ctrl_ht = height - min_ht # adjust ticks for greater height + width &= 0xfffe # Make divisible by 2: avoid 1 pixel pointer offset + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, self._constrain(value), active) + if active: + super()._set_callbacks(callback, args) + self.fontcolor = fontcolor if fontcolor is not None else self.fgcolor + + self.x0 = col + 2 + self.x1 = col + self.width - 2 + self.y0 = row + 2 + self.y1 = row + self.height - 2 + self.ptrcolor = pointercolor if pointercolor is not None else self.fgcolor + # Define tick dimensions + ytop = self.y0 + text_ht + 2 # Top of scale graphic (2 pixel gap) + ycl = ytop + (self.y1 - ytop) // 2 # Centre line + self.sdl = round(ctrl_ht * 1 / 3) # Length of small tick. + self.sdy0 = ycl - self.sdl // 2 + self.mdl = round(ctrl_ht * 2 / 3) # Medium tick + self.mdy0 = ycl - self.mdl // 2 + self.ldl = ctrl_ht # Large tick + self.ldy0 = ycl - self.ldl // 2 + self.dw = (self.x1 - self.x0) // 2 # Pixel width of a decade + self.draw = True # Ensure a redraw on next refresh + if active: # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + # Pre calculated log10(x) for x in range(1, 10) + def show(self, logs=(0.0, 0.3010, 0.4771, 0.6021, 0.6990, 0.7782, 0.8451, 0.9031, 0.9542)): + #start = ticks_ms() + x0: int = self.x0 # Internal rectangle occupied by scale and text + x1: int = self.x1 + y0: int = self.y0 + y1: int = self.y1 + xc: int = x0 + (x1 - x0) // 2 # x location of pointer + dw: int = self.dw # Width of a decade in pixels + wri = self.writer + if super().show(): + vc = self._value # Current value, corresponds to centre of display + d = int(log10(vc)) - 1 # 10**d is start of a decade guaranteed to be outside display + vs = max(10 ** d, 1.0) # vs: start value of current decade + while True: # For each decade until we run out of space + done = True # Assume completion + xs: float = xc - dw * log10(vc / vs) # x location of start of scale + tick: int + q: float + # log10 ~38us on Pi Pico + for tick, q in enumerate(logs): + vt: float = vs * (1 + tick) # Value of current tick + x: int = round(xs + q * dw) # x location of current tick + if x >= x1: + break # All visible ticks drawn + elif x > x0: # Tick is visible + if not tick: + txt = self.legendcb(vt) + tlen = wri.stringlen(txt) + Writer.set_textpos(ssd, y0, min(x, x1 - tlen)) + wri.setcolor(self.fontcolor, self.bgcolor) + wri.printstring(txt) + ys = self.ldy0 # Large tick + yl = self.ldl + elif tick == 4: + ys = self.mdy0 + yl = self.mdl + else: + ys = self.sdy0 + yl = self.sdl + if self.tickcb is None: + color = self.fgcolor + else: + color = self.tickcb(vt, self.fgcolor) + display.vline(x, ys, yl, color) # Draw tick + if (not tick) and (vt > 0.999 * self.mval): + break # Drawn last tick at end of data + else: + vs *= 10 # More to do. Next decade. + done = False + if done: + break + + display.vline(xc, y0, y1 - y0, self.ptrcolor) # Draw pointer + #print(ticks_diff(ticks_ms(), start)) 75-95ms on Pyboard D depending on calbacks + + def _constrain(self, v): + return min(max(v, 1.0), self.mval) + + def value(self, val=None): # User method to get or set value + if val is not None: + v = self._constrain(val) + if self._value is None or v != self._value: + self._value = v + self.draw = True # Ensure a redraw on next refresh + self.callback(self, *self.args) + return self._value + + + async def btnhan(self, button, up): + up = up == 1 + delta = self.delta + maxdelta = 0.64 + smul= (1 + delta) if up else (1 / (1 + delta)) + self.value(self.value() * smul) + t = ticks_ms() + while not button(): + await asyncio.sleep_ms(0) # Quit fast on button release + if ticks_diff(ticks_ms(), t) > 500: # Button was held down + delta = min(maxdelta, delta * 2) + smul = (1 + delta) if up else (1 / (1 + delta)) + self.value(self.value() * smul) + t = ticks_ms() diff --git a/gui/widgets/sliders.py b/gui/widgets/sliders.py new file mode 100644 index 0000000..a8d6c30 --- /dev/null +++ b/gui/widgets/sliders.py @@ -0,0 +1,161 @@ +# sliders.py Extension to ugui providing linear "potentiometer" widgets. + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +from micropython import const +from gui.core.ugui import LinearIO, display # Class and instance +from gui.widgets.label import Label + +# Null function +dolittle = lambda *_ : None + +# *********** SLIDER CLASSES *********** +# A slider's text items lie outside its bounding box (area sensitive to touch) + +_SLIDE_DEPTH = const(6) # Must be divisible by 2 +_TICK_VISIBLE = const(3) # No. of tick pixels visible either side of slider +_HALF_SLOT_WIDTH = const(2) # Width of slot /2 + +# Slider ontrols have been rewritten to avoid the need for reading back framebuffer +# contents as this is unreliable on RA8875. +class Slider(LinearIO): + def __init__(self, writer, row, col, *, + height=100, width=20, divisions=10, legends=None, + fgcolor=None, bgcolor=None, fontcolor=None, bdcolor=None, slotcolor=None, + callback=dolittle, args=[], value=0.0, active=True): + width &= 0xfe # ensure divisible by 2 + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active) + if active: + super()._set_callbacks(callback, args) + self.divisions = divisions + self.legends = legends + self.fontcolor = self.fgcolor if fontcolor is None else fontcolor + self.slotcolor = self.bgcolor if slotcolor is None else slotcolor + # Define slider + self.slide_x0 = col + _TICK_VISIBLE # Draw slider shorter than ticks + self.slide_w = width - 2 * _TICK_VISIBLE + # y coord of top left hand corner of slide when value == 0 + self.slide_y0 = row + height - _SLIDE_DEPTH - 1 + # Slot coordinates + centre = col + width // 2 + self.slot_x0 = centre - _HALF_SLOT_WIDTH + self.slot_y0 = row + _SLIDE_DEPTH // 2 + self.slot_h = height - _SLIDE_DEPTH - 1 + # Prevent Label objects being added to display list when already there. + self.drawn = False + self.draw = True # Ensure a redraw on next refresh + if active: # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + def show(self): + # Blank slot, ticks and slider + if super().show(False): # Honour bgcolor + x = self.col + y = self.slot_y0 + # Length of travel of slider + slot_len = self.slot_h # Dimensions of slot + slot_w = 2 * _HALF_SLOT_WIDTH + if self.divisions > 0: + dy = slot_len / (self.divisions) # Tick marks + xs = x + 1 + xe = x + self.width -1 + for tick in range(self.divisions + 1): + ypos = int(y + dy * tick) + display.line(xs, ypos, xe, ypos, self.fgcolor) + # Blank and redraw slot + display.fill_rect(self.slot_x0, self.slot_y0, slot_w, slot_len, self.slotcolor) + display.rect(self.slot_x0, self.slot_y0, slot_w, slot_len, self.fgcolor) + + # Legends: if redrawing, they are already on the Screen's display list + if self.legends is not None and not self.drawn: + if len(self.legends) <= 1: + dy = 0 + else: + dy = slot_len / (len(self.legends) -1) + yl = y + slot_len # Start at bottom + wri = self.writer + fhdelta = wri.height / 2 + for legend in self.legends: + Label(wri, int(yl - fhdelta), x + self.width + 4, legend, fgcolor = self.fontcolor) + yl -= dy + + slide_y = round(self.slide_y0 - self._value * slot_len) + display.fill_rect(self.slide_x0, slide_y, self.slide_w, _SLIDE_DEPTH, self.fgcolor) + self.drawn = True + + def color(self, color): + if color != self.fgcolor: + self.fgcolor = color + self.draw = True + + +class HorizSlider(LinearIO): + def __init__(self, writer, row, col, *, + height=20, width=100, divisions=10, legends=None, + fgcolor=None, bgcolor=None, fontcolor=None, bdcolor=None, + slotcolor=None, + callback=dolittle, args=[], value=0.0, active=True): + height &= 0xfe # ensure divisible by 2 + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, value, active) + if active: + super()._set_callbacks(callback, args) + self.divisions = divisions + self.legends = legends + self.fontcolor = self.fgcolor if fontcolor is None else fontcolor + self.slotcolor = self.bgcolor if slotcolor is None else slotcolor + + # Define slider + self.slide_y0 = row + _TICK_VISIBLE # Draw slider shorter than ticks + self.slide_h = height - 2 * _TICK_VISIBLE + + # Slot coordinates + self.slot_x0 = col + _SLIDE_DEPTH // 2 + self.slot_w = width - _SLIDE_DEPTH - 1 + centre = row + height // 2 + self.slot_y0 = centre - _HALF_SLOT_WIDTH + # Prevent Label objects being added to display list when already there. + self.drawn = False + self.draw = True # Ensure a redraw on next refresh + if active: # Run callback (e.g. to set dynamic colors) + self.callback(self, *self.args) + + def show(self): + # Blank slot, ticks and slider + if super().show(False): # Honour bgcolor + x = self.slot_x0 + y = self.row + slot_len = self.slot_w # Slot dimensions + slot_h = 2 * _HALF_SLOT_WIDTH + if self.divisions > 0: + dx = slot_len / (self.divisions) # Tick marks + ys = y + 1 + ye = y + self.height - 1 + for tick in range(self.divisions + 1): + xpos = int(x + dx * tick) + display.line(xpos, ys, xpos, ye, self.fgcolor) + # Blank and redraw slot + display.fill_rect(self.slot_x0, self.slot_y0, slot_len, slot_h, self.slotcolor) + display.rect(self.slot_x0, self.slot_y0, slot_len, slot_h, self.fgcolor) + + # Legends: if redrawing, they are already on the Screen's display list + if self.legends is not None and not self.drawn: + if len(self.legends) <= 1: + dx = 0 + else: + dx = slot_len / (len(self.legends) -1) + xl = x + wri = self.writer + for legend in self.legends: + offset = wri.stringlen(legend) / 2 + Label(wri, y - wri.height - 4, int(xl - offset), legend, fgcolor = self.fontcolor) + xl += dx + + self.slide_x = round(self.col + self._value * slot_len) + display.fill_rect(self.slide_x, self.slide_y0, _SLIDE_DEPTH, self.slide_h, self.fgcolor) + self.drawn = True + + def color(self, color): + if color != self.fgcolor: + self.fgcolor = color + self.draw = True diff --git a/gui/widgets/textbox.py b/gui/widgets/textbox.py new file mode 100644 index 0000000..990f837 --- /dev/null +++ b/gui/widgets/textbox.py @@ -0,0 +1,140 @@ +# textbox.py Extension to nanogui providing the Textbox class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2020 Peter Hinch + +# Usage: +# from gui.widgets.textbox import Textbox + +from gui.core.ugui import LinearIO +from hardware_setup import ssd # Display driver for Writer +from gui.core.writer import Writer + +from time import ticks_diff, ticks_ms +import uasyncio as asyncio + +# Reason for no tab support in nano-gui/private/reason_for_no_tabs + +class Textbox(LinearIO): + def __init__(self, writer, row, col, width, nlines, *, bdcolor=None, fgcolor=None, + bgcolor=None, clip=True, active=False): + height = nlines * writer.height + devht = writer.device.height + devwd = writer.device.width + if ((row + height + 2) > devht) or ((col + width + 2) > devwd): + raise ValueError('Textbox extends beyond physical screen.') + super().__init__(writer, row, col, height, width, + fgcolor, bgcolor, bdcolor, 0, active) + self.nlines = nlines + self.clip = clip + self.lines = [] + self.start = 0 # Start line for display + + def _add_lines(self, s): + width = self.width + font = self.writer.font + n = -1 # Index into string + newline = True + while True: + n += 1 + if newline: + newline = False + ls = n # Start of line being processed + col = 0 # Column relative to text area + if n >= len(s): # End of string + if n > ls: + self.lines.append(s[ls :]) + return + c = s[n] # Current char + if c == '\n': + self.lines.append(s[ls : n]) + newline = True + continue # Line fits window + col += font.get_ch(c)[2] # width of current char + if col > width: + if self.clip: + p = s[ls :].find('\n') # end of 1st line + if p == -1: + self.lines.append(s[ls : n]) # clip, discard all to right + return + self.lines.append(s[ls : n]) # clip, discard to 1st newline + n = p # n will move to 1st char after newline + elif c == ' ': # Easy word wrap + self.lines.append(s[ls : n]) + else: # Edge splits a word + p = s.rfind(' ', ls, n + 1) + if p >= 0: # spacechar in line: wrap at space + assert (p > 0), 'space char in position 0' + self.lines.append(s[ls : p]) + n = p + else: # No spacechar: wrap at end + self.lines.append(s[ls : n]) + n -= 1 # Don't skip current char + newline = True + + def _print_lines(self): + if len(self.lines) == 0: + return + + wri = self.writer + col = self.col + row = self.row + left = col + ht = wri.height + wri.setcolor(self.fgcolor, self.bgcolor) + # Print the first (or last?) lines that fit widget's height + #for line in self.lines[-self.nlines : ]: + for line in self.lines[self.start : self.start + self.nlines]: + Writer.set_textpos(ssd, row, col) + wri.printstring(line) + row += ht + col = left + wri.setcolor() # Restore defaults + + def show(self): + if super().show(): + self._print_lines() + + def append(self, s, ntrim=None, line=None): + self._add_lines(s) + if ntrim is None: # Default to no. of lines that can fit + ntrim = self.nlines + if len(self.lines) > ntrim: + self.lines = self.lines[-ntrim:] + self.goto(line) + + def scroll(self, n): # Relative scrolling + value = len(self.lines) + if n == 0 or value <= self.nlines: # Nothing to do + return False + s = self.start + self.start = max(0, min(self.start + n, value - self.nlines)) + if s != self.start: + self.draw = True # Cause a refresh + return True + return False + + def value(self): + return len(self.lines) + + def clear(self): + self.lines = [] + self.draw = True # Cause a refresh + + def goto(self, line=None): # Absolute scrolling + if line is None: + self.start = max(0, len(self.lines) - self.nlines) + else: + self.start = max(0, min(line, len(self.lines) - self.nlines)) + self.draw = True # Cause a refresh + + async def btnhan(self, button, up): + self.scroll(-up) + t = ticks_ms() + d = 1 + while not button(): + await asyncio.sleep_ms(0) # Quit fast on button release + if ticks_diff(ticks_ms(), t) > 500: # Button was held down + d = min(16, d * 2) + self.scroll(-up * d) + t = ticks_ms() diff --git a/gui/widgets/vectors.py b/gui/widgets/vectors.py new file mode 100644 index 0000000..3df570f --- /dev/null +++ b/gui/widgets/vectors.py @@ -0,0 +1,120 @@ +# vectors.py Extension to ugui providing vector display + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# VectorDial class is display-only: no practical way to perform +# input on multiple vectors + +from micropython import const +from gui.core.ugui import Screen, Widget, display +from gui.widgets.label import Label +from gui.core.colors import * +import cmath + +conj = lambda v : v.real - v.imag * 1j # Complex conjugate + +# Draw a vector in complex coordinates. Origin and end are complex. +# End is relative to origin. +def pline(origin, vec, color): + xs, ys = origin.real, origin.imag + display.line(round(xs), round(ys), round(xs + vec.real), round(ys - vec.imag), color) + +# 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. +def arrow(origin, vec, lc, color): + ccw = cmath.exp(3j * cmath.pi/4) # Unit vectors + 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 + pline(origin, vec, color) # Origin to tip + pline(origin, start, color) # Origin to tail + pline(origin + conj(vec), chev*ccw*uv, color) # Tip chevron + pline(origin + conj(vec), chev*cw*uv, color) + if length > lc: # Confusing appearance of very short vectors with tail chevron + pline(origin + conj(start), chev*ccw*uv, color) # Tail chevron + pline(origin + conj(start), chev*cw*uv, color) + +# Vector display +class Pointer: + def __init__(self, dial): + dial.vectors.add(self) + self.dial = dial + self.color = WHITE + self.val = 0j + + def value(self, v=None, color=None): + if color is not None: + self.color = color + if v is not None: + if isinstance(v, complex): + l = cmath.polar(v)[0] + newval = v / l if l > 1 else v # Max length = 1.0 + else: + raise ValueError('Pointer value must be complex.') + if v != self.val and self.dial.screen is Screen.current_screen: + self.show(newval) + self.val = newval + return self.val + + def show(self, newval=None): + v = self.val if newval is None else newval + dial = self.dial + color = self.color + vor = dial.vor # Dial's origin as a vector + r = dial.radius * (1 - dial.TICKLEN) + if dial.arrow: + arrow(vor, r * v, 5, color) + else: + pline(vor, r * v, color) + self.dial.draw = True # Mark dial as dirty + + +class VectorDial(Widget): + TICKLEN = 0.1 + def __init__(self, writer, row, col, *, + height=100, fgcolor=None, bgcolor=None, bdcolor=None, + ticks=4, arrow=False, pip=None): + super().__init__(writer, row, col, height, height, + fgcolor, bgcolor, bdcolor, 0, False) # Display only + self.arrow = arrow + self.pip = self.fgcolor if pip is None else pip + radius = height / 2 + self.radius = radius + self.ticks = ticks + self.xorigin = col + radius + self.yorigin = row + radius + self.vor = self.xorigin + 1j * self.yorigin # Origin as a vector + self.vectors = set() + self.draw = True + + def show(self): + if super().show(): + # cache bound variables + ticks = self.ticks + radius = self.radius + xo = self.xorigin + yo = self.yorigin + vor = self.vor + vtstart = (1 - self.TICKLEN) * radius + 0j # start of tick + vtick = self.TICKLEN * radius + 0j # tick + vrot = cmath.exp(2j * cmath.pi/ticks) # unit rotation + for _ in range(ticks): + pline(vor + conj(vtstart), vtick, self.fgcolor) + vtick *= vrot + vtstart *= vrot + display.circle(xo, yo, radius, self.fgcolor) + + vshort = 1000 # Length of shortest vector + for v in self.vectors: + val = v.value() * radius # val is complex + vshort = min(vshort, cmath.polar(val)[0]) + v.show() + if isinstance(self.pip, tuple) and vshort > 9: + display.fillcircle(xo, yo, 3, self.pip) + self.draw = False diff --git a/hardware_setup.py b/hardware_setup.py new file mode 100644 index 0000000..c96abf6 --- /dev/null +++ b/hardware_setup.py @@ -0,0 +1,55 @@ +# ili9341_pico.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# As written, supports: +# ili9341 240x320 displays on Pi Pico +# Edit the driver import for other displays. + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING +# Pico Display +# GPIO Pin +# 3v3 36 Vin +# IO6 9 CLK Hardware SPI0 +# IO7 10 DATA (AKA SI MOSI) +# IO8 11 DC +# IO9 12 Rst +# Gnd 13 Gnd +# IO10 14 CS + +# Pushbuttons are wired between the pin and Gnd +# Pico pin Meaning +# 16 Operate current control +# 17 Decrease value of current control +# 18 Select previous control +# 19 Select next control +# 20 Increase value of current control + +from machine import Pin, SPI, freq +import gc + +from drivers.ili93xx.ili9341 import ILI9341 as SSD +freq(250_000_000) # RP2 overclock +# Create and export an SSD instance +pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins +prst = Pin(9, Pin.OUT, value=1) +pcs = Pin(10, Pin.OUT, value=1) +spi = SPI(0, baudrate=30_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, usd=True) + +from gui.core.ugui import Display, setup +# Create and export a Display instance +# Define control buttons +nxt = Pin(19, Pin.IN, Pin.PULL_UP) # Move to next control +sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control +prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control +increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value +decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value +display = Display(ssd, nxt, sel, prev, increase, decrease) +setup(display) diff --git a/setup_examples/ili9341_pico.py b/setup_examples/ili9341_pico.py new file mode 100644 index 0000000..c96abf6 --- /dev/null +++ b/setup_examples/ili9341_pico.py @@ -0,0 +1,55 @@ +# ili9341_pico.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch + +# As written, supports: +# ili9341 240x320 displays on Pi Pico +# Edit the driver import for other displays. + +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING +# Pico Display +# GPIO Pin +# 3v3 36 Vin +# IO6 9 CLK Hardware SPI0 +# IO7 10 DATA (AKA SI MOSI) +# IO8 11 DC +# IO9 12 Rst +# Gnd 13 Gnd +# IO10 14 CS + +# Pushbuttons are wired between the pin and Gnd +# Pico pin Meaning +# 16 Operate current control +# 17 Decrease value of current control +# 18 Select previous control +# 19 Select next control +# 20 Increase value of current control + +from machine import Pin, SPI, freq +import gc + +from drivers.ili93xx.ili9341 import ILI9341 as SSD +freq(250_000_000) # RP2 overclock +# Create and export an SSD instance +pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins +prst = Pin(9, Pin.OUT, value=1) +pcs = Pin(10, Pin.OUT, value=1) +spi = SPI(0, baudrate=30_000_000) +gc.collect() # Precaution before instantiating framebuf +ssd = SSD(spi, pcs, pdc, prst, usd=True) + +from gui.core.ugui import Display, setup +# Create and export a Display instance +# Define control buttons +nxt = Pin(19, Pin.IN, Pin.PULL_UP) # Move to next control +sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control +prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control +increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value +decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value +display = Display(ssd, nxt, sel, prev, increase, decrease) +setup(display) diff --git a/setup_examples/st7789_ttgo.py b/setup_examples/st7789_ttgo.py new file mode 100644 index 0000000..f423d43 --- /dev/null +++ b/setup_examples/st7789_ttgo.py @@ -0,0 +1,123 @@ +# color_setup.py Customise for your hardware config + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa + +# Supports: +# TTGO T-Display 1.14" 135*240(Pixel) based on ST7789V +# http://www.lilygo.cn/claprod_view.aspx?TypeId=62&Id=1274 +# http://www.lilygo.cn/prod_view.aspx?TypeId=50044&Id=1126 +# https://github.com/Xinyuan-LilyGO/TTGO-T-Display +# https://github.com/Xinyuan-LilyGO/TTGO-T-Display/blob/master/image/pinmap.jpg +# https://github.com/Xinyuan-LilyGO/TTGO-T-Display/blob/master/schematic/ESP32-TFT(6-26).pdf + +# WIRING (TTGO T-Display pin numbers and names). +# Pinout of TFT Driver +# ST7789 ESP32 +# TFT_MISO N/A +TFT_MOSI = 19 # (SDA on schematic pdf) SPI interface output/input pin. +TFT_SCLK = 18 # This pin is used to be serial interface clock. +TFT_CS = 5 # Chip selection pin, low enable, high disable. +TFT_DC = 16 # Display data/command selection pin in 4-line serial interface. +TFT_RST = 23 # This signal will reset the device,Signal is active low. +TFT_BL = 4 # (LEDK on schematic pdf) Display backlight control pin + +ADC_IN = 34 # Measuring battery or USB voltage, see comment below +ADC_EN = 14 # (PWR_EN on schematic pdf) is the ADC detection enable port + +BUTTON1 = 35 # right of the USB connector +BUTTON2 = 0 # left of the USB connector + +# ESP32 pins, free for use in user applications +#I2C_SDA = 21 # hardware ID 0 +#I2C_SCL = 22 + +#UART2TXD = 17 + +#GPIO2 = 2 +#GPIO15 = 15 +#GPIO13 = 13 +#GPIO12 = 12 + +#GPIO37 = 37 +#GPIO38 = 38 +#UART1TXD = 4 +#UART1RXD = 5 +#GPIO18 = 18 +#GPIO19 = 19 +#GPIO17 = 17 + +#DAC1 = 25 +#DAC2 = 26 + +# Input only pins +#GPIO36 = 36 # input only +#GPIO39 = 39 # input only + +# 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. + +from machine import Pin, SPI, ADC, freq +import gc + +from drivers.st7789.st7789_4bit import * +SSD = ST7789 + +pdc = Pin(TFT_DC, Pin.OUT, value=0) # Arbitrary pins +pcs = Pin(TFT_CS, Pin.OUT, value=1) +prst = Pin(TFT_RST, Pin.OUT, value=1) +pbl = Pin(TFT_BL, Pin.OUT, value=1) + +gc.collect() # Precaution before instantiating framebuf +# Conservative low baudrate. Can go to 62.5MHz. +spi = SPI(1, 30_000_000, sck=Pin(TFT_SCLK), mosi=Pin(TFT_MOSI)) +freq(160_000_000) + +''' TTGO + v +----------------+ + 40 | | | + ^ | +------+ | pin 36 + | | | | | + | | | | | +240 | | | | | + | | | | | + | | | | | + v | +------+ | + 40 | | | Reset button + ^ +----------------+ + >----<------>----< + 52 135 xx + BUTTON2 BUTTON1 +''' +# Right way up landscape: defined as top left adjacent to pin 36 +ssd = SSD(spi, height=135, width=240, dc=pdc, cs=pcs, rst=prst, disp_mode=LANDSCAPE, display=TDISPLAY) +# Normal portrait display: consistent with TTGO logo at top +# ssd = SSD(spi, height=240, width=135, dc=pdc, cs=pcs, rst=prst, disp_mode=PORTRAIT, display=TDISPLAY) + +from gui.core.ugui import Display, setup +# Create and export a Display instance +# Define control buttons +nxt = Pin(36, Pin.IN, Pin.PULL_UP) # Move to next control +sel = Pin(37, Pin.IN, Pin.PULL_UP) # Operate current control +prev = Pin(38, Pin.IN, Pin.PULL_UP) # Move to previous control +increase = Pin(39, Pin.IN, Pin.PULL_UP) # Increase control's value +decrease = Pin(32, Pin.IN, Pin.PULL_UP) # Decrease control's value +display = Display(ssd, nxt, sel, prev, increase, decrease) +setup(display) + +# optional +# b1 = Pin(BUTTON1, Pin.IN) +# b2 = Pin(BUTTON2, Pin.IN) +# adc_en = Pin(ADC_EN, Pin.OUT, value=1) +# adc_in = ADC(Pin(ADC_IN)) +# adc_en.value(0) +''' +Set ADC_EN to "1" and read voltage in BAT_ADC, +if this voltage more than 4.3 V device have powered from USB. +If less then 4.3 V - device have power from battery. +To save battery you can set ADC_EN to "0" and in this case the USB converter +will be power off and do not use your battery. +When you need to measure battery voltage first set ADC_EN to "1", +measure voltage and then set ADC_EN back to "0" for save battery. +'''