Skip to content

Architecture Overview

cklit edited this page Sep 17, 2026 · 1 revision

Architecture Overview

BGAdaptor sits between an old B&O Datalink deck and the modern networked world. Two independent protocol boundaries meet in the middle of the firmware:

Deck (Datalink UART, 320 baud)
        │
        ▼
   src/beogram.cpp   ← transport-agnostic deck protocol (byte sequences ⇄ BeogramFeedback/BeogramCommand)
        │
        ▼
   src/transport.h + transport_ase.cpp / transport_moz.cpp   ← ASE or Mozart
        │
        ▼
   B&O product over network

Everything else hangs off that spine:

  • src/state.h / state.cpp — the shared-globals module. All timers, connection flags, persisted settings, and peripheral objects (NeoPixel, Preferences, HTTP client, WebSocket clients, web server) live here. Every other module reads/writes this state rather than passing data around directly — it's the seam where you can see how modules actually talk to each other.
  • src/discovery.cpp — mDNS scanning, used only to populate web-UI pickers and to power self-healing of a lost product IP. Actual runtime connections always use the stored IP.
  • src/halo.cpp — Beoremote Halo WebSocket integration (tactile remote). Independent of ASE/Mozart; both transports call into it to reflect state.
  • src/peer.cpp — links a second BGAdaptor so its deck appears as a second Halo page. One-way control (HTTP), state flows back over the peer's own UI-push WebSocket.
  • src/ha_mqtt.cpp — Home Assistant MQTT auto-discovery entities.
  • src/webui.cpp / webpush.cpp / webpage.h — local HTTP config/control page, plus a WebSocket (webpush) that pushes live state to that page (and to a linked peer).
  • src/led.cpp — single-NeoPixel connectivity status.
  • src/main.cpp — wires all of the above together in setup()/loop().

main.cpp responsibilities

setup(): bring up both serial ports, NeoPixel, WiFi (WiFiManager), mDNS, load all Preferences (including a migration path from older firmware that stored separate wsIP/sseIP instead of a unified productIP), wire up Mozart/Halo WebSocket callbacks, register HTTP routes, start the UI push socket, advertise mDNS services (_http._tcp, _bgadaptor._tcp with fn/id/deck/ver TXT records), and force-connect MQTT and (for ASE) SSE.

loop() is a round-robin: LED update → poll active transport → Halo connect/ping → product-recovery check → pending Beolink-expand check → read deck UART → webpushLoop() → peerLoop() → HTTP server → WiFi reconnect check → delayed-PLAY-after-digit → stopped-track timeout → Halo page/button updates → MQTT loop → tiny debug console.

Why two transports?

B&O has shipped two generations of network API on their products:

  • ASE — the legacy API (Server-Sent Events + BeoZone REST, port 8080). See Transport: ASE.
  • Mozart — the current API (dual WebSockets + /api/v1 REST, port 9339). See Transport: Mozart.

BGAdaptor auto-selects per installation (Platform setting, changeable in the web UI) so one firmware image supports both product generations. The deck-facing side (beogram.cpp) is identical either way — only the "talk to the product" side differs.

Clone this wiki locally