Code Monkey home page Code Monkey logo

micropython-nano-gui's Introduction

MicroPython nano-gui

A lightweight and minimal MicroPython GUI library for display drivers based on the FrameBuffer class. It is portable between a range of MicroPython hosts and display devices. Various display technologies are supported, including small color and monochrome OLED's, color TFT's, ePaper and Sharp units.

The nano-gui library is display-only. A library supporting user input is micro-gui; this comes at a cost of a substantially greater RAM requirement. It supports all displays available to nano-gui.

These images, most from OLED displays, fail to reproduce the quality of these displays. OLEDs are visually impressive displays with bright colors, wide viewing angle and extreme contrast. For some reason I find them hard to photograph.
Image The aclock.py demo.

Image Label objects in two fonts.

Image One of the demos running on an Adafruit 1.27 inch OLED. The colors change dynamically with low values showing green, intermediate yellow and high red.

Image The alevel.py demo. The Pyboard was mounted vertically: the length and angle of the vector arrow varies as the Pyboard is moved.

There is an optional graph plotting module for basic Cartesian and polar plots, also real time plotting including time series.

Image A sample image from the plot module.

These images from a TFT display illustrate the new widgets.
Image The Scale widget. Capable of precision display of floats as the notionally very long scale moves behind its small window.

Image The Textbox widget for scrolling text with word wrap or clipping.

Image A mockup of a seismograph screen on an ePaper display.

Image Circular display using gc9a01 controller.

Contents

  1. Introduction
    1.1 Change log
    1.2 Description
    1.3 Quick start Run without actually installing it.
         1.3.1 Quick install
    1.4 A performance boost
  2. Files and Dependencies
    2.1 Files
         2.1.1 Core files
         2.1.2 Demo Scripts
         2.1.3 Fonts
         2.1.4 Hardware setup examples
    2.2 Dependencies
    2.3 Verifying hardware configuration
  3. The nanogui module
    3.1 Application Initialisation Initial setup and refresh method.
         3.1.1 User defined colors
         3.1.2 Monochrome displays A slight "gotcha" with ePaper.
         3.1.3 Display update mechanism How updates are managed.
         3.1.4 ePaper displays New developments in ePaper.
    3.2 Label class Dynamic text at any screen location.
    3.3 Meter class A vertical panel meter.
    3.4 LED class Virtual LED of any color.
    3.5 Dial and Pointer classes Clock or compass style display of one or more pointers.
    3.6 Scale class Linear display with wide dynamic range.
    3.7 Class Textbox Scrolling text display.
  4. ESP8266 This can work. Contains information on minimising the RAM and flash footprints of the GUI.
    Appendix 1 Freezing bytecode Optional way to save RAM.
    Appendix 2 Round displays Alternative hardware check script.

The extras directory contains further widgets back-ported from micro-gui plus further demos and information. The aim is to avoid this document becoming over long and daunting to new users.

1. Introduction

This library provides a limited set of GUI objects (widgets) for displays whose display driver is subclassed from the FrameBuffer class. The drivers can have minimal code as graphics primitives are supplied by the FrameBuffer class.

Compatible and tested displays are detailed here. The device driver doc provides guidance on selecting the right driver for your display, platform and application.

The GUI is cross-platform. The device driver doc explains how to configure it for a given display and MicroPython host by adapting a single small file. The GUI supports multiple displays attached to a single target, but bear in mind the RAM requirements for multiple frame buffers. The GUI has been tested on Pyboard 1.1, Pyboard D, Raspberry Pi Pico and on the ESP32 reference board without SPIRAM. Running on ESP8266 is possible but frozen bytecode must be used owing to its restricted RAM - see Appendix 1 Freezing bytecode.

It uses synchronous code but is compatible with asyncio. Some demo programs illustrate this. Code is standard MicroPython, but some device drivers use the native and viper decorators.

The GUI is display-only and lacks provision for user input. Authors of applications requiring touch should consider the touch GUI's for the following displays:

For historical reasons and to ensure consistency, code and documentation for my GUI's employ the American spelling of color.

1.1 Change log

22 May 2024 Support circular displays with gc9a01 controller. 15 Mar 2023 Driver update to 4.2 inch Waveshare ePpaper display.
12 Feb 2023 Add support for sh1106 driver. Fix color compatibility of SSD1306.
5 Sep 2022 Add support for additional Pico displays.
8 Aug 2022 Typo and grammar fixes from @bfiics.
10 May 2022 Support Waveshare Pi Pico displays.
7 Sep 2021 Code reduction and faster color text display. Color use now requires firmware V1.17 or later.
26 Aug 2021 Support PR7682 for fast text rendering.
25 Apr 2021 Support TTGO T-Display.
26 Mar 2021 Add ST7789. Alter asyncio support on ili9341.

1.2 Description

Widgets are intended for the display of data from physical devices such as sensors. They are drawn using graphics primitives rather than icons to minimise RAM usage. It also enables them to be efficiently rendered at arbitrary scale by hosts with restricted processing power. The approach also enables widgets to maximise information in ways that are difficult with icons, in particular using dynamic color changes in conjunction with moving elements.

Copying the contents of the frame buffer to the display is relatively slow. The time depends on the size of the frame buffer and the interface speed, but the latency may be too high for applications such as games. For example the time to update a 128x128x8 color ssd1351 display on a Pyboard 1.0 is 41ms.

Drivers based on FrameBuffer must allocate contiguous RAM for the buffer. To avoid 'out of memory' errors it is best to instantiate the display before importing other modules. The example color_setup files illustrate this.

1.3 Quick start

An 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 edit the color_setup.py file on your PC to match your hardware. Install mpremote with:

$ pip3 install mpremote

Clone the repo to your PC with:

$ git clone https://github.com/peterhinch/micropython-nano-gui
$ cd micropython-nano-gui

As supplied, color_setup.py assumes a Pyboard (1.x or D) connected to an Adafruit 1.27" OLED as specified in that file. If that doesn't correspond to your hardware, it should be edited to suit. See example files in the setup_examples directory.

$ mpremote mount .

This should provide a REPL. Run a demo:

>>> import gui.demos.aclock

The directory setup_examples has examples of files to match various displays and targets. If one of these matches your hardware, it may be copied to the root as color_setup.py.

Note that the gui.demos.aclock.py demo comprises 38 lines of actual code. This stuff is easier than you might think.

1.3.1 Quick install

On networked hardware this is done with mip which is included in recent firmware. On non-networked hardware this is done using the official mpremote utility which should be installed on the PC as described above.

Networked hardware

The easy approach is to copy the entire GUI to your hardware using mip

>>> import mip
>>> mip.install("github:peterhinch/micropython-nano-gui")

Substantial pruning can be done to eliminate unused fonts, widgets and demos. The appropriate driver for the display hardware is installed as follows (example is for ST7789):

>>> mip.install("github:peterhinch/micropython-nano-gui/drivers/st7789")

The last part of the addresss (st7789) is the name of the directory holding drivers for the display in use.

After editing color_setup.py as discussed above it should be copied to the target hardware with:

$ mpremote cp color_setup.py :

Non networked hardware

Installation is as per networked hardware except that mip on the target is replaced by mpremote mip on the PC:

$ mpremote mip install "github:peterhinch/micropython-nano-gui"
$ mpremote mip install "github:peterhinch/micropython-nano-gui/drivers/st7789"

1.4 A performance boost

Use of color displays now requires firmware V1.17 or later which offered a performance boost. If upgrading nano-gui from an installation which pre-dated V1.17 the display driver and GUI core files should be updated and the new file drivers/boolpalette.py must exist.

2. Files and Dependencies

On monochrome displays firmware should be V1.13 or later. On the Pi Pico firmware should be V1.15 or later. For color displays it should be V1.17 or later.

Installation comprises copying the gui and drivers directories, with their contents, plus a hardware configuration file, to the target. The directory structure on the target must match that in the repo. This consumes about 300KiB of flash.

Filesystem space may be conserved by copying only the required driver from drivers, but the directory path to that file must be retained. For example, for SSD1351 displays only the following are actually required:
drivers/ssd1351/ssd1351.py, drivers/ssd1351/__init__.py.

The small color_setup.py file contains all hardware definitions (for color or monochrome displays). This is the only file which will require editing to match the display and its wiring. For information on how to do this, see the drivers document.

2.1 Files

