First pass at bitmap and qrcode widgets.

pull/16/head
Peter Hinch 2022-06-08 17:10:50 +01:00
rodzic 3f7be4ebba
commit c03ed90796
7 zmienionych plików z 139 dodań i 85 usunięć

Wyświetl plik

@ -2519,17 +2519,94 @@ different callback if the application required it.
## 6.19 BitMap Widget ## 6.19 BitMap Widget
```python
from gui.widgets import BitMap
```
This renders a monochrome bitmap stored in a file to a rectangular region. The This renders a monochrome bitmap stored in a file to a rectangular region. The
bitmap file format is C source code generated by the Linux `bitmap` editor. The bitmap file format is C source code generated by the Linux `bitmap` editor. The
data may be rendered in any color. Data and colors can be changed at run time. bitmap may be rendered in any color. Data and colors can be changed at run time.
Not suitable for animation due to blocking time. Constructor mandatory positional args:
Question generator in coroutine. 1. `writer` A `Writer` instance.
2. `row` Location on screen.
3. `col`
4. `height` Image height in pixels. Dimensions must exactly match the image file.
5. `width` Image width in pixels.
Keyword only args:
* `fgcolor=None` Foreground (1) color of image.
* `bgcolor=None` Background (0) color.
* `bdcolor=RED` Border color.
Methods:__
* `value` mandatory arg `fn` path to an image file. Causes the `BitMap` image
to be updated from the file. Blocks for a period depending on filesystem
performance.
* `color` args `fgcolor=None`, `bgcolor=None`. Causes the image colors to be
changed. The file will be re-read and the image updated.
The widget is designed to minimise RAM usage at cost of performance. This is
because images may be large. When an update occurs there will be a brief "dead
time" when the GUI is unresponsive. This is not noticeable if the image is
displayed when a screen initialises, or if it changes in response to a user
action. Use in animations is questionable.
See `gui/demos/bitmap.py` for a usage example.
###### [Contents](./README.md#0-contents) ###### [Contents](./README.md#0-contents)
## 6.20 QRMap Widget ## 6.20 QRMap Widget
```python
from gui.widgets import QRMap
```
This renders QR codes generated using the [uQR](https://github.com/JASchilz/uQR)
application. Images may be scaled to render them at larger sizes. Please see
the notes below on performance and RAM usage.
Constructor positional args:
1. `writer` A `Writer` instance.
2. `row` Location on screen.
3. `col`
4. `version=4` Defines the size of the image: see below.
5. `scale=1`
Keyword only args:
* `bdcolor=RED` Border color.
* `buf=None` Allows use of a pre-allocated image buffer.
Methods:__
* `value` mandatory arg `text` a string for display as a QR code. This method
can throw a `ValueError` if the string cannot be accommodated in the chosen
code size (i.e. `version`).
* `__call__` Synonym for `value`.
Static Method:__
* `make_buffer` args `version`, `scale`. Returns a buffer big enough to hold
the QR code bitmap. Use of this is optional: it is a solution if memory errors
are encountered when instantiating a `QRMap`.
Note on image sizes. The size of a QR code bitmap depends on the `version` and
`scale` parameters according to this formula:
`edge_length_in_pixels = (4 * version + 17) * scale`
To this must be added a mandatory 4 pixel border around every edge. So the
height and width occupied on screen is:
`dimension = (4 * version + 25) * scale`
Performance
The uQR `get_matrix()` method blocks: in my testing for about 750ms. A `QRMap`
buffers the scaled matrix and renders it using bit blitting. Blocking by
`QRMap` methods is minimal; refreshing a screen with the same contents is fast.
The `uQR` library is large, and compiling it uses a substantial amount of RAM.
If memory errors are encountered try cross-compiling or the use of frozen byte
code.
See `gui/demos/qrcode.py` for a usage example. The demo expects `uQR.py` to be
located in the root directory of the target.
###### [Contents](./README.md#0-contents) ###### [Contents](./README.md#0-contents)
# 7. Graph Plotting # 7. Graph Plotting

Wyświetl plik

@ -29,9 +29,9 @@ class BaseScreen(Screen):
self.image = 0 self.image = 0
def cb(self, _): def cb(self, _):
self.graphic.value(f"/moon/m{self.image:02d}") self.graphic.value(f"/gui/fonts/bitmaps/m{self.image:02d}")
self.image += 1 self.image += 1
self.image %= 28 self.image %= 4
if self.image == 3: if self.image == 3:
self.graphic.color(BLUE) self.graphic.color(BLUE)
else: else:

Wyświetl plik

@ -4,17 +4,12 @@
# Copyright (c) 2022 Peter Hinch # Copyright (c) 2022 Peter Hinch
# hardware_setup must be imported before other modules because of RAM use. # hardware_setup must be imported before other modules because of RAM use.
import gc
import hardware_setup # Create a display instance import hardware_setup # Create a display instance
from uQR import QRCode
from gui.core.ugui import Screen, ssd from gui.core.ugui import Screen, ssd
from gui.widgets import Label, Button, CloseButton, QRMap from gui.widgets import Label, Button, CloseButton, QRMap
# Create buffer for bitmapped graphic before fragmentation sets in scale = 2 # Magnification of graphic
scale = 3 # Magnification of graphic version = 4
qr_ht = scale * 41 #qr_buf = QRMap.make_buffer(version, scale)
qr_wd = scale * 41
qr_buf = QRMap.make_buffer(qr_ht, qr_wd)
gc.collect()
from gui.core.writer import CWriter from gui.core.writer import CWriter
import gui.fonts.arial10 as arial10 import gui.fonts.arial10 as arial10
from gui.core.colors import * from gui.core.colors import *
@ -24,23 +19,19 @@ class BaseScreen(Screen):
def __init__(self): def __init__(self):
def my_callback(button, graphic, qr): def my_callback(button, graphic):
qr.clear() graphic("https://en.wikipedia.org/wiki/QR_code")
qr.add_data("https://en.wikipedia.org/wiki/QR_code")
graphic.value(qr.get_matrix())
super().__init__() super().__init__()
wri = CWriter(ssd, arial10, GREEN, BLACK) wri = CWriter(ssd, arial10, GREEN, BLACK)
col = 2 col = 2
row = 2 row = 2
Label(wri, row, col, "QR code Demo.") Label(wri, row, col, "QR code Demo.")
row = 50 row = 25
graphic = QRMap(wri, row, col, (qr_ht, qr_wd), scale, fgcolor=BLACK, bgcolor=WHITE, buf=qr_buf) graphic = QRMap(wri, row, col, version, scale)
qr = QRCode(version=4) # Gives 41x41 matrix graphic("uQR rocks!")
qr.add_data("uQR rocks!") col = 120
graphic.value(qr.get_matrix()) Button(wri, row, col, text="URL", callback=my_callback, args=(graphic,))
col = 160
Button(wri, row, col, text="URL", callback=my_callback, args=(graphic, qr))
CloseButton(wri) # Quit the application CloseButton(wri) # Quit the application
def test(): def test():

Wyświetl plik

@ -13,16 +13,17 @@ import uasyncio as asyncio
from machine import Pin from machine import Pin
class Encoder: class Encoder:
delay = 100 # Pause (ms) for motion to stop/limit callback frequency
def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None,
mod=None, callback=lambda a, b : None, args=()): mod=None, callback=lambda a, b : None, args=(), delay=100):
self._pin_x = pin_x self._pin_x = pin_x
self._pin_y = pin_y self._pin_y = pin_y
self._x = pin_x() self._x = pin_x()
self._y = pin_y() self._y = pin_y()
self._v = v * div # Initialise hardware value self._v = v * div # Initialise hardware value
self._cv = v # Current (divided) value self._cv = v # Current (divided) value
self.delay = delay # Pause (ms) for motion to stop/limit callback frequency
if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax):
raise ValueError('Incompatible args: must have vmin <= v <= vmax') raise ValueError('Incompatible args: must have vmin <= v <= vmax')
self._tsf = asyncio.ThreadSafeFlag() self._tsf = asyncio.ThreadSafeFlag()

