Skip to content

Connecting the Dongle

OneSeventyFour edited this page May 15, 2026 · 2 revisions

Connecting the dongle

The dongle is the host's interface to the rest of the system. It's a small USB-C-attached box that contains:

  • An ESP32-S2 microcontroller running os4_dongle.ino.
  • An nRF24L01+ radio for the 2.4 GHz custom protocol.
  • A 433 MHz transmitter for legacy Bilusocn one-way receivers.
  • Three switches the operator interacts with: start/stop, arm, and manual fire.
  • 7 NeoPixel status LEDs.

The host computer talks to the dongle exclusively over USB-CDC at 115200 8N1.

How the host actually reaches the dongle

There's a chain of three components:

[ Daemon (in Docker) ]
   │  TCP to host.docker.internal:9000
   ▼
[ tcp_serial_bridge (Python, on host) ]
   │  pyserial @ 115200 8N1
   ▼
[ Dongle USB-CDC serial ]

Why the bridge? Docker Desktop on macOS and Windows runs in a VM that doesn't get easy access to the host's USB devices. The bridge runs natively on the host, holds the serial connection open, and exposes it as TCP. The daemon (which lives in the container) connects via TCP. On Linux you could in principle skip the bridge and pass /dev/ttyACM* straight into the container with a devices: mapping, but the bridge keeps the architecture identical across all three OSes — so we use it everywhere.

Identifying your serial port

OS Run this Typical answer
macOS ls /dev/tty.usbmodem* /dev/tty.usbmodem01 (or /dev/tty.usbmodem1101 etc.)
Linux ls /dev/ttyACM* /dev/ttyUSB* /dev/ttyACM0
Pi (installed via install.sh) ls -l /dev/byh_dongle /dev/byh_dongle -> ttyACM0
Windows Device Manager → Ports (COM & LPT) COM5 (varies)

Set the result as system.dongle_port in host/config/systemcfg.json:

{
  "host_version": 0.08,
  "system": {
    "dongle_port": "/dev/tty.usbmodem01",
    "dongle_baud": 115200,
    "dongle_protocol": "BKYD_TS_HYBRID"
  }
}

dongle_baud is fixed at 115200 by the dongle hardware — do not change it.

On a Pi: /dev/byh_dongle

On a Pi installed with host/run/pi/install.sh, the installer drops a udev rule that creates the stable symlink /dev/byh_dongle pointing at whatever /dev/ttyACM* slot the dongle landed on. The installer also auto-fills this path into systemcfg.json during install, so a fresh Pi just works:

"dongle_port": "/dev/byh_dongle"

Reasons to prefer the symlink over the raw /dev/ttyACM* path:

  • It survives /dev/ttyACM0 ↔ /dev/ttyACM1 order swaps when the dongle re-enumerates after a reboot or reflash.
  • flash_dongle.py uses it as its top-priority port-discovery candidate — so the same script that boots the host can also flash the dongle without configuration drift.
  • update_dongle.sh (the Pi's one-button dongle-firmware updater) assumes it.

Confirming the connection

After running the platform's launcher (host/run/pi/start.sh, host/run/osx/start.sh, or host/run/windows/start.bat), the bridge log will print:

Serial connected: /dev/tty.usbmodem01 at 115200 baud
TCP server listening on 0.0.0.0:9000
Accepted connection from ('192.168.65.2', 53114)

In the web UI's status bar (bottom of every page), you should see:

  • Link light: green ("WS connected, daemon fresh").
  • Daemon light: green.
  • Dongle: shows the active protocol (e.g. BKYD_TS_HYBRID), a small TX activity pulse, and the dongle's command-queue gauge.

If Dongle is yellow ("Silent") for more than ~10 s after start, the daemon is connected to the bridge but isn't getting per-second status frames back from the dongle. Likely causes:

  • Wrong serial port in systemcfg.json.
  • Dongle firmware not flashed (USB enumerates but no JSON comes back). See Flashing a Dongle.
  • Bad cable. Try another USB-C cable — some are charging-only and don't carry data.

Auto-reconnect

The bridge automatically reopens the serial port if the dongle reboots (its hardware watchdog firing during a hostile RF link, or you unplug/replug). Backoff goes from 1 s to 5 s. You'll see lines like Serial auto-reconnected: ... once it comes back.

The dongle's hardware watchdog has a 10-second timeout (see Dongle firmware → Watchdog). If the daemon stops getting status frames for >10 s during normal operation, that's the most likely culprit — the bridge will silently reconnect.

Stable port names (Linux, optional)

If you have multiple USB-CDC devices and the order can shift across reboots, you can pin a name with udev. On a Pi this is automatic via install.sh — you'll already have /dev/byh_dongle. For other Linux hosts, save as /etc/udev/rules.d/99-byh-dongle.rules:

SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="80??", \
  SYMLINK+="byh_dongle"

(Use lsusb to confirm the vendor/product IDs for your dongle.) Then:

sudo udevadm control --reload && sudo udevadm trigger

Set dongle_port: "/dev/byh_dongle" in systemcfg.json and you'll be immune to enumeration-order changes.

The three physical switches

These are read by the dongle firmware, encoded into a JSON gpio_status message every state change, and sent up to the daemon. The daemon then enforces interlocks (you can't load a show with the start switch up; you can't fire manually without arm + start + manual fire all engaged).

Switch UP / open (HIGH) DOWN / closed (LOW)
Start / Stop STOP — show paused / cannot start START — show schedule running
Arm DISARMED — fire commands rejected ARMED — fire commands allowed
Manual fire OFF — show mode ON — manual cue firing only

The UI shows each switch's current state in the status bar and gates its action buttons accordingly. See Show lifecycle for how these interact during a show.

Restarting the dongle

Two options:

  1. Physical: unplug and replug. The bridge will auto-reconnect within a couple of seconds.
  2. From the UI: click the Restart button next to "Dongle" in the status bar — this re-sends the select_serial configuration command to the bridge, which closes and reopens the port.

You should not need to do this routinely. The dongle is intended to stay up across days.

Clone this wiki locally