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. Un-Pairing
    2. Bluetooth Connection Diagram
  2. Bluetooth Services & Characteristics
    1. Halo Lua Service
    2. Battery Service
    3. OTA 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.

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

Un-Pairing

If previously paired to another device, Halo must be put into pairing mode before it can be paired to a new host device. Press and hold Halo’s button for 8 seconds until the white LED on the left arm flashes, indicating that Halo is in pairing mode. Host-side devices must also remove previous bonding before a new pairing can be accepted.

Bluetooth Connection Diagram

Bluetooth connection sequence diagram

Bluetooth Services & Characteristics

Halo implements three 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).

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%)

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.

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 total payload length is therefore MTU − 4 bytes.

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: 10ms frames (750µs or 1000µs frame duration), e.g. 16kHz 16-bit mono at 16kbps.

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 frame-2-firmware releases page.

For reference implementations of MCU-BOOT DFU over BLE: