kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Prior to EPD API branch
rodzic
70480d1e4e
commit
2868bcaf2e
111
DRIVERS.md
111
DRIVERS.md
|
@ -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)
|
||||
|
||||
|
|
41
README.md
41
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
||||

|
||||
|
||||
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.
|
|
@ -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}"
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue