periph 1.2.1__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.
Files changed (108) hide show
  1. periph/__init__.py +1 -0
  2. periph/chips/accelerometer/adxl345.py +524 -0
  3. periph/chips/accelerometer/adxl362.py +704 -0
  4. periph/chips/adc_dac/ad7705.py +476 -0
  5. periph/chips/adc_dac/ad7706.py +495 -0
  6. periph/chips/adc_dac/hx710a.py +187 -0
  7. periph/chips/adc_dac/hx710b.py +189 -0
  8. periph/chips/adc_dac/hx711.py +170 -0
  9. periph/chips/adc_dac/mcp4725.py +167 -0
  10. periph/chips/adc_dac/mcp4728.py +317 -0
  11. periph/chips/adc_dac/pcf8591.py +199 -0
  12. periph/chips/comms/mcp2515.py +651 -0
  13. periph/chips/comms/rda5807m.py +420 -0
  14. periph/chips/comms/rfm9x.py +594 -0
  15. periph/chips/display/pcf8576.py +223 -0
  16. periph/chips/environmental/aht21.py +150 -0
  17. periph/chips/environmental/bme280.py +384 -0
  18. periph/chips/environmental/bme680.py +501 -0
  19. periph/chips/gas/ens160.py +273 -0
  20. periph/chips/gnss/neo6.py +439 -0
  21. periph/chips/gyroscope/l3g4200d.py +448 -0
  22. periph/chips/gyroscope/l3gd20h.py +337 -0
  23. periph/chips/humidity/dht11.py +123 -0
  24. periph/chips/imu/mpu6050.py +251 -0
  25. periph/chips/imu/mpu9250.py +378 -0
  26. periph/chips/imu/mpu9255.py +434 -0
  27. periph/chips/io_expander/mcp23017.py +494 -0
  28. periph/chips/io_expander/pcf8574.py +401 -0
  29. periph/chips/io_expander/pcf8575.py +420 -0
  30. periph/chips/io_expander/tpic6b595.py +271 -0
  31. periph/chips/led/_color.py +23 -0
  32. periph/chips/led/_neopixel_rgb_base.py +175 -0
  33. periph/chips/led/_neopixel_rgbw_base.py +193 -0
  34. periph/chips/led/apa102.py +200 -0
  35. periph/chips/led/sk6812rgbw.py +52 -0
  36. periph/chips/led/ws2812b.py +46 -0
  37. periph/chips/led/ws2814.py +52 -0
  38. periph/chips/light/apds9960.py +496 -0
  39. periph/chips/light/apds_9930.py +364 -0
  40. periph/chips/magnetometer/as5600.py +290 -0
  41. periph/chips/magnetometer/hmc5883l.py +210 -0
  42. periph/chips/memory/_24aa02uid.py +175 -0
  43. periph/chips/motor/drv8830.py +236 -0
  44. periph/chips/other/mpr121.py +465 -0
  45. periph/chips/power/ade7953.py +1196 -0
  46. periph/chips/power/ina219.py +195 -0
  47. periph/chips/power/ina226.py +217 -0
  48. periph/chips/power/ina3221.py +359 -0
  49. periph/chips/pressure/bmp085.py +234 -0
  50. periph/chips/pressure/bmp180.py +234 -0
  51. periph/chips/pressure/bmp280.py +292 -0
  52. periph/chips/pressure/bmp384.py +471 -0
  53. periph/chips/pressure/bmp581.py +561 -0
  54. periph/chips/pressure/lps22df.py +394 -0
  55. periph/chips/pressure/lps28dfw.py +337 -0
  56. periph/chips/pressure/lps33hw.py +385 -0
  57. periph/chips/rfid/mfrc522.py +818 -0
  58. periph/chips/rtc/ds3231.py +522 -0
  59. periph/chips/rtc/pcf8523.py +549 -0
  60. periph/chips/temperature/mcp9808.py +453 -0
  61. periph/chips/temperature/tmp117.py +461 -0
  62. periph/chips/tof/_vl53_base.py +174 -0
  63. periph/chips/tof/vl53l0x.py +816 -0
  64. periph/chips/tof/vl53l1x.py +815 -0
  65. periph/connection/__init__.py +31 -0
  66. periph/connection/base.py +103 -0
  67. periph/connection/dhtxx_auto.py +52 -0
  68. periph/connection/dhtxx_circuitpython.py +138 -0
  69. periph/connection/dhtxx_linux.py +230 -0
  70. periph/connection/dhtxx_micropython.py +133 -0
  71. periph/connection/dhtxx_mock.py +34 -0
  72. periph/connection/hx711_auto.py +66 -0
  73. periph/connection/hx711_circuitpython.py +114 -0
  74. periph/connection/hx711_linux.py +121 -0
  75. periph/connection/hx711_micropython.py +113 -0
  76. periph/connection/hx711_mock.py +43 -0
  77. periph/connection/i2c_auto.py +54 -0
  78. periph/connection/i2c_circuitpython.py +84 -0
  79. periph/connection/i2c_linux.py +81 -0
  80. periph/connection/i2c_micropython.py +65 -0
  81. periph/connection/i2c_mock.py +57 -0
  82. periph/connection/input_pin.py +259 -0
  83. periph/connection/neo6_mock.py +36 -0
  84. periph/connection/neopixel_auto.py +75 -0
  85. periph/connection/neopixel_circuitpython.py +44 -0
  86. periph/connection/neopixel_linux.py +47 -0
  87. periph/connection/neopixel_micropython.py +38 -0
  88. periph/connection/output_pin.py +81 -0
  89. periph/connection/sipo_circuitpython.py +119 -0
  90. periph/connection/sipo_linux.py +149 -0
  91. periph/connection/sipo_micropython.py +104 -0
  92. periph/connection/smbus_auto.py +68 -0
  93. periph/connection/smbus_circuitpython.py +118 -0
  94. periph/connection/smbus_linux.py +119 -0
  95. periph/connection/smbus_micropython.py +100 -0
  96. periph/connection/spi_auto.py +79 -0
  97. periph/connection/spi_circuitpython.py +97 -0
  98. periph/connection/spi_linux.py +66 -0
  99. periph/connection/spi_micropython.py +64 -0
  100. periph/connection/spi_mock.py +96 -0
  101. periph/connection/uart_auto.py +76 -0
  102. periph/connection/uart_circuitpython.py +81 -0
  103. periph/connection/uart_linux.py +120 -0
  104. periph/connection/uart_micropython.py +81 -0
  105. periph-1.2.1.dist-info/METADATA +113 -0
  106. periph-1.2.1.dist-info/RECORD +108 -0
  107. periph-1.2.1.dist-info/WHEEL +5 -0
  108. periph-1.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,461 @@
