dln2 0.1.0__py3-none-any.whl

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.
dln2/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified DLN2 wrapper package.
4
+
5
+ Single USB connection shared by SPI, GPIO, I2C and ADC modules.
6
+ Eliminates echo-counter conflicts and USB contention from the
7
+ original four independent packages.
8
+
9
+ Usage:
10
+ from dln2 import Dln2Connection, SpiDev, GPIO, SMBus, ADC, list_devices
11
+
12
+ conn = Dln2Connection() # single USB connection
13
+ spi = SpiDev(conn) # all modules share conn
14
+ gpio = GPIO(conn)
15
+ i2c = SMBus(conn)
16
+
17
+ See also: dln2_wrapper.py (standalone legacy version)
18
+ """
19
+
20
+ from ._core import Dln2Connection, list_devices, Dln2DeviceInfo
21
+ from .spi import SpiDev
22
+ from .gpio import GPIO, \
23
+ GPIO_EVENT_NONE, GPIO_EVENT_CHANGE, \
24
+ GPIO_EVENT_LEVEL_HIGH, GPIO_EVENT_LEVEL_LOW
25
+ from .i2c import SMBus, i2c_msg
26
+ from .adc import ADC
27
+ from .bme280 import BME280
dln2/_core.py ADDED
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env python3
2
+ """DLN2 unified USB connection — single client for SPI + GPIO + I2C + ADC."""
3
+
4
+ import struct
5
+ import time
6
+ import usb.core
7
+ import usb.util
8
+
9
+ VID = 0x1D50
10
+ PID = 0x6170
11
+
12
+ # ═══════════════════════════════════════════════════════════════
13
+ # DLN2 protocol constants
14
+ # ═══════════════════════════════════════════════════════════════
15
+
16
+ DLN2_MODULE_GENERIC = 0x00
17
+ DLN2_MODULE_GPIO = 0x01
18
+ DLN2_MODULE_SPI = 0x02
19
+ DLN2_MODULE_I2C = 0x03
20
+ DLN2_MODULE_ADC = 0x06
21
+
22
+ DLN2_HANDLE_CTRL = 1
23
+ DLN2_HANDLE_GPIO = 2
24
+ DLN2_HANDLE_I2C = 3
25
+ DLN2_HANDLE_SPI = 4
26
+ DLN2_HANDLE_ADC = 5
27
+ DLN2_HANDLE_EVENT = 0
28
+
29
+ def DLN2_CMD(cmd, module):
30
+ return (cmd & 0xFF) | ((module & 0xFF) << 8)
31
+
32
+ # ── SPI ───────────────────────────────────────────────────────
33
+ DLN2_SPI_ENABLE = DLN2_CMD(0x11, DLN2_MODULE_SPI)
34
+ DLN2_SPI_DISABLE = DLN2_CMD(0x12, DLN2_MODULE_SPI)
35
+ DLN2_SPI_SET_MODE = DLN2_CMD(0x14, DLN2_MODULE_SPI)
36
+ DLN2_SPI_SET_FRAME_SIZE = DLN2_CMD(0x16, DLN2_MODULE_SPI)
37
+ DLN2_SPI_SET_FREQUENCY = DLN2_CMD(0x18, DLN2_MODULE_SPI)
38
+ DLN2_SPI_READ_WRITE = DLN2_CMD(0x1A, DLN2_MODULE_SPI)
39
+ DLN2_SPI_READ = DLN2_CMD(0x1B, DLN2_MODULE_SPI)
40
+ DLN2_SPI_WRITE = DLN2_CMD(0x1C, DLN2_MODULE_SPI)
41
+ DLN2_SPI_GET_SS_COUNT = DLN2_CMD(0x44, DLN2_MODULE_SPI)
42
+ DLN2_SPI_SS_MULTI_ENABLE = DLN2_CMD(0x38, DLN2_MODULE_SPI)
43
+ DLN2_SPI_SS_MULTI_DISABLE = DLN2_CMD(0x39, DLN2_MODULE_SPI)
44
+
45
+ # ── GPIO ──────────────────────────────────────────────────────
46
+ DLN2_GPIO_GET_PIN_COUNT = DLN2_CMD(0x01, DLN2_MODULE_GPIO)
47
+ DLN2_GPIO_PIN_GET_VAL = DLN2_CMD(0x0B, DLN2_MODULE_GPIO)
48
+ DLN2_GPIO_PIN_SET_OUT_VAL = DLN2_CMD(0x0C, DLN2_MODULE_GPIO)
49
+ DLN2_GPIO_PIN_GET_OUT_VAL = DLN2_CMD(0x0D, DLN2_MODULE_GPIO)
50
+ DLN2_GPIO_CONDITION_MET_EV = DLN2_CMD(0x0F, DLN2_MODULE_GPIO)
51
+ DLN2_GPIO_PIN_ENABLE = DLN2_CMD(0x10, DLN2_MODULE_GPIO)
52
+ DLN2_GPIO_PIN_DISABLE = DLN2_CMD(0x11, DLN2_MODULE_GPIO)
53
+ DLN2_GPIO_PIN_SET_DIRECTION = DLN2_CMD(0x13, DLN2_MODULE_GPIO)
54
+ DLN2_GPIO_PIN_GET_DIRECTION = DLN2_CMD(0x14, DLN2_MODULE_GPIO)
55
+ DLN2_GPIO_PIN_SET_EVENT_CFG = DLN2_CMD(0x1E, DLN2_MODULE_GPIO)
56
+
57
+ GPIO_EVENT_NONE = 0
58
+ GPIO_EVENT_CHANGE = 1
59
+ GPIO_EVENT_LEVEL_HIGH = 2
60
+ GPIO_EVENT_LEVEL_LOW = 3
61
+
62
+ # ── I2C ───────────────────────────────────────────────────────
63
+ DLN2_I2C_ENABLE = DLN2_CMD(0x01, DLN2_MODULE_I2C)
64
+ DLN2_I2C_DISABLE = DLN2_CMD(0x02, DLN2_MODULE_I2C)
65
+ DLN2_I2C_WRITE = DLN2_CMD(0x06, DLN2_MODULE_I2C)
66
+ DLN2_I2C_READ = DLN2_CMD(0x07, DLN2_MODULE_I2C)
67
+
68
+ # ── ADC ───────────────────────────────────────────────────────
69
+ DLN2_ADC_GET_CHANNEL_COUNT = DLN2_CMD(0x01, DLN2_MODULE_ADC)
70
+ DLN2_ADC_ENABLE = DLN2_CMD(0x02, DLN2_MODULE_ADC)
71
+ DLN2_ADC_DISABLE = DLN2_CMD(0x03, DLN2_MODULE_ADC)
72
+ DLN2_ADC_CHANNEL_ENABLE = DLN2_CMD(0x05, DLN2_MODULE_ADC)
73
+ DLN2_ADC_CHANNEL_DISABLE = DLN2_CMD(0x06, DLN2_MODULE_ADC)
74
+ DLN2_ADC_SET_RESOLUTION = DLN2_CMD(0x08, DLN2_MODULE_ADC)
75
+ DLN2_ADC_CHANNEL_GET_VAL = DLN2_CMD(0x0A, DLN2_MODULE_ADC)
76
+ DLN2_ADC_CHANNEL_GET_ALL_VAL = DLN2_CMD(0x0B, DLN2_MODULE_ADC)
77
+
78
+
79
+ class Dln2DeviceInfo:
80
+ """Metadata for one connected DLN2 device."""
81
+ __slots__ = ("dev", "serial", "index")
82
+
83
+ def __init__(self, dev, serial, index):
84
+ self.dev = dev
85
+ self.serial = serial
86
+ self.index = index
87
+
88
+
89
+ def list_devices():
90
+ """Return list of all connected DLN2 devices."""
91
+ devices = []
92
+ for i, dev in enumerate(usb.core.find(find_all=True,
93
+ idVendor=VID, idProduct=PID)):
94
+ try:
95
+ serial = usb.util.get_string(dev, dev.iSerialNumber)
96
+ except Exception:
97
+ serial = ""
98
+ devices.append(Dln2DeviceInfo(dev=dev, serial=serial, index=i))
99
+ return devices
100
+
101
+
102
+ class Dln2Connection:
103
+ """Unified DLN2 client — SPI + GPIO + I2C + ADC share one USB connection."""
104
+
105
+ def __init__(self, index=None, serial=None, debug=False):
106
+ self.debug = bool(debug)
107
+ self._echo = 1
108
+ self._event_queue = []
109
+
110
+ devices = list_devices()
111
+ if not devices:
112
+ raise ValueError(f"No DLN2 device found ({VID:04X}:{PID:04X})")
113
+
114
+ if serial is not None:
115
+ match = next((d for d in devices if d.serial == serial), None)
116
+ if not match:
117
+ raise ValueError(f"No DLN2 with serial {serial}")
118
+ self._dev = match.dev
119
+ elif index is not None:
120
+ if index >= len(devices):
121
+ raise ValueError(
122
+ f"DLN2 index {index} out of range ({len(devices)} found)")
123
+ self._dev = devices[index].dev
124
+ else:
125
+ self._dev = devices[0].dev
126
+
127
+ self._setup()
128
+
129
+ def _setup(self):
130
+ # Reset USB to clear stale state
131
+ try:
132
+ self._dev.reset()
133
+ except Exception:
134
+ pass
135
+
136
+ try:
137
+ self._dev.set_configuration()
138
+ except Exception:
139
+ pass
140
+
141
+ cfg = self._dev.get_active_configuration()
142
+ intf = cfg[(0, 0)]
143
+
144
+ ep_out = ep_in = None
145
+ for ep in intf.endpoints():
146
+ d = usb.util.endpoint_direction(ep.bEndpointAddress)
147
+ t = usb.util.endpoint_type(ep.bmAttributes)
148
+ if d == usb.util.ENDPOINT_OUT and t == usb.util.ENDPOINT_TYPE_BULK:
149
+ ep_out = ep
150
+ if d == usb.util.ENDPOINT_IN and t == usb.util.ENDPOINT_TYPE_BULK:
151
+ ep_in = ep
152
+ if not ep_out or not ep_in:
153
+ raise RuntimeError("No bulk endpoints found on DLN2")
154
+ self._ep_out = ep_out.bEndpointAddress
155
+ self._ep_in = ep_in.bEndpointAddress
156
+ self._interface = intf.bInterfaceNumber
157
+
158
+ if self._dev.is_kernel_driver_active(self._interface):
159
+ try:
160
+ self._dev.detach_kernel_driver(self._interface)
161
+ except Exception:
162
+ pass
163
+ usb.util.claim_interface(self._dev, self._interface)
164
+
165
+ # ─── USB raw ──────────────────────────────────────────────
166
+ def _send_raw(self, data: bytes):
167
+ self._dev.write(self._ep_out, data, timeout=2000)
168
+
169
+ def _read_raw(self, size=1024, timeout_ms=2000):
170
+ return bytes(self._dev.read(self._ep_in, size, timeout=timeout_ms))
171
+
172
+ # ─── Unified command dispatch ──────────────────────────
173
+ def send_cmd(self, cmd_id, payload=b"", handle=DLN2_HANDLE_SPI):
174
+ hdr_size = 8
175
+ size = hdr_size + len(payload)
176
+ pkt = struct.pack("<HHHH", size, cmd_id, self._echo & 0xFFFF,
177
+ handle & 0xFFFF) + payload
178
+ expected = self._echo & 0xFFFF
179
+ self._echo = (self._echo + 1) & 0xFFFF
180
+
181
+ if self.debug:
182
+ print(f"[DLN2] OUT echo={expected} cmd=0x{cmd_id:04X} "
183
+ f"handle={handle} len={len(payload)}")
184
+
185
+ self._send_raw(pkt)
186
+
187
+ # Read — retry for large payloads
188
+ deadline = time.monotonic() + 10.0
189
+ while time.monotonic() < deadline:
190
+ try:
191
+ raw = self._read_raw(1024, timeout_ms=3000)
192
+ except Exception:
193
+ time.sleep(0.05)
194
+ continue
195
+
196
+ if len(raw) < 10:
197
+ time.sleep(0.05)
198
+ continue
199
+
200
+ sz, rid, echo, hnd, result = struct.unpack("<HHHHH", raw[:10])
201
+ data = raw[10:sz]
202
+
203
+ if self.debug:
204
+ print(f" IN echo={echo} result={result} data={data[:16].hex()}")
205
+
206
+ if hnd == DLN2_HANDLE_EVENT:
207
+ # Queue event for GPIO.poll_event()
208
+ self._queue_event(raw)
209
+ continue # read next response
210
+
211
+ if echo != expected:
212
+ raise RuntimeError(
213
+ f"Echo mismatch: got {echo}, expected {expected}")
214
+
215
+ return {"result": result, "data": data, "echo": echo}
216
+
217
+ raise RuntimeError("Timeout waiting for DLN2 response")
218
+
219
+ def _queue_event(self, raw):
220
+ """Decode and queue a GPIO event packet."""
221
+ if len(raw) < 16:
222
+ return
223
+ try:
224
+ # Event packet: size, id, echo, handle(0), result
225
+ # Followed by event_type(2B) + pin(2B) + ?
226
+ _, _, _, _, _ = struct.unpack("<HHHHH", raw[:10])
227
+ ev_type = struct.unpack("<H", raw[10:12])[0]
228
+ ev_data = raw[12:14] # variable
229
+ self._event_queue.append({
230
+ "type": ev_type,
231
+ "pin": ev_data[0],
232
+ "value": ev_data[1] if len(ev_data) > 1 else 0,
233
+ })
234
+ except Exception:
235
+ pass
236
+
237
+ def close(self):
238
+ try:
239
+ usb.util.release_interface(self._dev, self._interface)
240
+ except Exception:
241
+ pass
242
+
243
+ # ═══════════════════════════════════════════════════════════
244
+ # SPI raw
245
+ # ═══════════════════════════════════════════════════════════
246
+
247
+ def spi_configure(self, mode, freq_hz, bpw=8):
248
+ self.send_cmd(DLN2_SPI_SET_FRAME_SIZE,
249
+ struct.pack("<BB", 0, bpw),
250
+ handle=DLN2_HANDLE_SPI)
251
+ self.send_cmd(DLN2_SPI_SET_FREQUENCY,
252
+ struct.pack("<BI", 0, freq_hz),
253
+ handle=DLN2_HANDLE_SPI)
254
+ self.send_cmd(DLN2_SPI_SET_MODE,
255
+ struct.pack("<BB", 0, mode & 3),
256
+ handle=DLN2_HANDLE_SPI)
257
+
258
+ def spi_enable(self):
259
+ self.send_cmd(DLN2_SPI_ENABLE, struct.pack("<B", 0),
260
+ handle=DLN2_HANDLE_SPI)
261
+
262
+ def spi_disable(self):
263
+ self.send_cmd(DLN2_SPI_DISABLE, struct.pack("<BB", 0, 0),
264
+ handle=DLN2_HANDLE_SPI)
265
+
266
+ def spi_read_write(self, tx_bytes, leave_ss_low=False):
267
+ attr = 1 if leave_ss_low else 0
268
+ payload = struct.pack("<BHB", 0, len(tx_bytes) & 0xFFFF,
269
+ attr & 0xFF) + tx_bytes
270
+ resp = self.send_cmd(DLN2_SPI_READ_WRITE, payload,
271
+ handle=DLN2_HANDLE_SPI)
272
+ if resp["result"] != 0:
273
+ raise RuntimeError(f"SPI transfer failed: result={resp['result']}")
274
+ if len(resp["data"]) < 2:
275
+ raise RuntimeError("SPI response too short")
276
+ rx_len = struct.unpack("<H", resp["data"][:2])[0]
277
+ return resp["data"][2:2 + rx_len]
278
+
279
+ def spi_write(self, data, leave_ss_low=False):
280
+ attr = 1 if leave_ss_low else 0
281
+ for i in range(0, len(data), 128):
282
+ chunk = data[i:i + 128]
283
+ pkt = struct.pack("<BHB", 0, len(chunk), attr) + chunk
284
+ self.send_cmd(DLN2_SPI_WRITE, pkt, handle=DLN2_HANDLE_SPI)
285
+
286
+ # ═══════════════════════════════════════════════════════════
287
+ # GPIO raw
288
+ # ═══════════════════════════════════════════════════════════
289
+
290
+ def gpio_get_pin_count(self):
291
+ resp = self.send_cmd(DLN2_GPIO_GET_PIN_COUNT, b"",
292
+ handle=DLN2_HANDLE_GPIO)
293
+ if resp["result"] != 0:
294
+ raise RuntimeError(f"GPIO get_pin_count failed: {resp['result']}")
295
+ if len(resp["data"]) < 2:
296
+ raise RuntimeError("GPIO get_pin_count response too short")
297
+ return struct.unpack("<H", resp["data"][:2])[0]
298
+
299
+ def gpio_pin_enable(self, pin):
300
+ resp = self.send_cmd(DLN2_GPIO_PIN_ENABLE,
301
+ struct.pack("<H", int(pin)),
302
+ handle=DLN2_HANDLE_GPIO)
303
+ if resp["result"] != 0:
304
+ raise RuntimeError(f"GPIO pin_enable({pin}) failed: {resp['result']}")
305
+
306
+ def gpio_pin_disable(self, pin):
307
+ resp = self.send_cmd(DLN2_GPIO_PIN_DISABLE,
308
+ struct.pack("<H", int(pin)),
309
+ handle=DLN2_HANDLE_GPIO)
310
+ if resp["result"] != 0:
311
+ raise RuntimeError(f"GPIO pin_disable({pin}) failed: {resp['result']}")
312
+
313
+ def gpio_pin_set_direction(self, pin, is_output):
314
+ resp = self.send_cmd(DLN2_GPIO_PIN_SET_DIRECTION,
315
+ struct.pack("<HB", int(pin), 1 if is_output else 0),
316
+ handle=DLN2_HANDLE_GPIO)
317
+ if resp["result"] != 0:
318
+ raise RuntimeError(
319
+ f"GPIO set_direction({pin}) failed: {resp['result']}")
320
+
321
+ def gpio_pin_get_direction(self, pin):
322
+ resp = self.send_cmd(DLN2_GPIO_PIN_GET_DIRECTION,
323
+ struct.pack("<H", int(pin)),
324
+ handle=DLN2_HANDLE_GPIO)
325
+ if resp["result"] != 0:
326
+ raise RuntimeError(
327
+ f"GPIO get_direction({pin}) failed: {resp['result']}")
328
+ if len(resp["data"]) < 3:
329
+ raise RuntimeError("GPIO get_direction response too short")
330
+ _, direction = struct.unpack("<HB", resp["data"][:3])
331
+ return bool(direction)
332
+
333
+ def gpio_get(self, pin):
334
+ resp = self.send_cmd(DLN2_GPIO_PIN_GET_VAL,
335
+ struct.pack("<H", int(pin)),
336
+ handle=DLN2_HANDLE_GPIO)
337
+ if resp["result"] != 0:
338
+ raise RuntimeError(f"GPIO get_val({pin}) failed: {resp['result']}")
339
+ if len(resp["data"]) < 3:
340
+ raise RuntimeError("GPIO get_val response too short")
341
+ _, value = struct.unpack("<HB", resp["data"][:3])
342
+ return value
343
+
344
+ def gpio_set(self, pin, value):
345
+ resp = self.send_cmd(DLN2_GPIO_PIN_SET_OUT_VAL,
346
+ struct.pack("<HB", int(pin), 1 if value else 0),
347
+ handle=DLN2_HANDLE_GPIO)
348
+ if resp["result"] != 0:
349
+ raise RuntimeError(f"GPIO set_out_val({pin}) failed: {resp['result']}")
350
+
351
+ def gpio_get_out_val(self, pin):
352
+ resp = self.send_cmd(DLN2_GPIO_PIN_GET_OUT_VAL,
353
+ struct.pack("<H", int(pin)),
354
+ handle=DLN2_HANDLE_GPIO)
355
+ if resp["result"] != 0:
356
+ raise RuntimeError(
357
+ f"GPIO get_out_val({pin}) failed: {resp['result']}")
358
+ if len(resp["data"]) < 3:
359
+ raise RuntimeError("GPIO get_out_val response too short")
360
+ _, value = struct.unpack("<HB", resp["data"][:3])
361
+ return value
362
+
363
+ def gpio_set_event_cfg(self, pin, event_type, period=0):
364
+ resp = self.send_cmd(DLN2_GPIO_PIN_SET_EVENT_CFG,
365
+ struct.pack("<HBH", int(pin), int(event_type),
366
+ int(period)),
367
+ handle=DLN2_HANDLE_GPIO)
368
+ if resp["result"] != 0:
369
+ raise RuntimeError(
370
+ f"GPIO set_event_cfg({pin}) failed: {resp['result']}")
371
+
372
+ def gpio_poll_event(self, timeout_ms=0):
373
+ if self._event_queue:
374
+ return self._event_queue.pop(0)
375
+ try:
376
+ raw = self._read_raw(1024, timeout_ms=timeout_ms)
377
+ if len(raw) < 10:
378
+ return None
379
+ # Parse as event
380
+ self._queue_event(raw)
381
+ if self._event_queue:
382
+ return self._event_queue.pop(0)
383
+ except Exception:
384
+ pass
385
+ return None
386
+
387
+ # ═══════════════════════════════════════════════════════════
388
+ # I2C raw
389
+ # ═══════════════════════════════════════════════════════════
390
+
391
+ def i2c_enable(self):
392
+ return self.send_cmd(DLN2_I2C_ENABLE, struct.pack("<B", 0),
393
+ handle=DLN2_HANDLE_I2C)
394
+
395
+ def i2c_disable(self):
396
+ return self.send_cmd(DLN2_I2C_DISABLE, struct.pack("<B", 0),
397
+ handle=DLN2_HANDLE_I2C)
398
+
399
+ def i2c_write(self, addr, data, mem_addr_len=0, mem_addr=0):
400
+ tx = bytes(data)
401
+ payload = struct.pack("<BBBIH", 0, int(addr) & 0x7F,
402
+ int(mem_addr_len) & 0xFF,
403
+ int(mem_addr) & 0xFFFFFFFF,
404
+ len(tx) & 0xFFFF) + tx
405
+ resp = self.send_cmd(DLN2_I2C_WRITE, payload,
406
+ handle=DLN2_HANDLE_I2C)
407
+ if resp["result"] != 0:
408
+ raise RuntimeError(f"I2C write(0x{addr:02X}) failed: {resp['result']}")
409
+ return resp
410
+
411
+ def i2c_read(self, addr, length, mem_addr_len=0, mem_addr=0):
412
+ payload = struct.pack("<BBBIH", 0, int(addr) & 0x7F,
413
+ int(mem_addr_len) & 0xFF,
414
+ int(mem_addr) & 0xFFFFFFFF,
415
+ int(length) & 0xFFFF)
416
+ resp = self.send_cmd(DLN2_I2C_READ, payload,
417
+ handle=DLN2_HANDLE_I2C)
418
+ if resp["result"] != 0:
419
+ raise RuntimeError(f"I2C read(0x{addr:02X}) failed: {resp['result']}")
420
+ if len(resp["data"]) < 2:
421
+ raise RuntimeError("I2C response too short")
422
+ rx_len = struct.unpack("<H", resp["data"][:2])[0]
423
+ rx = resp["data"][2:2 + rx_len]
424
+ if len(rx) != rx_len:
425
+ raise RuntimeError("Short I2C read payload")
426
+ return rx
427
+
428
+ # ═══════════════════════════════════════════════════════════
429
+ # ADC raw
430
+ # ═══════════════════════════════════════════════════════════
431
+
432
+ def adc_get_channel_count(self, port=0):
433
+ resp = self.send_cmd(DLN2_ADC_GET_CHANNEL_COUNT,
434
+ struct.pack("<B", port & 0xFF),
435
+ handle=DLN2_HANDLE_ADC)
436
+ if resp["result"] != 0:
437
+ raise RuntimeError(f"ADC get_channel_count failed: {resp['result']}")
438
+ if len(resp["data"]) != 1:
439
+ raise RuntimeError("ADC channel count response size mismatch")
440
+ return resp["data"][0]
441
+
442
+ def adc_enable(self, port=0):
443
+ resp = self.send_cmd(DLN2_ADC_ENABLE,
444
+ struct.pack("<B", port & 0xFF),
445
+ handle=DLN2_HANDLE_ADC)
446
+ if resp["result"] != 0:
447
+ raise RuntimeError(f"ADC enable failed: {resp['result']}")
448
+ if len(resp["data"]) < 2:
449
+ raise RuntimeError("ADC enable response too short")
450
+ return struct.unpack("<H", resp["data"][:2])[0]
451
+
452
+ def adc_disable(self, port=0):
453
+ resp = self.send_cmd(DLN2_ADC_DISABLE,
454
+ struct.pack("<B", port & 0xFF),
455
+ handle=DLN2_HANDLE_ADC)
456
+ if resp["result"] != 0:
457
+ raise RuntimeError(f"ADC disable failed: {resp['result']}")
458
+ if len(resp["data"]) < 2:
459
+ raise RuntimeError("ADC disable response too short")
460
+ return struct.unpack("<H", resp["data"][:2])[0]
461
+
462
+ def adc_channel_enable(self, channel, port=0):
463
+ resp = self.send_cmd(DLN2_ADC_CHANNEL_ENABLE,
464
+ struct.pack("<BB", port & 0xFF, int(channel) & 0xFF),
465
+ handle=DLN2_HANDLE_ADC)
466
+ if resp["result"] != 0:
467
+ raise RuntimeError(
468
+ f"ADC channel_enable({channel}) failed: {resp['result']}")
469
+
470
+ def adc_channel_disable(self, channel, port=0):
471
+ resp = self.send_cmd(DLN2_ADC_CHANNEL_DISABLE,
472
+ struct.pack("<BB", port & 0xFF, int(channel) & 0xFF),
473
+ handle=DLN2_HANDLE_ADC)
474
+ if resp["result"] != 0:
475
+ raise RuntimeError(
476
+ f"ADC channel_disable({channel}) failed: {resp['result']}")
477
+
478
+ def adc_set_resolution(self, bits, port=0):
479
+ resp = self.send_cmd(DLN2_ADC_SET_RESOLUTION,
480
+ struct.pack("<BB", port & 0xFF, int(bits) & 0xFF),
481
+ handle=DLN2_HANDLE_ADC)
482
+ if resp["result"] != 0:
483
+ raise RuntimeError(f"ADC set_resolution failed: {resp['result']}")
484
+
485
+ def adc_read_channel(self, channel, port=0):
486
+ resp = self.send_cmd(DLN2_ADC_CHANNEL_GET_VAL,
487
+ struct.pack("<BB", port & 0xFF, int(channel) & 0xFF),
488
+ handle=DLN2_HANDLE_ADC)
489
+ if resp["result"] != 0:
490
+ raise RuntimeError(f"ADC read_channel failed: {resp['result']}")
491
+ if len(resp["data"]) < 2:
492
+ raise RuntimeError("ADC read_channel response too short")
493
+ return struct.unpack("<H", resp["data"][:2])[0]
494
+
495
+ def adc_read_all(self, port=0):
496
+ resp = self.send_cmd(DLN2_ADC_CHANNEL_GET_ALL_VAL,
497
+ struct.pack("<B", port & 0xFF),
498
+ handle=DLN2_HANDLE_ADC)
499
+ if resp["result"] != 0:
500
+ raise RuntimeError(f"ADC read_all failed: {resp['result']}")
501
+ if len(resp["data"]) < 18:
502
+ raise RuntimeError("ADC read_all response too short")
503
+ channel_mask = struct.unpack("<H", resp["data"][:2])[0]
504
+ values = list(struct.unpack("<8H", resp["data"][2:18]))
505
+ return {"channel_mask": channel_mask, "values": values}
dln2/adc.py ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """DLN2 ADC wrapper."""
3
+
4
+
5
+ class ADC:
6
+ MAX_CHANNELS = 3
7
+ DEFAULT_VREF = 3.3
8
+ DEFAULT_BITS = 10
9
+ DEFAULT_MAX_VALUE = (1 << DEFAULT_BITS) - 1
10
+
11
+ def __init__(self, connection=None, port=0, resolution_bits=10,
12
+ vref=3.3):
13
+ if connection is None:
14
+ from ._core import Dln2Connection
15
+ connection = Dln2Connection()
16
+ self._conn = connection
17
+ self.port = int(port)
18
+ self.resolution_bits = int(resolution_bits)
19
+ self.vref = float(vref)
20
+ self._enabled = False
21
+
22
+ def open(self):
23
+ if self._enabled:
24
+ return self
25
+ self._conn.adc_enable(self.port)
26
+ self._conn.adc_set_resolution(self.resolution_bits, self.port)
27
+ self._enabled = True
28
+ return self
29
+
30
+ def close(self):
31
+ if not self._enabled:
32
+ return
33
+ try:
34
+ self._conn.adc_disable(self.port)
35
+ except Exception:
36
+ pass
37
+ self._enabled = False
38
+
39
+ def _require(self):
40
+ if not self._enabled:
41
+ self.open()
42
+ return self._conn
43
+
44
+ def get_channel_count(self):
45
+ return self._require().adc_get_channel_count(self.port)
46
+
47
+ def enable_channel(self, channel):
48
+ self._validate(channel)
49
+ self._require().adc_channel_enable(channel, self.port)
50
+
51
+ def disable_channel(self, channel):
52
+ self._validate(channel)
53
+ self._require().adc_channel_disable(channel, self.port)
54
+
55
+ def set_resolution(self, bits):
56
+ self.resolution_bits = int(bits)
57
+ self._require().adc_set_resolution(self.resolution_bits, self.port)
58
+
59
+ def read_channel(self, channel, enable=False):
60
+ self._validate(channel)
61
+ c = self._require()
62
+ if enable:
63
+ c.adc_channel_enable(channel, self.port)
64
+ return c.adc_read_channel(channel, self.port)
65
+
66
+ def read_all(self):
67
+ data = self._require().adc_read_all(self.port)
68
+ return {
69
+ "channel_mask": data["channel_mask"],
70
+ "values": data["values"][:self.MAX_CHANNELS],
71
+ }
72
+
73
+ def read_volts(self, channel, enable=False):
74
+ raw = self.read_channel(channel, enable=enable)
75
+ return float(raw) * self.vref / self.DEFAULT_MAX_VALUE
76
+
77
+ def to_voltage(self, raw):
78
+ return float(raw) * self.vref / self.DEFAULT_MAX_VALUE
79
+
80
+ def _validate(self, channel):
81
+ channel = int(channel)
82
+ if channel < 0 or channel >= self.MAX_CHANNELS:
83
+ raise ValueError(f"ADC channel must be in range 0..{self.MAX_CHANNELS - 1}")
84
+
85
+ def __enter__(self):
86
+ return self.open()
87
+
88
+ def __exit__(self, *a):
89
+ self.close()
dln2/bme280.py ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Minimal BME280 helper built on top of the DLN2 SMBus wrapper.
4
+ """
5
+
6
+ import struct
7
+
8
+ from .i2c import SMBus
9
+
10
+
11
+ class BME280:
12
+ CHIP_ID_REGISTER = 0xD0
13
+ RESET_REGISTER = 0xE0
14
+ STATUS_REGISTER = 0xF3
15
+ CTRL_HUM_REGISTER = 0xF2
16
+ CTRL_MEAS_REGISTER = 0xF4
17
+ CONFIG_REGISTER = 0xF5
18
+ DATA_REGISTER = 0xF7
19
+
20
+ EXPECTED_CHIP_ID = 0x60
21
+
22
+ def __init__(self, bus=1, address=0x76, debug=False):
23
+ if isinstance(bus, SMBus):
24
+ self.bus = bus
25
+ self._owns_bus = False
26
+ elif hasattr(bus, 'send_cmd'): # Dln2Connection passed directly
27
+ self.bus = SMBus(connection=bus)
28
+ self._owns_bus = True
29
+ else:
30
+ self.bus = SMBus()
31
+ self._owns_bus = True
32
+
33
+ self.address = int(address) & 0x7F
34
+ self.debug = bool(debug)
35
+ self.calibration = None
36
+ self.t_fine = 0.0
37
+
38
+ def close(self):
39
+ if self._owns_bus:
40
+ self.bus.close()
41
+
42
+ def __enter__(self):
43
+ return self
44
+
45
+ def __exit__(self, exc_type, exc, tb):
46
+ self.close()
47
+
48
+ def read_chip_id(self):
49
+ return self.bus.read_byte_data(self.address, self.CHIP_ID_REGISTER)
50
+
51
+ def check_chip_id(self):
52
+ chip_id = self.read_chip_id()
53
+ if chip_id != self.EXPECTED_CHIP_ID:
54
+ raise RuntimeError(
55
+ f"Unexpected BME280 chip ID: 0x{chip_id:02x} at 0x{self.address:02x}"
56
+ )
57
+ return chip_id
58
+
59
+ def reset(self):
60
+ self.bus.write_byte_data(self.address, self.RESET_REGISTER, 0xB6)
61
+
62
+ def configure(self, osrs_t=1, osrs_p=1, osrs_h=1, mode=3, standby=5, filter_coef=0):
63
+ self.bus.write_byte_data(self.address, self.CTRL_HUM_REGISTER, osrs_h & 0x07)
64
+ ctrl_meas = ((osrs_t & 0x07) << 5) | ((osrs_p & 0x07) << 2) | (mode & 0x03)
65
+ config = ((standby & 0x07) << 5) | ((filter_coef & 0x07) << 2)
66
+ self.bus.write_byte_data(self.address, self.CONFIG_REGISTER, config)
67
+ self.bus.write_byte_data(self.address, self.CTRL_MEAS_REGISTER, ctrl_meas)
68
+
69
+ def read_calibration(self):
70
+ block1 = self.bus.read_i2c_block_data(self.address, 0x88, 26)
71
+ block2 = self.bus.read_i2c_block_data(self.address, 0xE1, 7)
72
+
73
+ calib1 = bytes(block1)
74
+ calib2 = bytes(block2)
75
+
76
+ self.calibration = {
77
+ "dig_T1": struct.unpack_from("<H", calib1, 0)[0],
78
+ "dig_T2": struct.unpack_from("<h", calib1, 2)[0],
79
+ "dig_T3": struct.unpack_from("<h", calib1, 4)[0],
80
+ "dig_P1": struct.unpack_from("<H", calib1, 6)[0],
81
+ "dig_P2": struct.unpack_from("<h", calib1, 8)[0],
82
+ "dig_P3": struct.unpack_from("<h", calib1, 10)[0],
83
+ "dig_P4": struct.unpack_from("<h", calib1, 12)[0],
84
+ "dig_P5": struct.unpack_from("<h", calib1, 14)[0],
85
+ "dig_P6": struct.unpack_from("<h", calib1, 16)[0],
86
+ "dig_P7": struct.unpack_from("<h", calib1, 18)[0],
87
+ "dig_P8": struct.unpack_from("<h", calib1, 20)[0],
88
+ "dig_P9": struct.unpack_from("<h", calib1, 22)[0],
89
+ "dig_H1": calib1[25],
90
+ "dig_H2": struct.unpack_from("<h", calib2, 0)[0],
91
+ "dig_H3": calib2[2],
92
+ "dig_H4": (calib2[3] << 4) | (calib2[4] & 0x0F),
93
+ "dig_H5": (calib2[5] << 4) | (calib2[4] >> 4),
94
+ "dig_H6": struct.unpack_from("<b", calib2, 6)[0],
95
+ }
96
+ if self.calibration["dig_H4"] & 0x800:
97
+ self.calibration["dig_H4"] -= 4096
98
+ if self.calibration["dig_H5"] & 0x800:
99
+ self.calibration["dig_H5"] -= 4096
100
+ return self.calibration
101
+
102
+ def _ensure_calibration(self):
103
+ if self.calibration is None:
104
+ self.read_calibration()
105
+ return self.calibration
106
+
107
+ def read_raw_data(self):
108
+ data = self.bus.read_i2c_block_data(self.address, self.DATA_REGISTER, 8)
109
+ raw_press = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
110
+ raw_temp = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
111
+ raw_hum = (data[6] << 8) | data[7]
112
+ return {
113
+ "pressure": raw_press,
114
+ "temperature": raw_temp,
115
+ "humidity": raw_hum,
116
+ }
117
+
118
+ def compensate_temperature(self, adc_t):
119
+ cal = self._ensure_calibration()
120
+ var1 = (adc_t / 16384.0 - cal["dig_T1"] / 1024.0) * cal["dig_T2"]
121
+ var2 = (
122
+ (adc_t / 131072.0 - cal["dig_T1"] / 8192.0)
123
+ * (adc_t / 131072.0 - cal["dig_T1"] / 8192.0)
124
+ * cal["dig_T3"]
125
+ )
126
+ self.t_fine = var1 + var2
127
+ return self.t_fine / 5120.0
128
+
129
+ def compensate_pressure(self, adc_p):
130
+ cal = self._ensure_calibration()
131
+ var1 = self.t_fine / 2.0 - 64000.0
132
+ var2 = var1 * var1 * cal["dig_P6"] / 32768.0
133
+ var2 = var2 + var1 * cal["dig_P5"] * 2.0
134
+ var2 = var2 / 4.0 + cal["dig_P4"] * 65536.0
135
+ var1 = (
136
+ cal["dig_P3"] * var1 * var1 / 524288.0 + cal["dig_P2"] * var1
137
+ ) / 524288.0
138
+ var1 = (1.0 + var1 / 32768.0) * cal["dig_P1"]
139
+ if var1 == 0:
140
+ return 0.0
141
+
142
+ pressure = 1048576.0 - adc_p
143
+ pressure = (pressure - var2 / 4096.0) * 6250.0 / var1
144
+ var1 = cal["dig_P9"] * pressure * pressure / 2147483648.0
145
+ var2 = pressure * cal["dig_P8"] / 32768.0
146
+ pressure = pressure + (var1 + var2 + cal["dig_P7"]) / 16.0
147
+ return pressure / 100.0
148
+
149
+ def compensate_humidity(self, adc_h):
150
+ cal = self._ensure_calibration()
151
+ humidity = self.t_fine - 76800.0
152
+ humidity = (
153
+ adc_h
154
+ - (cal["dig_H4"] * 64.0 + cal["dig_H5"] / 16384.0 * humidity)
155
+ ) * (
156
+ cal["dig_H2"]
157
+ / 65536.0
158
+ * (
159
+ 1.0
160
+ + cal["dig_H6"] / 67108864.0 * humidity
161
+ * (1.0 + cal["dig_H3"] / 67108864.0 * humidity)
162
+ )
163
+ )
164
+ humidity = humidity * (1.0 - cal["dig_H1"] * humidity / 524288.0)
165
+ if humidity < 0.0:
166
+ return 0.0
167
+ if humidity > 100.0:
168
+ return 100.0
169
+ return humidity
170
+
171
+ def read_measurements(self):
172
+ raw = self.read_raw_data()
173
+ temperature_c = self.compensate_temperature(raw["temperature"])
174
+ pressure_hpa = self.compensate_pressure(raw["pressure"])
175
+ humidity_percent = self.compensate_humidity(raw["humidity"])
176
+ return {
177
+ "temperature_c": temperature_c,
178
+ "pressure_hpa": pressure_hpa,
179
+ "humidity_percent": humidity_percent,
180
+ "raw": raw,
181
+ }
dln2/gpio.py ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """DLN2 GPIO wrapper."""
3
+
4
+ from ._core import GPIO_EVENT_NONE, GPIO_EVENT_CHANGE, \
5
+ GPIO_EVENT_LEVEL_HIGH, GPIO_EVENT_LEVEL_LOW
6
+
7
+
8
+ class GPIO:
9
+ DIRECTION_IN = "in"
10
+ DIRECTION_OUT = "out"
11
+
12
+ def __init__(self, connection=None):
13
+ if connection is None:
14
+ from ._core import Dln2Connection
15
+ connection = Dln2Connection()
16
+ self._conn = connection
17
+
18
+ def get_pin_count(self):
19
+ return self._conn.gpio_get_pin_count()
20
+
21
+ def enable_pin(self, pin):
22
+ self._conn.gpio_pin_enable(pin)
23
+
24
+ def disable_pin(self, pin):
25
+ self._conn.gpio_pin_disable(pin)
26
+
27
+ def set_direction(self, pin, direction):
28
+ if direction not in (self.DIRECTION_IN, self.DIRECTION_OUT):
29
+ raise ValueError("direction must be 'in' or 'out'")
30
+ self._conn.gpio_pin_set_direction(pin, direction == self.DIRECTION_OUT)
31
+
32
+ def get_direction(self, pin):
33
+ is_out = self._conn.gpio_pin_get_direction(pin)
34
+ return self.DIRECTION_OUT if is_out else self.DIRECTION_IN
35
+
36
+ def read(self, pin):
37
+ return self._conn.gpio_get(pin)
38
+
39
+ def write(self, pin, value):
40
+ self._conn.gpio_set(pin, value)
41
+
42
+ def toggle(self, pin):
43
+ v = self.read_output(pin)
44
+ new = 0 if v else 1
45
+ self.write(pin, new)
46
+ return new
47
+
48
+ def read_output(self, pin):
49
+ return self._conn.gpio_get_out_val(pin)
50
+
51
+ def set_event(self, pin, event_type=GPIO_EVENT_CHANGE, period=0):
52
+ self._conn.gpio_set_event_cfg(pin, event_type, period)
53
+
54
+ def clear_event(self, pin):
55
+ self._conn.gpio_set_event_cfg(pin, GPIO_EVENT_NONE, 0)
56
+
57
+ def poll_event(self, timeout_ms=0):
58
+ return self._conn.gpio_poll_event(timeout_ms)
59
+
60
+ def __enter__(self):
61
+ return self
62
+
63
+ def __exit__(self, *a):
64
+ pass
dln2/i2c.py ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """DLN2 SMBus-compatible I2C wrapper."""
3
+
4
+
5
+ def _to_bytes(data):
6
+ if isinstance(data, bytes):
7
+ return data
8
+ if isinstance(data, bytearray):
9
+ return bytes(data)
10
+ return bytes(int(v) & 0xFF for v in data)
11
+
12
+
13
+ class i2c_msg:
14
+ I2C_M_RD = 0x0001
15
+
16
+ def __init__(self, addr, flags, data=b"", length=0):
17
+ self.addr = int(addr) & 0x7F
18
+ self.flags = int(flags)
19
+ self._data = bytes(data)
20
+ self._length = int(length)
21
+
22
+ @classmethod
23
+ def write(cls, addr, data):
24
+ payload = _to_bytes(data)
25
+ return cls(addr, 0, data=payload, length=len(payload))
26
+
27
+ @classmethod
28
+ def read(cls, addr, length):
29
+ return cls(addr, cls.I2C_M_RD, length=length)
30
+
31
+ @property
32
+ def is_read(self):
33
+ return bool(self.flags & self.I2C_M_RD)
34
+
35
+ @property
36
+ def len(self):
37
+ return self._length if self.is_read else len(self._data)
38
+
39
+ @property
40
+ def buf(self):
41
+ return self._data
42
+
43
+ def set_data(self, data):
44
+ self._data = bytes(data)
45
+
46
+
47
+ class SMBus:
48
+ """SMBus-compatible I2C API backed by Dln2Connection."""
49
+
50
+ def __init__(self, connection=None):
51
+ if connection is None:
52
+ from ._core import Dln2Connection
53
+ connection = Dln2Connection()
54
+ self._conn = connection
55
+ self._opened = False
56
+
57
+ def open(self, bus=0):
58
+ if self._opened:
59
+ return
60
+ self._conn.i2c_enable()
61
+ self._opened = True
62
+
63
+ def close(self):
64
+ if not self._opened:
65
+ return
66
+ try:
67
+ self._conn.i2c_disable()
68
+ except Exception:
69
+ pass
70
+ self._opened = False
71
+
72
+ def _require(self):
73
+ if not self._opened:
74
+ self.open()
75
+ return self._conn
76
+
77
+ def enable(self, bus=0):
78
+ self.open(bus)
79
+
80
+ # ── SMBus API ──────────────────────────────────────
81
+
82
+ def write_byte(self, addr, value):
83
+ self._require().i2c_write(addr, [value])
84
+
85
+ def write_byte_data(self, addr, cmd, value):
86
+ self._require().i2c_write(addr, [cmd, value])
87
+
88
+ def write_word_data(self, addr, cmd, value):
89
+ data = [(cmd & 0xFF), (value & 0xFF), ((value >> 8) & 0xFF)]
90
+ self._require().i2c_write(addr, data)
91
+
92
+ def write_block_data(self, addr, cmd, data):
93
+ payload = [cmd] + list(_to_bytes(data))
94
+ self._require().i2c_write(addr, payload)
95
+
96
+ def write_i2c_block_data(self, addr, cmd, data):
97
+ self.write_block_data(addr, cmd, data)
98
+
99
+ def read_byte(self, addr):
100
+ return self._require().i2c_read(addr, 1)[0]
101
+
102
+ def read_byte_data(self, addr, cmd):
103
+ return self._require().i2c_read(addr, 1, mem_addr_len=1,
104
+ mem_addr=cmd)[0]
105
+
106
+ def read_word_data(self, addr, cmd):
107
+ rx = self._require().i2c_read(addr, 2, mem_addr_len=1, mem_addr=cmd)
108
+ return rx[0] | (rx[1] << 8)
109
+
110
+ def read_block_data(self, addr, cmd):
111
+ # Read count byte first, then data
112
+ count = self._require().i2c_read(addr, 1, mem_addr_len=1,
113
+ mem_addr=cmd)[0]
114
+ if count == 0:
115
+ return []
116
+ return list(self._require().i2c_read(addr, count))
117
+
118
+ def read_i2c_block_data(self, addr, cmd, length=32):
119
+ return list(self._require().i2c_read(addr, length,
120
+ mem_addr_len=1, mem_addr=cmd))
121
+
122
+ # ── Raw access ─────────────────────────────────────
123
+
124
+ def write(self, addr, data):
125
+ self._require().i2c_write(addr, data)
126
+
127
+ def read(self, addr, length):
128
+ return self._require().i2c_read(addr, length)
129
+
130
+ def __enter__(self):
131
+ self.open()
132
+ return self
133
+
134
+ def __exit__(self, *a):
135
+ self.close()
dln2/py.typed ADDED
File without changes
dln2/spi.py ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """DLN2 SpiDev-compatible SPI wrapper."""
3
+
4
+ import struct
5
+ import time
6
+
7
+
8
+ class SpiDev:
9
+ """spidev-compatible API backed by a shared Dln2Connection."""
10
+
11
+ def __init__(self, connection=None):
12
+ if connection is None:
13
+ from ._core import Dln2Connection
14
+ connection = Dln2Connection()
15
+ self._conn = connection
16
+ self.debug = False
17
+ self._max_speed_hz = 1_000_000
18
+ self._mode = 0
19
+ self._bits_per_word = 8
20
+ self.cshigh = False
21
+ self.lsbfirst = False
22
+ self.host_hold_cs = False
23
+
24
+ @property
25
+ def max_speed_hz(self):
26
+ return self._max_speed_hz
27
+
28
+ @max_speed_hz.setter
29
+ def max_speed_hz(self, v):
30
+ self._max_speed_hz = int(v)
31
+
32
+ @property
33
+ def mode(self):
34
+ return self._mode
35
+
36
+ @mode.setter
37
+ def mode(self, v):
38
+ self._mode = int(v) & 3
39
+
40
+ @property
41
+ def bits_per_word(self):
42
+ return self._bits_per_word
43
+
44
+ @bits_per_word.setter
45
+ def bits_per_word(self, v):
46
+ self._bits_per_word = int(v)
47
+
48
+ def open(self, bus=0, device=0):
49
+ """No-op — connection already open. For spidev compatibility."""
50
+ return self
51
+
52
+ def close(self):
53
+ pass # connection is shared, caller manages _conn.close()
54
+
55
+ def xfer2(self, data):
56
+ """Full-duplex transfer. Returns MISO data."""
57
+ bpw = self._bits_per_word
58
+ if bpw <= 8:
59
+ tx = bytes(b & 0xFF for b in data)
60
+ else:
61
+ out = bytearray()
62
+ for w in data:
63
+ out += struct.pack("<H", int(w) & 0xFFFF)
64
+ tx = bytes(out)
65
+ return list(self._conn.spi_read_write(
66
+ tx, leave_ss_low=self.host_hold_cs))
67
+
68
+ def xfer(self, data):
69
+ return self.xfer2(data)
70
+
71
+ def writebytes(self, data):
72
+ self.xfer2(data)
73
+
74
+ def readbytes(self, length):
75
+ return self.xfer2([0] * length)
76
+
77
+ def __enter__(self):
78
+ return self
79
+
80
+ def __exit__(self, *a):
81
+ pass
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 IPM Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: dln2
3
+ Version: 0.1.0
4
+ Summary: Unified DLN2 driver — SPI, GPIO, I2C, ADC over a single USB connection. Originally developed for the Pico USB I/O Board.
5
+ Home-page: https://github.com/syabyr/dln2_wrapper
6
+ Author: IPM Group
7
+ Author-email: contact@ipmgroup.dev
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: System :: Hardware
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: pyusb >=1.2
24
+
25
+ # dln2 — Unified DLN2 USB I/O Board Wrapper
26
+
27
+ Drop-in replacement for the four independent `dln2_*_wrapper` packages.
28
+ **All modules share a single USB connection** — no echo-counter conflicts, no USB contention.
29
+
30
+ Originally developed for the [Pico USB I/O Board](https://github.com/syabyr/pico-usb-io-board).
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install git+https://github.com/syabyr/dln2_wrapper
36
+ ```
37
+
38
+ Requires: `pyusb`
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from dln2 import Dln2Connection, SpiDev, GPIO, SMBus, ADC
44
+
45
+ conn = Dln2Connection() # one USB connection
46
+
47
+ # ── SPI ─────────────────────────────────────────────────
48
+ spi = SpiDev(conn)
49
+ spi.mode = 3
50
+ spi.max_speed_hz = 16_000_000
51
+ rx = spi.xfer2([0x9F, 0x00, 0x00, 0x00]) # JEDEC ID
52
+
53
+ # ── GPIO ────────────────────────────────────────────────
54
+ gpio = GPIO(conn)
55
+ gpio.set_direction(20, "out")
56
+ gpio.write(20, 1) # set pin HIGH
57
+ val = gpio.read(20) # read pin level
58
+
59
+ # ── I2C ─────────────────────────────────────────────────
60
+ i2c = SMBus(conn)
61
+ i2c.open()
62
+ devices = []
63
+ for addr in range(1, 128):
64
+ try:
65
+ i2c.write_byte(addr, 0)
66
+ devices.append(addr)
67
+ except: pass
68
+
69
+ # ── ADC ─────────────────────────────────────────────────
70
+ adc = ADC(conn, vref=3.3)
71
+ volts = adc.read_volts(0) # read channel 0 voltage
72
+
73
+ conn.close()
74
+ ```
75
+
76
+ ## Multi-Device Support
77
+
78
+ ```python
79
+ from dln2 import list_devices, Dln2Connection
80
+
81
+ for d in list_devices():
82
+ print(f"[{d.index}] serial={d.serial}")
83
+
84
+ lcd_conn = Dln2Connection(index=0) # first board — ST7789 display
85
+ nfc_conn = Dln2Connection(index=1) # second board — NFC reader
86
+ # or by exact serial:
87
+ # conn = Dln2Connection(serial="4250304B38353817")
88
+ ```
89
+
90
+ ## Module API
91
+
92
+ ### SpiDev
93
+ spidev-compatible API: `xfer()`, `xfer2()`, `writebytes()`, `readbytes()`,
94
+ `mode`, `max_speed_hz`, `bits_per_word`, `cshigh`, `lsbfirst`, `host_hold_cs`
95
+
96
+ ### GPIO
97
+ `set_direction(pin, "in"|"out")`, `write(pin, value)`, `read(pin)`,
98
+ `toggle(pin)`, `get_direction(pin)`, `set_event(pin, type)`, `poll_event()`
99
+
100
+ ### SMBus
101
+ smbus-compatible API: `write_byte()`, `read_byte_data()`,
102
+ `write_word_data()`, `read_word_data()`, `read_block_data()`,
103
+ `write_block_data()`, `write_i2c_block_data()`, `read_i2c_block_data()`
104
+
105
+ ### ADC
106
+ `read_channel(channel)`, `read_all()`, `read_volts(channel)`,
107
+ `set_resolution(bits)`, `enable_channel(ch)`, `disable_channel(ch)`
108
+
109
+ ### BME280
110
+ `read_chip_id()`, `get_temperature()`, `get_humidity()`, `get_pressure()`,
111
+ `get_altitude()`, `get_all()`
112
+
113
+ ## Examples
114
+
115
+ ```bash
116
+ # SPI JEDEC ID read
117
+ python3 examples/spidev_test.py
118
+
119
+ # GPIO pin toggle (default: Pico LED)
120
+ python3 examples/gpio_toggle.py --pin 25
121
+
122
+ # I2C bus scan
123
+ python3 examples/i2c_scan.py
124
+
125
+ # ADC monitor
126
+ python3 examples/adc_info.py
127
+ ```
128
+
129
+ ## Why Unified?
130
+
131
+ | | Old (4 packages) | New (`dln2`) |
132
+ |---|---|---|
133
+ | USB connections | 4 separate (echo conflicts) | 1 shared |
134
+ | Code duplication | ~800 lines (4× Dln2Usb) | 0 lines |
135
+ | GPIO events steal SPI responses | Yes | No (proper event queue) |
136
+ | Multi-device | Manual USB enumeration | `list_devices()` |
137
+ | Cross-module data flow | Impossible (4 echo counters) | Trivial (same counter) |
138
+
139
+ ## Related
140
+
141
+ - [pico-usb-io-board](https://github.com/syabyr/pico-usb-io-board) — RP2040 firmware
142
+ - [st7789 display driver](https://github.com/syabyr/dln2_wrapper) — example ST7789 + BME280 application
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,13 @@
1
+ dln2/__init__.py,sha256=Zmp033q4GLszZLBp7k1rdB15rYDx_Rw2F9FzjHBErxs,817
2
+ dln2/_core.py,sha256=7ARFQlst4oAGVT0Yzr41j8v_aujW0eWRkOYMfLU2Bps,22198
3
+ dln2/adc.py,sha256=k8IQSg5Uu0x46kCQoFuVNzZEnvmQ47aISXx_Xic9tVs,2661
4
+ dln2/bme280.py,sha256=Ju2le83bMulC0RG5GKyjPNH5jGNmXI_ApJlsJPqafaY,6515
5
+ dln2/gpio.py,sha256=3zZch84enYorumuELri556Y8E-TK1QBryLX2JuW2Gss,1823
6
+ dln2/i2c.py,sha256=_7Saeg04Aeg_elg_ouaVroj06ra-fL2DUdEhF5mRNPY,3899
7
+ dln2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ dln2/spi.py,sha256=-tNn5q1NNcCde3JCed9yyg_r7JI9Z84f1A08KQb5VF8,1999
9
+ dln2-0.1.0.dist-info/LICENSE,sha256=R657Wi0nj-QnCHv5kUQ2_yE1mNwXLNsM5wWQwPE5cas,1066
10
+ dln2-0.1.0.dist-info/METADATA,sha256=FJoHLHPp7gb4I__wf9VNLczbekWBkzus8d_L4ISw7gs,4731
11
+ dln2-0.1.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
12
+ dln2-0.1.0.dist-info/top_level.txt,sha256=0_zQFVfHajItivvqoMkbM1HGutiVazNrNd6AjkKaToQ,5
13
+ dln2-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ dln2