Prior to EPD API branch

pull/56/head
peterhinch 2023-05-09 16:50:29 +01:00
rodzic 70480d1e4e
commit 2868bcaf2e
15 zmienionych plików z 1282 dodań i 45 usunięć

Wyświetl plik

@ -354,7 +354,9 @@ def spi_init(spi):
## 3.2 Drivers for ILI9341
Adafruit make several displays using this chip, for example
[this 3.2 inch unit](https://www.adafruit.com/product/1743).
[this 3.2 inch unit](https://www.adafruit.com/product/1743). This display is
large by microcontroller standards. See below for discussion of which hosts can
be expected to work.
The `color_setup.py` file should initialise the SPI bus with a baudrate of
10_000_000. Args `polarity`, `phase`, `bits`, `firstbit` are defaults. Hard or
@ -384,10 +386,15 @@ def spi_init(spi):
```
The ILI9341 class uses 4-bit color to conserve RAM. Even with this adaptation
the buffer size is 37.5KiB. See [Color handling](./DRIVERS.md#11-color-handling)
for details of the implications of 4-bit color. On a Pyboard 1.1 the `scale.py`
demo ran with 34.5K free with no modules frozen, and with 47K free with `gui`
and contents frozen.
the buffer size is 37.5KiB which is too large for some platforms. On a Pyboard
1.1 the `scale.py` demo ran with 34.5K free with no modules frozen, and with
47K free with `gui` and contents frozen. An ESP32 with SPIRAM has been tested.
On an ESP32 without SPIRAM, `nano-gui` runs but
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) requires
frozen bytecode. The RP2 Pico runs both GUI's.
See [Color handling](./DRIVERS.md#11-color-handling) for details of the
implications of 4-bit color.
The driver uses the `micropython.viper` decorator. If your platform does not
support this, the Viper code will need to be rewritten with a substantial hit
@ -400,7 +407,8 @@ are required. However this period may be unacceptable for some `uasyncio`
applications. The driver provides an asynchronous `do_refresh(split=4)` method.
If this is run the display will be refreshed, but will periodically yield to
the scheduler enabling other tasks to run. This is documented
[here](./ASYNC.md).
[here](./ASYNC.md). [micro-gui](https://github.com/peterhinch/micropython-micro-gui)
uses this automatically.
Another option to reduce blocking is overclocking the SPI bus.
@ -653,9 +661,10 @@ In addition to ILI9486 these have been tested: ILI9341, ILI9488 and HX8357D.
The ILI9486 supports displays of up to 480x320 pixels which is large by
microcontroller standards. Even with 4-bit color the frame buffer requires
76,800 bytes. On a Pico `nanogui` works fine, but `micro-gui` fails to
76,800 bytes. On a Pico `nanogui` works fine, but
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) fails to
compile unless frozen bytecode is used, in which case it runs with about 75K of
free RAM.
free RAM. An ESP32 with SPIRAM should work.
##### Generic display wiring
@ -951,12 +960,17 @@ completely with the device retaining the image indefinitely. Present day EPD
units perform the slow refresh autonomously - the process makes no demands on
the CPU enabling user code to continue to run.
The drivers are compatible with `uasyncio`. One approach is to use synchronous
methods only and the standard demos (some of which use `uasyncio`) may be run.
However copying the framebuffer to the device blocks for some time - 250ms or
more - which may be problematic for applications which need to respond to
external events. A specific asynchronous mode provides support for reducing
blocking time. See [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
The standard refresh method blocks (monopolises the CPU) until refresh is
complete, adding an additional 2s delay. This enables the demo scripts to run
unchanged, with the 2s delay allowing the results to be seen before the next
refresh begins. This is fine for simple applications. The drivers also support
concurrency with `uasyncio`. Such applications can perform other tasks while a
refresh is in progress. See
[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
Finally the [Waveshare 400x300 Pi Pico display](./DRIVERS.md#53-waveshare-400x300-pi-pico-display)
supports partial updates. This is a major improvement in usability. This unit
is easily used with hosts other than Pico/Pico W and is highly recommended.
## 5.1 Adafruit monochrome eInk Displays
@ -1285,30 +1299,46 @@ ghosting.
# 6. EPD Asynchronous support
Normally when GUI code issues
The following applies to nano-gui. Under micro-gui the update mechanism is
a background task. Use with micro-gui is covered
[here](https://github.com/peterhinch/micropython-micro-gui/blob/main/README.md#10-epaper-displays).
Further, the comments address the case where the driver is instantiated with
`asyn=True`. In the default case an EPD can be used like any other display.
When GUI code issues
```python
refresh(ssd) # 250ms or longer depending on platform
refresh(ssd) # Several seconds on an EPD
```
display data is copied to the device and a physical refresh is initiated. The
code blocks while copying data to the display before returning. Subsequent
physical refresh is performed by the display hardware taking several seconds.
While physical refresh is nonblocking, the initial blocking period is too long
for many `uasyncio` applications.
the GUI updates the frame buffer contents and calls the device driver's `show`
method. This causes the contents to be copied to the display hardware and a
redraw to be inititated. This typically takes several seconds unless partial
updates are enabled. The method (and hence `refresh`) blocks until the physical
refresh is complete. The device drivers block for an additional 2 seconds: this
enables demos written for normal displays to work (the 2 second pause allowing
the result of each refresh to be seen).
If an `EPD` is instantiated with `asyn=True` the process of copying the data to
the device is performed by a task which periodically yields to the scheduler.
By default blocking is limited to around 30ms.
This long blocking period is not ideal in asynchronous code, and the process is
modified if, in `color_setup.py`, an `EPD` is instantiated with `asyn=True`. In
this case `refresh` calls the `show` method as before, but `show` creates a
task `._as_show` and returns immediately. The task yields to the scheduler as
necessary to ensure that blocking is limited to around 30ms. With `asyn=True`
synchronous applications will not work: it is necessary to take control of the
sequencing of refresh.
A `.updated()` method lets user code pause after issuing `refresh()`. The pause
lasts until the framebuf has been entirely copied to the hardware. The
application is then free to alter the framebuf contents.
In this case user code should ensure that changes to the framebuffer are
postponed until the buffer contents have been copied to the display. Further, a
subsequent refresh should be postponed until the physical refresh is complete.
To achieve this the `ssd` instance has the following methods:
* `.updated()` (async) Pauses until the buffer is copied to the device.
* `.wait()` (async) Pauses until physical refresh is complete.
* `.ready()` (synchronous) Immediate return: `True` if physical refresh is
complete.
It is invalid to issue `.refresh()` until the physical display refresh is
complete; if this is attempted a `RuntimeError` will occur. The most efficient
way to ensure that this cannot occur is to await the `.wait()` method prior to
a refresh. This method will pause until any physical update is complete.
If `.refresh()` is issued before the physical display refresh is complete a
`RuntimeError` will occur.
The following illustrates the kind of approach which may be used:
The following illustrates the kind of approach which may be used with a display
instantiated with `asyn=True`:
```python
while True:
# Before refresh, ensure that a previous refresh is complete
@ -1319,10 +1349,21 @@ The following illustrates the kind of approach which may be used:
await ssd.updated()
# Trigger an event which allows other tasks to update the
# framebuffer in background
evt.set()
evt.clear()
await asyncio.sleep(180)
evt.set() # Waiting task must clear the Event
await asyncio.sleep(180) #
```
Some displays support partial updates. This is currently restricted to the
[Pico Epaper 4.2"](https://www.waveshare.com/pico-epaper-4.2.htm). Partial
updates are much faster and are visually non-intrusive at a cost of "ghosting"
where black pixels fail to be fully cleared. All ghosting is removed when a
full refresh is issued. Where a driver supports partial updates the following
synchronous methods are provided:
* `set_partial()` Enable partial updates.
* `set_full()` Restore normal update operation.
These must not be issued while an update is in progress.
See the demo `eclock_async.py` for an example of managing partial updates: once
per hour a full update is performed.
###### [Contents](./DRIVERS.md#contents)

Wyświetl plik

@ -62,6 +62,7 @@ display.
     3.1.1 [User defined colors](./README.md#311-user-defined-colors)
     3.1.2 [Monochrome displays](./README.md#312-monochrome-displays) A slight "gotcha" with ePaper.
     3.1.3 [Display update mechanism](./README.md#313-display-update-mechanism) How updates are managed.
     3.1.4 [ePaper displays](./README.md#314-epaper-displays) New developments in ePaper.
3.2 [Label class](./README.md#32-label-class) Dynamic text at any screen location.
3.3 [Meter class](./README.md#33-meter-class) A vertical panel meter.
3.4 [LED class](./README.md#34-led-class) Virtual LED of any color.
@ -79,6 +80,13 @@ display.
#### [Graph plotting module.](./FPLOT.md)
#### [The extras directory.](./extras/README.md)
The `extras` directory contains further widgets back-ported from
[micro-gui](https://github.com/peterhinch/micropython-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
@ -192,16 +200,13 @@ may need to be adapted for non-pyboard targets):
> cp color_setup.py /sd
> repl ~ import gui.demos.aclock
```
This demo reports to the REPL whether the performance boost described below is
active.
## 1.4 A performance boost
A firmware change in V1.17 has enabled the code size to be reduced. It has also
accelerated text rendering on color displays. Use of color displays now
requires firmware V1.17 or later. Existing users should update the display
driver and GUI core files and should ensure that the new file
`drivers/boolpalette.py` exists.
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.
###### [Contents](./README.md#contents)
@ -364,7 +369,8 @@ in this repo but may be found here:
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.
that pixels up to the edges of the display can be accessed. It is highly
recommended that this be run on any new installation.
```python
from color_setup import ssd # Create a display instance
from gui.core.colors import RED, BLUE, GREEN
@ -482,8 +488,13 @@ demo `color15.py` for an example.
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. This can produce a surprise on
ePaper units where the foreground is white.
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
@ -504,6 +515,16 @@ 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](https://www.waveshare.com/pico-epaper-4.2.htm).
This can be used in such roles and is discussed in
[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
###### [Contents](./README.md#contents)
## 3.2 Label class

Wyświetl plik

@ -309,6 +309,7 @@ class EPD(framebuf.FrameBuffer):
self._busy = False
self.display_on()
self.wait_until_ready()
time.sleep_ms(2000) # Give time for user to see result
def sleep(self):
# self.send_command(b"\x02") # power off

112
extras/DATE.md 100644
Wyświetl plik

@ -0,0 +1,112 @@
# Simple Date classes
The official [datetime module](https://github.com/micropython/micropython-lib/tree/master/python-stdlib/datetime)
is fully featured but substantial. This `Date` class has no concept of time,
but is very compact. Dates are stored as a small int. Contrary to normal MP
practice, properties are used. This allows basic arithmetic syntax while
ensuring automatic rollover. The speed penalty of properties is unlikely to be
a factor in date operations.
The `Date` class provides basic arithmetic and comparison methods. The
`DateCal` subclass adds pretty printing and methods to assist in creating
calendars.
# Date class
The `Date` class embodies a single date value which may be modified, copied
and compared with other `Date` instances.
## Constructor
This takes a single optional arg:
* `lt=None` By default the date is initialised from system time. To set the
date from another time source, a valid
[localtime/gmtime](http://docs.micropython.org/en/latest/library/time.html#time.localtime)
tuple may be passed.
## Method
* `now` Arg `lt=None`. Sets the instance to the current date, from system time
or `lt` as described above.
## Writeable properties
* `year` e.g. 2023.
* `month` 1 == January. May be set to any number, years will roll over if
necessary. e.g. `d.month += 15` or `d.month -= 1`.
* `mday` Adjust day in current month. Allowed range `1..month_length`.
* `day` Days since epoch. Note that the epoch varies with platform - the value
may be treated as an opaque small integer. Use to adjust a date with rollover
(`d.day += 7`) or to assign one date to another (`date2.day = date1.day`). May
also be used to represnt a date as a small int for saving to a file.
## Read-only property
* `wday` Day of week. 0==Monday 6==Sunday.
## Date comparisons
Python "magic methods" enable date comparisons using standard operators `<`,
`<=`, `>`, `>=`, `==`, `!=`.
# DateCal class
This adds pretty formatting and functionality to return additional information
about the current date. The added methods and properties do not change the
date value. Primarily intended for calendars.
## Constructor
This takes a single optional arg:
* `lt=None` See `Date` constructor.
## Methods
* `time_offset` arg `hr=6`. This returns 0 or 1, being the offset in hours of
UK local time to UTC. By default the change occurs when the date changes at
00:00 UTC on the last Sunday in March and October. If an hour value is passed,
the change will occur at the correct 01:00 UTC. This method will need to be
adapted for other geographic locations.
* `wday_n` arg `mday=1`. Return the weekday for a given day of the month.
* `mday_list` arg `wday`. Given a weekday, for the current month return an
ordered list of month days matching that weekday.
## Read-only properties
* `month_length` Length of month in days.
* `day_str` Day of week as a string, e.g. "Wednesday".
* `month_str` Month as a string, e.g. "August".
## Class variables
* `days` A 7-tuple `("Monday", "Tuesday"...)`
* `months` A 12-tuple `("January", "February",...)`
# Example usage
```python
from date import Date
d = Date()
d.month = 1 # Set to January
d.month -= 2 # Date changes to same mday in November previous year.
d.mday = 25 # Set absolute day of month
d.day += 7 # Advance date by one week. Month/year rollover is handled.
today = Date()
if d == today: # Date comparisons
# do something
new_date = Date()
new_date.day = d.day # Assign d to new_date: now new_date == d.
print(d) # Basic numeric print.
```
The DateCal class:
```python
from date import DateCal
d = DateCal()
# Correct a UK clock for DST
d.now()
hour = (hour_utc + d.time_offset(hour_utc)) % 24
print(d) # Pretty print
x = d.wday_n(1) # Get day of week of 1st day of month
sundays = d.mday_list(6) # List Sundays for the month.
wday_last = d.wday_n(d.month_length) # Weekday of last day of month
```

222
extras/README.md 100644
Wyświetl plik

@ -0,0 +1,222 @@
# Nano-gui extras
This directory contains additional widgets and demos. Further widgets may be
back-ported here from micro-gui.
# Demos
These were tested on the Waveshare Pico Res Touch 2.8" display, a 320*240 LCD.
Scaling will be required for smaller units. They are found in `extras/demos`
and are run by issuing (for example):
```py
import extras.demos.calendar
```
* `calendar.py` Demonstrates the `Calendar` widget, which uses the `grid`
widget.
The following demos run on color displays. If an ePaper display is used,
partial updates must be supported. Currently these are only supported by the
Waveshare 400x300 Pi Pico display.
* `clock_test.py` Runs the `Clock` widget, showing current RTC time.
* `eclock_test.py` Runs the `EClock` widget, showing current RTC time.
* `eclock_async.py` Illustrates asynchronous coding with partial updates.
# Widgets
These are found in `extras/widgets`.
## Grid
```python
from gui.widgets import Grid # File: grid.py
```
![Image](https://github.com/peterhinch/micropython-micro-gui/blob/19b369e6e710174612bcfa1fa1bdf40d645f3b6f/images/grid.JPG)
This is a rectangular array of `Label` instances. Rows are of a fixed height
equal to the font height + 4 (i.e. the label height). Column widths are
specified in pixels with the column width being the specified width +4 to
allow for borders. The dimensions of the widget including borders are thus:
height = no. of rows * (font height + 4)
width = sum(column width + 4)
Cells may be addressed as a 1-dimensional list or by a `[row, col]` 2-list or
2-tuple.
Constructor args:
1. `writer` The `Writer` instance (font and screen) to use.
2. `row` Location of grid on screen.
3. `col`
4. `lwidth` If an integer N is passed all labels will have width of N pixels.
A list or tuple of integers will define the widths of successive columns. If
the list has fewer entries than there are columns, the last entry will define
the width of those columns. Thus `[20, 30]` will produce a grid with column 0
being 20 pixels and all subsequent columns being 30.
5. `nrows` Number of rows.
6. `ncols` Number of columns.
7. `invert=False` Display in inverted or normal style.
8. `fgcolor=None` Color of foreground (the control itself). If `None` the
`Writer` foreground default is used.
9. `bgcolor=BLACK` Background color of cells. If `None` the `Writer`
background default is used.
10. `bdcolor=None` Color of border of the widget and its internal grid. If
`False` no border or grid will be drawn. If `None` the `fgcolor` will be used,
otherwise a color may be passed.
11. `align=ALIGN_LEFT` By default text in labels is left aligned. Options are
`ALIGN_RIGHT` and `ALIGN_CENTER`. Justification can only occur if there is
sufficient space in the `Label` as defined by `lwidth`.
Methods:
* `show` Draw the grid lines to the framebuffer.
* `__getitem__` This enables an individual `Label`'s `value` method to be
retrieved using index notation. The args detailed above enable inividual cells
to be updated.
Sample usage (complete example):
```python
from color_setup import ssd
from gui.core.writer import CWriter
from gui.core.nanogui import refresh
import gui.fonts.font10 as font
from gui.core.colors import *
from extras.widgets.grid import Grid
from gui.widgets.label import ALIGN_CENTER, ALIGN_LEFT
wri = CWriter(ssd, font, verbose=False)
wri.set_clip(True, True, False) # Clip to screen, no wrap
refresh(ssd, True) # Clear screen and initialise GUI
colwidth = (40, 25) # Col 0 width is 40, subsequent columns 25
row, col = 10, 10 # Placement
rows, cols = 6, 8 # Grid dimensions
grid = Grid(wri, row, col, colwidth, rows, cols, align=ALIGN_CENTER)
grid.show() # Draw grid lines
# Populate grid
col = 0
for y, txt in enumerate("ABCDE"):
grid[[y + 1, col]] = txt
row = 0
for col in range(1, cols):
grid[[row, col]] = str(col)
grid[20] = "" # Clear cell 20 by setting its value to ""
grid[[2, 5]] = str(42) # Note syntax
# Dynamic formatting
def txt(text):
return {"text": text}
redfg = {"fgcolor": RED}
grid[[3, 7]] = redfg | txt(str(99)) # Specify color as well as text
invla = {"invert": True, "align": ALIGN_LEFT}
grid[[2, 1]] = invla | txt("x") # Invert using invert flag
bkongn = {"fgcolor": BLACK, "bgcolor": GREEN, "align": ALIGN_LEFT} # Invert by swapping bg and fg
grid[[3, 1]] = bkongn | txt("a")
grid[[4,2]] = {"fgcolor": BLUE} | txt("go")
refresh(ssd)
```
## Calendar
This builds on the `grid` to create a calendar. This shows a one month view
which may be updated to show any month. The date matching the system's date
("today") may be highlighted. The calendar also has a "current day" which
may be highlighted in a different fashion. The current day may be moved at
will.
Constructor args:
* `wri` The `Writer` instance (font and screen) to use.
* `row` Location of grid on screen.
* `col`
* `colwidth` Width of grid columns.
* `fgcolor` Foreground color (grid lines).
* `bgcolor` Background color.
* `today_c` Color of text for today.
* `cur_c` Color of text for current day.
* `sun_c` Color of text for Sundays.
* `today_inv=False` Show today's date inverted (good for ePaper/monochrome).
* `cur_inv=False` Show current day inverted.
Method:
* `show` No args. (Re)draws the control. Primarily for internal use by GUI.
Bound object:
* `date` This is a `DateCal` instance, defined in `date.py`. It supports the
following properties, enabling the calendar's current day to be accessed and
changed.
* `year`
* `month` Range 1 <= `month` <= 12
* `mday` Day in month. Range depends on month and year.
* `day` Day since epoch.
Read-only property:
* `wday` Day of week. 0 = Monday.
Method:
* `now` Set date to system date.
The `DateCal` class is documented [here](https://github.com/peterhinch/micropython-samples/blob/master/date/DATE.md).
A demo of the Calendar class is `extras/demos/calendar.py`. Example navigation
fragments:
```python
cal = Calendar(wri, 10, 10, 35, GREEN, BLACK, RED, CYAN, BLUE, True)
cal.date.month += 1 # One month forward
cal.date.day += 7 # One week forward
cal.update() # Update framebuffer
refresh(ssd) # Redraw screen
```
## Clock
This displays a conentional clock with an optional seconds hand. See
`extras/demos/clock.py`.
Constructor args:
* `writer` The `Writer` instance (font and screen) to use.
* `row` Location of clock on screen.
* `col`
* `height` Dimension in pixels.
* `fgcolor=None` Foreground, background and border colors.
* `bgcolor=None`
* `bdcolor=RED`
* `pointers=(CYAN, CYAN, RED)` Colors for hours, mins and secs hands. If
`pointers[2] = None` no second hand will be drawn.
* `label=None` If an integer is passed a label of that width will be ceated
which will show the current time in digital format.
Methods:
* `value=t` Arg `t: int` is a time value e.g. `time.localtime()`. Causes clock
to be updated and redrawn to the framebuffer.
* `show` No args. (Re)draws the clock. Primarily for internal use by GUI.
## EClock
This is an unconventional clock display discussed [here](https://github.com/peterhinch/micropython-epaper/tree/master/epd_clock)
and [here](https://forum.micropython.org/viewtopic.php?f=5&t=7590&p=48092&hilit=clock#p48092).
In summary, it is designed to eliminate the effects of ghosting on ePaper
displays. Updating is additive, with white pixels being converted to black,
with a full refresh occurring once per hour. It also has the property that time
is displayed in the way that we think of it, "ten to seven" rather than 6:50.
It can be displayed in full color on suitable displays, which misses the point
of the design other than to be different...
See `extras/demos/eclock.py`.
Constructor args:
* `writer` The `Writer` instance (font and screen) to use.
* `row` Location of clock on screen.
* `col`
* `height` Dimension in pixels.
* `fgcolor=None` Foreground, background and border colors.
* `bgcolor=None`
* `bdcolor=RED`
* `int_colors=None` An optional 5-tuple may be passed to define internal
colors. In its absence all members will be `WHITE` (for ePaper use). Tuple
members must be color constants and are as follows:
0. Hour ticks: the ticks around the outer circle.
1. Arc: the color of the main arc.
2. Mins ticks: Ticks on the main arc.
3. Mins arc: color of the elapsed minutes arc.
4. Pointer: color of the hours chevron.
Methods:
* `value=t` Arg `t: int` is a time value e.g. `time.localtime()`. Causes clock
to be updated and redrawn to the framebuffer.
* `show` No args. (Re)draws the clock. Primarily for internal use by GUI.

162
extras/date.py 100644
Wyświetl plik

@ -0,0 +1,162 @@
# date.py Minimal Date class for micropython
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from time import mktime, localtime
_SECS_PER_DAY = const(86400)
def leap(year):
return bool((not year % 4) ^ (not year % 100))
class Date:
def __init__(self, lt=None):
self.callback = lambda : None # No callback until set
self.now(lt)
def now(self, lt=None):
self._lt = list(localtime()) if lt is None else list(lt)
self._update()
def _update(self, ltmod=True): # If ltmod is False ._cur has been changed
if ltmod: # Otherwise ._lt has been modified
self._lt[3] = 6
self._cur = mktime(self._lt) // _SECS_PER_DAY
self._lt = list(localtime(self._cur * _SECS_PER_DAY))
self.callback()
def _mlen(self, d=bytearray((31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31))):
days = d[self._lt[1] - 1]
return days if days else (29 if leap(self._lt[0]) else 28)
@property
def year(self):
return self._lt[0]
@year.setter
def year(self, v):
if self.mday == 29 and self.month == 2 and not leap(v):
self.mday = 28 # Ensure it doesn't skip a month
self._lt[0] = v
self._update()
@property
def month(self):
return self._lt[1]
# Can write d.month = 4 or d.month += 15
@month.setter
def month(self, v):
y, m = divmod(v - 1, 12)
self._lt[0] += y
self._lt[1] = m + 1
self._lt[2] = min(self._lt[2], self._mlen())
self._update()
@property
def mday(self):
return self._lt[2]
@mday.setter
def mday(self, v):
if not 0 < v <= self._mlen():
raise ValueError(f"mday {v} is out of range")
self._lt[2] = v
self._update()
@property
def day(self): # Days since epoch.
return self._cur
@day.setter
def day(self, v): # Usage: d.day += 7 or date_1.day = d.day.
self._cur = v
self._update(False) # Flag _cur change
# Read-only properties
@property
def wday(self):
return self._lt[6]
# Date comparisons
def __lt__(self, other):
return self.day < other.day
def __le__(self, other):
return self.day <= other.day
def __eq__(self, other):
return self.day == other.day
def __ne__(self, other):
return self.day != other.day
def __gt__(self, other):
return self.day > other.day
def __ge__(self, other):
return self.day >= other.day
def __str__(self):
return f"{self.year}/{self.month}/{self.mday}"
class DateCal(Date):
days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
months = (
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
def __init__(self, lt=None):
super().__init__(lt)
@property
def month_length(self):
return self._mlen()
@property
def day_str(self):
return self.days[self.wday]
@property
def month_str(self):
return self.months[self.month - 1]
def wday_n(self, mday=1):
return (self._lt[6] - self._lt[2] + mday) % 7
def mday_list(self, wday):
ml = self._mlen() # 1 + ((wday - wday1) % 7)
d0 = 1 + ((wday - (self._lt[6] - self._lt[2] + 1)) % 7)
return [d for d in range(d0, ml + 1, 7)]
# Optional: return UK DST offset in hours. Can pass hr to ensure that time change occurs
# at 1am UTC otherwise it occurs on date change (0:0 UTC)
# offs is offset by month
def time_offset(self, hr=6, offs=bytearray((0, 0, 3, 1, 1, 1, 1, 1, 1, 10, 0, 0))):
ml = self._mlen()
wdayld = self.wday_n(ml) # Weekday of last day of month
mday_sun = self.mday_list(6)[-1] # Month day of last Sunday
m = offs[self._lt[1] - 1]
if m < 3:
return m # Deduce time offset from month alone
return int(
((self._lt[2] < mday_sun) or (self._lt[2] == mday_sun and hr <= 1)) ^ (m == 3)
) # Months where offset changes
def __str__(self):
return f"{self.day_str} {self.mday} {self.month_str} {self.year}"

Wyświetl plik

@ -0,0 +1,48 @@
from color_setup import ssd
from time import sleep
from gui.core.writer import CWriter
from gui.core.nanogui import refresh
import gui.fonts.font10 as font
from gui.core.colors import *
from extras.widgets.calendar import Calendar
from gui.widgets.label import Label
epaper = hasattr(ssd, "wait_until_ready")
def test():
wri = CWriter(ssd, font, verbose=False)
wri.set_clip(True, True, False) # Clip to screen, no wrap
refresh(ssd, True) # Clear screen and initialise GUI
lbl = Label(wri, 200, 5, 300, bdcolor=RED)
# Invert today. On ePper also invert current date.
cal = Calendar(wri, 10, 10, 35, GREEN, BLACK, RED, CYAN, BLUE, True, epaper)
lbl.value("Show today's date.")
refresh(ssd) # With ePaper should issue wait_until_ready()
sleep(5) # but we're waiting 5 seconds anyway, which is long enough
date = cal.date
lbl.value("Adding one month")
date.month += 1
refresh(ssd)
sleep(5)
lbl.value("Adding one day")
date.day += 1
refresh(ssd)
sleep(5)
date.now() # Today
for n in range(13):
lbl.value(f"Go to {n + 1} weeks of 13 after today")
date.day += 7
refresh(ssd)
sleep(5)
lbl.value("Back to today")
date.now() # Back to today
refresh(ssd)
sleep(5)
try:
test()
finally:
if epaper:
ssd.sleep()

Wyświetl plik

@ -0,0 +1,66 @@
# eclock_test.py Unusual clock display for nanogui
# see micropython-epaper/epd-clock
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
"""
# color_setup.py:
import gc
from drivers.epaper.pico_epaper_42 import EPD as SSD
gc.collect() # Precaution before instantiating framebuf
ssd = SSD() #asyn=True) # Create a display instance
"""
from color_setup import ssd
import time
from machine import lightsleep, RTC
from gui.core.writer import CWriter
from gui.core.nanogui import refresh
import gui.fonts.font10 as font
from gui.core.colors import *
from extras.widgets.clock import Clock
epaper = hasattr(ssd, "wait_until_ready")
if epaper and not hasattr(ssd, "set_partial"):
raise OSError("ePaper display does not support partial update.")
def test():
#rtc = RTC()
#rtc.datetime((2023, 3, 18, 5, 10, 0, 0, 0))
wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
wri.set_clip(True, True, False) # Clip to screen, no wrap
if epaper:
ssd.set_full()
refresh(ssd, True)
if epaper:
ssd.wait_until_ready()
ec = Clock(wri, 10, 10, 200, label=120, pointers=(CYAN, CYAN, None))
ec.value(t := time.localtime()) # Initial drawing
refresh(ssd)
if epaper:
ssd.wait_until_ready()
ssd.set_partial()
mins = t[4]
while True:
t = time.localtime()
if t[4] != mins: # Minute has changed
mins = t[4]
if epaper:
if mins == 0: # Full refresh on the hour
ssd.set_full()
else:
ssd.set_partial()
ec.value(t)
refresh(ssd)
if epaper:
ssd.wait_until_ready()
#lightsleep(10_000)
time.sleep(10)
try:
test()
finally:
if epaper:
ssd.sleep()

Wyświetl plik

@ -0,0 +1,61 @@
# eclock_async.py Unusual clock display for nanogui
# see micropython-epaper/epd-clock
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
"""
# color_setup.py:
import gc
from drivers.epaper.pico_epaper_42 import EPD as SSD
gc.collect() # Precaution before instantiating framebuf
ssd = SSD() #asyn=True) # Create a display instance. See link for meaning of asyn
# https://github.com/peterhinch/micropython-nano-gui/blob/master/DRIVERS.md#6-epd-asynchronous-support
"""
from color_setup import ssd
import uasyncio as asyncio
import time
from gui.core.writer import Writer
from gui.core.nanogui import refresh
import gui.fonts.font10 as font
from gui.core.colors import *
from extras.widgets.eclock import EClock
epaper = hasattr(ssd, "wait_until_ready")
if epaper and not hasattr(ssd, "set_partial"):
raise OSError("ePaper display does not support partial update.")
async def test():
wri = Writer(ssd, font, verbose=False)
wri.set_clip(True, True, False) # Clip to screen, no wrap
refresh(ssd, True)
if epaper:
await ssd.wait()
ec = EClock(wri, 10, 10, 200, fgcolor=WHITE, bgcolor=BLACK)
ec.value(t := time.localtime()) # Initial drawing
refresh(ssd)
if epaper:
await ssd.wait()
mins = t[4]
while True:
t = time.localtime()
if t[4] != mins: # Minute has changed
mins = t[4]
if epaper:
if mins == 30:
ssd.set_full()
else:
ssd.set_partial()
ec.value(t)
refresh(ssd)
if epaper:
await ssd.wait()
await asyncio.sleep(10)
try:
asyncio.run(test())
finally:
if epaper:
ssd.sleep()

Wyświetl plik

@ -0,0 +1,63 @@
# eclock_test.py Unusual clock display for nanogui
# see micropython-epaper/epd-clock
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
"""
# color_setup.py:
import gc
from drivers.epaper.pico_epaper_42 import EPD as SSD
gc.collect() # Precaution before instantiating framebuf
ssd = SSD() #asyn=True) # Create a display instance
"""
from color_setup import ssd
import time
from machine import lightsleep, RTC
from gui.core.writer import CWriter
from gui.core.nanogui import refresh
import gui.fonts.font10 as font
from gui.core.colors import *
from extras.widgets.eclock import EClock
epaper = hasattr(ssd, "wait_until_ready")
if epaper and not hasattr(ssd, "set_partial"):
raise OSError("ePaper display does not support partial update.")
def test():
rtc = RTC()
#rtc.datetime((2023, 3, 18, 5, 10, 0, 0, 0))
wri = CWriter(ssd, font, verbose=False)
wri.set_clip(True, True, False) # Clip to screen, no wrap
refresh(ssd, True)
if epaper:
ssd.wait_until_ready()
ec = EClock(wri, 10, 10, 200, fgcolor=WHITE, bgcolor=BLACK)
ec.value(t := time.localtime()) # Initial drawing
refresh(ssd)
if epaper:
ssd.wait_until_ready()
mins = t[4]
while True:
t = time.localtime()
if t[4] != mins: # Minute has changed
mins = t[4]
if epaper:
if mins == 30:
ssd.set_full()
else:
ssd.set_partial()
ec.value(t)
refresh(ssd)
if epaper:
ssd.wait_until_ready()
#lightsleep(10_000)
time.sleep(10)
try:
test()
finally:
if epaper:
ssd.sleep()

Wyświetl plik

@ -0,0 +1,85 @@
# calendar.py Calendar object
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from extras.widgets.grid import Grid
from gui.widgets.label import Label, ALIGN_CENTER
from extras.date import DateCal
class Calendar:
def __init__(
self, wri, row, col, colwidth, fgcolor, bgcolor, today_c, cur_c, sun_c, today_inv=False, cur_inv=False
):
self.fgcolor = fgcolor
self.bgcolor = bgcolor
self.today_c = today_c # Color of "today" cell
self.today_inv = today_inv
self.cur_c = cur_c # Calendar currency
self.cur_inv = cur_inv
self.sun_c = sun_c # Sundays
self.date = DateCal()
self.date.callback = self.show
rows = 6
cols = 7
lw = (colwidth + 4) * cols # Label width = width of grid
kwargs = {"align": ALIGN_CENTER, "fgcolor": fgcolor, "bgcolor": bgcolor}
self.lbl = Label(wri, row, col, lw, **kwargs)
row += self.lbl.height + 3 # Two border widths
self.grid = Grid(wri, row, col, colwidth, rows, cols, **kwargs)
self.grid.show() # Draw grid lines
for n, day in enumerate(DateCal.days): # Populate day names
self.grid[[0, n]] = day[:3]
self.show()
def show(self):
def cell(): # Populate dict for a cell
d["fgcolor"] = self.fgcolor
d["bgcolor"] = self.bgcolor
if cur.year == today.year and cur.month == today.month and mday == today.mday: # Today
if self.today_inv:
d["fgcolor"] = self.bgcolor
d["bgcolor"] = self.today_c
else:
d["fgcolor"] = self.today_c
elif mday == cur.mday: # Currency
if self.cur_inv:
d["fgcolor"] = self.bgcolor
d["bgcolor"] = self.cur_c
else:
d["fgcolor"] = self.cur_c
elif mday in sundays:
d["fgcolor"] = self.sun_c
else:
d["fgcolor"] = self.fgcolor
d["text"] = str(mday)
self.grid[idx] = d
today = DateCal()
cur = self.date # Currency
self.lbl.value(f"{DateCal.months[cur.month - 1]} {cur.year}")
d = {} # Args for Label.value
wday = 0
wday_1 = cur.wday_n(1) # Weekday of 1st of month
mday = 1
seek = True
sundays = cur.mday_list(6)
for idx in range(7, self.grid.ncells):
if seek: # Find column for 1st of month
if wday < wday_1:
self.grid[idx] = ""
wday += 1
else:
seek = False
if not seek:
if mday <= cur.month_length:
cell()
mday += 1
else:
self.grid[idx] = ""
idx = 7 # Where another row would be needed, roll over to top few cells.
while mday <= cur.month_length:
cell()
idx += 1
mday += 1

Wyświetl plik

@ -0,0 +1,59 @@
# clock.py Analog clock widget for nanogui
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from gui.core.nanogui import DObject
from gui.widgets.dial import Dial, Pointer
from gui.core.colors import *
from cmath import rect, pi
class Clock(DObject):
def __init__(
self,
writer,
row,
col,
height,
fgcolor=None,
bgcolor=None,
bdcolor=RED,
pointers=(CYAN, CYAN, RED),
label=None,
):
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor)
dial = Dial(writer, row, col, height=height, ticks=12, bdcolor=None, label=label)
self._dial = dial
self._pcolors = pointers
self._hrs = Pointer(dial)
self._mins = Pointer(dial)
if pointers[2] is not None:
self._secs = Pointer(dial)
def value(self, t):
super().value(t)
self.show()
def show(self):
super().show()
t = super().value()
# Return a unit vector of phase phi. Multiplying by this will
# rotate a vector anticlockwise which is mathematically correct.
# Alas clocks, modelled on sundials, were invented in the northern
# hemisphere. Otherwise they would have rotated widdershins like
# the maths. Hence negative sign when called.
def uv(phi):
return rect(1, phi)
hc, mc, sc = self._pcolors # Colors for pointers
hstart = 0 + 0.7j # Pointer lengths. Will rotate relative to top.
mstart = 0 + 1j
sstart = 0 + 1j
self._hrs.value(hstart * uv(-t[3] * pi / 6 - t[4] * pi / 360), hc)
self._mins.value(mstart * uv(-t[4] * pi / 30), mc)
if sc is not None:
self._secs.value(sstart * uv(-t[5] * pi / 30), sc)
if self._dial.label is not None:
v = f"{t[3]:02d}.{t[4]:02d}" if sc is None else f"{t[3]:02d}.{t[4]:02d}.{t[5]:02d}"
self._dial.label.value(v)

Wyświetl plik

@ -0,0 +1,223 @@
# eclock.py Unusual clock display for nanogui
# see micropython-epaper/epd-clock
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from cmath import rect, phase
from math import sin, cos, pi
from array import array
from gui.core.nanogui import DObject, Writer
from gui.core.colors import *
# **** BEGIN DISPLAY CONSTANTS ****
THETA = pi/3 # Intersection of arc with unit circle
PHI = pi/12 # Arc is +-30 minute segment
# **** BEGIN DERIVED CONSTANTS ****
RADIUS = sin(THETA) / sin(PHI)
XLT = cos(THETA) - RADIUS * cos(PHI) # Convert arc relative to [0,0] relative
RV = pi / 360 # Interpolate arc to 1 minute
TV = RV / 5 # Small increment << I minute
# OR = cos(THETA) - RADIUS * cos(PHI) + 0j # Origin of arc
# **** BEGIN VECTOR CODE ****
# A vector is a line on the complex plane defined by a tuple of two complex
# numbers. Vectors presented for display lie in the unit circle.
def conj(n): # Complex conjugate
return n.real - n.imag * 1j
# Generate vectors comprising sectors of an arc. hrs defines location of arc,
# angle its length.
# 1 <= hrs <= 12 0 <= angle < 60 in normal use
# To print full arc angle == 60
def arc(hrs, angle=60, mul=1.0):
vs = rect(RADIUS * mul, PHI) # Coords relative to arc origin
ve = rect(RADIUS * mul, PHI)
pe = PHI - angle * RV + TV
rv = rect(1, -RV) # Rotation vector for 1 minute (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
while phase(vs) > pe:
ve *= rv
# Translate to 0, 0
yield ((vs + XLT) * rot, (ve + XLT) * rot)
vs *= rv
def progress(hrs, angle, mul0, mul1):
vs = rect(RADIUS * mul0, PHI) # Coords relative to arc origin
pe = PHI - angle * RV + TV
rv = rect(1, -RV) # CW Rotation vector for 1 minute (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
while phase(vs) > pe: # CW
# Translate to 0, 0
yield (vs + XLT) * rot
vs *= rv
yield (vs + XLT) * rot
pe = PHI
vs = rect(RADIUS * mul1, PHI - angle * RV)
rv = conj(rv) # Reverse direction of rotation
while phase(vs) < pe: # CCW
yield (vs + XLT) * rot
vs *= rv
yield (vs + XLT) * rot
# Hour ticks for main circle
def hticks(length):
segs = 12
phi = 2 * pi / segs
rv = rect(1, phi)
vs = 1 + 0j
ve = vs * (1 - length)
for _ in range(segs):
ve *= rv
vs *= rv
yield vs, ve
# Generate vectors for the minutes ticks
def ticks(hrs, length):
vs = rect(RADIUS, PHI) # Coords relative to arc origin
ve = rect(RADIUS - length, PHI) # Short tick
ve1 = rect(RADIUS - 1.5 * length, PHI) # Long tick
ve2 = rect(RADIUS - 2.0 * length, PHI) # Extra long tick
rv = rect(1, -5 * RV) # Rotation vector for 5 minutes (about OR)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
for n in range(13):
# Translate to 0, 0
if n == 6: # Overdrawn by hour pointer: visually cleaner if we skip
yield
elif n % 3 == 0:
yield ((vs + XLT) * rot, (ve2 + XLT) * rot) # Extra Long
elif n % 2 == 0:
yield ((vs + XLT) * rot, (ve1 + XLT) * rot) # Long
else:
yield ((vs + XLT) * rot, (ve + XLT) * rot) # Short
vs *= rv
ve *= rv
ve1 *= rv
ve2 *= rv
# Generate vector for the hour line
def hour(hrs):
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
return -rot, rot
# Points for arrow head
def head(hrs):
ve = 1 + 0j
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
yield ve * rot
vs = 0.9 + 0.1j
yield vs * rot
vs = conj(vs)
yield vs * rot
def tail(hrs):
rot = rect(1, (3 - hrs) * pi / 6) # Rotation
xlt = (-1.05 + 0j) * rot # Translation
phi = pi / 3
r = 0.13
vs = rect(r, phi)
vr = rect(1.0, -phi/6) # 6 segments per arc
while phase(vs) > -phi:
yield vs * rot + xlt
vs *= vr
yield vs * rot + xlt
# Generate vector for inner legends
def inner(hrs):
phi = pi * 0.35 #.33
length = 0.75
vec = rect(length, phi)
rot = rect(1, (3 - hrs) * pi / 6) # hrs rotation (about [0,0])
yield vec * rot
yield conj(vec) * rot
# **** BEGIN POPULATE DISPLAY ****
# colors: hour ticks, arc, mins ticks, mins arc, pointer
class EClock(DObject):
def __init__(self, writer, row, col, height, fgcolor=None, bgcolor=None, bdcolor=RED, int_colors=None):
super().__init__(writer, row, col, height, height, fgcolor, bgcolor, bdcolor)
self.colors = (WHITE, WHITE, WHITE, WHITE, WHITE) if int_colors is None else int_colors
self.radius = height / 2
self.xlat = self.col + 1j * self.row # Translation vector
# Convert from maths coords to graphics (invert y)
# Shift so real and imag are positive (0 <= re <= 2, 0 <= im <= 2)
# Multiply by widget size scalar
# Shift by adding widget position vector
def scale(self, point):
return (conj(point) + 1 + 1j) * self.radius + self.xlat
# Draw a vector scaling it for display and converting to integer x, y
def draw_vec(self, vec, color):
vs = self.scale(vec[0])
ve = self.scale(vec[1])
self.device.line(round(vs.real), round(vs.imag), round(ve.real), round(ve.imag), color)
def draw_poly(self, points, color):
xp = array("h")
for p in points:
p = self.scale(p)
xp.append(round(p.real))
xp.append(round(p.imag))
self.device.poly(0, 0, xp, color, True)
def map_point(self, point):
point = self.scale(point)
return round(point.imag), round(point.real)
def value(self, t):
super().value(t)
self.show()
def show(self): # Called by an async task whenever minutes changes
super().show()
wri = self.writer
dev = self.device
c = self.colors
t = super().value()
mins = t[4]
angle = mins + 30 if mins < 30 else mins - 30
# mins angle
# 5 35
# 29 59
# 31 1
# 59 29
if mins < 30:
hrs = (t[3] % 12)
else:
hrs = (t[3] + 1) % 12
# 0 <= hrs <= 11 changes on half-hour
# Draw graphics.
rad = round(self.height / 2)
row = self.row + rad
col = self.col + rad
dev.ellipse(col, row, rad, rad, self.fgcolor)
for vec in hticks(0.05):
self.draw_vec(vec, c[0])
for vec in arc(hrs): # -30 to +30 arc
self.draw_vec(vec, c[1]) # Arc
for vec in ticks(hrs, 0.05): # Ticks
if vec is not None: # Not overdrawn by hour pointer
self.draw_vec(vec, c[2])
self.draw_poly(progress(hrs, angle, 1.0, 0.99), c[3]) # Elapsed minutes
self.draw_vec(hour(hrs), c[4]) # Chevron shaft
self.draw_poly(head(hrs), c[4]) # Chevron head
self.draw_poly(tail(hrs), c[4]) # Chevron tail
# Inner text
co = round(self.writer.stringlen("+30") / 2) # Row and col offsets to
ro = round(self.writer.height / 2) # position text relative to string centre.
txt = "-30"
for point in inner(hrs):
row, col = self.map_point(point) # Convert to display coords
Writer.set_textpos(dev, row - ro, col - co)
wri.setcolor(self.fgcolor, self.bgcolor)
wri.printstring(txt, False)
txt = "+30"
wri.setcolor() # Restore defaults

Wyświetl plik

@ -0,0 +1,70 @@
# grid.py nano-gui widget providing the Grid class: a 2d array of Label instances.
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2023 Peter Hinch
from gui.core.nanogui import DObject, Writer
from gui.core.colors import *
from gui.widgets.label import Label
# lwidth may be integer Label width in pixels or a tuple/list of widths
class Grid(DObject):
def __init__(self, writer, row, col, lwidth, nrows, ncols, invert=False, fgcolor=None, bgcolor=BLACK, bdcolor=None, align=0):
self.nrows = nrows
self.ncols = ncols
self.ncells = nrows * ncols
self.cheight = writer.height + 4 # Cell height including borders
# Build column width list. Column width is Label width + 4.
if isinstance(lwidth, int):
self.cwidth = [lwidth + 4] * ncols
else: # Ensure len(.cwidth) == ncols
self.cwidth = [w + 4 for w in lwidth][:ncols]
self.cwidth.extend([lwidth[-1] + 4] * (ncols - len(lwidth)))
width = sum(self.cwidth) - 4 # Dimensions of widget interior
height = nrows * self.cheight - 4
super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor)
self.cells = []
r = row
c = col
for _ in range(self.nrows):
for cw in self.cwidth:
self.cells.append(Label(writer, r, c, cw - 4, invert, fgcolor, bgcolor, False, align)) # No border
c += cw
r += self.cheight
c = col
def _idx(self, n):
if isinstance(n, tuple) or isinstance(n, list):
if n[0] >= self.nrows:
raise ValueError("Grid row index too large")
if n[1] >= self.ncols:
raise ValueError("Grid col index too large")
idx = n[1] + n[0] * self.ncols
else:
idx = n
if idx >= self.ncells:
raise ValueError("Grid cell index too large")
return idx
def __getitem__(self, n): # Return the Label instance
return self.cells[self._idx(n)]
# allow grid[[r, c]] = "foo" or kwargs for Label:
# grid[[r, c]] = {"text": str(n), "fgcolor" : RED}
def __setitem__(self, n, x):
v = self.cells[self._idx(n)].value
_ = v(**x) if isinstance(x, dict) else v(x)
def show(self):
super().show() # Draw border
if self.has_border: # Draw grid
dev = self.device
color = self.bdcolor
x = self.col - 2 # Border top left corner
y = self.row - 2
dy = self.cheight
for row in range(1, self.nrows):
dev.hline(x, y + row * dy, self.width + 4, color)
for cw in self.cwidth[:-1]:
x += cw
dev.vline(x, y, self.height + 4, color)

Wyświetl plik

@ -13,7 +13,10 @@ from gui.core.colors import * # Populate color LUT before use.
from gui.core.writer import Writer
import framebuf
import gc
import sys
if sys.implementation.version < (1, 20, 0):
raise OSError("Firmware V1.20 or later required.")
def circle(dev, x0, y0, r, color, _=1): # Draw circle
x, y, r = int(x0), int(y0), int(r)