conduyt-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. conduyt_py-0.1.0/PKG-INFO +143 -0
  2. conduyt_py-0.1.0/README.md +124 -0
  3. conduyt_py-0.1.0/pyproject.toml +38 -0
  4. conduyt_py-0.1.0/setup.cfg +4 -0
  5. conduyt_py-0.1.0/src/conduyt/__init__.py +18 -0
  6. conduyt_py-0.1.0/src/conduyt/core/__init__.py +0 -0
  7. conduyt_py-0.1.0/src/conduyt/core/cobs.py +60 -0
  8. conduyt_py-0.1.0/src/conduyt/core/constants.py +119 -0
  9. conduyt_py-0.1.0/src/conduyt/core/crc8.py +44 -0
  10. conduyt_py-0.1.0/src/conduyt/core/errors.py +30 -0
  11. conduyt_py-0.1.0/src/conduyt/core/wire.py +78 -0
  12. conduyt_py-0.1.0/src/conduyt/device.py +141 -0
  13. conduyt_py-0.1.0/src/conduyt/hello.py +125 -0
  14. conduyt_py-0.1.0/src/conduyt/modules/__init__.py +18 -0
  15. conduyt_py-0.1.0/src/conduyt/modules/dht.py +36 -0
  16. conduyt_py-0.1.0/src/conduyt/modules/encoder.py +32 -0
  17. conduyt_py-0.1.0/src/conduyt/modules/neopixel.py +48 -0
  18. conduyt_py-0.1.0/src/conduyt/modules/oled.py +41 -0
  19. conduyt_py-0.1.0/src/conduyt/modules/pid.py +41 -0
  20. conduyt_py-0.1.0/src/conduyt/modules/servo.py +35 -0
  21. conduyt_py-0.1.0/src/conduyt/modules/stepper.py +34 -0
  22. conduyt_py-0.1.0/src/conduyt/sync.py +54 -0
  23. conduyt_py-0.1.0/src/conduyt/transports/__init__.py +7 -0
  24. conduyt_py-0.1.0/src/conduyt/transports/mock.py +46 -0
  25. conduyt_py-0.1.0/src/conduyt/transports/mqtt.py +78 -0
  26. conduyt_py-0.1.0/src/conduyt/transports/serial.py +75 -0
  27. conduyt_py-0.1.0/src/conduyt_py.egg-info/PKG-INFO +143 -0
  28. conduyt_py-0.1.0/src/conduyt_py.egg-info/SOURCES.txt +36 -0
  29. conduyt_py-0.1.0/src/conduyt_py.egg-info/dependency_links.txt +1 -0
  30. conduyt_py-0.1.0/src/conduyt_py.egg-info/requires.txt +10 -0
  31. conduyt_py-0.1.0/src/conduyt_py.egg-info/top_level.txt +1 -0
  32. conduyt_py-0.1.0/tests/test_cobs.py +58 -0
  33. conduyt_py-0.1.0/tests/test_conformance.py +39 -0
  34. conduyt_py-0.1.0/tests/test_crc8.py +32 -0
  35. conduyt_py-0.1.0/tests/test_device.py +353 -0
  36. conduyt_py-0.1.0/tests/test_hello.py +264 -0
  37. conduyt_py-0.1.0/tests/test_modules.py +302 -0
  38. conduyt_py-0.1.0/tests/test_wire.py +72 -0
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: conduyt-py
3
+ Version: 0.1.0
4
+ Summary: CONDUYT protocol SDK for Python — host-side hardware control
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/virgilvox/conduyt
7
+ Project-URL: Repository, https://github.com/virgilvox/conduyt
8
+ Project-URL: Issues, https://github.com/virgilvox/conduyt/issues
9
+ Keywords: conduyt,iot,hardware,serial,mqtt,microcontroller,binary-protocol
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: serial
13
+ Requires-Dist: pyserial-asyncio>=0.6; extra == "serial"
14
+ Provides-Extra: mqtt
15
+ Requires-Dist: aiomqtt>=2.0; extra == "mqtt"
16
+ Provides-Extra: all
17
+ Requires-Dist: pyserial-asyncio>=0.6; extra == "all"
18
+ Requires-Dist: aiomqtt>=2.0; extra == "all"
19
+
20
+ # conduyt-py
21
+
22
+ CONDUYT protocol SDK for Python. Async and sync APIs for host-side hardware control.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install conduyt-py
28
+
29
+ # With serial transport
30
+ pip install conduyt-py[serial]
31
+
32
+ # With MQTT transport
33
+ pip install conduyt-py[mqtt]
34
+
35
+ # All transports
36
+ pip install conduyt-py[all]
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### Async
42
+
43
+ ```python
44
+ from conduyt import ConduytDevice
45
+ from conduyt.transports.serial import SerialTransport
46
+
47
+ device = ConduytDevice(SerialTransport("/dev/ttyUSB0"))
48
+ hello = await device.connect()
49
+
50
+ await device.pin(13).mode("output")
51
+ await device.pin(13).write(1)
52
+ value = await device.pin(0).read("analog")
53
+
54
+ await device.disconnect()
55
+ ```
56
+
57
+ ### Sync
58
+
59
+ ```python
60
+ from conduyt import ConduytDeviceSync
61
+ from conduyt.transports.serial import SerialTransport
62
+
63
+ device = ConduytDeviceSync(SerialTransport("/dev/ttyUSB0"))
64
+ device.connect()
65
+
66
+ device.pin(13).mode("output")
67
+ device.pin(13).write(1)
68
+ value = device.pin(0).read("analog")
69
+
70
+ device.disconnect()
71
+ ```
72
+
73
+ ### Module Usage
74
+
75
+ ```python
76
+ from conduyt.modules.servo import ConduytServo
77
+
78
+ servo = ConduytServo(device)
79
+ await servo.attach(9)
80
+ await servo.write(90)
81
+ ```
82
+
83
+ ## API Reference
84
+
85
+ ### ConduytDevice (async)
86
+
87
+ | Method | Returns | Description |
88
+ |--------|---------|-------------|
89
+ | `connect()` | `HelloResp` | Connect and run HELLO handshake |
90
+ | `disconnect()` | `None` | Close connection |
91
+ | `ping()` | `None` | Ping/pong roundtrip |
92
+ | `reset()` | `None` | Reset device state |
93
+ | `pin(num)` | `PinProxy` | Pin control object |
94
+
95
+ ### ConduytDeviceSync
96
+
97
+ Synchronous wrapper around `ConduytDevice`. Same methods, no `await`.
98
+
99
+ ### PinProxy
100
+
101
+ | Method | Returns | Description |
102
+ |--------|---------|-------------|
103
+ | `mode(mode)` | `None` | Set pin mode: `"input"`, `"output"`, `"pwm"`, `"analog"`, `"input_pullup"` |
104
+ | `write(value)` | `None` | Digital or PWM write |
105
+ | `read(mode?)` | `int` | Read pin value |
106
+
107
+ ### Errors
108
+
109
+ | Class | When |
110
+ |-------|------|
111
+ | `ConduytNAKError` | Device rejected a command |
112
+ | `ConduytTimeoutError` | No response within timeout |
113
+ | `ConduytDisconnectedError` | Transport disconnected |
114
+
115
+ ## Transports
116
+
117
+ | Transport | Module | Dependency |
118
+ |-----------|--------|------------|
119
+ | `SerialTransport` | `conduyt.transports.serial` | `pyserial-asyncio` |
120
+ | `MQTTTransport` | `conduyt.transports.mqtt` | `aiomqtt` |
121
+ | `MockTransport` | `conduyt.transports.mock` | None |
122
+
123
+ ## Modules
124
+
125
+ | Module | Import | Hardware |
126
+ |--------|--------|----------|
127
+ | `ConduytServo` | `conduyt.modules.servo` | Hobby servos |
128
+ | `ConduytNeoPixel` | `conduyt.modules.neopixel` | WS2812/SK6812 LEDs |
129
+ | `ConduytDHT` | `conduyt.modules.dht` | DHT11/DHT22 sensors |
130
+ | `ConduytOLED` | `conduyt.modules.oled` | SSD1306 OLED displays |
131
+ | `ConduytStepper` | `conduyt.modules.stepper` | Stepper motors |
132
+ | `ConduytEncoder` | `conduyt.modules.encoder` | Rotary encoders |
133
+ | `ConduytPID` | `conduyt.modules.pid` | PID controller |
134
+
135
+ ## Requirements
136
+
137
+ - Python >= 3.10
138
+ - `pyserial-asyncio >= 0.6` (for serial transport)
139
+ - `aiomqtt >= 2.0` (for MQTT transport)
140
+
141
+ ## License
142
+
143
+ MIT. Copyright (c) 2026 LumenCanvas.
@@ -0,0 +1,124 @@
1
+ # conduyt-py
2
+
3
+ CONDUYT protocol SDK for Python. Async and sync APIs for host-side hardware control.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install conduyt-py
9
+
10
+ # With serial transport
11
+ pip install conduyt-py[serial]
12
+
13
+ # With MQTT transport
14
+ pip install conduyt-py[mqtt]
15
+
16
+ # All transports
17
+ pip install conduyt-py[all]
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Async
23
+
24
+ ```python
25
+ from conduyt import ConduytDevice
26
+ from conduyt.transports.serial import SerialTransport
27
+
28
+ device = ConduytDevice(SerialTransport("/dev/ttyUSB0"))
29
+ hello = await device.connect()
30
+
31
+ await device.pin(13).mode("output")
32
+ await device.pin(13).write(1)
33
+ value = await device.pin(0).read("analog")
34
+
35
+ await device.disconnect()
36
+ ```
37
+
38
+ ### Sync
39
+
40
+ ```python
41
+ from conduyt import ConduytDeviceSync
42
+ from conduyt.transports.serial import SerialTransport
43
+
44
+ device = ConduytDeviceSync(SerialTransport("/dev/ttyUSB0"))
45
+ device.connect()
46
+
47
+ device.pin(13).mode("output")
48
+ device.pin(13).write(1)
49
+ value = device.pin(0).read("analog")
50
+
51
+ device.disconnect()
52
+ ```
53
+
54
+ ### Module Usage
55
+
56
+ ```python
57
+ from conduyt.modules.servo import ConduytServo
58
+
59
+ servo = ConduytServo(device)
60
+ await servo.attach(9)
61
+ await servo.write(90)
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### ConduytDevice (async)
67
+
68
+ | Method | Returns | Description |
69
+ |--------|---------|-------------|
70
+ | `connect()` | `HelloResp` | Connect and run HELLO handshake |
71
+ | `disconnect()` | `None` | Close connection |
72
+ | `ping()` | `None` | Ping/pong roundtrip |
73
+ | `reset()` | `None` | Reset device state |
74
+ | `pin(num)` | `PinProxy` | Pin control object |
75
+
76
+ ### ConduytDeviceSync
77
+
78
+ Synchronous wrapper around `ConduytDevice`. Same methods, no `await`.
79
+
80
+ ### PinProxy
81
+
82
+ | Method | Returns | Description |
83
+ |--------|---------|-------------|
84
+ | `mode(mode)` | `None` | Set pin mode: `"input"`, `"output"`, `"pwm"`, `"analog"`, `"input_pullup"` |
85
+ | `write(value)` | `None` | Digital or PWM write |
86
+ | `read(mode?)` | `int` | Read pin value |
87
+
88
+ ### Errors
89
+
90
+ | Class | When |
91
+ |-------|------|
92
+ | `ConduytNAKError` | Device rejected a command |
93
+ | `ConduytTimeoutError` | No response within timeout |
94
+ | `ConduytDisconnectedError` | Transport disconnected |
95
+
96
+ ## Transports
97
+
98
+ | Transport | Module | Dependency |
99
+ |-----------|--------|------------|
100
+ | `SerialTransport` | `conduyt.transports.serial` | `pyserial-asyncio` |
101
+ | `MQTTTransport` | `conduyt.transports.mqtt` | `aiomqtt` |
102
+ | `MockTransport` | `conduyt.transports.mock` | None |
103
+
104
+ ## Modules
105
+
106
+ | Module | Import | Hardware |
107
+ |--------|--------|----------|
108
+ | `ConduytServo` | `conduyt.modules.servo` | Hobby servos |
109
+ | `ConduytNeoPixel` | `conduyt.modules.neopixel` | WS2812/SK6812 LEDs |
110
+ | `ConduytDHT` | `conduyt.modules.dht` | DHT11/DHT22 sensors |
111
+ | `ConduytOLED` | `conduyt.modules.oled` | SSD1306 OLED displays |
112
+ | `ConduytStepper` | `conduyt.modules.stepper` | Stepper motors |
113
+ | `ConduytEncoder` | `conduyt.modules.encoder` | Rotary encoders |
114
+ | `ConduytPID` | `conduyt.modules.pid` | PID controller |
115
+
116
+ ## Requirements
117
+
118
+ - Python >= 3.10
119
+ - `pyserial-asyncio >= 0.6` (for serial transport)
120
+ - `aiomqtt >= 2.0` (for MQTT transport)
121
+
122
+ ## License
123
+
124
+ MIT. Copyright (c) 2026 LumenCanvas.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "conduyt-py"
3
+ version = "0.1.0"
4
+ description = "CONDUYT protocol SDK for Python — host-side hardware control"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+ keywords = ["conduyt", "iot", "hardware", "serial", "mqtt", "microcontroller", "binary-protocol"]
10
+
11
+ [project.urls]
12
+ Homepage = "https://github.com/virgilvox/conduyt"
13
+ Repository = "https://github.com/virgilvox/conduyt"
14
+ Issues = "https://github.com/virgilvox/conduyt/issues"
15
+
16
+ [project.optional-dependencies]
17
+ serial = ["pyserial-asyncio>=0.6"]
18
+ mqtt = ["aiomqtt>=2.0"]
19
+ all = ["pyserial-asyncio>=0.6", "aiomqtt>=2.0"]
20
+
21
+ [build-system]
22
+ requires = ["setuptools>=68"]
23
+ build-backend = "setuptools.build_meta"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.mypy]
29
+ strict = true
30
+ python_version = "3.10"
31
+
32
+ [tool.ruff]
33
+ target-version = "py310"
34
+ line-length = 100
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ """
2
+ conduyt-py — CONDUYT Protocol SDK for Python
3
+
4
+ Reference host-side SDK for the CONDUYT protocol.
5
+ Transport-agnostic, capability-first hardware control.
6
+ """
7
+
8
+ from conduyt.device import ConduytDevice
9
+ from conduyt.sync import ConduytDeviceSync
10
+ from conduyt.core.errors import ConduytNAKError, ConduytTimeoutError, ConduytDisconnectedError
11
+
12
+ __all__ = [
13
+ "ConduytDevice",
14
+ "ConduytDeviceSync",
15
+ "ConduytNAKError",
16
+ "ConduytTimeoutError",
17
+ "ConduytDisconnectedError",
18
+ ]
File without changes
@@ -0,0 +1,60 @@
1
+ """COBS — Consistent Overhead Byte Stuffing"""
2
+
3
+
4
+ def cobs_encode(src: bytes | bytearray) -> bytes:
5
+ """Encode data using COBS. Output never contains 0x00."""
6
+ if len(src) == 0:
7
+ return bytes([0x01])
8
+
9
+ dst = bytearray()
10
+ code_idx = len(dst)
11
+ dst.append(0) # placeholder for first code byte
12
+ code = 1
13
+
14
+ for b in src:
15
+ if b == 0x00:
16
+ dst[code_idx] = code
17
+ code_idx = len(dst)
18
+ dst.append(0) # placeholder
19
+ code = 1
20
+ else:
21
+ dst.append(b)
22
+ code += 1
23
+ if code == 0xFF:
24
+ dst[code_idx] = code
25
+ code_idx = len(dst)
26
+ dst.append(0)
27
+ code = 1
28
+
29
+ dst[code_idx] = code
30
+ return bytes(dst)
31
+
32
+
33
+ def cobs_decode(src: bytes | bytearray) -> bytes | None:
34
+ """Decode COBS-encoded data. Returns None on decode error."""
35
+ if len(src) == 0:
36
+ return None
37
+
38
+ dst = bytearray()
39
+ idx = 0
40
+
41
+ while idx < len(src):
42
+ code = src[idx]
43
+ idx += 1
44
+
45
+ if code == 0x00:
46
+ return None # unexpected zero
47
+
48
+ count = code - 1
49
+
50
+ if idx + count > len(src):
51
+ return None
52
+
53
+ for _ in range(count):
54
+ dst.append(src[idx])
55
+ idx += 1
56
+
57
+ if code < 0xFF and idx < len(src):
58
+ dst.append(0x00)
59
+
60
+ return bytes(dst)
@@ -0,0 +1,119 @@
1
+ """
2
+ CONDUYT Protocol Constants
3
+ Generated from protocol/constants.json — DO NOT EDIT
4
+ """
5
+
6
+ PROTOCOL_VERSION = 0x01
7
+ MAGIC = bytes([0x43, 0x44]) # "CD"
8
+ HEADER_SIZE = 8 # MAGIC(2) + VER(1) + TYPE(1) + SEQ(1) + LEN(2) + CRC(1)
9
+
10
+ # Host -> Device Commands
11
+ class CMD:
12
+ PING = 0x01
13
+ HELLO = 0x02
14
+ PIN_MODE = 0x10
15
+ PIN_WRITE = 0x11
16
+ PIN_READ = 0x12
17
+ PIN_SUBSCRIBE = 0x13
18
+ PIN_UNSUBSCRIBE = 0x14
19
+ I2C_WRITE = 0x20
20
+ I2C_READ = 0x21
21
+ I2C_READ_REG = 0x22
22
+ SPI_XFER = 0x30
23
+ MOD_CMD = 0x40
24
+ STREAM_START = 0x50
25
+ STREAM_STOP = 0x51
26
+ DS_WRITE = 0x60
27
+ DS_READ = 0x61
28
+ DS_SUBSCRIBE = 0x62
29
+ OTA_BEGIN = 0x70
30
+ OTA_CHUNK = 0x71
31
+ OTA_FINALIZE = 0x72
32
+ RESET = 0xF0
33
+
34
+ # Device -> Host Events
35
+ class EVT:
36
+ PONG = 0x80
37
+ HELLO_RESP = 0x81
38
+ ACK = 0x82
39
+ NAK = 0x83
40
+ PIN_EVENT = 0x90
41
+ PIN_READ_RESP = 0x91
42
+ I2C_READ_RESP = 0xA0
43
+ SPI_XFER_RESP = 0xB0
44
+ MOD_EVENT = 0xC0
45
+ MOD_RESP = 0xC1
46
+ STREAM_DATA = 0xD0
47
+ DS_EVENT = 0xD1
48
+ DS_READ_RESP = 0xD2
49
+ LOG = 0xE0
50
+ FATAL = 0xFF
51
+
52
+ # NAK Error Codes
53
+ class ERR:
54
+ UNKNOWN_TYPE = 0x01
55
+ CRC_MISMATCH = 0x02
56
+ PAYLOAD_TOO_LARGE = 0x03
57
+ INVALID_PIN = 0x04
58
+ PIN_MODE_UNSUPPORTED = 0x05
59
+ I2C_NOT_AVAILABLE = 0x06
60
+ I2C_NACK = 0x07
61
+ MODULE_NOT_LOADED = 0x08
62
+ UNKNOWN_MODULE_CMD = 0x09
63
+ MODULE_BUSY = 0x0A
64
+ SUB_LIMIT_REACHED = 0x0B
65
+ OUT_OF_MEMORY = 0x0C
66
+ UNKNOWN_DATASTREAM = 0x0D
67
+ DATASTREAM_READONLY = 0x0E
68
+ OTA_INVALID = 0x0F
69
+ VERSION_MISMATCH = 0x10
70
+
71
+ ERR_NAMES: dict[int, str] = {
72
+ ERR.UNKNOWN_TYPE: "UNKNOWN_TYPE",
73
+ ERR.CRC_MISMATCH: "CRC_MISMATCH",
74
+ ERR.PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
75
+ ERR.INVALID_PIN: "INVALID_PIN",
76
+ ERR.PIN_MODE_UNSUPPORTED: "PIN_MODE_UNSUPPORTED",
77
+ ERR.I2C_NOT_AVAILABLE: "I2C_NOT_AVAILABLE",
78
+ ERR.I2C_NACK: "I2C_NACK",
79
+ ERR.MODULE_NOT_LOADED: "MODULE_NOT_LOADED",
80
+ ERR.UNKNOWN_MODULE_CMD: "UNKNOWN_MODULE_CMD",
81
+ ERR.MODULE_BUSY: "MODULE_BUSY",
82
+ ERR.SUB_LIMIT_REACHED: "SUB_LIMIT_REACHED",
83
+ ERR.OUT_OF_MEMORY: "OUT_OF_MEMORY",
84
+ ERR.UNKNOWN_DATASTREAM: "UNKNOWN_DATASTREAM",
85
+ ERR.DATASTREAM_READONLY: "DATASTREAM_READONLY",
86
+ ERR.OTA_INVALID: "OTA_INVALID",
87
+ ERR.VERSION_MISMATCH: "VERSION_MISMATCH",
88
+ }
89
+
90
+ # Datastream Type Codes
91
+ class DS_TYPE:
92
+ BOOL = 0x01
93
+ INT8 = 0x02
94
+ UINT8 = 0x03
95
+ INT16 = 0x04
96
+ UINT16 = 0x05
97
+ INT32 = 0x06
98
+ FLOAT32 = 0x07
99
+ STRING = 0x08
100
+ BYTES = 0x09
101
+
102
+ # Pin Capability Bitmask
103
+ class PIN_CAP:
104
+ DIGITAL_IN = 1 << 0
105
+ DIGITAL_OUT = 1 << 1
106
+ PWM_OUT = 1 << 2
107
+ ANALOG_IN = 1 << 3
108
+ I2C_SDA = 1 << 4
109
+ I2C_SCL = 1 << 5
110
+ SPI = 1 << 6
111
+ INTERRUPT = 1 << 7
112
+
113
+ # Pin Modes
114
+ class PIN_MODE:
115
+ INPUT = 0x00
116
+ OUTPUT = 0x01
117
+ PWM = 0x02
118
+ ANALOG = 0x03
119
+ INPUT_PULLUP = 0x04
@@ -0,0 +1,44 @@
1
+ """CRC8 Dallas/Maxim (polynomial 0x31, init 0x00)"""
2
+
3
+ _TABLE = bytes([
4
+ 0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97,
5
+ 0xB9, 0x88, 0xDB, 0xEA, 0x7D, 0x4C, 0x1F, 0x2E,
6
+ 0x43, 0x72, 0x21, 0x10, 0x87, 0xB6, 0xE5, 0xD4,
7
+ 0xFA, 0xCB, 0x98, 0xA9, 0x3E, 0x0F, 0x5C, 0x6D,
8
+ 0x86, 0xB7, 0xE4, 0xD5, 0x42, 0x73, 0x20, 0x11,
9
+ 0x3F, 0x0E, 0x5D, 0x6C, 0xFB, 0xCA, 0x99, 0xA8,
10
+ 0xC5, 0xF4, 0xA7, 0x96, 0x01, 0x30, 0x63, 0x52,
11
+ 0x7C, 0x4D, 0x1E, 0x2F, 0xB8, 0x89, 0xDA, 0xEB,
12
+ 0x3D, 0x0C, 0x5F, 0x6E, 0xF9, 0xC8, 0x9B, 0xAA,
13
+ 0x84, 0xB5, 0xE6, 0xD7, 0x40, 0x71, 0x22, 0x13,
14
+ 0x7E, 0x4F, 0x1C, 0x2D, 0xBA, 0x8B, 0xD8, 0xE9,
15
+ 0xC7, 0xF6, 0xA5, 0x94, 0x03, 0x32, 0x61, 0x50,
16
+ 0xBB, 0x8A, 0xD9, 0xE8, 0x7F, 0x4E, 0x1D, 0x2C,
17
+ 0x02, 0x33, 0x60, 0x51, 0xC6, 0xF7, 0xA4, 0x95,
18
+ 0xF8, 0xC9, 0x9A, 0xAB, 0x3C, 0x0D, 0x5E, 0x6F,
19
+ 0x41, 0x70, 0x23, 0x12, 0x85, 0xB4, 0xE7, 0xD6,
20
+ 0x7A, 0x4B, 0x18, 0x29, 0xBE, 0x8F, 0xDC, 0xED,
21
+ 0xC3, 0xF2, 0xA1, 0x90, 0x07, 0x36, 0x65, 0x54,
22
+ 0x39, 0x08, 0x5B, 0x6A, 0xFD, 0xCC, 0x9F, 0xAE,
23
+ 0x80, 0xB1, 0xE2, 0xD3, 0x44, 0x75, 0x26, 0x17,
24
+ 0xFC, 0xCD, 0x9E, 0xAF, 0x38, 0x09, 0x5A, 0x6B,
25
+ 0x45, 0x74, 0x27, 0x16, 0x81, 0xB0, 0xE3, 0xD2,
26
+ 0xBF, 0x8E, 0xDD, 0xEC, 0x7B, 0x4A, 0x19, 0x28,
27
+ 0x06, 0x37, 0x64, 0x55, 0xC2, 0xF3, 0xA0, 0x91,
28
+ 0x47, 0x76, 0x25, 0x14, 0x83, 0xB2, 0xE1, 0xD0,
29
+ 0xFE, 0xCF, 0x9C, 0xAD, 0x3A, 0x0B, 0x58, 0x69,
30
+ 0x04, 0x35, 0x66, 0x57, 0xC0, 0xF1, 0xA2, 0x93,
31
+ 0xBD, 0x8C, 0xDF, 0xEE, 0x79, 0x48, 0x1B, 0x2A,
32
+ 0xE1, 0xD0, 0x83, 0xB2, 0x25, 0x14, 0x47, 0x76,
33
+ 0x58, 0x69, 0x3A, 0x0B, 0x9C, 0xAD, 0xFE, 0xCF,
34
+ 0xA2, 0x93, 0xC0, 0xF1, 0x66, 0x57, 0x04, 0x35,
35
+ 0x1B, 0x2A, 0x79, 0x48, 0xDF, 0xEE, 0xBD, 0x8C,
36
+ ])
37
+
38
+
39
+ def crc8(data: bytes | bytearray) -> int:
40
+ """Compute CRC8 Dallas/Maxim over a byte sequence."""
41
+ crc = 0x00
42
+ for b in data:
43
+ crc = _TABLE[crc ^ b]
44
+ return crc
@@ -0,0 +1,30 @@
1
+ """CONDUYT Error Types"""
2
+
3
+ from .constants import ERR_NAMES
4
+
5
+
6
+ class ConduytNAKError(Exception):
7
+ """Raised when the device replies with a NAK packet."""
8
+
9
+ def __init__(self, code: int, seq: int) -> None:
10
+ name = ERR_NAMES.get(code, f"UNKNOWN_0x{code:02x}")
11
+ super().__init__(f"NAK: {name} (code=0x{code:02x}, seq={seq})")
12
+ self.code = code
13
+ self.error_name = name
14
+ self.seq = seq
15
+
16
+
17
+ class ConduytTimeoutError(Exception):
18
+ """Raised when a command does not receive a response in time."""
19
+
20
+ def __init__(self, seq: int, timeout_ms: int) -> None:
21
+ super().__init__(f"Timeout: no response for seq={seq} after {timeout_ms}ms")
22
+ self.seq = seq
23
+ self.timeout_ms = timeout_ms
24
+
25
+
26
+ class ConduytDisconnectedError(Exception):
27
+ """Raised when an operation is attempted on a disconnected device."""
28
+
29
+ def __init__(self, message: str = "Device is not connected") -> None:
30
+ super().__init__(message)
@@ -0,0 +1,78 @@
1
+ """CONDUYT Wire Format — Packet encode/decode"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from dataclasses import dataclass
7
+
8
+ from .constants import PROTOCOL_VERSION, MAGIC, HEADER_SIZE
9
+ from .crc8 import crc8
10
+
11
+
12
+ @dataclass
13
+ class ConduytPacket:
14
+ version: int
15
+ type: int
16
+ seq: int
17
+ payload: bytes
18
+
19
+
20
+ def wire_encode(packet: ConduytPacket) -> bytes:
21
+ """Encode a ConduytPacket into raw wire bytes."""
22
+ payload_len = len(packet.payload)
23
+ buf = bytearray(HEADER_SIZE + payload_len)
24
+
25
+ # MAGIC
26
+ buf[0] = MAGIC[0]
27
+ buf[1] = MAGIC[1]
28
+ # VER
29
+ buf[2] = packet.version
30
+ # TYPE
31
+ buf[3] = packet.type
32
+ # SEQ
33
+ buf[4] = packet.seq
34
+ # LEN (little-endian uint16)
35
+ struct.pack_into("<H", buf, 5, payload_len)
36
+ # PAYLOAD
37
+ buf[7 : 7 + payload_len] = packet.payload
38
+ # CRC8 over [VER..end of PAYLOAD]
39
+ crc_region = buf[2 : 7 + payload_len]
40
+ buf[7 + payload_len] = crc8(crc_region)
41
+
42
+ return bytes(buf)
43
+
44
+
45
+ def wire_decode(data: bytes | bytearray) -> ConduytPacket:
46
+ """Decode raw wire bytes into a ConduytPacket. Raises ValueError on error."""
47
+ if len(data) < HEADER_SIZE:
48
+ raise ValueError(f"Incomplete packet: need {HEADER_SIZE} bytes, got {len(data)}")
49
+
50
+ if data[0] != MAGIC[0] or data[1] != MAGIC[1]:
51
+ raise ValueError(f"Invalid magic: 0x{data[0]:02x}{data[1]:02x}")
52
+
53
+ version = data[2]
54
+ if version != PROTOCOL_VERSION:
55
+ raise ValueError(f"Version mismatch: expected {PROTOCOL_VERSION}, got {version}")
56
+
57
+ pkt_type = data[3]
58
+ seq = data[4]
59
+ payload_len = struct.unpack_from("<H", data, 5)[0]
60
+
61
+ total = HEADER_SIZE + payload_len
62
+ if len(data) < total:
63
+ raise ValueError(f"Incomplete packet: need {total} bytes, got {len(data)}")
64
+
65
+ crc_region = data[2 : 7 + payload_len]
66
+ expected_crc = crc8(crc_region)
67
+ actual_crc = data[7 + payload_len]
68
+
69
+ if expected_crc != actual_crc:
70
+ raise ValueError(f"CRC mismatch: expected 0x{expected_crc:02x}, got 0x{actual_crc:02x}")
71
+
72
+ payload = bytes(data[7 : 7 + payload_len])
73
+ return ConduytPacket(version=version, type=pkt_type, seq=seq, payload=payload)
74
+
75
+
76
+ def make_packet(pkt_type: int, seq: int, payload: bytes = b"") -> ConduytPacket:
77
+ """Create a ConduytPacket ready for encoding."""
78
+ return ConduytPacket(version=PROTOCOL_VERSION, type=pkt_type, seq=seq, payload=payload)