2.1.1 Core files

The root directory contains an example setup file color_setup.py for a color OLED display. Other examples may be found in the setup_examples directory. These are templates for adaptation: only one file is copied to the target. On the target the file should be named color_setup.py and put in the root of the filesystem.

The chosen template will need to be edited to match the display in use, the MicroPython target and the electrical connections between display and target. Electrical connections are detailed in the template source.

  • color_setup.py Hardware setup for the display. As written supports an SSD1351 display connected to a Pyboard.

The gui/core directory contains the GUI core and its principal dependencies:

  • nanogui.py The library.
  • writer.py Module for rendering Python fonts.
  • fplot.py The graph plotting module.
  • colors.py Color constants.

2.1.2 Demo scripts

The gui/demos directory contains test/demo scripts.

Demos for small displays:

  • mono_test.py Tests/demos using the official SSD1306 or SH1106 driver for monochrome 128*64 OLED displays.
  • color96.py Tests/demos for the Adafruit 0.96 inch color OLED.

Demos for larger displays.

  • color15.py Demonstrates a variety of widgets. Cross platform.
  • aclock.py Analog clock demo. Cross platform.
  • alevel.py Spirit level using Pyboard accelerometer.
  • fpt.py Plot demo. Cross platform.
  • scale.py A demo of the Scale widget. Cross platform. Uses asyncio.
  • asnano_sync.py Two Pyboard specific demos using the GUI with asyncio.
  • asnano.py Could readily be adapted for other targets.
  • tbox.py Demo Textbox class. Cross-platform.
  • round.py Demo for 240*240 circular displays.

Demos for ePaper displays:

  • epd_async.py Demo of asynchronous code on an eInk display. Needs a large display.
  • epd29_sync.py Demo for Adafruit 2.9" eInk display: emulates a seismograph.
  • epd29_async.py Asynchronous demo for Adafruit 2.9" eInk display.
  • epd29_lowpower.py Micropower demo for Adafruit 2.9" eInk display. This doc Micropower use should be read before attempting to run this.

Demos for Sharp displays:

  • sharptest.py Basic functionality check.
  • clocktest.py Digital and analog clock demo.
  • clock_batt.py Low power demo of battery operated clock.

Usage with asyncio is discussed here. In summary the GUI works well with asyncio but the blocking which occurs during transfer of the framebuffer to the display may affect more demanding applications. Some display drivers have an additional asynchronous refresh method. This may optionally be used to mitigate the resultant latency.

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: the ESP8266 provides an illustration.

To create alternatives, Python fonts may be generated from industry standard font files with font_to_py.py. 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.

2.1.4 Hardware setup examples

The setup_examples directory contains example setup files for various hardware. These are templates which may be adapted to suit the hardware in use, then copied to the hardware root as color_setup.py. Example files:

  • ssd1306_pyb.py Setup file for monochrome displays using the official driver. Supports hard or soft SPI or I2C connections.
  • ssd1106_spi_pico.py Setup file for monochrome displays. Supports hard or soft SPI or I2C connections.
  • ssd1351_esp32.py As written supports an ESP32 connected to a 128x128 SSD1351 display. After editing to match the display and wiring, it should be copied to the target as /pyboard/color_setup.py.
  • ssd1351_esp8266.py Similar for ESP8266. Usage is somewhat experimental.
  • st7735r_pyb.py Assumes a Pyboard with an Adafruit 1.8 inch TFT display.
  • st7735r144_pyb.py For a Pyboard with an Adafruit 1.44 inch TFT display.
  • ili9341_esp32.py A 240*320 ILI9341 display on ESP32.
  • st7789_pico.py Example with SSD7789 driver and Pi Pico host.
  • st7789_ttgo.py Setup for the TTGO T-Display device.
  • waveshare_pyb.py 176*274 ePaper display on Pyboard.
  • epd29_pyb_sync.py Adafruit 2.9 inch ePaper display for synchronous code.
  • epd29_pyb_async.py Adafruit 2.9 inch ePaper display: asyncio applications.

2.2 Dependencies

The source tree now includes all dependencies. These are listed to enable users to check for newer versions:

  • 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:

A copy of the unofficial driver for OLED displays using the SH1106 chip is provided. The unofficial file is here:

Displays based on the Nokia 5110 (PCD8544 chip) require this driver. It is not in this repo but may be found here:

2.3 Verifying hardware configuration

This script performs a basic check that the color_setup.py file matches the hardware, that (on color units) all three primary colors can be displayed and that pixels up to the edges of the display can be accessed. It is highly recommended that this be run on any new installation.

from color_setup import ssd  # Create a display instance
from gui.core.colors import RED, BLUE, GREEN
from gui.core.nanogui import refresh
refresh(ssd, True)  # Initialise and clear display.
# Uncomment for ePaper displays
# ssd.wait_until_ready()
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
refresh(ssd)

For round displays please see Appendix 2 Round displays for a suitable hardware check script.

3. The nanogui module

The GUI supports a variety of widgets, some of which include text elements. The coordinates of a widget are those of its top left corner. If a border is specified, this is drawn outside of the limits of the widgets with a margin of 2 pixels. If the widget is placed at [row, col] the top left hand corner of the border is at [row-2, col-2].

When a widget is drawn or updated (typically with its value method) it is not immediately displayed. To update the display nanogui.refresh is called: this enables multiple updates to the FrameBuffer contents before once copying the buffer to the display. Postponement enhances performance providing a visually instant update.

Text components of widgets are rendered using the Writer (monochrome) or CWriter (colour) classes.

3.1 Application Initialisation

The GUI is initialised by issuing:

from color_setup import ssd

This defines the hardware as described in the drivers document.

A typical application then imports nanogui modules and clears the display:

from gui.core.nanogui import refresh
from gui.widgets.label import Label  # Import any widgets you plan to use
from gui.widgets.dial import Dial, Pointer
refresh(ssd, True)  # Initialise and clear display.

Initialisation of color text display follows. For each font a CWriter instance is created:

from gui.core.writer import CWriter  # Renders color text
from gui.fonts import arial10  # A Python Font
from gui.core.colors import *  # Standard color constants

CWriter.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
 # Instantiate any CWriters to be used (one for each font)
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)  # Colors are defaults
# wri = Writer(ssd, arial10, verbose=False)  # Monochrome display uses Writer
wri.set_clip(True, True, False)

Initialisation of monochrome text display follows. For each font a Writer instance is created:

from gui.core.writer import Writer  # Renders color text
from gui.fonts import arial10

Writer.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
# Instantiate any Writers to be used (one for each font)
wri = Writer(ssd, arial10, verbose=False)  # Monochrome display uses Writer
wri.set_clip(True, True, False)

Calling nanogui.refresh on startup sets up and clears the display. The method will subsequently be called whenever a refresh is required. It takes two args:

  1. device The display instance (the GUI supports multiple displays).
  2. clear=False If set True the display will be blanked; it is also blanked when a device is refreshed for the first time.

3.1.1 User defined 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:

from color_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:

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. See the vari_fields function in the demo color15.py for an example.

3.1.2 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. Consequently demos written for color displays will work on monochrome units.

On a monochrome OLED display the background is black and the foreground is white. This contrasts with ePaper units where the foreground is black on a white background. The display drivers perform this inversion so that user code renders as expected on color, mono OLED or ePaper units.

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. On an ePaper display it represents black. Given that 1 is the foreground color, explicitly specifying BLACK on an ePaper will produce 0 as black has (very) low saturation. In this context the resultant physically white background color may come as a surprise.

In general the solution is to leave color settings at default.

3.1.3 Display update mechanism

A typical application comprises various widgets displaying user data. When a widget's value method is called, the framebuffer's contents are updated to reflect the widget's current state. The framebuffer is transferred to the physical hardware when refresh(device) is called. This allows multiple widgets to be refreshed at the same time. It also minimises processor overhead: .value is generally fast, while refresh is slow because of the time taken to transfer an entire buffer over SPI.

3.1.4 ePaper displays

On ePaper displays refresh is both slow and visually intrusive, with the display flashing repeatedly. This made them unsatisfactory for displaying rapidly changing information. There is a new breed of ePaper display supporting effective partial updates notably the Waveshare Pico paper 4.2. This can be used in such roles and is discussed in EPD Asynchronous support.

3.2 Label class

The purpose of a Label instance is to display text at a specific screen location.

