This repository defines OpenEarable binary protocols as YAML schemas and generates language bindings from those schemas.
Run the generator from the repository root:
.venv/bin/python -m protocol_generatorBy default, schemas are read from schemas/*/protocol.yml and generated code is
written to:
generated/dart/lib/open_earable_protocols.dartgenerated/dart/lib/src/protocol_runtime.dartgenerated/dart/lib/src/*_protocol.dartgenerated/c/include/protocol_runtime.hgenerated/c/include/*_protocol.hgenerated/c/include/zephyr/*_ble.hgenerated/c/src/protocol_runtime.cgenerated/c/src/*_protocol.cgenerated/c/CMakeLists.txt
Custom directories can be provided when integrating with a build system:
.venv/bin/python -m protocol_generator --schema-dir schemas --output-dir generatedGenerate only one language target:
.venv/bin/python -m protocol_generator --language dartThe generator is implemented as the protocol_generator package. See
docs/architecture.md for module responsibilities and
the process for adding another language.
Each language emits one shared binary codec runtime. Generated protocol files import or include that runtime instead of embedding duplicate reader, writer, status, and primitive codec helpers.
The generated Dart bindings form the publishable open_earable_protocols
package under generated/dart. Flutter and Dart applications can depend on
that directory and import all generated protocols from:
import 'package:open_earable_protocols/open_earable_protocols.dart';Before publishing a release, update generated/dart/pubspec.yaml and
generated/dart/CHANGELOG.md, regenerate the bindings, and run:
cd generated/dart
dart pub publish --dry-runEach protocol schema defines metadata and ordered message fields:
protocol: audio-response
version: 1
description: Protocol description.
messages:
message_name:
description: Human-readable documentation for the generated message type.
fields:
- name: count
type: uint8
- name: values
type: uint16[count]Message descriptions are optional but recommended. The generator emits them as Dart documentation comments and C Doxygen comments. When omitted, it generates a stable description from the message and protocol names.
Protocol changes must remain backwards compatible. Existing devices, applications, and generated bindings must continue to understand payloads that were valid before the change. In practice, this means existing message names, field order, field types, BLE UUIDs, and tagged-union discriminator values are stable once released.
Prefer additive changes. New messages, new fields at explicitly compatible extension points, new tagged-union variants with unused discriminator values, and new BLE characteristics are safer than changing existing wire contracts. Never reuse a removed discriminator value or characteristic UUID for a different meaning.
Optional BLE transport metadata is retained in generated APIs:
transport:
ble:
service_uuid: 12345678-1234-5678-1234-56789abcdef0
characteristics:
- name: measurements
uuid: 12345678-1234-5678-1234-56789abcdef1
properties: [read, notify]Supported characteristic properties are broadcast, read,
write_without_response, write, notify, indicate,
authenticated_signed_writes, and extended_properties. Hyphenated and
UniversalBLE-style camelCase spellings such as writeWithoutResponse are also
accepted.
Dart exposes framework-neutral service and characteristic definitions through
a protocol-specific *BleUuids constants class. Its property enum names match
UniversalBLE's CharacteristicProperty names, so an application can convert
them without making the generated package depend on UniversalBLE:
import 'package:open_earable_protocols/open_earable_protocols.dart';
import 'package:universal_ble/universal_ble.dart';
final definition = AudioResponseBleUuids.transferControlCharacteristic;
final properties = definition.mapPropertiesByName(
CharacteristicProperty.values,
(property) => property.name,
);
final characteristic = BleCharacteristic(definition.uuid, properties, []);isReadable and isWritable are available when deriving peripheral
permissions.
C exposes UUID strings and standard GATT property bitmasks as protocol-prefixed macros. An additional opt-in Zephyr header maps those values to Zephyr UUID declarations, characteristic properties, and default permissions:
#include "zephyr/audio_response_ble.h"
BT_GATT_SERVICE_DEFINE(audio_response_service,
BT_GATT_PRIMARY_SERVICE(AUDIO_RESPONSE_ZEPHYR_SERVICE_UUID),
BT_GATT_CHARACTERISTIC(
AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_UUID,
AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_PROPERTIES,
AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_PERMISSIONS,
read_callback, write_callback, NULL),
BT_GATT_CCC(configuration_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE));Add a BT_GATT_CCC entry when a characteristic uses notify or indicate.
The Zephyr adapter derives ordinary read/write permissions; applications remain
responsible for selecting encrypted or authenticated permission variants when
required.
Supported scalar field types are:
- Integers:
uint8,uint16,uint32,int8,int16, andint32. - Floating point:
floatanddouble.
Multi-byte values are encoded little-endian. float uses the 4-byte IEEE-754
binary32 representation and double uses the 8-byte IEEE-754 binary64
representation.
Supported dynamic field types:
bytes[length_field]for variable-length byte payloads.uint16[length_field]and other scalar arrays for variable-length arrays.float[length_field]anddouble[length_field]for floating-point arrays.
Length fields must be integer fields and must appear before the dynamic field that references them.
Tagged unions declare their discriminator width and stable numeric tags explicitly:
- name: command
type: union
tag_type: uint8
variants:
0: transfer_start
1: transfer_commit
2: transfer_abortThe generated C API exposes a named discriminator enum, typed constructors, and
a typed visitor. It also generates typed setters for union fields. Construct
single-union messages with functions such as
audio_response_transfer_control_from_start, or set an existing message with
audio_response_transfer_control_set_command_start; dispatch decoded values
with audio_response_transfer_control_dispatch. The generated Dart API uses a
sealed payload interface and named factories such as
AudioResponseTransferControl.start. Both APIs infer the discriminator from
the selected payload type.
Generated C structs use pointers for variable-length byte and array fields.
The caller owns that memory and must allocate enough space before calling a
decode function. The decoder fills the caller-provided buffers and reports
PROTOCOL_ERROR_INVALID_DATA if the input is truncated or has an unknown union
tag.
Compile protocol_runtime.c once and link it together with all generated
protocol-specific C sources. All generated C encode/decode functions return the
shared protocol_status_t type.
The generated C99 bindings under generated/c are a CMake library. CMake
projects can add the directory and link OpenEarable::Protocols. The generated
target compiles every protocol source and exposes generated/c/include to
consumers.
See generated/c/README.md for general usage and
docs/open-earable-2-integration.md for
explicit firmware integration.