Talking to Halo Over Bluetooth


You may want to communicate with Halo manually over Bluetooth Low Energy if we don’t yet have an SDK available for your preferred development language, or if you want full control of what’s going on under the hood.

Once you’re communicating with Halo over Bluetooth, you can use the Lua API Reference to see everything you can tell it to do.

  1. Pairing & Connecting
    1. Connection Behavior
    2. Bluetooth Connection Diagram
  2. Bluetooth Services & Characteristics
    1. Halo Lua Service
    2. Battery Service
    3. OTA Service
    4. LE Audio Service
  3. Executing Lua Statements Over Bluetooth
    1. Bluetooth Lua Execution Sequence
  4. Sending Data
    1. Bluetooth Raw Data Exchange Sequence
  5. Control Characters
  6. Lua Main Loop — A Special Case
  7. Camera
  8. Audio
    1. Sending audio to Halo (playback)
    2. Receiving audio from Halo (recording)
  9. Firmware Updates

Pairing & Connecting

Halo uses BLE bonding and must pair with a host device before any communication can take place. When attempting to connect to Halo using an SDK, Halo will automatically initiate pairing, which the OS handles and the user will have to agree to.

Halo stores up to 5 bonded devices. Out of the box (or after a factory reset) it is always pairable. Once at least one device is bonded, unknown devices are refused — to pair an additional device, hold Halo’s button for 5 seconds (a chime plays and the LED starts flashing) to open a ~60-second pairing window; the next new device to connect is paired and bonded. If all 5 slots are in use, the least recently used bond is replaced.

Device Naming: Halo devices are named as Halo XX, where XX is the 4th byte (in hex) of the device’s EUI-48 MAC address.

Full Bluetooth Connection Details

Connection Behavior

  • Halo accepts one connection at a time: connectable advertising stops while a device is connected and resumes on disconnect, so a second bonded device doesn’t see Halo until the first releases it. A connection from a device Halo doesn’t recognize is dropped without pairing unless the pairing window is open — if your connection appears to succeed and then immediately drops, this is why.
  • While the pairing window is open, already-bonded devices are refused so the new device can get in; they reconnect normally once the window closes (on a successful pairing, after ~60 seconds, or on reboot). Opening the window disconnects any current connection.
  • A host where you used “Forget This Device” can re-pair itself on its next connection — no button press needed.
  • To make Halo forget all paired devices, factory reset it (15-second button hold, charger disconnected).

Bluetooth Connection Diagram

Bluetooth connection sequence diagram

Bluetooth Services & Characteristics

Halo implements four BLE services:

Halo Lua Service

Service UUID: 7A230001-5475-A6A4-654C-8431F6AD49C4

Characteristic names are from the host’s perspective — TX means the host transmits to Halo, RX means the host receives from Halo.

CharacteristicUUIDPermissionsDescription
LUA TX7A230002-5475-A6A4-654C-8431F6AD49C4Write Without Response, WriteHost sends Lua commands and data to Halo
LUA RX7A230003-5475-A6A4-654C-8431F6AD49C4NotifyHost receives Lua print() output and data from Halo
AUDIO TX7A230005-5475-A6A4-654C-8431F6AD49C4Write Without Response, WriteHost streams PCM or LC3 audio to Halo’s speaker

The maximum packet size on each characteristic is the negotiated MTU (up to 512 bytes).

The service also exposes two further characteristics — 7A230004 (Notify) and 7A230006 (Notify) — which you will see when enumerating the service but which the firmware does not use.

Camera images and microphone audio are not streamed on dedicated characteristics. Camera image chunks are sent from Halo to host over the regular LUA RX characteristic using frame.bluetooth.send(). Microphone audio is also sent back on the LUA RX characteristic in the same way.

Battery Service

Service UUID: 0x180F (Standard BLE Battery Service)

CharacteristicUUIDPermissionsDescription
Battery Level0x2A19Read, NotifyBattery level (0–100%)
Battery Power State0x2A1ARead, NotifyBitfield: battery presence, charging, and discharging state

OTA Service

Service UUID: 8D53DC1D-1DB7-4CD3-868B-8A527460AA84

CharacteristicUUIDPermissionsDescription
SMPDA2E7828-FBCE-4E01-AE9E-261174997C48Write Without Response, Write, NotifySMP firmware update control

Firmware updates use the MCU-BOOT scheme over BLE using the Simple Management Protocol (SMP). See Firmware Updates below.

LE Audio Service

Halo is also a standard LE Audio device: a BAP Unicast Server with a speaker sink and a microphone source using the LC3 codec (8/16 kHz in both directions, with 32/48 kHz capture also available), plus volume control (VCS) and host-side microphone mute (MICS). Hosts with LE Audio support can stream audio to Halo’s speaker and record from its microphone through these standard profiles, without using the custom characteristics at all.