Text can be static or dynamic. In the case of dynamic text the background is cleared to ensure that short strings cleanly replace longer ones.

Labels can be displayed with an optional single pixel border.

Colors are handled flexibly. By default the colors used are those of the Writer instance, however they can be changed dynamically; this might be used to warn of overrange or underrange values. The color15.py demo illustrates this.

Constructor args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col
  4. text If a string is passed it is displayed: typically used for static text. If an integer is passed it is interpreted as the maximum text length in pixels; typically obtained from writer.stringlen('-99.99'). Nothing is displayed until .value() is called. Intended for dynamic text fields.
  5. invert=False Display in inverted or normal style.
  6. fgcolor=None Optionally overrides the Writer colors.
  7. bgcolor=None
  8. bdcolor=False If False no border is displayed. If None a border is shown in the Writer foreground color. If a color is passed, it is used.
  9. align=ALIGN_LEFT By default text in labels is left aligned. Options are ALIGN_RIGHT and ALIGN_CENTER. These options can only take effect if a large enough field width is passed to text.

The constructor displays the string at the required location.

Methods:

  1. value Redraws the label. This takes the following args:
    • text=None The text to display. If None displays the last value.
    • invert=False If true, show inverse text.
    • fgcolor=None Foreground color: if None the Writer default is used.
    • bgcolor=None Background color, as per foreground.
    • bdcolor=None Border color. As per above except that if False is passed, no border is displayed. This clears a previously drawn border.
    • align=None Use alignment specified in constructor unless one of the module constants is passed.
      Returns the current text string.
  2. show No args. (Re)draws the label. Primarily for internal use by GUI.

Module Constants:

  • ALIGN_LEFT=0
  • ALIGN_RIGHT=1
  • ALIGN_CENTER=2

If populating a label would cause it to extend beyond the screen boundary a warning is printed at the console. The label may appear at an unexpected place. The following is a complete "Hello world" script.

from color_setup import ssd  # Create a display instance
from gui.core.nanogui import refresh
from gui.core.writer import CWriter
from gui.core.colors import *

from gui.widgets.label import Label
import gui.fonts.freesans20 as freesans20

refresh(ssd)  # Initialise and clear display.
CWriter.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
wri.set_clip(True, True, False)

# End of boilerplate code. This is our application:
Label(wri, 2, 2, 'Hello world!')
refresh(ssd)

3.3 Meter class

This provides a vertical linear meter display of values scaled between 0.0 and 1.0.

Constructor positional args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col

Keyword only args:

  1. height=50 Height of meter.
  2. width=10 Width.
  3. fgcolor=None Foreground color: if None the Writer default is used.
  4. bgcolor=None Background color, as per foreground.
  5. ptcolor=None Color of meter pointer or bar. Default is foreground color.
  6. bdcolor=False If False no border is displayed. If None a border is shown in the Writer foreground color. If a color is passed, it is used.
  7. divisions=5 No. of graduations to show.
  8. label=None A text string will cause a Label to be drawn below the meter. An integer will create a Label of that width for later use.
  9. style=Meter.LINE The pointer is a horizontal line. Meter.BAR causes a vertical bar to be displayed. Much easier to read on monochrome displays.
  10. legends=None If a tuple of strings is passed, Label instances will be displayed to the right hand side of the meter, starting at the bottom. E.G. ('0.0', '0.5', '1.0')
  11. value=None Initial value. If None the meter will not be drawn until its value() method is called.

Methods:

  1. value Args: n=None, color=None.
    • n should be a float in range 0 to 1.0. Causes the meter to be updated. Out of range values are constrained. If None is passed the meter is not updated.
    • color Updates the color of the bar or line if a value is also passed. None causes no change. Returns the current value.
  2. text Updates the label if present (otherwise throws a ValueError). Args:
    • text=None The text to display. If None displays the last value.
    • invert=False If true, show inverse text.
    • fgcolor=None Foreground color: if None the Writer default is used.
    • bgcolor=None Background color, as per foreground.
    • bdcolor=None Border color. As per above except that if False is passed, no border is displayed. This clears a previously drawn border.
  3. show No args. (Re)draws the meter. Primarily for internal use by GUI.

3.4 LED class

This is a virtual LED whose color may be altered dynamically.

Constructor positional args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col

Keyword only args:

  1. height=12 Height of LED.
  2. fgcolor=None Foreground color: if None the Writer default is used.
  3. bgcolor=None Background color, as per foreground.
  4. bdcolor=False If False no border is displayed. If None a border is shown in the Writer foreground color. If a color is passed, it is used.
  5. label=None A text string will cause a Label to be drawn below the LED. An integer will create a Label of that width for later use.

Methods:

  1. color arg c=None Change the LED color to c. If c is None the LED is turned off (rendered in the background color).
  2. text Updates the label if present (otherwise throws a ValueError). Args:
    • text=None The text to display. If None displays the last value.
    • invert=False If true, show inverse text.
    • fgcolor=None Foreground color: if None the Writer default is used.
    • bgcolor=None Background color, as per foreground.
    • bdcolor=None Border color. As per above except that if False is passed, no border is displayed. This clears a previously drawn border.
  3. show No args. (Re)draws the LED. Primarily for internal use by GUI.

3.5 Dial and Pointer classes

A Dial is a circular display capable of displaying a number of vectors; each vector is represented by a Pointer instance. The format of the display may be chosen to resemble an analog clock or a compass. In the CLOCK case a pointer resembles a clock's hand extending from the centre towards the periphery. In the COMPASS case pointers are chevrons extending equally either side of the circle centre.

In both cases the length, angle and color of each Pointer may be changed dynamically. A Dial can include an optional Label at the bottom which may be used to display any required text.

In use, a Dial is instantiated then one or more Pointer objects are instantiated and assigned to it. The Pointer.value method enables the Dial to be updated affecting the length, angle and color of the Pointer. Pointer values are complex numbers.

Dial class

Constructor positional args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col

Keyword only args:

  1. height=50 Height and width of dial.
  2. fgcolor=None Foreground color: if None the Writer default is used.
  3. bgcolor=None Background color, as per foreground.
  4. bdcolor=False If False no border is displayed. If None a border is shown in the Writer foreground color. If a color is passed, it is used.
  5. ticks=4 No. of gradutions to show.
  6. label=None A text string will cause a Label to be drawn below the meter. An integer will create a Label of that width for later use.
  7. style=Dial.CLOCK Pointers are drawn from the centre of the circle as per the hands of a clock. Dial.COMPASS causes pointers to be drawn as arrows centred on the control's centre. Arrow tail chevrons are suppressed for very short pointers.
  8. pip=None Draws a central dot. A color may be passed, otherwise the foreground color will be used. If False is passed, no pip will be drawn. The pip is suppressed if the shortest pointer would be hard to see.

When a Pointer is instantiated it is assigned to the Dial by the Pointer constructor.

Bound variable:

  1. label The Label instance if one was created.

Pointer class

Constructor arg:

  1. dial The Dial instance on which it is to be displayed.

Methods:

  1. value Args:
    • v=None The value is a complex number. A magnitude exceeding unity is reduced (preserving phase) to constrain the Pointer within the unit circle.
    • color=None By default the pointer is rendered in the foreground color of the parent Dial. Otherwise the passed color is used. Returns the current value.
  2. show No args. (Re)draws the control. Primarily for internal use by GUI.

Typical usage (ssd is the device and wri is the current Writer):

def clock(ssd, wri):
    # Border in Writer foreground color:
    dial = Dial(wri, 5, 5, ticks = 12, bdcolor=None)
    hrs = Pointer(dial)
    mins = Pointer(dial)
    hrs.value(0 + 0.7j, RED)
    mins.value(0 + 0.9j, YELLOW)
    dm = cmath.exp(-1j * cmath.pi / 30)  # Rotate by 1 minute
    dh = cmath.exp(-1j * cmath.pi / 1800)  # Rotate hours by 1 minute
    # Twiddle the hands: see aclock.py for an actual clock
    for _ in range(80):
        utime.sleep_ms(200)
        mins.value(mins.value() * dm, RED)
        hrs.value(hrs.value() * dh, YELLOW)
        refresh(ssd)

3.6 Scale class

