nextion-tools 0.1.0__py3-none-any.whl

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.
@@ -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"
nextion_tools/crc.py ADDED
@@ -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
+ ]
@@ -0,0 +1,55 @@
1
+ """Run an emulated display and log what it is sent.
2
+
3
+ python -m nextion_tools.emu --tcp 4000 # listen; e.g. qemu ... -serial tcp:127.0.0.1:4000
4
+ python -m nextion_tools.emu --pty # prints the pty to point a program at
5
+ python -m nextion_tools.emu --serial /dev/ttyUSB0
6
+ python -m nextion_tools.emu --tcp 4000 --hmi gui.HMI # the project's pages and code
7
+ """
8
+
9
+ import argparse
10
+ import logging
11
+ import time
12
+
13
+ from .display import Display
14
+ from ..hmi import Project
15
+ from .protocol import TERMINATOR
16
+ from .transport import Pty, Serial, TcpServer
17
+
18
+
19
+ def main(argv=None) -> None:
20
+ parser = argparse.ArgumentParser(prog="python -m nextion_tools.emu", description=__doc__.split("\n")[0])
21
+ where = parser.add_mutually_exclusive_group(required=True)
22
+ where.add_argument("--tcp", type=int, metavar="PORT", help="listen on 127.0.0.1:PORT (0 picks one)")
23
+ where.add_argument("--pty", action="store_true", help="create a pseudo-terminal")
24
+ where.add_argument("--serial", metavar="DEVICE", help="use a serial port (needs pyserial)")
25
+ parser.add_argument("--baud", type=int, default=115200)
26
+ parser.add_argument("--flow", action="store_true", help="model the input buffer and stalls")
27
+ parser.add_argument("--banner", default="", help="a frame the TFT prints when it starts, e.g. NSPM")
28
+ parser.add_argument("--hmi", help="the project's .HMI: its pages, ids and code")
29
+ args = parser.parse_args(argv)
30
+
31
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(name)s %(message)s")
32
+ if args.tcp is not None:
33
+ transport = TcpServer(port=args.tcp)
34
+ print("listening on %s:%d" % (transport.host, transport.port), flush=True)
35
+ elif args.pty:
36
+ transport = Pty()
37
+ print("pty: %s" % transport.path, flush=True)
38
+ else:
39
+ transport = Serial(args.serial, args.baud)
40
+ banner = args.banner.encode() + TERMINATOR if args.banner else b""
41
+ display = Display(transport, flow_model=args.flow, baud=args.baud, banner=banner,
42
+ hmi=Project.load(args.hmi) if args.hmi else None)
43
+ try:
44
+ # Power on (the banner, Program.s) once the MCU is there to hear it.
45
+ while not display.connected:
46
+ time.sleep(0.1)
47
+ display.boot()
48
+ while True:
49
+ time.sleep(1)
50
+ except KeyboardInterrupt:
51
+ display.close()
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,23 @@
1
+ """A clock a test moves by hand, for timers that must fire exactly when the test says.
2
+
3
+ clock = ManualClock()
4
+ display = Display(..., hmi=hmi, clock=clock)
5
+ clock.advance(0.05)
6
+ display.tick() # runs whatever is now due
7
+ """
8
+
9
+ import threading
10
+
11
+
12
+ class ManualClock:
13
+ def __init__(self, start: float = 1000.0):
14
+ self._now = start
15
+ self._lock = threading.Lock()
16
+
17
+ def __call__(self) -> float:
18
+ with self._lock:
19
+ return self._now
20
+
21
+ def advance(self, seconds: float) -> None:
22
+ with self._lock:
23
+ self._now += seconds
@@ -0,0 +1,29 @@
1
+ """What differs between Nextion display models, as measured on each.
2
+
3
+ A Device holds the numbers the emulator needs that belong to the display, not to the project it
4
+ runs or the MCU that drives it. Only one model has been measured so far. Another gets its own
5
+ profile once someone measures it: replay the probes in tests/emu/test_calibration.py against
6
+ the real display and record what it does.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Device:
14
+ model: str
15
+ comok: bytes # the reply to `connect`, without its terminator
16
+ flash_size: int # bytes, for TFT uploads
17
+ buffer_capacity: int # bytes of input the display holds before it drops what arrives
18
+ redraw_s: float # how long a full-page redraw (`page`, `ref`) blocks the next instruction
19
+
20
+
21
+ # The Sonoff NSPanel's display, measured on a panel ("Office") 2026-10-23/24: the input buffer
22
+ # held 1080-1092 bytes, and a page change blocked the next instruction for about 400 ms.
23
+ NX4832T035 = Device(
24
+ model="NX4832T035_011R",
25
+ comok=b"comok 2,30614-0,NX4832T035_011R,163,61488,D264B8204F0E1828,16777216",
26
+ flash_size=16777216,
27
+ buffer_capacity=1088,
28
+ redraw_s=0.4,
29
+ )