An LE Audio stream established by the host takes priority over the custom channels. In particular, an active frame.microphone capture is preempted — frame.microphone.status() returns "le_audio" and frame.microphone.start() fails until the host releases the stream.

Executing Lua Statements Over Bluetooth

Send Lua commands to Halo over Bluetooth using a REPL-like interaction on the LUA TX characteristic. Halo evaluates every message and only returns a response if it resulted in a Lua print() call, or if an error occurred.

print('hello world') -- Returns 'hello world' on LUA RX
print(1 + 2)         -- Returns '3'
a = 1 + 2            -- Evaluates and stores, no output
1 + 2                -- Returns an error

The length of data on LUA TX is limited to the negotiated MTU. To execute larger scripts, first save them to the device filesystem using frame.file.open() / f:write() / f:close(), then execute with require().

Responses on LUA RX are also limited to the MTU. For very long strings, break them into smaller print() calls.

To determine the MTU in use: frame.bluetooth.max_length() in Lua.

Bluetooth Lua Execution Sequence

Bluetooth Lua execution sequence diagram

Sending Data

For binary payloads (e.g. image data, sprite data), prefix the payload with a byte of value 0x01 on the LUA TX characteristic. This triggers a callback if one was registered with frame.bluetooth.receive_callback(). The maximum data payload per write is best obtained by calling frame.bluetooth.max_length() on the device (the usable ATT payload less the 0x01 marker byte, i.e. ATT MTU − 8).

Raw byte data can be returned to the host using frame.bluetooth.send(). It is prefixed with 0x01 in the first byte of the LUA RX notification.

Bluetooth Raw Data Exchange Sequence

Bluetooth Raw Data Exchange Sequence Diagram

Control Characters

While a Lua script is running, Halo will ignore additional Lua strings on LUA TX (raw data with 0x01 prefix is still processed). To interrupt a running script, send one of these single-byte control signals on LUA TX:

ByteSignalEffect
0x02CTRL+BReboot device
0x03CTRL+CInterrupt running script
0x04CTRL+DRestart Lua runtime / run main.lua
0x05CTRL+EReset and remove main.lua
0x06CTRL+FExit Lua runtime completely
0x07CTRL+GRemove all files/folders (except settings)

Most commonly used:

  • 0x03 (break) — terminate any running script or loop
  • 0x04 (reset) — clear all variables and run main.lua if it exists

Lua Main Loop — A Special Case

require('my_module') executes the entire file my_module.lua. If the file contains a main loop, it may never return. Subsequent Lua REPL commands sent on LUA TX will not execute because the previous command hasn’t returned — however, raw data (with 0x01 prefix) will still be processed by receive_callback().

This behavior is the basis of Haloside applications: once a Lua file is running in a main loop, all host↔Halo communication takes place through raw data messages on LUA TX (host→Halo) and LUA RX (Halo→host). The Lua loop receives messages from the host via receive_callback() and sends data back using bluetooth.send().

Lua loops can be interrupted with the break (0x03) and reset (0x04) control signals. When first connecting to Halo, it’s good practice to send a break signal to ensure the device is in REPL mode before sending new application files.

If main.lua exists on the filesystem, it is automatically run after reboot or after a reset (0x04) signal. If it contains a main loop, send a break (0x03) signal after the reboot (with a short delay) to ensure further Lua REPL commands can execute.

Camera

Camera images are captured using the Lua camera API (frame.camera.capture() / frame.camera.read()), and the Lua app is responsible for sending image chunks back to the host using frame.bluetooth.send() — which delivers them on the LUA RX characteristic.

A typical Lua camera pattern:

frame.camera.capture({ quality = "HIGH" })
while not frame.camera.image_ready() do frame.sleep(0.05) end

local mtu = frame.bluetooth.max_length()
while true do
    local data = frame.camera.read(mtu)
    if data == nil then break end
    frame.bluetooth.send(data)  -- arrives on LUA RX
end

Audio

Sending audio to Halo (playback)

Audio data for playback is written to the AUDIO TX characteristic (7A230005). Start the speaker first using frame.speaker.start() over the Lua channel, then stream PCM or LC3 audio frames directly to AUDIO TX — bypassing the Lua VM for low-latency delivery to the speaker hardware.

Receiving audio from Halo (recording)

There is no dedicated audio receive characteristic. Microphone audio is sent back to the host via frame.bluetooth.send() in Lua, which delivers it on the LUA RX characteristic. Start the microphone using frame.microphone.start(), then read chunks and send them in the main Lua loop.

LC3 format: 7.5 ms or 10 ms frames, e.g. 16kHz 16-bit mono at 16kbps. (The duration values 750 and 1000 in the Lua API are in units of µs/10.)

Firmware Updates

Halo firmware updates use the MCU-BOOT bootloader scheme over BLE, using the Simple Management Protocol (SMP) on the OTA service.

The latest official firmware release is available from the halo-firmware releases page.

For reference implementations of MCU-BOOT DFU over BLE: