Skip to content

Commit a3f07e9

Browse files
committed
2026.09.15
1 parent 54f43dc commit a3f07e9

73 files changed

Lines changed: 956 additions & 306 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/code-summary.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ All modules in `src/core/` follow the **class + global instance** pattern:
240240
- `station_t` fields (`name`, `url`, `title`) are sized by `STATION_FIELD_LENGTH` (default 170, defined in `options.h`). These are RAM-only fields — not NVS-stored. `BUFLEN` has been retired; use `STATION_FIELD_LENGTH` for station metadata buffers across the codebase.
241241
- `SD_PATH_LENGTH` (256, defined in `sdmanager.h`) is used for SD filesystem path buffers where paths may exceed 170 bytes.
242242
- `Config::keyMap` declaration controls Preferences key mapping.
243+
- IR remote codes use a separate named store: `struct irstore_t` with one `uint64_t[3]` field per button (`power`, `mute`, `up`, `down`, `prev`, `next`, `play`, `mode`, `hash`, `n0``n9`). It is persisted in its own NVS namespace (`ehradioir`) through a dedicated key map in `config.cpp`; buttons are addressed by name, never by array position. The old positional `ircodes_t` blob was removed.
243244
- `Config::saveValue(...)` API now has two simple overloads only:
244245
- typed: `saveValue(T* field, const T& value)`
245246
- string: `saveValue(char* field, const char* value)`
@@ -260,6 +261,8 @@ All modules in `src/core/` follow the **class + global instance** pattern:
260261
- playlist-mode initialization and file-presence checks before delegating playlist indexing/load helpers to `utility`
261262
- canonical SPIFFS asset allowlists (`Config::wwwFiles[]`, `Config::dataFiles[]`) used by startup recovery and file-maintenance flows
262263
- reset section handlers (`defaultSettings(...)`)
264+
- named IR code storage in the dedicated `ehradioir` NVS namespace (`IR_MAGIC` 1812 stored under key `irset`, one key per button via `irKeyMap[]`); helpers `loadIR()`, `saveIR()` / `saveIR(button)`, `irCodes()`, `clearIR()`, `irButtonByName()`, `irButtonCount()`, `irButtonKey()`, `irAction()`
265+
- `deleteOldKeys()` also drops the legacy `ircodes` key from the `ehradio` namespace
263266
- SPI bus initialization: `Config::init()` calls `SPI.begin(SPIA_SCK, SPIA_MISO, SPIA_MOSI)` only when `SPIA_SCK` is defined and `!= 255`, and `SPIB.begin(SPIB_SCK, SPIB_MISO, SPIB_MOSI)` only when `SPIB_SCK` is defined and `!= 255`. I2C-only builds skip SPI init entirely. Both buses are initialized before `_initHW()` and before `display.init()` / `player.init()`. Both SPI buses are fully configured before any peripheral uses them. `SPIClass SPIB(SPI_BUS_SECONDARY)` is declared at file scope in `config.cpp`; extern declared in `config.h`.
264267
- SD-specific behavior:
265268
- `_initHW()` configures `SD_CARD_DETECT_PIN` as `INPUT_PULLUP` when available
@@ -318,7 +321,7 @@ All modules in `src/core/` follow the **class + global instance** pattern:
318321
- weather cache and formatting logic
319322
- centralized runtime logging for reconnect/weather/boot progress/time-sync via `FUNCTIONLOG`/`SERIALLOG`/`BOOTLOGX`
320323
- web-stream reconnect now resumes through `player.resumeLastWebSource()` so direct URL sources can recover via `/data/laststation.url` instead of always falling back to `lastStation`
321-
- `retryStreamConnection` task (40 attempts × 15 s) can be externally cancelled by `commandhandler.cpp` `cancelStreamRetry()` when the user issues any playback-changing command; the task also cleans itself up when conditions change (user stops, WiFi drops, or playback resumes)
324+
- `retryStreamConnection` task (40 attempts × 15 s) is cancelled through `MyNetwork::cancelStreamRetry()`, the single owner of `streamRetryTaskHandle` (called by commandhandler on playback-changing commands, by `player.prev()`/`next()`/`toggle()`, and by `utility.turnoff()`); the task also cleans itself up when conditions change (user stops, WiFi drops, or playback resumes)
322325
- Coupling:
323326
- pushes display updates (`display.putRequest(...)`)
324327
- calls player/netserver hooks
@@ -338,6 +341,8 @@ All modules in `src/core/` follow the **class + global instance** pattern:
338341
- error reporting and display/net updates
339342
- command queue depth: `xQueueCreate(10, ...)` — increased from 5 to prevent queue overflow during rapid mode-switch sequences (SD→web transitions) where multiple commands (PR_STOP, PR_PLAY, PR_VUTONUS) arrive before the first finishes processing.
340343
- direct playback lifecycle side effects for `rgbled` and `backlightControls` (start/stop + initial stopped-state sync)
344+
- `mute()`: volume-0 toggle backed by the private `_muteVol` member; uses raw `getVolume()`/`setVolume()` so `config.store.volume` and the displayed volume are deliberately untouched. Shared by the physical mute buttons, the `mute` command, and the IR mute button — the `DSP_DUMMY` suppression lives only at the physical-button call site.
345+
- `prev()` / `next()` / `toggle()` delegate retry cancellation to `network.cancelStreamRetry()` (previously duplicated inline).
341346
- VS1053 SPI: `Player::Player()` constructor passes `&VS1053_SPIBUS` to the `Audio(CS, DCS, DREQ, SPIClass*)` constructor. `VS1053_SPIBUS` is the `SPIB` or `SPIA` object resolved by `options.h`. No `SPIClass` declared in `player.cpp` or `player.h`.
342347
- Coupling:
343348
- updates display queue and websocket state
@@ -411,6 +416,7 @@ All modules in `src/core/` follow the **class + global instance** pattern:
411416
- `/settings.html`, `/update.html`, `/ir.html` no longer served via `index_html[]` — handled by PSRAM cache fallthrough
412417
- websocket command parsing and outbound updates
413418
- state request queue processing (`GETSYSTEM`, `GETSCREEN`, `GETLOCALE`, etc.)
419+
- IR websocket helpers: `irToWs()` (protocol + code) and `irValsToWs()` (the active button's 3 codes, read through `config.irCodes()`)
414420
- online update check/start tasks
415421
- radio-browser search and curated task management
416422
- exact-match-first preview/add handling on `/search`; unmatched preview now uses the same direct URL playback path as `playurl` instead of a mutating playlist scan
@@ -437,7 +443,9 @@ All modules in `src/core/` follow the **class + global instance** pattern:
437443
- own shared command aliases across ingress channels (`playstation`/`play`, `boot`/`reboot`, `vol+`/`volup`, `dim`/`brightness`, `dspon`/`screenon`)
438444
- player-command parity helpers (including exact-match-first direct URL playback command routing for `playurl` / `burl`)
439445
- trigger curated operations and locale update tasks
440-
- cancel the stream retry task (`cancelStreamRetry()`) before executing user-initiated playback commands (`stop`, `playstation`, `prev`, `next`, `toggle`, `turnoff`, `burl`, `mode`, `submitplaylist`) so explicit user actions always interrupt automatic reconnection loops
446+
- cancel the stream retry task (`network.cancelStreamRetry()`) before executing user-initiated playback commands (`stop`, `playstation`, `prev`, `next`, `toggle`, `turnoff`, `burl`, `mode`, `submitplaylist`) so explicit user actions always interrupt automatic reconnection loops
447+
- `turnon` / `turnoff` delegate to `utility.turnon()` / `utility.turnoff()`; the `mute` command maps to `player.mute()`
448+
- IR recorder commands: `irbtn` resolves a button **name** via `config.irButtonByName()` (`-1` stops recording and saves), `chkid` selects the slot, and `irclr` clears a slot through `config.clearIR()`
441449
- Critical coupling file for setting changes.
442450
- New commands: `theme` (theme switching), `layout` (layout switching), `inverttitle` (invert title toggle). All persist via `saveValue` and trigger `display._applyState()`.
443451

@@ -471,6 +479,8 @@ All modules in `src/core/` follow the **class + global instance** pattern:
471479
- Converts hardware input events into same core actions used by WebUI (`controlsEvent`, player commands, display mode changes).
472480
- `Controls::loop()` now calls `backlightControls.controlsLoop()` directly for non-PLAYER backlight wake behavior.
473481
- IR record debug text now routes through centralized logging macros.
482+
- IR dispatch is name-based: `irLoop()` iterates `config.irButtonCount()`, matches codes from `config.irCodes(button)`, then switches on the behaviour id from `config.irAction(button)` (`IRACT_POWER`, `IRACT_MUTE`, `IRACT_UP`, `IRACT_DOWN`, `IRACT_PREV`, `IRACT_NEXT`, `IRACT_PLAY`, `IRACT_MODE`, `IRACT_HASH`, `IRACT_DIGIT`). Digit buttons derive their value from the `n0``n9` key. The old positional `IR_UP``IR_HASH` enum is gone. Power/mute/mode are local actions and are allowed while offline or showing `LOST`.
483+
- Physical mute (`EVT_ENC2_SW` / `EVT_BTN_MODE` double-click) calls `player.mute()` and keeps the `DSP_MODEL == DSP_DUMMY` no-op guard at the call site.
474484
- Screensaver wake hardening:
475485
- `controlsEvent()` now flushes pending display requests (`display.resetQueue()`) and zeroes screensaver tick counters before queueing `NEWMODE, PLAYER` when waking from `SCREENSAVER`/`SCREENBLANK`, preventing one-detent rotary wake races where a stale queued screensaver mode request could immediately re-apply.
476486

@@ -520,6 +530,7 @@ All modules in `src/core/` follow the **class + global instance** pattern:
520530
- playlist CSV parsing and station lookup/load helpers
521531
- WiFi credential parse/save/import helpers
522532
- deep-sleep entrypoints (`doSleepW`, `sleepForAfter`)
533+
- standby on/off helpers `standbyon()`, `standbyoff()`, `standbytoggle()` (shared by the `standbyon`/`turnon` and `standbyoff`/`turnoff` commands and the IR power button); `standbyoff()` also calls `network.cancelStreamRetry()`
523534
- SPIFFS file-maintenance helpers shared with startup and WebUI update paths:
524535
- `cleanupSpiffs()`
525536
- `deleteMainwwwFile()`
@@ -646,6 +657,8 @@ All modules in `src/core/` follow the **class + global instance** pattern:
646657

647658
## `data/www/ir.html`
648659
- IR recording and assignment UI.
660+
- Every `.irbutton` carries a `data-irid` name (`power`, `mute`, `up`, `down`, `prev`, `next`, `play`, `mode`, `number`, `n0``n9`) that maps 1:1 to the `irstore` field / NVS key, so DOM order is irrelevant.
661+
- The shell loads the page body from `irrecord.html`; the `/ir.html` route itself is handled by the PSRAM cache fallthrough.
649662

650663
## `data/www/search.html`
651664
- Search UI for radio-browser integration.
@@ -658,7 +671,7 @@ All modules in `src/core/` follow the **class + global instance** pattern:
658671
- Contains logic previously in `ir.js`, `updform.js`, `playstation.js`
659672
- station preview/play helper (`sendStationAction`)
660673
- online update check/start UI helpers
661-
- IR setup/learn interactions (`initControls`, `checkSelect`, `irClear`, `backRecord`)
674+
- IR setup/learn interactions (`initControls`, `checkSelect`, `irClear`, `backRecord`); `irbuttonClick()` sends the button's `data-irid` name as `irbtn=<name>` and `irbtn=-1` on deselect
662675
- also consolidated with `data/www/locale.js`
663676
- i18n runtime helper (`t(...)`) and translation application (`applyI18n`).
664677
- Applies key-based translations to DOM and fallback behavior.
@@ -830,7 +843,7 @@ Each config type has its own field that makes a widget meaningful, and that is w
830843
## Screen Rendering Fixes (Session: VU Rotated Layout)
831844

832845
- **New layout flag** `LayoutData::rotateVU` (exposed via `rotateVU_ptr`), treated exactly like `boomboxStyle` — absent means false. `VuWidget::_rotate` is read from `rotateVU_ptr` in `init()`.
833-
- **Layout ordering** in `displayTFT480x320conf.h`: `_layoutNames` is now `Default`, `Default (VU Rotated)`, `VaraiTamas (BoomBox)`. The rotated layout is layout #2 (`bandsConf = { 32, 130, 4, 2, 10, 3 }`, `.rotateVU = true`); BoomBox moved to #3.
846+
- **Layout ordering** in `displayTFT480x320conf.h`: `_layoutNames` is now `Default`, `Default (VU Rotated)`, `BoomBox (VaraiTamas)`. The rotated layout is layout #2 (`bandsConf = { 32, 130, 4, 2, 10, 3 }`, `.rotateVU = true`); BoomBox moved to #3.
834847
- **Blit choice**: `VuWidget::_draw()` uses the manual `startWrite()` / `setAddrWindow()` / `writePixels()` / `endWrite()` sequence for all three modes. `drawRGBBitmap()` was deliberately removed from the widget layer — the manual path depends only on `setAddrWindow` and `writePixels`, which every TFT driver is guaranteed to implement, and it issues a single bulk transfer rather than one `writePixels` call per scanline. Do not switch this back.
835848
- **Direction**: the rotated VU fills left-to-right with `_vumaxcolor` at the right end.
836849

Commands.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ Order and sections match that file.
1818
| `middle` | Set middle (-16..16). |
1919
| `bass` | Set bass (-16..16). |
2020
| `volume`, `vol` | Set absolute volume (clamped 0..VOLUME_SCALE). |
21-
| `turnoff` | Turn display off, stop playback, and preserve smartstart value. |
22-
| `turnon` | Turn display on, optionally resume smartstart playback. |
21+
| `mute` | Toggle mute: volume 0 <-> last active level. Same behaviour as the IR mute button; physical-button mute is suppressed without a display. |
22+
| `standby` | Toggle standby on or off |
23+
| `standbyoff` | Turn display off, stop playback, and preserve smartstart value. |
24+
| `standbyon` | Turn display on, optionally resume smartstart playback. |
2325
| `burl`, `playurl` | Play direct stream URL (http/https). |
2426
| `sdpos` | Set SD playback position when in SD mode. |
2527
| `playstation`, `play` | Play station by playlist index (clamped to valid range). |
@@ -164,9 +166,9 @@ Only available when built with IR_PIN != 255.
164166

165167
| Command(s) | Action |
166168
| --- | --- |
167-
| `irbtn` | Set IR recording index and update IR recording mode/state. Blocked in HTTP/MQTT/Telnet. |
168-
| `chkid` | Set IR check slot id. Blocked in HTTP/MQTT/Telnet. |
169-
| `irclr` | Clear selected IR slot value at active index. Blocked in HTTP/MQTT/Telnet. |
169+
| `irbtn` | Select the IR button to record by name (for example `power`, `mute`, `n1`); `-1` stops recording and saves. Blocked in HTTP/MQTT/Telnet. |
170+
| `chkid` | Set IR check slot id (0-2). Blocked in HTTP/MQTT/Telnet. |
171+
| `irclr` | Clear the selected IR slot (0-2) of the active button. Blocked in HTTP/MQTT/Telnet. |
170172

171173
## Curated Playlists
172174

Controls.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,23 @@ Repeat for other buttons.
6666

6767
| Button | Action | Longpress Action |
6868
| ------ | ---------------------- | ---------------- |
69+
| Power | toggle the display/radio on and off | - |
70+
| Mute | toggle mute (volume 0 and back) | - |
6971
| &#9199; | start/stop playing | - |
7072
| &#9664; | previous station/track | - |
7173
| &#9654; | next station/track | - |
7274
| &#9650; | volume up | quick volume up |
7375
| &#9660; | volume down | quick volume down
74-
| # | toggle between player/playlist mode | - |
76+
| # | toggle between player/playlist mode or cancel entering station number | - |
7577
| * | toggle between stations/SD mode | - |
76-
| 0-9 | Start entering the station number. To finish input and start playback, press the play button. To cancel, press hash. | - |
78+
| 0-9 | Start entering the station number. To finish input and start playback, press the play button. To cancel, press #. | - |
79+
80+
Each button stores up to 3 alternative IR codes (the three slots in the recorder).
81+
82+
#### Power Toggle
83+
84+
This is not a real power control. It is not actually possible in code to wake up the radio with an IR signal.
85+
This merely fakes power control by stopping audio, blanking the display, and turning off the display backlight (if possible).
7786

7887
### Touchscreen
7988

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ ehRadio inherits a lot from ёRadio, but improvements have been made to many fun
7474
- UDA1334
7575
- MAX98357A
7676
- ES8311 (mono)
77-
- VS1053 updated but not recommended
77+
- VS1053 updated and well-supported
7878

7979
- Display architecture based on ёRadio
8080
- simplified and expanded
@@ -152,19 +152,21 @@ The settings for smart start and auto update will appear as off but they will re
152152
### SD Offline Mode <img src="images/Booticon_SD.png">
153153

154154
To enter a special SD-card only mode (with network functionality disabled), hold down any button (including encoder switches) shortly after powering-up (until the display shows something).
155-
It is not necessary to hold these buttons while powering-up, and actually could cause issues if the builder put a button on a strapping pin.
155+
It is not necessary to hold this button while powering-up, and actually could cause issues if the builder put that button on a strapping pin.
156156

157157
You can also enter this mode by pressing the play button, clicking a rotary encoder button, or tapping the touch screen
158158
in AP/Improv Mode or when you see `* LOST *`, which will trigger a reboot.
159159

160-
If no RTC is connected, the clock will not display.
160+
An RTC module is required for the time to be displayed in this mode.
161+
Otherwise, the clock will not be shown.
162+
161163
Most settings, as set in the WebUI, are preserved in this mode.
162164
These are disabled: Safe mode, Deep Sleep, Mode switch.
163165

164166
SD Shuffle (which makes the "previous" button do nothing) will be read from preferences and changeable using Mode switch (the mode button or double-click of a rotary encoder).
165167
It is not saved to preferences in this mode. All other buttons will have expected behaviour.
166168

167-
Exit this mode (reboot with network functionality) by powering off and powering on again.
169+
Exit this mode (and reboot with network functionality) by powering off and powering on again.
168170

169171
---
170172

@@ -179,7 +181,8 @@ For a detailed guide to supported hardware and peripherals, wiring, and audio is
179181
## A Warning & Disclaimer
180182

181183
With the `2026.07.31` release, the display architecture was overhauled to make layouts and colors changeable while running.
182-
This involved significant changes to the original code and layouts.
184+
This involved significant changes to the original ёRadio code and layouts.
185+
Tools are available in the repository to assist in converting files from ёRadio mods.
183186

184187
Trip5 builds include OLED 128x64 and TFTs 480x320, 320x240, 160x128.
185188
Other display sizes may have quirks or issues with layouts that need repair.
@@ -202,7 +205,7 @@ There is also a `no_display` build here if you just wish to test functionality.
202205
Note that if following this path, you don't necessarily to attach all peripherals (rotary, buttons, IR Receiver, SD card, display).
203206
The radio needs only the ESP board and the audio decoder to function... although your WebUI may show links to peripherals that don't exist.
204207

205-
If don't want to mess around with VS Code but would still like your own build added to the Releases, you can make a
208+
If you don't want to mess around with VS Code but would still like your own build added to the Releases, you can make a
206209
[firmware request](https://github.com/trip5/ehRadio/discussions/categories/firmware-requests).
207210
Please also do some research before requesting a firmware.
208211

@@ -315,7 +318,7 @@ For that and other major needed changes to the codebase, there is a `code-issues
315318

316319
| Date | Release Notes |
317320
| ---------- | ---------------- |
318-
| 2026.09.13 `dev` | NV3007 added (work in progress), Fixes to: SSD1327, `ROTATE_90` for square displays, TFT display resolutions, volume page, VU meter (timing, orientation, peaks), Firefox mobile |
321+
| 2026.09.15 `dev` | NV3007 added (work in progress), Fixes to: SSD1327, `ROTATE_90` for square displays, TFT display resolutions, volume page, VU meter (timing, orientation, peaks, OLED), Firefox mobile, IR code overhauled and mute/power added |
319322
| 2026.08.19 | Stability and bug fixes (SD Offline), documentation |
320323
| 2026.08.13 | Memory usage, stability, and bug fixes (especially to SD, VS1053) |
321324
| 2026.08.03 | Minor fixes (and whoops) fixed Search and Curated |
@@ -356,7 +359,7 @@ A full history of ёRadio from v0.4.177 to v0.9.533 can be seen in the [old Read
356359

357360
Thanks to:
358361

359-
- [kle7rx](https://github.com/kle7rx) - `ru_RU` translation, debugging, mute feature, VS1053/I2S fixing, and amplifier schematics
362+
- [kle7rx](https://github.com/kle7rx) - `ru_RU` translation, debugging, mute feature, VS1053/I2S fixing, amplifier schematics, SSD1327 fixes, NV3007 support
360363
- [Kasperaitis](https://github.com/kasperaitis) - `lt_LT` translation, initiating locales, battery support and widget, and a bunch of work for ES3C28P (including ES8311 decoder, FT6336 touchscreen)
361364
- [e2002](https://github.com/e2002) - for [ёRadio](https://github.com/e2002/yoradio/) without which ehRadio would not be possible
362365

0 commit comments

Comments
 (0)