Wyświetl plik

@ -23,7 +23,7 @@ class BitMap(Widget):
if self._value is None: if self._value is None:
return return
with open(self._value, "r") as f: with open(self._value, "r") as f:
g = self.gen_bytes(f) g = self._gen_bytes(f)
bit = 1 bit = 1
wrap = False wrap = False
for row in range(self.height): for row in range(self.height):
@ -38,7 +38,7 @@ class BitMap(Widget):
byte = next(g) byte = next(g)
bit = 1 bit = 1
def gen_bytes(self, f): # Yield data bytes from file stream def _gen_bytes(self, f): # Yield data bytes from file stream
f.readline() f.readline()
f.readline() # Advance file pointer to data start f.readline() # Advance file pointer to data start
s = f.readline() s = f.readline()

Wyświetl plik

@ -2,77 +2,62 @@
# Released under the MIT License (MIT). See LICENSE. # Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2022 Peter Hinch # Copyright (c) 2022 Peter Hinch
import gc
from framebuf import FrameBuffer, MONO_HLSB from framebuf import FrameBuffer, MONO_HLSB
from gui.core.ugui import Widget from gui.core.ugui import Widget
from gui.core.colors import * from gui.core.colors import *
from gui.core.ugui import ssd from gui.core.ugui import ssd
from uQR import QRCode
from utime import ticks_diff, ticks_ms
class QRMap(Widget): class QRMap(Widget):
@staticmethod
def len_side(version):
return 4 * version + 17
@staticmethod @staticmethod
def make_buffer(height, width): # Given dimensions in pixels def make_buffer(version, scale):
w = (width >> 3) + int(width & 7 > 0) side = QRMap.len_side(version) * scale
return bytearray(height * w) width = (side >> 3) + int(side & 7 > 0) # Width in bytes
return bytearray(side * width)
def __init__(self, writer, row, col, image, scale=1, *, fgcolor=None, bgcolor=None, bdcolor=RED, buf=None): def __init__(self, writer, row, col, version=4, scale=1, *, bdcolor=RED, buf=None):
self._version = version
self._scale = scale self._scale = scale
self._image = image self._iside = self.len_side(version) # Dimension of unscaled QR image less border
try: side = self._iside * scale
height, width = self.dimensions() # Widget allows 4 * scale border around each edge
except OSError: border = 4 * scale
print(f"Failed to access {obj}.") wside = side + 2 * border # Widget dimension
raise super().__init__(writer, row, col, wside, wside, BLACK, WHITE, bdcolor, False)
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, False) super()._set_callbacks(self._update, ())
if buf is None: if buf is None:
buf = QRMap.make_buffer(height, width) buf = QRMap.make_buffer(version, scale)
else: self._fb = FrameBuffer(buf, side, side, MONO_HLSB)
if len(buf) != ((width >> 3) + int(width & 7 > 0)) * height: self._irow = row + border
raise OSError("Buffer size does not match width and height.") self._icol = col + border
self._fb = FrameBuffer(buf, width, height, MONO_HLSB) self._qr = QRCode(version, border=0)
if isinstance(image, list):
self.value(image)
def show(self): def show(self):
if super().show(True): # Draw or erase border if super().show(False): # Show white border
palette = ssd.palette palette = ssd.palette
palette.bg(self.bgcolor) palette.bg(self.bgcolor)
palette.fg(self.fgcolor) palette.fg(self.fgcolor)
ssd.blit(self._fb, self.col, self.row, -1, palette) ssd.blit(self._fb, self._icol, self._irow, -1, palette)
def color(self, fgcolor=None, bgcolor=None): def _update(self, _): # Runs when value changes
if fgcolor is not None: t = ticks_ms()
self.fgcolor = fgcolor qr = self._qr
if bgcolor is not None: qr.clear()
self.bgcolor = bgcolor qr.add_data(self._value)
self.draw = True matrix = qr.get_matrix() # 750ms. Rest of the routine adds 50ms
if qr.version != self._version:
def dimensions(self): # Dimensions of current image in pixels raise ValueError("Text too long for QR version.")
obj = self._image wd = self._iside
if isinstance(obj, list): # 2d list of booleans s = self._scale
return len(obj) * self._scale, len(obj[0] * self._scale) for row in range(wd):
if isinstance(obj, tuple): for col in range(wd):
return obj v = matrix[row][col]
raise OSError for nc in range(s):
for nr in range(s):
def value(self, obj): self._fb.pixel(col * s + nc, row * s + nr, v)
self._image = obj
self._fb.fill(self.bgcolor) # In case tuple was passed or image smaller than buffer
if isinstance(obj, list): # 2d list of booleans
wd, ht = self.dimensions()
s = self._scale
if wd > self.width or ht > self.height:
print('Object too large for buffer', wd, self.width, ht, self.height)
else:
print(f"Object is {wd} x {ht} pixels")
for row in range(ht//s):
for col in range(wd//s):
v = obj[row][col]
for nc in range(s):
for nr in range(s):
self._fb.pixel(col * s + nc, row * s + nr, v)
else:
print(f"Invalid QR code {obj}.")
self.draw = True
gc.collect()

Wyświetl plik

@ -42,7 +42,7 @@ pcs = Pin(10, Pin.OUT, value=1)
spi = SPI(0, baudrate=30_000_000) spi = SPI(0, baudrate=30_000_000)
gc.collect() # Precaution before instantiating framebuf gc.collect() # Precaution before instantiating framebuf
ssd = SSD(spi, pcs, pdc, prst, usd=True) ssd = SSD(spi, pcs, pdc, prst, usd=True)
gc.collect()
from gui.core.ugui import Display, quiet from gui.core.ugui import Display, quiet
# quiet() # quiet()
# Create and export a Display instance # Create and export a Display instance