Complete API documentation for the BLE Parser library.
Class-level annotation that marks a class as a BLE data object parser.
Constructor:
const BleObject({
Endian endian = Endian.little,
bool signed = false,
})Parameters:
- Type:
Endian - Default:
Endian.little - Description: Sets the default byte order for all fields in the class. Can be overridden per-field using the
endianparameter in@BleField(). - Values:
Endian.little- Little-endian byte order (least significant byte first)Endian.big- Big-endian byte order (most significant byte first)
Example:
@BleObject(endian: Endian.big)
class NetworkPacket {
@BleField(length: 2) // Uses big endian
final int packetId;
}- Type:
bool - Default:
false - Description: Sets the default signed/unsigned flag for all integer fields in the class. When
true, integers are parsed as signed (can be negative). Can be overridden per-field using thesignedparameter in@BleField().
Example:
@BleObject(signed: true)
class SensorData {
@BleField(length: 2) // Parsed as int16 (signed)
final int temperature; // Can be negative
}Field-level annotation that marks a field for BLE data parsing.
Constructor:
const BleField({
required int length,
int? offset,
Endian? endian,
bool? signed,
Type? objectType,
})Parameters:
- Type:
int - Default: N/A (required parameter)
- Description: Specifies the size of the field in bytes. Determines which parsing method to use.
- Valid values and their mappings:
1→ Parses asint8/uint8(1 byte)2→ Parses asint16/uint16(2 bytes)4→ Parses asint32/uint32(4 bytes)8→ Parses asint64/uint64(8 bytes)- Any other value → Returns
List<int>(raw bytes sublist)
Examples:
@BleField(length: 1)
final int flags; // uint8 or int8
@BleField(length: 4)
final int timestamp; // uint32 or int32
@BleField(length: 10)
final List<int> rawData; // Custom length, returns raw bytes- Type:
int? - Default:
null(auto-calculated) - Description: Forces the field to be read from a specific byte offset in the data. When
null, the offset is automatically calculated based on the previous field's position and size. - Note: When using manual offsets, ensure fields don't overlap.
Examples:
@BleObject()
class SparseData {
@BleField(length: 2, offset: 0) // Read from byte 0
final int field1;
@BleField(length: 2, offset: 4) // Skip bytes 2-3, read from byte 4
final int field2;
@BleField(length: 4, offset: 10) // Skip bytes 6-9, read from byte 10
final int field3;
}- Type:
Endian? - Default:
null(uses@BleObjectdefault) - Description: Overrides the byte order for this specific field. When
null, uses the default specified in@BleObject(). - Values:
null- Use default from@BleObjectEndian.little- Little-endian byte orderEndian.big- Big-endian byte order
Examples:
@BleObject(endian: Endian.big)
class MixedEndianPacket {
@BleField(length: 2) // Uses big endian (from class default)
final int header;
@BleField(length: 4, endian: Endian.little) // Override to little endian
final int payload;
}- Type:
bool? - Default:
null(uses@BleObjectdefault) - Description: Overrides the signed flag for this specific integer field. When
null, uses the default specified in@BleObject(). - Values:
null- Use default from@BleObjecttrue- Parse as signed integer (can be negative)false- Parse as unsigned integer (always positive)
Examples:
@BleObject(signed: true)
class SensorData {
@BleField(length: 2) // Uses signed=true (from class default)
final int temperature; // int16, can be negative
@BleField(length: 4, signed: false) // Override to unsigned
final int timestamp; // uint32, always positive
}- Type:
Type? - Default:
null - Description: Specifies the type of a nested
@BleObjectclass. Required when parsing nested objects. Thelengthparameter must match the total size of all fields in the nested object.
Examples:
@BleObject()
class InnerData {
@BleField(length: 2)
final int field1;
@BleField(length: 2)
final int field2;
// Total size = 4 bytes
}
@BleObject()
class OuterData {
@BleField(length: 1)
final int header;
@BleField(length: 4, objectType: InnerData) // Must specify type and correct length
final InnerData nested;
}The library automatically infers the parsing method based on the field's Dart type. No manual type specification needed - just use the natural Dart type.
Based on the length and signed parameters, different integer types are generated:
| Length | Signed=false | Signed=true | Range |
|---|---|---|---|
| 1 byte | uint8 |
int8 |
-128 to 127 / 0 to 255 |
| 2 bytes | uint16 |
int16 |
-32768 to 32767 / 0 to 65535 |
| 4 bytes | uint32 |
int32 |
-2¹⁰ to 2³¹-1 / 0 to 2²²-1 |
| 8 bytes | uint64 |
int64 |
-2²³ to 2²³-1 / 0 to 2²⁴-1 |
Example:
@BleField(length: 4)
final int value; // → view.getUint32() or view.getInt32()For double type fields, IEEE 754 floating point parsing is used:
| Length | Type | Method | Range |
|---|---|---|---|
| 4 bytes | Float32 | getFloat32() |
~1.2×10⁻³⁸ to ~3.4×10³⁸ |
| 8 bytes | Float64 | getFloat64() |
~5.0×10⁻³²⁴ to ~1.8×10³⁰⁸ |
Example:
@BleField(length: 4)
final double temperature; // → view.getFloat32(offset, endian)
@BleField(length: 8)
final double pressure; // → view.getFloat64(offset, endian)For String type fields, character decoding is performed:
| Method | Character Support | Imports Required |
|---|---|---|
String.fromCharCodes() |
ASCII/Latin-1 (0-255) | None ✅ |
(Future option) utf8.decode() |
Full UTF-8 (Unicode) | dart:convert |
Current Implementation:
@BleField(length: 10)
final String deviceName; // → String.fromCharCodes(bytes)Character Support:
- ✅ ASCII characters (0-127)
- ✅ Latin-1 characters (128-255)
- ❌ UTF-8 multibyte characters (Chinese, emoji, etc.)
For BLE Protocols: Perfect for standard BLE use cases:
- Device names (ASCII)
- UUID strings (ASCII/hex)
- Protocol commands (ASCII)
- Serial numbers (ASCII/Latin-1)
Note: If you need full UTF-8 support (Chinese characters, emoji, etc.), you would need to use dart:convert and utf8.decode(), which requires adding import 'dart:convert' show utf8; to your model file.
When length is not 1, 2, 4, or 8 (or for types without special handling), the field returns List<int>:
@BleField(length: 3)
final List<int> customData; // Returns 3 bytes as List<int>For each class annotated with @BleObject(), the builder generates two parsing functions:
Function Signature:
ClassName _$ClassNameFromBytes(Uint8List rawData)Parameters:
rawData: Raw byte array asUint8Listfrom BLE device
Returns:
- Parsed object of type
ClassName
Performance Characteristics:
- Zero-copy parsing using
ByteData.sublistView() - Directly views the data without copying
- Best for BLE devices that return
Uint8List - No unnecessary memory allocation
Example:
HeartRatePacket _$HeartRatePacketFromBytes(Uint8List rawData) {
final view = ByteData.sublistView(rawData);
return HeartRatePacket(
flags: view.getUint8(0),
heartRate: view.getUint16(1, Endian.little)
);
}Function Signature:
ClassName _$ClassNameFromBytesList(List<int> rawData)Parameters:
rawData: Raw byte array asList<int>from any source
Returns:
- Parsed object of type
ClassName
Performance Characteristics:
- One-copy parsing using
Uint8List.fromList() - Automatically converts
List<int>toUint8List - Works with any
List<int>data source - Slightly more memory overhead due to conversion
Example:
HeartRatePacket _$HeartRatePacketFromBytesList(List<int> rawData) {
final view = ByteData.sublistView(Uint8List.fromList(rawData));
return HeartRatePacket(
flags: view.getUint8(0),
heartRate: view.getUint16(1, Endian.little)
);
}Recommended factory method implementation:
@BleObject()
class HeartRatePacket {
@BleField(length: 1)
final int flags;
@BleField(length: 2)
final int heartRate;
HeartRatePacket({
required this.flags,
required this.heartRate,
});
// Convenience method - works with List<int>
static HeartRatePacket fromBytes(List<int> data) {
return _$HeartRatePacketFromBytesList(data);
}
// Optimized method - zero-copy parsing from Uint8List
static HeartRatePacket fromBytesUint8(Uint8List data) {
return _$HeartRatePacketFromBytes(data);
}
}When to use each:
// Using Uint8List (recommended for BLE data)
final bleData = Uint8List.fromList([0x06, 0x78, 0x00]);
final packet1 = HeartRatePacket.fromBytesUint8(bleData); // Zero-copy
// Using List<int> (convenience)
final listData = [0x06, 0x78, 0x00];
final packet2 = HeartRatePacket.fromBytes(listData); // Auto-conversionNote: Both methods produce identical parsing results. The choice depends on your data source and performance requirements.
Complete example with nested objects:
@BleObject(endian: Endian.little)
class GPSLocation {
@BleField(length: 4, signed: true)
final int latitude; // int32
@BleField(length: 4, signed: true)
final int longitude; // int32
GPSLocation({
required this.latitude,
required this.longitude,
});
static GPSLocation fromBytes(List<int> data) {
return _$GPSLocationFromBytes(data);
}
}
@BleObject(endian: Endian.little)
class TrackerData {
@BleField(length: 1)
final int deviceId;
@BleField(length: 8, objectType: GPSLocation) // 4+4=8 bytes
final GPSLocation location;
@BleField(length: 4)
final int timestamp;
TrackerData({
required this.deviceId,
required this.location,
required this.timestamp,
});
static TrackerData fromBytes(List<int> data) {
return _$TrackerDataFromBytes(data);
}
}@BleObject(endian: Endian.little, signed: true)
class EnvironmentalReading {
@BleField(length: 2) // int16 (signed)
final int temperature; // Can be negative: -10°C
@BleField(length: 2) // int16 (signed)
final int pressure; // Can be negative: below sea level
@BleField(length: 1, signed: false) // uint8 (unsigned)
final int humidity; // Always positive: 0-100%
@BleField(length: 4, signed: false) // uint32 (unsigned)
final int timestamp; // Unix timestamp
EnvironmentalReading({
required this.temperature,
required this.pressure,
required this.humidity,
required this.timestamp,
});
static EnvironmentalReading fromBytes(List<int> data) {
return _$EnvironmentalReadingFromBytes(data);
}
}Auto-offset (recommended):
@BleObject()
class CompactPacket {
@BleField(length: 1) // offset 0, total: 1 byte
final int a;
@BleField(length: 2) // offset 1, total: 3 bytes
final int b;
@BleField(length: 4) // offset 3, total: 7 bytes
final int c;
}Manual offset (for sparse data):
@BleObject()
class SparsePacket {
@BleField(length: 1, offset: 0) // at byte 0
final int header;
@BleField(length: 2, offset: 4) // skip bytes 2-3
final int value1;
@BleField(length: 4, offset: 10) // skip bytes 6-9
final int value2;
}// Data: [0x34, 0x12]
@BleObject(endian: Endian.little)
class LittleEndianData {
@BleField(length: 2)
final int value; // Parsed as 0x1234 (4660)
}// Data: [0x12, 0x34]
@BleObject(endian: Endian.big)
class BigEndianData {
@BleField(length: 2)
final int value; // Parsed as 0x1234 (4660)
}@BleObject(endian: Endian.big)
class MixedData {
@BleField(length: 2) // Big endian
final int networkValue;
@BleField(length: 4, endian: Endian.little) // Little endian
final int deviceValue;
}-
Missing
lengthparameter:Error: Missing required parameter: lengthSolution: Always provide
lengthin@BleField() -
Missing part directive:
Error: Could not resolve _$ClassNameFromBytesSolution: Add
part 'filename.g.dart';to your model file
-
Insufficient data: If the input data is shorter than expected, you'll get a
RangeError -
Incorrect nested object length: May cause parsing errors or data misalignment
Best Practice: Always validate input data length before parsing:
class MyPacket {
static MyPacket fromBytes(List<int> data) {
if (data.length < 7) {
throw ArgumentError('Data too short: expected 7 bytes, got ${data.length}');
}
return _$MyPacketFromBytes(data);
}
}-
Always add part directives:
part 'my_packet.g.dart';
-
Use descriptive field names:
@BleField(length: 2) final int heartRateBpm; // Good
-
Document byte layouts in comments:
// BLE Protocol: // Byte 0: Flags // Bytes 1-2: Heart Rate (uint16, little endian) // Bytes 3-4: Energy Expended (uint16, little endian)
-
Validate data length:
static MyPacket fromBytes(List<int> data) { if (data.length < expectedLength) { throw ArgumentError('Insufficient data'); } return _$MyPacketFromBytes(data); }
-
Use defaults to reduce repetition:
// Good - uses defaults @BleObject(endian: Endian.little, signed: true) class Sensor { @BleField(length: 2) // Uses defaults final int temp; } // Verbose - repeats values @BleObject() class Sensor { @BleField(length: 2, endian: Endian.little, signed: true) final int temp; }
import 'package:ble_parser/ble_parser.dart';
part 'device_status.g.dart';
/// BLE Device Status Packet
/// Byte layout:
/// - Byte 0: Device ID (uint8)
/// - Bytes 1-2: Temperature (int16, little endian, °C × 100)
/// - Bytes 3-4: Humidity (uint16, little endian, % × 100)
/// - Bytes 5-8: Timestamp (uint32, little endian, Unix epoch)
@BleObject(endian: Endian.little)
class DeviceStatus {
@BleField(length: 1)
final int deviceId;
@BleField(length: 2, signed: true)
final int temperature; // int16, divide by 100 for actual value
@BleField(length: 2)
final int humidity; // uint16, divide by 100 for actual value
@BleField(length: 4)
final int timestamp; // uint32, Unix timestamp
const DeviceStatus({
required this.deviceId,
required this.temperature,
required this.humidity,
required this.timestamp,
});
/// Parse from raw BLE data
factory DeviceStatus.fromBytes(List<int> data) {
if (data.length < 9) {
throw ArgumentError('Invalid data length: expected 9, got ${data.length}');
}
return _$DeviceStatusFromBytes(data);
}
/// Get temperature in Celsius
double get temperatureInC => temperature / 100.0;
/// Get humidity in percent
double get humidityInPercent => humidity / 100.0;
/// Get DateTime from timestamp
DateTime get dateTime => DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
@override
String toString() {
return 'DeviceStatus(deviceId: $deviceId, '
'temp: ${temperatureInC}°C, '
'humidity: ${humidityInPercent}%, '
'time: $dateTime)';
}
}
// Usage
void main() {
// Raw BLE data from device
final data = [
0x01, // deviceId = 1
0x58, 0x02, // temperature = 600 (6.00°C)
0x10, 0x27, // humidity = 10000 (100.00%)
0x00, 0x00, 0x00, 0x01, // timestamp = 1672531200 (Jan 1, 2023)
];
final status = DeviceStatus.fromBytes(data);
print(status);
// Output: DeviceStatus(deviceId: 1, temp: 6.00°C, humidity: 100.00%, time: 2023-01-01 00:00:00.000)
}For more examples, see the /example directory in the package repository.