This displays floating point data having a wide dynamic range. It is modelled on old radios where a large scale scrolls past a small window having a fixed pointer. This enables a scale with (say) 200 graduations (ticks) to readily be visible on a small display, with sufficient resolution to enable the user to interpolate between ticks. Default settings enable estimation of a value to within about +-0.1%.

Legends for the scale are created dynamically as it scrolls past the window. The user may control this by means of a callback. The example lscale.py illustrates a variable with range 88.0 to 108.0, the callback ensuring that the display legends match the user variable. A further callback enables the scale's color to change over its length or in response to other circumstances.

The scale displays floats in range -1.0 <= V <= 1.0.

Constructor positional args:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col

Keyword only arguments (all optional):

  • ticks=200 Number of "tick" divisions on scale. Must be divisible by 2.
  • legendcb=None Callback for populating scale legends (see below).
  • tickcb=None Callback for setting tick colors (see below).
  • height=0 Pass 0 for a minimum height based on the font height.
  • width=200
  • bdcolor=None Border color. If None, fgcolor will be used.
  • fgcolor=None Foreground color. Defaults to system color.
  • bgcolor=None Background color defaults to system background.
  • pointercolor=None Color of pointer. Defaults to .fgcolor.
  • fontcolor=None Color of legends. Default fgcolor.

Method:

  • value=None Set or get the current value. Always returns the current value. A passed float is constrained to the range -1.0 <= V <= 1.0 and becomes the Scale's current value. The Scale is updated. Passing None enables reading the current value, but see note below on precision.

Callback legendcb

The display window contains 20 ticks comprising two divisions; by default a division covers a range of 0.1. A division has a legend at the start and end whose text is defined by the legendcb callback. If no user callback is supplied, legends will be of the form 0.3, 0.4 etc. User code may override these to cope with cases where a user variable is mapped onto the control's range. The callback takes a single float arg which is the value of the tick (in range -1.0 <= v <= 1.0). It must return a text string. An example from the lscale.py demo shows FM radio frequencies:

def legendcb(f):
    return '{:2.0f}'.format(88 + ((f + 1) / 2) * (108 - 88))

The above arithmetic aims to show the logic. It can (obviously) be simplified.

Callback tickcb

This callback enables the tick color to be changed dynamically. For example a scale might change from green to orange, then to red as it nears the extremes. The callback takes two args, being the value of the tick (in range -1.0 <= v <= 1.0) and the default color. It must return a color. This example is taken from the scale.py demo:

def tickcb(f, c):
    if f > 0.8:
        return RED
    if f < -0.8:
        return BLUE
    return c

Increasing the ticks value

This increases the precision of the display.

It does this by lengthening the scale while keeping the window the same size, with 20 ticks displayed. If the scale becomes 10x longer, the value diference between consecutive large ticks and legends is divided by 10. This means that the tickcb callback must return a string having an additional significant digit. If this is not done, consecutive legends will have the same value.

Precision

For performance reasons the control stores values as integers. This means that if you set value and subsequently retrieve it, there may be some loss of precision. Each visible division on the control represents 10 integer units.

3.7 Class 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 will occur. Access to text that has scrolled out of view may be achieved by calling a method. The widget supports fixed and variable pitch fonts.

from gui.widgets.textbox import Textbox

Constructor mandatory positional arguments:

  1. writer The Writer instance (font and screen) to use.
  2. row Location on screen.
  3. col
  4. width Width of the object in pixels.
  5. nlines Number of lines of text to display. The object's height is determined from the height of the font:
    height in pixels = nlines*font_height
    As per most widgets the border is drawn two pixels beyond the control's boundary.

Keyword only arguments:

  • bdcolor=None Border color. If None, fgcolor will be used.
  • fgcolor=None Color of border. Defaults to system color.
  • bgcolor=None Background color of object. Defaults to system background.
  • clip=True By default lines too long to display are right clipped. If False is passed, word-wrap is attempted. If the line contains no spaces it will be wrapped at the right edge of the window.

Methods:

  • append Args s, ntrim=None, line=None Append the string s to the display and scroll up as required to show it. By default only the number of lines which will fit on screen are retained. If an integer ntrim=N is passed, only the last N lines are retained; ntrim may be greater than can be shown in the control, hidden lines being accessed by scrolling.
    If an integer (typically 0) is passed in line the display will scroll to show that line.
  • scroll Arg n Number of lines to scroll. A negative number scrolls up. If scrolling would achieve nothing because there are no extra lines to display, nothing will happen. Returns True if scrolling occurred, otherwise False.
  • value No args. Returns the number of lines of text stored in the widget.
  • clear No args. Clears all lines from the widget and refreshes the display.
  • goto Arg line=None Fast scroll to a line. By default shows the end of the text. 0 shows the start.

Fast updates:
Rendering text to the screen is relatively slow. To send a large amount of text the fastest way is to perform a single append. Text may contain newline ('\n') characters as required. In that way rendering occurs once only.

ntrim__ If text is regularly appended to a Textbox its buffer grows, using RAM. The value of ntrim sets a limit to the number of lines which are retained, with the oldest (topmost) being discarded as required.

4. ESP8266

Some personal observations on successful use with an ESP8266.

I chose an Adafruit 128x128 OLED display to represent the biggest display I thought the ESP8266 might support. I reasoned that, if this can be made to work, smaller or monochrome displays would present no problem.

The ESP8266 is a minimal platform with typically 36.6KiB of free RAM. The framebuffer for a 128*128 OLED requires 16KiB of contiguous RAM (the display hardware uses 16 bit color but my driver uses an 8 bit buffer to conserve RAM). The 4-bit driver halves this size.

A further issue is that, by default, ESP8266 firmware does not support complex numbers. This rules out the plot module and the Dial widget. It is possible to turn on complex support in the build, but I haven't tried this.

I set out to run the scale.py and textbox.py demos as these use asyncio to create dynamic content, and the widgets themselves are relatively complex.

I froze a subset of the drivers and the gui directories. A subset minimises the size of the firmware build and eliminates modules which won't compile due to the complex number issue. The directory structure in my frozen modules directory matched that of the source. This was the structure of my frozen directory before I added the 4 bit driver:
Image

I erased the flash, built and installed the new firmware. Finally I copied setup_examples/esp8266_setup.py to /pyboard/color_setup.py. This could have been frozen but I wanted to be able to change pins if required.

Both demos worked perfectly.

I modified the demos to regularly report free RAM. scale.py reported 10480 bytes, tbox.py reported 10512 bytes, sometimes more, as the demo progressed. With the 4 bit driver scale.py reported 18112 bytes. In conclusion I think that applications of moderate complexity should be feasible.

Appendix 1 Freezing bytecode

This achieves a major saving of RAM. The correct way to do this is via a manifest file. The first step is to clone MicroPython and prove that you can build and deploy firmware to the chosen platform. Build instructions vary between ports and can be found in the MicroPython source tree in ports/<port>/README.md.

The following is an example of how the entire GUI with fonts, demos and all widgets can be frozen on RP2.

Build script:

cd /mnt/qnap2/data/Projects/MicroPython/micropython/ports/rp2
MANIFEST='/mnt/qnap2/Scripts/manifests/rp2_manifest.py'

make submodules
make clean
if make -j 8 BOARD=PICO FROZEN_MANIFEST=$MANIFEST
then
    echo Firmware is in build-PICO/firmware.uf2
else
    echo Build failure
fi
cd -

Manifest file contents (first line ensures that the default files are frozen):

include("$(MPY_DIR)/ports/rp2/boards/manifest.py")
freeze('/mnt/qnap2/Scripts/modules/rp2_modules')

The directory /mnt/qnap2/Scripts/modules/rp2_modules contains only a symlink to the gui directory of the micropython-micro-gui source tree. The freezing process follows symlinks and respects directory structures.

It is usually best to keep hardware_setup.py unfrozen for ease of making changes. I also keep the display driver and boolpalette.py in the filesystem as I have experienced problems freezing display drivers - but feel free to experiment.

Appendix 2 Round displays

The normal test script is unsuitable as the rectangles are off-screen. Please paste this at the REPL to verify hardware and display orientation:

from color_setup import ssd  # Create a display instance
from gui.core.colors import RED, BLUE, GREEN
from gui.core.nanogui import refresh, circle
refresh(ssd, True)  # Initialise and clear display.
ssd.fill(0)
w = ssd.width
ssd.line(0, 0, w - 1, w - 1, GREEN)  # Green diagonal corner-to-corner
offs = round(0.29289 * w / 2)
ssd.rect(offs, offs, 15, 15, RED)  # Red square at top left
ssd.rect(w - offs - 15, w - offs - 15, 15, 15, BLUE)  # Blue square at bottom right
circle(ssd, 119, 119, 119, GREEN)
refresh(ssd)

