Python

The Python API is designed to be simple to use. It requires at least Python 3.8. It is not currently compatible with MicroPython.

Example

Code generation

$ bakelite gen -l python -i proto.bakelite -o proto.py

Use the generated code to implement a simple protocol.

from proto import Protocol, TestMessage, Ack  # Generated by bakelite 
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)

String Handling

In Python 3, strings are Unicode by default. You'll need to encode the string as bytes before sending a message and decode them after receiving a message.

For example:

# send a message
msg TestMessage(
  text: "This is a test string".encode('utf-8') # encode the string as utf-8
)
proto.send(msg)

# receive a message
msg = proto.poll()
print(msg.text.decode('utf-8'))

Any encoding that doesn't add null characters will work. The two most common encoding types are utf-8 and ascii. If you're working with an embedded device that doesn't have native Unicode support, ascii is recommended. 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:

protocol {
  maxLength = 256
  framing = COBS
  crc = CRC8

  messageIds {
    ...
  }
}
__init__(self, stream: BufferedIOBase)

arguments:

  • stream - Any stream like object that implements read() and write() functions.
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.

returns:
Poll() will either return a valid struct or None if there is no message to decode.

send(self, message: Any) -> None

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.

Struct

A struct class is generated for each struct in your protocol definition. Structs are dataclasses where each field in the struct becomes a member of the class.

For example, this struct definition:

struct TestMessage {
  text: string[]
  code: uint8
}

is equivalent to:

@dataclass
class TestMessage:
  text: str
  code: int

  pack(self, stream) -> None:
    ...

  unpack(stream) -> TestMessage:
    ...

The pack() and unpack() function are provided for serialization.

pack(self, stream: BufferedIOBase) -> None

Serialize the struct and write it to the stream.

arguments:

  • stream - Any stream like object that implements read() and write() functions.
unpack(stream: BufferedIOBase) -> Struct

Deserialize a struct from the stream. unpack() is a static method which returns an instance of the struct.

example:

msg = TestMessage.unpack(stream)
print(msg.text)

arguments:

  • stream - Any stream like object that implements read() and write() functions.

returns:
An instance of the struct class.

Enum

An enum class is generated for each enum in your protocol definition. Enums map to python's enum class.

For example:

enum MyColor: uint8 {
  Red = 1
  Green = 2
  Blue = 3
}

Would map to:

class MyColor(enum):
  Red = 1
  Green = 2
  Blue = 3

  pack(self, stream) -> None:
    ...

  unpack(stream) -> MyColor:
    ...
pack(self, stream: BufferedIOBase) -> None

Serialize the enum and write it to the stream.

arguments:

  • stream - Any stream like object that implements read() and write() functions.
unpack(stream: BufferedIOBase) -> Struct

Deserialize an enum from the stream. unpack() is a static method which returns an instance of the enum class.

example:

color = MyColor.unpack(stream)
print(color)

arguments:

  • stream - Any stream like object that implements read() and write() functions.

returns:
An instance of the enum class.