Skip to content

Commit

Permalink
Start writing README
Browse files Browse the repository at this point in the history
  • Loading branch information
fchorney committed Feb 18, 2025
1 parent deac995 commit 7275e54
Show file tree
Hide file tree
Showing 7 changed files with 485 additions and 227 deletions.
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,93 @@
# PyStructTypes

Leverage Python Types to Define C-Struct Interfaces


# Reasoning

I made this project for 2 reasons:
1. I wanted to see if I could leverage the typing system to effectively automatically
decode and encode c-type structs in python.
2. Build a tool to do this for a separate project I am working on.

I am aware of other very similar c-type struct to python class libraries available,
but I wanted to try something new so here we are.

This may or may not end up being super useful, as there are quite a few bits of
hacky metaprogramming to get the type system to play nicely for what I want, but
perhaps over time it can be cleaned up and made more useful.

# Basic Structs

Basic structs can mostly be copied over 1:1

```c
struct MyStruct {
int16_t myNum;
char myLetter;
};
```

```python
@struct_dataclass
class MyStruct(StructDataclass):
myNum: int16_t
myLetter: char_t

s = MyStruct()
s.decode([4, 2, 65])
# MyStruct(myNum=1026, myLetter=b"A")
s.decode([4, 2, 65], little_endian=True)
# MyStruct(myNum=516, myLetter=b"A")
```

For arrays of basic elements, you need to Annotate them with
the `TypeMeta` object, and set their type to `list[_type_]`.

```c
struct MyStruct {
uint8_t myInts[4];
uint16_t myBiggerInts[2];
};
```
```python
@struct_dataclass
class MyStruct(StructDataclass):
myInts: Annotated[list[uint8_t], TypeMeta(size=4)]
myBiggerInts: Annotated[list[uint16_t], TypeMeta(size=2)]

s = MyStruct()
s.decode([0, 64, 128, 255, 16, 0, 255, 255])
# MyStruct(myInts=[0, 64, 128, 255], myBiggerInts=[4096, 65535])
```

# The Bits Abstraction

This library includes a `bits` abstraction to map bits to variables for easier access.

One example of this is converting a C enum like so:

```c
enum ConfigFlags {
lights_flag = 1 << 0,
platform_flag = 1 << 1,
};
#pragma pack(push, 1)
```

```python
@bits(uint8_t, {"lights_flag": 0, "platform_flag": 1})
class FlagsType(BitsType): ...

f = FlagsType()
f.decode([3])
# FlagsType(lights_flag=True, platform_flag=True)
f.decode([2])
# FlagsType(lights_flag=False, platform_flag=True)
f.decode([1])
# FlagsType(lights_flag=True, platform_flag=False)
```

# Custom StructDataclass Processing and Extensions

TODO: do this section
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ universal = 1

[mypy]
# ignore_missing_imports = true
check_untyped_defs = true
check_untyped_defs = true
8 changes: 6 additions & 2 deletions src/pystructtype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ class TypeInfo:
byte_size: int


# TODO: Support proper "c-string" types

# Fixed Size Types
char_t = Annotated[int, TypeInfo("c", 1)]
int8_t = Annotated[int, TypeInfo("b", 1)]
uint8_t = Annotated[int, TypeInfo("B", 1)]
int16_t = Annotated[int, TypeInfo("h", 2)]
Expand All @@ -74,8 +77,9 @@ class TypeInfo:
int64_t = Annotated[int, TypeInfo("q", 8)]
uint64_t = Annotated[int, TypeInfo("Q", 8)]

# TODO: Make a special Bool class to auto-convert from int to bool

# Named Types
bool_t = Annotated[bool, TypeInfo("?", 1)]
float_t = Annotated[float, TypeInfo("f", 4)]
double_t = Annotated[float, TypeInfo("d", 8)]

Expand Down Expand Up @@ -169,7 +173,7 @@ def __post_init__(self):
pass
self._simplify_format()
self._byte_length = struct.calcsize("=" + self.struct_fmt)
print(f"{self.__class__.__name__}: {self._byte_length} : {self.struct_fmt}")
# print(f"{self.__class__.__name__}: {self._byte_length} : {self.struct_fmt}")

def _simplify_format(self) -> None:
# First expand the format
Expand Down
223 changes: 223 additions & 0 deletions test/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import itertools
from dataclasses import field
from enum import IntEnum
from typing import Annotated

from pystructtype import (
BitsType,
StructDataclass,
TypeMeta,
bits,
list_chunks,
struct_dataclass,
uint8_t,
uint16_t,
)

TEST_CONFIG_DATA = [
# masterVersion
5,
# configVersion
5,
# flags
3,
# debounceNodelayMilliseconds
15, 0,
# debounceDelayMilliseconds
0, 0,
# panelDebounceMicroseconds
160, 15,
# autoCalibrationMaxDeviation
100,
# badSensorMinimumDelaySeconds
15,
# autoCalibrationAveragesPerUpdate
44, 1,
# autoCalibrationSamplesPerAverage
100, 0,
# autoCalibrationMaxTare
255, 255,
# enabledSensors[5]
15, 15, 15, 15, 0,
# autoLightsTimeout
7,
# stepColor[3 * 9]
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
170, 170, 170,
# platformStripColor[3]
0, 72, 143,
# autoLightPanelMask
170, 0,
# panelRotation
0,
# panelSettings[9]
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
33, 42, 235, 235, 235, 235, 238, 238, 238, 238, 255, 255, 255, 255, 0, 0,
# preDetailsDelayMilliseconds
5,
# padding[49]
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255,
]


