Source code for adafruit_tcs34725

# The MIT License (MIT)
#
# Copyright (c) 2017 Tony DiCola for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_tcs34725`
====================================================

CircuitPython module for the TCS34725 color sensor.  Ported from the
micropython-adafruit-tcs34725 module by Radomir Dopieralski:
https://github.com/adafruit/micropython-adafruit-tcs34725

See examples/tcs34725_simpletest.py for an example of the usage.

* Author(s): Tony DiCola, Carter Nelson

Implementation Notes
--------------------

**Hardware:**

* Adafruit `RGB Color Sensor with IR filter and White LED - TCS34725
  <https://www.adafruit.com/product/1334>`_ (Product ID: 1334)

* Flora `Color Sensor with White Illumination LED - TCS34725
  <https://www.adafruit.com/product/1356>`_ (Product ID: 1356)

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards:
  https://github.com/adafruit/circuitpython/releases
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""
import time

import adafruit_bus_device.i2c_device as i2c_device
from micropython import const

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_TCS34725.git"

# Register and command constants:
# pylint: disable=bad-whitespace
_COMMAND_BIT       = const(0x80)
_REGISTER_ENABLE   = const(0x00)
_REGISTER_ATIME    = const(0x01)
_REGISTER_AILT     = const(0x04)
_REGISTER_AIHT     = const(0x06)
_REGISTER_ID       = const(0x12)
_REGISTER_APERS    = const(0x0c)
_REGISTER_CONTROL  = const(0x0f)
_REGISTER_SENSORID = const(0x12)
_REGISTER_STATUS   = const(0x13)
_REGISTER_CDATA    = const(0x14)
_REGISTER_RDATA    = const(0x16)
_REGISTER_GDATA    = const(0x18)
_REGISTER_BDATA    = const(0x1a)
_ENABLE_AIEN       = const(0x10)
_ENABLE_WEN        = const(0x08)
_ENABLE_AEN        = const(0x02)
_ENABLE_PON        = const(0x01)
_GAINS  = (1, 4, 16, 60)
_CYCLES = (0, 1, 2, 3, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60)
_INTEGRATION_TIME_THRESHOLD_LOW = 2.4
_INTEGRATION_TIME_THRESHOLD_HIGH = 614.4
# pylint: enable=bad-whitespace


