Skip to content

Firmware protocol and diagnostics

For installation, wiring and normal operation, use the setup guide. This reference describes the C3 firmware and its host protocol.

Pairing feedback and diagnostics without external LEDs

The table shows each mode on the three external LEDs and on the onboard LED, so a board without external LEDs can still be diagnosed. The configured onboard LED is a single-color, active-low GPIO8 LED; it encodes session colors by pulse count, not by changing color. The SuperMini manufacturer guide documents that LED, and Apache NuttX's SuperMini reference describes GPIO8's polarity and reset strapping constraint. Board variants differ: this build does not detect whether an LED is fitted. A separate power LED is not controlled by this firmware. GPIO8 is driven only after normal boot; do not add external circuitry that pulls this strapping pin low during reset.

Mode / condition External red/yellow/green LEDs Onboard LED
startup: first three seconds after firmware setup All three breathe together for two 1.5-second rise-and-fall cycles Same fades, with its peak scaled to 32/255
preview: brightness slider on a connected lamp All three steadily show the preview percentage; returns to status two seconds after the last preview Same preview percentage applied to its 32/255 base
identify: Identify on a connected lamp Red → yellow → green three times, one LED at a time, 100 ms per LED (0.9 seconds total); the app requests 100% One 40 ms pulse per step at 32/255, scaled by the requested percentage
connected: first valid status write (N, R, Y or G) of each BLE connection; skipped during startup breathing, after a status disconnect or when interrupting a watchdog fade All three on once for 250 ms On for the same 250 ms at 32/255 brightness
status: fresh R, Y or G Corresponding LED steadily on Repeating groups: green = one, yellow = two, red = three 100 ms pulses, spaced 250 ms apart; one group every three seconds at 32/255
status: fresh N Off Off
fading: confirmed BLE disconnect while showing a status, or missing valid BLE updates for five seconds Disconnect: fade immediately from the displayed color and brightness to black over three seconds. Connected watchdog: hold for five seconds, fade for five seconds, fully off at ten seconds. Serial expiry fades over 700 ms Fades with the color if already lit; starts no new pulses
pairing: window open, no fresh status Red → yellow → green, 250 ms per step One 100 ms pulse per step at 32/255
unpaired: no board bonds, window closed, no fresh status All three one second on, one second off Same cadence at 32/255
waiting: board has bonds, BLE disconnected, window closed, no fresh status Off Off
stale: BLE connected, no fresh status, and either recovering after disconnect, confirmed status expired, or the board has bonds and the window is closed Off Off

The duty values above are at 100% lamp brightness. The saved setting scales every controlled output, including startup and connection indications; preview temporarily uses the slider's percentage instead; Identify uses its requested percentage (100% from the app). At 0% the controlled LEDs are dark. See brightness control.

The firmware picks the first matching mode in this order:

  1. Startup breathing: once per boot, for three seconds. Status and pairing continue in the background.
  2. Identify or preview: on a confirmed, authenticated connection with fresh status. Disconnect, lost authentication or stale status cancels it.
  3. Connection flash: 250 ms after the first valid status write of a connection. If that write arrives during startup breathing, no flash follows; startup already confirms power. Further N/R/Y/G writes update the stored status without canceling or restarting the flash. Disconnect, lost authentication or status expiry cancels it, so even a delayed first write cannot light the LEDs past the ten-second stale deadline.
  4. Fade: a confirmed BLE disconnect starts one three-second fade immediately from the displayed status color and brightness. Diagnostics already report state=N. Reconnection alone does not restore status or restart the fade. A fresh valid status cancels the fade and shows the latest color at the configured brightness; N stays off. Recovery skips the connection flash, including after the fade has finished. Queued pre-disconnect colors are discarded; an acknowledged N can still turn the lamp off. A connection that stays up without valid updates holds its status for five seconds, then fades for five seconds to black at the ten-second deadline. Its logical status remains until that deadline. Fading never changes the brightness setting. A fade starts from the current output capped at the configured brightness, including a brightness reduction received at fade entry. Off status, preview and connection flash do not start a fade. Timing follows the original disconnect or expiry, so a delayed loop cannot replay a completed fade.
  5. Fresh status, including serial commands and N.
  6. stale while recovering without fresh status or on a confirmed connection whose status expired, even if the pairing window is open.
  7. pairing, then unpaired, then stale (connected), then waiting.