micropython-nano-gui's People

Contributors

darkxex avatar freemansoft avatar ihornehrutsa avatar jahr avatar jose1711 avatar pbrunot avatar peterhinch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

micropython-nano-gui's Issues

Ref README.md - photographing OLED display images

Hi Peter, I just read the contents of README.md from this repo because I want to try to use this repo.
In this README.md you wrote: OLEDs are visually impressive displays with bright colors, wide viewing angle and extreme contrast. For some reason I find them hard to photograph..

I am not a photographer. Regularly I take photographs of hardware setups and of images on displays. Usually, to be quick, I use my iPhone to take photographs. My experience using this mobile phone to take photographs of displays is not overwhelmingly positive. I also own a Canon PowerShot G7X Mk II camera. With that camera I have much better success in making photographs of displays. The Canon Camera can deal better with the big brilliance of the OLED displays because you can set diaphragma, shutter time etc. The camera of the mobile phone is more automatic controlled thus more limited in adjustments by the user; in this case resulting in blurry characters (as shown below) and in other occasions showing artifacts too.

Below a comparison: image 1 taken with an iPhon8Plus; image 2 taken with the Canon PowerShot mentioned above.

Image 1:
IMG_3488

Technical details of image 1:

IMG_3488_info

Image 2. I did not take too much time adjusting settings of the Canon camera. I am sure that if I did, the quality of the image would be even better.

2022-01-03_23h18_

Technical details of image 2:

2022-01-03_23h18_info

Warning: refresh(ssd, True) called before color initialization in gui\core\colors.py

Several examples have an incompletely correct initialization sequence.
They call refresh(ssd, True) but 'lut' colors is not initialized.
The more correct sequence is:

from color_setup import ssd  # Create a display instance
from gui.core.colors import * # Init colors
from gui.core.nanogui import refresh
refresh(ssd, True)  # clear display.

This is not full list of files:
micropython-nano-gui\gui\demos\aclock.py
micropython-nano-gui\gui\demos\aclock_large.py
micropython-nano-gui\gui\demos\aclock_ttgo.py
micropython-nano-gui\gui\demos\alevel.py
micropython-nano-gui\gui\demos\asnano_sync.py

It's not critical, but not clean.

Port to interface with x,y pixel addressing....

I really love the screenshots on your homepage! Those are awesome looking "widgets".

I was wondering if it's possible to port the high level code so that I can use it with a tkinter canvas. I can make the canvas essentially pixel-addressable using whatever x,y coordinate system you want. Then perhaps that can be exposed to your graphing functions. I can create a "draw pixel" routine easily enough.

Is your code designed in such a way it would be moved to other "Framebuffers"?

I'm looking for output widgets that can't be generally found anywhere else to can add to my GUI package, PySimpleGUI.

esp8266 memory error

Good evening, i'm triyng to use nano-gui on esp8266 with SSSD1331 display.
I'm importing (not using frozen modules) the minimum .py files structure as suggested in README.
I can instatiate the ssd object, but when i try to create a Label object i get memory error.
Has something changed from that written in the README?
Does Importing library as frozen module resolve the issue?

Textbox and border

Hi I can't delete borders of bordered textboxes. I tried some things but I couldn't do it

refresh(ssd, True)  # Initialise and clear display.
pargs0 = (3, 4, 124, 1)
pargs1 = (43, 4, 124, 1)
pargs2 = (63, 4, 124, 1)
pargs3 = (83, 4, 124, 1)
pargs4 = (103, 4, 124, 1)
pargs5 = (123, 4, 124, 1)
pargs6 = (143, 4, 124, 1)

#
tbargs0 = {'fgcolor' : WHITE,
          'bdcolor' : BLACK,
          'bgcolor' : BLACK,
         }
#
tbargs1 = {'fgcolor' : WHITE,
          'bdcolor' : RED,
          'bgcolor' : BLACK,
         }
refresh(ssd, True)
wri = CWriter(ssd, eux15, WHITE, BLACK, verbose=False)
s = '''The textbox displays'''
tb1 = Textbox(wri, *pargs1, clip=False, **tbargs1)
tb1.append(s, ntrim = 18, line = 0)
refresh(ssd)

Then I replace tbargs0 with tbargs1. there is no change.

tb1 = Textbox(wri, *pargs1, clip=False, **tbargs0)
tb1.append(s, ntrim = 18, line = 0)
refresh(ssd)

no change

tb1.bdcolor=BLACK
tb1.append(s, ntrim = 18, line = 0)
refresh(ssd)

no change.

tb1.has_border=False
tb1.append(s, ntrim = 18, line = 0)
refresh(ssd)

no change.

framebuf.GS4_HMSB format

Hello, Peter.
Could you please clarify for me?
The framebuf defined as 4bit greyscale.

mode = framebuf.GS4_HMSB # Use 4bit greyscale.

I guess that
high 4bit nibble of a byte in framebuf is one pixel and
low 4bit nibble of a byte in framebuf is the next pixel.
Every pixel is a color number/index in the LUT.
But why they are shifted by 3 and 1 instead of 4 and 0?
d = (c & 0xf0) >> 3 # 2* LUT indices (LUT is 16 bit color)
e = (c & 0x0f) << 1

Thank you.

Pico SPI requires "polarity=1"

In order to get the Pico working I had to add polarity=1 as an argument to the SPI instantiation.

Also the language about the color15 demo not working on the Pico can be updated because uos.urandom() works now and the test will pass.

nanogui.py in esp8266 build error

