[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81918":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":12,"openIssues":13,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":14,"stars7d":14,"stars30d":15,"stars90d":13,"forks30d":13,"starsTrendScore":16,"compositeScore":17,"rankGlobal":8,"rankLanguage":8,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":8,"pushedAt":8,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":13,"starSnapshotCount":13,"syncStatus":26,"lastSyncTime":27,"discoverSource":28},81918,"AtlasCube","marcinozog\u002FAtlasCube","marcinozog",null,"C",33,12,6,0,3,4,9,48.24,"MIT License",false,"main",true,[],"2026-06-12 04:01:36","# AtlasCube\n\n*English | [Polski](README.pl.md)*\n\n[![Build firmware](https:\u002F\u002Fgithub.com\u002Fmarcinozog\u002FAtlasCube\u002Factions\u002Fworkflows\u002Fbuild.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fmarcinozog\u002FAtlasCube\u002Factions\u002Fworkflows\u002Fbuild.yml)\n\nA hobby project — internet radio and smart clock running on a generic dev board (for now) with ESP32-S3 (AtlasCube). Streams internet radio, shows a clock, manages reminders, and exposes a web UI for configuration. Everything runs on the device with no cloud dependency.\n\n🌐 **[atlascube.net](https:\u002F\u002Fatlascube.net)**\n\n➡️ **[Web interface demo](https:\u002F\u002Fatlascube.net\u002Fdemo)** — a live, in-browser preview of the device web UI. The demo mirrors what runs on the ESP32-S3: playlist, settings, events, equalizer, layout editor, and file editor are all interactive, backed by a mock state so you can click around without owning the hardware.\n\n⚡ **[Flash firmware from browser](https:\u002F\u002Fatlascube.net\u002Fflash)** — install prebuilt firmware directly over USB from Chromium-based browsers (Chrome \u002F Edge \u002F Opera \u002F Brave) via WebSerial. No ESP-IDF, no esptool, no CLI.\n\n---\n\n## Screenshots\n\n### Device screens\n\n\u003Ctable>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Clock — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Clock — light\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Radio — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Radio — light\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_d_2.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_d_2.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Clock — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_l_2.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_clock_l_2.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Clock — light\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_d_2.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_d_2.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Radio — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_l_2.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_radio_l_2.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Radio — light\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_playlist_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_playlist_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Playlist — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_playlist_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_playlist_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Playlist — light\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_settings_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_settings_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Settings — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_settings_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_settings_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Settings — light\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_event_noti_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_event_noti_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Event — dark\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_event_noti_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_event_noti_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Event — light\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_eq_l.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_eq_l.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Equalizer\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_bt_d.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fscr_bt_d.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Bluetooth\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fdiagram_ili9341.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fdiagram_ili9341.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Diagram with ILI9341\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fdiagram_co5300.jpg\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fdiagram_co5300.jpg\" width=\"200\">\u003C\u002Fa>\u003Cbr>\u003Csub>Diagram with CO5300\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002Ftable>\n\n### Web UI\n\n\u003Ctable>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_index.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_index.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Radio\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_bt.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_bt.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Bluetooth\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_layouts.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_layouts.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Layouts editor\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_disp.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_disp.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Display\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_mqtt.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_mqtt.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>MQTT\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_ss.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_ss.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Screensavers\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_theme.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_theme.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Theme\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_tools.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_settings_tools.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Tools\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_editor.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_editor.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Editor\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_playlist.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_playlist.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Playlist\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_events.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_events.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Events\u003C\u002Fsub>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Ca href=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_eq.png\" target=\"_blank\" rel=\"noopener\">\u003Cimg src=\"https:\u002F\u002Fatlascube.net\u002Fimages\u002Fwww_eq.png\" width=\"400\">\u003C\u002Fa>\u003Cbr>\u003Csub>Equalizer\u003C\u002Fsub>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002Ftable>\n\n---\n\n## Features\n\n**Audio**\n- Internet radio streaming — MP3, AAC, FLAC (via [esp-adf](https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesp-adf))\n- ICY metadata — station name and now-playing track shown on screen and in the web UI\n- 10-band parametric EQ + soft volume (custom DSP element, core 1)\n- Playlist — up to 50 stations, stored in SPIFFS\n- Bluetooth audio — A2DP sink and HFP hands-free (external QCC5125 module, Bluetooth 5.1); supported codecs: LDAC, aptX HD, aptX LL, aptX, SBC, AAC\n- Hardware I2S source switching — a 74HC157D multiplexer routes either the ESP32-S3 or the QCC5125 I2S output to the DAC, selected by a single GPIO\n- Automatic retry on stream loss\n\n**UI**\n- LVGL-based GUI — supports ILI9341 320×240 (SPI), ST7796U 480×320 (SPI), and CO5300 240×296 round AMOLED (QSPI), switched via a single compile-time define\n- Screens: clock, playlist, equalizer, settings, Bluetooth, events, WiFi AP\n- Rotary encoder navigation (turn + press)\n- Capacitive touch — CST816D (CO5300 round AMOLED) or FT6336U (ST7796U 480×320), both on I2C; coexists with the rotary encoder, either input works at any time\n- Swipe gestures — horizontal swipes navigate between clock ↔ radio ↔ bt; swipe-up opens settings (clock) or playlist (radio); detection runs through LVGL on the existing pointer indev, no per-chip glue\n- On-screen controls overlay — tap a media screen to bring up a 5-button cross (play\u002Fpause, vol±, prev\u002Fnext), auto-hides after a short timeout\n- Configurable layout (widget positions editable via JSON)\n- Screensavers — kick in after a configurable idle timeout; choose from clock hands, starfield, fireworks, plasma, Conway's Game of Life, blank (AMOLED-friendly \"off\"), or **Dashboard** (see below)\n\n**Dashboard screensaver**\n- A user-configurable ambient display that polls any JSON HTTP\u002FHTTPS endpoint and renders a single value\n- Configurable from the Settings web UI: **title**, **URL**, **JSON path** (dot\u002Fbracket notation, e.g. `rates[0].mid` or `main.temp`), **suffix** (e.g. ` PLN`, `°C`), and **poll interval** (≥ 5 s)\n- HTTPS supported out of the box via the ESP-IDF certificate bundle — works with public APIs that need no auth\n- Defaults ship with the NBP USD\u002FPLN exchange rate; swap the fields to read pretty much anything serving JSON (weather, crypto, home automation, GitHub stats, …)\n- Polling runs in a dedicated FreeRTOS task only while the screensaver is active — no background traffic when another screen is shown\n\n**Events & reminders**\n- Birthdays, namedays, anniversaries, plain reminders, alarms (radio)\n- Recurring: daily \u002F weekly \u002F monthly \u002F yearly\n- On-screen fullscreen notification + buzzer melody at trigger time; melodies are programmable in firmware (web-based editor planned)\n- Alarm type — at trigger time starts a configured playlist station (the stream itself is the \"ringtone\", keeps playing after dismiss until the radio is stopped) instead of the buzzer\n- CRUD via web UI\n\n**Connectivity**\n- WiFi STA with AP fallback (first-boot setup via 192.168.4.1)\n- HTTP server + WebSocket for real-time state sync\n- NTP time sync with configurable timezone\n- Web UI served from SPIFFS (no internet required after flash)\n- MQTT client — remote control of the radio (play\u002Fstop\u002Fvolume\u002Fstation) plus up to 6 configurable widgets (toggle \u002F slider \u002F label) on a dedicated on-device screen, driving any external MQTT device (Tasmota, zigbee2mqtt, Home Assistant, …); see [MQTT](#mqtt) below\n\n**Android app** *(in development)*\n- Remote control for playback, station switching, and volume\n- Modelled after the YoRadio Remote interface, extended with AtlasCube-specific features: event management, equalizer, layout editor\n\n---\n\n## Hardware\n\n| Component | Details |\n|---|---|\n| MCU | ESP32-S3, 240 MHz, dual-core |\n| Board | Atlas Hub (custom) |\n| Flash | 8 MB |\n| PSRAM | OctoSPI, 80 MHz |\n| Display | ILI9341 320×240 (SPI), ST7796U 480×320 (SPI), or CO5300 240×296 AMOLED (QSPI) — selected at compile time |\n| Touch | CST816D or FT6336U capacitive controller (I2C) — gestures detected by LVGL on the standard pointer indev |\n| Input | Rotary encoder with push button + capacitive touch (swipes + tap-to-control overlay) |\n| I2S mux | 74HC157D — hardware switch between ESP32-S3 and QCC5125 I2S outputs; controlled via GPIO |\n| Audio out | I2S DAC \u002F amplifier (fed from 74HC157D output) |\n| Bluetooth | QCC5125 external module, Bluetooth 5.1, A2DP + HFP |\n| Microphone | Built-in, used for HFP hands-free |\n| Buzzer | LEDC PWM tone generator |\n\n---\n\n## Quick start — flash prebuilt firmware\n\nYou don't need ESP-IDF or a toolchain to put AtlasCube on the device. Tagged releases publish ready-to-flash images, and there's a one-click installer in the browser.\n\n### Easiest: flash from your browser\n\nOpen **[atlascube.net\u002Fflash](https:\u002F\u002Fatlascube.net\u002Fflash\u002F)** in Chrome \u002F Edge \u002F Opera \u002F Brave, pick your display variant, plug the device in over USB, click Install. Zero CLI, zero install. (Firefox and Safari don't support WebSerial.)\n\n> First flash: hold the **BOOT** button while plugging USB, then release — puts ESP32-S3 into download mode. Required because the running firmware drives native USB-CDC and ignores the auto-reset.\n\n### Or: flash from CLI with esptool\n\n**1. Pick your display variant:**\n\n| File | Display | Touch |\n|---|---|---|\n| `AtlasCube-ili9341.bin` | ILI9341 320×240 (SPI) | FT6336U |\n| `AtlasCube-st7796.bin`  | ST7796U 480×320 (SPI) | FT6336U |\n| `AtlasCube-co5300.bin`  | CO5300 240×296 (QSPI AMOLED) | CST816D |\n\n**2. Download** the matching `.bin` from the [latest Release](https:\u002F\u002Fgithub.com\u002Fmarcinozog\u002FAtlasCube\u002Freleases\u002Flatest).\n\n**3. Flash** with [esptool](https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesptool) (one-time `pip install esptool`):\n\n```bash\nesptool.py --chip esp32s3 -p \u003CPORT> write_flash 0x0 AtlasCube-\u003Cvariant>.bin\n```\n\nSubstitute `\u003CPORT>` with your serial port (`\u002Fdev\u002FttyUSB0`, `COM3`, …).\n\n**4. First boot:** the device starts an AP named `AtlasCube-XXXXXX`. Connect, open `192.168.4.1` and configure Wi-Fi.\n\nThat's it — no ESP-IDF, no ESP-ADF, no patches. The rest of this README describes the dev build, needed only when you want to modify the firmware.\n\n---\n\n## Build\n\n**Requirements**\n\n- [ESP-IDF v5.5.4](https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesp-idf)\n- [ESP-ADF v2.8](https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesp-adf)\n\n**One-shot setup**\n\nAfter cloning ESP-IDF and ESP-ADF, run the bundled setup script:\n\n```bash\nADF_PATH=\u003Cpath-to-esp-adf> IDF_PATH=\u003Cpath-to-esp-idf> bash scripts\u002Fpatch-esp-adf.sh\n```\n\nThe script is idempotent (safe to re-run) and does the following:\n\n- Initializes ESP-ADF submodules `components\u002Fesp-adf-libs` and `components\u002Fesp-sr` (pre-compiled libraries not pulled by a plain clone).\n- Copies the AtlasCube board definition into `esp-adf\u002Fcomponents\u002Faudio_board\u002Fesp32_s3_atlascube\u002F`.\n- Patches `Kconfig.projbuild`, `CMakeLists.txt` and `component.mk` in `esp-adf\u002Fcomponents\u002Faudio_board\u002F` to register the board.\n- Applies the FreeRTOS patch (`idf_v5.5_freertos.patch`) on ESP-IDF — required for `xTaskCreateRestrictedPinnedToCore`, without which the MP3 decoder task fails at runtime (`E AUDIO_THREAD: Not found right xTaskCreateRestrictedPinnedToCore`).\n\n`sdkconfig.defaults` already contains `CONFIG_ESP32_S3_ATLASCUBE_BOARD=y`.\n\n\u003Cdetails>\n\u003Csummary>What the script does — manual steps, for reference\u003C\u002Fsummary>\n\nIf you'd rather do it by hand (or are debugging the script):\n\n1. **ESP-ADF submodules:**\n   ```bash\n   git -C $ADF_PATH submodule update --init components\u002Fesp-adf-libs components\u002Fesp-sr\n   ```\n2. **Board sources** — copy or symlink:\n   ```bat\n   mklink \u002FD %ADF_PATH%\\components\\audio_board\\esp32_s3_atlascube \u003Cpath-to-repo>\\components\\audio_board\\esp32_s3_atlascube\n   ```\n3. **Register in `esp-adf\u002Fcomponents\u002Faudio_board\u002FKconfig.projbuild`** (inside the `AUDIO_BOARD` choice):\n   ```kconfig\n   config ESP32_S3_ATLASCUBE_BOARD\n       bool \"ESP32-S3-AtlasCube\"\n   ```\n4. **Register in `esp-adf\u002Fcomponents\u002Faudio_board\u002FCMakeLists.txt`** (before `register_component()`):\n   ```cmake\n   if (CONFIG_ESP32_S3_ATLASCUBE_BOARD)\n       message(STATUS \"Current board name is \" CONFIG_ESP32_S3_ATLASCUBE_BOARD)\n       list(APPEND COMPONENT_ADD_INCLUDEDIRS .\u002Fesp32_s3_atlascube)\n       set(COMPONENT_SRCS\n           .\u002Fesp32_s3_atlascube\u002Fboard.c\n           .\u002Fesp32_s3_atlascube\u002Fboard_pins_config.c\n       )\n   endif()\n   ```\n5. **Register in `esp-adf\u002Fcomponents\u002Faudio_board\u002Fcomponent.mk`** (legacy GNU Make build):\n   ```makefile\n   ifdef CONFIG_ESP32_S3_ATLASCUBE_BOARD\n   COMPONENT_ADD_INCLUDEDIRS += .\u002Fesp32_s3_atlascube\n   COMPONENT_SRCDIRS += .\u002Fesp32_s3_atlascube\n   endif\n   ```\n6. **FreeRTOS patch on ESP-IDF:**\n   ```bash\n   git -C $IDF_PATH apply --ignore-whitespace $ADF_PATH\u002Fidf_patches\u002Fidf_v5.5_freertos.patch\n   ```\n\n\u003C\u002Fdetails>\n\n**Pick the hardware variant**\n\nThe active variant lives in [`main\u002Finclude\u002Fdefines.h`](main\u002Finclude\u002Fdefines.h) — three independent `#define` groups: `DISPLAY_*`, `UI_PROFILE_*`, `TOUCH_*`. Edit by hand, or use the helper:\n\n```bash\nbash scripts\u002Fselect-variant.sh ili9341   # or st7796 \u002F co5300\n```\n\nAfter switching the variant, run `idf.py fullclean` so `sdkconfig` is regenerated from the new combination.\n\n**Build and flash**\n\n```bash\nidf.py build\nidf.py flash\n```\n\n**Flash web UI (SPIFFS)**\n\nThe web UI assets live in the `storage` SPIFFS partition. Bundling is **off by\ndefault** — so a plain `idf.py build` \u002F `flash` stays fast when you only touch\nfirmware — and is enabled per-build through the `ATLAS_SPIFFS` env var. After\nchanging anything in `spiffs_image\u002Fwww\u002F`, regenerate the gzipped assets and\nflash with the bundle:\n\n```bash\npython spiffs_image\u002Ftools\u002Fcompress_web.py   # www\u002F -> web\u002F*.gz\nATLAS_SPIFFS=1 idf.py reconfigure           # register the SPIFFS image\nATLAS_SPIFFS=1 idf.py flash\n```\n\nOn Windows a helper wraps the whole sequence (and resets back to the fast,\nno-SPIFFS config afterwards so the IDE flash buttons stay quick):\n\n```powershell\n.\u002Fscripts\u002Fflash-web.ps1 -p COM5 flash\n```\n\n> `ATLAS_SPIFFS` is read at CMake **configure** time, so flipping it requires\n> `idf.py reconfigure` (the helper does this for you).\n\n**Single merged image (for distribution)**\n\nCombines bootloader, partition table, app, and SPIFFS into one file that can be flashed from offset `0x0` with `esptool` or a web flasher. Set `ATLAS_SPIFFS=1` (and compress the assets first) so the web UI is included:\n\n```bash\npython spiffs_image\u002Ftools\u002Fcompress_web.py\nATLAS_SPIFFS=1 idf.py build\nidf.py merge-bin -o AtlasCube.bin\n```\n\nFlash with:\n\n```bash\nesptool.py write_flash 0x0 AtlasCube.bin\n```\n\nCI builds always set `ATLAS_SPIFFS=1`, so the per-variant release binaries already bundle the web UI.\n\n---\n\n## Web UI\n\nAvailable at the device IP (STA mode) or `192.168.4.1` (AP mode).\n\n| Page | Path |\n|---|---|\n| Radio \u002F now playing | `\u002F` |\n| Settings | `\u002Fsettings.html` |\n| Playlist | `\u002Fplaylist.html` |\n| Events | `\u002Fevents.html` |\n| Equalizer | `\u002Feq.html` |\n| Layout editor | `\u002Flayout.html` |\n| File editor | `\u002Feditor.html` |\n| MQTT widgets | `\u002Fmqtt.html` |\n\nWebSocket endpoint: `ws:\u002F\u002F\u003Cdevice-ip>\u002Fws` — pushes state changes (volume, track, radio state) in real time.\n\nThe running firmware version (from `git describe`) is shown in the web UI header and on the Wi-Fi setup page — a quick way to confirm exactly what was flashed.\n\n---\n\n## MQTT\n\nThe device runs an MQTT client that connects to a local broker (e.g. Mosquitto) on the LAN. Configure it from **Settings → MQTT** in the web UI: host, port, username\u002Fpassword, client ID, base topic. After saving, the client reconnects on the fly — no reboot needed.\n\n- **Compile-time switch**: `CONFIG_MQTT_ENABLE` (menuconfig → *MQTT configuration*). Default `y`; set `n` to drop the component entirely from the firmware.\n- **Connection**: plain TCP (LAN-only, no TLS), QoS 0, automatic reconnect (handled by `esp-mqtt`).\n- **Will \u002F online status**: the device publishes `online` (retained) to `\u003Cbase_topic>\u002Fstatus` on connect, and the broker delivers `offline` (LWT, retained) on unexpected disconnect.\n- **Payload style**: plain text on hierarchical topics (Tasmota\u002FHA-style) — easy to use from `mosquitto_pub` and to wire into Home Assistant via `command_topic`\u002F`state_topic` in YAML.\n\n### Topic map\n\nAll radio topics use the prefix `\u003Cbase_topic>\u002F` (default: `atlascube\u002F`). The MQTT `client_id` is a separate broker-level identifier and does not appear in topic names.\n\n| Topic suffix | Direction | Payload | Notes |\n|---|---|---|---|\n| `cmd\u002Fplay` | subscribe | any | resumes the currently selected station |\n| `cmd\u002Fstop` | subscribe | any | |\n| `cmd\u002Fnext` \u002F `cmd\u002Fprev` | subscribe | any | wraps around the playlist |\n| `cmd\u002Fvolume` | subscribe | `0`–`100` | clamped |\n| `cmd\u002Fstation` | subscribe | playlist index | 0-based |\n| `state\u002Fplaying` | publish (retain) | `playing` \\| `stopped` \\| `buffering` \\| `error` | |\n| `state\u002Fvolume` | publish (retain) | `0`–`100` | |\n| `state\u002Fstation_index` | publish (retain) | playlist index | |\n| `state\u002Fstation` | publish (retain) | station name | from playlist entry |\n| `state\u002Ftitle` | publish (retain) | ICY title | \"\" when stopped |\n| `status` | publish (retain) + LWT | `online` \\| `offline` | LWT delivers `offline` if the device drops |\n\n### Widgets screen\n\nA dedicated on-device screen (swipe right from the clock) hosts **up to 6 user-defined widgets** in a grid. Each slot is configured independently from `\u002Fmqtt.html` (linked from Settings → MQTT). Set a slot's *Type* to `None` to disable it.\n\n**Widget types**\n\n- **Toggle** — publishes `ON`\u002F`OFF` on the cmd topic when tapped; visual state follows the state topic (so the UI stays in sync if the device is toggled from HA, a physical button, an automation, …).\n- **Slider** — configurable `min` \u002F `max` \u002F `step`; publishes the numeric value on the cmd topic and tracks the state topic.\n- **Label** — read-only; displays the latest value from the state topic, with an optional `unit` suffix (`°C`, `%`, …).\n\n**Common fields**\n\n- **Title** — short string shown above the widget.\n- **Command topic** — published on user interaction (toggle\u002Fslider). Example (Tasmota): `cmnd\u002Flivingroom\u002FPOWER`. Example (zigbee2mqtt accepts plain text on `\u002Fset`): `zigbee2mqtt\u002F\u003Cname>\u002Fset`.\n- **State topic** — subscribed on connect\u002Freconnect; drives the widget's displayed value.\n- **JSON path** — when non-empty, extracts a single field from JSON payloads (works on both directions):\n  - *Incoming*: e.g. `state` pulls `\"ON\"` out of zigbee2mqtt's `{\"state\":\"ON\", ...}`.\n  - *Outgoing*: the cmd publish is wrapped as `{\"\u003Cpath>\": \u003Cvalue>}` instead of raw text — handy for devices that expect JSON (zigbee2mqtt `{\"brightness\":128}`).\n  - Empty path = plain-text mode in both directions.\n- Plain-text payload parser accepts `ON`\u002F`OFF`\u002F`on`\u002F`off`\u002F`true`\u002F`false`\u002F`1`\u002F`0` for booleans, bare numbers for sliders.\n\n### Examples\n\nWatch everything the device publishes:\n\n```bash\nmosquitto_sub -h 192.168.1.10 -v -t 'atlascube\u002F#'\n```\n\nControl the radio:\n\n```bash\nmosquitto_pub -h 192.168.1.10 -t atlascube\u002Fcmd\u002Fplay\nmosquitto_pub -h 192.168.1.10 -t atlascube\u002Fcmd\u002Fvolume  -m 30\nmosquitto_pub -h 192.168.1.10 -t atlascube\u002Fcmd\u002Fstation -m 2\n```\n\nMinimal Home Assistant YAML:\n\n```yaml\nmqtt:\n  switch:\n    - name: AtlasCube Radio\n      command_topic: atlascube\u002Fcmd\u002Fplay\n      payload_off:   stopped       # use cmd\u002Fstop for off; or split into two switches\n      state_topic:   atlascube\u002Fstate\u002Fplaying\n      payload_on:    playing\n  number:\n    - name: AtlasCube Volume\n      command_topic: atlascube\u002Fcmd\u002Fvolume\n      state_topic:   atlascube\u002Fstate\u002Fvolume\n      min: 0\n      max: 100\n  sensor:\n    - name: AtlasCube Title\n      state_topic: atlascube\u002Fstate\u002Ftitle\n```\n\n> HA MQTT Discovery (auto-registration) is not implemented yet — entities are declared manually as above.\n\n**File editor**\n\n`\u002Feditor.html` is an in-browser editor for files stored in SPIFFS — JSON configs (layouts, playlist, events), HTML\u002FCSS\u002FJS of the web UI itself, and any other text assets on the device. Lists files from the storage partition, lets you edit them with syntax highlighting, and saves back over HTTP without reflashing. Useful for tweaking layouts or web UI on a deployed device.\n\n---\n\n## Project docs\n\nArchitecture and design notes in [`docs\u002F`](docs\u002F):\n\n- [`audio_pipeline.md`](docs\u002Faudio_pipeline.md) — streaming pipeline, DSP, TCP tuning, task affinity\n- [`events.md`](docs\u002Fevents.md) — reminder\u002Fevent system design\n- [`layout_editor.md`](docs\u002Flayout_editor.md) — UI layout customization\n- [`display_drivers.md`](docs\u002Fdisplay_drivers.md) — display driver gotchas (QSPI AMOLED even-boundary, shared SPI mutex, LVGL buffer vs internal DRAM budget)\n\n---\n\n## Roadmap\n\n- **Enclosure** — 3D-printed case currently in design; firmware is developed and tested on the bare development board\n- **SD card** — local storage for station logos, music files, and voice notification clips\n- **Additional displays** — SSD1322 (256×64 OLED) support in progress (ILI9341 320×240, ST7796U 480×320, and CO5300 240×296 already supported)\n- **Web melody editor** — in-browser tool for composing custom buzzer notification tunes\n\n---\n\n## License\n\nMIT\n","AtlasCube 是一个基于 ESP32-S3 开发板的互联网收音机和智能时钟项目。它能够播放网络广播、显示时钟、管理提醒，并提供了一个用于配置的网页界面，所有功能都在设备上本地运行，无需依赖云端服务。该项目使用 C 语言编写，支持通过 WebSerial 从浏览器直接刷写固件，简化了安装过程。其核心功能包括流媒体播放、时间显示以及事件提醒等，并且用户可以通过直观的网页界面进行个性化设置。AtlasCube 适合那些希望拥有一个独立于互联网的智能家居小工具的 DIY 爱好者或开发者使用。",2,"2026-06-11 04:07:12","CREATED_QUERY"]