@stormotion/ble-protocol-generator
v0.0.4
Published
TypeScript code generator for parsing BLE protocols from YAML schemas.
Readme
@stormotion/ble-protocol-generator
A schema-driven protocol parser and serializer generator for BLE communications.
@stormotion/ble-protocol-generator allows you to define your binary protocols in human-readable YAML, then automatically generates robust, ready-to-use TypeScript code to parse and serialize that data.
Under the hood, it leverages the power of binary-layout for performant and accurate bit-level manipulation.
Navigation
💡 Motivation
Mobile / Web apps interacting with BLE hardware often require complex binary data handling.
- Stop manual bit-shifting: No more maintaining fragile
DataViewcode. - Single Source of Truth: Define your protocol once in YAML; use the generated code in your mobile / web app.
- Bi-directional: Generates both Parsers (Binary → TS Object) and Serializers (TS Object → Binary) automatically.
📦 Installation
npm install @stormotion/ble-protocol-generator
# or
yarn add @stormotion/ble-protocol-generator🚀 Quick Start
Follow these steps to integrate protocol parsing into your project.
1. Setup protocol directory
Create a dedicated directory in your project root to store your schema definitions.
mkdir protocol2. Create file for schema
Create a YAML file inside the protocol folder to describe your binary structure.
Naming Convention:
Files must use snake_case (underscores _) for multi-word names.
- ❌
ThermostatControl.yaml - ❌
thermostat-control.yaml - ✅
smart_light.yaml
Example: thermostat/thermostat_control.yaml
touch protocol/thermostat_control.yaml3. Define schema
Define yaml schema using special syntax
Basic example:
# thermostat_control.yaml
meta:
type: root
endianness: little
service_uuid: 0000181A-0000-1000-8000-00805F9B34FB
notifiable_characteristic_uuid: 00002A6E-0000-1000-8000-00805F9B34FB
seq:
- id: control_flags
binary: uint
size: 1
- id: current_temperature
binary: int
size: 2
- id: target_temperature
binary: uint
size: 2
- id: humidity_percentage
binary: uint
size: 1
- id: hvac_cycles_count
binary: uint
size: 4
- id: wifi_ssid
binary: bytes
size: 324. Generate parser
Generate parser with command
# Syntax
ble-gen <source_path> <output_parser_name>
# Example
ble-gen ./thermostat ThermostatProtocolParserOutput
If the command runs successfully, you will see a confirmation message indicating where the file with your parser was saved.
✅ Generated <output_parser_name> into generated/<output_parser_name>.ts
# Example
✅ Generated ThermostatProtocolParser into generated/ThermostatProtocolParser.tsNow you free to use generated parser anywhere in the code!
import ThermostatProtocolParser from "./generated/ThermostatProtocolParser.ts";
const bleProtocolBuffer = Buffer.from([
// control_flags (1 byte: 170)
0xaa,
// current_temperature (2 bytes: -55 or 0xFFC9 LE)
0xc9, 0xff,
// target_temperature (2 bytes: 240 or 0x00F0 LE)
0xf0, 0x00,
// humidity_percentage (1 byte: 45)
0x2d,
// hvac_cycles_count (4 bytes: 10000 or 0x00002710 LE)
0x10, 0x27, 0x00, 0x00,
// wifi_ssid (32 bytes: "HomeNetwork" + 21 null bytes)
0x48, 0x6f, 0x6d, 0x65, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
const parser = new ThermostatProtocolParser();
const deserializedValue = parser.deserializeBinaryThermostat(bleProtocolBuffer);
console.log(deserializedValue);
# {
# control_flags: 170,
# current_temperature: -13825,
# target_temperature: 240,
# humidity_percentage: 45,
# hvac_cycles_count: 10000,
# wifi_ssid: <Buffer 48 6f 6d 65 4e 65 74 77 6f 72 6b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
# }
const serializedValue = parser.serializeDataThermostat(deserializedValue);
console.log(serializedValue);
# Uint8Array(42) [
# 170, 201, 255, 240, 0, 45, 16, 39, 0, 0,
# 72, 111, 109, 101, 78, 101, 116, 119, 111, 114,
# 107, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0
# ]📝 Protocol Definition
Meta
Meta data of the schema.
| Property | Type | Description |
| ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------- |
| type | "root" \| "layout" | Set "root" for common schema. Check details in Root and Layout section |
| endianness | "little" \| "big" (optional) | Endianness. Default is "big" |
| writable_characteristic_uuid | string (optional) | Writable characteristic uuid |
| notifiable_characteristic_uuid | string (optional) | Notifiable characteristic uuid |
Seq
Sequence of bytes in binary with definitions.
| Property | Type | Description |
| -------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | string | Name of the property |
| binary | "(u)int" \| "bytes" \| "switch" | Type of data. Check details in Binary section |
| size | number | Size of data |
| id_size | number | Defines the byte size of the field that immediately precedes this "switch" block. This field's value is used to select the correct layout. |
| layouts | array | A map of types field. |
| layout | string | Specifies the name of a separate, reusable layout schema file to be embedded inline at this position. layout |
| bits | string[] | An ordered list of names used to parse the current field (must be a uint or int) into individual Boolean flags. Use to define bitmasks or flag structures within a single byte/integer field. Check Bits section. |
🔢 Binary
The binary types used in the seq block closely align with those provided by the robust binary-layout library. Where necessary, certain types have been abstracted or modified to better suit the declarative style of YAML schema definition.
u(int)
Numeric value (signed or unsigned). By default, converted into a number or bigint depending on its size. Source
Definition in yaml example:
- id: header
binary: uint
size: 1Bytes
Raw bytes or sub-layouts. By default, converted into a Uint8Array when not specifying a sub-layout, or an object otherwise. Source
Definition in yaml example::
Returns raw bytes as Uint8Array.
- id: reserve
binary: bytes
size: 2Returns deserialized object.
- id: weekly_schedule
binary: bytes
layout: schedule_day_configSwitch
Enables branching logic. Comparable to Rust enums. Converted into a union type of the derived types of its layout variants. Source
The following example demonstrates how to map specific layout sequences to the values 42 and 254 which are address bytes.
- id: command
binary: switch
id_size: 1
layouts:
- address: 42 // address byte
seq: // sequence of layouts for specific address
- id: data_bytes
binary: uint
size: 1
- id: data
binary: bytes
layout: schedule_day_config
- address: 254
seq:
- id: data_bytes
binary: uint
size: 1
- id: data
binary: bytes
layout: schedule_night_config🦸 Advanced
Root and Layout
To keep protocol definitions maintainable and scalable, this library distinguishes between Root Schemas (Entry Points) and Nested Layouts (Reusable Parts).
Root Schema (root)
A Root Schema represents a complete BLE Characteristic. It serves as the entry point for the parser. It should be stored at the root level of the protocols folder. You can have as much root schemas as you wish in this folder. Our codegen will unite them in one single parser.
Nested Layout (layout)
A Nested Layout is a reusable building block. It can be injected in bytes binary in layout field.
Usage example
Imagine a Smart Thermostat where multiple characteristics share a common "Schedule" structure.
1. Setup file structure
Create layout folder and put your layouts yaml's there.
[!IMPORTANT] Directory Naming Convention It is strictly necessary to place your reusable nested schemas in a dedicated folder named
layout. This ensures clear separation from your root protocol files.
protocols/
├── thermostat_control.yaml # [ROOT] Entry point
├── thermostat_settings.yaml # [ROOT] Another entry point
└── layouts/
└── weekly_schedule.yaml # [LAYOUT] Reusable partSet type filed to layout.
# weekly_schedule.yaml
meta:
type: layout
seq:
- id: active_days
binary: uint
size: 1
bits:
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
- sunday
- is_active
- id: start_time_minutes
binary: uint
size: 2
- id: target_temp
binary: int
size: 2
description: "Value in 0.1 increments (e.g., 210 = 21.0C)"
- id: fan_mode
binary: uint
size: 1
description: "0=Auto, 1=Low, 2=High"Add layout name to the root schema.
# thermostat_control.yaml
meta:
type: root
endianness: little
service_uuid: 0000181A-0000-1000-8000-00805F9B34FB
notifiable_characteristic_uuid: 00002A6E-0000-1000-8000-00805F9B34FB
seq:
- id: control_flags
binary: uint
size: 1
- id: current_temperature
binary: int
size: 2
- id: target_temperature
binary: uint
size: 2
- id: humidity_percentage
binary: uint
size: 1
- id: hvac_cycles_count
binary: uint
size: 4
- id: weekly_schedule
binary: bytes
layout: weekly_schedule # add layout nameBits
Any single u(int) (signed or unsigned integer) field can be treated as a bitmask. This technique allows you to use the bits property to decompose the field, where each individual bit is parsed and exposed as a separate boolean flag.
Definition in yaml example:
- id: control_flags
binary: uint
size: 1
bits:
- is_power_on
- is_heating
- is_cooling
- fan_auto_mode
- eco_save_enabledUsage example:
import ThermostatProtocolParser from "./generated/ThermostatProtocolParser.ts";
const bleProtocolBuffer = Buffer.from([
// control_flags (1 byte: 170)
0xaa,
]);
const parser = new ThermostatProtocolParser();
const deserializedValue = parser.deserializeBinaryThermostat(bleProtocolBuffer);
console.log(deserializedValue);
# {
# control_flags: {
# is_power_on: false,
# is_heating: true,
# is_cooling: false,
# fan_auto_mode: true,
# eco_save_enabled: false
# }
# }🐛 Troubleshooting
Feel free to open issue in case of any troubles
