src.nth.io/

summaryrefslogtreecommitdiff
path: root/TESTING.md
blob: dd17d94491117bfc5e4f8a9a4010454daf8be457 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# Testing & Verification

This file tracks which milestones have been verified on hardware and how. Per-milestone entries below each list:

- **Acceptance** β€” what "done" means (mirrors the impl guide).
- **How to verify** β€” exact commands or actions.
- **Status** β€” ⬜ not verified / 🟑 partial / βœ… verified, with a one-line note (date, board rev, anything weird).

Hardware-required milestones can't be verified by a clean build alone β€” a checked-in build success counts as 🟑 (compiles, not yet run).

---

## M1 β€” Board Bring-Up

**Acceptance**: device gets a DHCP lease over Ethernet and logs its IP.

**How to verify**

```bash
source ~/Dev/code/git/esp32/env.sh
idf.py -p /dev/cu.usbmodem* flash monitor
```

Expected log within ~5–10s of boot:

```
I (xxx) viewport: Scrypted Viewport boot
I (xxx) net_eth: ethernet driver started, waiting for link + DHCP
I (xxx) net_eth: link up, mac xx:xx:xx:xx:xx:xx
I (xxx) net_eth: got ip 192.168.x.x  gw 192.168.x.1  netmask 255.255.255.0
I (xxx) viewport: online at 192.168.x.x
```

Cross-check from another host:

```bash
ping 192.168.x.x
```

**Status**: 🟑 builds clean against ESP-IDF 5.4 (commit e2ac22e). Not yet flashed to hardware β€” awaiting first board run.

> Combined hardware verification of M1+M2 is fine β€” they both run from the same flash session.

---

## M2 β€” HTTP + mDNS

**Acceptance**: `GET /state` returns JSON; mDNS service is discoverable.

**How to verify**

After flash, from a host on the same LAN:

```bash
# Direct call by IP (always works):
curl http://<device-ip>/state | jq .

# mDNS hostname (requires OS-level .local resolution: macOS Bonjour, Linux nss-mdns):
curl http://viewport.local/state | jq .

# Service discovery browse (preferred β€” what Scrypted will do, no OS .local resolver needed):
dns-sd -B _scrypted-viewport._tcp local.        # macOS
avahi-browse -r _scrypted-viewport._tcp         # Linux
```

Expected `/state` body on a fresh device (unconfigured):

```json
{
  "name": null,
  "version": "0.1.0",
  "configured": false,
  "state": "unconfigured",
  "uptime_ms": 12345,
  "last_frame_ms_ago": null,
  "frames_received": 0,
  "decode_errors": 0,
  "state_post_failures": 0,
  "resolution": "480x800",
  "ip": "192.168.x.x",
  "free_heap": 200000,
  "free_psram": 30000000
}
```

Expected mDNS browse output should show a `_scrypted-viewport._tcp` instance with TXT records `version=`, `resolution=`, `orientation=`, `name=` (empty for `name`).

**Status**: 🟑 builds clean against ESP-IDF 5.4 with espressif/mdns managed component. Awaiting first board run.

---

## M3 β€” Display Bring-Up

**Acceptance**: panel powers on, MCU at I2C 0x45 responds, color-bar test pattern renders, brightness control works.

**Wiring** (Hosyond 5" 800x480 panel ↔ Waveshare ESP32-P4-ETH):

The DSI FPC adapter (15-pin Pi side β†’ 22-pin Waveshare side) carries the DSI data lanes and power. I2C and panel power are jumpered separately from the ESP32-P4 board to the Hosyond's auxiliary header (the same header Pi users normally connect to the Pi's 40-pin GPIO):

| Hosyond panel pin | Wire to ESP32-P4 board |
| --- | --- |
| 5V | board 5V rail |
| GND | board GND |
| SDA | GPIO 7 |
| SCL | GPIO 8 |

GPIO 7/8 match Waveshare's BSP convention for touch I2C on their bundled-panel kit. If different pins are more convenient, change `PIN_I2C_SDA` / `PIN_I2C_SCL` in `display.c`.

**Bring-up sequence**

