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
|
# Scrypted Viewport v1 Technical Design Specification
Version: 1.0
## Overview
Scrypted Viewport is an Ethernet-powered ambient display appliance optimized for Scrypted camera and doorbell events.
Design goals:
- No Matter
- No HomeKit
- No polling
- No configuration UI
- No authentication
- No rendering engine
- No business logic on the device
Scrypted owns rendering, overlays, camera selection and interaction logic.
Scrypted Viewport owns Ethernet, JPEG decode, display, touch input and callback delivery.
## Hardware
### Controller
Waveshare ESP32-P4-ETH-POE
### Display
5" 800x480 IPS Capacitive Touch MIPI DSI display
## Boot
Power
-> DHCP
-> mDNS (_scrypted-viewport._tcp.local)
-> Wait
## Resolution
800x480 native.
Scrypted always renders 800x480 JPEGs.
## Network
HTTP listen port: `80`.
mDNS service: `_scrypted-viewport._tcp.local` advertised on port 80.
mDNS TXT records:
- `version=1.0.0`
- `resolution=800x480`
- `name=<display name>` (empty until `/config`)
Hostname: `viewport.local` before configuration, `viewport-<display>.local` after.
Trust model: LAN-only, no auth, no TLS. Deploy on a trusted VLAN.
## API
### GET /health
Returns `200 OK` with JSON:
```json
{
"name": "mudroom",
"version": "1.0.0",
"configured": true,
"uptime_ms": 12345678,
"resolution": "800x480",
"ip": "192.168.1.42",
"free_heap": 123456,
"free_psram": 12345678
}
```
### POST /config
```json
{
"display": "mudroom",
"callback": "http://scrypted.local:11080/api/viewport/touch"
}
```
- Persisted to NVS, survives reboot.
- Idempotent; subsequent calls atomically replace prior config.
- `display` must be non-empty; `callback` must be `http://...`.
- Response: `204 No Content`. Invalid body: `400`.
### POST /frame
- `Content-Type: image/jpeg`, body is raw JPEG bytes.
- Image must be 800x480 baseline JPEG. Device does not scale or letterbox.
- Max size: 1 MB.
- Wakes display (backlight on) and resets the idle timer.
- Single in-flight frame; concurrent posts may be rejected with `503`.
- Returns `204` once decoded and pushed to the panel.
- `400` malformed JPEG, `413` over size, `500` decode/display failure. On failure the previous frame stays on screen.
### POST /sleep
- Backlight off; framebuffer preserved.
- Device stays online. Next `/frame` wakes the display. There is no `/wake`.
- Response: `204`.
### POST /brightness
```json
{ "brightness": 75 }
```
- Range `0`–`100`. Out-of-range: `400`.
- Persisted to NVS. Applied immediately if awake, otherwise on next wake.
- Response: `204`.
## Touch Callback
Device POSTs to the `callback` URL registered via `/config`:
```json
{
"display": "mudroom",
"event": "tap",
"timestamp": 1730000000
}
```
Events: `tap`, `long_press`, `swipe_left`, `swipe_right`. No coordinates — the device emits gestures, not points.
Delivery: best-effort, ~1 second timeout, no retry queue. Events before `/config` registers a callback are dropped. The device does not interpret events; Scrypted decides what to show next.
## Scrypted Integration
Scrypted owns a static list of viewports and pushes frames to each. On startup, register every viewport:
```ts
await fetch(`${viewport}/config`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
display: "mudroom",
callback: "http://scrypted.local:11080/api/viewport/touch"
})
});
```
Push a frame:
```ts
await fetch(`${viewport}/frame`, {
method: "POST",
headers: { "Content-Type": "image/jpeg" },
body: jpegBuffer // 800x480 baseline JPEG, <1 MB
});
```
Handle touch at `POST /api/viewport/touch`. The handler is the entire interaction layer — camera selection, page cycling, snapshot vs live, sleep — all in Scrypted, keyed off `display` and `event`.
Idle behavior is device-local: the viewport sleeps the backlight after ~30s with no new frame. Scrypted does not need to send `/sleep` unless it wants to dim early.
## Ops
- Firmware updates: reflash over USB. No OTA in v1.
- Factory reset: hold BOOT during power-on to clear NVS (display name, callback, brightness).
- Boot screen: black until first `/frame`. `/health` is available as soon as DHCP completes.
- No DHCP lease: keep retrying; do not reboot.
- Ethernet disconnect: reconnect automatically; keep last frame on screen.
## Build
```sh
source ~/Dev/code/git/esp32/env.sh
cd ~/Dev/code/git/esp32/projects/esp32-poe-scrypted-viewport
idf.py set-target esp32p4
idf.py build
idf.py -p /dev/cu.usbmodem* flash monitor
```
## Philosophy
Scrypted Viewport is a thin network framebuffer appliance.
ESP:
- DHCP
- mDNS
- HTTP server
- JPEG decode
- framebuffer
- touch
- callback
Everything else belongs in Scrypted.
|