Skip to content

Latest commit

 

History

History
291 lines (216 loc) · 9.19 KB

File metadata and controls

291 lines (216 loc) · 9.19 KB

BLE UART Bridge Client for Unity

A Unity client for the uart-ble-websocket bridge. The bridge owns the Bluetooth radio and exposes one BLE UART device as a WebSocket; this package turns that into an ordinary C# API.

  • Lines in, lines outSendLine("AT") and a LineReceived event, with the stream reassembly BLE forces on you handled for you.
  • Binary tooSend(bytes) / DataReceived, plus an optional length-prefix framer for structured packets and chunked upload with progress.
  • Scanning, connecting, state — the whole control protocol as awaitable methods.
  • Reconnects to the bridge on its own, with backoff.
  • No dependencies. Works on every player, including WebGL.

Requires Unity 6.3 LTS (6000.3) or newer.

Install

The bridge repo carries the package under unity/com.uartble.bridge. Point the Package Manager at it:

Window → Package Manager → + → Install package from disk… and pick unity/com.uartble.bridge/package.json.

Or add it to your project's Packages/manifest.json:

{
  "dependencies": {
    "com.uartble.bridge": "file:../../uart-ble-websocket/unity/com.uartble.bridge"
  }
}

A relative path is resolved from the Packages folder. To vendor it instead, copy the directory into your project's Packages/.

Start the bridge

The package talks to a running bridge — it does not do Bluetooth itself. On the machine with the radio:

.venv/Scripts/uart-ble-ws --name MyDevice

That binds ws://127.0.0.1:8765, which is the package's default.

Five-line version

using UartBle.Bridge;

var client = new UartBridgeClient();
client.LineReceived += line => Debug.Log($"device: {line}");
await client.StartAsync();
await client.WaitForDeviceAsync(TimeSpan.FromSeconds(30));
client.SendLine("AT");

StartAsync returns once the bridge has answered; WaitForDeviceAsync returns once the BLE device is actually connected. Those are two different things — the bridge is up long before the peripheral is.

Prefer the inspector? Add a UART Bridge component, set the URL, and hook OnLineReceived. It owns a client and exposes it as .Client.

Threading

Frames arrive on a background thread and are queued. Every event fires on the main thread, drained once per frame from the player loop — so you can touch Transforms and UI directly in a handler, and no MonoBehaviour is required anywhere.

Turn off AutoPump and call Pump() yourself if you want a different cadence (a fixed step, or a worker thread of your own).

Because the pump runs from the player loop, awaiting any of this from the main thread works — but never block on it. client.ConnectAsync().Wait() will deadlock: the reply it is waiting for arrives through a pump that the blocked frame will never run.

Text

var options = new UartBridgeOptions();
options.Line.Terminator = LineTerminator.Lf;   // default is CRLF
options.Line.Encoding = UartTextEncoding.Utf8;

var client = new UartBridgeClient(options);
client.LineReceived += Handle;

Or poll, if an Update loop suits you better — both work at once:

void Update()
{
    while (client.TryReadLine(out string line)) Handle(line);
}

A device that prints a prompt without a newline ("OK> ") leaves those bytes buffered forever, by design — call FlushPartialLine() when you decide the device has finished talking.

Binary

DataReceived gives you every chunk exactly as the peripheral sent it:

client.DataReceived += data =>
{
    // data is a ReadOnlySpan<byte>, valid only inside this call
    Process(data);
};

client.Send(new byte[] { 0x01, 0xFF });
await client.SendAsync(payload);              // awaits the actual send

BLE UART is a stream. One notification is not one message: a payload can be split across chunks, and several can share one. If your firmware sends structured binary, frame it. LengthPrefixFramer implements the simplest scheme that works — a little-endian uint16 length, then that many bytes:

var framer = new LengthPrefixFramer();
client.DataReceived += data =>
{
    framer.Append(data);
    while (framer.TryRead(out byte[] packet)) Handle(packet);
};

client.SendPacket(payload);   // extension method; frames and sends

Your firmware has to write the same shape. If it already uses COBS or a length-and-CRC header, write the equivalent class for that — the point is that something must delimit messages, and it cannot be the transport.