Hello deterhinch! Thank you for shared the project, it is impressive! I wanted building it forzen byte code to fimware,but any error as show follow:
GEN build-GENERIC/frozen_content.c
CC build-GENERIC/frozen_content.c
build-GENERIC/frozen_content.c:15136:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__conj_0 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)1};
^
build-GENERIC/frozen_content.c:15136:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__conj_0') [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__conj_0') [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__conj_0') [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15136:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__conj_0') [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__arrow_0 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)0};
^
build-GENERIC/frozen_content.c:15300:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__arrow_0') [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__arrow_0') [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__arrow_0') [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:15300:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__arrow_0') [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__Pointer___init___0 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)0};
^
build-GENERIC/frozen_content.c:17411:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Pointer___init___0') [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Pointer___init___0') [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Pointer___init___0') [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17411:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Pointer___init___0') [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__Dial_show_0 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)1};
^
build-GENERIC/frozen_content.c:17925:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_0') [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_0') [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_0') [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17925:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_0') [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__Dial_show_2 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)0};
^
build-GENERIC/frozen_content.c:17929:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_2') [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_2') [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_2') [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17929:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_2') [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__Dial_show_4 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)0};
^
build-GENERIC/frozen_content.c:17933:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_4') [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_4') [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_4') [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17933:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_4') [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__Dial_show_5 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)2};
^
build-GENERIC/frozen_content.c:17934:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_5') [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_5') [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_5') [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:17934:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__Dial_show_5') [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__0 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)3};
^
build-GENERIC/frozen_content.c:18185:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__0') [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__0') [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__0') [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18185:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__0') [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: unknown type name 'mp_obj_complex_t'
STATIC const mp_obj_complex_t const_obj_nanogui__lt_module_gt__1 = {{&mp_type_complex}, (mp_float_t)0, (mp_float_t)3};
^
build-GENERIC/frozen_content.c:18186:1: error: braces around scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__1') [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: initialization makes integer from pointer without a cast [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__1') [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__1') [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: excess elements in scalar initializer [-Werror]
build-GENERIC/frozen_content.c:18186:1: error: (near initialization for 'const_obj_nanogui__lt_module_gt__1') [-Werror]
cc1: all warnings being treated as errors
../../py/mkrules.mk:63: recipe for target 'build-GENERIC/build-GENERIC/frozen_content.o' failed
make: *** [build-GENERIC/build-GENERIC/frozen_content.o] Error 1
How to solve this issue.Thanks!

ssd.wait (for class EPD) does not exist

I was looking at the two async EPD demos:

  • epd_async.py
  • epd29_async.py

and both make use of a ssd.wait()
However, my linter, as well as manually browsing through ...
from color_setup import ssd
v
ssd = SSD()
v
from drivers.epaper.pico_epaper_42 import EPD as SSD
v
class EPD(framebuf.FrameBuffer):
v
class FrameBuffer:
... confirms that there is no property of .wait in that line of inheritance.

Rotation for SSD1351

Hi Peter, Thank you so much for your work on these libraries.

I'm looking to rotate the SSD1351 by 180 degrees (for a physically upside down display).
I noticed 1327 has the ability to be rotated. Though its driver looks in some ways more simplified / abstracted compared to SSD1351.

Do you have a suggestion of how I might achieve a 180 rotation of a 1351 OLED RGB screen? (Its 16bit color model).

The dial class and pointers

Thanks for the excellent library, I like it a lot. Must admit my heart sank with the dial widget when I saw it used complex numbers which I did not have a clue what they were and initial googles did not enlighten me. I expect complex numbers are well understood by real programmers and engineers, but for this old boy hobby dabbler I've never come across them before.

However I came across mathsisfun.com which did put me on the right path, at least as far as its used for the dial pointer class. I don't really get the imaginary number stuff but I have worked out that the dial has 2 ranges - a horizontal one that goes from -x to +x (left to right) and a vertical one that goes from +y to -y (top to bottom). Both these ranges meet in the middle at 0x and 0y.

These ranges can be addresses in python with the built in function 'complex(x,y), for the horizontal and vertical axis. So for a compass bearing of North at the top (0 degrees) one can put complex(0,90) (no horizontal jut a vertical going up). Other bearings follow this pattern so 90 degrees is complex(90,0) (horizontal to the right and no vertical). 180 degrees is complex(0,-90) (no horizontal but vertical going down thus a minus). The bearings in-between are extrapolated so a bearing of 230 degrees would be complex(-50,-40) (horizontal to the left so a '-' and vertical going down so another '-'

I give this explanation for any, who like me, have not a clue about these complex numbers and as the words probably don't adequately explain it I give my small demo program (amateur python programming) to illustrate.

I create a function 'position_calc(..) that has arguments for the pointer to use, the number of segments in the circle desired (so 360 for a compass or 60 for an hours worth of minutes etc) and a position to display (e.g 270 for 270 degrees of a 360 degree circle or 15 for 15 minutes of a circle representing an hour of 60 minutes circle.

Its probably not very well written, and there must be a more elegant way (but don't mention complicated maths :-) ). I found it easier to do the maths with the positive and negative complex numbers to think of the circle as divided into 4 quadrants, so the first bit of the program calculates the quartile the desired bearing is in and then does the simple maths, just addition or subtraction no use is made of more complicated maths from the cmath library :-)

Program Code Starts:
from color_setup import ssd # Create a display instance
from gui.core.colors import *
from gui.core.nanogui import refresh
from time import sleep

import gui.fonts.freesans20 as freesans20
from gui.core.writer import CWriter
from gui.widgets.dial import Dial, Pointer

CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, freesans20, GREEN, BLACK, verbose=False)
refresh(ssd, True) # Initialise and clear display.

dial = Dial(wri, 60, 40, height = 120, ticks = 8,
label = ' Bearing', style = Dial.CLOCK, pip=CYAN,
fgcolor = CYAN)
dial.show()

pointer = Pointer(dial)

def position_calc(pointer, segments, position):
if position > segments:
print('position value greater than segments')
return
if (segments % 4) != 0:
print('segments must be divisible by 4 without any left over')
return
q_segments = segments / 4
if position <= q_segments:
quartile = 1
elif position > q_segments and position <= q_segments *2:
quartile = 2
position = position - q_segments
elif position > q_segments *2 and position <= q_segments *3:
quartile = 3
position = position - q_segments *2
else:
quartile = 4
position = position - q_segments *3

if quartile == 1: # 0, q_segments
    Horizontal = 0 + position
    Vertical = q_segments - position
    pointer.value(complex(Horizontal, Vertical), MAGENTA)
elif quartile == 2: # q_segments, 0
    Horizontal = q_segments - position
    Vertical = 0 - position
    pointer.value(complex(Horizontal, Vertical), RED)
elif quartile == 3: # 0, -q_segments
    Horizontal =  0 - position
    Vertical = (q_segments *-1) + position
    pointer.value(complex(Horizontal, Vertical), CYAN)
elif quartile == 4: # -q_segments, 0
    Horizontal =  (q_segments *-1) + position
    Vertical = 0 + position
    pointer.value(complex(Horizontal, Vertical), GREEN)

print('H:',Horizontal, ' V:',Vertical)

segments = 360
bearing = 230

position_calc(pointer, segments, bearing)
refresh(ssd)

"""

for 60 minutes (so 60 segments)

start at 1 and wiz round to 60 with 0.25 intervals

segments = 60
for seg in range(1, segments+1):
position_calc(pointer, segments, seg)
refresh(ssd)
sleep(0.25)

"""
Code Ends

OK all the above is probably only of use to a mathematically challenged old boy so feel free to junk this post.

But I have another motivation for this post and that was to see if you have any tips for creating a bigger arrow pointer. I'm using the ili9341 screen with a rpi pico. I cannot really see the arrow head and it rather merges with a 'tick' mark when the compass pointer stops at the tick point. (the demo code I presented uses the CLOCK rather than the COMPASS as its not so clear which end is which as the arrow head is rather indistinct.)

Also, the docs mention a pointer method of text(), but the code in the dial.py widget does not appear to have any such method. (I was a good boy and actually read the docs!)

I'm trying to set up a dial to show the current wind direction, but also to show the direction as text. I can use the label class just as well for this, so if its not intended for the pointer to have a text() method then its not really an issue.

Again, many thanks for a really useful library.

Issues with the SSD1339 driver

I have an SSD1339 OLED display, and although the driver I wrote (ssd1339.py) can display content, the image shown when running the test program is skewed. Could you please tell me what the issue might be?

ssd1339.py
test.py

MicroPython v1.22.2, pico v1.22.2 on 2024-03-06; Raspberry Pi Pico with RP2040

IMG_8671

TTGO SPI

# Conservative low baudrate. Can go to 62.5MHz.
spi = SPI(1, 30_000_000, sck=Pin(TFT_SCLK), mosi=Pin(TFT_MOSI))

The screen is black if the baud rate grater than 33Mhz.
Is it a hardware limit?

ssd._spi.deinit() is required at the end of demos examples.

Help for 4bit

Hello. I have st7789 and st7735 lcd screens.
st7789 lcd 240x240 pixels
st7735 lcd 128x160 pixels (actually 132x162 ones)

Screen works when I use st7735r.py for st7735.

When I use st7735r_4bit.py for st7735 the screen does not work.

When I use st7789_4bit.py for st7789 the screen does not work.

esp32-wroom-32d
micropython version MicroPython v1.19.1 on 2022-06-18; ESP32 module with ESP32

from st7789_4bit import *
from machine import Pin, SPI
import gc


SSD = ST7789

pdc = Pin(2, Pin.OUT, value=0)  # Arbitrary pins
pcs = Pin(15, Pin.OUT, value=1)
prst = Pin(4, Pin.OUT, value=1)

gc.collect()  # Precaution before instantiating framebuf
spi = SPI(1, 30_000_000, sck=Pin(14), mosi=Pin(13), miso=None)
ssd = SSD(spi, height=240, width=240, dc=pdc, cs=pcs, rst=prst)

GREEN = ssd.rgb(0, 255, 0)
RED = ssd.rgb(255,0,0)
BLUE = ssd.rgb(0,0,255)
WHITE = ssd.rgb(255, 255, 255)
BLACK = ssd.rgb(0,0,0)

ssd.fill(WHITE)
ssd.text("HELLO",10,10,RED)
ssd.show()

framebuf_utils.mpy runtime error

Hi Peter - I really admire and appreciate all of the contributions you've been making to micropython. I'm not sure if you want me to post an issue I'm having with nano-gui here or on the forum. Quite willing to cross-post if that's preferable. My issue is that I'm encountering a runtime error after cross-compiling framebuf_utils to ESP32. Here are the steps I've taken and I'm probably doing something incorrectly.

  1. Using a Lolin D32 Pro with WROVER-B, I compiled micropython with GENERIC_SPIRAM flag from source to build the latest version - MicroPython v1.13-321-gef9fde733 on 2021-01-31; ESP32 module (spiram) with ESP32 - from scratch. Installed this version on the ESP32 (see attached pic). I'm fairly confident that the WROVER-B supports SPIRAM/PSRAM and I experienced zero errors compiling and deploying it on the D32. I'm getting pretty good at doing this.
  2. If I don't install a cross-compiled framebuf_utils.mpy and thus default to the STM version (which properly ignores the framebuf_utils.mpy), your aclock and other demos work flawlessly. So does my metered 'grow.py' demo which runs indefinitely.
  3. I then edit your micropython-font-to-py/writer/framebuf_utils/Makefile to use the same micropython distribution's mpy-cross and cross-compile your latest framebuf_utils to ESP32. (ARCH = xtensawin). No errors (see attached screen grab).
  4. When I install my version in gui/core on the ESP32, the demos use the mpy executable and run for multiple iterations before crashing with the following errors:

Traceback (most recent call last):
File "", line 1, in
File "grow.py", line 55, in
File "grow.py", line 44, in meter
File "gui/widgets/meter.py", line 31, in value
File "gui/widgets/meter.py", line 56, in show
File "gui/widgets/label.py", line 21, in init
File "gui/widgets/label.py", line 32, in value
File "gui/widgets/label.py", line 44, in show
File "gui/core/writer.py", line 144, in printstring
File "gui/core/writer.py", line 164, in _printline
File "gui/core/writer.py", line 281, in _pchfast
TypeError:

I'm quite happy to help debug in any way that we need - realize, however, that you're dealing with an amateur and patience will be required. Would love to get this working as I'm really looking forward to using your light gui! Thank you. Pascal ([email protected]).

Screen Shot 2021-01-31 at 4 12 33 PM

Screen Shot 2021-01-31 at 4 27 08 PM

Screen Shot 2021-01-31 at 4 45 58 PM

Pico E-Paper 4.2, Displaying Grey

I wanted to bring your attention to a bug I encountered while working with my Pico-e-Paper-4.2 gray-scale display. Specifically, correctly rendering widgets on the screen. Because the widgets use the monochrome method (bg=0, fg=1), they would display with white backgrounds (0) and light gray foregrounds (1). Again, because it used the monochrome method, overriding each label (eg. fgcolor=3), did not effect the color of text. I did some digging and unfortunately it doesn't look easy to fix, but I got a temporary flex-tape solution by editing my driver files as seen below.

Original

# R21
EPD_grey_lut_ww = b".....etc
# R22H r
EPD_grey_lut_bw = b".....etc
# R23H w
EPD_grey_lut_wb = b".....etc
#  R24H b
EPD_grey_lut_bb = b".....etc

Edited

# R21
EPD_grey_lut_ww = b".....etc
# R22H r
EPD_grey_lut_bw = b".....etc
# R23H w
EPD_grey_lut_bb = b".....etc
#  R24H b
EPD_grey_lut_wb = b".....etc

I switched the lut_bw with lut_bb so that it gives light grey to the value bb and black to the value wb, so that the light grey color (actually black now) continues to be passed on as 1, so that the default rendering is black-on-white. I assume there's probably a fancier solution that can be worked out, and just wanted to make you aware :). Also, it's possible I could have missed the instructions on how to display widgets in different shades of grey, but those would be a welcome addition to the e-paper section in the driver files. Great job on the code btw :)