[docs]class TCS34725: """Driver for the TCS34725 color sensor.""" # Class-level buffer for reading and writing data with the sensor. # This reduces memory allocations but means the code is not re-entrant or # thread safe! _BUFFER = bytearray(3) def __init__(self, i2c, address=0x29): self._device = i2c_device.I2CDevice(i2c, address) self._active = False self.integration_time = 2.4 self._glass_attenuation = None self.glass_attenuation = 1.0 # Check sensor ID is expectd value. sensor_id = self._read_u8(_REGISTER_SENSORID) if sensor_id not in (0x44, 0x10): raise RuntimeError('Could not find sensor, check wiring!') @property def lux(self): """The lux value computed from the color channels.""" return self._temperature_and_lux_dn40()[0] @property def color_temperature(self): """The color temperature in degrees Kelvin.""" return self._temperature_and_lux_dn40()[1] @property def color_rgb_bytes(self): """Read the RGB color detected by the sensor. Returns a 3-tuple of red, green, blue component values as bytes (0-255). """ r, g, b, clear = self.color_raw # Avoid divide by zero errors ... if clear = 0 return black if clear == 0: return (0, 0, 0) # pylint: disable=bad-whitespace red = int(pow((int((r/clear) * 256) / 255), 2.5) * 255) green = int(pow((int((g/clear) * 256) / 255), 2.5) * 255) blue = int(pow((int((b/clear) * 256) / 255), 2.5) * 255) # Handle possible 8-bit overflow if red > 255: red = 255 if green > 255: green = 255 if blue > 255: blue = 255 return (red, green, blue) @property def color(self): """Read the RGB color detected by the sensor. Returns an int with 8 bits per channel. Examples: Red = 16711680 (0xff0000), Green = 65280 (0x00ff00), Blue = 255 (0x0000ff), SlateGray = 7372944 (0x708090) """ r, g, b = self.color_rgb_bytes return (r << 16) | (g << 8) | b @property def active(self): """The active state of the sensor. Boolean value that will enable/activate the sensor with a value of True and disable with a value of False. """ return self._active @active.setter def active(self, val): val = bool(val) if self._active == val: return self._active = val enable = self._read_u8(_REGISTER_ENABLE) if val: self._write_u8(_REGISTER_ENABLE, enable | _ENABLE_PON) time.sleep(0.003) self._write_u8(_REGISTER_ENABLE, enable | _ENABLE_PON | _ENABLE_AEN) else: self._write_u8(_REGISTER_ENABLE, enable & ~(_ENABLE_PON | _ENABLE_AEN)) @property def integration_time(self): """The integration time of the sensor in milliseconds.""" return self._integration_time @integration_time.setter def integration_time(self, val): if not _INTEGRATION_TIME_THRESHOLD_LOW <= val <= _INTEGRATION_TIME_THRESHOLD_HIGH: raise ValueError("Integration Time must be between '{0}' and '{1}'".format( _INTEGRATION_TIME_THRESHOLD_LOW, _INTEGRATION_TIME_THRESHOLD_HIGH)) cycles = int(val / 2.4) self._integration_time = cycles * 2.4 # pylint: disable=attribute-defined-outside-init self._write_u8(_REGISTER_ATIME, 256-cycles) @property def gain(self): """The gain of the sensor. Should be a value of 1, 4, 16, or 60. """ return _GAINS[self._read_u8(_REGISTER_CONTROL)] @gain.setter def gain(self, val): if val not in _GAINS: raise ValueError("Gain should be one of the following values: {0}".format(_GAINS)) self._write_u8(_REGISTER_CONTROL, _GAINS.index(val)) @property def interrupt(self): """True if the interrupt is set. Can be set to False (and only False) to clear the interrupt. """ return bool(self._read_u8(_REGISTER_STATUS) & _ENABLE_AIEN) @interrupt.setter def interrupt(self, val): if val: raise ValueError("Interrupt should be set to False in order to clear the interrupt") with self._device: self._device.write(b'\xe6') @property def color_raw(self): """Read the raw RGBC color detected by the sensor. Returns a 4-tuple of 16-bit red, green, blue, clear component byte values (0-65535). """ was_active = self.active self.active = True while not self._valid(): time.sleep((self._integration_time + 0.9)/1000.0) data = tuple(self._read_u16(reg) for reg in ( _REGISTER_RDATA, _REGISTER_GDATA, _REGISTER_BDATA, _REGISTER_CDATA, )) self.active = was_active return data @property def cycles(self): """The persistence cycles of the sensor.""" if self._read_u8(_REGISTER_ENABLE) & _ENABLE_AIEN: return _CYCLES[self._read_u8(_REGISTER_APERS) & 0x0f] return -1 @cycles.setter def cycles(self, val): enable = self._read_u8(_REGISTER_ENABLE) if val == -1: self._write_u8(_REGISTER_ENABLE, enable & ~(_ENABLE_AIEN)) else: if val not in _CYCLES: raise ValueError("Only the following cycles are permitted: {0}".format(_CYCLES)) self._write_u8(_REGISTER_ENABLE, enable | _ENABLE_AIEN) self._write_u8(_REGISTER_APERS, _CYCLES.index(val)) @property def min_value(self): """The minimum threshold value (AILT register) of the sensor as a 16-bit unsigned value. """ return self._read_u16(_REGISTER_AILT) @min_value.setter def min_value(self, val): self._write_u16(_REGISTER_AILT, val) @property def max_value(self): """The minimum threshold value (AIHT register) of the sensor as a 16-bit unsigned value. """ return self._read_u16(_REGISTER_AIHT) @max_value.setter def max_value(self, val): self._write_u16(_REGISTER_AIHT, val) def _temperature_and_lux_dn40(self): """Converts the raw R/G/B values to color temperature in degrees Kelvin using the algorithm described in DN40 from Taos (now AMS). Also computes lux. Returns tuple with both values or tuple of Nones if computation can not be done. """ # pylint: disable=bad-whitespace, invalid-name, too-many-locals # Initial input values ATIME = self._read_u8(_REGISTER_ATIME) ATIME_ms = (256 - ATIME) * 2.4 AGAINx = self.gain R, G, B, C = self.color_raw # Device specific values (DN40 Table 1 in Appendix I) GA = self.glass_attenuation # Glass Attenuation Factor DF = 310.0 # Device Factor R_Coef = 0.136 # | G_Coef = 1.0 # | used in lux computation B_Coef = -0.444 # | CT_Coef = 3810 # Color Temperature Coefficient CT_Offset = 1391 # Color Temperatuer Offset # Analog/Digital saturation (DN40 3.5) SATURATION = 65535 if 256 - ATIME > 63 else 1024 * (256 - ATIME) # Ripple saturation (DN40 3.7) if ATIME_ms < 150: SATURATION -= SATURATION / 4 # Check for saturation and mark the sample as invalid if true if C >= SATURATION: return None, None # IR Rejection (DN40 3.1) IR = (R + G + B - C) / 2 if R + G + B > C else 0. R2 = R - IR G2 = G - IR B2 = B - IR # Lux Calculation (DN40 3.2) G1 = R_Coef * R2 + G_Coef * G2 + B_Coef * B2 CPL = (ATIME_ms * AGAINx) / (GA * DF) lux = G1 / CPL # CT Calculations (DN40 3.4) CT = CT_Coef * B2 / R2 + CT_Offset return lux, CT @property def glass_attenuation(self): """The Glass Attenuation (FA) factor used to compensate for lower light levels at the device due to the possible presence of glass. The GA is the inverse of the glass transmissivity (T), so GA = 1/T. A transmissivity of 50% gives GA = 1 / 0.50 = 2. If no glass is present, use GA = 1. See Application Note: DN40-Rev 1.0 – Lux and CCT Calculations using ams Color Sensors for more details. """ return self._glass_attenuation @glass_attenuation.setter def glass_attenuation(self, value): if value < 1: raise ValueError("Glass attenuation factor must be at least 1.") self._glass_attenuation = value def _valid(self): # Check if the status bit is set and the chip is ready. return bool(self._read_u8(_REGISTER_STATUS) & 0x01) def _read_u8(self, address): # Read an 8-bit unsigned value from the specified 8-bit address. with self._device as i2c: self._BUFFER[0] = (address | _COMMAND_BIT) & 0xFF i2c.write(self._BUFFER, end=1, stop=False) i2c.readinto(self._BUFFER, end=1) return self._BUFFER[0] def _read_u16(self, address): # Read a 16-bit unsigned value from the specified 8-bit address. with self._device as i2c: self._BUFFER[0] = (address | _COMMAND_BIT) & 0xFF i2c.write(self._BUFFER, end=1, stop=False) i2c.readinto(self._BUFFER, end=2) return (self._BUFFER[1] << 8) | self._BUFFER[0] def _write_u8(self, address, val): # Write an 8-bit unsigned value to the specified 8-bit address. with self._device as i2c: self._BUFFER[0] = (address | _COMMAND_BIT) & 0xFF self._BUFFER[1] = val & 0xFF i2c.write(self._BUFFER, end=2) def _write_u16(self, address, val): # Write a 16-bit unsigned value to the specified 8-bit address. with self._device as i2c: self._BUFFER[0] = (address | _COMMAND_BIT) & 0xFF self._BUFFER[1] = val & 0xFF self._BUFFER[2] = (val >> 8) & 0xFF i2c.write(self._BUFFER)