ble_framed_protocol 0.1.0
ble_framed_protocol: ^0.1.0 copied to clipboard
Encode and decode XOR-checksummed framed-byte protocols common to industrial BLE devices (e-locks, GPS trackers, Nordic NUS clones). Configurable header and trailer bytes; pure Dart, no Flutter dependency.
ble_framed_protocol #
Encode and decode XOR-checksummed framed-byte protocols common to industrial BLE devices — electronic locks, GPS trackers, smart-meter dongles, Nordic NUS clones. Pure Dart, no Flutter dependency.
The frame layout #
[HEADER bytes ...][CMD][LEN_H][LEN_L][PAYLOAD ...LEN bytes][XOR][TRAILER bytes ...]
- Header — fixed magic bytes, default
[0xAA, 0xBB]. - CMD — single command byte (caller-defined).
- LEN — big-endian 16-bit unsigned length of
PAYLOAD. - PAYLOAD —
LENarbitrary bytes. - XOR — XOR of every byte from the frame start up to (but not including) the checksum byte.
- Trailer — fixed magic bytes, default
[0x0D, 0x0A](CRLF).
If you've reverse-engineered the protocol of a Chinese-OEM e-lock or GPS tracker recently, this is probably the layout you found. Default bytes match the most common variant; custom magic bytes are configurable.
Install #
dart pub add ble_framed_protocol
Usage #
import 'dart:typed_data';
import 'package:ble_framed_protocol/ble_framed_protocol.dart';
final codec = FramedProtocol.standard;
// Encode a command for transmission.
final tx = codec.encode(
Frame(
cmdId: 0x01,
payload: Uint8List.fromList([0x10, 0x20, 0x30]),
),
);
// → 0xAA 0xBB 0x01 0x00 0x03 0x10 0x20 0x30 0x13 0x0D 0x0A
// Decode a response from the device. Returns null on any framing error
// (bad header, bad trailer, length mismatch, bad checksum).
final rx = codec.decode(receivedBytes);
if (rx != null) {
final responseCode = rx.payload.first; // your domain split
final responseData = rx.payload.sublist(1); // (this lib stays
print('cmd 0x${rx.cmdId.toRadixString(16)} → ' // framing-only)
'$responseCode / ${responseData.length} bytes');
}
Custom magic bytes #
final codec = FramedProtocol(
headerBytes: const [0x55, 0xAA],
trailerBytes: const [0xFF],
);
Any non-empty header/trailer works. The XOR checksum still covers everything up to its own position regardless of header/trailer length.
API #
Frame #
| Field | Type | Notes |
|---|---|---|
cmdId |
int |
Single byte, 0..0xFF. |
payload |
Uint8List |
Bytes between LEN and XOR. May be empty. |
FramedProtocol #
| Method | Returns | Notes |
|---|---|---|
encode(Frame) |
Uint8List |
Throws ArgumentError on out-of-range cmdId or oversized payload. |
decode(Uint8List) |
Frame? |
Returns null on any framing/checksum error. |
verifyChecksum(Uint8List) |
bool |
Cheap structural + checksum check, no allocation. |
| Static / constant | Type | Notes |
|---|---|---|
FramedProtocol.standard |
FramedProtocol |
Default [0xAA, 0xBB] / [0x0D, 0x0A] codec. |
FramedProtocol.maxPayloadSize |
int |
0xFFFF — hard limit from the 16-bit length field. |
Why "framing-only"? #
The library deliberately does not know about response codes, command enums, BCD time encoding, or device-specific status bits. Those vary by manufacturer. This package gives you a tested codec for the bytes on the wire — you build the typed command/response layer on top.
A typical higher layer (~30 lines per device) looks like:
class MyDeviceCommand {
static Uint8List buildLock(Uint8List deviceId, Uint8List password) {
final data = Uint8List(13)
..setRange(0, 6, deviceId)
..setRange(6, 12, password)
..[12] = 0x00;
return FramedProtocol.standard.encode(
Frame(cmdId: 0x01, payload: data),
);
}
}
Testing #
dart test
Tests cover round-trips at varied payload sizes (0, 1, 16, 255, 1024 bytes), custom magic bytes, and rejection of every malformed-frame variant: bad header, bad trailer, length mismatch, checksum mismatch, too-short input, oversized payload, out-of-range command id.
Example #
dart run example/encode_decode.dart
License #
MIT — see LICENSE.