SSD1351 Not working on ESP32

Hello, I have tried to get this to work several times, but for some reason my display does not light up. The ssd1351_16.py runs, and it says all of the print lines as if it is running, but it doesnt show anything on screen. Any ideas as to why this is not working? Thanks.

Edit: I also just tried the ssd1351_generic.py and it still does not show anything.

upip support

Hello,
please consider upip support for easier upload to ESP32. Because rhsell have some issues with large and lot of files... So upip will safe lots of broken keyboards and USB cables ;)

/micropython-nano-gui> cp -r gui /pyboard/lib/
timed out or error in transfer to remote: b''
/micropython-nano-gui> ls gui/

ili9341 driver gives a blank screen

Hi. I'd double love to use your library, with the ESP32
I've tried 2 hardware versions of the ili9341 display, and get the same results on both, a blank screen.

My connections are:
CLK -> IO14
MISO -> IO12
MOSI -> IO13
LED -> IO17
RST -> IO25
DC ->IO26
CS -> IO5

Here's my color_setup.py:

from machine import Pin, SPI
import gc
from drivers.ili93xx.ili9341 import ILI9341 as SSD

pdc = Pin(26, Pin.OUT, value=0) # Arbitrary pins
pcs = Pin(5, Pin.OUT, value=1)
prst = Pin(25, Pin.OUT, value=1)
pled = Pin(17, Pin.OUT, value=1)

gc.collect() # Precaution before instantiating framebuf
spi = SPI(1, 10000000)
ssd = SSD(spi, dc=pdc, cs=pcs, rst=prst)

Here's what happens at REPL:
MicroPython v1.14 on 2021-02-02; ESP32 module with ESP32
Type "help()" for more information.

import gui.demos.aclock
Ignoring framebuf_utils.mpy: compiled for incorrect architecture.

I was looking to see if the SPI but gets initiated, I can see init_spi = False so I was wondering about that. Otherwise it seems tha the code is running and the display is just white.

Hoping you can point me in the right direction.

Regards

Andy

OSError: [Errno 19] ENODEV

Hi, I get a example from your code and changes somethings (just for tests), but after running many hours I have this error:

press
release
press
release
double click
release
press
release
press
release
double click
release
Task exception wasn't retrieved
future: <Task> coro= <generator object 'multi_fields' at 3df79d40>
Traceback (most recent call last):
  File "uasyncio/core.py", line 1, in run_until_complete
  File "display.py", line 112, in multi_fields
  File "gui/core/nanogui.py", line 77, in refresh
  File "ssd1306.py", line 99, in show
  File "ssd1306.py", line 119, in write_cmd
OSError: [Errno 19] ENODEV

Ps: I'm using only one button for the display, so the idea is press go to next window, double press back to previous window and long press enter to a new menu of windows

The code is this:

start.py:

    async def _start_display(self):
    task_dict[1] = asyncio.create_task(multi_fields())

display.py:

# mono_test.py Demo program for nano_gui on an SSD1306 OLED display.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2018-2021 Peter Hinch

# https://learn.adafruit.com/monochrome-oled-breakouts/wiring-128x32-spi-oled-display
# https://www.proto-pic.co.uk/monochrome-128x32-oled-graphic-display.html

# V0.33 16th Jan 2021 Hardware configuration is now defined in color_setup to be
# consistent with other displays.
# V0.32 5th Nov 2020 Replace uos.urandom for minimal ports
import gc

from machine import Pin, Timer
import uasyncio as asyncio
#from primitives import Pushbutton
from abutton import Pushbutton


import uasyncio as asyncio

import utime
# import uos
from color_setup import ssd
# On a monochrome display Writer is more efficient than CWriter.
from gui.core.writer import Writer
from gui.core.nanogui import refresh
from gui.widgets.meter import Meter
from gui.widgets.label import Label

# Fonts
import gui.fonts.arial10 as arial10
import gui.fonts.courier20 as fixed
import gui.fonts.font6 as small

# Some ports don't support uos.urandom.
# See https://github.com/peterhinch/micropython-samples/tree/master/random
def xorshift64star(modulo, seed = 0xf9ac6ba4):
    x = seed
    def func():
        nonlocal x
        x ^= x >> 12
        x ^= ((x << 25) & 0xffffffffffffffff)  # modulo 2**64
        x ^= x >> 27
        return (x * 0x2545F4914F6CDD1D) % modulo
    return func

async def fields():
    ssd.fill(0)
    refresh(ssd)
    Writer.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
    wri = Writer(ssd, fixed, verbose=False)
    wri.set_clip(False, False, False)
    textfield = Label(wri, 0, 2, wri.stringlen('longer'))
    numfield = Label(wri, 25, 2, wri.stringlen('99.99'), bdcolor=None)
    countfield = Label(wri, 0, 90, wri.stringlen('1'))
    n = 1
    random = xorshift64star(65535)
    for s in ('short', 'longer', '1', ''):
        textfield.value(s)
        numfield.value('{:5.2f}'.format(random() /1000))
        countfield.value('{:1d}'.format(n))
        n += 1
        refresh(ssd)
        #utime.sleep(2)
        await asyncio.sleep(1)
    textfield.value('Done', True)
    refresh(ssd)