For BLE, the watchdog fade takes the second half of the stale timeout.

Outputs use 2 kHz, eight-bit PWM. In platformio.ini, LED_RED_PIN=0, LED_YELLOW_PIN=1 and LED_GREEN_PIN=3 select the external LED pins (GPIO0..10, except 2, 8 and 9), and STATUS_TIMEOUT_MS=10000 sets the stale timeout (must exceed 2000 ms). BUILTIN_LED_PIN=8 and BUILTIN_LED_ACTIVE_LOW=1 select the usual onboard LED; set the pin to -1 to disable that output on a different board. BUILTIN_LED_BRIGHTNESS=32 controls its normal pulses. Waiting remains dark on all four outputs. Rebuild and flash the identified board after changing these options; PWM values are commanded duty levels, not measured brightness.

With USB serial connected, the firmware prints these lines:

  • At boot: gatt_change_manual=<0|1> result=<n>, then lamp=BroGlow C3 XXXX security=sc-bonded button=BOOT.
  • pairing=open duration_ms=60000 and pairing=closed.
  • authentication=ok reason=0 or authentication=failed reason=<n>, where n is the Bluetooth SMP failure code.
  • bond_reset=requested count=<n> or bond_reset=error after a 10-second BOOT hold. requested means deletion was requested, not confirmed.

A ?\n request returns a diagnostic line, even while BLE owns the lamp.

Set BOARD_PORT to the identified board as in Build and identify the board. Only one program can open the port. If BroGlow mirrors to this board over USB, run broglow ble off and wait three seconds (broglow ble on restores it); close foreground broglow led and other serial monitors. Run from the source checkout or the extracted package root:

uv run --frozen python - "$BOARD_PORT" <<'PY'
import sys
import time
import serial

with serial.Serial(sys.argv[1], 115200, timeout=0.5, write_timeout=2) as port:
    port.rts = False
    port.dtr = True
    time.sleep(0.3)
    port.reset_input_buffer()
    port.write(b"?\n")
    port.flush()
    until = time.monotonic() + 3
    while time.monotonic() < until:
        line = port.readline().decode(errors="replace").strip()
        if line:
            print(line)
        if line.startswith("state="):
            break
PY

The line reports stored session state, commanded red/yellow/green on/off flags, owner, connected, authenticated, pairing, bonds, age_ms (time since the last accepted status; board uptime before the first one), and valid-state accepted count. It also reports mode, red_pwm, yellow_pwm, green_pwm, builtin_pwm, builtin_pin, and brightness (current percentage; volatile, resets to 100 on reboot). The last four fields, gatt_change_manual, gatt_change_requests, gatt_change_request and gatt_change_result, belong to the GATT cache check. During mode=preview or mode=identify, the duties use the temporary requested percentage; the brightness field and stored session state continue to describe the underlying settings.

On/off flags mean a nonzero commanded duty, not pin readback. PWM values are 0..255 light-on duty after brightness scaling and before onboard active-low inversion, so state=N can coexist with nonzero duties during startup, preview, Identify, setup or the connection flash. authenticated=1 means the BLE link reached encrypted access; age_ms and accepted help verify two-second heartbeat delivery. None of these fields proves wiring, current or visible light. After diagnostics, move to brick-only power if testing operation with no USB data connection.

For setup, exact INFO\n (or CRLF) returns the identity without changing pairing, LEDs or timeout state. PAIR\n opens/renews the window and returns the same identity; it does not delete bonds or disconnect the current central. The CLI sends INFO before PAIR. Example reply (identity varies by board):

broglow_pair identity=001122334455 service=7d2ea28a-f7bd-485a-bd9d-92ad6ecfe93e

These setup commands also work while BLE owns the LEDs. The advertised identity is the six base-MAC bytes, not the possibly changing over-the-air private Bluetooth address.

BLE contract and firmware behavior