class Panel(IntEnum):
UPLEFT = 0
UP = 1
UPRIGHT = 2
LEFT = 3
CENTER = 4
RIGHT = 5
DOWNLEFT = 6
DOWN = 7
DOWNRIGHT = 8


class Sensor(IntEnum):
LEFT = 0
RIGHT = 1
UP = 2
DOWN = 3


@bits(uint8_t, {"autolights": 0, "fsr": 1})
class FlagsType(BitsType): ...


@bits(uint16_t, {"steps": [0, 1, 2, 3, 4, 5, 6, 7, 8]})
class PanelMaskType(BitsType):
def __getitem__(self, index: int) -> bool:
# This lets us access the data with square brackets
# ex. `config.PanelMaskType[Panel.UP]`
return getattr(self, "steps", [])[index]

def __setitem__(self, index: int, value: bool) -> None:
# This lets us set the data with square brackets
# ex. `config.PanelMaskType[Panel.DOWN] = True`
steps = getattr(self, "steps", [])
assert index <= len(steps)
steps[index] = value


@struct_dataclass
class EnabledSensors(StructDataclass):
# We can define the actual data we are ingesting here
_raw: Annotated[list[uint8_t], TypeMeta(size=5)]

# We use this to store the data in the way we actually want
_data: list[list[bool]] = field(default_factory=list)

def _decode(self, data: list[int]) -> None:
# First call the super function to put the values in to _raw
super()._decode(data)

# Erase everything in self._data to remove any old data
self._data = []

# 2 Panels are packed into a single uint8_t, the left most 4 bits for the first
# and the right most 4 bits for the second
for bitlist in (list(map(bool, map(int, format(_byte, "#010b")[2:]))) for _byte in self._raw):
self._data.append(bitlist[0:4])
self._data.append(bitlist[4:])

# Remove the last item in self._data as there are only 9 panels
del self._data[-1]

def _encode(self) -> list[int]:
# Modify self._raw with updates values from self._data
for idx, items in enumerate(list_chunks(self._data, 2)):
# Last chunk
if len(items) == 1:
items.append([False, False, False, False])
self._raw[idx] = sum(v << i for i, v in enumerate(list(itertools.chain.from_iterable(items))[::-1]))
# Run the super function to return the data in self._raw()
return super()._encode()

def __getitem__(self, index: int) -> list[bool]:
# This lets us access the data with square brackets
# ex. `config.enabled_sensors[Panel.UP][Sensor.RIGHT]`
return self._data[index]

def __setitem__(self, index: int, value: list[bool]) -> None:
# Only use this to set a complete set for a panel
# ex. `config.enabled_sensors[Panel.UP] = [True, True, False, True]`
if len(value) != 4 or not all(isinstance(x, bool) for x in value):
raise Exception("use the right type of data scrub")
self._data[index] = value


@struct_dataclass
class PackedPanelSettingsType(StructDataclass):
load_cell_low_threshold: uint8_t
load_cell_high_threshold: uint8_t

fsr_low_threshold: Annotated[list[uint8_t], TypeMeta(size=4)]
fsr_high_threshold: Annotated[list[uint8_t], TypeMeta(size=4)]

combined_low_threshold: uint16_t
combined_high_threshold: uint16_t

reserved: uint16_t


@struct_dataclass
class RGBType(StructDataclass):
r: uint8_t
g: uint8_t
b: uint8_t


@struct_dataclass
class SMXConfigType(StructDataclass):
master_version: uint8_t = 0xFF

config_version: uint8_t = 0x05

flags: FlagsType

debounce_no_delay_milliseconds: uint16_t = 0
debounce_delay_milliseconds: uint16_t = 0
panel_debounce_microseconds: uint16_t = 4000
auto_calibration_max_deviation: uint8_t = 100
bad_sensor_minimum_delay_seconds: uint8_t = 15
auto_calibration_averages_per_update: uint16_t = 60
auto_calibration_samples_per_average: uint16_t = 500

auto_calibration_max_tare: uint16_t = 0xFFFF

enabled_sensors: EnabledSensors

auto_lights_timeout: uint8_t = 1000 // 128

step_color: Annotated[list[RGBType], TypeMeta(size=9)]

platform_strip_color: RGBType

auto_light_panel_mask: PanelMaskType

panel_rotation: uint8_t = 0x00

packed_panel_settings: Annotated[list[PackedPanelSettingsType], TypeMeta(size=9)]

pre_details_delay_milliseconds: uint8_t = 0x05

padding: Annotated[list[uint8_t], TypeMeta(size=49)]
Empty file added test/py.typed
Empty file.
Loading

0 comments on commit 7275e54

Please sign in to comment.