1. Power the ESP32-P4 board over USB (don't plug DSI yet).
2. Connect 5V + GND jumpers to the panel. Panel LED (if present) should light.
3. Connect SDA + SCL jumpers.
4. Plug the DSI FPC cable.
5. Flash:

```bash
idf.py -p /dev/cu.usbmodem* flash monitor
```

**Expected log**

```
I (xxx) display: panel MCU id 0xC3 β€” Pi 7" architecture ack'd
I (xxx) display: panel powered on
I (xxx) display: DSI up: 800x480 30 MHz, 2-lane 480 Mbps
I (xxx) viewport: display up β€” test pattern on screen
```

**Visual check**: 8 vertical color bars (white, yellow, cyan, green, magenta, red, blue, black) across the panel. Brightness should look perceptually mid-range (default 80/100 with gamma).

**Failure-mode signals**

- `panel MCU @0x45 unreachable` β†’ jumper or pull-up problem on I2C, no need to suspect DSI yet.
- I2C ack but no image β†’ DSI cable / FPC adapter orientation. Pi FPCs are easy to install upside-down.
- Image but wrong colors β†’ check RGB565 byte order; flip `LCD_COLOR_PIXEL_FORMAT_RGB565` to a variant with swap.
- Image but vertical/horizontal sync issues β†’ adjust the `PANEL_*SYNC_*` timings; Pi 7" canonical values used as defaults.

**Status**: 🟑 builds clean against ESP-IDF 5.4. Driver ported from Linux `panel-raspberrypi-touchscreen.c`. Awaiting hardware bring-up with confirmed jumper wiring.

---

## M4 β€” Config Persistence

**Acceptance**: `POST /config` persists across reboot; partial-update semantics work.

**How to verify**

```bash
# Full config:
curl -X POST -H "Content-Type: application/json" \
  -d '{"viewport":"mudroom","scrypted":"http://host/endpoint/scrypted-viewport","orientation":"landscape"}' \
  http://<device-ip>/config

# Read back:
curl http://<device-ip>/config | jq .

# Partial update (only brightness):
curl -X POST -H "Content-Type: application/json" \
  -d '{"brightness":50}' \
  http://<device-ip>/config

# Reboot, then re-read β€” values should survive:
curl http://<device-ip>/config | jq .
```

Also verify validation:

```bash
# idle_timeout_ms below 5000 (and non-zero) β€” expect 400:
curl -i -X POST -H "Content-Type: application/json" \
  -d '{"idle_timeout_ms":1000}' http://<device-ip>/config

# bogus orientation β€” expect 400:
curl -i -X POST -H "Content-Type: application/json" \
  -d '{"orientation":"sideways"}' http://<device-ip>/config

# brightness out of range β€” expect 400:
curl -i -X POST -H "Content-Type: application/json" \
  -d '{"brightness":150}' http://<device-ip>/config

# scrypted without http:// β€” expect 400:
curl -i -X POST -H "Content-Type: application/json" \
  -d '{"scrypted":"scrypted.local"}' http://<device-ip>/config

# Garbage JSON β€” expect 400:
curl -i -X POST -H "Content-Type: application/json" \
  -d 'not json' http://<device-ip>/config
```

Idle-timer disable (`idle_timeout_ms: 0`) is intentionally allowed.

Side-effects to confirm:
- After `POST /config` with `brightness`: panel brightness changes immediately (if display is up).
- After `POST /config` with `viewport` or `orientation`: mDNS TXT records update; `viewport-<name>.local` resolves; browse shows new TXT.
- After `POST /config` with both `viewport` and `scrypted` (any order, on any subsequent call): `GET /state` shows `configured: true`, `state: "asleep"`.

**Status**: 🟑 builds clean against ESP-IDF 5.4. Logic exercised in code but unverified on hardware.

---

## M5 β€” JPEG Frame Push

**Acceptance**: `POST /frame` paints an 800x480 (or 480x800 portrait) JPEG.

**How to verify**

```bash
# After /config sets state=wake (M6 dep β€” for now test post-/wake or hardcode awake):
curl -X POST -H "Content-Type: image/jpeg" \
  --data-binary @test-480x800.jpg \
  http://<device-ip>/frame
```

Visual: matching pattern appears on the panel. Re-test in landscape orientation with `test-800x480.jpg`.

Negative tests:
- Non-JPEG body β†’ 400.
- 1.1 MB body β†’ 413.
- 640x480 body (wrong size) β†’ 400 or visible distortion (depends on decoder strictness).

**Status**: ⬜ pending.

---

## M6 β€” State + Idle Timer

**Acceptance**: `POST /state` toggles wake/sleep; `/frame` is 409 when asleep; idle timer fires after `idle_timeout_ms`.

**How to verify**

```bash
curl -X POST -d '{"state":"sleep"}' http://<device-ip>/state   # backlight off
curl -X POST -d '{"state":"wake"}'  http://<device-ip>/state   # backlight on, loading

# /frame while asleep:
curl -X POST -d '{"state":"sleep"}' http://<device-ip>/state
curl -i -X POST -H "Content-Type: image/jpeg" \
  --data-binary @test-480x800.jpg \
  http://<device-ip>/frame
# expect: HTTP/1.1 409 Conflict

# Idle timer (assuming default 60000ms):
curl -X POST -d '{"state":"wake"}' http://<device-ip>/state
# wait 65s without sending /frame
curl http://<device-ip>/state | jq .state
# expect: "asleep"
```

**Status**: ⬜ pending.

---

## M7 β€” Touch + Outbound `/state` POST

**Acceptance**: tap toggles wake/sleep locally; device POSTs `{viewport,state}` to `<scrypted>/state`.

**How to verify**

Stand up a test HTTP receiver:

```bash
# In a separate terminal on Scrypted host:
python3 -m http.server 11080  # or a small flask app that logs body
```

Configure the device to point at it:

```bash
curl -X POST -H "Content-Type: application/json" \
  -d '{"viewport":"mudroom","scrypted":"http://<host>:11080"}' \
  http://<device-ip>/config
```

Then:
- Tap while asleep β†’ device wakes, receiver logs `POST /state {"viewport":"mudroom","state":"wake"}`.
- Tap while awake β†’ device sleeps, receiver logs `POST /state {"viewport":"mudroom","state":"sleep"}`.
- Wait `idle_timeout_ms` with no `/frame` β†’ receiver logs same sleep POST.
- Kill receiver, tap β†’ `/state` `state_post_failures` increments.

**Status**: ⬜ pending.

---

## M8 β€” Local Screens + BOOT button

**Acceptance**: IP screen on first boot; loading screen on every wake; BOOT button works.

**How to verify**

- Fresh flash β†’ screen shows `viewport.local` and the device IP centered.
- `POST /config` β†’ IP screen clears; backlight off (device enters sleep).
- Tap while asleep β†’ "Loading…" screen until next `/frame`.
- BOOT short-press (any state) β†’ IP screen overlays for 15s, then prior state restored.
- BOOT 5s-hold β†’ NVS clears, device reboots, IP screen reappears.

**Status**: ⬜ pending.

---

## M9 β€” Live Stream (`POST /stream`)

**Acceptance**: β‰₯ 10 fps multipart MJPEG over a single chunked POST.

**How to verify**: ffmpeg-driven test stream from a host, measure fps from the device's frame counter.

**Status**: ⬜ pending.

---

## Integration & system tests (post-M9)

Run these once all milestones are implemented and individually verified. The point is to exercise races, edge cases, and longevity that single-milestone tests miss.

### A. End-to-end with Scrypted

- Real Scrypted-side Script (M7's scope): doorbell-event β†’ `/state {wake}` β†’ frame stream β†’ idle timer fires β†’ `/state {sleep}` callback β†’ Scrypted stops.
- Bound camera switching: change the binding in Scrypted, confirm next wake routes to the new camera.

### B. Race conditions

- Concurrent tap-while-Scrypted-POSTs-`/state`: spam both, confirm the device converges (mutex serializes; last-write-wins). Use the wake/sleep counters in `/state` to track transitions.
- Mid-stream tap-to-sleep: while Scrypted is streaming frames, tap to sleep. Inflight `/frame` should return 409. Confirm no half-painted frames, no re-wake.
- Idle-timeout coincides with a fresh tap-wake: the second event wins. Verify by repeating with timing jitter.
- Stale Scrypted sleep timer races a fresh wake callback: Scrypted-side `cancelPendingSleep` must work β€” if a stale sleep lands after a wake, viewport sleeps; user taps again; recovery in one extra tap.

### C. Failure modes

- Cable pull mid-frame: device idle-sleeps after `idle_timeout_ms`. On reconnect, mDNS re-advertises; Scrypted re-finds and continues.
- Scrypted unreachable on tap: device still toggles backlight, `state_post_failures` increments. Recovery: Scrypted comes back, next tap syncs.
- DHCP lease change: device gets new IP, re-advertises via mDNS. Scrypted's periodic browse picks up the new address.
- `/state_post_failures` count should be observable via `GET /state`.

### D. Longevity

- 24h soak with a slow camera stream (~1 frame every 10s): verify no memory leaks (`free_heap` / `free_psram` stable in `/state`), no decode_errors growing.
- Frame storm: 30 minutes at max sustainable fps. Verify watchdog never fires, no resets.

### E. Power & boot

- PoE injector cycle: device boots clean, gets DHCP, re-registers via mDNS, no manual intervention needed.
- Brown-out (PoE on edge of spec): device should reboot cleanly, not corrupt NVS.

### F. Negative protocol

- Garbage JSON to `/config` β†’ 400.
- `/frame` with `Content-Type: image/png` β†’ 400.
- `/state` with `{"state":"middle"}` β†’ 400.
- `/frame` with body > 1 MB β†’ 413.
- Two concurrent `/frame` posts β†’ one wins, the other 503.

### G. Multi-viewport

Once at least two physical units are configured: confirm each routes its callback to Scrypted with its own `viewport` name; confirm Scrypted's discovery map has both IPs; confirm a camera event on the wrong-named viewport is correctly ignored on the right one.