Skip to content

Architecture

mprattmd edited this page May 31, 2026 · 1 revision

Architecture

Handi-Talky Commander is a cross-platform port of the original Windows/WinForms HTCommander. The strategy: extract all platform-neutral logic into a shared Core, isolate OS-specific concerns behind interfaces, and build an Avalonia UI on top.


Project layout

cross/HTCommander.Core            (net9.0)          ← portable: protocols, codecs, DSP, broker, abstractions
cross/HTCommander.Platform.Linux  (net9.0)          ← BlueZ/RFCOMM, PortAudio, SQLite, JSON config
cross/HTCommander.Platform.Mac    (net9.0)          ← IOBluetooth bridge, PortAudio, SQLite, JSON config
cross/HTCommander.UI.Avalonia     (net9.0)          ← the cross-platform UI (this app)

src/                              (net9.0-windows)  ← original WinForms app, shares the same Core
  • HTCommander.Core — portable, UI-agnostic logic: radio control, AX.25, APRS, the SBC voice codec, Winlink, BBS, channel import/export, and the DSP engines (AFSK/PSK/G3RUH/HDLC, Morse, DTMF). No platform dependencies — uses only System.*.
  • HTCommander.Platform.Linux — Linux backends: BlueZ/RFCOMM transport, PortAudio audio, SQLite mail store, JSON config.
  • HTCommander.Platform.Mac — macOS backends: an IOBluetooth RFCOMM bridge (libhtbt) plus PortAudio / SQLite / JSON.
  • HTCommander.UI.Avalonia — the Avalonia XAML UI.

A shared static DataBroker (publish/subscribe) connects the layers and marshals to the UI thread.


The platform seam (key interfaces)

Platform-specific behavior is hidden behind interfaces in HTCommander.Core/Abstractions, so the Core and UI never reference an OS API directly:

Interface Purpose
IRadioTransport / IRadioTransportDiscovery Bluetooth connect / discovery / read-write
IRadioPlatform platform entry point that wires up a radio backend
IRadioAudioChannel radio Bluetooth audio (SBC) stream
IAudioCapture / IAudioPlayback / IAudioDeviceEnumerator mic / speaker / device list (PortAudio)
IConfigStore settings persistence (Registry on Windows, JSON elsewhere)
IUiDispatcher thread marshalling (WinForms Invoke / Avalonia Dispatcher.UIThread)
ILogger, IPlatformTimer / ITimerFactory logging and injectable timers
IAprsTransport / IRadioTransport decouple protocol stacks from the host app

Why Bluetooth was the hard part

The radios use Classic Bluetooth RFCOMM / SPP, not BLE GATT:

  • Linux uses BlueZ (raw RFCOMM/SDP) directly.
  • macOS can't use the public CoreBluetooth API (no classic-RFCOMM support), so the port ships a small IOBluetooth bridge to do RFCOMM. "Avalonia gets macOS for free" applies to the UI and build — not to radio connectivity, which needed dedicated work.

Notable porting swaps

Windows Cross-platform
WinForms Avalonia (XAML)
Registry (Microsoft.Win32) JSON config store (XDG paths on Linux)
NAudio (WASAPI) PortAudio
GMap.NET.WinForms Mapsui (Avalonia-native)
System.Drawing custom painting Avalonia DrawingContext / FormattedText
InvokeRequired / BeginInvoke Dispatcher.UIThread via IUiDispatcher
WinRT / IOBluetooth (RFCOMM) BlueZ (Linux) / IOBluetooth bridge (macOS)

See also

Clone this wiki locally