circuitpython-adxl36x 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.
adxl36x/__init__.py ADDED
@@ -0,0 +1,747 @@
1
+ """CircuitPython driver for the Analog Devices ADXL366/ADXL367 accelerometers."""
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ from adafruit_bus_device.i2c_device import I2CDevice
7
+ from adafruit_bus_device.spi_device import SPIDevice
8
+ from micropython import const
9
+
10
+ if TYPE_CHECKING:
11
+ from busio import I2C, SPI
12
+ from digitalio import DigitalInOut
13
+ from typing_extensions import Self
14
+
15
+ _STANDARD_GRAVITY = 9.80665
16
+
17
+ # -- I2C addresses (ASEL pin low/high) --
18
+ _DEFAULT_I2C_ADDRESS = const(0x53)
19
+ _ALT_I2C_ADDRESS = const(0x1D)
20
+
21
+ # -- SPI protocol commands --
22
+ _SPI_WRITE_REG = const(0x0A)
23
+ _SPI_READ_REG = const(0x0B)
24
+ _SPI_READ_FIFO = const(0x0D)
25
+
26
+ # -- Register addresses --
27
+ _REG_DEVID_AD = const(0x00)
28
+ _REG_DEVID_MST = const(0x01)
29
+ _REG_PARTID = const(0x02)
30
+ _REG_REVID = const(0x03)
31
+ _REG_SERIAL_NUMBER_3 = const(0x04)
32
+ _REG_XDATA = const(0x08)
33
+ _REG_STATUS = const(0x0B)
34
+ _REG_FIFO_ENTRIES_L = const(0x0C)
35
+ _REG_FIFO_ENTRIES_H = const(0x0D)
36
+ _REG_XDATA_H = const(0x0E)
37
+ _REG_TEMP_H = const(0x14)
38
+ _REG_EX_ADC_H = const(0x16)
39
+ _REG_I2C_FIFO_DATA = const(0x18)
40
+ _REG_SOFT_RESET = const(0x1F)
41
+ _REG_THRESH_ACT_H = const(0x20)
42
+ _REG_THRESH_ACT_L = const(0x21)
43
+ _REG_TIME_ACT = const(0x22)
44
+ _REG_THRESH_INACT_H = const(0x23)
45
+ _REG_THRESH_INACT_L = const(0x24)
46
+ _REG_TIME_INACT_H = const(0x25)
47
+ _REG_TIME_INACT_L = const(0x26)
48
+ _REG_ACT_INACT_CTL = const(0x27)
49
+ _REG_FIFO_CONTROL = const(0x28)
50
+ _REG_FIFO_SAMPLES = const(0x29)
51
+ _REG_INTMAP1_LWR = const(0x2A)
52
+ _REG_INTMAP2_LWR = const(0x2B)
53
+ _REG_FILTER_CTL = const(0x2C)
54
+ _REG_POWER_CTL = const(0x2D)
55
+ _REG_SELF_TEST = const(0x2E)
56
+ _REG_TAP_THRESH = const(0x2F)
57
+ _REG_TAP_DUR = const(0x30)
58
+ _REG_TAP_LATENT = const(0x31)
59
+ _REG_TAP_WINDOW = const(0x32)
60
+ _REG_X_OFFSET = const(0x33)
61
+ _REG_Y_OFFSET = const(0x34)
62
+ _REG_Z_OFFSET = const(0x35)
63
+ _REG_TIMER_CTL = const(0x39)
64
+ _REG_INTMAP1_UPPER = const(0x3A)
65
+ _REG_INTMAP2_UPPER = const(0x3B)
66
+ _REG_ADC_CTL = const(0x3C)
67
+ _REG_TEMP_CTL = const(0x3D)
68
+ _REG_AXIS_MASK = const(0x43)
69
+ _REG_PEDOMETER_STEP_CNT_H = const(0x47)
70
+ _REG_PEDOMETER_CTL = const(0x49)
71
+
72
+ # -- Device/part identification --
73
+ _DEVID_AD = const(0xAD)
74
+ _DEVID_MST = const(0x1D)
75
+ _PART_ID = const(0xF7)
76
+ _REVID_ADXL367 = const(0x3)
77
+ _REVID_ADXL366 = const(0x5)
78
+ _RESET_KEY = const(0x52)
79
+
80
+ # -- POWER_CTL (0x2D) --
81
+ _POWER_CTL_MODE_MASK = const(0x03)
82
+
83
+
84
+ class OpMode:
85
+ """Values for `ADXL367.power_mode`."""
86
+
87
+ STANDBY = const(0x00)
88
+ MEASURE = const(0x02)
89
+
90
+
91
+ # -- FILTER_CTL (0x2C) --
92
+ _FILTER_CTL_RANGE_MASK = const(0xC0)
93
+ _FILTER_CTL_RANGE_SHIFT = const(6)
94
+ _FILTER_CTL_ODR_MASK = const(0x07)
95
+
96
+
97
+ class Range:
98
+ """Values for `ADXL367.g_range`.
99
+
100
+ Unlike the ADXL34x family, ±16g isn't supported.
101
+ """
102
+
103
+ RANGE_2_G = const(0x00)
104
+ RANGE_4_G = const(0x01)
105
+ RANGE_8_G = const(0x02)
106
+
107
+
108
+ _RANGE_SCALE_MULTIPLIER = (1, 2, 4) # indexed by Range value
109
+
110
+
111
+ class DataRate:
112
+ """Values for `ADXL367.data_rate`."""
113
+
114
+ RATE_12_5_HZ = const(0x00)
115
+ RATE_25_HZ = const(0x01)
116
+ RATE_50_HZ = const(0x02)
117
+ RATE_100_HZ = const(0x03)
118
+ RATE_200_HZ = const(0x04)
119
+ RATE_400_HZ = const(0x05)
120
+
121
+
122
+ _DATA_RATE_HZ = (12.5, 25.0, 50.0, 100.0, 200.0, 400.0) # indexed by DataRate value
123
+
124
+ # -- Acceleration/temperature scaling --
125
+ _ACCEL_SCALE_MUL = const(245166)
126
+ _ACCEL_SCALE_DIV = const(1_000_000_000)
127
+ _TEMP_OFFSET = const(1185)
128
+ _TEMP_SCALE_MUL = const(18518518)
129
+ _TEMP_SCALE_DIV = const(1_000_000_000)
130
+
131
+ # -- ACT_INACT_CTL (0x27) --
132
+ _ACT_INACT_CTL_ACT_MASK = const(0x03)
133
+ _ACT_INACT_CTL_INACT_MASK = const(0x0C)
134
+ _ACT_INACT_CTL_INACT_SHIFT = const(2)
135
+ _ACT_INACT_CTL_REF_READBACK_MASK = const(0xC0)
136
+ _ACTIVITY_ENABLE = const(0x01)
137
+ _REFERENCED_ACTIVITY_ENABLE = const(0x03)
138
+ _THRESHOLD_MAX = const(0x1FFF)
139
+
140
+ # -- SELF_TEST (0x2E) --
141
+ _SELF_TEST_ST = const(0x01)
142
+ _SELF_TEST_ST_FORCE = const(0x02)
143
+ _SELF_TEST_MIN = const(360) # 90 * 100 / 25
144
+ _SELF_TEST_MAX = const(1080) # 270 * 100 / 25
145
+
146
+ # -- TEMP_CTL (0x3D) / ADC_CTL (0x3C) --
147
+ _TEMP_CTL_EN = const(0x01)
148
+ _TEMP_CTL_NL_COMP_EN = const(0x80)
149
+ _ADC_CTL_EN = const(0x01)
150
+
151
+ # -- Per-axis offset trim (0x33-0x35) --
152
+ _OFFSET_MAX = const(0x1F)
153
+
154
+ # -- FIFO --
155
+ _FIFO_CONTROL_MODE_MASK = const(0x03)
156
+ _FIFO_CONTROL_FORMAT_MASK = const(0x78)
157
+ _FIFO_CONTROL_FORMAT_SHIFT = const(3)
158
+ _FIFO_CONTROL_SAMPLES_MSB = const(0x04)
159
+ _FIFO_ENTRIES_H_MASK = const(0x03)
160
+ _FIFO_SAMPLE_SETS_MAX = const(0x1FF)
161
+
162
+
163
+ class FIFOMode:
164
+ """Values for `ADXL367.configure_fifo(mode=...)`."""
165
+
166
+ DISABLED = const(0x00)
167
+ OLDEST_SAVED = const(0x01)
168
+ STREAM = const(0x02)
169
+ TRIGGERED = const(0x03)
170
+
171
+
172
+ class FIFOFormat:
173
+ """Values for `ADXL367.configure_fifo(fifo_format=...)`.
174
+
175
+ T/A suffixes add a temperature-or-ADC sample (whichever is enabled via
176
+ `temperature`/`adc_value`) to each sample set.
177
+ """
178
+
179
+ XYZ = const(0x00)
180
+ X = const(0x01)
181
+ Y = const(0x02)
182
+ Z = const(0x03)
183
+ XYZT = const(0x04)
184
+ XT = const(0x05)
185
+ YT = const(0x06)
186
+ ZT = const(0x07)
187
+ XYZA = const(0x08)
188
+ XA = const(0x09)
189
+ YA = const(0x0A)
190
+ ZA = const(0x0B)
191
+
192
+
193
+ _FIFO_CHANNEL_NAMES = ("x", "y", "z", "temp_or_adc") # indexed by FIFO channel ID
194
+
195
+ # -- Interrupt mapping (INTMAP1/2 LWR + UPPER) --
196
+ _INTMAPX_UPPER_MASK = const(0xDF)
197
+ # name -> (is_upper_byte, bit_index)
198
+ _INTERRUPT_EVENTS = {
199
+ "data_ready": (False, 0),
200
+ "fifo_ready": (False, 1),
201
+ "fifo_watermark": (False, 2),
202
+ "fifo_overrun": (False, 3),
203
+ "activity": (False, 4),
204
+ "inactivity": (False, 5),
205
+ "awake": (False, 6),
206
+ "active_low": (False, 7),
207
+ "single_tap": (True, 0),
208
+ "double_tap": (True, 1),
209
+ "temp_adc_low": (True, 2),
210
+ "temp_adc_high": (True, 3),
211
+ "keep_alive_timer": (True, 4),
212
+ "user_register_error": (True, 6),
213
+ "fuse_error": (True, 7),
214
+ }
215
+
216
+ # -- STATUS (0x0B) bits --
217
+ _STATUS_DATA_RDY = const(0x01)
218
+ _STATUS_FIFO_RDY = const(0x02)
219
+ _STATUS_FIFO_WATERMARK = const(0x04)
220
+ _STATUS_FIFO_OVERRUN = const(0x08)
221
+ _STATUS_ACT = const(0x10)
222
+ _STATUS_INACT = const(0x20)
223
+ _STATUS_AWAKE = const(0x40)
224
+ _STATUS_ERR_USER_REGS = const(0x80)
225
+
226
+
227
+ def _decode_s14(msb: int, lsb: int) -> int:
228
+ """Decode a signed 14-bit value from an H/L register pair."""
229
+ value = ((msb << 6) | (lsb >> 2)) & 0x3FFF
230
+ if value & 0x2000:
231
+ value -= 0x4000
232
+ return value
233
+
234
+
235
+ # -- bus transport --
236
+
237
+
238
+ class _I2CDevice(I2CDevice):
239
+ """`I2CDevice` with a correctly-typed `__exit__`.
240
+
241
+ `adafruit_bus_device-stubs` declares `__exit__(self) -> None` (zero
242
+ args), which doesn't match the real 3-argument context-manager protocol
243
+ and makes every `with I2CDevice(...) as ...:` a type error. The real
244
+ `__exit__` only unlocks the bus and always returns a falsy value, so we
245
+ reimplement it directly here instead of delegating to the mistyped
246
+ parent method.
247
+ """
248
+
249
+ i2c: "I2C"
250
+
251
+ def __exit__(self, *exc_info: object) -> None:
252
+ self.i2c.unlock()
253
+
254
+
255
+ class I2CBus:
256
+ """Register-level access to a device over I2C."""
257
+
258
+ def __init__(self, i2c_bus: "I2C", address: int) -> None:
259
+ self._device = _I2CDevice(i2c_bus, address)
260
+
261
+ def read_into(self, register: int, buffer: bytearray) -> None:
262
+ with self._device as i2c:
263
+ i2c.write_then_readinto(bytes((register,)), buffer)
264
+
265
+ def write(self, register: int, value: int) -> None:
266
+ with self._device as i2c:
267
+ i2c.write(bytes((register, value & 0xFF)))
268
+
269
+ def read_fifo_into(self, buffer: bytearray) -> None:
270
+ with self._device as i2c:
271
+ i2c.write_then_readinto(bytes((_REG_I2C_FIFO_DATA,)), buffer)
272
+
273
+
274
+ class _SPIDevice(SPIDevice):
275
+ """`SPIDevice` with a correctly-typed `__exit__`. See `_I2CDevice`."""
276
+
277
+ spi: "SPI"
278
+ chip_select: "DigitalInOut | None"
279
+ cs_active_value: bool
280
+
281
+ def __exit__(self, *exc_info: object) -> None:
282
+ # extra_clocks handling is intentionally omitted: we never construct
283
+ # this with a nonzero extra_clocks, so upstream's post-CS clock-out
284
+ # step would always be a no-op here.
285
+ if self.chip_select:
286
+ self.chip_select.value = not self.cs_active_value
287
+ self.spi.unlock()
288
+
289
+
290
+ class _SPIBus:
291
+ """Register-level access to a device over SPI."""
292
+
293
+ def __init__(self, spi_bus: "SPI", cs: "DigitalInOut", *, baudrate: int) -> None:
294
+ self._device = _SPIDevice(spi_bus, cs, baudrate=baudrate, polarity=0, phase=0)
295
+
296
+ def read_into(self, register: int, buffer: bytearray) -> None:
297
+ with self._device as spi:
298
+ spi.write(bytes((_SPI_READ_REG, register)))
299
+ spi.readinto(buffer)
300
+
301
+ def write(self, register: int, value: int) -> None:
302
+ with self._device as spi:
303
+ spi.write(bytes((_SPI_WRITE_REG, register, value & 0xFF)))
304
+
305
+ def read_fifo_into(self, buffer: bytearray) -> None:
306
+ with self._device as spi:
307
+ spi.write(bytes((_SPI_READ_FIFO,)))
308
+ spi.readinto(buffer)
309
+
310
+
311
+ class ADXL367:
312
+ """Driver for the Analog Devices ADXL367 accelerometer.
313
+
314
+ Also the base class for `ADXL366`, which shares this entire register map
315
+ and adds a few extra features. Connect over I2C with the constructor
316
+ (mirroring `adafruit_adxl34x.ADXL345`) or over SPI with `from_spi()`.
317
+ """
318
+
319
+ _revid = _REVID_ADXL367
320
+
321
+ def __init__(self, i2c_bus: "I2C", *, address: int = _DEFAULT_I2C_ADDRESS) -> None:
322
+ self._bus: I2CBus | _SPIBus = I2CBus(i2c_bus, address)
323
+ self._range = Range.RANGE_2_G
324
+ self._data_rate = DataRate.RATE_100_HZ
325
+ self._initialize()
326
+
327
+ @classmethod
328
+ def from_spi(
329
+ cls,
330
+ spi_bus: "SPI",
331
+ cs: "DigitalInOut",
332
+ *,
333
+ baudrate: int = 1_000_000,
334
+ ) -> "Self":
335
+ """Construct a driver instance over SPI instead of I2C."""
336
+ self = cls.__new__(cls)
337
+ self._bus = _SPIBus(spi_bus, cs, baudrate=baudrate)
338
+ self._range = Range.RANGE_2_G
339
+ self._data_rate = DataRate.RATE_100_HZ
340
+ self._initialize()
341
+ return self
342
+
343
+ # -- low-level bus access --
344
+
345
+ def _read_into(self, register: int, buffer: bytearray) -> None:
346
+ self._bus.read_into(register, buffer)
347
+
348
+ def _read_u8(self, register: int) -> int:
349
+ buffer = bytearray(1)
350
+ self._read_into(register, buffer)
351
+ return buffer[0]
352
+
353
+ def _write_u8(self, register: int, value: int) -> None:
354
+ self._bus.write(register, value)
355
+
356
+ def _write_masked(self, register: int, value: int, mask: int) -> None:
357
+ current = self._read_u8(register)
358
+ self._write_u8(register, (current & ~mask) | (value & mask))
359
+
360
+ def _read_fifo_into(self, buffer: bytearray) -> None:
361
+ self._bus.read_fifo_into(buffer)
362
+
363
+ def _set_threshold(
364
+ self,
365
+ high_register: int,
366
+ low_register: int,
367
+ threshold: int,
368
+ ) -> None:
369
+ if not 0 <= threshold <= _THRESHOLD_MAX:
370
+ msg = f"threshold must be in range 0-{_THRESHOLD_MAX}, got {threshold!r}"
371
+ raise ValueError(msg)
372
+ self._write_u8(high_register, (threshold >> 6) & 0x7F)
373
+ self._write_u8(low_register, (threshold & 0x3F) << 2)
374
+
375
+ # -- initialization --
376
+
377
+ def reset(self) -> None:
378
+ """Perform a software reset, returning all registers to power-on defaults."""
379
+ self._write_u8(_REG_SOFT_RESET, _RESET_KEY)
380
+ time.sleep(0.02)
381
+
382
+ def _initialize(self) -> None:
383
+ self.reset()
384
+ seen = (
385
+ self._read_u8(_REG_DEVID_AD),
386
+ self._read_u8(_REG_DEVID_MST),
387
+ self._read_u8(_REG_PARTID),
388
+ self._read_u8(_REG_REVID),
389
+ )
390
+ expected = (_DEVID_AD, _DEVID_MST, _PART_ID, self._revid)
391
+ if seen != expected:
392
+ msg = f"{type(self).__name__}: unexpected device ID bytes {seen!r} (expected {expected!r})"
393
+ raise RuntimeError(msg)
394
+ self.power_mode = OpMode.MEASURE
395
+
396
+ # -- identification --
397
+
398
+ @property
399
+ def serial_number(self) -> int:
400
+ """This chip's factory-programmed 32-bit unique serial number."""
401
+ buffer = bytearray(4)
402
+ self._read_into(_REG_SERIAL_NUMBER_3, buffer)
403
+ return int.from_bytes(bytes(buffer), "big")
404
+
405
+ # -- power/range/rate --
406
+
407
+ @property
408
+ def power_mode(self) -> int:
409
+ """The current `OpMode` (standby or measure)."""
410
+ return self._read_u8(_REG_POWER_CTL) & _POWER_CTL_MODE_MASK
411
+
412
+ @power_mode.setter
413
+ def power_mode(self, mode: int) -> None:
414
+ self._write_masked(_REG_POWER_CTL, mode, _POWER_CTL_MODE_MASK)
415
+
416
+ @property
417
+ def g_range(self) -> int:
418
+ """The measurement range, a `Range` value (tops out at ±8g, unlike ADXL34x)."""
419
+ return self._range
420
+
421
+ @g_range.setter
422
+ def g_range(self, value: int) -> None:
423
+ if value not in (Range.RANGE_2_G, Range.RANGE_4_G, Range.RANGE_8_G):
424
+ msg = f"invalid Range value: {value!r}"
425
+ raise ValueError(msg)
426
+ self._write_masked(
427
+ _REG_FILTER_CTL,
428
+ value << _FILTER_CTL_RANGE_SHIFT,
429
+ _FILTER_CTL_RANGE_MASK,
430
+ )
431
+ self._range = value
432
+
433
+ @property
434
+ def data_rate(self) -> int:
435
+ """The output data rate, a `DataRate` value."""
436
+ return self._data_rate
437
+
438
+ @data_rate.setter
439
+ def data_rate(self, value: int) -> None:
440
+ if not 0 <= value <= DataRate.RATE_400_HZ:
441
+ msg = f"invalid DataRate value: {value!r}"
442
+ raise ValueError(msg)
443
+ self._write_masked(_REG_FILTER_CTL, value, _FILTER_CTL_ODR_MASK)
444
+ self._data_rate = value
445
+
446
+ # -- acceleration --
447
+
448
+ @property
449
+ def raw_x(self) -> int:
450
+ """Raw, signed 14-bit X-axis acceleration code."""
451
+ return self._read_raw_axis(_REG_XDATA_H)
452
+
453
+ @property
454
+ def raw_y(self) -> int:
455
+ """Raw, signed 14-bit Y-axis acceleration code."""
456
+ return self._read_raw_axis(_REG_XDATA_H + 2)
457
+
458
+ @property
459
+ def raw_z(self) -> int:
460
+ """Raw, signed 14-bit Z-axis acceleration code."""
461
+ return self._read_raw_axis(_REG_XDATA_H + 4)
462
+
463
+ def _read_raw_axis(self, high_register: int) -> int:
464
+ buffer = bytearray(2)
465
+ self._read_into(high_register, buffer)
466
+ return _decode_s14(buffer[0], buffer[1])
467
+
468
+ @property
469
+ def raw_acceleration(self) -> tuple[int, int, int]:
470
+ """Raw, signed 14-bit (x, y, z) codes, read in a single bus transaction."""
471
+ buffer = bytearray(6)
472
+ self._read_into(_REG_XDATA_H, buffer)
473
+ return (
474
+ _decode_s14(buffer[0], buffer[1]),
475
+ _decode_s14(buffer[2], buffer[3]),
476
+ _decode_s14(buffer[4], buffer[5]),
477
+ )
478
+
479
+ @property
480
+ def acceleration(self) -> tuple[float, float, float]:
481
+ """(x, y, z) acceleration in m/s^2."""
482
+ scale = _RANGE_SCALE_MULTIPLIER[self._range] * _ACCEL_SCALE_MUL / _ACCEL_SCALE_DIV * _STANDARD_GRAVITY
483
+ x, y, z = self.raw_acceleration
484
+ return (x * scale, y * scale, z * scale)
485
+
486
+ # -- offsets --
487
+
488
+ @property
489
+ def offset(self) -> tuple[int, int, int]:
490
+ """Per-axis trim offsets: raw 5-bit codes (0-31); units unconfirmed."""
491
+ return (
492
+ self._read_u8(_REG_X_OFFSET) & _OFFSET_MAX,
493
+ self._read_u8(_REG_Y_OFFSET) & _OFFSET_MAX,
494
+ self._read_u8(_REG_Z_OFFSET) & _OFFSET_MAX,
495
+ )
496
+
497
+ @offset.setter
498
+ def offset(self, value: tuple[int, int, int]) -> None:
499
+ x, y, z = value
500
+ for axis_value in (x, y, z):
501
+ if not 0 <= axis_value <= _OFFSET_MAX:
502
+ msg = f"offset values must be in range 0-{_OFFSET_MAX}, got {value!r}"
503
+ raise ValueError(msg)
504
+ self._write_u8(_REG_X_OFFSET, x)
505
+ self._write_u8(_REG_Y_OFFSET, y)
506
+ self._write_u8(_REG_Z_OFFSET, z)
507
+
508
+ # -- activity/inactivity detection --
509
+
510
+ def enable_motion_detection(
511
+ self,
512
+ *,
513
+ threshold: int = 100,
514
+ time_: int = 1,
515
+ referenced: bool = False,
516
+ ) -> None:
517
+ """Enable activity (motion) detection.
518
+
519
+ `threshold` is a raw accelerometer-code magnitude (same units as
520
+ `raw_acceleration`, i.e. scaled by `range`) rather than the ADXL34x's
521
+ fixed 62.5 mg/LSB - the exact mg/LSB figure for this threshold field
522
+ is not confirmed against the datasheet. `time_` is in seconds
523
+ (unconfirmed exact LSB scale; treat as approximate).
524
+ """
525
+ self._set_threshold(_REG_THRESH_ACT_H, _REG_THRESH_ACT_L, threshold)
526
+ self._write_u8(_REG_TIME_ACT, time_ & 0xFF)
527
+ mode = _REFERENCED_ACTIVITY_ENABLE if referenced else _ACTIVITY_ENABLE
528
+ self._write_masked(_REG_ACT_INACT_CTL, mode, _ACT_INACT_CTL_ACT_MASK)
529
+
530
+ def disable_motion_detection(self) -> None:
531
+ """Disable activity (motion) detection."""
532
+ self._write_masked(_REG_ACT_INACT_CTL, 0, _ACT_INACT_CTL_ACT_MASK)
533
+
534
+ def enable_inactivity_detection(
535
+ self,
536
+ *,
537
+ threshold: int = 50,
538
+ time_: int = 3,
539
+ referenced: bool = False,
540
+ ) -> None:
541
+ """Enable inactivity detection.
542
+
543
+ See `enable_motion_detection` for unit caveats.
544
+ """
545
+ self._set_threshold(_REG_THRESH_INACT_H, _REG_THRESH_INACT_L, threshold)
546
+ self._write_u8(_REG_TIME_INACT_H, (time_ >> 8) & 0xFF)
547
+ self._write_u8(_REG_TIME_INACT_L, time_ & 0xFF)
548
+ mode = _REFERENCED_ACTIVITY_ENABLE if referenced else _ACTIVITY_ENABLE
549
+ shifted = mode << _ACT_INACT_CTL_INACT_SHIFT
550
+ self._write_masked(_REG_ACT_INACT_CTL, shifted, _ACT_INACT_CTL_INACT_MASK)
551
+
552
+ def disable_inactivity_detection(self) -> None:
553
+ """Disable inactivity detection."""
554
+ self._write_masked(_REG_ACT_INACT_CTL, 0, _ACT_INACT_CTL_INACT_MASK)
555
+
556
+ @property
557
+ def events(self) -> dict[str, bool]:
558
+ """Currently-set event flags from STATUS (0x0B).
559
+
560
+ Does not yet include tap, pedometer-overflow, or ADC/temperature
561
+ threshold events - their status-register bit positions
562
+ (STATUS_COPY/STATUS_2/STATUS_3) aren't confirmed against the
563
+ datasheet yet.
564
+ """
565
+ status = self._read_u8(_REG_STATUS)
566
+ return {
567
+ "data_ready": bool(status & _STATUS_DATA_RDY),
568
+ "fifo_ready": bool(status & _STATUS_FIFO_RDY),
569
+ "fifo_watermark": bool(status & _STATUS_FIFO_WATERMARK),
570
+ "fifo_overrun": bool(status & _STATUS_FIFO_OVERRUN),
571
+ "motion": bool(status & _STATUS_ACT),
572
+ "inactivity": bool(status & _STATUS_INACT),
573
+ "awake": bool(status & _STATUS_AWAKE),
574
+ "error": bool(status & _STATUS_ERR_USER_REGS),
575
+ }
576
+
577
+ # -- interrupt routing --
578
+
579
+ def map_interrupt(self, pin: int, events: "set[str] | frozenset[str]") -> None:
580
+ """Route the named events (keys of `_INTERRUPT_EVENTS`) to INT1 or INT2."""
581
+ if pin not in (1, 2):
582
+ msg = f"pin must be 1 or 2, got {pin!r}"
583
+ raise ValueError(msg)
584
+ lower = upper = 0
585
+ for name in events:
586
+ if name not in _INTERRUPT_EVENTS:
587
+ msg = f"unknown interrupt event: {name!r}"
588
+ raise ValueError(msg)
589
+ is_upper, bit = _INTERRUPT_EVENTS[name]
590
+ if is_upper:
591
+ upper |= 1 << bit
592
+ else:
593
+ lower |= 1 << bit
594
+ lwr_register = _REG_INTMAP1_LWR if pin == 1 else _REG_INTMAP2_LWR
595
+ upper_register = _REG_INTMAP1_UPPER if pin == 1 else _REG_INTMAP2_UPPER
596
+ self._write_u8(lwr_register, lower)
597
+ self._write_u8(upper_register, upper & _INTMAPX_UPPER_MASK)
598
+
599
+ # -- temperature / external ADC --
600
+
601
+ @property
602
+ def temperature(self) -> float:
603
+ """On-chip temperature in Celsius. Mutually exclusive with `adc_value`."""
604
+ self._write_masked(_REG_TEMP_CTL, _TEMP_CTL_EN, _TEMP_CTL_EN)
605
+ self._write_masked(_REG_ADC_CTL, 0, _ADC_CTL_EN)
606
+ buffer = bytearray(2)
607
+ self._read_into(_REG_TEMP_H, buffer)
608
+ raw = _decode_s14(buffer[0], buffer[1])
609
+ return (raw + _TEMP_OFFSET) * _TEMP_SCALE_MUL / _TEMP_SCALE_DIV
610
+
611
+ @property
612
+ def adc_value(self) -> int:
613
+ """Raw signed 14-bit ADC code. Mutually exclusive with `temperature`."""
614
+ self._write_masked(_REG_ADC_CTL, _ADC_CTL_EN, _ADC_CTL_EN)
615
+ self._write_masked(_REG_TEMP_CTL, 0, _TEMP_CTL_EN)
616
+ buffer = bytearray(2)
617
+ self._read_into(_REG_EX_ADC_H, buffer)
618
+ return _decode_s14(buffer[0], buffer[1])
619
+
620
+ # -- FIFO --
621
+
622
+ def configure_fifo(
623
+ self,
624
+ *,
625
+ mode: int = FIFOMode.STREAM,
626
+ fifo_format: int = FIFOFormat.XYZ,
627
+ sample_sets: int = 128,
628
+ ) -> None:
629
+ """Configure the FIFO.
630
+
631
+ `sample_sets` (0-511) is the total number of sample sets to store.
632
+ """
633
+ if not 0 <= sample_sets <= _FIFO_SAMPLE_SETS_MAX:
634
+ msg = f"sample_sets must be in range 0-{_FIFO_SAMPLE_SETS_MAX}, got {sample_sets!r}"
635
+ raise ValueError(msg)
636
+ control = mode & _FIFO_CONTROL_MODE_MASK
637
+ control |= (fifo_format << _FIFO_CONTROL_FORMAT_SHIFT) & _FIFO_CONTROL_FORMAT_MASK
638
+ if sample_sets & 0x100:
639
+ control |= _FIFO_CONTROL_SAMPLES_MSB
640
+ self._write_u8(_REG_FIFO_CONTROL, control)
641
+ self._write_u8(_REG_FIFO_SAMPLES, sample_sets & 0xFF)
642
+
643
+ @property
644
+ def fifo_entries(self) -> int:
645
+ """Number of 16-bit words currently stored in the FIFO."""
646
+ low = self._read_u8(_REG_FIFO_ENTRIES_L)
647
+ high = self._read_u8(_REG_FIFO_ENTRIES_H) & _FIFO_ENTRIES_H_MASK
648
+ return (high << 8) | low
649
+
650
+ def read_fifo(self) -> list[tuple[str, int]]:
651
+ """Read and drain all available FIFO entries.
652
+
653
+ Returns a list of (channel_name, raw_signed_code) pairs, where
654
+ channel_name is one of "x", "y", "z", "temp_or_adc". Accel-channel
655
+ values use the same raw-code scale as `raw_acceleration`.
656
+ """
657
+ entries = self.fifo_entries
658
+ buffer = bytearray(entries * 2)
659
+ self._read_fifo_into(buffer)
660
+ result = []
661
+ for i in range(entries):
662
+ high, low = buffer[2 * i], buffer[2 * i + 1]
663
+ channel = high >> 6
664
+ raw = ((high & 0x3F) << 8) | low
665
+ if raw & 0x2000:
666
+ raw -= 0x4000
667
+ result.append((_FIFO_CHANNEL_NAMES[channel], raw))
668
+ return result
669
+
670
+ # -- self-test --
671
+
672
+ def self_test(self) -> bool:
673
+ """Run the built-in electrostatic self-test. Returns True if it passes."""
674
+ previous_mode = self.power_mode
675
+ self.power_mode = OpMode.MEASURE
676
+ delay = 4.0 / _DATA_RATE_HZ[self._data_rate]
677
+ both_bits = _SELF_TEST_ST | _SELF_TEST_ST_FORCE
678
+ try:
679
+ self._write_masked(_REG_SELF_TEST, _SELF_TEST_ST, both_bits)
680
+ time.sleep(delay)
681
+ before = self.raw_x
682
+ self._write_masked(_REG_SELF_TEST, both_bits, both_bits)
683
+ time.sleep(delay)
684
+ after = self.raw_x
685
+ finally:
686
+ self._write_u8(_REG_SELF_TEST, 0)
687
+ self.power_mode = previous_mode
688
+ diff = after - before
689
+ scale = _RANGE_SCALE_MULTIPLIER[self._range]
690
+ return _SELF_TEST_MIN * scale <= diff <= _SELF_TEST_MAX * scale
691
+
692
+
693
+ class ADXL366(ADXL367):
694
+ """Driver for the Analog Devices ADXL366 accelerometer.
695
+
696
+ Adds Z-axis nonlinearity compensation, activity/inactivity reference
697
+ readback, and a step-counting pedometer on top of everything `ADXL367`
698
+ provides - the ADXL367 silicon doesn't implement these three features.
699
+ """
700
+
701
+ _revid = _REVID_ADXL366
702
+
703
+ @property
704
+ def z_nonlinearity_compensation(self) -> bool:
705
+ """Z-axis temperature-informed nonlinearity compensation state."""
706
+ return bool(self._read_u8(_REG_TEMP_CTL) & _TEMP_CTL_NL_COMP_EN)
707
+
708
+ @z_nonlinearity_compensation.setter
709
+ def z_nonlinearity_compensation(self, enable: bool) -> None:
710
+ value = _TEMP_CTL_NL_COMP_EN if enable else 0
711
+ self._write_masked(_REG_TEMP_CTL, value, _TEMP_CTL_NL_COMP_EN)
712
+
713
+ def reference_readback(self, *, inactivity: bool = False) -> tuple[int, int, int]:
714
+ """Read back the activity/inactivity reference point.
715
+
716
+ Only meaningful when the corresponding detector is in referenced mode.
717
+ """
718
+ select = 0x02 if inactivity else 0x01
719
+ self._write_masked(
720
+ _REG_ACT_INACT_CTL,
721
+ select << 6,
722
+ _ACT_INACT_CTL_REF_READBACK_MASK,
723
+ )
724
+ try:
725
+ return self.raw_acceleration
726
+ finally:
727
+ self._write_masked(_REG_ACT_INACT_CTL, 0, _ACT_INACT_CTL_REF_READBACK_MASK)
728
+
729
+ @property
730
+ def pedometer_enabled(self) -> bool:
731
+ """Whether the step-counting pedometer is enabled."""
732
+ return bool(self._read_u8(_REG_PEDOMETER_CTL) & 0x01)
733
+
734
+ @pedometer_enabled.setter
735
+ def pedometer_enabled(self, enable: bool) -> None:
736
+ self._write_masked(_REG_PEDOMETER_CTL, 0x01 if enable else 0x00, 0x01)
737
+
738
+ @property
739
+ def steps(self) -> int:
740
+ """Cumulative pedometer step count."""
741
+ buffer = bytearray(2)
742
+ self._read_into(_REG_PEDOMETER_STEP_CNT_H, buffer)
743
+ return (buffer[0] << 8) | buffer[1]
744
+
745
+ def reset_steps(self) -> None:
746
+ """Reset the pedometer step count to zero."""
747
+ self._write_masked(_REG_PEDOMETER_CTL, 0x04, 0x04)
adxl36x/py.typed ADDED
File without changes
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: circuitpython-adxl36x
3
+ Version: 0.1.0
4
+ Summary: CircuitPython driver for the Analog Devices ADXL366/ADXL367 accelerometers
5
+ Keywords: adxl366,adxl367,accelerometer,blinka,circuitpython,hardware,i2c,micropython,spi
6
+ Author: Mike Mabey
7
+ Author-email: Mike Mabey <email@mikemabey.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Embedded Systems
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: System :: Hardware
16
+ Requires-Dist: adafruit-circuitpython-busdevice>=5.2.11
17
+ Requires-Dist: circuitpython-stubs>=10.2.1
18
+ Requires-Python: >=3.10
19
+ Project-URL: Homepage, https://github.com/mmabey/circuitpython-adxl36x
20
+ Project-URL: Repository, https://github.com/mmabey/circuitpython-adxl36x
21
+ Project-URL: Issues, https://github.com/mmabey/circuitpython-adxl36x/issues
22
+ Description-Content-Type: text/markdown
23
+
24
+ # circuitpython-adxl36x
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/circuitpython-adxl36x)](https://pypi.org/project/circuitpython-adxl36x/)
27
+ [![Python versions](https://img.shields.io/pypi/pyversions/circuitpython-adxl36x)](https://pypi.org/project/circuitpython-adxl36x/)
28
+ [![License: MIT](https://img.shields.io/pypi/l/circuitpython-adxl36x)](LICENSE)
29
+ [![CI](https://github.com/mmabey/circuitpython-adxl36x/actions/workflows/ci.yml/badge.svg)](https://github.com/mmabey/circuitpython-adxl36x/actions/workflows/ci.yml)
30
+ [![Downloads](https://img.shields.io/pypi/dm/circuitpython-adxl36x)](https://pypi.org/project/circuitpython-adxl36x/)
31
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
32
+
33
+ CircuitPython driver for the Analog Devices [ADXL366](https://www.analog.com/en/products/adxl366.html) and
34
+ [ADXL367](https://www.analog.com/en/products/adxl367.html) ultra-low-power 3-axis MEMS accelerometers.
35
+
36
+ `ADXL367` implements the full shared register map and is the base class. `ADXL366` subclasses it, adding the pedometer,
37
+ Z-axis nonlinearity compensation, and activity/inactivity reference readback - the three features the ADXL367 silicon
38
+ doesn't implement. Both support SPI and I2C.
39
+
40
+ ## Relationship to `adafruit_adxl34x`
41
+
42
+ This library's public API deliberately mirrors the shape of [Adafruit's
43
+ `adafruit_adxl34x`](https://github.com/adafruit/Adafruit_CircuitPython_ADXL34x) driver for the ADXL34x family
44
+ (`.acceleration`, `.events`, `enable_tap_detection()`, `enable_motion_detection()`, `data_rate`) so that code written
45
+ against a 34x-family chip needs minimal changes to run against a 366/367. The two chip families are **not**
46
+ register-compatible — only the driver's Python API shape is shared. Notable differences:
47
+
48
+ - `range` is named `g_range` here instead — `range` shadows the Python builtin, and compatibility with the ADXL34x API
49
+ takes a back seat to that on this one property.
50
+ - No `enable_freefall_detection()` — the 366/367 has no freefall-detection hardware.
51
+ - `Range` tops out at `RANGE_8_G` — the 366/367 doesn't support ±16g.
52
+ - `DataRate` only has 6 discrete rates (12.5 Hz-400 Hz) vs. the 34x's 16.
53
+ - `.events` includes many more keys (FIFO, pedometer, ADC/temperature thresholds) since the 366/367 exposes far more
54
+ interrupt sources.
55
+
56
+ ## Installation
57
+
58
+ On a CircuitPython board, copy `src/adxl36x/` into `CIRCUITPY/lib/adxl36x/`, along with the `adafruit_bus_device` bundle
59
+ library.
60
+
61
+ For host-side development (type checking, tests) in another project:
62
+
63
+ ```bash
64
+ uv add --editable ~/src/circuitpython-adxl36x
65
+ ```
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ uv sync
71
+ uv run ruff check --fix
72
+ uv run ruff format
73
+ uv run ty check
74
+ uv run pytest
75
+ ```
@@ -0,0 +1,6 @@
1
+ adxl36x/__init__.py,sha256=9fRNYWiTk-0KtlFIajzUFUpvdGkityVJil30pTatRxA,24919
2
+ adxl36x/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ circuitpython_adxl36x-0.1.0.dist-info/licenses/LICENSE,sha256=y6Dkapl6bO30fL3Dv-RKQnxT73xuLBeGTmiZdUljU5A,1067
4
+ circuitpython_adxl36x-0.1.0.dist-info/WHEEL,sha256=EmLkUISDECbcUx3FMCYOqokNOJqNp2r0d4mJzjErvvs,80
5
+ circuitpython_adxl36x-0.1.0.dist-info/METADATA,sha256=eZBVKMt-PTAEHthZTAxrYSJ9iocSAkuK8R2YLskkOHk,3783
6
+ circuitpython_adxl36x-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Mabey
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.