For a large payload:

await client.SendChunkedAsync(
    firmwareImage,
    chunkSize: 0,                                   // 0 = use the negotiated MTU
    progress: new Progress<float>(p => bar.value = p));

Connection state

Two links, tracked separately:

Property Meaning
Connection Our WebSocket to the bridge process
Device The bridge's BLE link to the peripheral
IsDeviceConnected Both are up — writes will actually arrive
client.DeviceStateChanged += change =>
{
    if (change.IsFailure) Debug.LogWarning(change.Error);
    status.text = change.State.ToString();
};

Writes while the socket is down throw InvalidOperationException rather than being buffered — matching the bridge, which fails writes instead of silently queueing them, so your application decides whether to retry or drop. Use TrySend / TrySendLine for the paths where you would rather branch than catch.

The bridge reconnects to the device on its own, and this client reconnects to the bridge on its own. Treat any state other than Connected as "the link is down, wait".

Scanning and connecting

Skip this if the bridge was started with --name or --address; it will already be connecting.

DeviceInfo[] found = await client.ScanAsync(timeoutSeconds: 5f);
// strongest signal first; Name is often null, and plenty of UART peripherals
// do not advertise their service UUIDs, so this is everything nearby

await client.ConnectDeviceAsync(address: found[0].Address);
await client.WaitForDeviceAsync(TimeSpan.FromSeconds(20));

ConnectDeviceAsync returns when the request is accepted, not when the device is connected — that is what WaitForDeviceAsync is for.

To have the client do this at startup, set AutoConnectDevice with a DeviceAddress or DeviceName. Leave both empty to use the bridge's own configured target.

Authentication

A bridge started with --token needs one:

var options = new UartBridgeOptions
{
    Url = "ws://192.168.1.20:8765",
    Token = "…",
};

TokenPlacement.Auto sends Authorization: Bearer where headers work and falls back to ?token= on WebGL, where browsers will not let anything set them. The header is preferable — URLs end up in logs and proxy history.

There is no TLS in the bridge. Off localhost, put it behind a reverse proxy that terminates wss:// and point Url at that.

WebGL

Supported, through a .jslib that wraps the browser's own WebSocket. Two things change:

  • The token has to travel in the query string.
  • A page served over https:// cannot open a ws:// socket. You need wss://, which means the reverse proxy above.

Errors

What Where it shows up
Reply to a command failed The awaited call throws BridgeException with a Code
Unsolicited bridge error ErrorReceived
Bridge speaks another protocol version BridgeProtocolException; no reconnect is attempted
Command got no reply TimeoutException after RequestTimeout
Dropped for being too slow BridgeDisconnected with WasTooSlow

BridgeErrorCodes has the codes as constants: NotConnected, ConnectFailed, WriteFailed, Busy, ScanFailed, and the rest.

Falling behind is worth taking seriously. The bridge buffers a bounded amount per client and then either sheds messages or closes the connection with 1013, depending on its slow_client_policy. If you see that, your DataReceived handler is too slow — queue the work, do not do it inline.

Options

Everything on UartBridgeOptions, all serializable so the component can expose them:

Option Default
Url ws://127.0.0.1:8765
Token, TokenPlacement none, Auto
AutoReconnect true With ReconnectInitialDelay / ReconnectMaxDelay
AutoConnectDevice false With DeviceAddress / DeviceName
RequestTimeout 10 s Per control command
ScanTimeout 5 s Default for ScanAsync
AutoPump true Off means you call Pump()
EnforceProtocolVersion true
LogToConsole false Connection diagnostics
Line.* Encoding, terminator, max length, poll queue

Options are copied on construction — edit them before, not after.

Samples

Import from the Package Manager:

  • Line Console — scan, connect, and exchange text with a device.
  • Binary Transfer — length-prefixed packets and a chunked upload with a progress bar.

Tests

The package ships edit-mode tests for the JSON reader, the control protocol, the line assembler, and the framer. To run them, add the package to testables in your project's Packages/manifest.json:

{
  "testables": ["com.uartble.bridge"]
}

They need no bridge and no Bluetooth hardware.

Licence

MIT