ertftm070 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.
ertftm070/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ """ertftm070 — Python driver for the EastRising ER-TFTM070-4V2.1.
2
+
3
+ A 7" 800x480 TFT (SSD1963 controller) driven over a bit-banged 16-bit
4
+ 8080 bus from a Raspberry Pi Zero/1/2/3/4. The board is strapped for
5
+ 16-bit 8080, so one WR strobe writes one 16-bit pixel.
6
+
7
+ Quickstart::
8
+
9
+ pip install ertftm070[Pillow]
10
+
11
+ from ertftm070 import Display
12
+
13
+ with Display() as lcd: # verified wiring + init, backlight on
14
+ lcd.fill(0xF800) # red screen in ~0.6 s
15
+ lcd.image(pil_image, x=0, y=0)
16
+
17
+ ``BACKEND`` reports which pixel path is active: ``"fast"`` (the compiled
18
+ C extension) or ``"slow"`` (the pure-Python fallback).
19
+
20
+ Full wiring table: docs/WIRING.md. Debugging lore: docs/LESSONS.md.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from importlib.metadata import PackageNotFoundError, version
25
+
26
+ from . import backends
27
+ from .colors import rgb565
28
+ from .display import Display
29
+ from .errors import Ertftm070Error, NotOnRaspberryPi
30
+ from .init import INIT_ALT, INIT_BD, INIT_UTFT
31
+ from .pins import DEFAULT_PINS, Pins
32
+ from .touch import Touch, TouchCalibration, TouchPoint
33
+
34
+ try:
35
+ __version__ = version("ertftm070")
36
+ except PackageNotFoundError: # not installed (e.g. running from a source tree)
37
+ __version__ = "0.1.0"
38
+
39
+ BACKEND = backends.BACKEND
40
+
41
+ __all__ = [
42
+ "Display",
43
+ "Pins",
44
+ "DEFAULT_PINS",
45
+ "rgb565",
46
+ "INIT_UTFT",
47
+ "INIT_ALT",
48
+ "INIT_BD",
49
+ "Ertftm070Error",
50
+ "NotOnRaspberryPi",
51
+ "Touch",
52
+ "TouchPoint",
53
+ "TouchCalibration",
54
+ "BACKEND",
55
+ "__version__",
56
+ ]
ertftm070/__main__.py ADDED
@@ -0,0 +1,302 @@
1
+ """ertftm070 command-line tool.
2
+
3
+ ertftm070 selftest bus self-test (device ID + round-trip)
4
+ ertftm070 bars 8 full-height color bars
5
+ ertftm070 fill F800 fill the whole screen with one RGB565 color
6
+ ertftm070 image photo.png show an image (needs Pillow)
7
+ ertftm070 gramcheck write known pixels, read them back from GRAM
8
+ ertftm070 refresh measure the pixel clock and frame rate
9
+ ertftm070 touch-test stream touches until Ctrl+C (needs the I2C wires)
10
+
11
+ The drawing commands (bars/fill/image) hold the picture on screen until
12
+ Ctrl+C or SIGTERM (both turn the backlight off); pass --once to exit
13
+ immediately instead. The diagnostics (selftest/gramcheck) run once and
14
+ exit 0 on success, 1 on failure. Global options (--init, --rotation,
15
+ --once) are accepted before or after the subcommand. Works identically
16
+ as ``python -m ertftm070``.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import logging
22
+ import signal
23
+ import sys
24
+ import time
25
+
26
+ from . import init
27
+ from ._i2c import I2CError
28
+ from .display import Display, crystal_guess
29
+ from .errors import Ertftm070Error
30
+
31
+ BARS = [
32
+ ("black", 0x0000),
33
+ ("red", 0xF800),
34
+ ("green", 0x07E0),
35
+ ("blue", 0x001F),
36
+ ("white", 0xFFFF),
37
+ ("yellow", 0xFFE0),
38
+ ("cyan", 0x07FF),
39
+ ("magenta", 0xF81F),
40
+ ]
41
+
42
+ _TABLES = {"utft": init.INIT_UTFT, "alt": init.INIT_ALT, "bd": init.INIT_BD}
43
+
44
+ # Commands that leave a picture on the panel: hold until interrupted.
45
+ # The diagnostics are one-shot; holding them open would defeat their
46
+ # exit codes.
47
+ _HOLD_COMMANDS = frozenset({"bars", "fill", "image"})
48
+
49
+
50
+ def _add_global_options(
51
+ parser: argparse.ArgumentParser, *, suppress_defaults: bool = False
52
+ ) -> None:
53
+ """The options valid in front of the subcommand AND behind it.
54
+
55
+ Registered on the top-level parser (with real defaults) and on every
56
+ subparser (via a shared parent). On the subparser level the defaults
57
+ are SUPPRESSED: argparse versions before 3.12 re-parse the remaining
58
+ args into a fresh namespace and copy it over the outer one, which
59
+ would clobber a ``--rotation 90`` given *before* the subcommand with
60
+ the subparser's default of 0. With no default to apply, the
61
+ subparser only writes what the user actually typed there — so
62
+ ``ertftm070 bars --rotation 90`` and ``ertftm070 --rotation 90 bars``
63
+ behave identically on every supported Python.
64
+ """
65
+ parser.add_argument(
66
+ "--init",
67
+ choices=sorted(_TABLES),
68
+ default=argparse.SUPPRESS if suppress_defaults else "utft",
69
+ help="init table (default: utft — the one verified on hardware)",
70
+ )
71
+ parser.add_argument(
72
+ "--rotation",
73
+ type=int,
74
+ choices=[0, 90, 180, 270],
75
+ default=argparse.SUPPRESS if suppress_defaults else 0,
76
+ help="logical rotation (default: 0)",
77
+ )
78
+ parser.add_argument(
79
+ "--once",
80
+ action="store_true",
81
+ default=argparse.SUPPRESS if suppress_defaults else False,
82
+ help="exit right after drawing instead of keeping the display on "
83
+ "until Ctrl+C (turns the backlight off)",
84
+ )
85
+
86
+
87
+ def _rgb565_hex(value: str) -> int:
88
+ """argparse type: a hex string that must be a 16-bit RGB565 color."""
89
+ try:
90
+ color = int(value, 16)
91
+ except ValueError:
92
+ raise argparse.ArgumentTypeError(f"not a hex number: {value!r}") from None
93
+ if not 0 <= color <= 0xFFFF:
94
+ raise argparse.ArgumentTypeError(
95
+ f"{value!r} is outside 0..FFFF — RGB565 colors are 16 bits"
96
+ )
97
+ return color
98
+
99
+
100
+ def build_parser() -> argparse.ArgumentParser:
101
+ parser = argparse.ArgumentParser(
102
+ prog="ertftm070",
103
+ description="Drive an ER-TFTM070-4V2.1 (SSD1963) display on a Raspberry Pi.",
104
+ )
105
+ _add_global_options(parser) # before the subcommand
106
+ subparsers = parser.add_subparsers(dest="command", required=True)
107
+
108
+ common = argparse.ArgumentParser(add_help=False)
109
+ _add_global_options(common, suppress_defaults=True) # ... and after it
110
+
111
+ subparsers.add_parser(
112
+ "selftest", parents=[common],
113
+ help="bus self-test (device ID + register round-trip)",
114
+ )
115
+ subparsers.add_parser(
116
+ "bars", parents=[common], help="draw 8 full-height color bars"
117
+ )
118
+
119
+ fill = subparsers.add_parser(
120
+ "fill", parents=[common], help="fill the whole screen with one color"
121
+ )
122
+ fill.add_argument(
123
+ "color", type=_rgb565_hex, help="RGB565 hex, e.g. F800 for red (0..FFFF)"
124
+ )
125
+
126
+ image = subparsers.add_parser(
127
+ "image", parents=[common], help="show an image file (needs Pillow)"
128
+ )
129
+ image.add_argument("path", help="path to a PNG/JPEG/… file")
130
+ image.add_argument("--x", type=int, default=0, help="top-left x (default 0)")
131
+ image.add_argument("--y", type=int, default=0, help="top-left y (default 0)")
132
+
133
+ subparsers.add_parser(
134
+ "gramcheck", parents=[common],
135
+ help="write known pixels, read them back from GRAM",
136
+ )
137
+
138
+ subparsers.add_parser(
139
+ "refresh", parents=[common],
140
+ help="measure the pixel clock and frame rate from the TE pin",
141
+ )
142
+
143
+ subparsers.add_parser(
144
+ "touch-test", parents=[common],
145
+ help="stream touch points until Ctrl+C (needs SCL/SDA/WAKE wired)",
146
+ )
147
+ return parser
148
+
149
+
150
+ def _make_display(args: argparse.Namespace) -> Display:
151
+ return Display(init_table=_TABLES[args.init], rotation=args.rotation)
152
+
153
+
154
+ def run(args: argparse.Namespace, display: Display | None = None) -> int:
155
+ """Execute parsed args. ``display`` is injectable for tests."""
156
+ own = display is None
157
+ if own:
158
+ display = _make_display(args)
159
+ display.open()
160
+ try:
161
+ if args.command == "selftest":
162
+ return 0 if display.selftest() else 1
163
+
164
+ if args.command == "bars":
165
+ t0 = time.time()
166
+ w = display.width // len(BARS)
167
+ for i, (_name, color) in enumerate(BARS):
168
+ display.fill_rect(i * w, 0, w, display.height, color)
169
+ print("8 color bars drawn in %.1f s" % (time.time() - t0))
170
+ print("Expected left->right: {}".format(", ".join(n for n, _ in BARS)))
171
+ return 0
172
+
173
+ if args.command == "fill":
174
+ t0 = time.time()
175
+ display.fill(args.color) # already validated by _rgb565_hex
176
+ print("filled in %.1f s" % (time.time() - t0))
177
+ return 0
178
+
179
+ if args.command == "image":
180
+ try:
181
+ from PIL import Image # lazy — needs the pillow extra
182
+ except ImportError:
183
+ print(
184
+ "the image command needs Pillow: "
185
+ "pip install 'ertftm070[Pillow]'",
186
+ file=sys.stderr,
187
+ )
188
+ return 2
189
+ try:
190
+ img = Image.open(args.path)
191
+ except OSError as exc:
192
+ print(f"cannot open {args.path!r}: {exc}", file=sys.stderr)
193
+ return 2
194
+ t0 = time.time()
195
+ display.image(img, args.x, args.y, fit=True)
196
+ print(
197
+ f"{img.size[0]}x{img.size[1]} image blitted in "
198
+ f"{time.time() - t0:.1f} s"
199
+ )
200
+ return 0
201
+
202
+ if args.command == "gramcheck":
203
+ return 0 if display.gramcheck() else 1
204
+
205
+ if args.command == "refresh":
206
+ try:
207
+ pclk_khz, hz = display.refresh_rate()
208
+ except (TimeoutError, RuntimeError) as exc:
209
+ # TimeoutError: no TE pulses (wiring / panel asleep);
210
+ # RuntimeError: Pins.te is None, or the init table lacks
211
+ # 0xB4/0xB6. Both mean "cannot measure" — report and fail.
212
+ print(f"error: {exc}", file=sys.stderr)
213
+ return 1
214
+ print(f"pixel clock (from TE): {pclk_khz} kHz")
215
+ print(f"frame rate (TE): {hz:.1f} Hz")
216
+ print(
217
+ "crystal: "
218
+ f"{crystal_guess(pclk_khz, display.init_table)}"
219
+ )
220
+ return 0
221
+
222
+ if args.command == "touch-test":
223
+ from .touch import Touch
224
+
225
+ touch = Touch(display.bus)
226
+ try:
227
+ touch.open()
228
+ except I2CError as exc:
229
+ print(f"touch unavailable: {exc}", file=sys.stderr)
230
+ return 1
231
+ print("touch test — touch the panel (Ctrl+C to exit)")
232
+ seen = None
233
+ try:
234
+ while True:
235
+ points = touch.read()
236
+ key = tuple((p.id, p.x, p.y) for p in points)
237
+ if key != seen:
238
+ seen = key
239
+ for p in points:
240
+ m = touch.calibration.map(p)
241
+ # mapped points are panel-native; --rotation
242
+ # users want the logical frame instead
243
+ ux, uy = display.unmap_point(m.x, m.y)
244
+ print(
245
+ f" finger {p.id}: raw=({p.x},{p.y}) "
246
+ f"-> panel=({m.x},{m.y}) "
247
+ f"screen=({ux},{uy}) event={p.event}"
248
+ )
249
+ time.sleep(0.02)
250
+ finally:
251
+ touch.close()
252
+ return 0
253
+
254
+ print("no command given — see ertftm070 --help", file=sys.stderr)
255
+ return 2
256
+ finally:
257
+ if own:
258
+ display.close()
259
+
260
+
261
+ def _sigterm(_signum, _frame) -> None:
262
+ """SIGTERM (kill, systemd stop) unwinds like Ctrl-C: the finally
263
+ blocks close the display and the process exits."""
264
+ raise KeyboardInterrupt
265
+
266
+
267
+ def main(argv: list[str] | None = None) -> None:
268
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
269
+ args = build_parser().parse_args(argv)
270
+ signal.signal(signal.SIGTERM, _sigterm) # together with Ctrl-C: exit cleanly
271
+ code = 0
272
+ display = None
273
+ try:
274
+ display = _make_display(args)
275
+ display.open()
276
+ code = run(args, display)
277
+ except Ertftm070Error as exc:
278
+ print(f"error: {exc}", file=sys.stderr)
279
+ code = 1
280
+ except ValueError as exc:
281
+ print(f"error: {exc}", file=sys.stderr)
282
+ code = 2
283
+ except KeyboardInterrupt:
284
+ print() # user aborted mid-command; nothing to report
285
+ else:
286
+ if not args.once and args.command in _HOLD_COMMANDS:
287
+ # keep the picture on screen until the user is done looking;
288
+ # Ctrl-C (or SIGTERM) turns the backlight off and exits
289
+ print("display on — press Ctrl+C to exit")
290
+ try:
291
+ while True:
292
+ time.sleep(1)
293
+ except KeyboardInterrupt:
294
+ print()
295
+ finally:
296
+ if display is not None:
297
+ display.close()
298
+ sys.exit(code)
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()