async def multi_fields():
    ssd.fill(0)
    refresh(ssd)
    Writer.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
    wri = Writer(ssd, small, verbose=False)
    wri.set_clip(False, False, False)
    '''
    Label(wri, 25, 0, 'Canais 1,2,3,4,5,6', True)
    refresh(ssd)
    utime.sleep(2)
    ssd.fill(0)
    refresh(ssd)
    '''
    nfields = []
    dy = small.height() + 6
    y = 2
    col = 15
    width = wri.stringlen('999.99')
    for txt in ('1:', '2:', '3:'):
        Label(wri, y, 0, txt)
        nfields.append(Label(wri, y, col, width, bdcolor=None))  # Draw border
        y += dy
    #print(len(nfields))

    dy = small.height() + 6
    y = 2
    col = 82
    for txt in ('4:', '5:', '6:'):
        Label(wri, y, 67, txt)
        nfields.append(Label(wri, y, col, width, bdcolor=None))  # Draw border
        y += dy
    #print(len(nfields))
    #print(nfields)
 

    random = xorshift64star(2**24 - 1)
    #for _ in range(5):
    while True:
        for field in nfields:
            value = random() / 16777
            field.value('{:5.2f}'.format(value))
        start_time = utime.ticks_ms()
        refresh(ssd)
        #await ssd.do_refresh()
        end_time = utime.ticks_ms()
        diff_time = utime.ticks_diff(end_time, start_time)
        #print('{} {}ms'.format('The time to the refresh display is:', diff_time))
        #utime.sleep(1)
        await asyncio.sleep_ms(500)
        #ssd.fill(0)
        #print(gc.mem_free())
    ssd.fill(0)
    Label(wri, 25, 0, 'Grafico canais 1,2,3', True)
    refresh(ssd)
    utime.sleep(2)

async def meter1():
    ssd.fill(0)
    refresh(ssd)
    wri = Writer(ssd, arial10, verbose=False)
    m0 = Meter(wri, 5, 2, height = 45, divisions = 4,  legends=('0.0', '50', '100', '145!', '150!'), label='1', style=Meter.BAR)
    m1 = Meter(wri, 5, 44, height = 45, divisions = 4, legends=('0.0', '50', '100', '195!', '200!'), label='2', style=Meter.BAR)
    m2 = Meter(wri, 5, 86, height = 45, divisions = 4, legends=('0.0', '50', '100', '235!', '240!'), label='3', style=Meter.BAR)
    steps = 10
    random = xorshift64star(2**24 - 1)
    c = 1
    for n in range(steps + 1):
        #m0.value(random() / 16777216)
        m0.value(c/steps)
        m1.value(n/steps)
        m2.value(1 - n/steps)
        refresh(ssd)
        #Label(wri, 64, 0, '1', False)
        #utime.sleep(1)
        await asyncio.sleep(1)
        c += 1
    ssd.fill(0)

    Writer.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
    wri = Writer(ssd, small, verbose=False)
    wri.set_clip(False, False, False)
    Label(wri, 25, 0, 'Grafico canais 4,5,6', True)
    refresh(ssd)
    utime.sleep(2)

async def meter2():
    ssd.fill(0)
    refresh(ssd)
    wri = Writer(ssd, arial10, verbose=False)
    m0 = Meter(wri, 5, 2, height = 45, divisions = 4,  legends=('0.0', '50', '100', '245!', '250!'), label='4', style=Meter.BAR)
    m1 = Meter(wri, 5, 44, height = 45, divisions = 4, legends=('0.0', '50', '200', '295!', '300!'), label='5', style=Meter.BAR)
    m2 = Meter(wri, 5, 86, height = 45, divisions = 4, legends=('0.0', '50', '200', '335!', '340!'), label='6', style=Meter.BAR)
    steps = 10
    random = xorshift64star(2**24 - 1)
    c = 1
    for n in range(steps + 1):
        #m0.value(random() / 16777216)
        m0.value(c/steps)
        m1.value(n/steps)
        m2.value(1 - n/steps)
        refresh(ssd)
        #Label(wri, 64, 0, '1', False)
        #utime.sleep(1)
        await asyncio.sleep(1)
        c += 1



tstr = '''Test assumes a 128*64 (w*h) display. Edit WIDTH and HEIGHT in ssd1306_setup.py for others.
Device pinouts are comments in ssd1306_setup.py.

Test runs to completion.
'''

'''
print(tstr)
print('Basic test of fields.')
#fields()
print('More fields.')
multi_fields()
print('Meters 1.')
meter1()
print('Done.')
print('Meters 2.')
meter2()
print('Done.')
'''


def double():
    print('double click')
    global task_dict, double_active
    double_active = True
    task_running_now = asyncio.current_task()
    #print(dir(task_running_now))
    #print('The currently running task is: {}'.format(task_running_now))
    #print(task_dict[1])
    try:
        task_dict[1].cancel()
        task_dict[2].cancel()
    except:
        print('Error')
    task_dict[1] = asyncio.create_task(multi_fields())

def release():
    print('release')
    global task_dict, double_active
    await asyncio.sleep_ms(150)
    if double_active:
        return
    task_running_now = asyncio.current_task()
    #print(dir(task_running_now))
    #print('The currently running task is: {}'.format(task_running_now))
    #print(task_dict[1])
    try:
        task_dict[1].cancel()
        task_dict[2].cancel()
    except:
        print('Error')
    task_dict[2] = asyncio.create_task(meter2())

def press():
    print('press')
    global task_dict, double_active
    double_active = False

def long():
    print('long press')

global task_dict, double_active

task_dict = {}
double_active = False
pin = Pin(12, Pin.IN, Pin.PULL_UP)
pb = Pushbutton(pin)
pb.press_func(press, ())
pb.release_func(release, ())
pb.double_func(double, ())
pb.long_func(long, ())

Simple procedure to rotate display

I have the Nano-Gui demos working on a Raspberry Pi Pico with the Adafruit 3.5" TFT display using IlI9486 driver. However, the default text coordinate(0,0) seems to be in the lower right vs the upper left. I've addressed it for now by simply turning the display itself upside down and rotating it. However, I have used the same display with an Arduino Nano / Uno and Adafruit's HX8357 driver. That driver has a simple Rotate display option as one of the first setup command lines. Is anything like this possible with ili9486 driver (or did I miss something in the library itself)? Note only other issue that I had was to to comment out the check for the firmware. I'm using Pimoroni's UF2 Micropython version since I use many of their other products for the Pico and its use triggered an exception until I removed the firmware check.
Thank you for providing this program to the community !

Pico and SPI

First of all, thanks a million for making such a comprehensive tool.

I am a total novice at things this low-level, so forgive the very basic question. I'm struggling to get up and running on a pico with a (128x128) ssd1351 screen. I am thinking that my configuration of the color_setup.py may be wrong.

This is a snippet from my color_setup.py that I think may be the issue:

`
height = 128 # 1.5 inch 128*128 display

pdc = machine.Pin(20, machine.Pin.OUT)
pcs = machine.Pin(17, machine.Pin.OUT)
prst = machine.Pin(21, machine.Pin.OUT)
spi = machine.SPI(0)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance
`

Is there a glaring error in this?

rsync command does not process source argument as a full rsync

In full rsync this:

rsync -av gui /dest  # copy what's inside gui directory

is not equal to,

rsync -av gui/ /dest  # create gui/ inside /dest, then recursively copy its contents

Rsync command does not really differentiate between the two (always contents is copied and not the directory itself), which is somewhat confusing.

No align= in Label widget

As per the docs - option 9 of the Label class has an the ability to do align=ALIGN_CENTER, but this does not work and a peruse of the Label class shows its not there. Is this a doc error or has it be left out of the code by mistake?
Thanks

Print screens as BMP images for the demo images

Hi @peterhinch ,
It's not really an issue. Just read "These images, most from OLED displays, fail to reproduce the quality of these displays." from README.md.
Do you consider printing screens as BMP images for the demo images?
If so, you may try micropython-microbmp.
There are examples (for SSD1306 and SSD1351) on how to do it.
It's a pure python module, so it's not fast. But for printing demo images, it should be all right.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.