Item Value
Name BroGlow <label> after naming; otherwise BroGlow C3 plus a four-hex-digit board suffix
Advertisement Full service UUID and manufacturer data 0xffff followed by the six base-MAC bytes (the INFO identity)
Service UUID 7d2ea28a-f7bd-485a-bd9d-92ad6ecfe93e
Writable status characteristic 7d2ea28b-f7bd-485a-bd9d-92ad6ecfe93e
Writable brightness characteristic 7d2ea28c-f7bd-485a-bd9d-92ad6ecfe93e
Writable name characteristic 7d2ea28d-f7bd-485a-bd9d-92ad6ecfe93e
GATT operation Write with response
Status payload Exactly one ASCII byte: R, Y, G, or N; no newline
Brightness payload Exactly one binary byte, percentage 0..100; not ASCII digits. Board default 100; volatile until reset.
Preview payload On the same encrypted brightness characteristic: two binary bytes [percent, 1], with percent 0..100. Temporarily overrides all controlled lights for two seconds from receipt; does not change saved brightness.
Identify payload On the same encrypted brightness characteristic: two binary bytes [percent, 2], with percent 0..100. Scans red → yellow → green three times at 100 ms per LED (0.9 seconds from receipt), then restores the latest status and saved brightness. The app sends [100, 2]; no preferences change.
Name payload 1..21 UTF-8 bytes of the label, without the BroGlow prefix or control characters; a single 0x00 restores the factory name. The full Bluetooth name fits the 29-byte scan-response name field.
Host heartbeat Two seconds
Board stale timeout Ten seconds since the last valid status; BLE holds the color for five seconds and fades for the next five, reaching black and logical state N at ten seconds. Serial expiry fades over 700 ms after ten seconds. Then indications follow connection/bond state
Confirmed BLE disconnect Clears the BLE status to N immediately and fades the displayed status color and brightness to black over three seconds; recovery needs a fresh valid status and skips the connection flash
Ownership Connected BLE client; serial only while BLE is disconnected
Security LE Secure Connections Just Works bonding; all writable characteristics require encryption. New pairing is admitted only while the pairing window is open.
Pairing window 60 seconds. Opens at boot when the board has no bonds, on serial PAIR, or when BOOT (GPIO9) is held three seconds after normal boot. Holding BOOT ten seconds requests deletion of all bonds and reopens the window. See Pair without a USB data cable.

After discovery, the host writes brightness before its initial N/status write, so the firmware uses the saved level for its first connection indication. Later percentage changes are written on the existing connection.

The name characteristic receives the saved label on connection and when it changes, without disconnecting the lamp. Firmware updates the GAP device name and scan-response name, and commits changed labels to NVS; repeated names do not write flash. An empty write is ignored; only 0x00 clears a name. Invalid UTF-8, control characters and oversized labels are ignored. Old-connection requests are discarded; an acknowledged request from the current generation survives disconnect. Naming never refreshes session status or alters pairing keys or manufacturer identity. Storage failures report name=save_failed over serial; GAP name failures report name=apply_failed. A GATT write response alone does not prove persistence or a macOS Settings refresh. Reconnecting may be needed to refresh the Mac's cached name.

The UI throttles slider preview changes to about 80 ms and renews a held pointer every 500 ms. Identify sends one request for the selected connected lamp. The host retains only the latest event per target (or all-lamps request) for two seconds for both slider preview and Identify. Each connection uses the newer matching targeted or all-lamps event; a targeted request cannot affect a different lamp. Normal heartbeats do not renew either. Firmware discards an expired or previous-connection request.

Empty or multi-byte status payloads, unknown status bytes, brightness above 100, and brightness payloads other than one byte, [percent, 1] or [percent, 2] are ignored. Brightness, preview and Identify do not refresh the status timeout. A GATT write response only acknowledges receipt. All GPIO changes and timeout decisions happen in the main loop. Disconnect restarts advertising.

GATT cache after a firmware upgrade

A firmware update that changes the GATT services can leave a bonded Mac using its cached copy. The firmware requests a Service Changed indication for the connecting peer from ESP_GATTS_CONNECT_EVT, using BLEDevice::setCustomGattsHandler and the public esp_ble_gatts_send_service_change_indication(gatts_if, remote_bda) API (see the Espressif GATTS API and the IDF 4.4.7 indication dispatch and mode setter). This is intended to trigger rediscovery while preserving the existing bond.

To check it: gatt_change_manual=1 at boot and in ? confirms the firmware can send the indication; gatt_change_requests counts connections that requested the indication; gatt_change_request=0 means the indication was queued; gatt_change_result is the stack's send status (-1 until reported). None of these proves that macOS refreshed its cache. Do not erase bonds as a routine upgrade step.