sds-library 0.0.1a6__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.
- sds_library-0.0.1a6/MANIFEST.in +11 -0
- sds_library-0.0.1a6/PKG-INFO +559 -0
- sds_library-0.0.1a6/README.md +526 -0
- sds_library-0.0.1a6/pyproject.toml +88 -0
- sds_library-0.0.1a6/sds/__init__.py +152 -0
- sds_library-0.0.1a6/sds/_bindings.py +99 -0
- sds_library-0.0.1a6/sds/_build_ffi.py +144 -0
- sds_library-0.0.1a6/sds/_cdefs.h +330 -0
- sds_library-0.0.1a6/sds/_logging.py +78 -0
- sds_library-0.0.1a6/sds/_types.py +209 -0
- sds_library-0.0.1a6/sds/node.py +1402 -0
- sds_library-0.0.1a6/sds/table.py +735 -0
- sds_library-0.0.1a6/sds/tables.py +395 -0
- sds_library-0.0.1a6/sds_library.egg-info/PKG-INFO +559 -0
- sds_library-0.0.1a6/sds_library.egg-info/SOURCES.txt +43 -0
- sds_library-0.0.1a6/sds_library.egg-info/dependency_links.txt +1 -0
- sds_library-0.0.1a6/sds_library.egg-info/requires.txt +8 -0
- sds_library-0.0.1a6/sds_library.egg-info/top_level.txt +1 -0
- sds_library-0.0.1a6/setup.cfg +4 -0
- sds_library-0.0.1a6/setup.py +9 -0
- sds_library-0.0.1a6/tests/test_concurrent.py +165 -0
- sds_library-0.0.1a6/tests/test_eviction.py +227 -0
- sds_library-0.0.1a6/tests/test_imports.py +94 -0
- sds_library-0.0.1a6/tests/test_node.py +649 -0
- sds_library-0.0.1a6/tests/test_signed_integers.py +182 -0
- sds_library-0.0.1a6/tests/test_table.py +448 -0
- sds_library-0.0.1a6/tests/test_types.py +226 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Include C source files for building from source
|
|
2
|
+
recursive-include ../src *.c
|
|
3
|
+
recursive-include ../include *.h
|
|
4
|
+
recursive-include ../platform *.c *.cpp
|
|
5
|
+
|
|
6
|
+
# Include the CFFI definitions
|
|
7
|
+
include sds/_cdefs.h
|
|
8
|
+
|
|
9
|
+
# Include documentation
|
|
10
|
+
include README.md
|
|
11
|
+
include ../LICENSE
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sds-library
|
|
3
|
+
Version: 0.0.1a6
|
|
4
|
+
Summary: Python bindings for the SDS (Synchronized Data Structures) library
|
|
5
|
+
Author: SDS Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/EdgeKVM-Inc/sds-library
|
|
8
|
+
Project-URL: Documentation, https://github.com/EdgeKVM-Inc/sds-library#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/EdgeKVM-Inc/sds-library
|
|
10
|
+
Keywords: iot,mqtt,embedded,synchronization,dds
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: cffi>=1.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy; extra == "dev"
|
|
31
|
+
Requires-Dist: black; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
|
|
34
|
+
# SDS Library - Python Bindings
|
|
35
|
+
|
|
36
|
+
Python bindings for the SDS (Synchronized Data Structures) library, providing
|
|
37
|
+
a Pythonic interface for IoT state synchronization over MQTT.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install sds-library
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For development installation:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd python
|
|
49
|
+
pip install -e ".[dev]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
### Generate Types from Schema
|
|
55
|
+
|
|
56
|
+
The code generator creates Python types from your `.sds` schema file:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Generate Python types (and optionally C types)
|
|
60
|
+
python tools/sds_codegen.py schema.sds --python -o python/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This creates `sds_types.py` with dataclasses matching your C structs.
|
|
64
|
+
|
|
65
|
+
### Device Node (Using Generated Types)
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from sds import SdsNode, Role
|
|
69
|
+
from sds_types import SensorData # Generated from schema.sds
|
|
70
|
+
|
|
71
|
+
with SdsNode("py_sensor_01", "localhost") as node:
|
|
72
|
+
# Register with schema bundle - no manual dataclass needed!
|
|
73
|
+
table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
|
|
74
|
+
|
|
75
|
+
@node.on_config("SensorData")
|
|
76
|
+
def handle_config(table_type):
|
|
77
|
+
print(f"Config: threshold={table.config.threshold}")
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
# C-like attribute access
|
|
81
|
+
table.state.temperature = 23.5
|
|
82
|
+
table.state.humidity = 65.0
|
|
83
|
+
table.status.error_code = 0
|
|
84
|
+
|
|
85
|
+
node.poll(timeout_ms=1000)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Device Node (Manual Schema Definition)
|
|
89
|
+
|
|
90
|
+
If you prefer not to use the code generator:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from dataclasses import dataclass
|
|
94
|
+
from sds import SdsNode, Role, Field
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class SensorState:
|
|
98
|
+
temperature: float = Field(float32=True)
|
|
99
|
+
humidity: float = Field(float32=True)
|
|
100
|
+
|
|
101
|
+
with SdsNode("py_sensor_01", "localhost") as node:
|
|
102
|
+
table = node.register_table(
|
|
103
|
+
"SensorData", Role.DEVICE,
|
|
104
|
+
state_schema=SensorState,
|
|
105
|
+
)
|
|
106
|
+
table.state.temperature = 23.5
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Owner Node (C-like Syntax)
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from sds import SdsNode, Role
|
|
113
|
+
|
|
114
|
+
with SdsNode("py_owner_01", "localhost") as node:
|
|
115
|
+
table = node.register_table(
|
|
116
|
+
"SensorData",
|
|
117
|
+
Role.OWNER,
|
|
118
|
+
status_schema=SensorStatus, # For reading device status
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Set config
|
|
122
|
+
table.config.threshold = 25.0
|
|
123
|
+
|
|
124
|
+
@node.on_status("SensorData")
|
|
125
|
+
def handle_status(table_type, from_node):
|
|
126
|
+
device = table.get_device(from_node)
|
|
127
|
+
if device and device.online:
|
|
128
|
+
print(f"{from_node}: battery={device.status.battery_percent}%")
|
|
129
|
+
|
|
130
|
+
while True:
|
|
131
|
+
node.poll(timeout_ms=1000)
|
|
132
|
+
|
|
133
|
+
# Iterate all devices
|
|
134
|
+
for node_id, device in table.iter_devices():
|
|
135
|
+
print(f"{node_id}: online={device.online}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Features
|
|
139
|
+
|
|
140
|
+
- **C-like Syntax**: Access table data directly (`table.state.temperature = 23.5`)
|
|
141
|
+
- **Zero Protocol Drift**: Python uses the exact same C implementation
|
|
142
|
+
- **Pythonic API**: Context managers, decorators, and keyword arguments
|
|
143
|
+
- **Thread-Safe**: All operations protected by locks for multi-threaded use
|
|
144
|
+
- **Cross-Platform**: Linux (x86_64, ARM64) and macOS
|
|
145
|
+
- **Type Hints**: Full type annotations for IDE support
|
|
146
|
+
- **Device Eviction**: Automatic cleanup of offline device slots
|
|
147
|
+
|
|
148
|
+
## Device Eviction
|
|
149
|
+
|
|
150
|
+
When devices disconnect unexpectedly, SDS receives MQTT LWT (Last Will and Testament)
|
|
151
|
+
messages and can automatically evict them from status slots after a grace period.
|
|
152
|
+
This prevents slots from being permanently consumed by devices that never reconnect.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Enable eviction with 60-second grace period
|
|
156
|
+
with SdsNode("owner", "localhost", eviction_grace_ms=60000) as node:
|
|
157
|
+
table = node.register_table("SensorData", Role.OWNER, schema=SensorData)
|
|
158
|
+
|
|
159
|
+
@node.on_device_evicted()
|
|
160
|
+
def handle_eviction(table_type: str, node_id: str):
|
|
161
|
+
print(f"Device {node_id} was evicted from {table_type}")
|
|
162
|
+
|
|
163
|
+
while True:
|
|
164
|
+
node.poll(timeout_ms=1000)
|
|
165
|
+
|
|
166
|
+
# Check if a device has eviction pending
|
|
167
|
+
device = table.get_device("sensor_01")
|
|
168
|
+
if device and device.eviction_pending:
|
|
169
|
+
print("Device will be evicted soon if it doesn't reconnect")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Thread Safety
|
|
173
|
+
|
|
174
|
+
SdsNode is fully thread-safe. Multiple threads can safely:
|
|
175
|
+
- Call `poll()` concurrently
|
|
176
|
+
- Access table data (`table.state.temperature`)
|
|
177
|
+
- Register tables and callbacks
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import threading
|
|
181
|
+
from sds import SdsNode, Role
|
|
182
|
+
|
|
183
|
+
with SdsNode("my_node", "localhost") as node:
|
|
184
|
+
table = node.register_table("SensorData", Role.DEVICE, schema=SensorData)
|
|
185
|
+
|
|
186
|
+
def polling_thread():
|
|
187
|
+
while True:
|
|
188
|
+
node.poll(timeout_ms=100)
|
|
189
|
+
|
|
190
|
+
def update_thread():
|
|
191
|
+
while True:
|
|
192
|
+
table.state.temperature = read_sensor()
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
|
|
195
|
+
# Both threads can safely access the node and table
|
|
196
|
+
t1 = threading.Thread(target=polling_thread)
|
|
197
|
+
t2 = threading.Thread(target=update_thread)
|
|
198
|
+
t1.start()
|
|
199
|
+
t2.start()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Note:** Callbacks are executed while holding the lock. Avoid blocking
|
|
203
|
+
operations in callbacks to prevent deadlocks.
|
|
204
|
+
|
|
205
|
+
## Logging Configuration
|
|
206
|
+
|
|
207
|
+
SDS uses Python's standard `logging` module. By default, log messages go
|
|
208
|
+
to a `NullHandler` (silent). To enable logging:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from sds import configure_logging
|
|
212
|
+
import logging
|
|
213
|
+
|
|
214
|
+
# Quick setup - logs to stderr at INFO level
|
|
215
|
+
configure_logging(level=logging.INFO)
|
|
216
|
+
|
|
217
|
+
# Or configure manually
|
|
218
|
+
logging.getLogger("sds").setLevel(logging.DEBUG)
|
|
219
|
+
logging.getLogger("sds").addHandler(logging.StreamHandler())
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Connection Resilience
|
|
223
|
+
|
|
224
|
+
SdsNode includes built-in connection retry with exponential backoff:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from sds import SdsNode
|
|
228
|
+
|
|
229
|
+
node = SdsNode(
|
|
230
|
+
"my_node",
|
|
231
|
+
"mqtt.example.com",
|
|
232
|
+
retry_count=5, # Try 5 times (default: 3)
|
|
233
|
+
retry_delay_ms=2000, # Start with 2 second delay (default: 1000)
|
|
234
|
+
connect_timeout_ms=10000 # 10 second timeout (default: 5000)
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
On connection failure, retries are attempted with exponential backoff
|
|
239
|
+
(delay doubles each retry). Non-connection errors are not retried.
|
|
240
|
+
|
|
241
|
+
## Error Handling Best Practices
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from sds import (
|
|
245
|
+
SdsNode, SdsMqttError, SdsTableError,
|
|
246
|
+
SdsValidationError, SdsError
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
with SdsNode("my_node", "localhost") as node:
|
|
251
|
+
table = node.register_table("SensorData", Role.DEVICE)
|
|
252
|
+
|
|
253
|
+
while True:
|
|
254
|
+
try:
|
|
255
|
+
node.poll()
|
|
256
|
+
except SdsMqttError:
|
|
257
|
+
# MQTT connection lost - will auto-reconnect
|
|
258
|
+
logging.warning("MQTT disconnected, waiting...")
|
|
259
|
+
time.sleep(1)
|
|
260
|
+
|
|
261
|
+
except SdsValidationError as e:
|
|
262
|
+
# Invalid node_id or configuration
|
|
263
|
+
logging.error(f"Configuration error: {e}")
|
|
264
|
+
except SdsMqttError as e:
|
|
265
|
+
# Failed to connect after all retries
|
|
266
|
+
logging.error(f"Could not connect to MQTT: {e}")
|
|
267
|
+
except SdsError as e:
|
|
268
|
+
# Other SDS errors
|
|
269
|
+
logging.error(f"SDS error: {e}")
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Requirements
|
|
273
|
+
|
|
274
|
+
- Python 3.8+
|
|
275
|
+
- MQTT broker (e.g., Mosquitto)
|
|
276
|
+
- libpaho-mqtt development headers (for building from source)
|
|
277
|
+
|
|
278
|
+
## Configuration Options
|
|
279
|
+
|
|
280
|
+
### Delta Sync (v0.5.0+)
|
|
281
|
+
|
|
282
|
+
Enable delta sync to only send changed fields, reducing bandwidth:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
with SdsNode(
|
|
286
|
+
"sensor_01",
|
|
287
|
+
"localhost",
|
|
288
|
+
enable_delta_sync=True, # Only send changed fields
|
|
289
|
+
delta_float_tolerance=0.01 # Ignore tiny float changes
|
|
290
|
+
) as node:
|
|
291
|
+
# ...
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Benefits:**
|
|
295
|
+
- Reduced bandwidth (only changed fields transmitted)
|
|
296
|
+
- Lower power consumption on battery devices
|
|
297
|
+
- Works automatically with codegen-generated tables
|
|
298
|
+
|
|
299
|
+
**Limitations:**
|
|
300
|
+
- Config messages are always full (retained on broker)
|
|
301
|
+
- Status heartbeats are always full (liveness detection)
|
|
302
|
+
- Manual schema definitions use full sync
|
|
303
|
+
|
|
304
|
+
### Eviction Grace Period
|
|
305
|
+
|
|
306
|
+
Configure how long to wait before evicting offline devices:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
with SdsNode(
|
|
310
|
+
"owner",
|
|
311
|
+
"localhost",
|
|
312
|
+
eviction_grace_ms=30000 # 30 seconds before eviction
|
|
313
|
+
) as node:
|
|
314
|
+
@node.on_device_evicted()
|
|
315
|
+
def handle_eviction(table_type, node_id):
|
|
316
|
+
print(f"Device {node_id} evicted from {table_type}")
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Raw MQTT Publish/Subscribe (v0.5.0+)
|
|
320
|
+
|
|
321
|
+
Send and receive custom MQTT messages through the SDS-managed connection. Useful for
|
|
322
|
+
logging, diagnostics, or application-specific messages that don't fit the table model.
|
|
323
|
+
|
|
324
|
+
**Publishing:**
|
|
325
|
+
```python
|
|
326
|
+
with SdsNode("sensor_01", "localhost") as node:
|
|
327
|
+
# Check connection status
|
|
328
|
+
if node.is_connected():
|
|
329
|
+
# Publish a log message
|
|
330
|
+
node.publish_raw(
|
|
331
|
+
f"log/{node.node_id}",
|
|
332
|
+
'{"level": "info", "msg": "Sensor started"}',
|
|
333
|
+
qos=0,
|
|
334
|
+
retained=False
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Publish binary data
|
|
338
|
+
node.publish_raw("sensor/raw_data", b'\x00\x01\x02\x03')
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Subscribing:**
|
|
342
|
+
```python
|
|
343
|
+
def on_log(topic: str, payload: bytes):
|
|
344
|
+
print(f"Log from {topic}: {payload.decode()}")
|
|
345
|
+
|
|
346
|
+
with SdsNode("controller", "localhost") as node:
|
|
347
|
+
# Subscribe to all logs (+ matches any single level)
|
|
348
|
+
node.subscribe_raw("log/+", on_log)
|
|
349
|
+
|
|
350
|
+
# Or use # for multi-level wildcard
|
|
351
|
+
node.subscribe_raw("sensors/#", on_sensor_data)
|
|
352
|
+
|
|
353
|
+
# Main loop
|
|
354
|
+
while True:
|
|
355
|
+
node.poll()
|
|
356
|
+
time.sleep(0.1)
|
|
357
|
+
|
|
358
|
+
# Unsubscribe when done
|
|
359
|
+
node.unsubscribe_raw("log/+")
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Methods:**
|
|
363
|
+
|
|
364
|
+
| Method | Description |
|
|
365
|
+
|--------|-------------|
|
|
366
|
+
| `is_connected()` | Returns `True` if connected to MQTT broker |
|
|
367
|
+
| `publish_raw(topic, payload, qos=0, retained=False)` | Publish arbitrary MQTT message |
|
|
368
|
+
| `subscribe_raw(topic, callback, qos=0)` | Subscribe to topic pattern with callback |
|
|
369
|
+
| `unsubscribe_raw(topic)` | Unsubscribe from a topic pattern |
|
|
370
|
+
|
|
371
|
+
**Notes:**
|
|
372
|
+
- Topics starting with `sds/` are reserved and will raise `ValueError`
|
|
373
|
+
- `payload` for publish can be `str` (UTF-8 encoded) or `bytes`
|
|
374
|
+
- Callback receives `(topic: str, payload: bytes)`
|
|
375
|
+
- Maximum 8 concurrent raw subscriptions
|
|
376
|
+
- Wildcard subscriptions (`log/+`) count as 1 subscription regardless of matching topics
|
|
377
|
+
|
|
378
|
+
## API Reference
|
|
379
|
+
|
|
380
|
+
### SdsNode
|
|
381
|
+
|
|
382
|
+
The main class for interacting with SDS.
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
class SdsNode:
|
|
386
|
+
def __init__(self, node_id: str, broker_host: str, port: int = 1883,
|
|
387
|
+
username: str | None = None, password: str | None = None,
|
|
388
|
+
eviction_grace_ms: int = 0):
|
|
389
|
+
"""
|
|
390
|
+
Initialize SDS node and connect to MQTT broker.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
eviction_grace_ms: Grace period before evicting offline devices (0 = disabled).
|
|
394
|
+
For OWNER roles, when a device disconnects (LWT), an eviction
|
|
395
|
+
timer starts. If the device doesn't reconnect within this period,
|
|
396
|
+
it's evicted from status slots, freeing the slot for new devices.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
def register_table(self, table_type: str, role: Role,
|
|
400
|
+
sync_interval_ms: int | None = None,
|
|
401
|
+
config_schema: Type | None = None,
|
|
402
|
+
state_schema: Type | None = None,
|
|
403
|
+
status_schema: Type | None = None) -> SdsTable:
|
|
404
|
+
"""Register a table and return SdsTable for C-like access."""
|
|
405
|
+
|
|
406
|
+
def get_table(self, table_type: str) -> SdsTable:
|
|
407
|
+
"""Get a previously registered table."""
|
|
408
|
+
|
|
409
|
+
def unregister_table(self, table_type: str) -> None:
|
|
410
|
+
"""Unregister a table."""
|
|
411
|
+
|
|
412
|
+
def poll(self, timeout_ms: int = 0) -> None:
|
|
413
|
+
"""Process MQTT messages and sync changes."""
|
|
414
|
+
|
|
415
|
+
def is_ready(self) -> bool:
|
|
416
|
+
"""Check if connected to MQTT broker."""
|
|
417
|
+
|
|
418
|
+
def on_error(self, callback) -> None:
|
|
419
|
+
"""Register error callback."""
|
|
420
|
+
|
|
421
|
+
def on_version_mismatch(self, callback) -> None:
|
|
422
|
+
"""Register version mismatch callback."""
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### SdsTable
|
|
426
|
+
|
|
427
|
+
Provides C-like attribute access to table sections.
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
class SdsTable:
|
|
431
|
+
table_type: str # The table type name
|
|
432
|
+
role: Role # OWNER or DEVICE
|
|
433
|
+
|
|
434
|
+
# Device role properties
|
|
435
|
+
state: SectionProxy # Read/write state section
|
|
436
|
+
status: SectionProxy # Read/write status section
|
|
437
|
+
config: SectionProxy # Read-only config (from owner)
|
|
438
|
+
|
|
439
|
+
# Owner role properties
|
|
440
|
+
config: SectionProxy # Read/write config section
|
|
441
|
+
device_count: int # Number of known devices
|
|
442
|
+
|
|
443
|
+
def get_device(self, node_id: str) -> DeviceView | None:
|
|
444
|
+
"""Get a device's data (owner role only)."""
|
|
445
|
+
|
|
446
|
+
def iter_devices(self) -> Iterator[tuple[str, DeviceView]]:
|
|
447
|
+
"""Iterate over all known devices (owner role only)."""
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Role Enum
|
|
451
|
+
|
|
452
|
+
```python
|
|
453
|
+
class Role(Enum):
|
|
454
|
+
OWNER = 0 # Publishes config, receives state/status
|
|
455
|
+
DEVICE = 1 # Receives config, publishes state/status
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### LogLevel Enum
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
class LogLevel(Enum):
|
|
462
|
+
NONE = 0 # Disable all logging
|
|
463
|
+
ERROR = 1 # Error conditions only
|
|
464
|
+
WARN = 2 # Warnings and errors
|
|
465
|
+
INFO = 3 # Informational messages
|
|
466
|
+
DEBUG = 4 # All messages
|
|
467
|
+
|
|
468
|
+
# Functions
|
|
469
|
+
set_log_level(level: LogLevel) -> None
|
|
470
|
+
get_log_level() -> LogLevel
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Field Helper
|
|
474
|
+
|
|
475
|
+
Define field types for schema dataclasses:
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
from sds import Field, FieldType
|
|
479
|
+
|
|
480
|
+
@dataclass
|
|
481
|
+
class MyConfig:
|
|
482
|
+
command: int = Field(uint8=True)
|
|
483
|
+
value: float = Field(float32=True)
|
|
484
|
+
name: str = Field(string_len=32)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### SectionProxy Convenience Methods
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
# Convert section to dictionary
|
|
491
|
+
data = table.state.to_dict()
|
|
492
|
+
# {'temperature': 23.5, 'humidity': 65.0}
|
|
493
|
+
|
|
494
|
+
# Set multiple fields from dictionary
|
|
495
|
+
table.state.from_dict({'temperature': 24.0, 'humidity': 60.0})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Exceptions
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
class SdsError(Exception):
|
|
502
|
+
"""Base exception for SDS errors."""
|
|
503
|
+
|
|
504
|
+
class SdsNotInitializedError(SdsError):
|
|
505
|
+
"""Raised when SDS is not initialized."""
|
|
506
|
+
|
|
507
|
+
class SdsMqttError(SdsError):
|
|
508
|
+
"""Raised on MQTT connection errors."""
|
|
509
|
+
|
|
510
|
+
class SdsTableError(SdsError):
|
|
511
|
+
"""Raised on table registration errors."""
|
|
512
|
+
|
|
513
|
+
class SdsValidationError(SdsError):
|
|
514
|
+
"""Raised when input validation fails (e.g., invalid node_id)."""
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Building from Source
|
|
518
|
+
|
|
519
|
+
### Prerequisites
|
|
520
|
+
|
|
521
|
+
**Linux (Debian/Ubuntu):**
|
|
522
|
+
```bash
|
|
523
|
+
sudo apt-get install libpaho-mqtt-dev python3-dev
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Linux (RHEL/CentOS):**
|
|
527
|
+
```bash
|
|
528
|
+
sudo yum install paho-c-devel python3-devel
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**macOS:**
|
|
532
|
+
```bash
|
|
533
|
+
brew install libpaho-mqtt
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Build
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
cd python
|
|
540
|
+
pip install build
|
|
541
|
+
python -m build
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## Testing
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
cd python
|
|
548
|
+
pip install -e ".[dev]"
|
|
549
|
+
pytest
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
For integration tests (requires MQTT broker):
|
|
553
|
+
```bash
|
|
554
|
+
pytest -m requires_mqtt
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## License
|
|
558
|
+
|
|
559
|
+
MIT License - see LICENSE file for details.
|