From d57c2a3c9d7e310b4d03b8ed0554cfc90d9b2458 Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 14:50:59 -0500 Subject: [PATCH 1/3] Update documentation - Add CLAUDE.md with project instructions - Update Python docs with async examples and messages() API - Update version requirement to Python 3.13+ - Add runtime generation steps to quickstart - Update .gitignore for Pixi and generated files --- CLAUDE.md | 55 ++++++++++++++++++++++ docs/python.md | 111 ++++++++++++++++++++++++--------------------- docs/quickstart.md | 3 +- 3 files changed, 116 insertions(+), 53 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ccb6e76 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# Bakelite + +Code generator for embedded systems communication protocols. Generates serialization, framing, and CRC code from protocol definition files targeting Python and C++. + +## Project Structure + +- `bakelite/generator/` - Code generation (parser, Jinja2 templates, CLI) +- `bakelite/proto/` - Python runtime (serialization, COBS framing, CRC) +- `bakelite/proto/types.py` - Runtime type descriptors (no external deps) +- `bakelite/tests/` - Test suite +- `docs/` - Documentation (mkdocs) + +## 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.def -o output.py +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 for this project are devex and an ergonomic api (even for generated code), small memory footpring for embedded platforms, corectness, performance. +- Prefer improvig type hints over # type-ignore comments +- The bakelite tool is just with with the bakelite command. No need for pixi run -- bakelite unless the pixi environment isn't active. 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 to the person reading the docs. When in doubt, ask me what the docs intended purpose is. +- Don't leave comments about what you've changed unless it's a commit/pr or changelog. It makes docs and code comments cofusing. +- Don't leave comments about what you've changed unless it's a commit/pr or changelog. It makes docs and code comments cofusing. +- make sure lint is clean and all tests pass after each change \ No newline at end of file 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 From 5126801d20c669c781df5ebf774d8a8e8f57511b Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sat, 29 Nov 2025 22:14:05 -0500 Subject: [PATCH 2/3] Rewrite protocol specification docs - Replace HTML tables with Unicode box-drawing diagrams - Remove unimplemented "Not for V1" sections - Add quick reference table and grammar reference - Reorganize into clearer structure --- docs/protocol.md | 514 +++++++++++++++++++++++------------------------ 1 file changed, 249 insertions(+), 265 deletions(-) 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: - - - - -
Typebytes[4]
Byte1234
Value0x050x000xAF0xDE
- -#### 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: - - - - - -
Typebytes[]
Byte1234
Value0x030xAA0x000xDE
PurposeSizeData
- -#### 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: - - - - -
Typestring[5]
Byte12345
ValueHey0x000x00
- -Strings must always contain a null byte. -The below example is invalid, because there is no room to store the null byte. - - - - -
Typestring[5]
Byte12345
ValueHello
- -#### 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: - - - - -
Typestring[]
Byte1234
ValueHey0x00
- -### 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: - - - - -
TypeMyTestStruct
Byte1234567
Memberabc
-### 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]` - - - - -
Typeuint16[3]
Byte123456
Element123
+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] ─ +``` - - - - -
Typeuint16[]
Byte1234567
ElementSize12...
+#### 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. - - - - - - - - - - - - - - - - - - - - -
RoleFramingMessageFraming
Byte1234567891011
ElementCOBS OverheadMessage IdMessageCRC16Null 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 ")"] +``` From 6a64334493cd362d82a774d745264516ee6b37cf Mon Sep 17 00:00:00 2001 From: Emma Powers Date: Sun, 30 Nov 2025 11:23:55 -0500 Subject: [PATCH 3/3] Update CLAUDE.md with project context and architecture Add purpose, target platforms, architecture, and design constraints sections. Update project structure to include templates, runtimes, and examples. Add 80% code coverage requirement for all target languages. Fix typos and remove duplicate line. --- CLAUDE.md | 63 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ccb6e76..4a60754 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,14 +1,51 @@ # Bakelite -Code generator for embedded systems communication protocols. Generates serialization, framing, and CRC code from protocol definition files targeting Python and C++. +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, Jinja2 templates, CLI) +- `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/proto/types.py` - Runtime type descriptors (no external deps) - `bakelite/tests/` - Test suite -- `docs/` - Documentation (mkdocs) +- `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 @@ -36,7 +73,9 @@ pixi run docs # Build documentation ## CLI ```bash -bakelite gen -l python -i proto.def -o output.py +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 ``` @@ -46,10 +85,10 @@ bakelite runtime -l cpptiny -o bakelite.h - Type hints enforced via mypy (strict mode) - 4-space indentation, 100 char line length - Modern type syntax (`X | None`, `list[X]`, etc.) -- Priorities for this project are devex and an ergonomic api (even for generated code), small memory footpring for embedded platforms, corectness, performance. -- Prefer improvig type hints over # type-ignore comments -- The bakelite tool is just with with the bakelite command. No need for pixi run -- bakelite unless the pixi environment isn't active. 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 to the person reading the docs. When in doubt, ask me what the docs intended purpose is. -- Don't leave comments about what you've changed unless it's a commit/pr or changelog. It makes docs and code comments cofusing. -- Don't leave comments about what you've changed unless it's a commit/pr or changelog. It makes docs and code comments cofusing. -- make sure lint is clean and all tests pass after each change \ No newline at end of file +- 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.