5.4 KiB
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 — 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. Printable shells for the original board are in
LilyGO's repo under
Shell_file/.
Build and flash
Needs ESP-IDF 5.5 (on this machine: . ~/esp/idf55-env.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 (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 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), the board name with its IP, and the power source.
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 / insecam unreachable, skipping |
| dim grey | menu open |
How it works
insecam.cfetches one listing page (/en/byrating/or/en/bynew/, random page) — six cameras per request, so insecam sees one hit every two minutes.httpstream.cis a small raw-socket HTTP client:esp_http_clientgives up on bodies withoutContent-Length, which is exactly what MJPEG cameras send.jpegframe.cpulls 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.cdecodes with Espressif'sesp_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.cruns the state machine: connect, play frames until the interval elapses or the knob moves, skip dead cameras, keep history for "previous".ui.cis LVGL 9 viaesp_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.ckeeps the settings blob and the 12 favourite slots in NVS (namespacewidget);menu.cis the knob-driven menu that edits them.
License
MIT — see LICENSE. Pin maps come from LilyGO's
T-Embed and
T-Embed-PN532 repositories
(MIT). esp_new_jpeg is fetched from Espressif's component registry at build
time under its own license.
