152 lines
7.7 KiB
Markdown
152 lines
7.7 KiB
Markdown
# insecam desk widget (LilyGO T-Embed)
|
||
|
||
A little window onto the world's public webcams. The T-Embed's 320×170 IPS
|
||
screen shows a random camera from [insecam.org](http://www.insecam.org) or
|
||
[opentopia.com](https://www.opentopia.com) — as live MJPEG video where the
|
||
camera streams it — and moves on to the next one every 20 seconds. The knob steps through cameras, the LED ring drains as the
|
||
countdown runs. Everything happens on the ESP32-S3; no bridge or PC needed.
|
||
|
||

|
||
|
||
Both sites list cameras that are reachable on the public internet without any
|
||
password (opentopia leans towards harbours, ski slopes and campuses; insecam
|
||
towards forgotten IP cams). The widget only ever does what a browser on those
|
||
sites does: fetch a listing page, then fetch the camera's own snapshot/stream URL.
|
||
|
||
## Hardware
|
||
|
||
Either LilyGO T-Embed. The firmware tells them apart at boot by probing the I2C
|
||
bus (ES7210 on the 2022 board, BQ27220/BQ25896/PN532 on the CC1101 board) and
|
||
picks the matching pin map from `main/board.c`. Nothing beyond the board is
|
||
needed: ESP32-S3R8, 16 MB flash, 8 MB PSRAM, ST7789 170×320, EC11 encoder,
|
||
APA102 (×7) or WS2812 (×8) ring. Printable shells for the original board are in
|
||
LilyGO's repo under
|
||
[`Shell_file/`](https://github.com/Xinyuan-LilyGO/T-Embed/tree/master/Shell_file).
|
||
|
||
## Build and flash
|
||
|
||
Needs ESP-IDF 5.5 (on this machine: `. ~/esp/idf55-env.sh`).
|
||
|
||
```sh
|
||
idf.py set-target esp32s3
|
||
idf.py build
|
||
idf.py -p /dev/ttyACM0 flash monitor
|
||
```
|
||
|
||
The partition table keeps `nvs` where the ESPHome build that shipped on this
|
||
unit had it (`0xf90000`), so stored Wi-Fi credentials survive re-flashing.
|
||
First-boot defaults (interval, listing, brightness, forced variant) live under
|
||
`idf.py menuconfig` → *insecam desk widget*; the on-device menu overrides them
|
||
and stores its choices in NVS.
|
||
|
||
## First boot / Wi-Fi
|
||
|
||
With no saved network the widget opens the open hotspot **insecam-setup**;
|
||
join it with a phone and the captive portal (or `http://192.168.4.1`) asks for
|
||
your Wi-Fi. It remembers up to 8 networks and tries them newest-first at boot,
|
||
so the widget follows you between locations; the portal lists the saved ones
|
||
with a **Forget** button and a form to add another. Holding the knob down while
|
||
the widget starts, or choosing **Wi-Fi setup** in the menu, reopens the portal
|
||
even when already connected.
|
||
|
||
## Controls
|
||
|
||
| input | action |
|
||
| --- | --- |
|
||
| turn clockwise | next camera |
|
||
| turn counter-clockwise | previous camera (history of 24) |
|
||
| click | pause / resume the countdown (the live stream keeps playing) |
|
||
| double-click | hide / show the caption |
|
||
| hold | open the menu (hold again, or *Close*, to leave it) |
|
||
|
||
In the menu: turn to move (the list wraps around), click to select or toggle,
|
||
click a number to edit it (turn changes it, click confirms), hold to go back.
|
||
Entries:
|
||
|
||
* **Save as favourite / Remove from favourites** — the camera that was playing;
|
||
saving asks for one of the 100 slots (it jumps to the first empty one; picking a used slot overwrites it).
|
||
* **Favourites** — the saved cameras; click one to play it. A saved camera shows a
|
||
yellow *FAV n* marker while it plays.
|
||
* **Source** — *Random* draws random listing pages and skips anything shown in
|
||
the last 1200 cameras, *Popular* walks the sites' top-rated listings in rank
|
||
order from the top, *Favourites* cycles the saved slots.
|
||
* **Provider** — *insecam*, *opentopia* or *Mixed* (a random site per batch).
|
||
* **Time per camera**, **Listing** (mixed / top rated / newest), **Screen
|
||
brightness**, **Ring brightness**, **Caption**.
|
||
* **Theme** — the look of the menu, boot page and pop-ups, applied as you
|
||
cycle: *Phosphor* (green), *Pink*, *Amber*, *Ice*, and *Terminal* (green with
|
||
an 8×16 pixel font). The camera caption keeps its own neutral style.
|
||
* **Wheel side** — *Right* (as built) or *Left*: rotates the picture 180° so
|
||
you can turn the unit round and keep the knob under your left hand.
|
||
* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), **Power
|
||
off**, the board name with its IP, and the power source.
|
||
|
||
*Power off* switches the peripheral rail off (screen, ring, codec) and puts the
|
||
ESP32-S3 into deep sleep. To wake it, **hold the button and turn the knob** —
|
||
or turn it and then click within 2.5 s while the ring glows softly. A plain
|
||
nudge of the knob wakes the chip (the RTC can't tell gestures apart) but the
|
||
firmware notices the missing button and goes straight back to sleep without
|
||
ever lighting the screen.
|
||
|
||
The menu closes by itself after 45 s without input.
|
||
|
||
## Battery
|
||
|
||
The original board has no fuel gauge, only a 100 k/100 k divider on the supply
|
||
rail (GPIO 4). On USB that rail sits at ~4.4 V (5 V minus a Schottky drop), so
|
||
anything above 4.25 V is reported as *USB powered*; below that it is the cell
|
||
voltage, turned into a rough percentage from a LiPo discharge table. While on
|
||
battery the caption bar shows a small battery glyph with the percentage; on USB
|
||
it stays out of the way. The CC1101 board's BQ27220 gauge is not supported.
|
||
|
||
## LED ring
|
||
|
||
| ring | meaning |
|
||
| --- | --- |
|
||
| white breathing | starting up |
|
||
| purple breathing | waiting in the Wi-Fi setup hotspot |
|
||
| blue dot running round | connecting to a camera |
|
||
| teal, draining | countdown to the next camera |
|
||
| amber, steady | paused |
|
||
| red blinking | camera offline / site unreachable, skipping |
|
||
| dim grey | menu open |
|
||
|
||
## How it works
|
||
|
||
* `insecam.c` fetches one listing page (`/en/byrating/` or `/en/bynew/`, random
|
||
page) — six cameras per request, so insecam sees one hit every two minutes.
|
||
insecam's whole catalogue is only ~1,700 cameras (287 pages × 6) and
|
||
opentopia's rated list ~2,000, so Random keeps a ring of the last 1,200
|
||
cameras shown (in RAM, cleared on power-off) and skips them; without it the
|
||
birthday effect makes repeats common within an hour.
|
||
* `opentopia.c` does the same for opentopia (`hiddencam.php`, rated or newest),
|
||
then, just before a camera plays, reads its page in *Live Feed* mode for the
|
||
camera's own MJPEG URL (or *Current Still* when there is no video). The site is
|
||
HTTPS-only, so those pages go through `webpage.c` — `esp_http_client` with the
|
||
certificate bundle — costing a TLS handshake (~0.7 s) per fetch. opentopia
|
||
stream URLs are re-resolved on every play because they go stale, so favourites
|
||
and history entries always reconnect to the live address.
|
||
* `httpstream.c` is a small raw-socket HTTP client: `esp_http_client` gives up on
|
||
bodies without `Content-Length`, which is exactly what MJPEG cameras send.
|
||
* `jpegframe.c` pulls complete JPEG files out of the byte stream by walking the
|
||
marker structure (so an EXIF thumbnail's EOI can't truncate a frame).
|
||
* `jpegdec.c` decodes with Espressif's `esp_new_jpeg`, scaling inside the
|
||
decoder to the smallest multiple-of-8 size that still covers the screen, then
|
||
bilinear-resamples the remainder to a full-bleed 320×170 (≈100 ms for a
|
||
1280×800 frame, so MJPEG streams play at a few fps).
|
||
* `slideshow.c` runs the state machine: connect, play frames until the interval
|
||
elapses or the knob moves, skip dead cameras, keep history for "previous".
|
||
* `ui.c` is LVGL 9 via `esp_lvgl_port`: a canvas for the frame, a translucent
|
||
caption bar, a LIVE/PAUSED badge, a hairline progress bar and a six-row menu page.
|
||
* `settings.c` keeps the settings blob and the 100 favourite slots in NVS
|
||
(namespace `widget`); `menu.c` is the knob-driven menu that edits them;
|
||
`power.c` is the deep-sleep power-off with ext1 wake on the encoder pins.
|
||
|
||
## License
|
||
|
||
MIT — see [LICENSE](LICENSE). Pin maps come from LilyGO's
|
||
[T-Embed](https://github.com/Xinyuan-LilyGO/T-Embed) and
|
||
[T-Embed-PN532](https://github.com/Xinyuan-LilyGO/T-Embed-PN532) repositories
|
||
(MIT). `esp_new_jpeg` is fetched from Espressif's component registry at build
|
||
time under its own license.
|