diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..4a60754
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,94 @@
+# Bakelite
+
+Code generator for reliable embedded communication protocols. Generates serialization, framing, and CRC code from protocol definitions.
+
+## Purpose
+
+Bakelite makes it easier to build reliable communication protocols for embedded systems. While desktop/mobile systems have HTTP/REST, gRPC, etc., these aren't suitable for embedded systems with constrained memory and CPU. Bakelite targets tiny systems without a heap or memory allocator.
+
+**Wire format philosophy**: Predictable from the definition—a developer should be able to guess the wire format after reading the spec once. Bakelite is unopinionated beyond message encoding and framing. No concept of client/server—works for mesh networks, DMA, IPC, etc.
+
+**Versioning** (planned): Will be minimal and developer-controlled.
+
+## Target Platforms
+
+**Embedded** (no heap, static allocation):
+- `cpptiny` - C++
+- `ctiny` - C99
+
+**Application**:
+- `python` - Full runtime
+
+**Planned**: SystemVerilog, MicroPython, embedded Rust, C++ with stdlib, C#, Java, Rust, TypeScript. New features should consider future language implementations.
+
+## Project Structure
+
+- `bakelite/generator/` - Code generation (parser, templates, CLI)
+ - `protodef.lark` - Lark grammar for `.bakelite` IDL
+ - `templates/` - Jinja2 templates (one per language)
+ - `runtimes/` - Static runtime support files (`common/`, `cpptiny/`, `ctiny/`)
+- `bakelite/proto/` - Python runtime (serialization, COBS framing, CRC)
+- `bakelite/tests/` - Test suite
+- `examples/` - Arduino (Python↔embedded) and TCP chat (C/C++)
+- `docs/` - mkdocs documentation
+
+## Architecture
+
+- **Generator**: Parses `.bakelite` IDL → generates language-specific code via Jinja2 templates
+- **Runtime**: Static helper code (framing, CRC) that doesn't change per-protocol
+- **Zero-copy design**: Embedded targets use single-buffer approach, no dynamic allocation
+- **Template mapping**: Each language has `{lang}.py` generator + `{lang}*.j2` templates
+
+## Design Constraints
+
+When adding features:
+- Embedded targets: No heap allocation, static buffers only
+- Wire format must remain predictable from protocol definition
+- Features must be implementable across all planned target languages
+- Keep protocol-level opinions minimal (except versioning)
+
+## Development
+
+Uses **Pixi** for dependency management and task running.
+
+```bash
+pixi run test # Run Python tests
+pixi run test-cpp # Run C++ tests
+pixi run test-all # Run all tests
+pixi run lint # Run ruff and mypy
+pixi run format # Auto-format with ruff
+pixi run build # Build package
+pixi run docs # Build documentation
+```
+
+## Key Technologies
+
+- **Lark** - Parser generator for protocol DSL (`protodef.lark`)
+- **Jinja2** - Template engine for code generation
+- **Ruff** - Linting and formatting
+- **mypy** - Static type checking
+- **pytest** - Testing with pytest-describe for BDD-style tests
+- **hatchling** - Build backend for packaging
+
+## CLI
+
+```bash
+bakelite gen -l python -i proto.bakelite -o output.py
+bakelite gen -l cpptiny -i proto.bakelite -o output.h
+bakelite gen -l ctiny -i proto.bakelite -o output.h
+bakelite runtime -l cpptiny -o bakelite.h
+```
+
+## Code Style
+
+- Python 3.13+
+- Type hints enforced via mypy (strict mode)
+- 4-space indentation, 100 char line length
+- Modern type syntax (`X | None`, `list[X]`, etc.)
+- Priorities: devex, ergonomic API (even for generated code), small memory footprint, correctness, performance
+- Prefer improving type hints over `# type: ignore` comments
+- The bakelite tool is invoked with `bakelite` command. User docs should assume bakelite is in the path.
+- PRs, commits and documentation should be to the point. Only be verbose when the details matter.
+- Don't leave comments about what you've changed unless it's a commit/PR or changelog.
+- Make sure lint is clean and all tests pass after each change.
+- All target languages must have tests with at least 80% code coverage.
diff --git a/docs/protocol.md b/docs/protocol.md
index 218bf7b..6141c14 100644
--- a/docs/protocol.md
+++ b/docs/protocol.md
@@ -1,101 +1,130 @@
-# Protocol Definition
+# Protocol Specification
+
+## Overview
+
+Bakelite's protocol definition language lets you describe message structures for embedded communication.
+
+**Design goals:**
+- Simple, easily understood on-wire layout
+- Suitable for resource-constrained embedded systems
+- Low boilerplate
+
+**Non-goals:**
+- Not intended as a file interchange format
+- Not useful when one side doesn't know the schema
+
+## Types
+
+### Quick Reference
+
+| Type | Size | Description |
+|------|------|-------------|
+| `int8` | 1 byte | Signed integer |
+| `int16` | 2 bytes | Signed integer |
+| `int32` | 4 bytes | Signed integer |
+| `int64` | 8 bytes | Signed integer |
+| `uint8` | 1 byte | Unsigned integer |
+| `uint16` | 2 bytes | Unsigned integer |
+| `uint32` | 4 bytes | Unsigned integer |
+| `uint64` | 8 bytes | Unsigned integer |
+| `float32` | 4 bytes | IEEE 754 single precision |
+| `float64` | 8 bytes | IEEE 754 double precision |
+| `bool` | 1 byte | `true` (1) or `false` (0) |
+| `bytes[N]` | N bytes | Fixed-length byte array |
+| `bytes[]` | 1 + data | Variable-length bytes (length-prefixed, max 255) |
+| `string[N]` | N bytes | Fixed-length null-terminated string |
+| `string[]` | data + 1 | Variable-length null-terminated string |
+| `T[N]` | N × sizeof(T) | Fixed-length array |
+| `T[]` | 1 + elements | Variable-length array (length-prefixed, max 255) |
-## Design Goals
-* Simple
-* Easily understood on-wire layout
-* Composable
-* Suitable for use on resource constrained embedded systems
-* Low boilerplate
+### Numeric Types
-## Design Non-Goals
-* Not intended for use as a file interchange format
-* Not useful in situations where one side doesn't know the schema
+All numeric types are little-endian.
-## Primitive Types
-### Numeric Types
-|Name |Size (Bytes) |Description |
-|------------------------------|--------------|-----------------------|
-|int8, int16, int32, int64 |8, 16, 32, 64 | Signed integer |
-|uint8, uint16, uint32, uint64 |8, 16, 32, 64 | Unsigned integer |
-|float32, float64 |32, 64 | Floating point number |
-|bool |1 | true/false value |
-
-
-### Variable Length
-|Name |Size (Bytes)|Description|
-|---------|------------|-----------|
-|bytes |Fixes, N+1 | A list of uint8s |
-|string |Fixed, N+1 | A null-terminated list of uint8s, with optional encoding (ascii or utf8) |
-
-#### Bytes (Fixed Length) `bytes[n]`
-Fixed length byte types contain a defined number of bytes.
-Each byte can have any value, including NULL.
-
-For Example:
-
-| Type | bytes[4] |
-| Byte | 1 | 2 | 3 | 4 |
-| Value | 0x05 | 0x00 | 0xAF | 0xDE |
-
-
-#### Bytes (Variable Length) `bytes[]`
-Variable length byte arrays may be any size up to 255 bytes.
-When one of these fields is serialized, the first byte indicates the size,
-while the remaining bytes contain the data.
-
-For example:
-
-| Type | bytes[] |
-| Byte | 1 | 2 | 3 | 4 |
-| Value | 0x03 | 0xAA | 0x00 | 0xDE |
-| Purpose | Size | Data |
-
-
-#### String (Fixed Length) `string[n]`
-Strings are always null-terminated.
-If a string has a fixed length, any characters after the null-terminator may have any value.
-
-For example:
-
-| Type | string[5] |
-| Byte | 1 | 2 | 3 | 4 | 5 |
-| Value | H | e | y | 0x00 | 0x00 |
-
-
-Strings must always contain a null byte.
-The below example is invalid, because there is no room to store the null byte.
-
-| Type | string[5] |
-| Byte | 1 | 2 | 3 | 4 | 5 |
-| Value | H | e | l | l | o |
-
-
-#### String (Variable Length) `string[]`
-Unlike `bytes[]` variable lengths strings do not contain a size byte.
-Instead, the serializer will read until it encounters the first null byte.
-
-For example:
-
-| Type | string[] |
-| Byte | 1 | 2 | 3 | 4 |
-| Value | H | e | y | 0x00 |
-
-
-### Bitfield Types
-__Not for V1__
-
-|Name |Size (Bits)|Description |
-|-----------|-----------|----------------------------|
-|uint{N} |N bits | Unsigned integer |
-|int{N} |N bits | Signed integer |
-|flag |1 bit | True/False value |
-|unused{N} |N bit | reserved or padding bits |
-
-All types have a maximum length of 64 bits.
+```
+uint16 = 0x1234:
+┌──────┬──────┐
+│ 34 │ 12 │
+└──────┴──────┘
+ LSB MSB
+```
+
+```
+int32 = -1 (0xFFFFFFFF):
+┌──────┬──────┬──────┬──────┐
+│ FF │ FF │ FF │ FF │
+└──────┴──────┴──────┴──────┘
+```
+
+### Bool
+
+A boolean is a single byte: `0x00` for false, `0x01` for true.
+
+```
+bool = true:
+┌──────┐
+│ 01 │
+└──────┘
+```
+
+### Bytes
+
+#### Fixed Length `bytes[N]`
+
+Contains exactly N bytes. Any byte value is valid, including null.
+
+```
+bytes[4] = [0x05, 0x00, 0xAF, 0xDE]:
+┌──────┬──────┬──────┬──────┐
+│ 05 │ 00 │ AF │ DE │
+└──────┴──────┴──────┴──────┘
+```
+
+#### Variable Length `bytes[]`
+
+Prefixed with a 1-byte length (max 255 bytes).
+
+```
+bytes[] = [0xAA, 0x00, 0xDE] (3 bytes):
+┌──────┬──────┬──────┬──────┐
+│ 03 │ AA │ 00 │ DE │
+└──────┴──────┴──────┴──────┘
+ len ─── data ───
+```
+
+### Strings
+
+All strings are null-terminated.
+
+#### Fixed Length `string[N]`
+
+A null-terminated string in an N-byte buffer. Characters after the null terminator are padding (undefined values).
+
+```
+string[5] = "Hey":
+┌──────┬──────┬──────┬──────┬──────┐
+│ H │ e │ y │ 00 │ ?? │
+└──────┴──────┴──────┴──────┴──────┘
+ ─── string ─── null pad
+```
+
+A `string[N]` can hold at most N-1 characters (one byte reserved for null).
+
+#### Variable Length `string[]`
+
+A null-terminated string with no length prefix. The serializer reads until the first null byte.
+
+```
+string[] = "Hey":
+┌──────┬──────┬──────┬──────┐
+│ H │ e │ y │ 00 │
+└──────┴──────┴──────┴──────┘
+```
### Enums
-Enums are a set of named constants that help make a protocol definition easier to understand.
-For example:
+
+Enums are named integer constants with a specified underlying type.
+
```
enum PinDirection: uint8 {
Input = 0
@@ -103,171 +132,119 @@ enum PinDirection: uint8 {
Floating = 2
}
```
-Enums are always numerical, and you must specify the underlying type when declaring the enum.
-The enum above is stored using a `uint8`.
-### Struct
-A struct is a composite data type made of named members of other data types.
-For example, this struct contains three members, each with a different type.
+On the wire, enums are encoded as their underlying type:
+
```
-struct MyTestStruct {
- a: int32,
- b: boolean,
- c: uint16
-}
+PinDirection.Output:
+┌──────┐
+│ 01 │
+└──────┘
```
-Structs can contain any primitive type, bytes, string, arrays, enums, bitfield structs and, other structs.
-They cannot contain bitfield types (unless using an embedded bitfield).
-The members of a struct are encoded in order.
-There is no type or size information encoded with the struct.
-
-For example:
-
-| Type | MyTestStruct |
-| Byte | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
-| Member | a | b | c |
-
-### Bitfield Structs
-__Not for V1__
+### Structs
-A bitfield struct lets you store data that is not byte aligned.
-These can be used in situations where memory or network bandwidth is very tightly constrained or when communicating with register-based hardware.
-I2C devices, for example.
+A struct is a composite type with named members, encoded in declaration order with no padding.
-Below is an example of a bitfield definition for the control register of a TMP1075 temperature sensor.
```
-enum ConversionRate: uint{2} {
- Rate_27_5 = 0 # 27.5ms
- Rate_55 = 1 # 55ms
- Rate_110 = 2 # 110ms
- Rate_220 = 3 # 220ms
+struct Sensor {
+ id: uint8
+ temperature: int16
+ active: bool
}
+```
-bitfield struct ControlRegister {
- oneShot: flag
- conversionRate: ConversionRate
- conversionFaults: uint{2}
- outputPriority: flag
- alertFunction: flag
- shutdownMode: flag
- reserved: unused{8}
-}
+```
+Sensor { id=1, temperature=256, active=true }:
+┌──────┬──────┬──────┬──────┐
+│ 01 │ 00 │ 01 │ 01 │
+└──────┴──────┴──────┴──────┘
+ id temperature active
+ (little-endian)
```
-#### Embedded bitfields
-Bitfield types are not allowed in structs.
-You can include bitfield structs as members of a standard struct (see limitations below).
-Sometimes though, this can become verbose.
-In cases where you only need a few non-byte aligned fields in an otherwise normal struct,
-embedded bitfields may be useful.
-
-For example, this struct uses an embedded bitfield to represent the states of 8 pins in a port.
-```
-struct WritePort {
- portNum: uint8
- bitfield {
- pin1: flag
- pin2: flag
- pin3: flag
- pin4: flag
- pin5: flag
- pin6: flag
- pin7: flag
- pin8: flag
- }
+Structs can contain any type including other structs:
+
+```
+struct Reading {
+ sensor: Sensor
+ timestamp: uint32
}
```
-#### Limitations
-Due to their flexibility, bitfield structs may have a size that's not an even multiple of 8bit.
-This causes many non-byte aligned reads.
-Non-byte aligned reads are size efficient but are expensive to serialize.
-To use a bitfield in a normal struct (embedded or otherwise), it must be a multiple of 8 bits.
-This limitation also applies when directly serializing a bitfield struct.
-If your bitfield struct is not naturally a multiple of 8 bits, the `unused{n}` data type can be used to pad the data to the correct size.
-
### Arrays
-Arrays are created by appending square brackets to the end of a type.
-`[n]` for fixed length arrays, `[]` for variable-length arrays.
-There is some ambiguity when using arrays of strings or bytes.
-In such cases, the array indicator is placed after the type size.
-For example, a list of 5 variable-length strings would look like this `string[][5]`.
-#### Fixed Length
-Fixed-length arrays always contain all their elements.
-For example, an array of type `uint16[3]` will be encoded like this:
+#### Fixed Length `T[N]`
-
-| Type | uint16[3] |
-| Byte | 1 | 2 | 3 | 4 | 5 | 6 |
-| Element | 1 | 2 | 3 |
-
+N contiguous elements with no length prefix.
-#### Variable Length
-Variable-length arrays are encoded with a length byte, followed by N elements.
-For example:
+```
+uint16[3] = [1, 2, 3]:
+┌──────┬──────┬──────┬──────┬──────┬──────┐
+│ 01 │ 00 │ 02 │ 00 │ 03 │ 00 │
+└──────┴──────┴──────┴──────┴──────┴──────┘
+ ─ [0] ─ ─ [1] ─ ─ [2] ─
+```
-
-| Type | uint16[] |
-| Byte | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
-| Element | Size | 1 | 2 | ... |
-
+#### Variable Length `T[]`
+A 1-byte length prefix followed by elements (max 255 elements).
-### Choice Types
-__Not for V1__
+```
+uint16[] = [1, 2] (2 elements):
+┌──────┬──────┬──────┬──────┬──────┐
+│ 02 │ 01 │ 00 │ 02 │ 00 │
+└──────┴──────┴──────┴──────┴──────┘
+ len ─ [0] ─ ─ [1] ─
+```
-### Union Types
-__Not for V1__
+#### Arrays of Variable-Length Types
-## Endianness
-__Not for V1__
+For arrays of strings or bytes, the array brackets follow the type size:
-This will wait for a future version, but a default endianness will be assumed.
-It can then be changed on a protocol, struct, or field level.
+- `string[32][5]` — 5 fixed-length strings of 32 bytes each
+- `string[][5]` — 5 variable-length strings
-## Framing
-A few different framing types will be supported. For the first version, only COBS will be implemented.
+## Wire Format
-### Fixed
-__Not for V1__
+### Byte Order
-Frames messages into fixed-length packets.
-Use this when your underlying hardware can reliably deliver corruption-free fixed-length messages.
+All multi-byte values are little-endian (LSB first).
-This is not suitable for serial or TCP links.
+### Message Frame Layout
-### Length Based
-__Not for V1__
+With COBS framing and CRC, a message is transmitted as:
-A simple framing method where a 4-byte length prefix is added to the data before it is transmitted.
-It's a lightweight protocol suitable for situations where the underlying stack provides error correction.
+```
+┌──────┬──────────┬─────────────────┬───────┬──────┐
+│ COBS │ Msg ID │ Message Payload │ CRC │ 0x00 │
+│ byte │ (1 byte) │ │ │ │
+└──────┴──────────┴─────────────────┴───────┴──────┘
+```
-This would be suitable for use with TCP, or with serial protocols that have well-defined frame boundaries.
-It is not suitable for Serial since you may start reading in the middle of a packet.
+Example: 6-byte message with COBS and CRC16:
-### COBS
-COBS is a robust framing algorithm with low, fixed overhead.
-It is also computationally efficient.
-Combined with CRC error checking, it is robust against corruption and synchronization loss. See the [Wikipedia article](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) for more details.
+```
+┌──────┬──────┬──────────────────────────────┬──────┬──────┬──────┐
+│ COBS │ ID │ Message (6 B) │ CRC │ CRC │ 0x00 │
+└──────┴──────┴──────────────────────────────┴──────┴──────┴──────┘
+ 1B 1B 6B 2B (CRC16) 1B
+ Total: 11 bytes on wire
+```
-COBS is suitable for serial protocols.
+## Protocol Block
-#### Error Checking
-CRC8, 16, and 32 are supported.
-CRC checks can be disabled if the link is reliable.
-Future versions may implement other error detection/correction schemes.
+The protocol block configures framing, error checking, and message IDs.
-## Protocol
-The protocol wraps structs, framing, CRC, and other settings into an object the code generator can use to generate helpers.
-The protocol works by sending and receiving messages.
-Messages are simply structs that have been assigned a message ID.
-When sent, each messages is wrapped in a frame, and proceeded by a message ID byte.
-See the [layout](#layout) section below for more details.
+```
+struct TestMessage {
+ value: uint16
+}
+
+struct Ack {
+ code: uint8
+}
-The below protocol would generate an implementation that uses COBS framing, CRC8, and supported up to 256 byte messages. It supports two messages `TestMessage` and `Ack`.
-```proto
protocol {
maxLength = 256
framing = COBS
@@ -281,59 +258,66 @@ protocol {
```
### Message IDs
-To send a struct, it must be assigned a message ID.
-Message IDs are used by the receiver to determine which struct to decode.
-If a struct is nested inside another struct, only the root struct needs to be assigned an ID.
-In the example below, we only intend to send TestMessage, so it is assigned the ID of one.
-SubMessage is never sent directly, so no ID is assigned.
-```proto
+Each message type needs a unique ID (1–255; 0 is reserved). Only top-level messages need IDs—nested structs don't.
+
+```
struct SubMessage {
code: uint8
- text: string[]
}
-TestMessage {
- data: SubMessage
- success: bool
+struct TestMessage {
+ sub: SubMessage
}
protocol {
- ...
messageIds {
- TestMessage = 1
+ TestMessage = 1 # SubMessage doesn't need an ID
}
}
```
-### Max Length
-The `maxLength` parameter describes the maximum size, in bytes, of a message.
-This does not include the framing overhead, CRC, or message ID byte.
-For example, a maxLength of 64 means that any struct with a size of up to 64 bytes can be sent.
-
-The length of the frame that is actually sent will be longer.
-For the above example, if we sent a struct that was 64 bytes in size using COBS framing and CRC16 error detection, then the on-wire frame size would be 69 bytes.
-
-### Layout
-Here's an example frame with an 6 byte message, COBS framing and CRC16 error checking.
-
-
- | Role |
- Framing |
- Message |
- Framing |
-
-
- | Byte |
- 1 | 2 | 3 | 4 | 5 | 6 |
- 7 | 8 | 9 | 10 | 11 |
-
-
- | Element |
- COBS Overhead |
- Message Id |
- Message |
- CRC16 |
- Null Byte |
-
-
\ No newline at end of file
+### Options
+
+| Option | Values | Description |
+|--------|--------|-------------|
+| `maxLength` | integer | Maximum message payload size in bytes (excludes framing/CRC overhead) |
+| `framing` | `COBS`, `None` | Framing algorithm |
+| `crc` | `CRC8`, `CRC16`, `CRC32`, `None` | Error detection |
+
+#### COBS Framing
+
+COBS (Consistent Overhead Byte Stuffing) provides reliable framing with low, fixed overhead. Messages are delimited by null bytes. See [Wikipedia](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) for details.
+
+#### CRC
+
+CRC error detection catches transmission errors. The CRC is computed over the message ID and payload, appended before COBS encoding.
+
+## Grammar Reference
+
+```ebnf
+file = (enum | struct | protocol | comment)+
+
+comment = "#"
+
+enum = [annotations] "enum" NAME ":" type "{" enum_value+ "}"
+enum_value = [annotations] NAME "=" NUMBER
+
+struct = [annotations] "struct" NAME "{" struct_member+ "}"
+struct_member = [annotations] NAME ":" type [array] ["=" default]
+
+protocol = "protocol" "{" (option | messageIds)+ "}"
+option = NAME "=" VALUE
+messageIds = "messageIds" "{" message_id+ "}"
+message_id = NAME "=" NUMBER
+
+type = primitive | NAME
+primitive = "int8" | "int16" | "int32" | "int64"
+ | "uint8" | "uint16" | "uint32" | "uint64"
+ | "float32" | "float64" | "bool"
+ | ("bytes" | "string") "[" [NUMBER] "]"
+
+array = "[" [NUMBER] "]"
+annotations = annotation+
+annotation = "@" NAME ["(" arguments ")"]
+```
diff --git a/docs/python.md b/docs/python.md
index 900447a..eaff3b3 100644
--- a/docs/python.md
+++ b/docs/python.md
@@ -1,40 +1,48 @@
# Python
The Python API is designed to be simple to use.
-It requires at least Python 3.8.
+It requires Python 3.13+.
It is not currently compatible with MicroPython.
-## Example
-Code generation
+## Code Generation
+
+Generate the runtime library and protocol code:
```sh
-$ bakelite gen -l python -i proto.bakelite -o proto.py
+bakelite runtime -l python # Creates bakelite_runtime/ folder
+bakelite gen -l python -i proto.bakelite -o proto.py
```
-Use the generated code to implement a simple protocol.
+## Example (Sync)
+
```python
-from proto import Protocol, TestMessage, Ack # Generated by bakelite
+from proto import Protocol, TestMessage, Ack
import serial
-# Open the serial port at 9600 baud
with serial.Serial("/dev/ttyusb0", 9600) as port:
- # Create an instance of our protocol, passing the port
- # we just opened as the stream.
- proto = Protocol(stream=port)
-
- # Send a test message
- proto.send(
- TestMessage(status=True, message="Ping!".encode('utf-8'))
- )
-
- # Wait for responses, and print them.
- while True:
- # Poll for new messages. By default, this function blocks
- # untill there is data to read.
- msg = proto.poll()
- if msg:
- if isinstance(msg, Ack): # if we got an ack, print it!
- print(f"Ack code={msg.code} message=\"{msg.message.decode('utf-8')}\"")
- else: # We got a message we didn't expect, print it too
- print(msg)
+ proto = Protocol(stream=port)
+ proto.send(TestMessage(status=True, message="Ping!"))
+
+ for msg in proto.messages():
+ if isinstance(msg, Ack):
+ print(f"Ack code={msg.code}")
+```
+
+## Example (Async)
+
+```python
+import asyncio
+from proto import Protocol, TestMessage, Ack
+
+async def main():
+ reader, writer = await asyncio.open_connection('localhost', 8888)
+ proto = Protocol(stream=(reader, writer))
+
+ await proto.send(TestMessage(status=True, message="Ping!"), async_=True)
+
+ async for msg in proto.messages(async_=True):
+ if isinstance(msg, Ack):
+ print(f"Ack code={msg.code}")
+
+asyncio.run(main())
```
### String Handling
@@ -62,43 +70,42 @@ If you need to send `utf-16` encoded text, use a bytes[] type instead.
## API
### Protocol
The Protocol class is generated by bakelite if you have a `protocol` section in your protocol definition.
-Pass any stream-like object to the constructor,
-a serial port from PySerial, or a TCP socket, for example.
-
-__example definition:__
-```proto
-protocol {
- maxLength = 256
- framing = COBS
- crc = CRC8
+Pass a stream-like object to the constructor:
+- For sync: `BufferedIOBase` (serial port, TCP socket, etc.)
+- For async: `tuple[StreamReader, StreamWriter]`
- messageIds {
- ...
- }
-}
-```
-
-##### \_\_init\_\_(self, stream: BufferedIOBase)
+##### \_\_init\_\_(self, stream)
__arguments:__
-* __stream__ - Any stream like object that implements `read()` and `write()` functions.
+* __stream__ - `BufferedIOBase` for sync, or `(StreamReader, StreamWriter)` for async.
-##### poll(self) -> Struct | None
-Call this function to wail for a message.
-It will read any available data from the stream.
-If a message is available, the function will return the decoded struct for that message.
-Otherwise, it returns None.
+##### messages(self, async_=False) -> Iterator | AsyncIterator
+The primary interface for consuming messages. Returns an iterator that yields messages as they arrive.
-__returns:__
-Poll() will either return a valid struct or None if there is no message to decode.
+```python
+# Sync
+for msg in proto.messages():
+ handle(msg)
-##### send(self, message: Any) -> None
+# Async
+async for msg in proto.messages(async_=True):
+ await handle(msg)
+```
+
+##### send(self, message, async_=False) -> None | Coroutine
Sends a message. The message is serialized, encoded as a frame, and sent to the stream.
-You can pass any struct, as long as it was assigned a message ID in the protocol spec.
__arguments:__
* __message__ - Any struct with an assigned message-id.
+* __async___ - If True, returns a coroutine for async sending.
+
+##### poll(self, async_=False) -> Struct | None | Coroutine
+Reads available data and returns a message if one is complete, otherwise None.
+
+__arguments:__
+
+* __async___ - If True, returns a coroutine for async polling.
### Struct
A struct class is generated for each struct in your protocol definition.
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 41ac387..4be6d6d 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -2,7 +2,7 @@
## Install Bakelite
-Bakelite requires Python 3.8 or above.
+Bakelite requires Python 3.13+.
Install it via pip.
```bash
@@ -43,5 +43,6 @@ $ bakelite runtime -l cpptiny -o bakelite.h
$ bakelite gen -l cpptiny -i my_proto.bakelite -o my_proto.h
# Generate Python code
+$ bakelite runtime -l python
$ bakelite gen -l python -i my_proto.bakelite -o my_proto.py
```
\ No newline at end of file