Protocol version 1. Clients should read protocol from the hello message
and refuse to run against a major version they do not know.
The two WebSocket frame types carry different things, and never mix:
| Frame type | Direction | Meaning |
|---|---|---|
| Binary | both | Raw UART payload, byte-for-byte. No header, no encoding. |
| Text | both | A single JSON object: a control message. |
A binary frame sent by a client is written to the device. A binary frame sent by the server is data the device emitted. Nothing is added or stripped, so a client that only ever sends and receives binary frames is a transparent pipe.
BLE UART is a stream, not a datagram service. The bridge splits outbound payloads larger than the negotiated MTU across several BLE writes, and emits one binary frame per inbound BLE notification. So:
- A binary frame you send may arrive at the peripheral as several writes.
- Device output arrives as however many notifications the peripheral sent; a logical message may be split across binary frames, or several may share one.
If your firmware protocol needs framing (length prefixes, newlines, COBS), implement it on top. Do not assume one WebSocket frame equals one device message.
max_write in hello and state reports the largest single BLE write, which is
useful if you want to pre-split payloads yourself.
ws://127.0.0.1:8765
If the bridge is configured with a token, present it either as a header
Authorization: Bearer <token>
or, when the client library cannot set handshake headers (browsers, and some Unity WebSocket implementations), as a query parameter:
ws://127.0.0.1:8765/?token=<token>
The header is preferable — URLs tend to end up in logs and proxy history.
Failures happen during the HTTP handshake, before the WebSocket opens:
| Status | Meaning |
|---|---|
401 |
Missing or wrong token |
503 |
max_clients already connected |
Any client control message may carry an id (string or number). The reply to it
carries the same id. Unsolicited server messages (device state changes) have no
id. Pick unique ids if you have more than one request in flight.
type |
Fields | Effect |
|---|---|---|
ping |
— | Server replies pong. Application-level; separate from WebSocket ping frames. |
status |
— | Server replies with a state message. |
scan |
timeout (seconds, optional) |
Discover nearby devices; replies scan_result. |
connect |
address and/or name (optional) |
Target a device and start connecting. With neither field, uses the configured target. |
disconnect |
— | Drop the current device and stop reconnecting. |
write |
data (base64 string) |
Same as sending a binary frame. For clients that cannot send binary frames. |
{"type": "connect", "address": "AA:BB:CC:DD:EE:FF", "id": 1}
{"type": "scan", "timeout": 5, "id": 2}
{"type": "write", "data": "aGVsbG8=", "id": 3}connect replies ok as soon as the request is accepted. That is not a
successful connection — watch the state messages that follow.
Sent once, immediately on connect.
{
"type": "hello",
"protocol": 1,
"server": "uart-ble-websocket/0.1.0",
"framing": "binary",
"target": {"address": null, "name": "MyDevice"},
"state": "disconnected",
"device": null,
"max_write": null
}Sent unsolicited on every transition, and as the reply to status.
{
"type": "state",
"state": "connected",
"device": {"address": "AA:BB:CC:DD:EE:FF", "name": "MyDevice"},
"max_write": 244
}state is one of disconnected, scanning, connecting, connected.
device is null unless a device is known. max_write is non-null only while
connected. An error field appears when the transition was caused by a failure:
{"type": "state", "state": "disconnected", "device": null, "max_write": null,
"error": "no device found with name 'MyDevice' within 10s"}Only connected means binary frames will reach the device. The bridge does not
buffer while disconnected — writes fail with not_connected instead of silently
queueing, so your application decides what to do about it.
{"type": "scan_result", "id": 2, "devices": [
{"address": "AA:BB:CC:DD:EE:FF", "name": "MyDevice", "rssi": -47},
{"address": "11:22:33:44:55:66", "name": null, "rssi": -83}
]}Strongest signal first. name may be null. Not every peripheral advertises its
service UUIDs, so a UART device may look unremarkable in this list.
Acknowledges a command that produced no other reply. write adds a bytes count.
{"type": "ok", "id": 1}
{"type": "ok", "id": 3, "bytes": 5}{"type": "pong", "id": 7}Carries the id of the offending request when there was one. An error never
closes the connection.
{"type": "error", "code": "not_connected", "message": "no device connected", "id": 4}code |
Meaning |
|---|---|
bad_request |
Malformed JSON, or a field of the wrong type |
unsupported_type |
Unknown type |
not_connected |
A write arrived while no device was connected |
busy |
A scan is already running |
scan_failed |
The adapter refused to scan (often: Bluetooth is off) |
connect_failed |
Device not found, or connection refused |
write_failed |
The write was rejected or the device vanished mid-write |
internal_error |
A bug in the bridge — please report it |
| Code | Meaning |
|---|---|
1000 |
Normal shutdown |
1013 |
The client could not keep up with device output and was dropped (slow_client_policy = "close") |
- Connect; read
hello; checkprotocol. - If
stateis notconnected, sendconnect(or wait, if the bridge auto-connects). - Wait for a
statemessage with"state": "connected". - Send binary frames to transmit; handle incoming binary frames as device output.
- Treat any
stateother thanconnectedas "the link is down" — the bridge reconnects on its own, so just wait for the nextconnected.