npm package: @stormotion/ble-protocol-generator
Description: TypeScript code generator for parsing BLE protocols from YAML schemas.
Author: yevhenii.burkovskyi@stormotion.io
Latest version: 0.0.4

# @stormotion/ble-protocol-generator

![npm version](https://img.shields.io/npm/v/@stormotion/ble-protocol-generator) ![License](https://img.shields.io/npm/l/@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](https://github.com/nonergodic/layout) for performant and accurate bit-level manipulation.

## Navigation

- [💡 Motivation](#-motivation)
- [📦 Installation](#-installation)
- [🚀 Quick Start](#-quick-start)
- [📝 Protocol Definition](#-protocol-definition)
- [🔢 Binary](#-binary)
- [🦸 Advanced](#-advanced)
- [🐛 Troubleshooting](#-troubleshooting)

## 💡 Motivation

Mobile / Web apps interacting with BLE hardware often require complex binary data handling.

- **Stop manual bit-shifting:** No more maintaining fragile `DataView` code.
- **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

```bash
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.

```bash
mkdir protocol
```

### 2. 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`

```bash
touch protocol/thermostat_control.yaml
```

### 3. Define schema

Define yaml schema using special syntax

**Basic example:**

```bash
# 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: 32
```

### 4. Generate parser

Generate parser with command

```bash
# Syntax
ble-gen <source_path> <output_parser_name>

# Example
ble-gen ./thermostat ThermostatProtocolParser
```

### Output

If the command runs successfully, you will see a confirmation message indicating where the file with your parser was saved.

```bash
✅ Generated <output_parser_name> into generated/<output_parser_name>.ts

# Example
✅ Generated ThermostatProtocolParser into generated/ThermostatProtocolParser.ts
```

Now you free to use generated parser anywhere in the code!

```bash
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](#root-and-layout) |
| 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](#-binary)                                                                                                                                                                  |
| 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](#bits). |

## 🔢 Binary

The binary types used in the `seq` block closely align with those provided by the robust [binary-layout](https://github.com/nonergodic/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](https://www.npmjs.com/package/binary-layout)

**Definition in yaml example:**

```bash
  - id: header
    binary: uint
    size: 1
```

### Bytes

Raw bytes or sub-layouts. By default, converted into a `Uint8Array` when not specifying a sub-layout, or an object otherwise. [Source](https://www.npmjs.com/package/binary-layout)

**Definition in yaml example::**

Returns raw bytes as `Uint8Array`.

```bash
  - id: reserve
    binary: bytes
    size: 2
```

Returns deserialized object.

```bash
  - id: weekly_schedule
    binary: bytes
    layout: schedule_day_config
```

### Switch

Enables branching logic. Comparable to Rust enums. Converted into a union type of the derived types of its layout variants. [Source](https://www.npmjs.com/package/binary-layout)

The following example demonstrates how to map specific `layout` sequences to the values `42` and `254` which are **address bytes**.

```bash
- 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.

```text
protocols/
├── thermostat_control.yaml   # [ROOT] Entry point
├── thermostat_settings.yaml   # [ROOT] Another entry point
└── layouts/
    └── weekly_schedule.yaml   # [LAYOUT] Reusable part
```

Set `type` filed to `layout`.

```bash
# 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.

```bash
# 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 name
```

### Bits

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:**

```bash
- id: control_flags
    binary: uint
    size: 1
    bits:
      - is_power_on
      - is_heating
      - is_cooling
      - fan_auto_mode
      - eco_save_enabled
```

**Usage example:**

```bash
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