adafruit-circuitpython-tcs3448 1.0.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.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: adafruit-circuitpython-tcs3448
3
+ Version: 1.0.0
4
+ Summary: CircuitPython driver for the Adafruit TCS3448 14-Channel Light / Color Sensor Breakout - STEMMA QT / Qwiic
5
+ Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/adafruit/Adafruit_CircuitPython_TCS3448
8
+ Keywords: adafruit,blinka,circuitpython,micropython,tcs3448,light,,lux,,color
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: Programming Language :: Python :: 3
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+ Requires-Dist: Adafruit-Blinka
17
+ Requires-Dist: adafruit-circuitpython-busdevice
18
+ Requires-Dist: adafruit-circuitpython-register
19
+ Provides-Extra: optional
20
+ Dynamic: license-file
21
+
22
+ Introduction
23
+ ============
24
+
25
+
26
+ .. image:: https://readthedocs.org/projects/adafruit-circuitpython-tcs3448/badge/?version=latest
27
+ :target: https://docs.circuitpython.org/projects/tcs3448/en/latest/
28
+ :alt: Documentation Status
29
+
30
+
31
+ .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
32
+ :target: https://adafru.it/discord
33
+ :alt: Discord
34
+
35
+
36
+ .. image:: https://github.com/adafruit/Adafruit_CircuitPython_TCS3448/workflows/Build%20CI/badge.svg
37
+ :target: https://github.com/adafruit/Adafruit_CircuitPython_TCS3448/actions
38
+ :alt: Build Status
39
+
40
+
41
+ .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
42
+ :target: https://github.com/astral-sh/ruff
43
+ :alt: Code Style: Ruff
44
+
45
+ CircuitPython driver for the Adafruit TCS3448 14-Channel Light / Color Sensor Breakout - STEMMA QT / Qwiic
46
+
47
+
48
+ Dependencies
49
+ =============
50
+ This driver depends on:
51
+
52
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
53
+ * `Bus Device <https://github.com/adafruit/Adafruit_CircuitPython_BusDevice>`_
54
+ * `Register <https://github.com/adafruit/Adafruit_CircuitPython_Register>`_
55
+
56
+ Please ensure all dependencies are available on the CircuitPython filesystem.
57
+ This is easily achieved by downloading
58
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
59
+ or individual libraries can be installed using
60
+ `circup <https://github.com/adafruit/circup>`_.
61
+
62
+ `Purchase one from the Adafruit shop <http://www.adafruit.com/products/6525>`_
63
+
64
+ Installing from PyPI
65
+ =====================
66
+
67
+ On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
68
+ PyPI <https://pypi.org/project/adafruit-circuitpython-tcs3448/>`_.
69
+ To install for current user:
70
+
71
+ .. code-block:: shell
72
+
73
+ pip3 install adafruit-circuitpython-tcs3448
74
+
75
+ To install system-wide (this may be required in some cases):
76
+
77
+ .. code-block:: shell
78
+
79
+ sudo pip3 install adafruit-circuitpython-tcs3448
80
+
81
+ To install in a virtual environment in your current project:
82
+
83
+ .. code-block:: shell
84
+
85
+ mkdir project-name && cd project-name
86
+ python3 -m venv .venv
87
+ source .env/bin/activate
88
+ pip3 install adafruit-circuitpython-tcs3448
89
+
90
+ Installing to a Connected CircuitPython Device with Circup
91
+ ==========================================================
92
+
93
+ Make sure that you have ``circup`` installed in your Python environment.
94
+ Install it with the following command if necessary:
95
+
96
+ .. code-block:: shell
97
+
98
+ pip3 install circup
99
+
100
+ With ``circup`` installed and your CircuitPython device connected use the
101
+ following command to install:
102
+
103
+ .. code-block:: shell
104
+
105
+ circup install adafruit_tcs3448
106
+
107
+ Or the following command to update an existing version:
108
+
109
+ .. code-block:: shell
110
+
111
+ circup update
112
+
113
+ Usage Example
114
+ =============
115
+
116
+ .. code-block:: python
117
+
118
+ import time
119
+ import board
120
+ from adafruit_tcs3448 import TCS3448
121
+
122
+ i2c = board.I2C()
123
+ sensor = TCS3448(i2c)
124
+
125
+ CHANNEL_LABELS = [
126
+ "FZ (450nm blue)",
127
+ "FY (560nm yellow-green)",
128
+ "FXL (596nm orange)",
129
+ "NIR (855nm near-IR)",
130
+ "VIS_TL_0 (clear top-left, cycle 1)",
131
+ "VIS_BR_0 (clear btm-right, cycle 1)",
132
+ "F2 (424nm violet-blue)",
133
+ "F3 (473nm blue-cyan)",
134
+ "F4 (516nm green)",
135
+ "F6 (636nm red)",
136
+ "VIS_TL_1 (clear top-left, cycle 2)",
137
+ "VIS_BR_1 (clear btm-right, cycle 2)",
138
+ "F1 (407nm violet)",
139
+ "F7 (687nm deep red)",
140
+ "F8 (748nm near-IR edge)",
141
+ "F5 (546nm green-yellow)",
142
+ "VIS_TL_2 (clear top-left, cycle 3)",
143
+ "VIS_BR_2 (clear btm-right, cycle 3)",
144
+ ]
145
+
146
+ while True:
147
+ readings = sensor.all_channels
148
+ print("--- TCS3448 Channel Readings ---")
149
+ for label, value in zip(CHANNEL_LABELS, readings):
150
+ print(f" {label}: {value}")
151
+ print()
152
+ time.sleep(1)
153
+
154
+ Documentation
155
+ =============
156
+ API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/tcs3448/en/latest/>`_.
157
+
158
+ For information on building library documentation, please check out
159
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
160
+
161
+ Contributing
162
+ ============
163
+
164
+ Contributions are welcome! Please read our `Code of Conduct
165
+ <https://github.com/adafruit/Adafruit_CircuitPython_TCS3448/blob/HEAD/CODE_OF_CONDUCT.md>`_
166
+ before contributing to help this project stay welcoming.
@@ -0,0 +1,6 @@
1
+ adafruit_tcs3448.py,sha256=JCClG_CbKt3SqQn3d1hs2o1Y7IcLP_fSsOfa_45ir-g,24891
2
+ adafruit_circuitpython_tcs3448-1.0.0.dist-info/licenses/LICENSE,sha256=0JsO0yQOspYeKuhk2Y_s1wan9NO0DYeWSRJBrYJdeko,1100
3
+ adafruit_circuitpython_tcs3448-1.0.0.dist-info/METADATA,sha256=2kWp7dwT9aZz9HQT1mBy3Xijkeah2KW9MB5KUl-jYBQ,5424
4
+ adafruit_circuitpython_tcs3448-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ adafruit_circuitpython_tcs3448-1.0.0.dist-info/top_level.txt,sha256=v4wVUk1z4zAVwDNnj8Fyva3wsrKiHvtwcOeNHEws-nQ,17
6
+ adafruit_circuitpython_tcs3448-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Liz Clark for Adafruit Industries
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 @@
1
+ adafruit_tcs3448
adafruit_tcs3448.py ADDED
@@ -0,0 +1,720 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 Liz Clark for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ `adafruit_tcs3448`
6
+ ================================================================================
7
+
8
+ CircuitPython driver for the Adafruit TCS3448 14-Channel Light / Color Sensor Breakout
9
+
10
+
11
+ * Author(s): Liz Clark
12
+
13
+ Implementation Notes
14
+ --------------------
15
+
16
+ **Hardware:**
17
+
18
+ * `Adafruit TCS3448 14-Channel Light / Color Sensor Breakout <https://www.adafruit.com/product/6525>`_
19
+
20
+ **Software and Dependencies:**
21
+
22
+ * Adafruit CircuitPython firmware for the supported boards:
23
+ https://circuitpython.org/downloads
24
+
25
+ * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
26
+ * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
27
+ """
28
+
29
+ import time
30
+
31
+ from adafruit_bus_device import i2c_device
32
+ from adafruit_register.i2c_bit import ROBit, RWBit
33
+ from adafruit_register.i2c_bits import RWBits
34
+ from adafruit_register.i2c_struct import Struct, UnaryStruct
35
+ from adafruit_register.i2c_struct_array import StructArray
36
+ from micropython import const
37
+
38
+ try:
39
+ from typing import List, Tuple
40
+
41
+ import busio
42
+ except ImportError:
43
+ pass
44
+
45
+ __version__ = "1.0.0"
46
+ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_TCS3448.git"
47
+
48
+ _I2C_ADDR = const(0x59)
49
+ _CHIP_ID = const(0x81)
50
+ _AUXID = const(0x58)
51
+ _REVID = const(0x59)
52
+ _ID = const(0x5A)
53
+ _CFG12 = const(0x66)
54
+ _GPIO = const(0x6B)
55
+ _ENABLE = const(0x80)
56
+ _ATIME = const(0x81)
57
+ _WTIME = const(0x83)
58
+ _TH_L = const(0x84)
59
+ _TH_H = const(0x86)
60
+ _STATUS2 = const(0x90)
61
+ _STATUS = const(0x93)
62
+ _ASTATUS = const(0x94)
63
+ _DATA = const(0x95)
64
+ _CFG0 = const(0xBF)
65
+ _CFG1 = const(0xC6)
66
+ _LED = const(0xCD)
67
+ _PERS = const(0xCF)
68
+ _ASTEP = const(0xD4)
69
+ _CFG20 = const(0xD6)
70
+ _AZ_CFG = const(0xDE)
71
+ _FD_STATUS = const(0xE3)
72
+ _INTENAB = const(0xF9)
73
+ _CONTROL = const(0xFA)
74
+
75
+
76
+ class CV:
77
+ """Constant-value helper used as an enum base class.
78
+
79
+ Subclasses define integer class attributes; :meth:`valid` and
80
+ :meth:`get_name` provide validation and reverse-lookup.
81
+ """
82
+
83
+ @classmethod
84
+ def valid(cls, value: int) -> bool:
85
+ """Return ``True`` if *value* is a defined member of this class."""
86
+ for name, member in cls.__dict__.items():
87
+ if name.startswith("_") or callable(member):
88
+ continue
89
+ if member == value:
90
+ return True
91
+ return False
92
+
93
+ @classmethod
94
+ def get_name(cls, value: int) -> str:
95
+ """Return the attribute name whose value equals *value*."""
96
+ for name, member in cls.__dict__.items():
97
+ if name.startswith("_") or callable(member):
98
+ continue
99
+ if member == value:
100
+ return name
101
+ raise KeyError(value)
102
+
103
+
104
+ class Gain(CV):
105
+ """Analog gain settings for the ALS engines
106
+
107
+ Higher gain increases sensitivity in low light but saturates sooner in
108
+ bright light. The multiplier for a gain code is ``0.5 * 2 ** code``.
109
+
110
+ +---------------------------+----------+
111
+ | Setting | Gain |
112
+ +===========================+==========+
113
+ | :py:const:`Gain.X0_5` | 0.5x |
114
+ +---------------------------+----------+
115
+ | :py:const:`Gain.X1` | 1x |
116
+ +---------------------------+----------+
117
+ | :py:const:`Gain.X2` | 2x |
118
+ +---------------------------+----------+
119
+ | :py:const:`Gain.X4` | 4x |
120
+ +---------------------------+----------+
121
+ | :py:const:`Gain.X8` | 8x |
122
+ +---------------------------+----------+
123
+ | :py:const:`Gain.X16` | 16x |
124
+ +---------------------------+----------+
125
+ | :py:const:`Gain.X32` | 32x |
126
+ +---------------------------+----------+
127
+ | :py:const:`Gain.X64` | 64x |
128
+ +---------------------------+----------+
129
+ | :py:const:`Gain.X128` | 128x |
130
+ +---------------------------+----------+
131
+ | :py:const:`Gain.X256` | 256x |
132
+ +---------------------------+----------+
133
+ | :py:const:`Gain.X512` | 512x |
134
+ +---------------------------+----------+
135
+ | :py:const:`Gain.X1024` | 1024x |
136
+ +---------------------------+----------+
137
+ | :py:const:`Gain.X2048` | 2048x |
138
+ +---------------------------+----------+
139
+
140
+ Default applied by the driver.
141
+ """
142
+
143
+ X0_5 = 0
144
+ X1 = 1
145
+ X2 = 2
146
+ X4 = 3
147
+ X8 = 4
148
+ X16 = 5
149
+ X32 = 6
150
+ X64 = 7
151
+ X128 = 8
152
+ X256 = 9 # driver default
153
+ X512 = 10
154
+ X1024 = 11
155
+ X2048 = 12
156
+
157
+
158
+ class SmuxMode(CV):
159
+ """Automatic SMUX channel-cycling mode (CFG20 bits 6:5).
160
+
161
+ The auto-SMUX hardware cycles through several SMUX configurations within a
162
+ single measurement, filling six data registers per cycle.
163
+
164
+ +-----------------------------+--------------------------------------------+
165
+ | Setting | Results produced |
166
+ +=============================+============================================+
167
+ | :py:const:`SmuxMode.CH6` | 6 results (1 cycle) |
168
+ +-----------------------------+--------------------------------------------+
169
+ | :py:const:`SmuxMode.CH12` | 12 results (2 cycles) |
170
+ +-----------------------------+--------------------------------------------+
171
+ | :py:const:`SmuxMode.CH18` | 18 results (3 cycles) * |
172
+ +-----------------------------+--------------------------------------------+
173
+
174
+ Default applied by the driver.
175
+ """
176
+
177
+ CH6 = 0 # FZ, FY, FXL, NIR, 2x VIS
178
+ CH12 = 2 # adds F2, F3, F4, F6, 2x VIS
179
+ CH18 = 3 # adds F1, F7, F8, F5, 2x VIS — driver default
180
+
181
+
182
+ class Channel(CV):
183
+ """Result index within the 18-result automatic SMUX data array.
184
+
185
+ The index is the position in the data registers, which follows the order
186
+ the auto-SMUX engine fills them rather than wavelength order. The index
187
+ order matches the Arduino driver's channel constants.
188
+
189
+ +----------------------------------+--------+---------------+-------------+
190
+ | Setting | Index | Peak | FWHM |
191
+ +==================================+========+===============+=============+
192
+ | :py:const:`Channel.FZ` | 0 | 450 nm | 67 nm |
193
+ +----------------------------------+--------+---------------+-------------+
194
+ | :py:const:`Channel.FY` | 1 | 560 nm | 123 nm |
195
+ +----------------------------------+--------+---------------+-------------+
196
+ | :py:const:`Channel.FXL` | 2 | 596 nm | 93 nm |
197
+ +----------------------------------+--------+---------------+-------------+
198
+ | :py:const:`Channel.NIR` | 3 | 855 nm | 61 nm |
199
+ +----------------------------------+--------+---------------+-------------+
200
+ | :py:const:`Channel.VIS_TL_0` | 4 | clear, top-left, cycle 1 |
201
+ +----------------------------------+--------+-----------------------------+
202
+ | :py:const:`Channel.VIS_BR_0` | 5 | clear, both-right, cycle 1 |
203
+ +----------------------------------+--------+---------------+-------------+
204
+ | :py:const:`Channel.F2` | 6 | 424 nm | 29 nm |
205
+ +----------------------------------+--------+---------------+-------------+
206
+ | :py:const:`Channel.F3` | 7 | 473 nm | 38 nm |
207
+ +----------------------------------+--------+---------------+-------------+
208
+ | :py:const:`Channel.F4` | 8 | 516 nm | 48 nm |
209
+ +----------------------------------+--------+---------------+-------------+
210
+ | :py:const:`Channel.F6` | 9 | 636 nm | 58 nm |
211
+ +----------------------------------+--------+---------------+-------------+
212
+ | :py:const:`Channel.VIS_TL_1` | 10 | clear, top-left, cycle 2 |
213
+ +----------------------------------+--------+-----------------------------+
214
+ | :py:const:`Channel.VIS_BR_1` | 11 | clear, both-right, cycle 2 |
215
+ +----------------------------------+--------+---------------+-------------+
216
+ | :py:const:`Channel.F1` | 12 | 407 nm | 28 nm |
217
+ +----------------------------------+--------+---------------+-------------+
218
+ | :py:const:`Channel.F7` | 13 | 687 nm | 63 nm |
219
+ +----------------------------------+--------+---------------+-------------+
220
+ | :py:const:`Channel.F8` | 14 | 748 nm | 77 nm |
221
+ +----------------------------------+--------+---------------+-------------+
222
+ | :py:const:`Channel.F5` | 15 | 546 nm | 44 nm |
223
+ +----------------------------------+--------+---------------+-------------+
224
+ | :py:const:`Channel.VIS_TL_2` | 16 | clear, top-left, cycle 3 |
225
+ +----------------------------------+--------+-----------------------------+
226
+ | :py:const:`Channel.VIS_BR_2` | 17 | clear, both-right, cycle 3 |
227
+ +----------------------------------+--------+-----------------------------+
228
+
229
+ In :py:const:`SmuxMode.CH12` only indices 0-11 are produced, and in
230
+ :py:const:`SmuxMode.CH6` only indices 0-5.
231
+ """
232
+
233
+ FZ = 0
234
+ FY = 1
235
+ FXL = 2
236
+ NIR = 3
237
+ VIS_TL_0 = 4
238
+ VIS_BR_0 = 5
239
+ F2 = 6
240
+ F3 = 7
241
+ F4 = 8
242
+ F6 = 9
243
+ VIS_TL_1 = 10
244
+ VIS_BR_1 = 11
245
+ F1 = 12
246
+ F7 = 13
247
+ F8 = 14
248
+ F5 = 15
249
+ VIS_TL_2 = 16
250
+ VIS_BR_2 = 17
251
+
252
+
253
+ class FlickerFreq(CV):
254
+ """Flicker detection result returned by :attr:`TCS3448.flicker_frequency`.
255
+
256
+ +-------------------------------+----------------------------+
257
+ | Setting | Meaning |
258
+ +===============================+============================+
259
+ | :py:const:`FlickerFreq.NONE` | No flicker detected |
260
+ +-------------------------------+----------------------------+
261
+ | :py:const:`FlickerFreq.HZ100` | 100 Hz mains flicker |
262
+ +-------------------------------+----------------------------+
263
+ | :py:const:`FlickerFreq.HZ120` | 120 Hz mains flicker |
264
+ +-------------------------------+----------------------------+
265
+ """
266
+
267
+ NONE = 0
268
+ HZ100 = 100
269
+ HZ120 = 120
270
+
271
+
272
+ class Measurement:
273
+ """One set of spectral results.
274
+
275
+ Supports indexing and ``len()``, so a :class:`Channel` constant can be
276
+ used directly::
277
+
278
+ blue = sensor.measurement[Channel.FZ]
279
+
280
+ :param tuple channels: Raw ADC counts in auto-SMUX storage order.
281
+ :param int gain: Gain code that was applied to these counts.
282
+ :param bool saturated: ``True`` when the frame is affected by saturation.
283
+ """
284
+
285
+ def __init__(self, channels: Tuple[int, ...], gain: int, saturated: bool) -> None:
286
+ self.channels = channels
287
+ """Raw 16-bit ADC counts indexed by :class:`Channel`."""
288
+
289
+ self.gain = gain
290
+ """:class:`Gain` code reported for this exact frame."""
291
+
292
+ self.saturated = saturated
293
+ """``True`` when analog or digital saturation affected this frame."""
294
+
295
+ @property
296
+ def gain_multiplier(self) -> float:
297
+ """Gain applied to this frame as a multiplier, e.g. ``256.0``."""
298
+ return 0.5 * (2**self.gain)
299
+
300
+ def __getitem__(self, index: int) -> int:
301
+ """Return the count for *index*, a :class:`Channel` constant."""
302
+ return self.channels[index]
303
+
304
+ def __len__(self) -> int:
305
+ """Return the number of results in this frame."""
306
+ return len(self.channels)
307
+
308
+ def __repr__(self) -> str:
309
+ """Return a readable one-line summary of the frame."""
310
+ gain = self.gain_multiplier
311
+ return f"<Measurement gain={gain}x saturated={self.saturated} {self.channels}>"
312
+
313
+
314
+ class _Bank1:
315
+ def __init__(self, sensor: "TCS3448") -> None:
316
+ self._sensor = sensor
317
+
318
+ def __enter__(self) -> None:
319
+ self._sensor._reg_bank = True
320
+
321
+ def __exit__(self, *args) -> None:
322
+ self._sensor._reg_bank = False
323
+
324
+
325
+ class TCS3448: # noqa: PLR0904
326
+ """CircuitPython driver for the ams OSRAM TCS3448 multi-spectral sensor.
327
+
328
+ :param ~busio.I2C i2c_bus: The I2C bus the device is connected to.
329
+ :param int address: I2C address. Defaults to :const:`0x59`.
330
+ """
331
+
332
+ _reg_bank = RWBit(_CFG0, 4) # 1 = bank 1, 0 = bank 0
333
+
334
+ low_power_enabled = RWBit(_CFG0, 5)
335
+ """``True`` when the sensor idles in low-power mode between measurements."""
336
+
337
+ power_enabled = RWBit(_ENABLE, 0)
338
+ """``True`` when the internal oscillator is running (PON)."""
339
+
340
+ spectral_measurement_enabled = RWBit(_ENABLE, 1)
341
+ """``True`` when the spectral measurement engine is running (ALS_EN)."""
342
+
343
+ wait_enabled = RWBit(_ENABLE, 3)
344
+ """``True`` when the :attr:`wtime` delay between measurements is applied."""
345
+
346
+ flicker_detection_enabled = RWBit(_ENABLE, 6)
347
+ """``True`` when flicker detection is enabled."""
348
+
349
+ atime = UnaryStruct(_ATIME, "B")
350
+ """Number of integration steps, 0-255."""
351
+
352
+ wtime = UnaryStruct(_WTIME, "B")
353
+ """Delay between consecutive measurements, 0-255."""
354
+
355
+ spectral_threshold_low = UnaryStruct(_TH_L, "<H")
356
+ """16-bit low threshold for the spectral interrupt."""
357
+
358
+ spectral_threshold_high = UnaryStruct(_TH_H, "<H")
359
+ """16-bit high threshold for the spectral interrupt."""
360
+
361
+ data_ready = ROBit(_STATUS2, 6)
362
+ """``True`` when a complete measurement is available (AVALID)."""
363
+
364
+ digital_saturated = ROBit(_STATUS2, 4)
365
+ """``True`` when an ADC counter hit its maximum during the last integration."""
366
+
367
+ analog_saturated = ROBit(_STATUS2, 3)
368
+ """``True`` when the analogue front end saturated during the last integration."""
369
+
370
+ status = UnaryStruct(_STATUS, "B")
371
+ """Raw value of the main STATUS register."""
372
+
373
+ _astatus = UnaryStruct(_ASTATUS, "B")
374
+ _frame_6ch = Struct(_ASTATUS, "<B6H")
375
+ _frame_12ch = Struct(_ASTATUS, "<B12H")
376
+ _frame_18ch = Struct(_ASTATUS, "<B18H")
377
+ _channel_data = StructArray(_DATA, "<H", 18)
378
+
379
+ _again = RWBits(5, _CFG1, 0)
380
+ _auto_smux = RWBits(2, _CFG20, 5)
381
+
382
+ led_enabled = RWBit(_LED, 7)
383
+ """``True`` when the LED driver sinks current through the LDR pin."""
384
+
385
+ _led_drive = RWBits(7, _LED, 0)
386
+ _persistence = RWBits(4, _PERS, 0)
387
+ _astep = UnaryStruct(_ASTEP, "<H")
388
+ _az_config = UnaryStruct(_AZ_CFG, "B")
389
+
390
+ flicker_status = UnaryStruct(_FD_STATUS, "B")
391
+ """Raw value of the flicker detection status register.
392
+
393
+ :attr:`flicker_frequency` decodes the detection bits.
394
+ """
395
+
396
+ system_interrupt_enabled = RWBit(_INTENAB, 0)
397
+ """``True`` when flicker-status and SMUX-completion interrupts are enabled."""
398
+
399
+ fifo_interrupt_enabled = RWBit(_INTENAB, 2)
400
+ """``True`` when the FIFO threshold interrupt is enabled."""
401
+
402
+ spectral_interrupt_enabled = RWBit(_INTENAB, 3)
403
+ """``True`` when the spectral threshold interrupt is enabled."""
404
+
405
+ _sw_reset = RWBit(_CONTROL, 3)
406
+ _part_id = UnaryStruct(_ID, "B")
407
+ _revid = UnaryStruct(_REVID, "B")
408
+ _auxid = UnaryStruct(_AUXID, "B")
409
+ _gpio_in = ROBit(_GPIO, 0)
410
+ _gpio_out = RWBit(_GPIO, 1)
411
+ _gpio_in_en = RWBit(_GPIO, 2)
412
+ _gpio_inv = RWBit(_GPIO, 3)
413
+ _th_ch = RWBits(3, _CFG12, 0)
414
+
415
+ def __init__(self, i2c_bus: "busio.I2C", address: int = _I2C_ADDR) -> None:
416
+ self.i2c_device = i2c_device.I2CDevice(i2c_bus, address)
417
+ self._bank1 = _Bank1(self)
418
+
419
+ time.sleep(0.001)
420
+
421
+ with self._bank1:
422
+ chip_id = self._part_id
423
+
424
+ if chip_id != _CHIP_ID:
425
+ raise RuntimeError(
426
+ f"Failed to find TCS3448 at 0x{address:02X} - check wiring! Got ID 0x{chip_id:02X}"
427
+ )
428
+
429
+ self.read_timeout: int = 1000
430
+ """Milliseconds :attr:`measurement` waits for :attr:`data_ready`."""
431
+
432
+ self.reset()
433
+
434
+ def reset(self) -> None:
435
+ """Force a power-on reset and re-apply the driver defaults.
436
+
437
+ :raises RuntimeError: If the sensor does not answer after the reset.
438
+ """
439
+ self._sw_reset = True
440
+
441
+ time.sleep(0.001)
442
+ for _ in range(250):
443
+ try:
444
+ with self.i2c_device as i2c:
445
+ i2c.write(b"")
446
+ break
447
+ except OSError:
448
+ time.sleep(0.001)
449
+ else:
450
+ raise RuntimeError("TCS3448 did not respond after a software reset")
451
+
452
+ self.power_enabled = True
453
+ self.gain = Gain.X256
454
+ self.atime = 29
455
+ self.astep = 599
456
+
457
+ self.smux_mode = SmuxMode.CH18
458
+
459
+ with self._bank1:
460
+ self._gpio_in_en = False # output mode
461
+
462
+ self.led_enabled = False
463
+
464
+ @property
465
+ def gain(self) -> int:
466
+ """Analog gain applied to the spectral measurement.
467
+
468
+ Must be a :class:`Gain` constant, e.g. ``Gain.X256``. Defaults to
469
+ :attr:`Gain.X256`. The gain in effect for a particular frame is
470
+ reported by :attr:`Measurement.gain`.
471
+ """
472
+ return self._again
473
+
474
+ @gain.setter
475
+ def gain(self, value: int) -> None:
476
+ if not Gain.valid(value):
477
+ raise ValueError("gain must be a Gain constant")
478
+ self._again = value
479
+
480
+ @property
481
+ def astep(self) -> int:
482
+ """Integration step size, 0-65534.
483
+
484
+ Each step is ``(astep + 1) * 2.78 us``. 65535 is reserved by the
485
+ hardware. See :attr:`integration_time_ms` for the resulting time.
486
+ """
487
+ return self._astep
488
+
489
+ @astep.setter
490
+ def astep(self, value: int) -> None:
491
+ if not 0 <= value <= 65534:
492
+ raise ValueError("astep must be 0-65534 (65535 is reserved)")
493
+ self._astep = value
494
+
495
+ @property
496
+ def integration_time_ms(self) -> float:
497
+ """Integration time in milliseconds (read-only).
498
+
499
+ ``(atime + 1) * (astep + 1) * 2.78 us``. Change it by setting
500
+ :attr:`atime` or :attr:`astep`.
501
+ """
502
+ return (self.atime + 1) * (self._astep + 1) * 0.00278
503
+
504
+ @property
505
+ def smux_mode(self) -> int:
506
+ """Automatic SMUX cycling mode.
507
+
508
+ Must be a :class:`SmuxMode` constant. Defaults to
509
+ :attr:`SmuxMode.CH18`. Only change this while no measurement is
510
+ running.
511
+ """
512
+ return self._auto_smux
513
+
514
+ @smux_mode.setter
515
+ def smux_mode(self, value: int) -> None:
516
+ if not SmuxMode.valid(value):
517
+ raise ValueError("smux_mode must be a SmuxMode constant")
518
+ self._auto_smux = value
519
+
520
+ @property
521
+ def channel_count(self) -> int:
522
+ """Number of results the current :attr:`smux_mode` produces (read-only)."""
523
+ mode = self._auto_smux
524
+ if mode == SmuxMode.CH18:
525
+ return 18
526
+ if mode == SmuxMode.CH12:
527
+ return 12
528
+ return 6
529
+
530
+ @property
531
+ def measurement(self) -> Measurement:
532
+ """Run one measurement and return it as a :class:`Measurement`.
533
+
534
+ Stops any measurement in progress, clears stale status, triggers a
535
+ single measurement and waits for :attr:`data_ready` before reading.
536
+ The number of results follows :attr:`channel_count`.
537
+
538
+ :raises TimeoutError: If no result arrives within :attr:`read_timeout`
539
+ milliseconds.
540
+ """
541
+ self.spectral_measurement_enabled = False
542
+ self.clear_status()
543
+ _ = self._astatus
544
+
545
+ self.spectral_measurement_enabled = True
546
+
547
+ deadline = time.monotonic() + self.read_timeout / 1000
548
+ while not self.data_ready:
549
+ if time.monotonic() > deadline:
550
+ self.spectral_measurement_enabled = False
551
+ raise TimeoutError("Timed out waiting for TCS3448 data ready")
552
+ time.sleep(0.001)
553
+
554
+ frame = self.last_measurement
555
+
556
+ self.spectral_measurement_enabled = False
557
+ return frame
558
+
559
+ @property
560
+ def last_measurement(self) -> Measurement:
561
+ """The frame currently held in the data registers, read without waiting."""
562
+ count = self.channel_count
563
+ if count == 18:
564
+ frame = self._frame_18ch
565
+ elif count == 12:
566
+ frame = self._frame_12ch
567
+ else:
568
+ frame = self._frame_6ch
569
+
570
+ astatus = frame[0]
571
+ return Measurement(frame[1:], astatus & 0x0F, bool(astatus & 0x80))
572
+
573
+ @property
574
+ def all_channels(self) -> List[int]:
575
+ """Run one measurement and return just the raw counts.
576
+
577
+ :raises TimeoutError: If no result arrives within :attr:`read_timeout`
578
+ milliseconds.
579
+ """
580
+ return list(self.measurement.channels)
581
+
582
+ def channel(self, channel: int) -> int:
583
+ """A single result from the data registers without waiting.
584
+
585
+ :param int channel: Result index, a :class:`Channel` constant (0-17).
586
+ :returns: Raw 16-bit ADC count.
587
+ """
588
+ if not 0 <= channel <= 17:
589
+ raise ValueError("channel must be 0-17 (use Channel constants)")
590
+
591
+ _ = self._astatus # latch the data registers
592
+ return self._channel_data[channel][0]
593
+
594
+ @property
595
+ def led_current_ma(self) -> int:
596
+ """LED drive current in milliamps, 4-258.
597
+
598
+ The hardware steps in 2 mA increments as ``4 + register * 2``, so odd
599
+ values round down. Enable the driver with :attr:`led_enabled`.
600
+ """
601
+ return 4 + (self._led_drive * 2)
602
+
603
+ @led_current_ma.setter
604
+ def led_current_ma(self, current_ma: int) -> None:
605
+ current_ma = max(4, min(258, current_ma))
606
+ self._led_drive = (current_ma - 4) // 2
607
+
608
+ @property
609
+ def flicker_frequency(self) -> int:
610
+ """Detected flicker frequency as a :class:`FlickerFreq` value.
611
+
612
+ Returns :attr:`FlickerFreq.HZ100`, :attr:`FlickerFreq.HZ120` or
613
+ :attr:`FlickerFreq.NONE`, reporting a frequency only when the
614
+ matching valid bit is also set. Requires
615
+ :attr:`flicker_detection_enabled`.
616
+ """
617
+ status = self.flicker_status
618
+ if (status & 0x08) and (status & 0x02):
619
+ return FlickerFreq.HZ120
620
+ if (status & 0x04) and (status & 0x01):
621
+ return FlickerFreq.HZ100
622
+ return FlickerFreq.NONE
623
+
624
+ def clear_status(self) -> None:
625
+ """Clear all flags in the main STATUS register."""
626
+ self.status = self.status
627
+
628
+ @property
629
+ def persistence(self) -> int:
630
+ """Consecutive out-of-threshold measurements needed to raise an interrupt.
631
+
632
+ Valid range 0-15. 0 raises an interrupt on every cycle.
633
+ """
634
+ return self._persistence
635
+
636
+ @persistence.setter
637
+ def persistence(self, value: int) -> None:
638
+ if not 0 <= value <= 15:
639
+ raise ValueError("persistence must be 0-15")
640
+ self._persistence = value
641
+
642
+ @property
643
+ def auto_zero_frequency(self) -> int:
644
+ """Measurement cycles between automatic zero-offset calibrations, 0-255.
645
+
646
+ * ``0`` — never, which is not recommended
647
+ * ``1`` — every cycle
648
+ * ``255`` — only before the first measurement, the hardware default
649
+ """
650
+ return self._az_config
651
+
652
+ @auto_zero_frequency.setter
653
+ def auto_zero_frequency(self, value: int) -> None:
654
+ if not 0 <= value <= 255:
655
+ raise ValueError("auto_zero_frequency must be 0-255")
656
+ self._az_config = value
657
+
658
+ @property
659
+ def threshold_channel(self) -> int:
660
+ """ADC channel 0-5 compared against the spectral interrupt thresholds."""
661
+ with self._bank1:
662
+ return self._th_ch
663
+
664
+ @threshold_channel.setter
665
+ def threshold_channel(self, channel: int) -> None:
666
+ if not 0 <= channel <= 5:
667
+ raise ValueError("threshold_channel must be 0-5")
668
+ with self._bank1:
669
+ self._th_ch = channel
670
+
671
+ @property
672
+ def gpio_output_mode(self) -> bool:
673
+ """``True`` when the GPIO pin is an output, ``False`` when an input."""
674
+ with self._bank1:
675
+ return not self._gpio_in_en
676
+
677
+ @gpio_output_mode.setter
678
+ def gpio_output_mode(self, output: bool) -> None:
679
+ with self._bank1:
680
+ self._gpio_in_en = not output
681
+
682
+ @property
683
+ def gpio_value(self) -> bool:
684
+ """State of the GPIO pin."""
685
+ with self._bank1:
686
+ return self._gpio_in
687
+
688
+ @gpio_value.setter
689
+ def gpio_value(self, high: bool) -> None:
690
+ with self._bank1:
691
+ self._gpio_out = high
692
+
693
+ @property
694
+ def gpio_inverted(self) -> bool:
695
+ """``True`` when the GPIO output polarity is inverted."""
696
+ with self._bank1:
697
+ return self._gpio_inv
698
+
699
+ @gpio_inverted.setter
700
+ def gpio_inverted(self, invert: bool) -> None:
701
+ with self._bank1:
702
+ self._gpio_inv = invert
703
+
704
+ @property
705
+ def part_id(self) -> int:
706
+ """Part ID register value, ``0x81`` on the TCS3448."""
707
+ with self._bank1:
708
+ return self._part_id
709
+
710
+ @property
711
+ def revision_id(self) -> int:
712
+ """Silicon revision."""
713
+ with self._bank1:
714
+ return self._revid & 0x07
715
+
716
+ @property
717
+ def aux_id(self) -> int:
718
+ """Auxiliary ID."""
719
+ with self._bank1:
720
+ return self._auxid & 0x0F