Professional 4-channel audio amplifier with PT2314E processor, LCD display, IR remote and full IoT integration
Previously known as "Audio Switcher". Renamed to reflect the amplifier functionality.
- Features
- Hardware
- Project Structure
- Installation
⚠️ TFT_eSPI Configuration- Web Interface
- Idle Clock
- MQTT API
- IR Remote
- Configuration
- Troubleshooting
- Technical Specifications
- Changelog
- License
- 4 audio inputs with seamless switching
- PT2314E audio processor:
- Volume control (0-56 user scale; PT2314 library inverts to hardware command)
- Bass/treble flat (0 dB) — tone is set with the amplifier module's own knobs on the front panel
- SSR relay control (4 channels)
- NVS persistence — all settings survive restart
- Deferred audio save — protects flash from wear during rapid adjustments
- NV3007 2.79" TFT display (142x428, used in landscape as 428x142, wide viewing angles) with Material Design
- 4 input tiles with 64x64 icons and a user-defined name under each (Web UI: Inputs → Edit); 400x120 boot logo shown centered
- Idle clock — after a configurable idle time the screen shows a dimmed clock with the Polish date and the active input (see Idle Clock)
- Sprite rendering — smooth animations without flicker
- Audio overlay with smooth fonts and segmented LED-style meters:
- Volume: 28 segments, colour by position Green→Yellow→Red
- Full screen: big value (
60%,font_volume), thick 28-segment bar, active input with its ring colour
- IR remote learning — universal recognition (NEC, RC6, Samsung, Sony, LG)
- Volume step +3 per button press, hold Vol+/Vol- for auto-repeat
- WiFi STA mode — connects to your home network
- WiFi AP fallback — configuration mode when STA fails
- WebSocket — real-time synchronization with JSON parsing and command validation
- MQTT — full bidirectional control with unique MAC-based ClientId
- OTA — firmware updates via WebUI with Basic Auth
- Home Assistant ready 🏠
| Component | Model | Pin |
|---|---|---|
| Microcontroller | ESP32-WROOM-32U | - |
| Audio Processor | PT2314E | SDA=21, SCL=22 |
| Display | NV3007 2.79" TFT 142x428 | CS=5, DC=17, RST=16, SCL=18, SDA=23, BL=15 |
| IR Receiver | TSOP32236 | OUT=19 |
| Relays | 4x SSR | 32, 33, 25, 26 |
| LED Ring | WS2812 8-bit (CJMCU) | DIN=27 |
| Volume knob | EC11 encoder with push button (KY-040 board, 3.3 V) | CLK=34, DT=35, SW=13 |
| Amplifier power | P-MOSFET AO4407A in +24 V, driven via PC817/PS2561 | 4 (HIGH = on) |
The previous ST7789 284x76 display is no longer supported.
| Module pin | ESP32 | Notes |
|---|---|---|
| GND | GND | |
| VDD | 5V | 3.3V also works (module has its own regulator) |
| SCL | GPIO18 | SPI clock |
| SDA | GPIO23 | SPI MOSI |
| RES | GPIO16 | Reset |
| DC | GPIO17 | Data/Command |
| CS | GPIO5 | Chip select |
| BL | GPIO15 | Backlight PWM (25 kHz) — connected directly; the module has its own backlight transistor, so the external transistor needed by the old display is not required |
src/
├── main.cpp # setup(), loop() — main loop
├── config.h # Pins, colors, layouts, timeouts
├── globals.h / globals.cpp # Global variable declarations and helpers
├── storage.cpp # NVS — loadPrefs, save*, deferred audio save
├── ui.cpp # Full TFT UI — tiles, overlays, splash, OTA screen
├── audio_ctrl.cpp # Audio, relay, input switch
├── network.cpp # WebSocket, OTA, MQTT, WiFi, HTTP server
├── clock.cpp # Idle clock (NTP time, dimmed clock screen)
├── ring.cpp # WS2812 ring under the volume knob
├── amp.cpp # Amplifier 24 V power switch + volume knob encoder (EC11)
├── PT2314.h / PT2314.cpp # PT2314E library (I2C)
├── web.h # Web interface (PROGMEM, generated)
├── icons.h # Input icons (generated)
├── logo.h # Boot logo (generated)
└── fonts.h # Smooth fonts with Polish characters: clock, date, input names (generated by tools/make_vlw.py)
lib/
├── TFT_eSPI/ # Pinned copy of TFT_eSPI 2.5.43 + NV3007 driver, with project User_Setup.h
├── clock_logic/ # Pure clock logic (unit-tested natively)
├── ir_logic/ # RC6 code with masked toggle bit (unit-tested natively)
├── ring_logic/ # LED ring colours (unit-tested natively)
├── name_logic/ # Input name cleanup / fitting (unit-tested natively)
├── meter_logic/ # Audio overlay meters: segments, colours, value text (unit-tested natively)
├── info_logic/ # Info screen: uptime, signal bars, heap text (unit-tested natively)
└── encoder_logic/ # Volume knob: quadrature decoder, acceleration, volume clamp (unit-tested natively)
hwtest/
└── lcd_test.cpp # Display bring-up sketch (env: lcdtest)
test/
└── test_*_logic/ # Native Unity tests for lib/*_logic (env: native)
tools/
└── make_vlw.py # Generates src/fonts.h
index.html # Web UI source → src/web.h
convert_icons.py # Generates src/web.h, icons.h, logo.h
input1-4.png, logo.png # Current graphics (64x64 icons, 400x120 logo) — copies of img/neon/
img/neon/ # Graphics set + source image
enclosure/ # 3D-printed enclosure (OpenSCAD), see enclosure/README.md
docs/ # Parts list, schematics, wiring, icon prompt
d0c/ # Module datasheets (local only, not in the repo)
pcb/ # KiCad project
src/web.h, src/icons.h and src/logo.h are generated — edit index.html / the PNGs in the repo root, then:
python3 convert_icons.py # needs Pillow (system python3)
cp web.h icons.h logo.h src/ && rm web.h icons.h logo.hGraphics: icons 64x64, logo 400x120, fully opaque on black (1-bit transparency in firmware). Requirements for new artwork: docs/prompt-ikony.md.
Note: Code was previously in a single
main.cpp(~2200 lines). It has been split into modules for better maintainability.
git clone https://github.com/mix-forever/Multi-Input-Audio-Switcher.git
cd Multi-Input-Audio-Switcher
pio run --target uploadOptional:
pio run -e lcdtest -t upload # display bring-up sketch (verify wiring, offset, colors)
pio test -e native # native unit tests (Unity, lib/*_logic, 51 tests)[platformio]
default_envs = esp32-wroom-32
[env:esp32-wroom-32]
platform = platformio/espressif32@6.12.0
board = esp32dev
framework = arduino
board_build.partitions = min_spiffs.csv
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
; TFT_eSPI is not in lib_deps — pinned, patched copy (NV3007) in lib/TFT_eSPI
lib_deps =
https://github.com/crankyoldgit/IRremoteESP8266.git
esphome/ESPAsyncWebServer-esphome@3.1.0
https://github.com/OttoWinter/async-mqtt-client.git
build_flags =
-D CORE_DEBUG_LEVEL=0
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1
-D DECODE_RC6=1
; Display bring-up sketch (colors, frames, offset, backlight PWM)
; pio run -e lcdtest -t upload
[env:lcdtest]
extends = env:esp32-wroom-32
build_src_filter = -<*> +<../hwtest/>
; Native tests of pure logic (lib/*_logic): pio test -e native
[env:native]
platform = native
test_framework = unity
lib_ignore = TFT_eSPITFT_eSPI is bundled in the repo — see section below.
TFT_eSPI is not downloaded by PlatformIO. lib/TFT_eSPI/ is a full, pinned copy of TFT_eSPI 2.5.43 with an added NV3007 driver (ported from mboehmerm/Three-IPS-Displays-with-ST7789-170x320-240x280-240x320), together with the project's User_Setup.h.
- Nothing needs to be copied into
.pio/libdepsanymore. pio run -t clean(and other libdeps refreshes) is safe — the display configuration lives in the repo.
| Setting | Purpose |
|---|---|
NV3007_OFFSET_A |
GRAM offset (default 12) |
TFT_RGB_ORDER |
Color order — switch TFT_RGB ↔ TFT_BGR if red and blue are swapped |
MULTIAMP_INVERT_BASE |
Base color inversion — the panel needs inversion; the "Invert colors" option in the Web UI is relative to it |
pio run -e lcdtest -t uploadhwtest/lcd_test.cpp shows RED/GREEN/BLUE/WHITE fills, edge frames in both rotations and a backlight ramp — use it after wiring to verify the offset and colors.
Screen orientation is handled in code via tft.setRotation(1|3).
| Section | Description |
|---|---|
| Inputs | 4 input tiles (active one glows in its ring colour), relay toggle under each; Edit shows name fields and ring colour pickers (Save / Cancel / Default colors) |
| Audio | Volume slider, amplifier power (always visible) |
| Settings Tab | Display & lighting (display brightness, ring brightness, invert, rotate, clock after idle, clock brightness) + IR codes (learned codes are saved immediately) |
| Network Tab | WiFi config + MQTT setup |
| System Tab | Security (admin password), Device (Restart button), OTA, System Logs |
Layout: Dashboard + Audio sliders at top, everything else organized in 3 tabs below.
- Enable logging toggle — show/hide logs
- Refresh button — manual refresh
- Clear button — clear log buffer
- Auto-refresh checkbox — automatic updates
- Interval field — set refresh rate (1-30 seconds)
WS2812 ring (CJMCU 8-bit) under the volume knob: DIN → GPIO 27, 5 V supply. Shows the active input's colour, set per input in the Web UI (Inputs → Edit) (defaults: IN1 white, IN2 (Bluetooth) blue, IN3 green, IN4 amber; "Default colors" restores them). Brightness = screen backlight × "Ring brightness" (Settings → Display & lighting) (0–100 %, 0 = off); in clock mode it dims the same way and restores on wake. Stored in NVS (ring_pct, ring_c0..3, deferred save). Defaults: lib/ring_logic/ring_logic.cpp.
The volume knob is an EC11 rotary encoder with a push button (KY-040 board powered from 3.3 V — its pull-ups on CLK/DT feed GPIO 34/35, which are input-only). The amplifier module's own volume pot is replaced by jumpers at full volume; volume is set only by the PT2314.
- Turn — volume 0–56 in steps of 1 per detent, faster spins step by 2 or 4 (
lib/encoder_logic); opens the audio overlay, synced with the Web UI / MQTT like the remote. - Short press — next input (like IR key Source); fires on release.
- Hold 2 s — amplifier on/off (fires while still held, the ring goes off / on;
ENC_LONG_PRESS_MS, logic inlib/encoder_logic). While the amplifier is off only the hold works — see Idle Clock. - Amplifier power — also IR key Power, Web UI (Audio → Amplifier power), WebSocket
set_amp_power, MQTT…/amp/set. The 24 V supply of the amplifier module is switched by a P-MOSFET (AO4407A) in the + line, driven from GPIO 4 through an optocoupler (schematic:docs/schemat_wzmacniacz_wl_enkoder.png, wiring:docs/montaz_klucz_wzmacniacza.png). Switching happens with the PT2314 speaker attenuators muted (sound returns 600 ms after power-on; power is cut 150 ms after muting) — no pops. The ring is off while the amplifier is off. The state is not stored: after a power cut or the mains rocker the amplifier always starts on. The mains rocker on the back switches everything off.
The encoder is polled every 1 ms by a separate task (GPIO interrupts would need IRAM code); direction can be flipped with ENC_REVERSE in config.h.
Mounting: break off the anti-rotation tab on the EC11 face before fitting it to the front and do not overtighten the nut — with the tab in place a tight nut bends the encoder body and the push button stops responding.
IR key Info (learn it in Settings → IR codes) shows a network & system screen: left column — WiFi signal bars + dBm, SSID, IP (large), MAC, MQTT status (in AP mode: AP name, IP, password, MAC); right column — firmware version, uptime, free heap, NTP sync. Values refresh every second (only changed fields are redrawn). Press Info again or any other key to close; it also closes after 15 s. Text/indicator logic: lib/info_logic (unit-tested).
The boot logo stays on screen with a status line and an animated progress bar while WiFi connects (like MixRadio): green bar + IP on success, orange bar + AP credentials on fallback, then the tiles. The logo is shown for at least 1.5 s.
After N seconds without activity (remote, Web UI or MQTT change) the screen switches to a dimmed clock:
- large HH:MM (24h) on the left with a blinking colon (on for the first half of each second), Polish date below (e.g. "niedziela, 25 października")
- active input icon + name on the right
Any remote button wakes the screen and performs its action. The clock is not shown while overlays are active (audio, OTA, WiFi, IR learning).
Amplifier off: 2 s after switching off (and after any Web UI / MQTT change) the same dimmed clock appears with a red power symbol and "OFF" instead of the input — regardless of the idle timeout, also without NTP sync (then without the time). The knob (turn, short press) and remote keys other than Power and Info do nothing and do not wake the screen; Web UI and MQTT work normally (e.g. pick an input before switching on).
Time source: NTP (pool.ntp.org, time.google.com), timezone Europe/Warsaw with DST (CET-1CEST,M3.5.0,M10.5.0/3). Requires internet access — in AP mode or without time sync the clock never appears.
| Option | Values | Default |
|---|---|---|
| Clock after idle | Off / 15 s / 30 s / 1 min / 2 min / 5 min | 30 s |
| Clock brightness | 5–100 % of normal brightness | 25 % |
| Command | Payload | Description |
|---|---|---|
set_clock_timeout |
{value: 0-3600} |
Idle time in seconds, 0 = off |
set_clock_dim |
{value: 5-100} |
Clock brightness in % of normal brightness |
set_ring_brightness |
{value: 0-100} |
Ring brightness in % of screen brightness (0 = off) |
set_ring_color |
{input: 0-3, color: "#RRGGBB"} |
Ring colour for an input |
set_input_names |
{name0..name3: "text"} |
Input names (max 12 chars, ASCII + Polish letters; empty = default InputN); missing fields unchanged |
restart |
{} |
Restart the device after saving pending settings; reply {"type":"restarting"} |
set_amp_power |
{value: true/false} |
Amplifier power (24 V switch); broadcast {"type":"amp_changed","on":…} |
The status JSON includes clock_timeout, clock_dim, ring_pct, ring_colors (4 × "#RRGGBB"), input_names (array of 4) and amp_on.
Published (state):
audio_switcher/status → "online" / "offline" (LWT, retained)
audio_switcher/state → active input as 1-4 (retained)
audio_switcher/audio → JSON: {"volume":0-100,"volume_raw":0-56}
audio_switcher/amp → amplifier power "ON" / "OFF" (retained)
Subscribed (commands, plain-number payloads — no JSON):
audio_switcher/set → input 1-4
audio_switcher/volume/set → volume percent 0-100
audio_switcher/amp/set → ON / OFF / TOGGLE (also 1 / 0)
mqtt:
select:
- name: "MultiAMP Input"
state_topic: "audio_switcher/state"
command_topic: "audio_switcher/set"
options: ["1", "2", "3", "4"]
number:
- name: "MultiAMP Volume"
state_topic: "audio_switcher/audio"
value_template: "{{ value_json.volume }}"
command_topic: "audio_switcher/volume/set"
min: 0
max: 100
unit_of_measurement: "%"| Protocol | Status | Notes |
|---|---|---|
| NEC | ✅ Full | Standard codes, repeat frames supported |
| RC6 | ✅ Full | Hash-based (toggle bit handling) |
| Samsung | ✅ Full | 32-bit |
| Sony | ✅ Full | 12/15/20-bit |
| LG | ✅ Full | 28-bit |
Any protocol decoded by IRremoteESP8266 can be learned — buttons are matched by their learned code (RC6 additionally by a hash of raw timings, see below). The Serial/WebUI log shows the decoded protocol name for every received code.
| Function | Default Action |
|---|---|
| Source | Next input (0→1→2→3→0) |
| Input 1-4 | Direct select |
| Vol+ | Volume +3, hold to auto-repeat |
| Vol- | Volume -3, hold to auto-repeat |
| Info | Network & system screen (toggle; closes after 15 s or on any other key) |
| Power | Amplifier on/off (like holding the volume knob for 2 s) |
Problem: Toggle bit changes with each press.
Solution: decoded RC6 value with the toggle bit cleared (lib/ir_logic, unit-tested):
// 20 bit (Mode 0) → toggle 0x10000; 24/32/36 bit (Mode 6A, MCE) → toggle 0x8000
irCode = rc6StableCode(results.value, results.bits);Result: Same button = same code. (v4.3 and older used a hash of raw timings rounded to 100 µs, which was unstable — the same button produced different hashes. Remote codes learned with older firmware must be learned again.)
VOLUME 75%
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▯▯▯▯▯▯▯
● Bluetooth ← active input, dot in its ring colour
- Vol+/- or the volume knob - adjust volume
- Auto-hide after 5 seconds
- Power on → attempts to connect to saved WiFi
- No saved network → setup mode: fullscreen AP info (SSID + password) with a countdown timer; the IR remote is disabled while this screen is shown
- Connect to AP "AudioSwitcher" (password:
audio1234), open http://192.168.4.1 - Configure WiFi → Save → device restarts and connects to your network (IP shown on display)
- After 3 minutes without configuration the screen returns to the normal UI and the remote is enabled — AP and WebUI stay active in the background
- Saved network unreachable (e.g. router down) → AP info is shown for 3 s, then the amp returns to normal operation (remote active, AP fallback available for reconfiguration)
- Enable MQTT toggle
- Host + port (default 1883)
- Authentication (optional)
- Topic prefix
- Save → Status: Connected ✅
MQTT config changes apply immediately — no device restart needed.
- Click Learn next to function
- Press button on remote (10s window)
- System saves code/hash
- Repeat for all buttons
- Test: press Vol+ → should increase volume 🔊
- Go to Security section in Web UI
- Admin Password — set password to protect OTA and WebSocket commands (empty = no protection)
AP password is fixed (
audio1234) and cannot be changed via WebUI.
Black screen:
- Check SPI wiring (see Display Wiring)
- Flash the bring-up sketch:
pio run -e lcdtest -t upload— color fills, edge frames and backlight ramp - Test backlight (GPIO15, connected directly to the module's BL pin)
Image shifted / garbage at the edges:
- Adjust
NV3007_OFFSET_Ainlib/TFT_eSPI/User_Setup.h(default 12)
Wrong colors:
- Red and blue swapped → change
TFT_RGB_ORDERinlib/TFT_eSPI/User_Setup.h - Inverted colors → check
MULTIAMP_INVERT_BASEand the "Invert colors" option in the Web UI (relative to the base)
Clock never appears:
- Requires NTP time sync (internet access) — not available in AP mode
- Check "Clock after idle" is not Off
Remote doesn't work:
- GPIO19 correctly connected
- Receiver powered from 3.3 V (at 5 V its output would drive 5 V into GPIO19)
- Check System Logs (
/api/logs) — shows protocol and code
Same button — different codes:
- RC6: should not happen since v4.4 (toggle bit is masked) — re-learn codes learned with older firmware
MQTT Status = Disconnected:
- Check System Logs — should show
WiFi: Got IPandMQTT: Configured for ... - Make sure broker is running and reachable
- Check if ClientId doesn't conflict with another device
- Toggle MQTT off/on in UI — reconnect happens automatically
Upload rejected (401 Unauthorized):
- Enter admin password in the Web UI prompt
- Or set password in Security section
Firmware doesn't boot:
- Check
board_build.partitions = min_spiffs.csv - Logs should show app0 ↔ app1 switching
- If always app0 — partition problem
- Bandwidth: 20Hz-20kHz
- THD+N: <0.1%
- SNR: >90dB
- Volume scale: 0-56 (user), PT2314 inverts to 0-63 hardware
- Boot: ~3s
- Switch time: <50ms
- WebSocket: <100ms
- Display: NV3007 2.79" TFT 142x428 (landscape 428x142), backlight PWM 25 kHz; sprite rendering, audio overlay refresh throttled to ~10 fps (reduces SPI interference)
- Clock: NTP (
pool.ntp.org,time.google.com), Europe/Warsaw with DST, 24h - OTA: stalled uploads abort after 30s
- Heap logging: every 30s in System Logs (
[HEAP] free=... minFree=... maxBlock=...)
- Tolerance: 30%
- Buffer: 300 raw values
- Debounce: 300ms (+150ms double-trigger filter)
- Volume step: +3/-3 per press
- Auto-repeat: every 150ms while holding Vol+/Vol-
- Mode: STA first, AP fallback
- AP SSID:
AudioSwitcher - AP Password:
audio1234(fixed) - Setup screen: 3 min countdown on first boot (IR disabled), then normal UI with AP still active
- Flash: 4MB
- RAM: ~17.7% static (~58KB used)
- Firmware: ~70.0% flash (~1.38MB)
- Partitions:
- app0: 1.875MB
- app1: 1.875MB (OTA)
- SPIFFS: 192KB
- 🎛️ Volume knob encoder + amplifier power — EC11 encoder replaces the module's volume pot (volume only in PT2314, steps with acceleration,
lib/encoder_logic, unit-tested); IR key "Power", Web UI, WSset_amp_poweror MQTTamp/setswitches the amplifier's 24 V via a P-MOSFET with PT2314 muting (no pops); always on after power-up, ring off while the amplifier is off; knob: short press = next input, hold 2 s = amplifier on/off; amplifier off: clock screen with a power symbol, knob/remote ignored except Power/Info - 🎚️ Audio overlay redesign — full-screen volume view: smooth fonts (
font_label, newfont_volumefor the big value), thick segmented LED-style meter with per-segment colours, value with units (lib/meter_logic, unit-tested) - ℹ️ Info screen — new IR key "Info": network (signal, SSID, IP, MAC, MQTT / AP credentials) and system (firmware, uptime, heap, NTP) on one screen (
lib/info_logic, unit-tested); IR keys handled through one table (IR_KEYS, NVSir_<key>) - 🚀 Boot screen — logo with a status line and an animated WiFi progress bar instead of a separate WiFi screen (like MixRadio)
- 🔄 Restart button — System → Device (with confirmation); pending deferred settings are saved first, the page reconnects automatically (
restart) - 🏷️ Firmware version in one place:
FW_VERSIONinconfig.h(boot banner, Info screen) - 🧰 Enclosure body rebuilt — 24 V PSU (105 × 56, M2×8) and ESP32 board (M2×10) screwed into wall bosses with self-tapping screws (no inserts inside); small boards snap into printed floor sleds (
sled_audioraised 2.5 mm over the foot bosses,sled_mains) slid in from the back; vents over both PSU heatsinks; collision checksclash,clash_sleds,clash_tools,clash_panels; front: volume scale (11 growing dots), "VOLUME / POWER" above and MultiAMP below the volume knob, min/centre/max dots and labels BASS / FREQ / TRE.VOL / TREBLE at the tone knobs; volume knob for the EC11 D-shaft (VOL_SHAFT, fitVOL_FIT0.15 from thed_bore_testsample); tone knobs with crush ribs; seeenclosure/README.md
- 🎛️ Tone only on the amplifier module — PT2314 bass/treble fixed at 0 dB (saved
audio_bass/audio_trebin NVS are ignored); removed Bass/Treble sliders from the Web UI, WSset_bass/set_treble, MQTTbass/set/treble/setand bass/treble status fields; the audio overlay shows only Volume; IR keys ↑/↓ (focus) removed
- 💡 Illuminated volume knob — WS2812 ring (8 LEDs, GPIO 27, same as MixRadio) shows the active input colour (IN1 white, IN2/Bluetooth blue, IN3 green, IN4 amber) and dims together with the screen in clock mode
- 🧰 3D-printed enclosure —
enclosure/(OpenSCAD, sibling of the MixRadio enclosure), seeenclosure/README.md - 🎮 RC6 remote codes — decoded value with the toggle bit masked instead of an unstable raw-timing hash; remote must be re-learned
- 🖥️ NV3007 2.79" display (142x428, landscape 428x142) replaces ST7789 284x76 — wiring change: BL connected directly to GPIO15 (module has its own backlight transistor)
- 🕒 Idle clock — dimmed NTP clock with Polish date and active input after idle; Web UI settings "Clock after idle" and "Clock brightness" (
set_clock_timeout,set_clock_dim) - 💡 Ring settings — ring brightness (% of screen, 0 = off) in Settings → Display & lighting, a colour per input in Inputs → Edit (
set_ring_brightness,set_ring_color) - 🧭 Web UI rework — each input card groups tile, relay toggle, name and ring colour; active tile glows in its ring colour; labelled sliders; "Connection lost" banner with controls disabled while the WebSocket reconnects; sliders throttled (≤ 1 message / 80 ms, final value always sent); logs polled only while the System tab is open; learned IR codes saved immediately ("Save IR Codes" button removed,
save_irstill accepted) - 🏷️ Custom input names — Inputs → Edit (max 12 chars, Polish letters), shown on the tiles, the clock screen and the IR list; stored in NVS (
set_input_names) - 🎨 New graphics — neon input icons in the LED ring colours (TV white, Bluetooth blue, radio green, music amber) and a 400x120 boot logo (
img/neon/) - ⏱️ Blinking clock colon — on for the first half of each second
- 📦 Pinned TFT_eSPI in repo —
lib/TFT_eSPI(2.5.43 + NV3007 driver); no more manualUser_Setup.hcopying - 🧪 Native unit tests —
pio test -e native(Unity,lib/clock_logic,ir_logic,ring_logic,name_logic) - 🔧
lcdtestenvironment — display bring-up sketch (pio run -e lcdtest -t upload)
- ✅ Log ring buffer — fixed out-of-bounds write (memory corruption after 60 log entries)
- ✅ IR protocol matching — handler compared against the wrong protocol constant (SAMSUNG instead of RC6); all protocols decoded by IRremoteESP8266 are now handled and learnable
- ✅ Thread safety — WS/MQTT commands are queued and executed in
loop(); no more I2C/TFT/NVS access from async callbacks; log buffer and WS auth list protected with spinlocks - ✅ Bass/Treble persistence — values of ±12/±14 dB no longer reset to 0 on boot
- ✅ OTA robustness — stalled uploads abort after 30s (IR no longer stays disabled), write errors abort immediately, audio unmutes and UI restores after a failed update
- ✅ WiFi test — restores the previous connection after the test finishes
- ✅ IR auto-repeat — holding Vol+/Vol- now repeats the adjustment (150ms rate)
- ✅ Audio overlay focus — returns to Volume after the overlay hides
- 🖥️ First-boot setup screen — AP credentials with a 3-minute countdown; IR disabled while shown; auto-exits to the normal UI (AP + WebUI stay active in background)
- 🌐 AP fallback UX — when a saved network is unreachable, AP info shows for 3s and the amp stays fully usable with the remote
- 🎮 IR log — shows the decoded protocol name for every received code
- 🎛️ Rebranding — "Audio Switcher" → "MultiAMP"
- 🖥️ Dashboard WebUI — redesigned layout: dashboard (inputs + relays) + audio sliders always visible, rest organized in 3 tabs (Settings/Network/System)
- 🖥️ Status dots — WiFi + MQTT connection status in header
- 🔒 Security — admin password (OTA Basic Auth, WebSocket authorization)
- 🌐 WiFi STA/AP modes — STA first, AP fallback for configuration
- 🌐 Async WiFi test — doesn't block the interface
- 🔄 Async restart after WiFi save — response sent before restart
- 🛡️ NVS data validation — all values clamped to valid ranges
- 💾 Deferred audio save — flash protection during rapid adjustments (1500ms delay)
- 📡 MQTT — unique MAC-based ClientId, topic validation, payload fragmentation
- 🔧 MQTT topic validation — removes problematic chars (
#,+,$) - 🖥️ System Logs — live logs in WebUI with enable/disable, auto-refresh, interval control
- 🖥️ OTA fullscreen UI — black screen with centered progress bar
- 🖥️ WiFi loading screen — clean fullscreen overlay with status
- ✅ Vol+/Vol- direction — fixed in IR
- ✅ Volume scale — unified 0-56 (0=mute, 56=max)
- ✅ Volume step — +3/-3 per press (no repeat needed)
- ✅ Bass/Treble scale — PT2314 API accepts dB (-14..+14), internal mapping
- ✅ Bass/Treble WebUI fix —
int8_t→intcast beforeString()in JSON (was sending ASCII chars instead of numbers) - ✅ Bass/Treble reset — sanity check on boot resets out-of-range values
- ✅ Audio soft-start —
audio.setVolume(0)instead of brokensetVolume(63) - ✅ PT2314 treble register — fixed
0x7F→0x77for 0dB - ✅ OTA upload — restored
FormDatawithupdatefield (multipart/form-data required by ESPAsyncWebServer) - ✅ WiFi auto-reconnect —
WiFi.setAutoReconnect(true)+WiFi.persistent(false)prevents flash wear - ✅ WiFi reconnect IP fix —
localIPnow updates correctly after WiFi reconnect - ✅ MQTT disconnect on WiFi loss —
mqttClient.disconnect()called onSTA_DISCONNECTED - ✅ WiFi events — new
ARDUINO_EVENT_*names for Arduino-ESP32 v3.x - ✅ OTA — nullptr partition checks, reject zero-size uploads
- ✅ OTA restart — moved from HTTP callback to
loop() - ✅ WebSocket parser —
extractJsonString/Int/UInt/Boolishextractors, error responses - ✅ JSON escape —
jsonEscape()for text fields in WebSocket status - ✅ WiFi test — async in
loop()instead of blocking loop in callback - ✅ IR double-trigger — 150ms debounce prevents duplicate actions
- Modular split —
config.h,globals.cpp,storage.cpp,ui.cpp,audio_ctrl.cpp,network.cpp - PlatformIO — pinned versions:
espressif32@6.12.0,ESPAsyncWebServer@3.1.0
MIT License — see LICENSE
- Bodmer — TFT_eSPI library
- crankyoldgit — IRremoteESP8266
- me-no-dev — ESPAsyncWebServer
- marvinroger — AsyncMqttClient
- mboehmerm — NV3007 driver port for TFT_eSPI
- ua6em — ST7789 284x76 offsets discovery (previous display)
- NV3007 TFT_eSPI port: GitHub
- TFT_eSPI Setup (previous ST7789 284x76 display): Arduino Forum
- IRremoteESP8266: GitHub
Made with ❤️ using ESP32
⭐ Star this repo if you find it useful!