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.
Files changed (38) hide show
  1. nextion_tools-0.1.0/LICENSE +21 -0
  2. nextion_tools-0.1.0/PKG-INFO +258 -0
  3. nextion_tools-0.1.0/README.md +230 -0
  4. nextion_tools-0.1.0/pyproject.toml +41 -0
  5. nextion_tools-0.1.0/setup.cfg +4 -0
  6. nextion_tools-0.1.0/src/nextion_tools/__init__.py +14 -0
  7. nextion_tools-0.1.0/src/nextion_tools/crc.py +26 -0
  8. nextion_tools-0.1.0/src/nextion_tools/emu/__init__.py +16 -0
  9. nextion_tools-0.1.0/src/nextion_tools/emu/__main__.py +55 -0
  10. nextion_tools-0.1.0/src/nextion_tools/emu/clock.py +23 -0
  11. nextion_tools-0.1.0/src/nextion_tools/emu/devices.py +29 -0
  12. nextion_tools-0.1.0/src/nextion_tools/emu/display.py +1163 -0
  13. nextion_tools-0.1.0/src/nextion_tools/emu/flow.py +199 -0
  14. nextion_tools-0.1.0/src/nextion_tools/emu/model.py +94 -0
  15. nextion_tools-0.1.0/src/nextion_tools/emu/protocol.py +150 -0
  16. nextion_tools-0.1.0/src/nextion_tools/emu/script.py +690 -0
  17. nextion_tools-0.1.0/src/nextion_tools/emu/transport.py +226 -0
  18. nextion_tools-0.1.0/src/nextion_tools/emu/upload.py +174 -0
  19. nextion_tools-0.1.0/src/nextion_tools/emu/viewer.py +295 -0
  20. nextion_tools-0.1.0/src/nextion_tools/emu/waiting.py +25 -0
  21. nextion_tools-0.1.0/src/nextion_tools/fonts.py +150 -0
  22. nextion_tools-0.1.0/src/nextion_tools/hmi.py +467 -0
  23. nextion_tools-0.1.0/src/nextion_tools/images.py +22 -0
  24. nextion_tools-0.1.0/src/nextion_tools/render.py +474 -0
  25. nextion_tools-0.1.0/src/nextion_tools/tft.py +91 -0
  26. nextion_tools-0.1.0/src/nextion_tools.egg-info/PKG-INFO +258 -0
  27. nextion_tools-0.1.0/src/nextion_tools.egg-info/SOURCES.txt +36 -0
  28. nextion_tools-0.1.0/src/nextion_tools.egg-info/dependency_links.txt +1 -0
  29. nextion_tools-0.1.0/src/nextion_tools.egg-info/requires.txt +11 -0
  30. nextion_tools-0.1.0/src/nextion_tools.egg-info/top_level.txt +1 -0
  31. nextion_tools-0.1.0/tests/test_crc.py +10 -0
  32. nextion_tools-0.1.0/tests/test_demo_files.py +98 -0
  33. nextion_tools-0.1.0/tests/test_edit.py +69 -0
  34. nextion_tools-0.1.0/tests/test_fonts.py +104 -0
  35. nextion_tools-0.1.0/tests/test_hmi.py +145 -0
  36. nextion_tools-0.1.0/tests/test_images.py +18 -0
  37. nextion_tools-0.1.0/tests/test_render.py +172 -0
  38. 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
+ ![The demo's menu, thermostat, paint and pong pages, as the emulator draws them](https://raw.githubusercontent.com/jamesmulcahy/nextion-tools/main/docs/demo.png)
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
+ ![The demo's menu, thermostat, paint and pong pages, as the emulator draws them](https://raw.githubusercontent.com/jamesmulcahy/nextion-tools/main/docs/demo.png)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]