{"id":48632421,"url":"https://github.com/doccaz/knobcast","last_synced_at":"2026-04-09T06:00:29.013Z","repository":{"id":349910182,"uuid":"1204450869","full_name":"doccaz/knobcast","owner":"doccaz","description":"A Chromecast control knob using a ESP32-C3 OLED + WiFi","archived":false,"fork":false,"pushed_at":"2026-04-08T03:57:53.000Z","size":62,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-08T05:23:35.144Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/doccaz/knobcast","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doccaz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-08T02:48:04.000Z","updated_at":"2026-04-08T03:58:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doccaz/knobcast","commit_stats":null,"previous_names":["doccaz/knobcast"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/doccaz/knobcast","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doccaz%2Fknobcast","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doccaz%2Fknobcast/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doccaz%2Fknobcast/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doccaz%2Fknobcast/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doccaz","download_url":"https://codeload.github.com/doccaz/knobcast/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doccaz%2Fknobcast/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31588038,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-09T05:33:47.836Z","status":"ssl_error","status_checked_at":"2026-04-09T05:32:26.579Z","response_time":112,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-09T06:00:19.934Z","updated_at":"2026-04-09T06:00:28.943Z","avatar_url":"https://github.com/doccaz.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# KnobCast: an ESP32-C3 Chromecast Physical Remote\n\n\u003cimg width=\"624\" height=\"1245\" alt=\"knobcast\" src=\"https://github.com/user-attachments/assets/db3a7a4c-7633-43b8-8b1c-1a881f3a4349\" /\u003e\n\n\nA PlatformIO project that turns an **ESP32-C3 + KY-040 rotary encoder + 72×40 OLED**\ninto a physical volume/playback controller for any Chromecast on your local network.\n\n**Passive / non-invasive:** the device attaches to whatever app is already running\non the Chromecast (Spotify, YouTube, etc.) and controls it without launching a new\nreceiver or interrupting playback.\n\n---\n\n## Why did I build this?\n\nI wanted a dedicated physical remote for my Chromecast that would allow me to control\nvolume and playback without having to pick up my phone or use the Google Home app.\nI also wanted to be able to see the current volume level and playback status at a\nglance, and have the ability to quickly switch between different Chromecasts on my\nnetwork.\n\n---\n\n## Features\n\n- **Volume control** via rotary encoder with 5× acceleration on fast spin\n- **Playback control** — play/pause, next/previous track, stop, seek\n- **Mute/unmute** toggle via long press\n- **On-device menu system** navigated entirely with the encoder + back button\n- **Multi-device support** — discover and switch between Chromecasts (including groups)\n- **72×40 OLED** with HUD, scrolling text, overlays, and configurable screen timeout\n- **Web UI** for configuration, control, status, WiFi scanning, and debug logs\n- **AP mode** captive portal for first-time WiFi setup (no hardcoded credentials)\n- **Persistent config** stored in NVS (survives reboots and reflashes)\n- **Auto-reconnect** on connection drop or app change\n- **Auto-connect** to last known device on boot (configurable)\n- **Scan on boot** — automatic mDNS discovery at startup (configurable)\n- **Progress bar modes** — show volume level or elapsed playback time\n- **LED status** — solid when connected, pulsing during scan/connect, off when idle\n- **mDNS registration** — device accessible at `knobcast.local`\n\n---\n\n## Hardware\n\n| Component | Detail |\n|---|---|\n| MCU | ESP32-C3 dev board (RISC-V, WiFi) |\n| Encoder | KY-040 rotary encoder with push-button |\n| Display | 72×40 SSD1306 OLED (I2C, addr 0x3C) — onboard |\n| LED | Onboard LED (GPIO 8, active-low) — solid when connected, pulses during scan/connect |\n\n### Wiring\n\n```\nKY-040 Pin  →  ESP32-C3 GPIO\n─────────────────────────────\nCLK (A)     →  2  (internal pull-up)\nDT  (B)     →  3  (internal pull-up)\nSW          →  4  (internal pull-up, active-low)\n+           →  3.3 V\nGND         →  GND\n\nBack button →  9  (internal pull-up, active-low)\n\nOLED (I2C)\n──────────\nSDA         →  5\nSCL         →  6\n```\n\n\u003e All ESP32-C3 GPIOs support internal pull-ups — no external resistors needed.\n\n---\n\n## Controls\n\n### HUD mode (default)\n\n| Action | Function |\n|---|---|\n| Rotate CW / CCW | Volume up / down (configurable step, 5× acceleration) |\n| Short press (\u003c 0.8 s) | Open menu |\n| Long press (≥ 0.8 s) | Mute / Unmute toggle |\n\n### Menu mode\n\n| Action | Function |\n|---|---|\n| Rotate CW / CCW | Navigate menu items |\n| Short press | Select item |\n| Back button (GPIO 9) | Go back / exit menu |\n\n### Menu structure\n\n```\nMain Menu\n├── Actions\n│   ├── Play / Pause\n│   ├── Previous track\n│   ├── Next track\n│   ├── Connection info\n│   └── Disconnect\n├── Devices\n│   ├── \u003cdiscovered devices\u003e\n│   ├── Scan network\n│   └── Back\n├── Scan network\n├── Connection info\n├── Disconnect\n├── About\n├── Settings\n│   ├── Menu Timeout (5s / 15s / 30s / 60s)\n│   ├── Screen Timeout (30s / 1m / 5m / 10m / Never)\n│   ├── Progress Bar (Volume / Elapsed time)\n│   ├── Scan on boot (on/off)\n│   ├── Auto-connect (on/off)\n│   └── Back\n├── Reboot\n└── Exit\n```\n\n---\n\n## How it works — Passive Cast Control\n\n```\nESP32-C3  ──── WiFi ────  Chromecast\n                 │\n            Port 8009\n            TLS (self-signed cert → setInsecure())\n                 │\n        [4-byte BE uint32 length] + [Protobuf CastMessage]\n                 │\n       Namespaces / channels:\n         tp.connection  → CONNECT (to receiver + transport)\n         tp.heartbeat   → PING / PONG (every 5 s)\n         receiver       → GET_STATUS, SET_VOLUME\n         media          → PLAY, PAUSE, NEXT, PREVIOUS, SEEK, STOP\n```\n\nThe key design choice is **no LAUNCH** — we never start a new receiver app:\n\n1. **CONNECT + GET_STATUS** — on connection, read `RECEIVER_STATUS` to discover\n   the currently running app and its `transportId`.\n2. **Volume/mute** — sent to `receiver-0` on the receiver namespace (device-level,\n   works regardless of which app is running).\n3. **Media commands** — sent to the existing `transportId` on the media namespace\n   after connecting to the transport and learning the `mediaSessionId`.\n4. **Session tracking** — if the app changes (e.g. Spotify → YouTube), the new\n   `transportId` is detected in `RECEIVER_STATUS` and reconnected automatically.\n5. **Dual command format** — next/previous sends both `NEXT`/`QUEUE_NEXT` and\n   `PREVIOUS`/`QUEUE_PREV` for maximum app compatibility.\n\nThe protobuf is hand-rolled in `cast_message.h` (no nanopb needed).\n\n---\n\n## OLED Display\n\n| Screen | When | Content |\n|---|---|---|\n| Splash | Boot | \"KnobCast\" + IP address |\n| AP Mode | No WiFi config | \"AP Mode\" + SSID + IP |\n| Connecting | WiFi/Cast connect | \"Connecting\" + target |\n| HUD | READY state | Device name, volume bar + %, play state, app name |\n| Menu | Encoder press | Title bar + 3 visible items with scroll |\n| Overlay | Any action | Large text (e.g. \"VOL 42%\", \"PAUSE\") for 1.2 s |\n\nLong device/app names scroll automatically (50 ms speed, 1.5 s pause).\n\n### Screen timeout (burn-in protection)\n\nThe display powers off after a configurable timeout (default 10 min). Any encoder\nor button input wakes it immediately. Options: 30 s, 1 min, 5 min, 10 min, or never.\n\n---\n\n## Web UI\n\n\u003cimg width=\"490\" height=\"985\" alt=\"knobcast-1\" src=\"https://github.com/user-attachments/assets/06a10528-14d2-444d-80e4-68aa65f51d39\" /\u003e\n\u003cimg width=\"490\" height=\"719\" alt=\"knobcast-2\" src=\"https://github.com/user-attachments/assets/9c1863ee-f1dd-4a55-8228-fe539ba665e2\" /\u003e\n\nRuns on port 80 in both AP and STA modes.\n\n| Endpoint | Method | Function |\n|---|---|---|\n| `/` | GET | Main HTML page (config + control + status) |\n| `/save` | POST | Save WiFi/Cast config → reboot |\n| `/status` | GET | JSON: volume, muted, playing, app, device, time, config |\n| `/control` | POST | Commands: play, pause, stop, next, prev, mute, unmute, volup, voldown, setvol:N, seek:N, disconnect |\n| `/scan` | GET | mDNS scan → JSON list of discovered Chromecasts |\n| `/connectcast` | POST | Connect to a specific device by IP and port |\n| `/wifiscan` | GET | Scan available WiFi networks |\n| `/testwifi` | POST | Test WiFi credentials without saving |\n| `/reset` | POST | Factory reset (clear NVS) → reboot |\n| `/log` | GET | Debug log (JSON array with timestamps) |\n| `/debug` | POST | Enable/disable/clear debug logging |\n\n### First-time setup (AP mode)\n\nOn first boot (or after factory reset), the device starts as a WiFi AP named\n`KnobCast-XXXX` with a captive portal. Connect to it and a config page opens\nautomatically. Enter your WiFi credentials and optionally a Chromecast IP\n(leave blank for mDNS auto-discovery).\n\n---\n\n## Configuration (NVS)\n\nStored in ESP32 NVS namespace `knobcast`:\n\n| Key | Type | Default | Description |\n|---|---|---|---|\n| `configured` | bool | false | Has WiFi been saved? |\n| `wifiSsid` | string | \"\" | WiFi SSID |\n| `wifiPass` | string | \"\" | WiFi password |\n| `castIp` | string | \"\" | Chromecast IP (blank = mDNS) |\n| `volStep` | float | 0.02 | Volume step per click (1–20 %) |\n| `menuTimeout` | int | 15 | Menu auto-close timeout in seconds |\n| `screenTimeout` | int | 600 | Display sleep timeout in seconds (0 = never) |\n| `scanOnBoot` | bool | true | Run mDNS scan on startup |\n| `barMode` | int | 0 | Progress bar: 0 = volume, 1 = elapsed time |\n| `autoConnect` | bool | true | Auto-connect to last device on boot |\n| `lastDevIp` | string | \"\" | Last connected device IP |\n| `lastDevName` | string | \"\" | Last connected device name |\n| `lastDevPort` | uint16 | 8009 | Last connected device port |\n\n---\n\n## State machine\n\n```\nINIT\n  │ config.configured?\n  ├── no ─→ AP_MODE        WiFi AP + captive portal + web config UI\n  │                          (loops until config saved → reboot)\n  └── yes\n       ▼\nWIFI_CONNECTING            Connect to saved SSID (timeout → AP_MODE fallback)\n       │ connected\n       ▼\nWIFI_CONNECTED             mDNS discovery or configured IP\n       │ found\n       ▼\nREADY                      loop(): TLS connect, heartbeat, encoder events,\n                            menu, web server, OLED, reconnect on drop\n```\n\n---\n\n## Build \u0026 flash\n\n```bash\n# Requires PlatformIO CLI or VS Code with PlatformIO extension\n\npio run --target upload                              # default env\npio run -e esp32c3dev_oled72x40 --target upload      # OLED variant\npio device monitor                                   # 115200 baud\n```\n\n---\n\n## File structure\n\n```\nchromecast-esp32/\n├── platformio.ini              ← board: esp32-c3-devkitm-1, libs: ArduinoJson, U8g2\n└── src/\n    ├── main.cpp                ← WiFi/AP setup, state machine, encoder→Cast glue\n    ├── config.h                ← NVS-backed persistent configuration\n    ├── display.h               ← OLED display manager (HUD, menu, overlays, timeout)\n    ├── menu.h                  ← Menu structure and navigation\n    ├── web_server.h            ← Web UI + AP mode captive portal\n    ├── rotary_encoder.h        ← Interrupt-driven KY-040 + quadrature state machine + acceleration\n    ├── chromecast_client.h     ← Cast client public API (passive mode)\n    ├── chromecast_client.cpp   ← Cast client implementation (connect, discover, control)\n    ├── cast_message.h          ← Hand-rolled protobuf CastMessage encoder/decoder\n    └── debug_log.h             ← Ring buffer debug log (60 lines, serial + web)\n```\n\n---\n\n## Limitations / future work\n\n- Device authentication (`com.google.cast.tp.deviceauth`) is not verified.\n- Some apps (e.g. Spotify) may not expose `mediaSessionId` for media commands —\n  volume/mute always works as it's receiver-level.\n- Potential additions: OTA firmware updates.\n- Also... I'm working on a cool 3D printed case for this remote, so stay tuned!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoccaz%2Fknobcast","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoccaz%2Fknobcast","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoccaz%2Fknobcast/lists"}