720 lines
28 KiB
Python
720 lines
28 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2025 Tim C for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
"""
|
|
`adafruit_opt4048`
|
|
================================================================================
|
|
|
|
CircuitPython driver library for the Adafruit OPT4048 breakout board, a high-speed,
|
|
high-precision tristimulus XYZ color sensor.
|
|
|
|
|
|
* Author(s): Tim C
|
|
|
|
Implementation Notes
|
|
--------------------
|
|
|
|
**Hardware:**
|
|
|
|
* `Adafruit OPT4048 Breakout <https://www.adafruit.com/products/6335>`_
|
|
|
|
**Software and Dependencies:**
|
|
|
|
* Adafruit CircuitPython firmware for the supported boards:
|
|
https://circuitpython.org/downloads
|
|
# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
|
|
# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
|
|
"""
|
|
|
|
# imports
|
|
|
|
__version__ = "0.0.0+auto.0"
|
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_OPT4048.git"
|
|
|
|
from time import sleep
|
|
|
|
from adafruit_bus_device import i2c_device
|
|
from adafruit_register.i2c_bit import RWBit
|
|
from adafruit_register.i2c_bits import ROBits, RWBits
|
|
from adafruit_register.i2c_struct import UnaryStruct
|
|
from micropython import const
|
|
|
|
try:
|
|
from typing import ClassVar, Optional, Tuple
|
|
|
|
import busio
|
|
except ImportError:
|
|
pass
|
|
|
|
__version__ = "0.0.0+auto.0"
|
|
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_OPT4048.git"
|
|
|
|
# Correct content of DEVICE_ID register
|
|
_OPT4048_CHIP_ID = const(0x0821) # OPT4048 default device id
|
|
|
|
|
|
class CV:
|
|
"""Constant value class helper for enums."""
|
|
|
|
# @classmethod
|
|
# def add_values(cls, value_tuples: Tuple[str, int, str, Optional[float]]) -> None:
|
|
# """Add CV values to the class"""
|
|
# cls.string = {}
|
|
# cls.lsb = {}
|
|
#
|
|
# for value_tuple in value_tuples:
|
|
# name, value, string, lsb = value_tuple
|
|
# setattr(cls, name, value)
|
|
# cls.string[value] = string
|
|
# cls.lsb[value] = lsb
|
|
|
|
@classmethod
|
|
def is_valid(cls, value: int) -> bool:
|
|
"""Validate that a given value is a member"""
|
|
IGNORE_LIST = [cls.__module__, cls.__name__]
|
|
print(IGNORE_LIST)
|
|
print(cls.__dict__.values())
|
|
if value in cls.__dict__.values() and value not in IGNORE_LIST:
|
|
return True
|
|
return False
|
|
|
|
|
|
class Range(CV):
|
|
"""Available range settings for the OPT4048 sensor.
|
|
|
|
Full-scale light level ranges as described in datasheet page 29.
|
|
|
|
+------------------------------+-------------------------+
|
|
| Setting | Range Value |
|
|
+==============================+=========================+
|
|
| :py:const:`Range.RANGE_2K` | 2.2 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_4K` | 4.5 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_9K` | 9 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_18K` | 18 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_36K` | 36 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_72K` | 72 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.RANGE_144K` | 144 klux |
|
|
+------------------------------+-------------------------+
|
|
| :py:const:`Range.AUTO` | Auto-range |
|
|
+------------------------------+-------------------------+
|
|
"""
|
|
|
|
RANGE_2K = 0 # 2.2 klux
|
|
RANGE_4K = 1 # 4.5 klux
|
|
RANGE_9K = 2 # 9 klux
|
|
RANGE_18K = 3 # 18 klux
|
|
RANGE_36K = 4 # 36 klux
|
|
RANGE_72K = 5 # 72 klux
|
|
RANGE_144K = 6 # 144 klux
|
|
AUTO = 12 # Auto-range
|
|
|
|
|
|
class ConversionTime(CV):
|
|
"""Available conversion time settings for the OPT4048 sensor.
|
|
|
|
These control the device conversion time per channel as described in datasheet page 29.
|
|
|
|
+------------------------------------------+-------------------------+
|
|
| Setting | Time |
|
|
+==========================================+=========================+
|
|
| :py:const:`ConversionTime.TIME_600US` | 600 microseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_1MS` | 1 millisecond |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_1_8MS` | 1.8 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_3_4MS` | 3.4 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_6_5MS` | 6.5 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_12_7MS` | 12.7 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_25MS` | 25 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_50MS` | 50 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_100MS` | 100 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_200MS` | 200 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_400MS` | 400 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
| :py:const:`ConversionTime.TIME_800MS` | 800 milliseconds |
|
|
+------------------------------------------+-------------------------+
|
|
"""
|
|
|
|
TIME_600US = 0 # 600 microseconds
|
|
TIME_1MS = 1 # 1 millisecond
|
|
TIME_1_8MS = 2 # 1.8 milliseconds
|
|
TIME_3_4MS = 3 # 3.4 milliseconds
|
|
TIME_6_5MS = 4 # 6.5 milliseconds
|
|
TIME_12_7MS = 5 # 12.7 milliseconds
|
|
TIME_25MS = 6 # 25 milliseconds
|
|
TIME_50MS = 7 # 50 milliseconds
|
|
TIME_100MS = 8 # 100 milliseconds
|
|
TIME_200MS = 9 # 200 milliseconds
|
|
TIME_400MS = 10 # 400 milliseconds
|
|
TIME_800MS = 11 # 800 milliseconds
|
|
|
|
|
|
class Mode(CV):
|
|
"""Available operating mode settings for the OPT4048 sensor.
|
|
|
|
Controls the device mode of operation as described in datasheet page 29.
|
|
|
|
+--------------------------------------+-------------------------+
|
|
| Setting | Mode |
|
|
+======================================+=========================+
|
|
| :py:const:`Mode.POWERDOWN` | Power-down mode |
|
|
+--------------------------------------+-------------------------+
|
|
| :py:const:`Mode.AUTO_ONESHOT` | Auto-range one-shot |
|
|
+--------------------------------------+-------------------------+
|
|
| :py:const:`Mode.ONESHOT` | One-shot mode |
|
|
+--------------------------------------+-------------------------+
|
|
| :py:const:`Mode.CONTINUOUS` | Continuous mode |
|
|
+--------------------------------------+-------------------------+
|
|
"""
|
|
|
|
POWERDOWN = 0 # Power-down mode
|
|
AUTO_ONESHOT = 1 # Forced auto-range one-shot mode
|
|
ONESHOT = 2 # One-shot mode
|
|
CONTINUOUS = 3 # Continuous mode
|
|
|
|
|
|
class FaultCount(CV):
|
|
"""Available fault count settings for the OPT4048 sensor.
|
|
|
|
Controls how many consecutive fault events are needed to trigger an interrupt.
|
|
|
|
+------------------------------------+-------------------------+
|
|
| Setting | Count |
|
|
+====================================+=========================+
|
|
| :py:const:`FaultCount.COUNT_1` | 1 fault count (default) |
|
|
+------------------------------------+-------------------------+
|
|
| :py:const:`FaultCount.COUNT_2` | 2 consecutive faults |
|
|
+------------------------------------+-------------------------+
|
|
| :py:const:`FaultCount.COUNT_4` | 4 consecutive faults |
|
|
+------------------------------------+-------------------------+
|
|
| :py:const:`FaultCount.COUNT_8` | 8 consecutive faults |
|
|
+------------------------------------+-------------------------+
|
|
"""
|
|
|
|
COUNT_1 = 0 # 1 fault count (default)
|
|
COUNT_2 = 1 # 2 consecutive fault counts
|
|
COUNT_4 = 2 # 4 consecutive fault counts
|
|
COUNT_8 = 3 # 8 consecutive fault counts
|
|
|
|
|
|
class IntConfig(CV):
|
|
"""Interrupt configuration settings for the OPT4048 sensor.
|
|
|
|
Controls the interrupt mechanism after end of conversion.
|
|
|
|
+----------------------------------------+----------------------------------+
|
|
| Setting | Configuration |
|
|
+========================================+==================================+
|
|
| :py:const:`IntConfig.SMBUS_ALERT` | SMBUS Alert |
|
|
+----------------------------------------+----------------------------------+
|
|
| :py:const:`IntConfig.DATA_READY_NEXT` | INT Pin data ready for next chan |
|
|
+----------------------------------------+----------------------------------+
|
|
| :py:const:`IntConfig.DATA_READY_ALL` | INT Pin data ready for all chans |
|
|
+----------------------------------------+----------------------------------+
|
|
"""
|
|
|
|
SMBUS_ALERT = 0 # SMBUS Alert
|
|
DATA_READY_NEXT = 1 # INT Pin data ready for next channel
|
|
DATA_READY_ALL = 3 # INT Pin data ready for all channels
|
|
|
|
|
|
# Default I2C address (ADDR pin connected to GND)
|
|
_OPT4048_DEFAULT_ADDR = const(0x44)
|
|
|
|
# Register addresses
|
|
_OPT4048_REG_CH0_MSB = const(0x00) # X channel MSB register
|
|
_OPT4048_REG_CH0_LSB = const(0x01) # X channel LSB register
|
|
_OPT4048_REG_CH1_MSB = const(0x02) # Y channel MSB register
|
|
_OPT4048_REG_CH1_LSB = const(0x03) # Y channel LSB register
|
|
_OPT4048_REG_CH2_MSB = const(0x04) # Z channel MSB register
|
|
_OPT4048_REG_CH2_LSB = const(0x05) # Z channel LSB register
|
|
_OPT4048_REG_CH3_MSB = const(0x06) # W channel MSB register
|
|
_OPT4048_REG_CH3_LSB = const(0x07) # W channel LSB register
|
|
_OPT4048_REG_THRESHOLD_LOW = const(0x08) # Low threshold register
|
|
_OPT4048_REG_THRESHOLD_HIGH = const(0x09) # High threshold register
|
|
_OPT4048_REG_CONFIG = const(0x0A) # Configuration register
|
|
_OPT4048_REG_THRESHOLD_CFG = const(0x0B) # Threshold configuration register
|
|
_OPT4048_REG_STATUS = const(0x0C) # Status register
|
|
_OPT4048_REG_DEVICE_ID = const(0x11) # Device ID register
|
|
|
|
# Status register (0x0C) bit flags
|
|
_OPT4048_FLAG_L = const(0x01) # Flag low - measurement smaller than threshold
|
|
_OPT4048_FLAG_H = const(0x02) # Flag high - measurement larger than threshold
|
|
_OPT4048_FLAG_CONVERSION_READY = const(0x04) # Conversion ready
|
|
_OPT4048_FLAG_OVERLOAD = const(0x08) # Overflow condition
|
|
|
|
|
|
class OPT4048:
|
|
"""Library for the OPT4048 Tristimulus XYZ Color Sensor
|
|
|
|
:param ~busio.I2C i2c_bus: The I2C bus the device is connected to
|
|
:param int address: The I2C device address. Defaults to :const:`0x44`
|
|
|
|
**Quickstart: Importing and using the device**
|
|
|
|
Here is an example of using the :class:`OPT4048`.
|
|
First you will need to import the libraries to use the sensor
|
|
|
|
.. code-block:: python
|
|
|
|
import board
|
|
from adafruit_opt4048 import OPT4048
|
|
|
|
Once this is done you can define your `board.I2C` object and define your sensor object
|
|
|
|
.. code-block:: python
|
|
|
|
i2c = board.I2C() # uses board.SCL and board.SDA
|
|
sensor = OPT4048(i2c)
|
|
|
|
Now you have access to the color data
|
|
|
|
.. code-block:: python
|
|
|
|
x, y, z, w = sensor.color_data
|
|
"""
|
|
|
|
# _device_id = ROBits(16, _OPT4048_REG_DEVICE_ID, 0, register_width=2, lsb_first=False)
|
|
_device_id = UnaryStruct(_OPT4048_REG_DEVICE_ID, ">H")
|
|
_interrupt_direction = RWBit(_OPT4048_REG_THRESHOLD_CFG, 4, register_width=2, lsb_first=False)
|
|
_interrupt_config = RWBits(2, _OPT4048_REG_THRESHOLD_CFG, 2, register_width=2, lsb_first=False)
|
|
_interrupt_latch = RWBit(_OPT4048_REG_CONFIG, 3, register_width=2, lsb_first=False)
|
|
_interrupt_polarity = RWBit(_OPT4048_REG_CONFIG, 2, register_width=2, lsb_first=False)
|
|
_range = RWBits(4, _OPT4048_REG_CONFIG, 10, register_width=2, lsb_first=False)
|
|
_conversion_time = RWBits(4, _OPT4048_REG_CONFIG, 6, register_width=2, lsb_first=False)
|
|
_mode = RWBits(2, _OPT4048_REG_CONFIG, 4, register_width=2, lsb_first=False)
|
|
|
|
# Threshold Low register bits (register 0x08)
|
|
_threshold_low_exponent = RWBits(
|
|
4, _OPT4048_REG_THRESHOLD_LOW, 12, register_width=2, lsb_first=False
|
|
)
|
|
_threshold_low_mantissa = RWBits(
|
|
12, _OPT4048_REG_THRESHOLD_LOW, 0, register_width=2, lsb_first=False
|
|
)
|
|
_all_channels_raw = RWBits(
|
|
16 * 8, _OPT4048_REG_CH0_MSB, 0, register_width=2 * 8, lsb_first=False
|
|
)
|
|
|
|
def __init__(self, i2c_bus, address=_OPT4048_DEFAULT_ADDR):
|
|
self.i2c_device = i2c_device.I2CDevice(i2c_bus, address)
|
|
self.init()
|
|
|
|
def init(self):
|
|
"""Initialize the sensor and verify the device ID"""
|
|
# Check device ID
|
|
print(f"id: {hex(self._device_id)}")
|
|
if self._device_id != _OPT4048_CHIP_ID:
|
|
raise RuntimeError("Failed to find an OPT4048 sensor - check your wiring!")
|
|
|
|
# Initialize the sensor with default settings
|
|
self.interrupt_direction = True
|
|
self.interrupt_config = IntConfig.DATA_READY_ALL
|
|
self.interrupt_latch = True
|
|
self.interrupt_polarity = True # Use active-high interrupts by default
|
|
self.interrupt_config = IntConfig.DATA_READY_ALL
|
|
|
|
@property
|
|
def interrupt_direction(self):
|
|
"""Get the current interrupt direction setting.
|
|
|
|
Returns True if interrupts are generated when measurement > high threshold,
|
|
False if interrupts are generated when measurement < low threshold.
|
|
"""
|
|
return self._interrupt_direction
|
|
|
|
@interrupt_direction.setter
|
|
def interrupt_direction(self, value):
|
|
"""Set the direction of interrupt generation.
|
|
|
|
:param bool value: True for interrupt when measurement > high threshold,
|
|
False for interrupt when measurement < low threshold
|
|
"""
|
|
self._interrupt_direction = value
|
|
|
|
@property
|
|
def interrupt_config(self):
|
|
"""Get the current interrupt configuration.
|
|
|
|
Returns the current interrupt configuration mode.
|
|
See IntConfig class for valid values.
|
|
"""
|
|
return self._interrupt_config
|
|
|
|
@interrupt_config.setter
|
|
def interrupt_config(self, value):
|
|
"""Set the interrupt configuration.
|
|
|
|
:param int value: The interrupt configuration value.
|
|
Must be a valid IntConfig value.
|
|
"""
|
|
if not IntConfig.is_valid(value):
|
|
raise ValueError("Interrupt configuration must be a valid IntConfig value")
|
|
self._interrupt_config = value
|
|
|
|
@property
|
|
def interrupt_latch(self):
|
|
"""Get the current interrupt latch setting.
|
|
|
|
Returns True if interrupts are latched (interrupt flag remains active until
|
|
register is read), False if interrupts are transparent (interrupt flag
|
|
is updated with each measurement).
|
|
"""
|
|
return self._interrupt_latch
|
|
|
|
@interrupt_latch.setter
|
|
def interrupt_latch(self, value):
|
|
"""Set the interrupt latch mode.
|
|
|
|
:param bool value: True for latched interrupts (interrupt flag remains active
|
|
until register is read), False for transparent interrupts
|
|
(interrupt flag is updated with each measurement)
|
|
"""
|
|
self._interrupt_latch = value
|
|
|
|
@property
|
|
def interrupt_polarity(self):
|
|
"""Get the current interrupt pin polarity setting.
|
|
|
|
Returns True if interrupts are active-high (1 = interrupt active),
|
|
False if interrupts are active-low (0 = interrupt active).
|
|
"""
|
|
return self._interrupt_polarity
|
|
|
|
@interrupt_polarity.setter
|
|
def interrupt_polarity(self, value):
|
|
"""Set the interrupt pin polarity.
|
|
|
|
:param bool value: True for active-high interrupts (1 = interrupt active),
|
|
False for active-low interrupts (0 = interrupt active)
|
|
"""
|
|
self._interrupt_polarity = value
|
|
|
|
@property
|
|
def range(self):
|
|
"""Get the current range setting for light measurements.
|
|
|
|
Returns the current range setting from the Range enum.
|
|
This controls the full-scale light level range of the device.
|
|
|
|
See the Range class for valid values:
|
|
- Range.RANGE_2K: 2.2 klux
|
|
- Range.RANGE_4K: 4.5 klux
|
|
- Range.RANGE_9K: 9 klux
|
|
- Range.RANGE_18K: 18 klux
|
|
- Range.RANGE_36K: 36 klux
|
|
- Range.RANGE_72K: 72 klux
|
|
- Range.RANGE_144K: 144 klux
|
|
- Range.AUTO: Auto-range
|
|
"""
|
|
return self._range
|
|
|
|
@range.setter
|
|
def range(self, value):
|
|
"""Set the range for light measurements.
|
|
|
|
:param int value: The range setting to use from Range enum.
|
|
Controls the full-scale light level range.
|
|
Must be a valid Range value.
|
|
"""
|
|
if not Range.is_valid(value):
|
|
raise ValueError("Range setting must be a valid Range value")
|
|
self._range = value
|
|
|
|
@property
|
|
def conversion_time(self):
|
|
"""Get the current conversion time setting for the OPT4048 sensor.
|
|
|
|
Returns the current conversion time setting from the ConversionTime enum.
|
|
This controls the device conversion time per channel.
|
|
|
|
See the ConversionTime class for valid values:
|
|
- ConversionTime.TIME_600US: 600 microseconds
|
|
- ConversionTime.TIME_1MS: 1 millisecond
|
|
- ConversionTime.TIME_1_8MS: 1.8 milliseconds
|
|
- ConversionTime.TIME_3_4MS: 3.4 milliseconds
|
|
- ConversionTime.TIME_6_5MS: 6.5 milliseconds
|
|
- ConversionTime.TIME_12_7MS: 12.7 milliseconds
|
|
- ConversionTime.TIME_25MS: 25 milliseconds
|
|
- ConversionTime.TIME_50MS: 50 milliseconds
|
|
- ConversionTime.TIME_100MS: 100 milliseconds
|
|
- ConversionTime.TIME_200MS: 200 milliseconds
|
|
- ConversionTime.TIME_400MS: 400 milliseconds
|
|
- ConversionTime.TIME_800MS: 800 milliseconds
|
|
"""
|
|
return self._conversion_time
|
|
|
|
@conversion_time.setter
|
|
def conversion_time(self, value):
|
|
"""Set the conversion time for light measurements.
|
|
|
|
:param int value: The conversion time setting to use from ConversionTime enum.
|
|
Controls the device conversion time per channel.
|
|
Must be a valid ConversionTime value.
|
|
"""
|
|
if not ConversionTime.is_valid(value):
|
|
raise ValueError("Conversion time setting must be a valid ConversionTime value")
|
|
self._conversion_time = value
|
|
|
|
@property
|
|
def mode(self):
|
|
"""Get the current operating mode setting for the OPT4048 sensor.
|
|
|
|
Returns the current operating mode setting from the Mode enum.
|
|
This controls the device operating mode.
|
|
|
|
See the Mode class for valid values:
|
|
- Mode.POWERDOWN: Power-down mode
|
|
- Mode.AUTO_ONESHOT: Auto-range one-shot mode
|
|
- Mode.ONESHOT: One-shot mode
|
|
- Mode.CONTINUOUS: Continuous mode
|
|
"""
|
|
return self._mode
|
|
|
|
@mode.setter
|
|
def mode(self, value):
|
|
"""Set the operating mode of the sensor.
|
|
|
|
:param int value: The operating mode setting to use from Mode enum.
|
|
Controls the device's operating mode.
|
|
Must be a valid Mode value.
|
|
"""
|
|
if not Mode.is_valid(value):
|
|
raise ValueError("Mode setting must be a valid Mode value")
|
|
self._mode = value
|
|
|
|
def get_channels_raw(self):
|
|
"""Read all four channels, verify CRC, and return raw ADC code values.
|
|
|
|
Reads registers for channels 0-3 in one burst, checks the CRC bits for each,
|
|
and computes the raw ADC code values.
|
|
|
|
:return: Tuple of 4 raw channel values (X, Y, Z, W) if successful
|
|
:rtype: Tuple[int, int, int, int]
|
|
:raises RuntimeError: If CRC check fails for any channel
|
|
"""
|
|
# Read all channels in one operation using the ROBits accessor
|
|
all_channels_data = self._all_channels_raw
|
|
|
|
# Process each channel's data
|
|
channels = []
|
|
|
|
for ch in range(4):
|
|
# Extract channel data from the appropriate position in the buffer
|
|
# Each channel has 4 bytes (32 bits) of data
|
|
channel_offset = ch * 32
|
|
|
|
# Extract exponent (4 bits), MSB (12 bits), LSB (8 bits), and counter/CRC (8 bits)
|
|
exp = (all_channels_data >> (channel_offset + 28)) & 0xF
|
|
msb = (all_channels_data >> (channel_offset + 16)) & 0xFFF
|
|
lsb = (all_channels_data >> (channel_offset + 8)) & 0xFF
|
|
counter = (all_channels_data >> (channel_offset + 4)) & 0xF
|
|
crc = all_channels_data >> channel_offset & 0xF
|
|
|
|
# Combine MSB and LSB to form the 20-bit mantissa
|
|
mant = (msb << 8) | lsb
|
|
# print(f"ch: {ch} exp: {exp} msb: {msb} lsb: {lsb} counter: {counter} crc: {crc} mant: {mant}") # noqa: E501
|
|
# Calculate CRC
|
|
# Initialize CRC variables
|
|
x0 = 0 # CRC bit 0
|
|
x1 = 0 # CRC bit 1
|
|
x2 = 0 # CRC bit 2
|
|
x3 = 0 # CRC bit 3
|
|
|
|
# Calculate bit 0 (x0): XOR of all bits
|
|
# XOR all exponent bits
|
|
for i in range(4):
|
|
x0 ^= (exp >> i) & 1
|
|
|
|
# XOR all mantissa bits
|
|
for i in range(20):
|
|
x0 ^= (mant >> i) & 1
|
|
|
|
# XOR all counter bits
|
|
for i in range(4):
|
|
x0 ^= (counter >> i) & 1
|
|
|
|
# Calculate bit 1 (x1)
|
|
# Include counter bits 1 and 3
|
|
x1 ^= (counter >> 1) & 1 # COUNTER_CHx[1]
|
|
x1 ^= (counter >> 3) & 1 # COUNTER_CHx[3]
|
|
|
|
# Include odd-indexed mantissa bits
|
|
for i in range(1, 20, 2):
|
|
x1 ^= (mant >> i) & 1
|
|
|
|
# Include exponent bits 1 and 3
|
|
x1 ^= (exp >> 1) & 1 # E[1]
|
|
x1 ^= (exp >> 3) & 1 # E[3]
|
|
|
|
# Calculate bit 2 (x2)
|
|
# Include counter bit 3
|
|
x2 ^= (counter >> 3) & 1 # COUNTER_CHx[3]
|
|
|
|
# Include mantissa bits at positions 3,7,11,15,19
|
|
for i in range(3, 20, 4):
|
|
x2 ^= (mant >> i) & 1
|
|
|
|
# Include exponent bit 3
|
|
x2 ^= (exp >> 3) & 1 # E[3]
|
|
|
|
# Calculate bit 3 (x3)
|
|
# XOR mantissa bits at positions 3, 11, 19
|
|
x3 ^= (mant >> 3) & 1 # R[3]
|
|
x3 ^= (mant >> 11) & 1 # R[11]
|
|
x3 ^= (mant >> 19) & 1 # R[19]
|
|
|
|
# Combine bits to form the CRC
|
|
calculated_crc = (x3 << 3) | (x2 << 2) | (x1 << 1) | x0
|
|
|
|
# Verify CRC
|
|
if crc != calculated_crc:
|
|
raise RuntimeError(f"CRC check failed for channel {ch}")
|
|
|
|
# Convert to raw ADC value format (mantissa << exponent)
|
|
output = mant << exp
|
|
|
|
# Add to channels list
|
|
# channels.append(output)
|
|
channels.insert(0, output)
|
|
|
|
# Return all four channels (X, Y, Z, W)
|
|
return tuple(channels)
|
|
|
|
@property
|
|
def threshold_low(self):
|
|
"""Get the current low threshold value.
|
|
|
|
Returns the current low threshold value as a 32-bit integer.
|
|
This value determines when a low threshold interrupt is generated
|
|
when interrupt_direction is False.
|
|
"""
|
|
# Read the exponent and mantissa from the threshold low register
|
|
exponent = self._threshold_low_exponent
|
|
mantissa = self._threshold_low_mantissa
|
|
print(f"exponent: {exponent} mantissa: {mantissa}")
|
|
# Calculate ADC code value by applying the exponent as a bit shift
|
|
# ADD 8 to the exponent as per datasheet equations 12-13
|
|
return mantissa << (8 + exponent)
|
|
|
|
@threshold_low.setter
|
|
def threshold_low(self, value):
|
|
"""Set the low threshold value for interrupt generation.
|
|
|
|
:param int value: The low threshold value as a 32-bit integer
|
|
"""
|
|
# Find the appropriate exponent and mantissa values that represent the threshold
|
|
exponent = 0
|
|
mantissa = value
|
|
|
|
# The mantissa needs to fit in 12 bits, so we start by shifting right
|
|
# to determine how many shifts we need (which gives us the exponent)
|
|
# Note that the threshold registers already have 8 added to exponent
|
|
# internally so we first subtract 8 from our target exponent
|
|
if mantissa > 0xFFF: # If value won't fit in 12 bits
|
|
while mantissa > 0xFFF and exponent < 15:
|
|
mantissa >>= 1
|
|
exponent += 1
|
|
if mantissa > 0xFFF: # If still won't fit with max exponent, clamp
|
|
mantissa = 0xFFF
|
|
exponent = 15 - 8 # Max exponent (15) minus the 8 that's added internally
|
|
|
|
# Write the exponent and mantissa to the register
|
|
self._threshold_low_exponent = exponent
|
|
self._threshold_low_mantissa = mantissa
|
|
|
|
def get_cie(self):
|
|
"""Calculate CIE chromaticity coordinates and lux from raw sensor values.
|
|
|
|
Reads all four channels and calculates CIE x and y chromaticity coordinates
|
|
and illuminance (lux) using a matrix transformation.
|
|
|
|
:return: Tuple of CIE x, CIE y, and lux values
|
|
:rtype: Tuple[float, float, float]
|
|
"""
|
|
# Read all four channels using get_channels_raw
|
|
ch0, ch1, ch2, ch3 = self.get_channels_raw()
|
|
|
|
# Matrix multiplication coefficients (from datasheet)
|
|
m0x = 2.34892992e-04
|
|
m0y = -1.89652390e-05
|
|
m0z = 1.20811684e-05
|
|
m0l = 0
|
|
|
|
m1x = 4.07467441e-05
|
|
m1y = 1.98958202e-04
|
|
m1z = -1.58848115e-05
|
|
m1l = 2.15e-3
|
|
|
|
m2x = 9.28619404e-05
|
|
m2y = -1.69739553e-05
|
|
m2z = 6.74021520e-04
|
|
m2l = 0
|
|
|
|
m3x = 0
|
|
m3y = 0
|
|
m3z = 0
|
|
m3l = 0
|
|
|
|
# Matrix multiplication to calculate X, Y, Z, L values
|
|
# [ch0 ch1 ch2 ch3] * [m0x m0y m0z m0l] = [X Y Z Lux]
|
|
# [m1x m1y m1z m1l]
|
|
# [m2x m2y m2z m2l]
|
|
# [m3x m3y m3z m3l]
|
|
x = ch0 * m0x + ch1 * m1x + ch2 * m2x + ch3 * m3x
|
|
y = ch0 * m0y + ch1 * m1y + ch2 * m2y + ch3 * m3y
|
|
z = ch0 * m0z + ch1 * m1z + ch2 * m2z + ch3 * m3z
|
|
lux = ch0 * m0l + ch1 * m1l + ch2 * m2l + ch3 * m3l
|
|
|
|
# Calculate CIE x, y chromaticity coordinates
|
|
sum_xyz = x + y + z
|
|
if sum_xyz <= 0:
|
|
# Avoid division by zero
|
|
return 0.0, 0.0, 0.0
|
|
|
|
cie_x = x / sum_xyz
|
|
cie_y = y / sum_xyz
|
|
|
|
return cie_x, cie_y, lux
|
|
|
|
def calculate_color_temperature(self, cie_x, cie_y):
|
|
"""Calculate the correlated color temperature (CCT) in Kelvin.
|
|
|
|
Uses McCamy's approximation formula to calculate CCT from CIE 1931 x,y
|
|
coordinates. This is accurate for color temperatures between 2000K and
|
|
30000K.
|
|
|
|
Formula:
|
|
n = (x - 0.3320) / (0.1858 - y)
|
|
CCT = 437 * n^3 + 3601 * n^2 + 6861 * n + 5517
|
|
|
|
:param float cie_x: CIE x chromaticity coordinate
|
|
:param float cie_y: CIE y chromaticity coordinate
|
|
:return: The calculated color temperature in Kelvin
|
|
:rtype: float
|
|
"""
|
|
# Check for invalid coordinates
|
|
if cie_x == 0 and cie_y == 0:
|
|
return 0.0
|
|
|
|
# Calculate using McCamy's formula
|
|
# n = (x - 0.3320) / (0.1858 - y)
|
|
n = (cie_x - 0.3320) / (0.1858 - cie_y)
|
|
|
|
# CCT = 437 * n^3 + 3601 * n^2 + 6861 * n + 5517
|
|
cct = (437.0 * n * n * n) + (3601.0 * n * n) + (6861.0 * n) + 5517.0
|
|
|
|
return cct
|