1
+ """TMP117 — ±0.1°C high-accuracy, low-power digital temperature sensor (Texas Instruments).
2
+
3
+ NIST-traceable 16-bit temperature sensor (0.0078125°C per LSB) read over an
4
+ I2C/SMBus-compatible two-wire bus. Offers continuous, one-shot, and shutdown
5
+ conversion modes with selectable averaging and cycle time, a window-alert /
6
+ latching-Therm / Data-Ready ALERT output, and EEPROM persistence of its
7
+ configuration, limit, and offset registers plus general-purpose scratch
8
+ storage. Four selectable addresses (0x48-0x4B) via the 4-level ADD0 strap. The
9
+ same driver file is used on MicroPython, CircuitPython, and Linux hosts.
10
+
11
+ Registers are 16-bit, big-endian, addressed through a non-incrementing
12
+ Register Pointer.
13
+
14
+ Args:
15
+ connection: Configured I2C connection pointing at the device
16
+ (0x48-0x4B, per the board's ADD0 strapping).
17
+ """
18
+
19
+ try:
20
+ import threading as _threading
21
+ _LINUX = True
22
+ except ImportError:
23
+ _LINUX = False
24
+
25
+ import time
26
+
27
+
28
+ I2C_ADDRESS = 0x48
29
+
30
+ DEVICE_ID = 0x117
31
+
32
+ # Interrupt sources — CONFIGURATION's HIGH_Alert / LOW_Alert flags.
33
+ SOURCE_HIGH = 0x01
34
+ SOURCE_LOW = 0x02
35
+
36
+ # Conversion cycle times in seconds, indexed by CONV[2:0] (no-averaging column).
37
+ _CYCLES = (0.0155, 0.125, 0.25, 0.5, 1.0, 4.0, 8.0, 16.0)
38
+
39
+ # Averaging counts, indexed by AVG[1:0].
40
+ _AVERAGINGS = (0, 8, 32, 64)
41
+
42
+ # Conversion modes, indexed by MOD[1:0] (0b10 reads back as continuous).
43
+ _MODES = ('continuous', 'shutdown', 'continuous', 'one_shot')
44
+
45
+ # EEPROM scratch slot -> register address.
46
+ _SCRATCH_REGS = {1: 0x05, 2: 0x06, 3: 0x08}
47
+
48
+ _LSB_C = 0.0078125
49
+
50
+
51
+ def _decode_temperature(raw16):
52
+ raw = raw16 - 0x10000 if raw16 & 0x8000 else raw16
53
+ return raw * _LSB_C
54
+
55
+
56
+ def _encode_temperature(celsius):
57
+ steps = celsius / _LSB_C
58
+ steps = int(steps + 0.5) if steps >= 0 else -int(-steps + 0.5)
59
+ if steps < -32768:
60
+ steps = -32768
61
+ elif steps > 32767:
62
+ steps = 32767
63
+ return steps & 0xFFFF
64
+
65
+
66
+ class TMP117Minimal:
67
+ """TMP117 ±0.1°C high-accuracy digital temperature sensor — minimal interface.
68
+
69
+ Reads temperature. No register writes at construction: the POR/EEPROM
70
+ default (continuous conversion, 8-conversion averaging, 1 s cycle, Alert
71
+ mode) already serves the primary use case.
72
+
73
+ Args:
74
+ connection: Configured I2C connection pointing at the device.
75
+
76
+ Raises:
77
+ ValueError: If the DEVICE_ID register does not identify a TMP117.
78
+ """
79
+
80
+ _REG_TEMP_RESULT = 0x00
81
+ _REG_CONFIG = 0x01
82
+ _REG_THIGH = 0x02
83
+ _REG_TLOW = 0x03
84
+ _REG_EEPROM_UL = 0x04
85
+ _REG_TEMP_OFFSET = 0x07
86
+ _REG_DEVICE_ID = 0x0F
87
+
88
+ def __init__(self, connection):
89
+ self._connection = connection
90
+ did = self._read_reg(self._REG_DEVICE_ID) & 0x0FFF
91
+ if did != DEVICE_ID:
92
+ raise ValueError('TMP117 not found: expected DEVICE_ID 0x{:03X}, got 0x{:03X}'.format(
93
+ DEVICE_ID, did))
94
+
95
+ def _read_reg(self, reg):
96
+ data = self._connection.write_read(bytes([reg]), 2)
97
+ return (data[0] << 8) | data[1]
98
+
99
+ def _write_reg(self, reg, value):
100
+ self._connection.write(bytes([reg, (value >> 8) & 0xFF, value & 0xFF]))
101
+
102
+ def read_temperature(self):
103
+ """Read the temperature.
104
+
105
+ Decodes TEMP_RESULT's 16-bit two's-complement value (0.0078125°C per
106
+ LSB). Returns -256.0 until the first conversion after power-up
107
+ completes.
108
+
109
+ Returns:
110
+ float: Temperature in °C.
111
+ """
112
+ return _decode_temperature(self._read_reg(self._REG_TEMP_RESULT))
113
+
114
+
115
+ class TMP117Full(TMP117Minimal):
116
+ """TMP117 full interface — extends Minimal with conversion mode, averaging,
117
+ and cycle-time control, one-shot triggering, both temperature limits, the
118
+ calibration offset, soft reset, EEPROM persistence and scratch storage,
119
+ and the Level-2 Alert/interrupt API.
120
+
121
+ Args:
122
+ connection: Configured I2C connection pointing at the device.
123
+
124
+ Raises:
125
+ ValueError: If the DEVICE_ID register does not identify a TMP117.
126
+ """
127
+
128
+ # CONFIGURATION (0x01) bits.
129
+ _CFG_HIGH_ALERT = 0x8000
130
+ _CFG_LOW_ALERT = 0x4000
131
+ _CFG_DATA_READY = 0x2000
132
+ _CFG_EEPROM_BUSY = 0x1000
133
+ _CFG_MOD_SHIFT = 10
134
+ _CFG_MOD_MASK = 0x0C00
135
+ _CFG_CONV_SHIFT = 7
136
+ _CFG_CONV_MASK = 0x0380
137
+ _CFG_AVG_SHIFT = 5
138
+ _CFG_AVG_MASK = 0x0060
139
+ _CFG_TNA = 0x0010
140
+ _CFG_POL = 0x0008
141
+ _CFG_DR_ALERT = 0x0004
142
+ _CFG_SOFT_RESET = 0x0002
143
+ # Writable bits: MOD/CONV/AVG/T-nA/POL/DR-Alert. Soft_Reset is set only on purpose.
144
+ _CFG_WRITE_MASK = 0x0FFC
145
+
146
+ _MOD_ONE_SHOT = 0x03
147
+
148
+ # EEPROM_UL (0x04) bits.
149
+ _EUN = 0x8000
150
+ _EEPROM_BUSY = 0x4000
151
+
152
+ def __init__(self, connection):
153
+ super().__init__(connection)
154
+ self._callback = None
155
+ self._int_pin_used = None
156
+ self._poll_stop = False
157
+ self._poll_thread = None
158
+ self._last_status = 0
159
+
160
+ def _read_config(self):
161
+ return self._read_reg(self._REG_CONFIG) & self._CFG_WRITE_MASK
162
+
163
+ def _write_config(self, value):
164
+ self._write_reg(self._REG_CONFIG, value & self._CFG_WRITE_MASK)
165
+
166
+ # --- Conversion -------------------------------------------------------
167
+
168
+ def configure(self, mode='continuous', averaging=8, cycle_seconds=1.0):
169
+ """Set conversion mode, averaging, and cycle time.
170
+
171
+ The cycle time is matched to the nearest CONV[2:0] step from the
172
+ no-averaging column (15.5 ms, 125 ms, 250 ms, 500 ms, 1 s, 4 s, 8 s,
173
+ 16 s); at higher averaging the hardware lengthens short cycles
174
+ automatically. The Alert configuration bits are preserved.
175
+
176
+ Args:
177
+ mode: 'continuous', 'shutdown', or 'one_shot'.
178
+ averaging: Conversions averaged per result: 0, 8, 32, or 64.
179
+ cycle_seconds: Desired conversion cycle time in s.
180
+
181
+ Raises:
182
+ ValueError: If mode or averaging is not one of the listed values.
183
+ """
184
+ if mode == 'continuous':
185
+ mod = 0x00
186
+ elif mode == 'shutdown':
187
+ mod = 0x01
188
+ elif mode == 'one_shot':
189
+ mod = self._MOD_ONE_SHOT
190
+ else:
191
+ raise ValueError("mode must be 'continuous', 'shutdown', or 'one_shot'")
192
+ if averaging not in _AVERAGINGS:
193
+ raise ValueError('averaging must be one of 0, 8, 32, 64')
194
+ avg = _AVERAGINGS.index(averaging)
195
+ conv = 0
196
+ for code, cycle in enumerate(_CYCLES):
197
+ if abs(cycle - cycle_seconds) < abs(_CYCLES[conv] - cycle_seconds):
198
+ conv = code
199
+ config = self._read_config() & ~(self._CFG_MOD_MASK | self._CFG_CONV_MASK | self._CFG_AVG_MASK)
200
+ config |= (mod << self._CFG_MOD_SHIFT) | (conv << self._CFG_CONV_SHIFT) | (avg << self._CFG_AVG_SHIFT)
201
+ self._write_config(config)
202
+
203
+ def get_config(self):
204
+ """Read the conversion mode, averaging, and cycle time.
205
+
206
+ Returns:
207
+ tuple: (mode: str, averaging: int, cycle_seconds: float), where
208
+ cycle_seconds is the no-averaging CONV[2:0] step.
209
+ """
210
+ config = self._read_reg(self._REG_CONFIG)
211
+ mode = _MODES[(config & self._CFG_MOD_MASK) >> self._CFG_MOD_SHIFT]
212
+ averaging = _AVERAGINGS[(config & self._CFG_AVG_MASK) >> self._CFG_AVG_SHIFT]
213
+ cycle = _CYCLES[(config & self._CFG_CONV_MASK) >> self._CFG_CONV_SHIFT]
214
+ return (mode, averaging, cycle)
215
+
216
+ def is_shutdown(self):
217
+ """Report whether the sensor is in Shutdown mode.
218
+
219
+ Returns:
220
+ bool: True if MOD[1:0] is Shutdown.
221
+ """
222
+ return (self._read_reg(self._REG_CONFIG) & self._CFG_MOD_MASK) >> self._CFG_MOD_SHIFT == 0x01
223
+
224
+ def trigger_one_shot(self):
225
+ """Start a single conversion (MOD[1:0]=One-Shot).
226
+
227
+ The sensor returns to Shutdown once the conversion (including
228
+ averaging) completes.
229
+ """
230
+ config = self._read_config() & ~self._CFG_MOD_MASK
231
+ self._write_config(config | (self._MOD_ONE_SHOT << self._CFG_MOD_SHIFT))
232
+
233
+ def is_data_ready(self):
234
+ """Report whether a fresh conversion result is available.
235
+
236
+ Reading this flag clears it (as does reading TEMP_RESULT).
237
+
238
+ Returns:
239
+ bool: True if Data_Ready is set.
240
+ """
241
+ return bool(self._read_reg(self._REG_CONFIG) & self._CFG_DATA_READY)
242
+
243
+ # --- Limits and offset ------------------------------------------------
244
+
245
+ def get_high_limit(self):
246
+ """Read THIGH_LIMIT.
247
+
248
+ Returns:
249
+ float: High limit in °C.
250
+ """
251
+ return _decode_temperature(self._read_reg(self._REG_THIGH))
252
+
253
+ def set_high_limit(self, celsius):
254
+ """Write THIGH_LIMIT, rounded to the nearest 0.0078125°C.
255
+
256
+ Args:
257
+ celsius: High limit in °C (-256.0 to 255.9921875, clamped).
258
+ """
259
+ self._write_reg(self._REG_THIGH, _encode_temperature(celsius))
260
+
261
+ def get_low_limit(self):
262
+ """Read TLOW_LIMIT.
263
+
264
+ Returns:
265
+ float: Low limit in °C.
266
+ """
267
+ return _decode_temperature(self._read_reg(self._REG_TLOW))
268
+
269
+ def set_low_limit(self, celsius):
270
+ """Write TLOW_LIMIT, rounded to the nearest 0.0078125°C.
271
+
272
+ In Therm mode this is HIGH_Alert's reset threshold (hysteresis).
273
+
274
+ Args:
275
+ celsius: Low limit in °C (-256.0 to 255.9921875, clamped).
276
+ """
277
+ self._write_reg(self._REG_TLOW, _encode_temperature(celsius))
278
+
279
+ def get_temperature_offset(self):
280
+ """Read TEMP_OFFSET.
281
+
282
+ Returns:
283
+ float: Calibration offset in °C.
284
+ """
285
+ return _decode_temperature(self._read_reg(self._REG_TEMP_OFFSET))
286
+
287
+ def set_temperature_offset(self, celsius):
288
+ """Write TEMP_OFFSET, added to every result after linearization.
289
+
290
+ Args:
291
+ celsius: Calibration offset in °C (-256.0 to 255.9921875, clamped).
292
+ """
293
+ self._write_reg(self._REG_TEMP_OFFSET, _encode_temperature(celsius))
294
+
295
+ # --- Reset ------------------------------------------------------------
296
+
297
+ def reset(self):
298
+ """Software reset (Soft_Reset=1), then wait the 2 ms reset time.
299
+
300
+ Reloads CONFIGURATION, THIGH_LIMIT, TLOW_LIMIT, and TEMP_OFFSET from
301
+ EEPROM.
302
+ """
303
+ self._write_reg(self._REG_CONFIG, self._CFG_SOFT_RESET)
304
+ time.sleep(0.002)
305
+
306
+ # --- EEPROM -----------------------------------------------------------
307
+
308
+ def unlock_eeprom(self):
309
+ """Unlock the EEPROM (EUN=1).
310
+
311
+ While unlocked, writes to CONFIGURATION, THIGH_LIMIT, TLOW_LIMIT,
312
+ TEMP_OFFSET, and EEPROM2 also program the EEPROM as the new power-on
313
+ default. Poll is_eeprom_busy() after each such write.
314
+ """
315
+ self._write_reg(self._REG_EEPROM_UL, self._EUN)
316
+
317
+ def lock_eeprom(self):
318
+ """Lock the EEPROM (EUN=0); register writes become volatile only."""
319
+ self._write_reg(self._REG_EEPROM_UL, 0x0000)
320
+
321
+ def is_eeprom_busy(self):
322
+ """Report whether an EEPROM programming operation is in progress.
323
+
324
+ Returns:
325
+ bool: True if EEPROM_Busy is set.
326
+ """
327
+ return bool(self._read_reg(self._REG_EEPROM_UL) & self._EEPROM_BUSY)
328
+
329
+ def read_eeprom_scratch(self, slot):
330
+ """Read a general-purpose EEPROM scratch register.
331
+
332
+ Args:
333
+ slot: 1 (EEPROM1), 2 (EEPROM2), or 3 (EEPROM3). Slots 1 and 3
334
+ hold factory NIST-traceability data.
335
+
336
+ Returns:
337
+ int: 16-bit register value.
338
+
339
+ Raises:
340
+ ValueError: If slot is not 1, 2, or 3.
341
+ """
342
+ if slot not in _SCRATCH_REGS:
343
+ raise ValueError('slot must be 1, 2, or 3')
344
+ return self._read_reg(_SCRATCH_REGS[slot])
345
+
346
+ def write_eeprom_scratch(self, slot, value):
347
+ """Write the general-purpose EEPROM2 scratch register.
348
+
349
+ Only slot 2 is writable — EEPROM1/EEPROM3 hold factory
350
+ NIST-traceability data. Persists across power cycles only while the
351
+ EEPROM is unlocked.
352
+
353
+ Args:
354
+ slot: Must be 2.
355
+ value: 16-bit value.
356
+
357
+ Raises:
358
+ ValueError: If slot is not 2.
359
+ """
360
+ if slot != 2:
361
+ raise ValueError('only EEPROM scratch slot 2 is writable')
362
+ self._write_reg(_SCRATCH_REGS[2], value & 0xFFFF)
363
+
364
+ # --- Alert output -----------------------------------------------------
365
+
366
+ def configure_alert(self, mode='alert', polarity='active_low', pin_function='alert'):
367
+ """Configure the ALERT output's mode, polarity, and pin function.
368
+
369
+ Args:
370
+ mode: 'alert' (window alert) or 'therm' (latching thermostat;
371
+ TLOW_LIMIT becomes the reset threshold).
372
+ polarity: 'active_low' (needs pull-up) or 'active_high'.
373
+ pin_function: 'alert' (alert/Therm status) or 'data_ready'.
374
+
375
+ Raises:
376
+ ValueError: If an argument is not one of the listed values.
377
+ """
378
+ if mode not in ('alert', 'therm'):
379
+ raise ValueError("mode must be 'alert' or 'therm'")
380
+ if polarity not in ('active_low', 'active_high'):
381
+ raise ValueError("polarity must be 'active_low' or 'active_high'")
382
+ if pin_function not in ('alert', 'data_ready'):
383
+ raise ValueError("pin_function must be 'alert' or 'data_ready'")
384
+ config = self._read_config() & ~(self._CFG_TNA | self._CFG_POL | self._CFG_DR_ALERT)
385
+ if mode == 'therm':
386
+ config |= self._CFG_TNA
387
+ if polarity == 'active_high':
388
+ config |= self._CFG_POL
389
+ if pin_function == 'data_ready':
390
+ config |= self._CFG_DR_ALERT
391
+ self._write_config(config)
392
+
393
+ # --- Interrupt API ----------------------------------------------------
394
+
395
+ def poll_interrupt(self):
396
+ """Read CONFIGURATION's HIGH_Alert / LOW_Alert flags.
397
+
398
+ In Alert mode this read also clears both flags (a hardware side
399
+ effect). In Therm mode HIGH_Alert clears only once the temperature
400
+ drops below TLOW_LIMIT.
401
+
402
+ Returns:
403
+ int: Mask of SOURCE_HIGH / SOURCE_LOW.
404
+ """
405
+ config = self._read_reg(self._REG_CONFIG)
406
+ status = 0
407
+ if config & self._CFG_HIGH_ALERT:
408
+ status |= SOURCE_HIGH
409
+ if config & self._CFG_LOW_ALERT:
410
+ status |= SOURCE_LOW
411
+ return status
412
+
413
+ def on_interrupt(self, callback, int_pin=None):
414
+ """Subscribe to ALERT events.
415
+
416
+ Delivery: if int_pin is given, it is wired directly. Otherwise falls
417
+ back to connection.int_pin, or a 5 ms polling thread on Linux if
418
+ neither is available. With a pin, the callback runs on every ALERT
419
+ edge (the edge direction follows the configured polarity). The
420
+ polling fallback calls it whenever the status mask changes.
421
+
422
+ Args:
423
+ callback: Callable(status: int) — the poll_interrupt() mask;
424
+ test it against the SOURCE_* constants.
425
+ int_pin: Optional InputPin for this call, overriding connection.int_pin.
426
+ """
427
+ self._callback = callback
428
+ pin = int_pin if int_pin is not None else getattr(self._connection, 'int_pin', None)
429
+ self._int_pin_used = pin
430
+ if pin is not None:
431
+ from periph.connection.input_pin import InputPin
432
+ active_high = self._read_reg(self._REG_CONFIG) & self._CFG_POL
433
+ pin.on_edge(self._int_handler, InputPin.RISING if active_high else InputPin.FALLING)
434
+ elif _LINUX:
435
+ self._last_status = self.poll_interrupt()
436
+ self._poll_stop = False
437
+ self._poll_thread = _threading.Thread(target=self._poll_loop, daemon=True)
438
+ self._poll_thread.start()
439
+
440
+ def off_interrupt(self):
441
+ """Unsubscribe and stop delivery."""
442
+ if self._int_pin_used is not None:
443
+ self._int_pin_used.off_edge(self._int_handler)
444
+ self._int_pin_used = None
445
+ elif _LINUX:
446
+ self._poll_stop = True
447
+ self._callback = None
448
+
449
+ def _int_handler(self):
450
+ status = self.poll_interrupt()
451
+ if self._callback:
452
+ self._callback(status)
453
+
454
+ def _poll_loop(self):
455
+ while not self._poll_stop:
456
+ status = self.poll_interrupt()
457
+ if status != self._last_status:
458
+ self._last_status = status
459
+ if self._callback:
460
+ self._callback(status)
461
+ time.sleep(0.005)
@@ -0,0 +1,174 @@
1
+ """Shared base for ST's VL53 FlightSense Time-of-Flight ranging family.
2
+
3
+ Internal module — not a public driver. VL53L0X and VL53L1X have different
4
+ register maps (8-bit vs 16-bit register index), so this base holds no
5
+ register addresses and no ranging logic, only the plumbing both chips share:
6
+ big-endian register access with a 1- or 2-byte index, the bounded poll
7
+ helper, the XSHUT boot wait, the lock that serializes multi-register
8
+ sequences against the interrupt poller, interrupt delivery (int_pin edge or
9
+ Linux polling thread), the volatile re-addressing helper, and the family's
10
+ shared constants. See specs/tof/_vl53_base.md.
11
+ """
12
+
13
+ try:
14
+ import threading as _threading
15
+ _LINUX = True
16
+ except ImportError:
17
+ _LINUX = False
18
+
19
+ import time
20
+
21
+
22
+ I2C_ADDRESS = 0x29
23
+
24
+ TIMEOUT_MS = 500
25
+ BOOT_US = 1200
26
+
27
+ # Logical interrupt sources shared by the family (mutually exclusive).
28
+ SOURCE_LEVEL_LOW = 1
29
+ SOURCE_LEVEL_HIGH = 2
30
+ SOURCE_OUT_OF_WINDOW = 3
31
+ SOURCE_NEW_SAMPLE_READY = 4
32
+ SOURCE_IN_WINDOW = 5 # VL53L1X only
33
+
34
+
35
+ class _NoLock:
36
+ def __enter__(self):
37
+ return self
38
+
39
+ def __exit__(self, *exc):
40
+ return False
41
+
42
+
43
+ def _ticks_ms():
44
+ if hasattr(time, 'ticks_ms'):
45
+ return time.ticks_ms()
46
+ if hasattr(time, 'monotonic'):
47
+ return int(time.monotonic() * 1000)
48
+ return int(time.time() * 1000)
49
+
50
+
51
+ def _elapsed_ms(start):
52
+ if hasattr(time, 'ticks_diff'):
53
+ return time.ticks_diff(time.ticks_ms(), start)
54
+ return _ticks_ms() - start
55
+
56
+
57
+ class _VL53Base:
58
+ """Protected helpers shared by the VL53 chip drivers.
59
+
60
+ Never instantiated directly. Subclasses implement
61
+ _poll_interrupt_status() for the interrupt delivery helpers.
62
+
63
+ Args:
64
+ connection: Configured I2C connection. Its optional en_pin drives
65
+ XSHUT (high = enabled), its optional int_pin receives GPIO1.
66
+ index_bytes: Register index width on the wire, 1 or 2.
67
+ chip_name: Chip name used in error messages.
68
+ """
69
+
70
+ def __init__(self, connection, index_bytes, chip_name):
71
+ self._connection = connection
72
+ self._index_bytes = index_bytes
73
+ self._chip_name = chip_name
74
+ # Serializes multi-register sequences against the interrupt polling
75
+ # thread (a status read/clear must not interleave with them).
76
+ self._lock = _threading.RLock() if _LINUX else _NoLock()
77
+ self._callback = None
78
+ self._int_pin_used = None
79
+ self._poll_stop = False
80
+ self._poll_thread = None
81
+
82
+ # --- Register access --------------------------------------------------
83
+
84
+ def _index(self, reg):
85
+ if self._index_bytes == 2:
86
+ return bytes([(reg >> 8) & 0xFF, reg & 0xFF])
87
+ return bytes([reg & 0xFF])
88
+
89
+ def _wr_block(self, reg, data):
90
+ self._connection.write(self._index(reg) + bytes(data))
91
+
92
+ def _rd_block(self, reg, n):
93
+ return self._connection.write_read(self._index(reg), n)
94
+
95
+ def _wr8(self, reg, value):
96
+ self._wr_block(reg, (value & 0xFF,))
97
+
98
+ def _rd8(self, reg):
99
+ return self._rd_block(reg, 1)[0]
100
+
101
+ def _wr16(self, reg, value):
102
+ self._wr_block(reg, ((value >> 8) & 0xFF, value & 0xFF))
103
+
104
+ def _rd16(self, reg):
105
+ data = self._rd_block(reg, 2)
106
+ return (data[0] << 8) | data[1]
107
+
108
+ def _wr32(self, reg, value):
109
+ self._wr_block(reg, ((value >> 24) & 0xFF, (value >> 16) & 0xFF,
110
+ (value >> 8) & 0xFF, value & 0xFF))
111
+
112
+ def _rd32(self, reg):
113
+ data = self._rd_block(reg, 4)
114
+ return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]
115
+
116
+ # --- Polling and boot -------------------------------------------------
117
+
118
+ def _wait_until(self, predicate, what):
119
+ start = _ticks_ms()
120
+ while True:
121
+ if predicate():
122
+ return
123
+ if _elapsed_ms(start) > TIMEOUT_MS:
124
+ raise OSError('{} timeout waiting for {}'.format(self._chip_name, what))
125
+
126
+ def _boot_wait(self):
127
+ if getattr(self._connection, 'en_pin', None):
128
+ self._connection.enable()
129
+ time.sleep(BOOT_US / 1000000)
130
+
131
+ # --- Re-addressing ----------------------------------------------------
132
+
133
+ def _set_address_reg(self, reg, address):
134
+ if address < 0x08 or address > 0x77:
135
+ raise ValueError('address must be 0x08 to 0x77')
136
+ self._wr8(reg, address & 0x7F)
137
+
138
+ # --- Interrupt delivery -----------------------------------------------
139
+
140
+ def _poll_interrupt_status(self):
141
+ """Read and clear a pending interrupt; return its SOURCE_* value or 0."""
142
+ raise NotImplementedError
143
+
144
+ def _subscribe(self, callback, int_pin=None):
145
+ self._callback = callback
146
+ pin = int_pin if int_pin is not None else getattr(self._connection, 'int_pin', None)
147
+ self._int_pin_used = pin
148
+ if pin is not None:
149
+ from periph.connection.input_pin import InputPin
150
+ pin.on_edge(self._int_handler, InputPin.FALLING)
151
+ elif _LINUX:
152
+ self._poll_stop = False
153
+ self._poll_thread = _threading.Thread(target=self._poll_loop, daemon=True)
154
+ self._poll_thread.start()
155
+
156
+ def _unsubscribe(self):
157
+ if self._int_pin_used is not None:
158
+ self._int_pin_used.off_edge(self._int_handler)
159
+ self._int_pin_used = None
160
+ elif _LINUX:
161
+ self._poll_stop = True
162
+ self._callback = None
163
+
164
+ def _int_handler(self):
165
+ status = self._poll_interrupt_status()
166
+ if status and self._callback:
167
+ self._callback(status)
168
+
169
+ def _poll_loop(self):
170
+ while not self._poll_stop:
171
+ status = self._poll_interrupt_status()
172
+ if status and self._callback:
173
+ self._callback(status)
174
+ time.sleep(0.005)