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
```python
from gui.widgets import BitMap
```
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
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.
Question generator in coroutine.
Constructor mandatory positional args:
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)
## 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)
# 7. Graph Plotting

Wyświetl plik

@ -29,9 +29,9 @@ class BaseScreen(Screen):
self.image = 0
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 %= 28
self.image %= 4
if self.image == 3:
self.graphic.color(BLUE)
else:

Wyświetl plik

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

Wyświetl plik

@ -13,16 +13,17 @@ import uasyncio as asyncio
from machine import Pin
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,
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_y = pin_y
self._x = pin_x()
self._y = pin_y()
self._v = v * div # Initialise hardware 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):
raise ValueError('Incompatible args: must have vmin <= v <= vmax')
self._tsf = asyncio.ThreadSafeFlag()

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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