Skip to content

About

ESP32-based 4-input audio switcher with TFT display, IR remote control and web interface.

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

🎛️ MultiAMP v4.5

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.

ESP32 PlatformIO MQTT License


📋 Table of Contents


✨ Features

Audio Processing

  • 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

Display and Control

  • 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

Connectivity

  • 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 🏠

🔧 Hardware

Components

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.

Display Wiring (NV3007)

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

📁 Project Structure

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

Regenerating generated files

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

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


📦 Installation

Quick Start

git clone https://github.com/mix-forever/Multi-Input-Audio-Switcher.git
cd Multi-Input-Audio-Switcher
pio run --target upload

Optional:

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

[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_eSPI

TFT_eSPI is bundled in the repo — see section below.


⚠️ TFT_eSPI Configuration

Pinned library in the repository — no manual steps

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/libdeps anymore.
  • pio run -t clean (and other libdeps refreshes) is safe — the display configuration lives in the repo.

Panel-specific settings (lib/TFT_eSPI/User_Setup.h)

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

Hardware bring-up sketch

pio run -e lcdtest -t upload

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


🌐 Web Interface

Sections

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.

System Logs Controls

  • 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)

💡 Volume Knob Ring

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.

🎛️ Volume Knob and Amplifier Power

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

ℹ️ Info Screen

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

🚀 Boot Screen

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.

🕒 Idle Clock

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.

Settings (Web UI → Settings → Display)

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 %

WebSocket Commands

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.


📡 MQTT API

Topics

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)

Home Assistant Example

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: "%"

🎮 IR Remote

Supported Protocols

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.

Button Mapping

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)

RC6 Toggle Bit Problem

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

Audio Overlay Navigation

VOLUME                                    75%
▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▯▯▯▯▯▯▯
● Bluetooth                    ← active input, dot in its ring colour
  • Vol+/- or the volume knob - adjust volume
  • Auto-hide after 5 seconds

⚙️ Configuration

First Boot

  1. Power on → attempts to connect to saved WiFi
  2. No saved network → setup mode: fullscreen AP info (SSID + password) with a countdown timer; the IR remote is disabled while this screen is shown
  3. Connect to AP "AudioSwitcher" (password: audio1234), open http://192.168.4.1
  4. Configure WiFi → Save → device restarts and connects to your network (IP shown on display)
  5. 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
  6. 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)

MQTT Setup

  1. Enable MQTT toggle
  2. Host + port (default 1883)
  3. Authentication (optional)
  4. Topic prefix
  5. Save → Status: Connected ✅

MQTT config changes apply immediately — no device restart needed.

IR Remote Learning

  1. Click Learn next to function
  2. Press button on remote (10s window)
  3. System saves code/hash
  4. Repeat for all buttons
  5. Test: press Vol+ → should increase volume 🔊

Security

  1. Go to Security section in Web UI
  2. Admin Password — set password to protect OTA and WebSocket commands (empty = no protection)

AP password is fixed (audio1234) and cannot be changed via WebUI.


🐛 Troubleshooting

Display Issues

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_A in lib/TFT_eSPI/User_Setup.h (default 12)

Wrong colors:

  • Red and blue swapped → change TFT_RGB_ORDER in lib/TFT_eSPI/User_Setup.h
  • Inverted colors → check MULTIAMP_INVERT_BASE and 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

IR Issues

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

WiFi / MQTT

MQTT Status = Disconnected:

  • Check System Logs — should show WiFi: Got IP and MQTT: 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

OTA Issues

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

📝 Technical Specifications

Audio

  • Bandwidth: 20Hz-20kHz
  • THD+N: <0.1%
  • SNR: >90dB
  • Volume scale: 0-56 (user), PT2314 inverts to 0-63 hardware

System

  • 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=...)

IR

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

WiFi

  • 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

Memory

  • Flash: 4MB
  • RAM: ~17.7% static (~58KB used)
  • Firmware: ~70.0% flash (~1.38MB)
  • Partitions:
    • app0: 1.875MB
    • app1: 1.875MB (OTA)
    • SPIFFS: 192KB

📋 Changelog

v4.5 (2026-10-31)

🆕 New

  • 🎛️ 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, WS set_amp_power or MQTT amp/set switches 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, new font_volume for 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, NVS ir_<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_VERSION in config.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_audio raised 2.5 mm over the foot bosses, sled_mains) slid in from the back; vents over both PSU heatsinks; collision checks clash, 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, fit VOL_FIT 0.15 from the d_bore_test sample); tone knobs with crush ribs; see enclosure/README.md

🔄 Changed

  • 🎛️ Tone only on the amplifier module — PT2314 bass/treble fixed at 0 dB (saved audio_bass/audio_treb in NVS are ignored); removed Bass/Treble sliders from the Web UI, WS set_bass/set_treble, MQTT bass/set/treble/set and bass/treble status fields; the audio overlay shows only Volume; IR keys ↑/↓ (focus) removed

v4.4 (2026-10-27)

🆕 New

  • 💡 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), see enclosure/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_ir still 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 manual User_Setup.h copying
  • 🧪 Native unit tests — pio test -e native (Unity, lib/clock_logic, ir_logic, ring_logic, name_logic)
  • 🔧 lcdtest environment — display bring-up sketch (pio run -e lcdtest -t upload)

v4.3

🔧 Fixes

  • ✅ 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

🆕 New

  • 🖥️ 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

v4.2

🆕 New

  • 🎛️ 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

🔧 Fixes

  • ✅ 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 → int cast before String() 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 broken setVolume(63)
  • ✅ PT2314 treble register — fixed 0x7F → 0x77 for 0dB
  • ✅ OTA upload — restored FormData with update field (multipart/form-data required by ESPAsyncWebServer)
  • ✅ WiFi auto-reconnect — WiFi.setAutoReconnect(true) + WiFi.persistent(false) prevents flash wear
  • ✅ WiFi reconnect IP fix — localIP now updates correctly after WiFi reconnect
  • ✅ MQTT disconnect on WiFi loss — mqttClient.disconnect() called on STA_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/Boolish extractors, 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

🏗️ Architecture

  • 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

📄 License

MIT License — see LICENSE


🙏 Acknowledgments

  • 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)

🔗 Links


Made with ❤️ using ESP32

⭐ Star this repo if you find it useful!

About

ESP32-based 4-input audio switcher with TFT display, IR remote control and web interface.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages