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.
- ertftm070-0.1.0/.gitignore +10 -0
- ertftm070-0.1.0/CHANGELOG.md +77 -0
- ertftm070-0.1.0/LICENSE +21 -0
- ertftm070-0.1.0/MANIFEST.in +12 -0
- ertftm070-0.1.0/PKG-INFO +278 -0
- ertftm070-0.1.0/README.md +244 -0
- ertftm070-0.1.0/SECURITY.md +25 -0
- ertftm070-0.1.0/pyproject.toml +69 -0
- ertftm070-0.1.0/setup.cfg +4 -0
- ertftm070-0.1.0/setup.py +27 -0
- ertftm070-0.1.0/src/ertftm070/__init__.py +56 -0
- ertftm070-0.1.0/src/ertftm070/__main__.py +302 -0
- ertftm070-0.1.0/src/ertftm070/_fastio.c +556 -0
- ertftm070-0.1.0/src/ertftm070/_i2c.py +135 -0
- ertftm070-0.1.0/src/ertftm070/backends.py +471 -0
- ertftm070-0.1.0/src/ertftm070/colors.py +132 -0
- ertftm070-0.1.0/src/ertftm070/display.py +801 -0
- ertftm070-0.1.0/src/ertftm070/errors.py +16 -0
- ertftm070-0.1.0/src/ertftm070/init.py +92 -0
- ertftm070-0.1.0/src/ertftm070/pins.py +76 -0
- ertftm070-0.1.0/src/ertftm070/touch.py +560 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/PKG-INFO +278 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/SOURCES.txt +27 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/dependency_links.txt +1 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/entry_points.txt +2 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/requires.txt +9 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/scm_file_list.json +52 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/scm_version.json +8 -0
- ertftm070-0.1.0/src/ertftm070.egg-info/top_level.txt +1 -0
|
@@ -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`.
|
ertftm070-0.1.0/LICENSE
ADDED
|
@@ -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
|
ertftm070-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/ertftm070/)
|
|
50
|
+
[](https://pypi.org/project/ertftm070/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
[](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
|
+
[](https://pypi.org/project/ertftm070/)
|
|
16
|
+
[](https://pypi.org/project/ertftm070/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](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.
|