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.
- Pairing & Connecting
- Bluetooth Services & Characteristics
- Executing Lua Statements Over Bluetooth
- Sending Data
- Control Characters
- Lua Main Loop — A Special Case
- Camera
- Audio
- 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 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.
| Characteristic | UUID | Permissions | Description |
|---|---|---|---|
| LUA TX | 7A230002-5475-A6A4-654C-8431F6AD49C4 | Write Without Response, Write | Host sends Lua commands and data to Halo |
| LUA RX | 7A230003-5475-A6A4-654C-8431F6AD49C4 | Notify | Host receives Lua print() output and data from Halo |
| AUDIO TX | 7A230005-5475-A6A4-654C-8431F6AD49C4 | Write Without Response, Write | Host 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)
| Characteristic | UUID | Permissions | Description |
|---|---|---|---|
| Battery Level | 0x2A19 | Read, Notify | Battery level (0–100%) |
| Battery Power State | 0x2A1A | Read, Notify | Bitfield: battery presence, charging, and discharging state |
OTA Service
Service UUID: 8D53DC1D-1DB7-4CD3-868B-8A527460AA84
| Characteristic | UUID | Permissions | Description |
|---|---|---|---|
| SMP | DA2E7828-FBCE-4E01-AE9E-261174997C48 | Write Without Response, Write, Notify | SMP 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
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
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:
| Byte | Signal | Effect |
|---|---|---|
0x02 | CTRL+B | Reboot device |
0x03 | CTRL+C | Interrupt running script |
0x04 | CTRL+D | Restart Lua runtime / run main.lua |
0x05 | CTRL+E | Reset and remove main.lua |
0x06 | CTRL+F | Exit Lua runtime completely |
0x07 | CTRL+G | Remove all files/folders (except settings) |
Most commonly used:
0x03(break) — terminate any running script or loop0x04(reset) — clear all variables and runmain.luaif 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: