kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Porównaj commity
4 Commity
da7188aef6
...
35f8b23a52
Autor | SHA1 | Data |
---|---|---|
peterhinch | 35f8b23a52 | |
peterhinch | 2868bcaf2e | |
peterhinch | 70480d1e4e | |
peterhinch | 838cf40522 |
245
DRIVERS.md
245
DRIVERS.md
|
@ -55,15 +55,21 @@ access via the `Writer` and `CWriter` classes is documented
|
|||
5. [ePaper displays](./DRIVERS.md#5-epaper-displays)
|
||||
5.1 [Adafruit monochrome eInk Displays](./DRIVERS.md#51-adafruit-monochrome-eink-displays)
|
||||
5.1.1 [EPD constructor args](./DRIVERS.md#511-epd-constructor-args)
|
||||
5.1.2 [EPD public methods](./DRIVERS.md#512-epd-public-methods)
|
||||
5.1.3 [EPD public bound variables](./DRIVERS.md#513-epd-public-bound-variables)
|
||||
5.1.4 [FeatherWing Wiring](./DRIVERS.md#514-featherwing-wiring)
|
||||
5.1.5 [Micropower use](./DRIVERS.md#515-micropower-use)
|
||||
5.1.2 [Public methods](./DRIVERS.md#512-public-methods)
|
||||
5.1.3 [Events](./DRIVERS.md#513-events)
|
||||
5.1.4 [Public bound variables](./DRIVERS.md#514-public-bound-variables)
|
||||
5.1.5 [FeatherWing Wiring](./DRIVERS.md#515-featherwing-wiring)
|
||||
5.1.6 [Micropower use](./DRIVERS.md#516-micropower-use)
|
||||
5.2 [Waveshare eInk Display HAT](./DRIVERS.md#52-waveshare-eink-display-hat) Pi HAT repurposed for MP hosts.
|
||||
5.2.1 [EPD constructor args](./DRIVERS.md#521-epd-constructor-args)
|
||||
5.2.2 [EPD public methods](./DRIVERS.md#522-epd-public-methods)
|
||||
5.2.3 [EPD public bound variables](./DRIVERS.md#523-epd-public-bound-variables)
|
||||
5.2.2 [Public methods](./DRIVERS.md#522-public-methods)
|
||||
5.2.3 [Events](./DRIVERS.md#523-events)
|
||||
5.2.4 [public bound variables](./DRIVERS.md#524-public-bound-variables)
|
||||
5.3 [Waveshare 400x300 Pi Pico display](./DRIVERS.md#53-waveshare-400x300-pi-pico-display) Excellent display can also be used with other hosts.
|
||||
5.3.1 [Constructor args](./DRIVERS.md#531-constructor-args)
|
||||
5.3.2 [Public methods](./DRIVERS.md#532-public-methods)
|
||||
5.3.3 [Events](./DRIVERS.md#533-events)
|
||||
5.3.4 [Public bound variables](./DRIVERS.md#534-public-bound-variables)
|
||||
6. [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support)
|
||||
7. [Writing device drivers](./DRIVERS.md#7-writing-device-drivers)
|
||||
8. [Links](./DRIVERS.md#8-links)
|
||||
|
@ -354,7 +360,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 +392,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 +413,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 +667,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 +966,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
|
||||
|
||||
|
@ -1022,9 +1042,10 @@ see below.
|
|||
* `asyn=False` Setting this `True` invokes an asynchronous mode. See
|
||||
[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
|
||||
|
||||
### 5.1.2 EPD public methods
|
||||
### 5.1.2 Public methods
|
||||
|
||||
All methods are synchronous.
|
||||
|
||||
##### Synchronous methods
|
||||
* `init` No args. Issues a hardware reset and initialises the hardware. This
|
||||
is called by the constructor. It needs to explicitly be called to exit from a
|
||||
deep sleep.
|
||||
|
@ -1035,12 +1056,18 @@ see below.
|
|||
a period: `ready` status should be checked before issuing `refresh`.
|
||||
* `wait_until_ready` No args. Pause until the device is ready.
|
||||
|
||||
##### Asynchronous methods
|
||||
* `updated` Asynchronous. No args. Pause until the framebuffer has been copied
|
||||
to the display.
|
||||
* `wait` Asynchronous. No args. Pause until the display refresh is complete.
|
||||
### 5.1.3 Events
|
||||
|
||||
### 5.1.3 EPD public bound variables
|
||||
These provide synchronisation in asynchronous applications where `asyn=True`.
|
||||
They are only needed in more advanced asynchronous applications and their use
|
||||
is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
|
||||
* `updated` Set when framebuf has been copied to device. It is now safe to
|
||||
modify widgets without risk of display corruption.
|
||||
* `complete` Set when display update is complete. It is now safe to call
|
||||
`ssd.refresh()`.
|
||||
EPD.
|
||||
|
||||
### 5.1.4 Public bound variables
|
||||
|
||||
* `height` Integer. Height in pixels. Treat as read-only.
|
||||
* `width` Integer. Width in pixels. Treat as read-only.
|
||||
|
@ -1049,7 +1076,11 @@ see below.
|
|||
seconds to enable viewing. This enables generic nanogui demos to be run on an
|
||||
EPD.
|
||||
|
||||
### 5.1.4 FeatherWing wiring
|
||||
Note that in synchronous applications with `demo_mode=False`, `refresh` returns
|
||||
while the display is updating. Applications should issue `wait_until_ready`
|
||||
before issuing another refresh.
|
||||
|
||||
### 5.1.5 FeatherWing wiring
|
||||
|
||||
The [pinout is listed here](https://learn.adafruit.com/adafruit-eink-display-breakouts/pinouts-2).
|
||||
The `busy` line is brought out to a labelled pad on the PCB. It can be linked
|
||||
|
@ -1082,7 +1113,7 @@ The FeatherWing has a reset button which shorts the RST line to Gnd. To avoid
|
|||
risk of damage to the microcontroller pin if the button is pressed, the pin
|
||||
should be configured as open drain.
|
||||
|
||||
### 5.1.5 Micropower use
|
||||
### 5.1.6 Micropower use
|
||||
|
||||
Developers of micropower applications will need to familiarise themselves with
|
||||
the power saving features of their board. Information may be found in
|
||||
|
@ -1196,12 +1227,14 @@ Pins 26-40 unused and omitted.
|
|||
* `rst` An initialised output pin. Initial value should be 1.
|
||||
* `busy` An initialised input pin.
|
||||
* `landscape=False` By default the long axis is vertical.
|
||||
* `asyn=False`
|
||||
* `asyn=False` Setting this `True` invokes an asynchronous mode. See
|
||||
[EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
|
||||
|
||||
### 5.2.2 EPD public methods
|
||||
|
||||
##### Synchronous methods
|
||||
* `init` No args. Issues a hardware reset and initialises the hardware. This
|
||||
All methods are synchronous.
|
||||
|
||||
* `init` No args. Issues a hardware reset and initialises the hardware. This
|
||||
is called by the constructor. It needs to explicitly be called to exit from a
|
||||
deep sleep.
|
||||
* `sleep` No args. Puts the display into deep sleep. If called while a refresh
|
||||
|
@ -1211,12 +1244,17 @@ Pins 26-40 unused and omitted.
|
|||
a period: `ready` status should be checked before issuing `refresh`.
|
||||
* `wait_until_ready` No args. Pause until the device is ready.
|
||||
|
||||
##### Asynchronous methods
|
||||
* `updated` Asynchronous. No args. Pause until the framebuffer has been copied
|
||||
to the display.
|
||||
* `wait` Asynchronous. No args. Pause until the display refresh is complete.
|
||||
### 5.2.3 Events
|
||||
|
||||
### 5.2.3 EPD public bound variables
|
||||
These provide synchronisation in asynchronous applications where `asyn=True`.
|
||||
They are only needed in more advanced asynchronous applications and their use
|
||||
is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
|
||||
* `updated` Set when framebuf has been copied to device. It is now safe to
|
||||
modify widgets without risk of display corruption.
|
||||
* `complete` Set when display update is complete. It is now safe to call
|
||||
`ssd.refresh()`.
|
||||
|
||||
### 5.2.4 Public bound variables
|
||||
|
||||
* `height` Integer. Height in pixels. Treat as read-only.
|
||||
* `width` Integer. Width in pixels. Treat as read-only.
|
||||
|
@ -1225,6 +1263,10 @@ Pins 26-40 unused and omitted.
|
|||
seconds to enable viewing. This enables generic nanogui demos to be run on an
|
||||
EPD.
|
||||
|
||||
Note that in synchronous applications with `demo_mode=False`, `refresh` returns
|
||||
while the display is updating. Applications should issue `wait_until_ready`
|
||||
before issuing another refresh.
|
||||
|
||||
## 5.3 Waveshare 400x300 Pi Pico display
|
||||
|
||||
The driver for this display now supports partial updates.
|
||||
|
@ -1241,6 +1283,8 @@ gc.collect() # Precaution before instantiating framebuf.
|
|||
ssd = SSD() # Create a display instance. For normal applications.
|
||||
# ssd = SSD(asyn=True) # Alternative for asynchronous applications.
|
||||
```
|
||||
### 5.3.1 Constructor args
|
||||
|
||||
For other hosts the pins need to be specified in `color_setup.py` via the
|
||||
following constructor args:
|
||||
|
||||
|
@ -1252,7 +1296,9 @@ following constructor args:
|
|||
* `asyn=False` Set `True` for asynchronous applications. Leave `False` for
|
||||
microgui where the arg has no effect.
|
||||
|
||||
##### Synchronous methods
|
||||
### 5.3.2 Public methods
|
||||
|
||||
All methods are synchronous.
|
||||
|
||||
* `init` No args. Issues a hardware reset and initialises the hardware. This
|
||||
is called by the constructor. It needs to explicitly be called to exit from a
|
||||
|
@ -1266,63 +1312,108 @@ following constructor args:
|
|||
* `set_partial()` Enable partial updates.
|
||||
* `set_full()` Restore normal update operation.
|
||||
|
||||
After issuing `set_partial()`, subsequent updates will be partial. Normal
|
||||
After issuing `set_partial()`, subsequent updates will be partial. Normal
|
||||
updates are restored by issuing `set_full()`. These methods should not be
|
||||
issued while an update is in progress.
|
||||
|
||||
Partial updates are fast and visually unobtrusive but they are prone to
|
||||
ghosting.
|
||||
|
||||
##### Asynchronous methods
|
||||
### 5.3.3 Events
|
||||
|
||||
* `wait` No args. If an update is in progress, pause until the display refresh
|
||||
is complete, otherwise return is immediate.
|
||||
* `updated` No args. Pause until the framebuffer has been copied to the
|
||||
display. It is now safe to modify the framebuf, but display update may still
|
||||
be in progress.
|
||||
These provide synchronisation in asynchronous applications where `asyn=True`.
|
||||
They are only needed in more advanced asynchronous applications and their use
|
||||
is discussed in [EPD Asynchronous support](./DRIVERS.md#6-epd-asynchronous-support).
|
||||
* `updated` Set when framebuf has been copied to device. It is now safe to
|
||||
modify widgets without risk of display corruption.
|
||||
* `complete` Set when display update is complete. It is now safe to call
|
||||
`ssd.refresh()`.
|
||||
|
||||
###### [Contents](./DRIVERS.md#contents)
|
||||
### 5.3.4 Public bound variables
|
||||
|
||||
* `height` Integer. Height in pixels. Treat as read-only.
|
||||
* `width` Integer. Width in pixels. Treat as read-only.
|
||||
* `demo_mode=False` Boolean. If set `True` after instantiating, `refresh()`
|
||||
will block until display update is complete, and then for a further two
|
||||
seconds to enable viewing. This enables generic nanogui demos to be run on an
|
||||
EPD.
|
||||
|
||||
Note that in synchronous applications with `demo_mode=False`, `refresh` returns
|
||||
while the display is updating. Applications should issue `wait_until_ready`
|
||||
before issuing another refresh.
|
||||
|
||||
###### [Contents](./DRIVERS.md#contents)
|
||||
|
||||
# 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`.
|
||||
|
||||
When synchronous 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. If `demo_mode` is set, device drivers block for an
|
||||
additional 2 seconds to enable demos written for normal displays to work (the
|
||||
2 second pause allows 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. If screen updates
|
||||
take place at a low rate the only precaution necessary is to ensure that
|
||||
sufficient time elapses between calls to `ssd.refresh()` for the update to
|
||||
complete. For example the following code fragment illustrates an application
|
||||
which performs a full EPD refresh once per minute:
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The following illustrates the kind of approach which may be used:
|
||||
```python
|
||||
async def run():
|
||||
while True:
|
||||
# Before refresh, ensure that a previous refresh is complete
|
||||
# Not strictly necessary if .updated() used after refresh.
|
||||
await ssd.wait()
|
||||
refresh(ssd) # Immediate return. Creates a task to copy content to EPD.
|
||||
# Wait until the framebuf content has been passed to EPD.
|
||||
await ssd.updated()
|
||||
# Trigger an event which allows other tasks to update the
|
||||
# framebuffer in background
|
||||
evt.set()
|
||||
evt.clear()
|
||||
await asyncio.sleep(180)
|
||||
# get data
|
||||
# Update screen widgets
|
||||
ssd.refresh() # Launches background refresh
|
||||
await asyncio.sleep(60)
|
||||
```
|
||||
With `asyn=True` other running tasks experience latency measured in tens of ms.
|
||||
|
||||
Finer control is available using the two public bound `Event` instances. This
|
||||
fragment assumes an application with a single task performing refreshes. The
|
||||
application has two `Event` instances, one requesting refresh and the other
|
||||
requesting widget updates:
|
||||
```python
|
||||
async def refresh_task():
|
||||
while True:
|
||||
await refresh_request.wait() # Another task has requested refresh
|
||||
refresh_request.clear()
|
||||
ssd.refresh() # Launch background refresh
|
||||
await ssd.updated.wait() # Wait until framebuf copied to device
|
||||
data_request.set() # Ask other tasks to update widgets
|
||||
await ssd.complete.wait()
|
||||
# Now safe to respond to refresh_request and issue ssd.refresh()
|
||||
```
|
||||
The `updated` and `complete` events are cleared when `ssd.refresh` is called
|
||||
and are set as the background refresh proceeds.
|
||||
|
||||
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 (on the half-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
|
||||
|
|
|
@ -4,40 +4,37 @@
|
|||
# Copyright (c) 2020 Peter Hinch
|
||||
|
||||
# As written, supports:
|
||||
# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431
|
||||
# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673
|
||||
# ili9341 240x320 displays on Pi Pico
|
||||
# Edit the driver import for other displays.
|
||||
|
||||
# Demo of initialisation procedure designed to minimise risk of memory fail
|
||||
# when instantiating the frame buffer. The aim is to do this as early as
|
||||
# possible before importing other modules.
|
||||
|
||||
# WIRING (Adafruit pin nos and names).
|
||||
# Pyb SSD
|
||||
# 3v3 Vin (10)
|
||||
# Gnd Gnd (11)
|
||||
# Y1 DC (3 DC)
|
||||
# Y2 CS (5 OC OLEDCS)
|
||||
# Y3 Rst (4 R RESET)
|
||||
# Y6 CLK (2 CL SCK)
|
||||
# Y8 DATA (1 SI MOSI)
|
||||
# WIRING
|
||||
# Pico Display
|
||||
# GPIO Pin
|
||||
# 3v3 36 Vin
|
||||
# IO6 9 CLK Hardware SPI0
|
||||
# IO7 10 DATA (AKA SI MOSI)
|
||||
# IO8 11 DC
|
||||
# IO9 12 Rst
|
||||
# Gnd 13 Gnd
|
||||
# IO10 14 CS
|
||||
|
||||
import machine
|
||||
from machine import Pin, SPI
|
||||
import gc
|
||||
|
||||
# *** Choose your color display driver here ***
|
||||
# Driver supporting non-STM platforms
|
||||
# from drivers.ssd1351.ssd1351_generic import SSD1351 as SSD
|
||||
# ili9341 specific driver
|
||||
from drivers.ili93xx.ili9341 import ILI9341 as SSD
|
||||
|
||||
# STM specific driver
|
||||
from drivers.ssd1351.ssd1351 import SSD1351 as SSD
|
||||
pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins
|
||||
prst = Pin(9, Pin.OUT, value=1)
|
||||
pcs = Pin(10, Pin.OUT, value=1)
|
||||
|
||||
height = 96 # 1.27 inch 96*128 (rows*cols) display
|
||||
# height = 128 # 1.5 inch 128*128 display
|
||||
|
||||
pdc = machine.Pin('Y1', machine.Pin.OUT_PP, value=0)
|
||||
pcs = machine.Pin('Y2', machine.Pin.OUT_PP, value=1)
|
||||
prst = machine.Pin('Y3', machine.Pin.OUT_PP, value=1)
|
||||
spi = machine.SPI(2)
|
||||
# Kept as ssd to maintain compatability
|
||||
gc.collect() # Precaution before instantiating framebuf
|
||||
ssd = SSD(spi, pcs, pdc, prst, height) # Create a display instance
|
||||
# See DRIVERS.md re overclocking the SPI bus
|
||||
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=30_000_000)
|
||||
ssd = SSD(spi, dc=pdc, cs=pcs, rst=prst)
|
||||
|
|
|
@ -32,7 +32,8 @@ class EPD(framebuf.FrameBuffer):
|
|||
self._lsc = landscape
|
||||
self._asyn = asyn
|
||||
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
|
||||
self._updated = asyncio.Event()
|
||||
self.updated = asyncio.Event()
|
||||
self.complete = asyncio.Event()
|
||||
# Dimensions in pixels. Waveshare code is portrait mode.
|
||||
# Public bound variables required by nanogui.
|
||||
self.width = 264 if landscape else 176
|
||||
|
@ -130,15 +131,6 @@ class EPD(framebuf.FrameBuffer):
|
|||
dt = ticks_diff(ticks_ms(), t)
|
||||
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
|
||||
|
||||
async def wait(self):
|
||||
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
|
||||
while not self.ready():
|
||||
await asyncio.sleep_ms(100)
|
||||
|
||||
# Pause until framebuf has been copied to device.
|
||||
async def updated(self):
|
||||
await self._updated.wait()
|
||||
|
||||
# For polling in asynchronous code. Just checks pin state.
|
||||
# 0 == busy. Comment in official code is wrong. Code is correct.
|
||||
def ready(self):
|
||||
|
@ -196,13 +188,13 @@ class EPD(framebuf.FrameBuffer):
|
|||
await asyncio.sleep_ms(0)
|
||||
t = ticks_ms()
|
||||
|
||||
self._updated.set() # framebuf has now been copied to the device
|
||||
self._updated.clear()
|
||||
self.updated.set() # framebuf has now been copied to the device
|
||||
cmd(b'\x12') # DISPLAY_REFRESH
|
||||
await asyncio.sleep(1)
|
||||
while self._busy() == 0:
|
||||
await asyncio.sleep_ms(200) # Don't release lock until update is complete
|
||||
self._as_busy = False
|
||||
self.complete.set()
|
||||
|
||||
# draw the current frame memory. Blocking time ~180ms
|
||||
def show(self, buf1=bytearray(1)):
|
||||
|
@ -210,6 +202,8 @@ class EPD(framebuf.FrameBuffer):
|
|||
if self._as_busy:
|
||||
raise RuntimeError('Cannot refresh: display is busy.')
|
||||
self._as_busy = True
|
||||
self.updated.clear()
|
||||
self.complete.clear()
|
||||
asyncio.create_task(self._as_show())
|
||||
return
|
||||
t = ticks_us()
|
||||
|
|
|
@ -42,7 +42,8 @@ class EPD(framebuf.FrameBuffer):
|
|||
# ._as_busy is set immediately on start of task. Cleared
|
||||
# when busy pin is logically false (physically 1).
|
||||
self._as_busy = False
|
||||
self._updated = asyncio.Event()
|
||||
self.updated = asyncio.Event()
|
||||
self.complete = asyncio.Event()
|
||||
# Public bound variables required by nanogui.
|
||||
# Dimensions in pixels as seen by nanogui (landscape mode).
|
||||
self.width = 296 if landscape else 128
|
||||
|
@ -114,16 +115,6 @@ class EPD(framebuf.FrameBuffer):
|
|||
while not self.ready():
|
||||
sleep_ms(100)
|
||||
|
||||
# Asynchronous wait on ready state. Pause (4.9s) for physical refresh.
|
||||
async def wait(self):
|
||||
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
|
||||
while not self.ready():
|
||||
await asyncio.sleep_ms(100)
|
||||
|
||||
# Pause until framebuf has been copied to device.
|
||||
async def updated(self):
|
||||
await self._updated.wait()
|
||||
|
||||
# Return immediate status. Pin state: 0 == busy.
|
||||
def ready(self):
|
||||
return not(self._as_busy or (self._busy() == 0))
|
||||
|
@ -162,8 +153,7 @@ class EPD(framebuf.FrameBuffer):
|
|||
t = ticks_ms()
|
||||
|
||||
cmd(b'\x11') # Data stop
|
||||
self._updated.set()
|
||||
self._updated.clear()
|
||||
self.updated.set()
|
||||
sleep_us(20) # Allow for data coming back: currently ignore this
|
||||
cmd(b'\x12') # DISPLAY_REFRESH
|
||||
# busy goes low now, for ~4.9 seconds.
|
||||
|
@ -171,6 +161,7 @@ class EPD(framebuf.FrameBuffer):
|
|||
while self._busy() == 0:
|
||||
await asyncio.sleep_ms(200)
|
||||
self._as_busy = False
|
||||
self.complete.set()
|
||||
|
||||
# draw the current frame memory.
|
||||
def show(self, buf1=bytearray(1)):
|
||||
|
@ -178,6 +169,8 @@ class EPD(framebuf.FrameBuffer):
|
|||
if self._as_busy:
|
||||
raise RuntimeError('Cannot refresh: display is busy.')
|
||||
self._as_busy = True # Immediate busy flag. Pin goes low much later.
|
||||
self.updated.clear()
|
||||
self.complete.clear()
|
||||
asyncio.create_task(self._as_show())
|
||||
return
|
||||
|
||||
|
|
|
@ -120,10 +120,17 @@ class EPD(framebuf.FrameBuffer):
|
|||
self.spi.init(baudrate = 4_000_000)
|
||||
self._asyn = asyn
|
||||
self._busy = False # Set immediately on .show(). Cleared when busy pin is logically false (physically 1).
|
||||
self._updated = asyncio.Event()
|
||||
self.updated = asyncio.Event()
|
||||
self.complete = asyncio.Event()
|
||||
|
||||
# Public bound variables required by nanogui.
|
||||
# Dimensions in pixels as seen by nanogui
|
||||
self.width = _EPD_WIDTH
|
||||
self.height = _EPD_HEIGHT
|
||||
# Other public bound variable.
|
||||
# Special mode enables demos written for generic displays to run.
|
||||
self.demo_mode = False
|
||||
|
||||
self.buf = bytearray(_EPD_HEIGHT * _BWIDTH)
|
||||
self.mvb = memoryview(self.buf)
|
||||
self.ibuf = bytearray(1000) # Buffer for inverted pixels
|
||||
|
@ -241,16 +248,6 @@ class EPD(framebuf.FrameBuffer):
|
|||
while not self.ready():
|
||||
time.sleep_ms(100)
|
||||
|
||||
async def wait(self):
|
||||
while not self.ready():
|
||||
await asyncio.sleep_ms(100)
|
||||
|
||||
# Pause until framebuf has been copied to device.
|
||||
async def updated(self):
|
||||
self._updated.clear()
|
||||
await self._updated.wait()
|
||||
self._updated.clear()
|
||||
|
||||
# For polling in asynchronous code. Just checks pin state.
|
||||
# 0 == busy. Comment in official code is wrong. Code is correct.
|
||||
def ready(self):
|
||||
|
@ -280,11 +277,12 @@ class EPD(framebuf.FrameBuffer):
|
|||
nbytes = min(nbytes, nleft)
|
||||
if not ((npass := npass + 1) % 16):
|
||||
await asyncio.sleep_ms(0) # Control blocking time
|
||||
self._updated.set()
|
||||
self.updated.set()
|
||||
self.send_command(b"\x12") # Nonblocking .display_on()
|
||||
while not self.busy_pin(): # Wait on display hardware
|
||||
await asyncio.sleep_ms(0)
|
||||
self._busy = False
|
||||
self.complete.set()
|
||||
|
||||
async def do_refresh(self, split): # For micro-gui
|
||||
assert (not self._busy), "Refresh while busy"
|
||||
|
@ -295,6 +293,8 @@ class EPD(framebuf.FrameBuffer):
|
|||
raise RuntimeError('Cannot refresh: display is busy.')
|
||||
self._busy = True # Immediate busy flag. Pin goes low much later.
|
||||
if self._asyn:
|
||||
self.updated.clear()
|
||||
self.complete.clear()
|
||||
asyncio.create_task(self._as_show())
|
||||
return
|
||||
self.send_command(b"\x13")
|
||||
|
@ -308,7 +308,12 @@ class EPD(framebuf.FrameBuffer):
|
|||
nbytes = min(nbytes, nleft)
|
||||
self._busy = False
|
||||
self.display_on()
|
||||
if not self.demo_mode:
|
||||
# Immediate return to avoid blocking the whole application.
|
||||
# User should wait for ready before calling refresh()
|
||||
return
|
||||
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
|
||||
```
|
||||
![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.
|
|
@ -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.complete.wait()
|
||||
ec = EClock(wri, 10, 10, 200, fgcolor=WHITE, bgcolor=BLACK)
|
||||
ec.value(t := time.localtime()) # Initial drawing
|
||||
refresh(ssd)
|
||||
if epaper:
|
||||
await ssd.complete.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.complete.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)
|
|
@ -4,18 +4,11 @@
|
|||
# Copyright (c) 2020 Peter Hinch
|
||||
|
||||
from color_setup import SSD
|
||||
from gui.core.writer import CWriter
|
||||
|
||||
# Code can be portable between 4-bit and other drivers by calling create_color
|
||||
def create_color(idx, r, g, b):
|
||||
c = SSD.rgb(r, g, b)
|
||||
if not hasattr(SSD, 'lut'):
|
||||
return c
|
||||
if not 0 <= idx <= 15:
|
||||
raise ValueError('Color nos must be 0..15')
|
||||
x = idx << 1
|
||||
SSD.lut[x] = c & 0xff
|
||||
SSD.lut[x + 1] = c >> 8
|
||||
return idx
|
||||
return CWriter.create_color(SSD, idx, r, g, b)
|
||||
|
||||
if hasattr(SSD, 'lut'): # Colors defined by LUT
|
||||
BLACK = create_color(0, 0, 0, 0)
|
||||
|
|
|
@ -13,49 +13,20 @@ from gui.core.colors import * # Populate color LUT before use.
|
|||
from gui.core.writer import Writer
|
||||
import framebuf
|
||||
import gc
|
||||
import sys
|
||||
|
||||
def _circle(dev, x0, y0, r, color): # Single pixel circle
|
||||
x = -r
|
||||
y = 0
|
||||
err = 2 -2*r
|
||||
while x <= 0:
|
||||
dev.pixel(x0 -x, y0 +y, color)
|
||||
dev.pixel(x0 +x, y0 +y, color)
|
||||
dev.pixel(x0 +x, y0 -y, color)
|
||||
dev.pixel(x0 -x, y0 -y, color)
|
||||
e2 = err
|
||||
if (e2 <= y):
|
||||
y += 1
|
||||
err += y*2 +1
|
||||
if (-x == y and e2 <= x):
|
||||
e2 = 0
|
||||
if (e2 > x):
|
||||
x += 1
|
||||
err += x*2 +1
|
||||
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)
|
||||
dev.ellipse(x, y, r, r, color)
|
||||
|
||||
def circle(dev, x0, y0, r, color, width =1): # Draw circle
|
||||
x0, y0, r = int(x0), int(y0), int(r)
|
||||
for r in range(r, r -width, -1):
|
||||
_circle(dev, x0, y0, r, color)
|
||||
|
||||
def fillcircle(dev, x0, y0, r, color): # Draw filled circle
|
||||
x0, y0, r = int(x0), int(y0), int(r)
|
||||
x = -r
|
||||
y = 0
|
||||
err = 2 -2*r
|
||||
while x <= 0:
|
||||
dev.line(x0 -x, y0 -y, x0 -x, y0 +y, color)
|
||||
dev.line(x0 +x, y0 -y, x0 +x, y0 +y, color)
|
||||
e2 = err
|
||||
if (e2 <= y):
|
||||
y +=1
|
||||
err += y*2 +1
|
||||
if (-x == y and e2 <= x):
|
||||
e2 = 0
|
||||
if (e2 > x):
|
||||
x += 1
|
||||
err += x*2 +1
|
||||
|
||||
x, y, r = int(x0), int(y0), int(r)
|
||||
dev.ellipse(x, y, r, r, color, True)
|
||||
|
||||
# If a (framebuf based) device is passed to refresh, the screen is cleared.
|
||||
# None causes pending widgets to be drawn and the result to be copied to hardware.
|
||||
# The pend mechanism enables a displayable object to postpone its renedering
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# writer.py Implements the Writer class.
|
||||
# Handles colour, word wrap and tab stops
|
||||
|
||||
# V0.5.1 Dec 2022 Support 4-bit color display drivers.
|
||||
# V0.5.0 Sep 2021 Color now requires firmware >= 1.17.
|
||||
# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682).
|
||||
# V0.4.0 Jan 2021 Improved handling of word wrap and line clip. Upside-down
|
||||
|
@ -25,7 +26,7 @@ from uctypes import bytearray_at, addressof
|
|||
from sys import implementation
|
||||
import os
|
||||
|
||||
__version__ = (0, 5, 0)
|
||||
__version__ = (0, 5, 1)
|
||||
|
||||
fast_mode = True # Does nothing. Kept to avoid breaking code.
|
||||
|
||||
|
@ -255,6 +256,17 @@ class Writer():
|
|||
# Writer for colour displays.
|
||||
class CWriter(Writer):
|
||||
|
||||
@staticmethod
|
||||
def create_color(ssd, idx, r, g, b):
|
||||
c = ssd.rgb(r, g, b)
|
||||
if not hasattr(ssd, 'lut'):
|
||||
return c
|
||||
if not 0 <= idx <= 15:
|
||||
raise ValueError('Color nos must be 0..15')
|
||||
x = idx << 1
|
||||
ssd.lut[x] = c & 0xff
|
||||
ssd.lut[x + 1] = c >> 8
|
||||
return idx
|
||||
|
||||
def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True):
|
||||
if not hasattr(device, 'palette'):
|
||||
|
|
Ładowanie…
Reference in New Issue