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 or 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.

The widget on a desk, paused on a favourite Tokyo camera

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/.

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. 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. 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.

S
Description
an esp slideshow viewer for insecam and opentopia
Readme MIT
380 KiB
0 Stars 1 Watchers 0 Forks
Languages
C 99.6%
CMake 0.4%