A lightweight and minimal MicroPython GUI library for display drivers based on
the FrameBuffer class. Various display technologies are supported, including
small color and monochrome OLED's and color TFT's. The GUI is cross-platform.
These images, most from OLED displays, are poor. OLEDs are visually impressive
displays with bright colors, wide viewing angle and extreme contrast. For some
reason I find them hard to photograph well.
The aclock.py demo.
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.
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 realtime plotting including time series.
A sample image from the plot module.
The following images are from a different display but illustrate the widgets.
The Scale widget. Capable of precision display of
floats.
The Textbox widget for scrolling text.
Notes on Adafruit and other OLED displays including wiring details, pin names and hardware issues.
- Introduction
1.1 Update
1.2 Description
1.3 Quick start - Files and Dependencies
2.1 Files
     2.1.1 Core files
     2.1.2 Demo Scripts
     2.1.3 Fonts
     2.1.4 Color setup examples
2.2 Dependencies
     2.2.1 Monochrome use
     2.2.2 Color use - The nanogui module
3.1 Application Initialisation Initial setup and refresh method.
     3.1.1 Setup file internals
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. - Device drivers Device driver compatibility requirements (these are minimal).
- ESP8266 This can work. Contains information on minimising the RAM and flash footprints of the GUI.
This library provides a limited set of GUI objects (widgets) for displays whose
display driver is subclassed from the FrameBuffer class. Such drivers can be
tiny as the graphics primitives are supplied by the FrameBuffer class.
The GUI is display-only and lacks provision for user input. Displays with touch
overlays are physically large, with correspondingly high pixel counts. Such
displays would require large frame buffers. These would consume RAM and be slow
to copy to the display. A FrameBuffer based driver is ill-suited to large
displays. Drivers should use graphics primitives hosted on the display
controller chip.
The GUI is cross-platform. By default it is configured for a Pyboard (1.x or D). This doc explains how to configure for other platforms 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. It is tested on the ESP32 reference board without SPIRAM. Running on ESP8266 is possible but frozen bytecode should be used owing to its restricted RAM.
Authors of applications requiring touch should consider my touch GUI's for the following displays. These have internal buffers:
29 Nov 2020 Add ST7735R TFT drivers.
17 Nov 2020 Add Textbox widget. Scale constructor arg border replaced by
bdcolor as per other widgets.
5 Nov 2020
This library has been refactored as a Python package. The aim is to reduce RAM
usage: widgets are imported on demand rather than unconditionally. This enabled
the addition of new widgets with zero impact on existsing applications. Another
aim was to simplify installation with dependencies such as writer included in
the tree. Finally hardware configuration is contained in a single file: details
only need to be edited in one place to run all demo scripts.
Existing users should re-install from scratch. In existing applications, import statements will need to be adapted as per the demos. The GUI API is otherwise unchanged.
Compatible and tested display drivers include:
- The official SSD1306 driver.
- The PCD8544/Nokia 5110.
- The Adafruit 0.96 inch color OLED with this driver.
- A driver for Adafruit 1.5 inch OLED and Adafruit 1.27 inch OLED may be found here.
- A driver for Sharp ultra low power consumption monochrome displays such as 2.7 inch 400x240 pixels is here.
- Drivers for Adafruit ST7735R based TFT's: 1.8 inch and 1.44 inch.
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 effciently rendered at arbitrary scale on 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.
Owing to RAM requirements and limitations on communication speed, FrameBuffer
based display drivers are intended for physically small displays with limited
numbers of pixels. The widgets are designed for displays as small as 0.96
inches: this involves some compromises.
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 demos illustrate this.
A GUI description can seem daunting because of the number of class config options. Defaults can usually be accepted and meaningful applications can be minimal. Installation can seem difficult. To counter this, this session using rshell installed and ran a demo showing analog and digital clocks.
Clone the repo to your PC, wire up a Pyboard (1.x or D) to an Adafruit 1.27"
OLED as per color_setup.py, move to the root directory of the repo and run
rshell.
> cp -r drivers /sd
> cp -r gui /sd
> cp color_setup.py /sd
> repl ~ import gui.demos.aclockNote also that the gui.demos.aclock.py demo comprises 38 lines of actual
code. This stuff is easier than you might think.
Firmware should be V1.13 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.
In the interests of conserving RAM, supplied drivers support only the functionality required by the GUI. More fully featured drivers may better suit other applications. See section 4.
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 root directory contains two example setup files, for monochrome and color
displays respectively. Other examples may be found in the color_setup
directory. These are templates for adaptation: only one file is copied to the
target. On the target a color files should be named color_setup.py. The
monochrome ssd1306_setup.py retains its own name.
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 driver source.
color_setup.pySetup for color displays. As written supports an SSD1351 display connected to a Pyboard.ssd1306_setup.pySetup file for monochrome displays using the official driver. Supports hard or soft SPI or I2C connections, as does the test scriptmono_test.py. On non Pyboard targets this will require adaptation to match the hardware connections.
The gui/core directory contains the GUI core and its principal dependencies:
nanogui.pyThe library.writer.pyModule for rendering Python fonts.fplot.pyThe graph plotting module.colors.pyColor constants.framebuf_utils.mpyAccelerator for theCWriterclass. This optional file is compiled for STM hardware and will be ignored on other ports (with a harmless warning message) unless recompiled. Instructions and code for compiling for other architectures may be found here.
The gui/demos directory contains test/demo scripts.
mono_test.pyTests/demos using the official SSD1306 driver for a monochrome 128*64 OLED display.color96.pyTests/demos for the Adafruit 0.96 inch color OLED.
Demos for larger displays.
color15.pyDemonstrates a variety of widgets. Cross platform.aclock.pyAnalog clock demo. Cross platform.alevel.pySpirit level using Pyboard accelerometer.fpt.pyPlot demo. Cross platform.scale.pyA demo of the newScalewidget. Cross platform.asnano_sync.pyTwo Pyboard specific demos using the GUI withuasyncio.asnano.pyCould readily be adapted for other targets.tbox.pyDemoTextboxclass. Cross-platform.
Usage with uasyncio is discussed here. In summary the blocking
which occurs during transfer of the framebuffer to the display may affect more
demanding uasyncio applications. More generally the GUI works well with it.
Demo scripts for Sharp displays are in drivers/sharp. Check source code for
wiring details. See the README. They may be run as
follows:
import drivers.sharp.sharptest
# or
import drivers.sharp.clocktestPython 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. Python fonts may be created using
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.pyVariable pitch Arial in various sizes.arial35.pyarial_50.pycourier20.pyFixed pitch font.font6.pyfont10.pyfreesans20.py
The color_setup 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.
esp32_setup.pyAs 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.esp8266_setup.pySimilar for ESP8266. Usage is somewhat experimental.st7735r_setup.pyAssumes a Pyboard with an Adafruit 1.8 inch TFT display.st7735r144_setup.pyFor a Pyboard with an Adafruit 1.44 inch TFT display.
The source tree now includes all dependencies. These are listed to enable users to check for newer versions.
- writer.py Provides text rendering.
Optional feature:
- An STM32 implementation of this optimisation.
A copy of the official driver for OLED displays using the SSD1306 chip is provided. The official 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:
The Sharp display is supported in drivers/sharp. See
README and demos.
Drivers for Adafruit 0.96", 1.27" and 1.5" OLEDS are included in the source
tree. Each driver has its own small README.md. The default driver for the
larger OLEDs is Pyboard specific, but there are slightly slower cross platform
alternatives in the directory - see the code below for usage on ESP32.
If using the Adafruit 1.5 or 1.27 inch color OLED displays it is suggested that
after installing the GUI the following script is pasted at the REPL. This will
verify the hardware. Please change height to 128 if using the 1.5 inch
display.
from machine import Pin, SPI
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Pyboard-specific driver
height = 96 # Ensure height is correct (96/128)
pdc = Pin('Y1', Pin.OUT_PP, value=0)
pcs = Pin('Y2', Pin.OUT_PP, value=1)
prst = Pin('Y3', Pin.OUT_PP, value=1)
spi = SPI(2)
ssd = SSD(spi, pcs, pdc, prst, height=height)
ssd.fill(0)
ssd.line(0, 0, 127, 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()On ESP32 the following may be used:
from machine import Pin, SPI
from drivers.ssd1351.ssd1351_generic import SSD1351 as SSD # Note generic driver
height = 128 # Ensure height is correct (96/128)
pdc = Pin(25, Pin.OUT, value=0)
pcs = Pin(26, Pin.OUT, value=1)
prst = Pin(27, Pin.OUT, value=1)
spi = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(13), miso=Pin(12))
ssd = SSD(spi, pcs, pdc, prst, height=height)
ssd.fill(0)
ssd.line(0, 0, 127, 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()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.
The GUI is initialised for color display by issuing:
from color_setup import ssd, heightThis works as described below.
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) # Initialise and clear display.This is followed by Python fonts. A CWriter instance is created for each
font (for monochrome displays a Writer is used). Upside down rendering is not
supported. Only the Textbox widget supports scrolling text.
from gui.core.writer import CWriter # Renders color text
import gui.fonts.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.set_clip(True, True, False)The application calls nanogui.refresh on initialisation to clear the display,
then subsequently whenever a refresh is required. The method takes two args:
deviceThe display instance (the GUI supports multiple displays).clear=FalseIf setTruethe display will be blanked; it is also blanked when a device is refreshed for the first time.
The file color_setup.py contains the hardware dependent code. It works as
described below, with the aim of allocating the FrameBuffer before importing
other modules. This is intended to reduce the risk of memory failures.
Firstly the file sets the display height and imports the driver:
height = 96 # 1.27 inch 96*128 (rows*cols) display. Set to 128 for 1.5 inch
import machine
import gc
from drivers.ssd1351.ssd1351 import SSD1351 as SSD # Import the display driverIt then sets up the bus (SPI or I2C) and instantiates the display. At this point the framebuffer is created:
pdc = machine.Pin('X1', machine.Pin.OUT_PP, value=0)
pcs = machine.Pin('X2', machine.Pin.OUT_PP, value=1)
prst = machine.Pin('X3', machine.Pin.OUT_PP, value=1)
spi = machine.SPI(1)
gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instanceThis supports applications where text is to be rendered at specific screen locations.
Text can be static or dynamic. In the case of dynamic text the background is cleared to ensure that short strings cleanly replace longer ones.
Labels can be displayed with an optional single pixel border.
Colors are handled flexibly. By default the colors used are those of the
Writer instance, however they can be changed dynamically; this might be used
to warn of overrange or underrange values.
Constructor args:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.coltextIf 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 fromwriter.stringlen('-99.99'). Nothing is dsplayed until.value()is called. Intended for dynamic text fields.invert=FalseDisplay in inverted or normal style.fgcolor=NoneOptionally override theWritercolors.bgcolor=Nonebdcolor=FalseIfFalseno border is displayed. IfNonea border is shown in theWriterforgeround color. If a color is passed, it is used.
The constructor displays the string at the required location.
Methods:
valueRedraws the label. This takes the following args:text=NoneThe text to display. IfNonedisplays last value.invert=FalseIf true, show inverse text.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=NoneBorder color. As per above except that ifFalseis passed, no border is displayed. This clears a previously drawn border.
Returns the current text string.
showNo args. (Re)draws the label. Primarily for internal use by GUI.
If populating a label would cause it to extend beyond the screen boundary a warning is printed at the console. The label may appear at an unexpected place. The following is a complete "Hello world" script.
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)This provides a vertical linear meter display of values scaled between 0.0 and 1.0.
Constructor positional args:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.col
Keyword only args:
height=50Height of meter.width=10Width.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.ptcolor=NoneColor of meter pointer or bar. Default is foreground color.bdcolor=FalseIfFalseno border is displayed. IfNonea border is shown in theWriterforgeround color. If a color is passed, it is used.divisions=5No. of graduations to show.label=NoneA text string will cause aLabelto be drawn below the meter. An integer will create aLabelof that width for later use.style=Meter.LINEThe pointer is a horizontal line.Meter.BARcauses a vertical bar to be displayed.legends=NoneIf a tuple of strings is passed,Labelinstances will be displayed to the right hand side of the meter, starting at the bottom. E.G.('0.0', '0.5', '1.0')value=NoneInitial value. IfNonethe meter will not be drawn until itsvalue()method is called.
Methods:
valueArgs:n=None, color=None.nshould be a float in range 0 to 1.0. Causes the meter to be updated. Out of range values are constrained. IfNoneis passed the meter is not updated.colorUpdates the color of the bar or line if a value is also passed.Nonecauses no change.
Returns the current value.
textUpdates the label if present (otherwise throws aValueError). Args:text=NoneThe text to display. IfNonedisplays last value.invert=FalseIf true, show inverse text.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=NoneBorder color. As per above except that ifFalseis passed, no border is displayed. This clears a previously drawn border.
showNo args. (Re)draws the meter. Primarily for internal use by GUI.
This is a virtual LED whose color may be altered dynamically.
Constructor positional args:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.col
Keyword only args:
height=12Height of LED.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=FalseIfFalseno border is displayed. IfNonea border is shown in theWriterforgeround color. If a color is passed, it is used.label=NoneA text string will cause aLabelto be drawn below the LED. An integer will create aLabelof that width for later use.
Methods:
colorargc=NoneChange the LED color toc. IfcisNonethe LED is turned off (rendered in the background color).textUpdates the label if present (otherwise throws aValueError). Args:text=NoneThe text to display. IfNonedisplays last value.invert=FalseIf true, show inverse text.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=NoneBorder color. As per above except that ifFalseis passed, no border is displayed. This clears a previously drawn border.
showNo args. (Re)draws the LED. Primarily for internal use by GUI.
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.
Constructor positional args:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.col
Keyword only args:
height=50Height and width of dial.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=FalseIfFalseno border is displayed. IfNonea border is shown in theWriterforgeround color. If a color is passed, it is used.ticks=4No. of gradutions to show.label=NoneA text string will cause aLabelto be drawn below the meter. An integer will create aLabelof that width for later use.style=Dial.CLOCKPointers are drawn from the centre of the circle as per the hands of a clock.Dial.COMPASScauses pointers to be drawn as arrows centred on the control's centre. Arrow tail chevrons are suppressed for very short pointers.pip=NoneDraws a central dot. A color may be passed, otherwise the foreground color will be used. IfFalseis 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.
Constructor arg:
dialTheDialinstance on which it is to be dsplayed.
Methods:
valueArgs:v=NoneThe value is a complex number. A magnitude exceeding unity is reduced (preserving phase) to constrain thePointerwithin the unit circle.color=NoneBy default the pointer is rendered in the foreground color of the parentDial. Otherwise the passed color is used.
Returns the current value.
textUpdates the label if present (otherwise throws aValueError). Args:text=NoneThe text to display. IfNonedisplays last value.invert=FalseIf true, show inverse text.fgcolor=NoneForeground color: ifNonetheWriterdefault is used.bgcolor=NoneBackground color, as per foreground.bdcolor=NoneBorder color. As per above except that ifFalseis passed, no border is displayed. This clears a previously drawn border.
showNo 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)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:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.col
Keyword only arguments (all optional):
ticks=200Number of "tick" divisions on scale. Must be divisible by 2.legendcb=NoneCallback for populating scale legends (see below).tickcb=NoneCallback for setting tick colors (see below).height=0Pass 0 for a minimum height based on the font height.width=200bdcolor=NoneBorder color. IfNone,fgcolorwill be used.fgcolor=NoneForeground color. Defaults to system color.bgcolor=NoneBackground color defaults to system background.pointercolor=NoneColor of pointer. Defaults to.fgcolor.fontcolor=NoneColor of legends. Defaultfgcolor.
Method:
value=NoneSet or get the current value. Always returns the current value. A passedfloatis constrained to the range -1.0 <= V <= 1.0 and becomes theScale's current value. TheScaleis updated. PassingNoneenables reading the current value.
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.
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 cThis 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.
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 TextboxConstructor mandatory positional arguments:
writerTheWriterinstance (font and screen) to use.rowLocation on screen.colwidthWidth of the object in pixels.nlinesNumber 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=NoneBorder color. IfNone,fgcolorwill be used.fgcolor=NoneColor of border. Defaults to system color.bgcolor=NoneBackground color of object. Defaults to system background.clip=TrueBy default lines too long to display are right clipped. IfFalseis passed, word-wrap is attempted. If the line contains no spaces it will be wrapped at the right edge of the window.
Methods:
appendArgss, ntrim=None, line=NoneAppend the stringsto 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 integerntrim=Nis passed, only the last N lines are retained;ntrimmay be greater than can be shown in the control, hidden lines being accessed by scrolling.
If an integer (typically 0) is passed inlinethe display will scroll to show that line.scrollArgnNumber 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. ReturnsTrueif scrolling occurred, otherwiseFalse.valueNo args. Returns the number of lines of text stored in the widget.clearNo args. Clears all lines from the widget and refreshes the display.gotoArgline=NoneFast 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.
Device drivers capable of supporting nanogui can be extremely simple: see
drivers/sharp/sharp.py for a minimal example. It should be noted that the
supplied device drivers are designed purely to support nanogui. To conserve RAM
they provide no functionality beyond the transfer of an external frame buffer
to the device. This transfer typically takes a few tens of milliseconds. While
visually instant, this period constitutes latency between an event occurring
and a consequent display update. This may be unacceptable in applications such
as games. In such cases the FrameBuffer approach is inappropriate. Many
driver chips support graphics primitives in hardware; drivers using these
capabilities will be faster than those provided here and may often be found
using a forum search.
For a driver to support nanogui it must be subclassed from
framebuf.FrameBuffer and provide height and width bound variables being
the display size in pixels. This, and a show method, are all that is required
for monochrome drivers.
Refresh must be handled by a show method taking no arguments; when called,
the contents of the buffer underlying the FrameBuffer must be copied to the
hardware.
For color drivers, to conserve RAM it is suggested that 8-bit color is used
for the FrameBuffer. If the hardware does not support this, conversion to the
supported color space needs to be done "on the fly" as per the SSD1351 driver.
This uses framebuf.GS8 to stand in for 8 bit color in rrrgggbb format. To
maximise update speed consider using native, viper or assembler for the
conversion, typically to RGB565 format.
Color drivers should have a static method converting rgb(255, 255, 255) to a form acceptable to the driver. For 8-bit rrrgggbb this can be:
@staticmethod
def rgb(r, g, b):
return (r & 0xe0) | ((g >> 3) & 0x1c) | (b >> 6)This should be amended if the hardware uses a different 8-bit format.
The Writer (monochrome) or CWriter (color) classes and the nanogui module
should then work automatically.
Drivers for displays using I2C may need to use I2C.writevto depending on the chip requirements.
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).
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 uasyncio
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 is the structure of my frozen
directory:
I erased flash, built and installed the new firmware. Finally I copied
color_setup/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.
In conclusion I think that applications of moderate complexity should be
feasible.
