# 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) — 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. insecam lists cameras that are reachable on the public internet without any password. The widget only ever does what a browser on that site 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. ## 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. Holding the knob down while the widget starts forces the portal. ## 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, 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 12 slots (overwrites it). * **Favourites** — the 12 slots; click one to play it. A saved camera shows a yellow *FAV n* marker while it plays. * **Source** — *Random* draws from insecam, *Favourites* cycles the saved slots. * **Time per camera**, **Listing** (mixed / top rated / newest), **Screen brightness**, **Ring brightness**, **Caption**. * **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), and the board name with its IP. The menu closes by itself after 45 s without input. ## 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. * `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 12 favourite slots in NVS (namespace `widget`); `menu.c` is the knob-driven menu that edits them.