ertftm070 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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv*/
7
+ venv/
8
+ *.so
9
+ .pytest_cache/
10
+ .ruff_cache/
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — 2026-09-06
8
+
9
+ ### Added
10
+
11
+ - `ertftm070` — the first installable release of the driver:
12
+ - `Display` API: `fill`, `fill_rect`, `set_pixel`, `image` (Pillow),
13
+ `rotation`, `backlight`, `sleep`/`wake`, `selftest`, `gramcheck`
14
+ - Optional C extension (`_fastio`, port of the verified `fill.c` pixel
15
+ blaster) with runtime spin calibration; automatic pure-Python fallback
16
+ - `ertftm070` command-line tool: `selftest`, `bars`, `fill`, `image`, `gramcheck`
17
+ - Off-hardware unit test suite, CI, and trusted PyPI publishing
18
+ - Touch: `ertftm070.touch` — FT5x06-family driver (FT5206 on V2.1,
19
+ FT5316 on V3) over a stdlib-only I²C layer (`_i2c.py`, no smbus), with
20
+ configurable calibration, INT//RST support, and the wake-on-touch
21
+ primitive `Touch.wait_touch()`. Verified on hardware 2026-09-06:
22
+ multitouch works; the panel's raw span measures panel-native
23
+ 0..799 × 0..479 (not 0..4095) — that span is the default calibration;
24
+ INT polarity is firmware-dependent (measured: idle low, high during
25
+ touches — opposite of the datasheet's active-low convention), so
26
+ `wait_touch` treats any INT change as an event, confirmed on
27
+ TD_STATUS with a ~200 ms safety poll
28
+ - TE vsync: `Display.vsync_wait()` and `vsync=` on `fill_rect`/`image`
29
+ pace row bursts into vertical blanking for tear-free updates (opt-in)
30
+ - Refresh measurement: `Display.refresh_rate()` derives the pixel clock
31
+ and frame rate from the TE period and the init table's scan totals;
32
+ `crystal_guess()` names the crystal (10 vs 12 MHz — see
33
+ docs/COMMUNITY-RESEARCH.md §2). Verified on hardware 2026-09-06:
34
+ **53.7 Hz / ~26.2 MHz → 10 MHz crystal** (the `0xE7` register returns
35
+ the FPR value, not a frequency, and is not used)
36
+ - `Bus.pin_read()` in both backends and the C extension; `Pins.te`;
37
+ init now enables the tearing effect (`0x35 = 0x00`)
38
+ - CLI: `ertftm070 refresh` and `ertftm070 touch-test`
39
+ - Examples: `touch_paint.py`, `vsync_demo.py`, `wake_on_touch.py`
40
+ - Docs: `docs/COMMUNITY-RESEARCH.md` (the issue #5 research sweep),
41
+ touch/TE wiring table, UART-conflict note
42
+
43
+ ### Changed
44
+
45
+ - `fill_rect`/`image`/`set_pixel` write one row per burst through a single
46
+ backend call (`Bus.row_blit` — one C call per row on the fast backend
47
+ instead of the ~27 Python↔C round trips per row, which dominated
48
+ per-row cost: a full-screen fill on a Pi Zero W dropped from ~1.5 s to
49
+ ~0.6 s). The bytes and WR strobes at the pins are unchanged; a custom
50
+ `Bus` passed to `Display(backend=...)` must now implement `row_blit`.
51
+
52
+ ### Fixed
53
+
54
+ - `Touch.open()`/`reset()` now drive the `/RST` pin with a real low pulse
55
+ (`open()` previously only set it high) and then poll TD_STATUS for up to
56
+ ~5 s until the FT5x06 leaves its phantom power-on state — a status
57
+ claiming five touches whose records carry impossible finger ids (> 4)
58
+ and frozen garbage coordinates, which a write-0 to `0x02` or a `/RST`
59
+ pulse alone does not clear (bench-verified 2026-09-06). A timeout logs
60
+ a one-time warning per episode (the latch re-arms once the chip is next
61
+ seen sane) and continues — the chip usually self-recovers within
62
+ minutes — and reads/wakes stay clean anyway: `read()` drops the
63
+ phantom's impossible-id points and `wait_touch()` does not wake on
64
+ them.
65
+ - I2C transfers are bounded best-effort: `_i2c.I2C.open()` sets the
66
+ i2c-dev `I2C_TIMEOUT` ioctl (100 × 10 ms = 1 s per transfer) where the
67
+ adapter accepts it (the Pi's i2c-bcm2835 times single transfers
68
+ against it), so a wedged FT5x06 holding the bus cannot stall the
69
+ process indefinitely there. The ioctl is not verified and an adapter
70
+ that ignores it keeps its own default.
71
+
72
+ ## [0.0.0] — 2026-09-05
73
+
74
+ Hardware bring-up era (pre-package):
75
+
76
+ - Verified 16-bit 8080 wiring, init sequence, and C pixel blaster for the
77
+ ER-TFTM070-4V2.1 on a Raspberry Pi Zero W — see `legacy/` and `docs/LESSONS.md`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Immanuel Dorneich
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,12 @@
1
+ # The sdist should carry only the package sources, README and LICENSE.
2
+ # setuptools_scm adds every git-tracked file by default, and a scratch
3
+ # setup.cfg appears transiently during the build — keep all of that out.
4
+ prune .github
5
+ prune dist
6
+ prune docs
7
+ prune examples
8
+ prune legacy
9
+ prune tests
10
+ prune .venv*
11
+ recursive-exclude src *.egg-info*
12
+ exclude setup.cfg
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.4
2
+ Name: ertftm070
3
+ Version: 0.1.0
4
+ Summary: Python driver for the EastRising ER-TFTM070-4V2.1 7-inch TFT (SSD1963) on Raspberry Pi
5
+ Author: Immanuel Dorneich
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/DorneichI/er-tftm070-4-driver
8
+ Project-URL: Documentation, https://github.com/DorneichI/er-tftm070-4-driver#readme
9
+ Project-URL: Repository, https://github.com/DorneichI/er-tftm070-4-driver
10
+ Project-URL: Issues, https://github.com/DorneichI/er-tftm070-4-driver/issues
11
+ Keywords: ssd1963,tft,display,lcd,raspberry-pi,8080
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: C
22
+ Classifier: Topic :: System :: Hardware
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: pillow
27
+ Requires-Dist: Pillow>=9.1; extra == "pillow"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+ Requires-Dist: ruff>=0.6; extra == "dev"
31
+ Requires-Dist: Pillow>=9.1; extra == "dev"
32
+ Requires-Dist: build; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # ertftm070
36
+
37
+ **A Python driver for the EastRising ER-TFTM070-4V2.1** — 7.0" TFT,
38
+ 800×480, SSD1963 controller — on a Raspberry Pi Zero/1/2/3/4.
39
+
40
+ ```bash
41
+ pip install ertftm070
42
+ ```
43
+
44
+ Open the display in three lines, fill the screen in ~0.6 s, blit Pillow
45
+ images, read the touch panel later (roadmap). Verified on hardware
46
+ (Pi Zero W, Raspberry Pi OS, September 2026); zero runtime dependencies
47
+ in the core.
48
+
49
+ [![PyPI](https://img.shields.io/pypi/v/ertftm070)](https://pypi.org/project/ertftm070/)
50
+ [![Python](https://img.shields.io/pypi/pyversions/ertftm070)](https://pypi.org/project/ertftm070/)
51
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
52
+ [![CI](https://github.com/DorneichI/er-tftm070-4-driver/actions/workflows/ci.yml/badge.svg)](https://github.com/DorneichI/er-tftm070-4-driver/actions/workflows/ci.yml)
53
+
54
+ > # ⚠️ THIS PROJECT IS COMPLETELY VIBECODED ⚠️
55
+ >
56
+ > **No human sat down and wrote this codebase. It was written by
57
+ > DeepSeek (the AI model, running in the Claude Code CLI) in a
58
+ > vibe-coding session, with a human in the loop whose job was watching
59
+ > the screen, describing what was wrong, and demanding better.**
60
+ >
61
+ > **The good part:** it genuinely works. Every feature was tested on
62
+ > real hardware — the display was on a Raspberry Pi Zero W for every
63
+ > release candidate, and the human confirmed the pixels with their own
64
+ > eyes. 89 automated tests pass, CI is green, and the ugly hardware
65
+ > quirks are documented instead of hidden.
66
+ >
67
+ > **The honest part:** no one has audited every line. There may be bugs
68
+ > nobody has stepped on yet, design choices a real engineer would
69
+ > question, and comments that overestimate their own cleverness. Treat
70
+ > it accordingly: check the code before you trust your life (or your
71
+ > graduation project) to it.
72
+ >
73
+ > MIT license, no warranty, no guarantees. If your display does
74
+ > something you don't like, [open an issue](https://github.com/DorneichI/er-tftm070-4-driver/issues)
75
+ > and a robot will be with you eventually — whenever its human feels
76
+ > like it.
77
+
78
+ ---
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pip install ertftm070 # the core
84
+ pip install ertftm070[Pillow] # + Pillow, for the image API
85
+ ```
86
+
87
+ `pip` compiles a tiny C extension at install time (the fast pixel path,
88
+ ~17× faster than the pure-Python fallback; see the timing table below).
89
+ No compiler on the machine?
90
+ The install still succeeds and the fallback takes over — slower, fully
91
+ functional, with a warning telling you so.
92
+
93
+ ## Quick start
94
+
95
+ Wire the display as described in [docs/WIRING.md](docs/WIRING.md), then:
96
+
97
+ ```python
98
+ from ertftm070 import Display
99
+
100
+ with Display() as lcd: # opens the bus, inits, backlight on
101
+ lcd.fill(0xF800) # red screen, ~0.6 s
102
+
103
+ lcd.fill_rect(10, 10, 100, 50, 0x07E0) # partial update
104
+ lcd.image(pil_image, x=20, y=20) # blit a Pillow image
105
+ lcd.rotation = 90
106
+ lcd.backlight(False)
107
+ lcd.sleep()
108
+ lcd.wake()
109
+
110
+ pclk_khz, hz = lcd.refresh_rate() # measured clocks (needs the TE wire)
111
+
112
+ # leaving the with-block turns the backlight off and releases the GPIOs
113
+ ```
114
+
115
+ Command-line equivalent, no code required:
116
+
117
+ ```bash
118
+ ertftm070 selftest # verify the wiring against the SSD1963 itself
119
+ ertftm070 bars # 8 color bars, the classic test pattern
120
+ ertftm070 fill F800 # solid fill
121
+ ertftm070 image photo.png # show an image
122
+ ertftm070 gramcheck # write + read back GRAM (pixel-path proof)
123
+ ertftm070 refresh # measure PCLK + frame rate — names the crystal (needs TE)
124
+ ertftm070 touch-test # stream touches until Ctrl+C (needs the touch wires)
125
+ ```
126
+
127
+ The picture drawn by `bars`/`fill`/`image` stays on screen until Ctrl+C
128
+ (which turns the backlight off and releases the GPIOs); pass `--once`
129
+ to exit immediately instead. `touch-test` also holds until Ctrl+C. The
130
+ diagnostics run once and exit with their verdict (0 = passed). Global
131
+ options like `--once` and `--rotation` work before or after the
132
+ subcommand.
133
+
134
+ Or from a source checkout: `python3 examples/color_bars.py`,
135
+ `python3 examples/show_image.py photo.png`,
136
+ `python3 examples/small_demo.py`.
137
+
138
+ ## The API
139
+
140
+ | What | How |
141
+ |---|---|
142
+ | Fill screen / rectangle | `lcd.fill(rgb565)` · `lcd.fill_rect(x, y, w, h, rgb565)` |
143
+ | Single pixel | `lcd.set_pixel(x, y, rgb565)` |
144
+ | Images | `lcd.image(pil_image, x=0, y=0, fit=False)` — draw text/shapes/UI in Pillow first; `fit=True` scales to the (possibly rotated) screen |
145
+ | Rotation | `lcd.rotation = 0 / 90 / 180 / 270` |
146
+ | Backlight | `lcd.backlight(True / False)` |
147
+ | Power | `lcd.sleep()` · `lcd.wake()` |
148
+ | Tear-free updates | `vsync=True` on `fill_rect`/`image` — rows paced into vertical blanking (needs the TE wire; opt-in, see the docstring) |
149
+ | Measured clocks | `lcd.refresh_rate()` → `(pclk_khz, hz)` · `lcd.vsync_wait()` |
150
+ | Touch | `Touch(lcd.bus)` — `read(mapped=True)`, `wait_touch()`, `reset()` |
151
+ | Diagnostics | `lcd.selftest()` · `lcd.gramcheck()` (both return bool) |
152
+ | Colors | `rgb565(r, g, b)` → 16-bit 565 word |
153
+
154
+ Everything is configurable: `Display(pins=…, init_table=…, rotation=…,
155
+ write_passes=…)`. Defaults are the hardware-verified values. Full
156
+ reference in [docs/API.md](docs/API.md).
157
+
158
+ ## The one fact that changes everything
159
+
160
+ **This display board is strapped for 16-bit 8080, not 8-bit.**
161
+
162
+ The board's R3/R4 jumpers select *8080-vs-6800* — **not** the bus width.
163
+ Most public example code targets the 8-bit configuration, and nothing
164
+ will look right until you wire DB8–DB15 and write one 16-bit pixel per
165
+ write strobe. This driver does that out of the box.
166
+
167
+ The sneaky part: the SSD1963 does register access over D[7:0] *regardless
168
+ of bus width*, so every register self-test passes in both modes. Only the
169
+ pixel path reveals the truth — a horizontally doubled, striped image.
170
+ That's exactly what `lcd.gramcheck()` is for. The full story:
171
+ [docs/LESSONS.md](docs/LESSONS.md).
172
+
173
+ ## Supported hardware
174
+
175
+ | Machine | Status |
176
+ |---|---|
177
+ | Raspberry Pi Zero W | ✅ verified — the reference platform |
178
+ | Raspberry Pi Zero / 1 / 2 / 3 / 4 | expected to work (same GPIO block); strobe timing self-calibrates per CPU |
179
+ | Raspberry Pi 5 | ❌ not supported — RP1 GPIO controller, different registers. A libgpiod backend is on the roadmap |
180
+ | Anything else | imports fine; `Display()` raises `NotOnRaspberryPi` with an explanation |
181
+
182
+ The display itself is plain 16-bit 8080 — Arduinos, ESP32s and friends
183
+ drive it too (that's where the init tables came from). Only this Python
184
+ package is Pi-specific.
185
+
186
+ ## Performance
187
+
188
+ Full-screen fill (800×480×16-bit), measured on a Pi Zero W:
189
+
190
+ - **~0.6 s** with the C extension (direct `/dev/gpiomem` register writes,
191
+ no syscalls in the hot loop, one C call per row)
192
+ - **~10 s** with the pure-Python fallback
193
+ - (For context: RPi.GPIO bit-banging manages ~460 px/s — the C path is
194
+ ~1400× faster, which is why the extension exists.)
195
+
196
+ `ertftm070.BACKEND` tells you which path is active (`"fast"`/`"slow"`);
197
+ `ERTFTM070_FORCE_SLOW=1` forces the fallback. If a row ever shows a
198
+ shifted pixel on your particular Pi (see LESSONS.md — the SSD1963
199
+ occasionally swallows a write strobe), `Display(write_passes=2)` heals
200
+ most of it at ~2× the time.
201
+
202
+ ## Touch
203
+
204
+ The V2.1 board ships with a **capacitive touch panel mounted by default**
205
+ and its controller (FocalTech FT5206-family) broken out on the display
206
+ connector: pins 33–37 carry `/RST`, `SCL`, `SDA`, `INT`, `WAKE` (pin 38
207
+ is the resistive-pen ground, unused). It speaks I²C, so the Pi can talk
208
+ to it with **no extra hardware** — just SCL/SDA to Pi GPIO 3/2, `/RST`
209
+ and `INT` to any two free GPIOs, and I²C enabled.
210
+
211
+ Touch is implemented: `ertftm070.touch` ships with v1.0. See
212
+ [docs/WIRING.md](docs/WIRING.md) for the pin details and
213
+ [docs/COMMUNITY-RESEARCH.md](docs/COMMUNITY-RESEARCH.md) §6 for the
214
+ controller facts: the V2.1 board ships the FT5206, the current V3 board
215
+ the FT5316 (same FT5x06 register map), I²C address 0x38, and the driver
216
+ needs almost no init — just timing and a dummy first read.
217
+
218
+ ```python
219
+ from ertftm070 import Display
220
+ from ertftm070.touch import Touch
221
+
222
+ with Display() as lcd, Touch(lcd.bus) as touch:
223
+ touch.wait_touch() # wake-on-touch primitive
224
+ for p in touch.read(mapped=True): # panel-native 800x480 coordinates
225
+ x = min(p.x, lcd.width - 4) # mapped points can sit right at the
226
+ y = min(p.y, lcd.height - 4) # edge — keep the mark on screen
227
+ lcd.fill_rect(x, y, 4, 4, 0xFFFF)
228
+ ```
229
+
230
+ `mapped=True` points live in the panel-native frame — the frame the
231
+ display draws at `rotation=0`. On a rotated display, pass them through
232
+ `Display.unmap_point` first (see the touch module docs).
233
+
234
+ ## Roadmap
235
+
236
+ - Backlight dimming — software PWM on the backlight pin
237
+ (register-based dimming via `0xBE`/`0xD0` would need the J3/J4 jumper
238
+ mod — see docs/COMMUNITY-RESEARCH.md §5)
239
+ - Hardware vertical scroll (SSD1963 `0x33`/`0x37`)
240
+ - GRAM screenshots — read the framebuffer back into a PIL image
241
+ - Pi 5 support via a libgpiod backend
242
+
243
+ Text, shapes and UI widgets are deliberately **not** part of the driver —
244
+ draw them in Pillow and blit with `lcd.image()`. It's the same pattern
245
+ at 60× less code.
246
+
247
+ ## Documentation
248
+
249
+ | Doc | What it is |
250
+ |---|---|
251
+ | [docs/WIRING.md](docs/WIRING.md) | The full wiring table — one table, every pin |
252
+ | [docs/INIT-SEQUENCE.md](docs/INIT-SEQUENCE.md) | The exact register sequence and why each register matters |
253
+ | [docs/LESSONS.md](docs/LESSONS.md) | Everything learned the hard way — read this before debugging |
254
+ | [docs/API.md](docs/API.md) | API reference |
255
+ | [docs/RELEASING.md](docs/RELEASING.md) | How releases are cut and published |
256
+ | [legacy/](legacy/) | The original verified bring-up scripts (unchanged baseline) |
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ pip install -e ".[dev]"
262
+ pytest # 89 tests, no hardware needed (a fake bus stands in)
263
+ ruff check src tests examples
264
+ ```
265
+
266
+ CI runs lint + tests + builds on every push, CodeQL scans the Python,
267
+ and the main branch is protected: merges go through pull requests,
268
+ squash-only, and only when CI is green. Dependabot keeps the handful of
269
+ dev dependencies fresh. See [SECURITY.md](SECURITY.md) to report a
270
+ vulnerability privately, and [docs/RELEASING.md](docs/RELEASING.md) for
271
+ how a version tag becomes a PyPI release.
272
+
273
+ Contributions are welcome — same rules as everything else here: open a
274
+ PR, keep CI green, and yes, feel free to have an AI write it.
275
+
276
+ ## License
277
+
278
+ [MIT](LICENSE) © 2026 Immanuel Dorneich
@@ -0,0 +1,244 @@
1
+ # ertftm070
2
+
3
+ **A Python driver for the EastRising ER-TFTM070-4V2.1** — 7.0" TFT,
4
+ 800×480, SSD1963 controller — on a Raspberry Pi Zero/1/2/3/4.
5
+
6
+ ```bash
7
+ pip install ertftm070
8
+ ```
9
+
10
+ Open the display in three lines, fill the screen in ~0.6 s, blit Pillow
11
+ images, read the touch panel later (roadmap). Verified on hardware
12
+ (Pi Zero W, Raspberry Pi OS, September 2026); zero runtime dependencies
13
+ in the core.
14
+
15
+ [![PyPI](https://img.shields.io/pypi/v/ertftm070)](https://pypi.org/project/ertftm070/)
16
+ [![Python](https://img.shields.io/pypi/pyversions/ertftm070)](https://pypi.org/project/ertftm070/)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
18
+ [![CI](https://github.com/DorneichI/er-tftm070-4-driver/actions/workflows/ci.yml/badge.svg)](https://github.com/DorneichI/er-tftm070-4-driver/actions/workflows/ci.yml)
19
+
20
+ > # ⚠️ THIS PROJECT IS COMPLETELY VIBECODED ⚠️
21
+ >
22
+ > **No human sat down and wrote this codebase. It was written by
23
+ > DeepSeek (the AI model, running in the Claude Code CLI) in a
24
+ > vibe-coding session, with a human in the loop whose job was watching
25
+ > the screen, describing what was wrong, and demanding better.**
26
+ >
27
+ > **The good part:** it genuinely works. Every feature was tested on
28
+ > real hardware — the display was on a Raspberry Pi Zero W for every
29
+ > release candidate, and the human confirmed the pixels with their own
30
+ > eyes. 89 automated tests pass, CI is green, and the ugly hardware
31
+ > quirks are documented instead of hidden.
32
+ >
33
+ > **The honest part:** no one has audited every line. There may be bugs
34
+ > nobody has stepped on yet, design choices a real engineer would
35
+ > question, and comments that overestimate their own cleverness. Treat
36
+ > it accordingly: check the code before you trust your life (or your
37
+ > graduation project) to it.
38
+ >
39
+ > MIT license, no warranty, no guarantees. If your display does
40
+ > something you don't like, [open an issue](https://github.com/DorneichI/er-tftm070-4-driver/issues)
41
+ > and a robot will be with you eventually — whenever its human feels
42
+ > like it.
43
+
44
+ ---
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install ertftm070 # the core
50
+ pip install ertftm070[Pillow] # + Pillow, for the image API
51
+ ```
52
+
53
+ `pip` compiles a tiny C extension at install time (the fast pixel path,
54
+ ~17× faster than the pure-Python fallback; see the timing table below).
55
+ No compiler on the machine?
56
+ The install still succeeds and the fallback takes over — slower, fully
57
+ functional, with a warning telling you so.
58
+
59
+ ## Quick start
60
+
61
+ Wire the display as described in [docs/WIRING.md](docs/WIRING.md), then:
62
+
63
+ ```python
64
+ from ertftm070 import Display
65
+
66
+ with Display() as lcd: # opens the bus, inits, backlight on
67
+ lcd.fill(0xF800) # red screen, ~0.6 s
68
+
69
+ lcd.fill_rect(10, 10, 100, 50, 0x07E0) # partial update
70
+ lcd.image(pil_image, x=20, y=20) # blit a Pillow image
71
+ lcd.rotation = 90
72
+ lcd.backlight(False)
73
+ lcd.sleep()
74
+ lcd.wake()
75
+
76
+ pclk_khz, hz = lcd.refresh_rate() # measured clocks (needs the TE wire)
77
+
78
+ # leaving the with-block turns the backlight off and releases the GPIOs
79
+ ```
80
+
81
+ Command-line equivalent, no code required:
82
+
83
+ ```bash
84
+ ertftm070 selftest # verify the wiring against the SSD1963 itself
85
+ ertftm070 bars # 8 color bars, the classic test pattern
86
+ ertftm070 fill F800 # solid fill
87
+ ertftm070 image photo.png # show an image
88
+ ertftm070 gramcheck # write + read back GRAM (pixel-path proof)
89
+ ertftm070 refresh # measure PCLK + frame rate — names the crystal (needs TE)
90
+ ertftm070 touch-test # stream touches until Ctrl+C (needs the touch wires)
91
+ ```
92
+
93
+ The picture drawn by `bars`/`fill`/`image` stays on screen until Ctrl+C
94
+ (which turns the backlight off and releases the GPIOs); pass `--once`
95
+ to exit immediately instead. `touch-test` also holds until Ctrl+C. The
96
+ diagnostics run once and exit with their verdict (0 = passed). Global
97
+ options like `--once` and `--rotation` work before or after the
98
+ subcommand.
99
+
100
+ Or from a source checkout: `python3 examples/color_bars.py`,
101
+ `python3 examples/show_image.py photo.png`,
102
+ `python3 examples/small_demo.py`.
103
+
104
+ ## The API
105
+
106
+ | What | How |
107
+ |---|---|
108
+ | Fill screen / rectangle | `lcd.fill(rgb565)` · `lcd.fill_rect(x, y, w, h, rgb565)` |
109
+ | Single pixel | `lcd.set_pixel(x, y, rgb565)` |
110
+ | Images | `lcd.image(pil_image, x=0, y=0, fit=False)` — draw text/shapes/UI in Pillow first; `fit=True` scales to the (possibly rotated) screen |
111
+ | Rotation | `lcd.rotation = 0 / 90 / 180 / 270` |
112
+ | Backlight | `lcd.backlight(True / False)` |
113
+ | Power | `lcd.sleep()` · `lcd.wake()` |
114
+ | Tear-free updates | `vsync=True` on `fill_rect`/`image` — rows paced into vertical blanking (needs the TE wire; opt-in, see the docstring) |
115
+ | Measured clocks | `lcd.refresh_rate()` → `(pclk_khz, hz)` · `lcd.vsync_wait()` |
116
+ | Touch | `Touch(lcd.bus)` — `read(mapped=True)`, `wait_touch()`, `reset()` |
117
+ | Diagnostics | `lcd.selftest()` · `lcd.gramcheck()` (both return bool) |
118
+ | Colors | `rgb565(r, g, b)` → 16-bit 565 word |
119
+
120
+ Everything is configurable: `Display(pins=…, init_table=…, rotation=…,
121
+ write_passes=…)`. Defaults are the hardware-verified values. Full
122
+ reference in [docs/API.md](docs/API.md).
123
+
124
+ ## The one fact that changes everything
125
+
126
+ **This display board is strapped for 16-bit 8080, not 8-bit.**
127
+
128
+ The board's R3/R4 jumpers select *8080-vs-6800* — **not** the bus width.
129
+ Most public example code targets the 8-bit configuration, and nothing
130
+ will look right until you wire DB8–DB15 and write one 16-bit pixel per
131
+ write strobe. This driver does that out of the box.
132
+
133
+ The sneaky part: the SSD1963 does register access over D[7:0] *regardless
134
+ of bus width*, so every register self-test passes in both modes. Only the
135
+ pixel path reveals the truth — a horizontally doubled, striped image.
136
+ That's exactly what `lcd.gramcheck()` is for. The full story:
137
+ [docs/LESSONS.md](docs/LESSONS.md).
138
+
139
+ ## Supported hardware
140
+
141
+ | Machine | Status |
142
+ |---|---|
143
+ | Raspberry Pi Zero W | ✅ verified — the reference platform |
144
+ | Raspberry Pi Zero / 1 / 2 / 3 / 4 | expected to work (same GPIO block); strobe timing self-calibrates per CPU |
145
+ | Raspberry Pi 5 | ❌ not supported — RP1 GPIO controller, different registers. A libgpiod backend is on the roadmap |
146
+ | Anything else | imports fine; `Display()` raises `NotOnRaspberryPi` with an explanation |
147
+
148
+ The display itself is plain 16-bit 8080 — Arduinos, ESP32s and friends
149
+ drive it too (that's where the init tables came from). Only this Python
150
+ package is Pi-specific.
151
+
152
+ ## Performance
153
+
154
+ Full-screen fill (800×480×16-bit), measured on a Pi Zero W:
155
+
156
+ - **~0.6 s** with the C extension (direct `/dev/gpiomem` register writes,
157
+ no syscalls in the hot loop, one C call per row)
158
+ - **~10 s** with the pure-Python fallback
159
+ - (For context: RPi.GPIO bit-banging manages ~460 px/s — the C path is
160
+ ~1400× faster, which is why the extension exists.)
161
+
162
+ `ertftm070.BACKEND` tells you which path is active (`"fast"`/`"slow"`);
163
+ `ERTFTM070_FORCE_SLOW=1` forces the fallback. If a row ever shows a
164
+ shifted pixel on your particular Pi (see LESSONS.md — the SSD1963
165
+ occasionally swallows a write strobe), `Display(write_passes=2)` heals
166
+ most of it at ~2× the time.
167
+
168
+ ## Touch
169
+
170
+ The V2.1 board ships with a **capacitive touch panel mounted by default**
171
+ and its controller (FocalTech FT5206-family) broken out on the display
172
+ connector: pins 33–37 carry `/RST`, `SCL`, `SDA`, `INT`, `WAKE` (pin 38
173
+ is the resistive-pen ground, unused). It speaks I²C, so the Pi can talk
174
+ to it with **no extra hardware** — just SCL/SDA to Pi GPIO 3/2, `/RST`
175
+ and `INT` to any two free GPIOs, and I²C enabled.
176
+
177
+ Touch is implemented: `ertftm070.touch` ships with v1.0. See
178
+ [docs/WIRING.md](docs/WIRING.md) for the pin details and
179
+ [docs/COMMUNITY-RESEARCH.md](docs/COMMUNITY-RESEARCH.md) §6 for the
180
+ controller facts: the V2.1 board ships the FT5206, the current V3 board
181
+ the FT5316 (same FT5x06 register map), I²C address 0x38, and the driver
182
+ needs almost no init — just timing and a dummy first read.
183
+
184
+ ```python
185
+ from ertftm070 import Display
186
+ from ertftm070.touch import Touch
187
+
188
+ with Display() as lcd, Touch(lcd.bus) as touch:
189
+ touch.wait_touch() # wake-on-touch primitive
190
+ for p in touch.read(mapped=True): # panel-native 800x480 coordinates
191
+ x = min(p.x, lcd.width - 4) # mapped points can sit right at the
192
+ y = min(p.y, lcd.height - 4) # edge — keep the mark on screen
193
+ lcd.fill_rect(x, y, 4, 4, 0xFFFF)
194
+ ```
195
+
196
+ `mapped=True` points live in the panel-native frame — the frame the
197
+ display draws at `rotation=0`. On a rotated display, pass them through
198
+ `Display.unmap_point` first (see the touch module docs).
199
+
200
+ ## Roadmap
201
+
202
+ - Backlight dimming — software PWM on the backlight pin
203
+ (register-based dimming via `0xBE`/`0xD0` would need the J3/J4 jumper
204
+ mod — see docs/COMMUNITY-RESEARCH.md §5)
205
+ - Hardware vertical scroll (SSD1963 `0x33`/`0x37`)
206
+ - GRAM screenshots — read the framebuffer back into a PIL image
207
+ - Pi 5 support via a libgpiod backend
208
+
209
+ Text, shapes and UI widgets are deliberately **not** part of the driver —
210
+ draw them in Pillow and blit with `lcd.image()`. It's the same pattern
211
+ at 60× less code.
212
+
213
+ ## Documentation
214
+
215
+ | Doc | What it is |
216
+ |---|---|
217
+ | [docs/WIRING.md](docs/WIRING.md) | The full wiring table — one table, every pin |
218
+ | [docs/INIT-SEQUENCE.md](docs/INIT-SEQUENCE.md) | The exact register sequence and why each register matters |
219
+ | [docs/LESSONS.md](docs/LESSONS.md) | Everything learned the hard way — read this before debugging |
220
+ | [docs/API.md](docs/API.md) | API reference |
221
+ | [docs/RELEASING.md](docs/RELEASING.md) | How releases are cut and published |
222
+ | [legacy/](legacy/) | The original verified bring-up scripts (unchanged baseline) |
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ pip install -e ".[dev]"
228
+ pytest # 89 tests, no hardware needed (a fake bus stands in)
229
+ ruff check src tests examples
230
+ ```
231
+
232
+ CI runs lint + tests + builds on every push, CodeQL scans the Python,
233
+ and the main branch is protected: merges go through pull requests,
234
+ squash-only, and only when CI is green. Dependabot keeps the handful of
235
+ dev dependencies fresh. See [SECURITY.md](SECURITY.md) to report a
236
+ vulnerability privately, and [docs/RELEASING.md](docs/RELEASING.md) for
237
+ how a version tag becomes a PyPI release.
238
+
239
+ Contributions are welcome — same rules as everything else here: open a
240
+ PR, keep CI green, and yes, feel free to have an AI write it.
241
+
242
+ ## License
243
+
244
+ [MIT](LICENSE) © 2026 Immanuel Dorneich
@@ -0,0 +1,25 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest release published to PyPI is the only supported version.
6
+ Issues are fixed forward; there are no backports.
7
+
8
+ ## Reporting a vulnerability
9
+
10
+ Please use GitHub's private vulnerability reporting so a fix can land
11
+ before the issue is public:
12
+
13
+ <https://github.com/DorneichI/er-tftm070-4-driver/security/advisories/new>
14
+
15
+ In scope: anything a user can trigger through the package — memory
16
+ safety in the C extension (`src/ertftm070/_fastio.c`), buffer handling
17
+ in the Python backends, and paths where untrusted input (image files,
18
+ CLI arguments) reaches the bus.
19
+
20
+ Out of scope: issues that require physical access to the device's GPIO
21
+ pins — no software can prevent those.
22
+
23
+ There is no response SLA. Reports are handled on a best-effort basis,
24
+ whenever the human gets to them — which may be a while. Do not count on
25
+ a prompt fix.