
The Plotter
class takes the data and plots it on the screen with optional output to the serial console for plotting by Mu or general data collection.
The full class diagram would reveal a lot of attributes and operations suggesting it's a bulky, complex class. Sometimes this is indicative that the design could benefit from some further decomposition and refinement.
The expected usage after instantiation of the object is:
-
y_range()
- set the initial range on the plot. -
y_min_range
- optional but limits the degree of zoom in. -
y_full_range()
- set the absolute range for data from sensor. -
channels
- set number of elements of data per sample. -
channel_colidx
- set colours to use in for of sequence of palette numbers. -
display_on()
- initialize display - once only. -
data_add()
- use repeatedly to draw new points or lines based on the settings - this will also eventually scroll or wrap. - Goto 7.
Accessibility
From The Role of Accessibility in a Universal Web:
"Universal design" is the process of creating products that are usable by people with the widest possible range of abilities, operating within the widest possible range of situations; whereas "accessibility" primarily refers to design for people with disabilities. While the focus of accessibility is disabilities, research and development in accessibility brings benefits to everyone, particularly users with situational limitations, including device limitations and environmental limitations.
Small devices like the CLUE with its 1.3" (33mm) screen are, by their nature, limited for the visual aspects of accessibility but we can still consider:
- contrast for foreground/background colours of text,
- thoughtful colour selection.
It's common and very tempting to represent (x,y,z)
values using the three primary colours: red, green and blue. Unfortunately this combination clashes with common forms of colour blindess.
The RGB palette is used in the sensor plotter for many of the sensors but the user can also override this with a "default palette". This is loosely based on common digital storage oscilloscopes:
- Yellow (
0xffff00
), - Cyan (
0x00ffff
), - "Almost Pink" (
0xff0080
).
These can be tested on a colour simulator but it's best to test with some real people.
Auto-scaling Algorithm
The PlotSource
object has methods to provide the absolute minimum and maximum values for the data from that source. A typical feature would be to set the y axis scale based on the observed data values to get a more detailed view.
The current algorithm is shown below in two flow charts, the second is a sub routine used in the first one.
The data_mins
and data_maxs
are lists of the recent minimum and maximums for approximate 1 second periods retained for a configurable number of seconds.
The change_range()
sub routine (implemented as a method) implements the optional y_min_range
feature. This prevents zooming in excessively showing uninteresting, random noise from some sensors.
The zoom out will always occur if the data is off the current range, i.e. off screen. The zoom in is a little more cautious.
There's one extra feature to reduce the frequency of zooming in not shown in the diagrams. A timestamp is recorded whenever a zoom in takes place and this is used to prevent zooming in again until N seconds has passed.
Based on acceptance testing, the zooming still occurs when it looks unnecessary. This algorithm needs further improvement perhaps using a hysterisis-based approach.
Efficient Use of displayio
The displayio library for CircuitPython (or Adafruit_GFX for Arduino) provides a single library which can be used with a variety of different size LED, LCD and ePaper screens. This abstraction is very useful and removes the need to directly program the CLUE's ST7789 LCD display. The only details the programmer needs to know for low update rates are:
- the resolution of the screen (240x240) and
- whether it has enough colour depth to render the desired colours sufficiently accurately (16bit).
These small LCD screens are not designed for high frame rates. If the screen needs to be updated frequently then the performance needs to be explored. The displayio
library is implemented in compiled code to improve the performance but it needs to be thoughtfully used from CircuitPython since this is slower due to being executed on an interpreter.
Scrolling
A plotter needs to do something when the points/lines reach the edge of the screen. It can either
- wrap like an oscilloscope or
- scroll the existing data to the left.
The Bitmap class does not currently provide clear, fill or scroll methods. Some early exploratory programming revealed that slice assignment isn't supported and clearing a large Bitmap
pixel by pixel is a very slow process. Some simple code to time clearing a Bitmap
is shown below.
# Quick benchmark of clearing a displayio Bitmap using for loops # See https://github.com/adafruit/circuitpython/issues/2688 import time import board, displayio WIDTH = 201 HEIGHT = 200 display = board.DISPLAY # eight colours is 3 bits per pixel when packed bitmap = displayio.Bitmap(WIDTH, HEIGHT, 8) palette = displayio.Palette(8) palette[0] = 0x000000 palette[1] = 0xff0000 palette[2] = 0x00ff00 palette[3] = 0x0000ff tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette) group = displayio.Group() group.append(tile_grid) display.auto_refresh=False display.show(group) def refresh_screen(disp): while True: refreshed = False try: refreshed = disp.refresh(minimum_frames_per_second=0) except Exception: pass if refreshed: break def fillscreen1(bmp, col_idx): for x in range(WIDTH): for y in range(HEIGHT): bmp[x, y] = col_idx def fillscreen2(bmp, col_idx): for y in range(HEIGHT): for x in range(WIDTH): bmp[x, y] = col_idx def fillscreen3(bmp, col_idx): for idx in range(WIDTH * HEIGHT): bmp[idx] = col_idx # "Big" Python has a timeit library but not present on CircuitPython # so it's time for some for loops for func in (fillscreen1, fillscreen2, fillscreen3): for _ in range(2): for colour_idx in (0, 0, 0, 1, 2, 3): t1 = time.monotonic_ns() func(bitmap, colour_idx) refresh_screen(display) t2 = time.monotonic_ns() func_name = str(func).split(" ")[1] print(func_name, colour_idx, "{:.3f}s".format((t2 - t1) / 1e9)) time.sleep(0.5)
This simple benchmark could be improved as it both updates the Bitmap data and performs a single refresh of the screen. It would be informative to observe the performance of the two actions individually.
The output is shown below. fillscreen1
takes 1.25 seconds, fillscreen3
is faster at 0.75 seconds, fillscreen2
isn't shown as it was same as fillscreen1
.
fillscreen1 0 1.252s fillscreen1 0 1.250s fillscreen1 0 1.250s fillscreen1 1 1.251s fillscreen1 2 1.251s fillscreen1 3 1.249s fillscreen1 0 1.250s fillscreen1 0 1.251s fillscreen1 0 1.251s fillscreen1 1 1.249s fillscreen1 2 1.250s fillscreen1 3 1.251s fillscreen3 0 0.753s fillscreen3 0 0.754s fillscreen3 0 0.755s fillscreen3 1 0.755s fillscreen3 2 0.753s fillscreen3 3 0.754s fillscreen3 0 0.755s fillscreen3 0 0.753s fillscreen3 0 0.754s fillscreen3 1 0.754s fillscreen3 2 0.755s fillscreen3 3 0.753s
These numbers would mean the screen would barely be able to update once per second. It's also slower if two bitmaps are overlaid which is a tempting solution to providing a static background.
This benchmarking lead to a change in design to use a more complex "un-drawing" technique. This reduces the number of pixel changes dramatically decreasing the time to clear the screen. The downside is the added complexity in storing the data and in the procedure to draw over the existing plot with background colour pixels.
Further testing revealed this undraw was still fairly slow. This lead to another iteration of the design. Reducing the frequency of scrolling was required and this could be achieved with a "jump" scroll - scrolling the data by more than one pixel at a time.
Resolution and Scaling
The final implementation of the Plotter
class uses a Bitmap
with a resolution of 192x201 pixels for the plot. The width was reduced to allow an extra character on the y axis tick labels.
Group
has a feature to scale objects by an integer amount. This is implemented in C and is likely to be efficient. Another potential option to speed up the code would be to lower the resolution and use scale=2
to display it - a trade-off between resolution and performance. This could be implemented as a user-selected option.
Page last edited March 08, 2024
Text editor powered by tinymce.