nextion-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextion_tools-0.1.0/LICENSE +21 -0
- nextion_tools-0.1.0/PKG-INFO +258 -0
- nextion_tools-0.1.0/README.md +230 -0
- nextion_tools-0.1.0/pyproject.toml +41 -0
- nextion_tools-0.1.0/setup.cfg +4 -0
- nextion_tools-0.1.0/src/nextion_tools/__init__.py +14 -0
- nextion_tools-0.1.0/src/nextion_tools/crc.py +26 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/__init__.py +16 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/__main__.py +55 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/clock.py +23 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/devices.py +29 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/display.py +1163 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/flow.py +199 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/model.py +94 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/protocol.py +150 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/script.py +690 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/transport.py +226 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/upload.py +174 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/viewer.py +295 -0
- nextion_tools-0.1.0/src/nextion_tools/emu/waiting.py +25 -0
- nextion_tools-0.1.0/src/nextion_tools/fonts.py +150 -0
- nextion_tools-0.1.0/src/nextion_tools/hmi.py +467 -0
- nextion_tools-0.1.0/src/nextion_tools/images.py +22 -0
- nextion_tools-0.1.0/src/nextion_tools/render.py +474 -0
- nextion_tools-0.1.0/src/nextion_tools/tft.py +91 -0
- nextion_tools-0.1.0/src/nextion_tools.egg-info/PKG-INFO +258 -0
- nextion_tools-0.1.0/src/nextion_tools.egg-info/SOURCES.txt +36 -0
- nextion_tools-0.1.0/src/nextion_tools.egg-info/dependency_links.txt +1 -0
- nextion_tools-0.1.0/src/nextion_tools.egg-info/requires.txt +11 -0
- nextion_tools-0.1.0/src/nextion_tools.egg-info/top_level.txt +1 -0
- nextion_tools-0.1.0/tests/test_crc.py +10 -0
- nextion_tools-0.1.0/tests/test_demo_files.py +98 -0
- nextion_tools-0.1.0/tests/test_edit.py +69 -0
- nextion_tools-0.1.0/tests/test_fonts.py +104 -0
- nextion_tools-0.1.0/tests/test_hmi.py +145 -0
- nextion_tools-0.1.0/tests/test_images.py +18 -0
- nextion_tools-0.1.0/tests/test_render.py +172 -0
- nextion_tools-0.1.0/tests/test_tft.py +39 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Mulcahy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nextion-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read and edit Nextion Editor projects, fonts and TFTs, and emulate the display they run on
|
|
5
|
+
Author: James Mulcahy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jamesmulcahy/nextion-tools
|
|
8
|
+
Project-URL: Source, https://github.com/jamesmulcahy/nextion-tools
|
|
9
|
+
Project-URL: Issues, https://github.com/jamesmulcahy/nextion-tools/issues
|
|
10
|
+
Keywords: nextion,hmi,tft,emulator,display,esp32
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: images
|
|
20
|
+
Requires-Dist: numpy; extra == "images"
|
|
21
|
+
Requires-Dist: pillow; extra == "images"
|
|
22
|
+
Provides-Extra: serial
|
|
23
|
+
Requires-Dist: pyserial>=3.5; extra == "serial"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-timeout>=2; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# nextion-tools
|
|
30
|
+
|
|
31
|
+
Tools for [Nextion](https://nextion.tech) HMI displays: the Nextion Editor's project files, the
|
|
32
|
+
TFTs it compiles, and an emulator of the display itself for testing the firmware that drives one.
|
|
33
|
+
|
|
34
|
+
## Try it
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
git clone https://github.com/jamesmulcahy/nextion-tools
|
|
38
|
+
cd nextion-tools
|
|
39
|
+
pip install -e ".[images]"
|
|
40
|
+
python demo/run.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then open http://127.0.0.1:8080/ and use the panel with your mouse.
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
That is an emulated Nextion display running a project made in the Nextion Editor
|
|
48
|
+
(`demo/demo.HMI`), with its own touch handling, scripts and timers, and a small Python program
|
|
49
|
+
(`demo/companion.py`) as the microcontroller behind it: the menu's buttons change page, the
|
|
50
|
+
thermostat's bar turns red near the top, Paint draws where you drag, and Pong plays against a
|
|
51
|
+
paddle that follows the pointer. The same companion drives a real display flashed with
|
|
52
|
+
`demo/demo.tft`. See [The demo](#the-demo) and [demo/README.md](https://github.com/jamesmulcahy/nextion-tools/blob/main/demo/README.md).
|
|
53
|
+
|
|
54
|
+
| Module | What |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `nextion_tools.hmi` | `.HMI` projects: pages, components, ids, geometry, attributes, event code, `Program.s`; numeric edits with re-stamping |
|
|
57
|
+
| `nextion_tools.fonts` | `.zi` fonts: metrics and glyph bitmaps, and which font a component's `font` index means |
|
|
58
|
+
| `nextion_tools.images` | pictures as RGB arrays, and the panel's RGB565 |
|
|
59
|
+
| `nextion_tools.tft` | a compiled `.tft`: its picture table, checksums and orientation, and same-size picture swaps |
|
|
60
|
+
| `nextion_tools.crc` | the CRC behind TFT checksums and HMI page stamps |
|
|
61
|
+
| `nextion_tools.render` | draws a page as the display shows it, from the HMI and the values its components hold |
|
|
62
|
+
| `nextion_tools.emu` | an emulated display, driven by the project's HMI (below) |
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install nextion-tools # hmi, tft, crc and the emulator: no dependencies
|
|
66
|
+
pip install nextion-tools[images] # fonts, pictures, rendering and the viewer: numpy and Pillow
|
|
67
|
+
pip install nextion-tools[serial] # the emulator on a real serial port: pyserial
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from nextion_tools.hmi import Project
|
|
72
|
+
|
|
73
|
+
project = Project.load("gui.HMI")
|
|
74
|
+
home = project.page("home") # KeyError if there is none; find_page() gives None
|
|
75
|
+
print(home.id, [(c.id, c.name, c.kind, c.rect) for c in home.components])
|
|
76
|
+
print(home.component("tm0").events["timer"])
|
|
77
|
+
print(project.program) # Program.s, line by line
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The format notes are in the module docstrings, and were checked against files the Editor saved.
|
|
81
|
+
|
|
82
|
+
## The demo
|
|
83
|
+
|
|
84
|
+
`demo/` holds a small project made in the Nextion Editor (`demo.HMI`, and `demo.tft` compiled
|
|
85
|
+
from it) and `companion.py`, a Python program that plays the MCU. `python demo/run.py` runs the
|
|
86
|
+
whole thing (the emulated display, the companion and the browser viewer) in one process, and
|
|
87
|
+
takes `--http PORT` and `--rotation 90` (the screen as the LCD itself holds it).
|
|
88
|
+
`python demo/companion.py --serial /dev/ttyUSB0 --fps 8` drives a real display running
|
|
89
|
+
`demo.tft`, which needs the `serial` extra. [demo/README.md](https://github.com/jamesmulcahy/nextion-tools/blob/main/demo/README.md) has what each page does and what
|
|
90
|
+
the project turned up.
|
|
91
|
+
Nextion and the Nextion Editor are ITEAD's; this project is not affiliated with them.
|
|
92
|
+
|
|
93
|
+
## The emulator
|
|
94
|
+
|
|
95
|
+
An emulated Nextion HMI display, for testing the firmware that drives one. Point the MCU's UART
|
|
96
|
+
at it (QEMU's `-serial tcp:`, a pty, or a real serial port). It executes what it is sent, keeps a
|
|
97
|
+
model of what the screen would show, replies as a real display does, and lets a test act as the
|
|
98
|
+
user.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from nextion_tools.emu import Display, TcpServer
|
|
102
|
+
from nextion_tools.hmi import Project
|
|
103
|
+
|
|
104
|
+
display = Display(TcpServer(port=4000), hmi=Project.load("gui.HMI")) # qemu ... -serial tcp:127.0.0.1:4000
|
|
105
|
+
display.wait_for_page("home")
|
|
106
|
+
display.press("h_ceiling") # a finger on it, by name or id: display.press(22)
|
|
107
|
+
display.wait_for_value("home.room.txt", "Kitchen")
|
|
108
|
+
assert not display.errors # nothing the display would have rejected
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or from a shell, to watch what firmware sends:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
python -m nextion_tools.emu --tcp 4000
|
|
115
|
+
python -m nextion_tools.emu --pty # prints /dev/pts/N
|
|
116
|
+
python -m nextion_tools.emu --serial /dev/ttyUSB0 # needs pyserial: pip install nextion-tools[serial]
|
|
117
|
+
python -m nextion_tools.emu --tcp 4000 --hmi gui.HMI # with the project's pages and code
|
|
118
|
+
python -m nextion_tools.emu.script gui.HMI # can every event of this HMI be run?
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## What it models
|
|
122
|
+
|
|
123
|
+
Replies follow what an NX4832T035 (the Sonoff NSPanel's display) was measured to do, which differs
|
|
124
|
+
from Nextion's documentation in places:
|
|
125
|
+
|
|
126
|
+
| Instruction | `bkcmd=3` | `bkcmd=0` |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| assignment | `01` | nothing |
|
|
129
|
+
| `get x` | `71` + 4 bytes (or `70` + text), **no** `01` | the same |
|
|
130
|
+
| `printh …` | the bytes, **no** `01` | the same |
|
|
131
|
+
| unknown component, variable, or bad syntax | `1a` (never `02`/`1b`) | nothing |
|
|
132
|
+
| `page` / `ref` | `01` at once, then the redraw blocks the next instruction ~400 ms | — |
|
|
133
|
+
| `delay=N` | `01` when it ends | — |
|
|
134
|
+
|
|
135
|
+
`bkcmd` starts at 2, as on a display that has just powered on, and `power_cycle()` puts it back.
|
|
136
|
+
|
|
137
|
+
**Ideal or flow model.** By default every instruction executes the moment it arrives, so tests
|
|
138
|
+
see exactly what the firmware meant to draw. With `flow_model=True` the display behaves as the
|
|
139
|
+
real one does:
|
|
140
|
+
|
|
141
|
+
- input arrives at line rate (QEMU's UART does not pace writes);
|
|
142
|
+
- it waits in a buffer of 1088 bytes;
|
|
143
|
+
- the display stalls on page changes, `ref` and `delay`;
|
|
144
|
+
- a burst that overruns the buffer during a stall loses commands (`display.lost`), and the
|
|
145
|
+
display sends `0x24` when it resumes.
|
|
146
|
+
|
|
147
|
+
`tests/emu/test_calibration.py` replays the hardware measurements against the model, so it cannot
|
|
148
|
+
drift from them unnoticed.
|
|
149
|
+
|
|
150
|
+
**TFT uploads** (`whmi-wri`, `whmi-wris`): the display acknowledges 4096-byte chunks, answers
|
|
151
|
+
v1.2's first chunk with a jump, and keeps the image in `display.flash`. It can be told to
|
|
152
|
+
misbehave: slow, dropped or duplicated acks, jumps, stray bytes, a power cycle mid-upload.
|
|
153
|
+
|
|
154
|
+
**The HMI.** Given the project (`hmi=Project.load(...)`, read straight from the Nextion Editor's
|
|
155
|
+
`.HMI`), the display knows what exists and resolves names as the real one does:
|
|
156
|
+
|
|
157
|
+
- `comp.attr` without a page means the current page;
|
|
158
|
+
- `page.comp.attr` reaches another page only for a global component;
|
|
159
|
+
- the attribute must be one the component has, and a literal must suit it (text or number);
|
|
160
|
+
- page and component ids, for `page N` and touch events, are the HMI's;
|
|
161
|
+
- `get` answers the HMI's value for anything not yet assigned (`display.value(key)`);
|
|
162
|
+
- loading a page resets its local components;
|
|
163
|
+
- the display starts on page 0.
|
|
164
|
+
|
|
165
|
+
Anything that does not resolve fails with `1a` and is recorded in `display.errors`. On a real
|
|
166
|
+
display running `bkcmd=0`, that is a label that is silently never drawn. Without an HMI every
|
|
167
|
+
name is accepted. `resolver=` overrides the HMI with a function of your own.
|
|
168
|
+
|
|
169
|
+
**The TFT's own code.** With an HMI, the display also runs the code in it (`emu/script.py`):
|
|
170
|
+
|
|
171
|
+
- `Program.s` at `boot()`: its globals, then `page 0`, whose load code may print a banner;
|
|
172
|
+
- each page's `unload`, `load` and `loadend` on a page change, whoever asked for it;
|
|
173
|
+
- the current page's timers, every `tim` ms while `en` is set, on the display's clock;
|
|
174
|
+
- touches. `touch_down(x, y)` / `touch_move` / `touch_up` are a finger on the screen, and
|
|
175
|
+
`press(name)`, `slide(name, value)`, `drag`, `swipe` and `tap` are built on them. The touch
|
|
176
|
+
lands on the visible component drawn last at that point (else the page). It sends the 0x65
|
|
177
|
+
edges the component's `sendkey` asks for, then runs the component's `down` / `up` (and a
|
|
178
|
+
slider's `slide`), then the page's touch capture's, with `tch0..3` and the capture's `val`
|
|
179
|
+
set as the display sets them. With `sendxy=1` each press and release is also reported with
|
|
180
|
+
its coordinates (0x67).
|
|
181
|
+
- sleep: with `sleep=1` the screen is dark and touches reach nothing; with `thup=1` the first
|
|
182
|
+
one wakes the display (0x87) and is used up doing it.
|
|
183
|
+
|
|
184
|
+
All of an HMI's code is compiled when the display loads it, and code outside what the
|
|
185
|
+
interpreter supports is refused then (`UnsupportedCode`, naming the event and line).
|
|
186
|
+
`python -m nextion_tools.emu.script gui.HMI` lists what an HMI uses and what it cannot run. An override
|
|
187
|
+
(`overrides={"page.component.event": fn}`, logged at startup) runs Python instead of an event's
|
|
188
|
+
code; `scripts=False` turns the code off, and touches are then bare 0x65 frames
|
|
189
|
+
(`send_touch_event`). A statement that fails when it runs (an unknown name, say) is reported as
|
|
190
|
+
an instruction would be (`1a`, `display.errors`, `display.script_errors`), and the code carries on.
|
|
191
|
+
|
|
192
|
+
For tests that need timers to fire exactly, pass `clock=ManualClock()`, then `clock.advance(s)`
|
|
193
|
+
and `display.tick()`. `expire_timer(name)` runs a timer out now, whatever the clock.
|
|
194
|
+
|
|
195
|
+
Observed on a real NSPanel: assigning `en=1` to a running timer restarts its countdown (tapping
|
|
196
|
+
keeps NSPanel Manager's screensaver off, and each tap runs `sleep_display.en=1`). The Editor's
|
|
197
|
+
Debug simulator does not restart it, so it is not a guide to this. Assigning `tim` restarts it
|
|
198
|
+
too, which is not observed either way.
|
|
199
|
+
|
|
200
|
+
Not measured on hardware yet, so each is a choice (see `emu/script.py` and `display.py`):
|
|
201
|
+
|
|
202
|
+
- the order on a touch: the 0x65 frame, then the component's code, then the touch capture's;
|
|
203
|
+
- `&&` and `||` stop at the first operand that decides; a failed condition skips its `if`;
|
|
204
|
+
- an expression mixing `+`/`-` with `*`/`/` is refused, since precedence is not known;
|
|
205
|
+
- `page` ends the event that ran it;
|
|
206
|
+
- a slider's value is linear in the touch position, ignoring the thumb's width;
|
|
207
|
+
- whether a failed statement in event code sends `1a` at `bkcmd` 2 and 3 (assumed yes);
|
|
208
|
+
- `sendxy` and sleep follow Nextion's instruction set: 0x67 on press and release only (whether
|
|
209
|
+
a drag streams them is not known), its order before the 0x65 frame, and the waking touch
|
|
210
|
+
going nowhere.
|
|
211
|
+
|
|
212
|
+
**What the screen shows.** `display.snapshot()` draws the current page as a numpy array, and
|
|
213
|
+
`display.save_png(path)` saves it (both need `[images]`). They draw from the HMI and what the
|
|
214
|
+
display holds now: pictures, crop pictures, text in the HMI's own fonts, buttons in their
|
|
215
|
+
pressed or second state, sliders, progress bars, hidden components left out, and the drawing
|
|
216
|
+
instructions sent since the page was shown (`fill`, `cls`, `pic`, `xstr`, `line`, `draw`,
|
|
217
|
+
`cir`, `cirs`). A sleeping display is black. See `render.py` for what is and is not drawn.
|
|
218
|
+
|
|
219
|
+
**In a browser.** `python -m nextion_tools.emu.viewer --hmi gui.HMI --tcp 4000` runs a display
|
|
220
|
+
and serves it on http://127.0.0.1:8080/. A press on the screen is a finger going down there, a
|
|
221
|
+
drag moves it, and the side panel shows the page, the last instructions, rejected ones, and the
|
|
222
|
+
input buffer when the flow model is on. `Viewer(display)` attaches one to a display a test or rig
|
|
223
|
+
already has.
|
|
224
|
+
|
|
225
|
+
**Orientation.** A page's coordinates are the view of someone looking at a panel mounted as its
|
|
226
|
+
TFT intends (NSPanel's US pages are 320 x 480, its EU pages 480 x 320), and touches use the same
|
|
227
|
+
frame, so pictures and the viewer are upright by default. `Display(rotation=90)` turns snapshots
|
|
228
|
+
and the viewer clockwise, and maps clicks back, to show a panel as it actually hangs.
|
|
229
|
+
`render.native_rotation(tft.orientation(data))` gives the rotation that shows the LCD's own
|
|
230
|
+
frame, the one a TFT stores its pictures in: a TFT made at 90 holds them turned 90 degrees
|
|
231
|
+
clockwise (`demo/demo.tft`). `Project.direction` is the same setting, read from the HMI.
|
|
232
|
+
Fonts from recent Editors are stored turned that way too, and are read upright.
|
|
233
|
+
|
|
234
|
+
## Not modelled (yet)
|
|
235
|
+
|
|
236
|
+
- **Pixel-exact rendering.** Pictures and glyphs are exact; borders, the Editor's compressed
|
|
237
|
+
pictures (drawn from the file they were imported from) and some component types are not.
|
|
238
|
+
- **Drawing instructions** in event code (`xstr`, `fill`, ...) and `click`, `tsw`, loops: the
|
|
239
|
+
HMIs tested do not use them, so they are refused rather than guessed at.
|
|
240
|
+
|
|
241
|
+
## Tests
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
pip install -e ".[images,serial,test]"
|
|
245
|
+
python -m pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Most tests build small projects in code (`tests/hmi_builder.py`), and some use the demo's
|
|
249
|
+
files, which the Editor made. The rest need NSPanel Manager's project and TFT, and are skipped
|
|
250
|
+
unless given them:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
NEXTION_TOOLS_HMI=path/to/gui.HMI NEXTION_TOOLS_TFT=path/to/gui.tft python -m pytest
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT; see [LICENSE](https://github.com/jamesmulcahy/nextion-tools/blob/main/LICENSE).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# nextion-tools
|
|
2
|
+
|
|
3
|
+
Tools for [Nextion](https://nextion.tech) HMI displays: the Nextion Editor's project files, the
|
|
4
|
+
TFTs it compiles, and an emulator of the display itself for testing the firmware that drives one.
|
|
5
|
+
|
|
6
|
+
## Try it
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
git clone https://github.com/jamesmulcahy/nextion-tools
|
|
10
|
+
cd nextion-tools
|
|
11
|
+
pip install -e ".[images]"
|
|
12
|
+
python demo/run.py
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then open http://127.0.0.1:8080/ and use the panel with your mouse.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
That is an emulated Nextion display running a project made in the Nextion Editor
|
|
20
|
+
(`demo/demo.HMI`), with its own touch handling, scripts and timers, and a small Python program
|
|
21
|
+
(`demo/companion.py`) as the microcontroller behind it: the menu's buttons change page, the
|
|
22
|
+
thermostat's bar turns red near the top, Paint draws where you drag, and Pong plays against a
|
|
23
|
+
paddle that follows the pointer. The same companion drives a real display flashed with
|
|
24
|
+
`demo/demo.tft`. See [The demo](#the-demo) and [demo/README.md](https://github.com/jamesmulcahy/nextion-tools/blob/main/demo/README.md).
|
|
25
|
+
|
|
26
|
+
| Module | What |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `nextion_tools.hmi` | `.HMI` projects: pages, components, ids, geometry, attributes, event code, `Program.s`; numeric edits with re-stamping |
|
|
29
|
+
| `nextion_tools.fonts` | `.zi` fonts: metrics and glyph bitmaps, and which font a component's `font` index means |
|
|
30
|
+
| `nextion_tools.images` | pictures as RGB arrays, and the panel's RGB565 |
|
|
31
|
+
| `nextion_tools.tft` | a compiled `.tft`: its picture table, checksums and orientation, and same-size picture swaps |
|
|
32
|
+
| `nextion_tools.crc` | the CRC behind TFT checksums and HMI page stamps |
|
|
33
|
+
| `nextion_tools.render` | draws a page as the display shows it, from the HMI and the values its components hold |
|
|
34
|
+
| `nextion_tools.emu` | an emulated display, driven by the project's HMI (below) |
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install nextion-tools # hmi, tft, crc and the emulator: no dependencies
|
|
38
|
+
pip install nextion-tools[images] # fonts, pictures, rendering and the viewer: numpy and Pillow
|
|
39
|
+
pip install nextion-tools[serial] # the emulator on a real serial port: pyserial
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from nextion_tools.hmi import Project
|
|
44
|
+
|
|
45
|
+
project = Project.load("gui.HMI")
|
|
46
|
+
home = project.page("home") # KeyError if there is none; find_page() gives None
|
|
47
|
+
print(home.id, [(c.id, c.name, c.kind, c.rect) for c in home.components])
|
|
48
|
+
print(home.component("tm0").events["timer"])
|
|
49
|
+
print(project.program) # Program.s, line by line
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The format notes are in the module docstrings, and were checked against files the Editor saved.
|
|
53
|
+
|
|
54
|
+
## The demo
|
|
55
|
+
|
|
56
|
+
`demo/` holds a small project made in the Nextion Editor (`demo.HMI`, and `demo.tft` compiled
|
|
57
|
+
from it) and `companion.py`, a Python program that plays the MCU. `python demo/run.py` runs the
|
|
58
|
+
whole thing (the emulated display, the companion and the browser viewer) in one process, and
|
|
59
|
+
takes `--http PORT` and `--rotation 90` (the screen as the LCD itself holds it).
|
|
60
|
+
`python demo/companion.py --serial /dev/ttyUSB0 --fps 8` drives a real display running
|
|
61
|
+
`demo.tft`, which needs the `serial` extra. [demo/README.md](https://github.com/jamesmulcahy/nextion-tools/blob/main/demo/README.md) has what each page does and what
|
|
62
|
+
the project turned up.
|
|
63
|
+
Nextion and the Nextion Editor are ITEAD's; this project is not affiliated with them.
|
|
64
|
+
|
|
65
|
+
## The emulator
|
|
66
|
+
|
|
67
|
+
An emulated Nextion HMI display, for testing the firmware that drives one. Point the MCU's UART
|
|
68
|
+
at it (QEMU's `-serial tcp:`, a pty, or a real serial port). It executes what it is sent, keeps a
|
|
69
|
+
model of what the screen would show, replies as a real display does, and lets a test act as the
|
|
70
|
+
user.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from nextion_tools.emu import Display, TcpServer
|
|
74
|
+
from nextion_tools.hmi import Project
|
|
75
|
+
|
|
76
|
+
display = Display(TcpServer(port=4000), hmi=Project.load("gui.HMI")) # qemu ... -serial tcp:127.0.0.1:4000
|
|
77
|
+
display.wait_for_page("home")
|
|
78
|
+
display.press("h_ceiling") # a finger on it, by name or id: display.press(22)
|
|
79
|
+
display.wait_for_value("home.room.txt", "Kitchen")
|
|
80
|
+
assert not display.errors # nothing the display would have rejected
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or from a shell, to watch what firmware sends:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
python -m nextion_tools.emu --tcp 4000
|
|
87
|
+
python -m nextion_tools.emu --pty # prints /dev/pts/N
|
|
88
|
+
python -m nextion_tools.emu --serial /dev/ttyUSB0 # needs pyserial: pip install nextion-tools[serial]
|
|
89
|
+
python -m nextion_tools.emu --tcp 4000 --hmi gui.HMI # with the project's pages and code
|
|
90
|
+
python -m nextion_tools.emu.script gui.HMI # can every event of this HMI be run?
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What it models
|
|
94
|
+
|
|
95
|
+
Replies follow what an NX4832T035 (the Sonoff NSPanel's display) was measured to do, which differs
|
|
96
|
+
from Nextion's documentation in places:
|
|
97
|
+
|
|
98
|
+
| Instruction | `bkcmd=3` | `bkcmd=0` |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| assignment | `01` | nothing |
|
|
101
|
+
| `get x` | `71` + 4 bytes (or `70` + text), **no** `01` | the same |
|
|
102
|
+
| `printh …` | the bytes, **no** `01` | the same |
|
|
103
|
+
| unknown component, variable, or bad syntax | `1a` (never `02`/`1b`) | nothing |
|
|
104
|
+
| `page` / `ref` | `01` at once, then the redraw blocks the next instruction ~400 ms | — |
|
|
105
|
+
| `delay=N` | `01` when it ends | — |
|
|
106
|
+
|
|
107
|
+
`bkcmd` starts at 2, as on a display that has just powered on, and `power_cycle()` puts it back.
|
|
108
|
+
|
|
109
|
+
**Ideal or flow model.** By default every instruction executes the moment it arrives, so tests
|
|
110
|
+
see exactly what the firmware meant to draw. With `flow_model=True` the display behaves as the
|
|
111
|
+
real one does:
|
|
112
|
+
|
|
113
|
+
- input arrives at line rate (QEMU's UART does not pace writes);
|
|
114
|
+
- it waits in a buffer of 1088 bytes;
|
|
115
|
+
- the display stalls on page changes, `ref` and `delay`;
|
|
116
|
+
- a burst that overruns the buffer during a stall loses commands (`display.lost`), and the
|
|
117
|
+
display sends `0x24` when it resumes.
|
|
118
|
+
|
|
119
|
+
`tests/emu/test_calibration.py` replays the hardware measurements against the model, so it cannot
|
|
120
|
+
drift from them unnoticed.
|
|
121
|
+
|
|
122
|
+
**TFT uploads** (`whmi-wri`, `whmi-wris`): the display acknowledges 4096-byte chunks, answers
|
|
123
|
+
v1.2's first chunk with a jump, and keeps the image in `display.flash`. It can be told to
|
|
124
|
+
misbehave: slow, dropped or duplicated acks, jumps, stray bytes, a power cycle mid-upload.
|
|
125
|
+
|
|
126
|
+
**The HMI.** Given the project (`hmi=Project.load(...)`, read straight from the Nextion Editor's
|
|
127
|
+
`.HMI`), the display knows what exists and resolves names as the real one does:
|
|
128
|
+
|
|
129
|
+
- `comp.attr` without a page means the current page;
|
|
130
|
+
- `page.comp.attr` reaches another page only for a global component;
|
|
131
|
+
- the attribute must be one the component has, and a literal must suit it (text or number);
|
|
132
|
+
- page and component ids, for `page N` and touch events, are the HMI's;
|
|
133
|
+
- `get` answers the HMI's value for anything not yet assigned (`display.value(key)`);
|
|
134
|
+
- loading a page resets its local components;
|
|
135
|
+
- the display starts on page 0.
|
|
136
|
+
|
|
137
|
+
Anything that does not resolve fails with `1a` and is recorded in `display.errors`. On a real
|
|
138
|
+
display running `bkcmd=0`, that is a label that is silently never drawn. Without an HMI every
|
|
139
|
+
name is accepted. `resolver=` overrides the HMI with a function of your own.
|
|
140
|
+
|
|
141
|
+
**The TFT's own code.** With an HMI, the display also runs the code in it (`emu/script.py`):
|
|
142
|
+
|
|
143
|
+
- `Program.s` at `boot()`: its globals, then `page 0`, whose load code may print a banner;
|
|
144
|
+
- each page's `unload`, `load` and `loadend` on a page change, whoever asked for it;
|
|
145
|
+
- the current page's timers, every `tim` ms while `en` is set, on the display's clock;
|
|
146
|
+
- touches. `touch_down(x, y)` / `touch_move` / `touch_up` are a finger on the screen, and
|
|
147
|
+
`press(name)`, `slide(name, value)`, `drag`, `swipe` and `tap` are built on them. The touch
|
|
148
|
+
lands on the visible component drawn last at that point (else the page). It sends the 0x65
|
|
149
|
+
edges the component's `sendkey` asks for, then runs the component's `down` / `up` (and a
|
|
150
|
+
slider's `slide`), then the page's touch capture's, with `tch0..3` and the capture's `val`
|
|
151
|
+
set as the display sets them. With `sendxy=1` each press and release is also reported with
|
|
152
|
+
its coordinates (0x67).
|
|
153
|
+
- sleep: with `sleep=1` the screen is dark and touches reach nothing; with `thup=1` the first
|
|
154
|
+
one wakes the display (0x87) and is used up doing it.
|
|
155
|
+
|
|
156
|
+
All of an HMI's code is compiled when the display loads it, and code outside what the
|
|
157
|
+
interpreter supports is refused then (`UnsupportedCode`, naming the event and line).
|
|
158
|
+
`python -m nextion_tools.emu.script gui.HMI` lists what an HMI uses and what it cannot run. An override
|
|
159
|
+
(`overrides={"page.component.event": fn}`, logged at startup) runs Python instead of an event's
|
|
160
|
+
code; `scripts=False` turns the code off, and touches are then bare 0x65 frames
|
|
161
|
+
(`send_touch_event`). A statement that fails when it runs (an unknown name, say) is reported as
|
|
162
|
+
an instruction would be (`1a`, `display.errors`, `display.script_errors`), and the code carries on.
|
|
163
|
+
|
|
164
|
+
For tests that need timers to fire exactly, pass `clock=ManualClock()`, then `clock.advance(s)`
|
|
165
|
+
and `display.tick()`. `expire_timer(name)` runs a timer out now, whatever the clock.
|
|
166
|
+
|
|
167
|
+
Observed on a real NSPanel: assigning `en=1` to a running timer restarts its countdown (tapping
|
|
168
|
+
keeps NSPanel Manager's screensaver off, and each tap runs `sleep_display.en=1`). The Editor's
|
|
169
|
+
Debug simulator does not restart it, so it is not a guide to this. Assigning `tim` restarts it
|
|
170
|
+
too, which is not observed either way.
|
|
171
|
+
|
|
172
|
+
Not measured on hardware yet, so each is a choice (see `emu/script.py` and `display.py`):
|
|
173
|
+
|
|
174
|
+
- the order on a touch: the 0x65 frame, then the component's code, then the touch capture's;
|
|
175
|
+
- `&&` and `||` stop at the first operand that decides; a failed condition skips its `if`;
|
|
176
|
+
- an expression mixing `+`/`-` with `*`/`/` is refused, since precedence is not known;
|
|
177
|
+
- `page` ends the event that ran it;
|
|
178
|
+
- a slider's value is linear in the touch position, ignoring the thumb's width;
|
|
179
|
+
- whether a failed statement in event code sends `1a` at `bkcmd` 2 and 3 (assumed yes);
|
|
180
|
+
- `sendxy` and sleep follow Nextion's instruction set: 0x67 on press and release only (whether
|
|
181
|
+
a drag streams them is not known), its order before the 0x65 frame, and the waking touch
|
|
182
|
+
going nowhere.
|
|
183
|
+
|
|
184
|
+
**What the screen shows.** `display.snapshot()` draws the current page as a numpy array, and
|
|
185
|
+
`display.save_png(path)` saves it (both need `[images]`). They draw from the HMI and what the
|
|
186
|
+
display holds now: pictures, crop pictures, text in the HMI's own fonts, buttons in their
|
|
187
|
+
pressed or second state, sliders, progress bars, hidden components left out, and the drawing
|
|
188
|
+
instructions sent since the page was shown (`fill`, `cls`, `pic`, `xstr`, `line`, `draw`,
|
|
189
|
+
`cir`, `cirs`). A sleeping display is black. See `render.py` for what is and is not drawn.
|
|
190
|
+
|
|
191
|
+
**In a browser.** `python -m nextion_tools.emu.viewer --hmi gui.HMI --tcp 4000` runs a display
|
|
192
|
+
and serves it on http://127.0.0.1:8080/. A press on the screen is a finger going down there, a
|
|
193
|
+
drag moves it, and the side panel shows the page, the last instructions, rejected ones, and the
|
|
194
|
+
input buffer when the flow model is on. `Viewer(display)` attaches one to a display a test or rig
|
|
195
|
+
already has.
|
|
196
|
+
|
|
197
|
+
**Orientation.** A page's coordinates are the view of someone looking at a panel mounted as its
|
|
198
|
+
TFT intends (NSPanel's US pages are 320 x 480, its EU pages 480 x 320), and touches use the same
|
|
199
|
+
frame, so pictures and the viewer are upright by default. `Display(rotation=90)` turns snapshots
|
|
200
|
+
and the viewer clockwise, and maps clicks back, to show a panel as it actually hangs.
|
|
201
|
+
`render.native_rotation(tft.orientation(data))` gives the rotation that shows the LCD's own
|
|
202
|
+
frame, the one a TFT stores its pictures in: a TFT made at 90 holds them turned 90 degrees
|
|
203
|
+
clockwise (`demo/demo.tft`). `Project.direction` is the same setting, read from the HMI.
|
|
204
|
+
Fonts from recent Editors are stored turned that way too, and are read upright.
|
|
205
|
+
|
|
206
|
+
## Not modelled (yet)
|
|
207
|
+
|
|
208
|
+
- **Pixel-exact rendering.** Pictures and glyphs are exact; borders, the Editor's compressed
|
|
209
|
+
pictures (drawn from the file they were imported from) and some component types are not.
|
|
210
|
+
- **Drawing instructions** in event code (`xstr`, `fill`, ...) and `click`, `tsw`, loops: the
|
|
211
|
+
HMIs tested do not use them, so they are refused rather than guessed at.
|
|
212
|
+
|
|
213
|
+
## Tests
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
pip install -e ".[images,serial,test]"
|
|
217
|
+
python -m pytest
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Most tests build small projects in code (`tests/hmi_builder.py`), and some use the demo's
|
|
221
|
+
files, which the Editor made. The rest need NSPanel Manager's project and TFT, and are skipped
|
|
222
|
+
unless given them:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
NEXTION_TOOLS_HMI=path/to/gui.HMI NEXTION_TOOLS_TFT=path/to/gui.tft python -m pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT; see [LICENSE](https://github.com/jamesmulcahy/nextion-tools/blob/main/LICENSE).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nextion-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read and edit Nextion Editor projects, fonts and TFTs, and emulate the display they run on"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = []
|
|
14
|
+
authors = [{ name = "James Mulcahy" }]
|
|
15
|
+
keywords = ["nextion", "hmi", "tft", "emulator", "display", "esp32"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
21
|
+
"Topic :: Software Development :: Testing",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/jamesmulcahy/nextion-tools"
|
|
26
|
+
Source = "https://github.com/jamesmulcahy/nextion-tools"
|
|
27
|
+
Issues = "https://github.com/jamesmulcahy/nextion-tools/issues"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
# Fonts, pictures and picture swaps in TFTs. Reading HMIs and the emulator need neither.
|
|
31
|
+
images = ["numpy", "pillow"]
|
|
32
|
+
serial = ["pyserial>=3.5"]
|
|
33
|
+
test = ["pytest>=8", "pytest-timeout>=2"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
pythonpath = ["tests", "tests/emu"]
|
|
41
|
+
timeout = 60
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Tools for Nextion HMI displays: the Editor's project files and the display itself.
|
|
2
|
+
|
|
3
|
+
- `nextion_tools.hmi`: .HMI projects: pages, components, ids, geometry, event code, Program.s;
|
|
4
|
+
numeric edits and re-stamping
|
|
5
|
+
- `nextion_tools.fonts`: .zi fonts: glyph bitmaps and metrics (numpy)
|
|
6
|
+
- `nextion_tools.images`: pictures as RGB arrays, RGB565 (numpy, Pillow)
|
|
7
|
+
- `nextion_tools.tft`: a compiled .tft's picture table, checksums and orientation
|
|
8
|
+
- `nextion_tools.crc`: the CRC behind TFT checksums and HMI stamps
|
|
9
|
+
- `nextion_tools.render`: draw a page as the display shows it (numpy, Pillow)
|
|
10
|
+
- `nextion_tools.emu`: an emulated display, driven by an HMI, for testing MCU firmware, with a
|
|
11
|
+
browser viewer (`nextion_tools.emu.viewer`)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The CRC behind TFT checksums and HMI page stamps.
|
|
2
|
+
|
|
3
|
+
Both are CRC-32/MPEG-2 (poly 0x04C11DB7, MSB first, init 0xFFFFFFFF, no final XOR) over the
|
|
4
|
+
data with each byte widened to a big-endian 32-bit word, which is how an STM32's CRC unit
|
|
5
|
+
checksums a byte stream. See https://github.com/UNUF/TFTTool for the TFT side.
|
|
6
|
+
"""
|
|
7
|
+
import zlib
|
|
8
|
+
|
|
9
|
+
_REVERSED = bytes(int(f'{i:08b}'[::-1], 2) for i in range(256))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def crc32_mpeg2(data):
|
|
13
|
+
"""CRC-32/MPEG-2. zlib computes the bit-reflected CRC-32 with the same polynomial, so this is
|
|
14
|
+
zlib's CRC of the bit-reversed input, bit-reversed, without zlib's final XOR."""
|
|
15
|
+
return int(f'{zlib.crc32(data.translate(_REVERSED)) ^ 0xFFFFFFFF:032b}'[::-1], 2)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def widen(data):
|
|
19
|
+
"""Each byte as a big-endian 32-bit word."""
|
|
20
|
+
words = bytearray(4 * len(data))
|
|
21
|
+
words[3::4] = data
|
|
22
|
+
return bytes(words)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def nextion(data):
|
|
26
|
+
return crc32_mpeg2(widen(data))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""An emulated Nextion display, for testing the firmware that drives one. See display.py."""
|
|
2
|
+
|
|
3
|
+
from .clock import ManualClock
|
|
4
|
+
from .devices import NX4832T035, Device
|
|
5
|
+
from .display import COMOK, REDRAW_S, Display
|
|
6
|
+
from .flow import FlowModel
|
|
7
|
+
from .model import Screen
|
|
8
|
+
from .script import ScriptError, ScriptSyntaxError, UnsupportedCode
|
|
9
|
+
from .transport import NotConnected, Pty, Serial, SocketTransport, TcpServer, Transport
|
|
10
|
+
from .upload import Upload
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"COMOK", "REDRAW_S", "Device", "Display", "NX4832T035", "FlowModel", "ManualClock", "NotConnected", "Screen", "ScriptError",
|
|
14
|
+
"ScriptSyntaxError", "UnsupportedCode", "Pty", "Serial", "SocketTransport",
|
|
15
|
+
"TcpServer", "Transport", "Upload",
|
|
16
|
+
]
|