# SPDX-FileCopyrightText: 2017 Michael McWethy for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`APDS9960`
====================================================
Driver class for the APDS9960 board. Supports gesture, proximity, and color
detection.
* Author(s): Michael McWethy, Erik Hess
Implementation Notes
--------------------
**Hardware:**
* Adafruit `APDS9960 Proximity, Light, RGB, and Gesture Sensor
<https://www.adafruit.com/product/3595>`_ (Product ID: 3595)
* Adafruit `CLUE
<https://www.adafruit.com/product/4500>`_ (Product ID: 4500)
* Adafruit `Feather nRF52840 Sense
<https://www.adafruit.com/product/4516>`_ (Product ID: 4516)
* Adafruit `Proximity Trinkey
<https://www.adafruit.com/product/5022>`_ (Product ID: 5022)
**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
"""
import time
from adafruit_bus_device.i2c_device import I2CDevice
from micropython import const
try:
# Only used for typing
from typing import Tuple
from busio import I2C
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_APDS9960.git"
# Only one address is possible for the APDS9960, no alternates are available
_APDS9960_I2C_ADDRESS = const(0x39)
_DEVICE_ID = const(0xAB)
# APDS9960_RAM = const(0x00)
_APDS9960_ENABLE = const(0x80)
_APDS9960_ATIME = const(0x81)
# _APDS9960_WTIME = const(0x83)
# _APDS9960_AILTIL = const(0x84)
# _APDS9960_AILTH = const(0x85)
# _APDS9960_AIHTL = const(0x86)
# _APDS9960_AIHTH = const(0x87)
_APDS9960_PILT = const(0x89)
_APDS9960_PIHT = const(0x8B)
_APDS9960_PERS = const(0x8C)
# _APDS9960_CONFIG1 = const(0x8D)
# _APDS9960_PPULSE = const(0x8E)
_APDS9960_CONTROL = const(0x8F)
# _APDS9960_CONFIG2 = const(0x90)
_APDS9960_ID = const(0x92)
_APDS9960_STATUS = const(0x93)
_APDS9960_CDATAL = const(0x94)
# _APDS9960_CDATAH = const(0x95)
# _APDS9960_RDATAL = const(0x96)
# _APDS9960_RDATAH = const(0x97)
# _APDS9960_GDATAL = const(0x98)
# _APDS9960_GDATAH = const(0x99)
# _APDS9960_BDATAL = const(0x9A)
# _APDS9960_BDATAH = const(0x9B)
_APDS9960_PDATA = const(0x9C)
# _APDS9960_POFFSET_UR = const(0x9D)
# _APDS9960_POFFSET_DL = const(0x9E)
# _APDS9960_CONFIG3 = const(0x9F)
_APDS9960_GPENTH = const(0xA0)
_APDS9960_GEXTH = const(0xA1)
_APDS9960_GCONF1 = const(0xA2)
_APDS9960_GCONF2 = const(0xA3)
# _APDS9960_GOFFSET_U = const(0xA4)
# _APDS9960_GOFFSET_D = const(0xA5)
# _APDS9960_GOFFSET_L = const(0xA7)
# _APDS9960_GOFFSET_R = const(0xA9)
_APDS9960_GPULSE = const(0xA6)
# _APDS9960_GCONF3 = const(0xAA)
_APDS9960_GCONF4 = const(0xAB)
_APDS9960_GFLVL = const(0xAE)
_APDS9960_GSTATUS = const(0xAF)
# _APDS9960_IFORCE = const(0xE4)
# _APDS9960_PICLEAR = const(0xE5)
# _APDS9960_CICLEAR = const(0xE6)
_APDS9960_AICLEAR = const(0xE7)
_APDS9960_GFIFO_U = const(0xFC)
# APDS9960_GFIFO_D = const(0xFD)
# APDS9960_GFIFO_L = const(0xFE)
# APDS9960_GFIFO_R = const(0xFF)
_BIT_MASK_ENABLE_EN = const(0x01)
_BIT_MASK_ENABLE_COLOR = const(0x02)
_BIT_MASK_ENABLE_PROX = const(0x04)
_BIT_MASK_ENABLE_PROX_INT = const(0x20)
_BIT_MASK_ENABLE_GESTURE = const(0x40)
_BIT_MASK_STATUS_AVALID = const(0x01)
_BIT_MASK_STATUS_GINT = const(0x04)
_BIT_MASK_GSTATUS_GFOV = const(0x02)
_BIT_MASK_GCONF4_GFIFO_CLR = const(0x04)
_BIT_POS_PERS_PPERS = const(4)
_BIT_MASK_PERS_PPERS = const(0xF0)
_BIT_POS_CONTROL_AGAIN = const(0)
_BIT_MASK_CONTROL_AGAIN = const(3)
_BIT_POS_CONTROL_PGAIN = const(2)
_BIT_MASK_CONTROL_PGAIN = const(0x0C)
_BIT_POS_GCONF2_GGAIN = const(5)
_BIT_MASK_GCONF2_GGAIN = const(0x60)
# pylint: disable-msg=too-many-instance-attributes
[docs]
class APDS9960:
"""
Provide basic driver services for the APDS9960 breakout board
:param ~busio.I2C i2c: The I2C bus the APDS9960 is connected to
:param int rotation: Rotation of the device. Defaults to :const:`0`
:param bool reset: If true, reset device on init. Defaults to :const:`True`
:param bool set_defaults: If true, set sensible defaults on init. Defaults to :const:`True`
**Quickstart: Importing and using the APDS9960**
Here is an example of using the :class:`APDS9960` class.
First you will need to import the libraries to use the sensor
.. code-block:: python
import board
from adafruit_apds9960.apds9960 import APDS9960
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
apds = APDS9960(i2c)
Now you have access to the :attr:`apds.proximity_enable` and :attr:`apds.proximity`
attributes
.. code-block:: python
apds.proximity_enable = True
proximity = apds.proximity
.. note:: There is no ``address`` argument because the APDS-9960 only has one address and
doesn't offer any option to configure alternative addresses.
"""
def __init__(
self,
i2c: I2C,
*,
rotation: int = 0,
reset: bool = True,
set_defaults: bool = True
):
self.rotation = rotation
self.buf129 = None # Gesture FIFO buffer, only instantiated if needed
self.buf4 = None # Gesture data processing buffer, only instantiated if needed
self.buf2 = bytearray(2) # I2C communication buffer
self.i2c_device = I2CDevice(i2c, _APDS9960_I2C_ADDRESS)
if self._read8(_APDS9960_ID) != _DEVICE_ID:
raise RuntimeError()
if reset:
# Disable prox, gesture, and color engines
self.enable_proximity = False
self.enable_gesture = False
self.enable_color = False
# Reset basic config registers to power-on defaults
self.proximity_interrupt_threshold = (0, 0, 0)
self._write8(_APDS9960_GPENTH, 0)
self._write8(_APDS9960_GEXTH, 0)
self._write8(_APDS9960_GCONF1, 0)
self._write8(_APDS9960_GCONF2, 0)
self._write8(_APDS9960_GCONF4, 0)
self._write8(_APDS9960_GPULSE, 0)
self._write8(_APDS9960_ATIME, 255)
self._write8(_APDS9960_CONTROL, 0)
# Clear all non-gesture interrupts
self.clear_interrupt()
# Clear gesture FIFOs and interrupt
self._set_bit(_APDS9960_GCONF4, _BIT_MASK_GCONF4_GFIFO_CLR, True)
# Disable sensor and all functions/interrupts
self._write8(_APDS9960_ENABLE, 0)
time.sleep(0.025) # Sleeping could take at ~2-25 ms if engines were looping
# Re-enable sensor and wait 10ms for the power on delay to finish
self.enable = True
time.sleep(0.010)
if set_defaults:
# Trigger proximity interrupt at >= 5, PPERS: 4 cycles
self.proximity_interrupt_threshold = (0, 5, 4)
# Enter gesture engine at >= 5 proximity counts
self._write8(_APDS9960_GPENTH, 0x05)
# Exit gesture engine if all counts drop below 30
self._write8(_APDS9960_GEXTH, 0x1E)
# GEXPERS: 2 (4 cycles), GEXMSK: 0 (default) GFIFOTH: 2 (8 datasets)
self._write8(_APDS9960_GCONF1, 0x82)
# GGAIN: 2 (4x), GLDRIVE: 100 mA (default), GWTIME: 1 (2.8ms)
self._write8(_APDS9960_GCONF2, 0x41)
# GPULSE: 5 (6 pulses), GPLEN: 2 (16 us)
self._write8(_APDS9960_GPULSE, 0x85)
# ATIME: 256 (712ms color integration time, max count of 65535)
self.color_integration_time = 256
# AGAIN: 1 (4x color gain)
self.color_gain = 1
## BOARD
@property
def enable(self) -> bool:
"""If ``True``, the sensor is enabled.
If set to ``False``, the sensor will enter a low-power sleep state
When enabled, the sensor's state machine will run through the following steps in sequence,
repeating from the top after all states are run through.
#. **Idle State**
- Will only remain in this state if all three sense engines are disabled.
#. **Proximity Engine** *(if enabled)*
- Will only run if `enable_proximity` is ``True``.
- Will run once, storing fresh data in the sensor's proximity data registers. If
proximity data is is lower than or exceeds the configured proximity thresholds an
internal persistence is incremented on each run as well.
#. **Gesture Engine** *(if enabled)*
- Will only run if `enable_gesture` is ``True`` and if entry threshold of `proximity`
is greater or equal to the gesture proximity entry threshold of 5 counts.
- Will continuously loop, storing new results in the sensor's gesture FIFO buffers,
until one of four conditions occur.
- Exit threshold is met. *(all gesture measurements <= 30 counts)*
- The gesture engine or sensor are disabled. *(`enable_gesture` or `enable`
properties are set to ``False``)*
- The sensor is re-initalized by the driver
- The sensor is power cycled
#. **Wait Timer** *(set to 0 by default)*
- This driver does not set or make available the ``WAIT`` or ``WLONG`` registers that
control this function and, on intialization, leaves the timer at its power-on default
state of ``0``, effectively disabling this timer.
#. **Color/Light Engine** *(if enabled)*
- Will run start if `enable_color` is ``True``.
- Will run once, storing fresh data in the sensor's color data registers on each run.
.. note:: Waking the sensor from its sleep state takes at least 7 ms. Disabling the sensor
and entering a sleep state can take as little as 2.78 ms, more typically about 25 ms, or
potentially quite a bit longer depending on what engines were enabled and what state was
active at the time the disable command was received.
.. hint:: When in a sleep state the sensor's power usage drops to as little as 1-10 uA,
compared as much as 790 uA of power usage when enabled with proximity and/or gesture
engines running. While in a sleep state, the sensor will still listen for and respond
to I2C communication which can lead to minor increases in power usage.
"""
return self._get_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_EN)
@enable.setter
def enable(self, value: bool) -> None:
self._set_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_EN, value)
## Proximity Properties
@property
def enable_proximity(self) -> bool:
"""If ``True``, the sensor's proximity engine is enabled."""
return self._get_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_PROX)
@enable_proximity.setter
def enable_proximity(self, value: bool) -> None:
self._set_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_PROX, value)
@property
def enable_proximity_interrupt(self) -> bool:
"""If ``True``, the internal proximity interrupt asserts the sensor's interrupt pin.
Internal proximity interrupt triggering is configured via `proximity_interrupt_threshold`.
.. tip:: Using this interrupt will require attaching the sensor's ``INT`` pin to an
available digital I/O with an internal or external pull-up resistor.
For boards with built-in sensors the pin is likely already mapped within ``board``.
.. csv-table::
:header: "Board", "Pin Mapping"
"CLUE", "``board.PROXIMITY_LIGHT_INTERRUPT``"
"Feather nRF52840 Sense", "``board.PROXIMITY_LIGHT_INTERRUPT``"
"Proximity Trinkey", "``board.INTERRUPT``"
"""
return self._get_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_PROX_INT)
@enable_proximity_interrupt.setter
def enable_proximity_interrupt(self, value: bool) -> None:
self._set_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_PROX_INT, value)
@property
def proximity_interrupt_threshold(self) -> Tuple[int, int, int]:
"""Tuple representing proximity engine low/high threshold and persistence, which determine
when the sensor's proximity interrupt is asserted.
1. Low Threshold (``PILT``)
2. High Threshold (``PIHT``) *(optional)*
3. Proximity Persistence (``PERS<PPERS>``) *(optional)*
The first two items are the "low threshold" and "high threshold" values. These can be set
to any number between ``0`` and ``255``. If the proximity value is lower than the low
threshold or higher than the high threshold for enough cycles, an interrupt will be
asserted.
The third item is the "persistence" value. This can be set to any value between ``0`` to
``15``. This represents the number of 2.78 ms out-of-threshold cycles to wait for before
asserting the interrupt. This is basically a filter to prevent premature/false interrupts.
.. hint:: For example, setting a low threshold of ``0`` and a high threshold of ``5`` will
cause the interrupt to be asserted very early when an object **enters the sensor's line
of sight**. Coversely, a low threshold of ``5`` and a high threshold of ``255`` will
trigger an interrupt only if an object **is removed from the sensor's line of sight**.
.. hint:: Tuning the persistence value can be useful in some use cases but for most
situations the driver's default value of 4 should provide for stable results without
much delay in interrupt triggering.
"""
return (
self._read8(_APDS9960_PILT),
self._read8(_APDS9960_PIHT),
self._get_bits(_APDS9960_PERS, _BIT_POS_PERS_PPERS, _BIT_MASK_PERS_PPERS),
)
@proximity_interrupt_threshold.setter
def proximity_interrupt_threshold(self, setting_tuple: Tuple[int, ...]) -> None:
if setting_tuple and 0 <= setting_tuple[0] <= 255:
self._write8(_APDS9960_PILT, setting_tuple[0])
if len(setting_tuple) > 1 and 0 <= setting_tuple[0] <= 255:
self._write8(_APDS9960_PIHT, setting_tuple[1])
persist = 4 # default 4
if len(setting_tuple) > 2 and 0 <= setting_tuple[0] <= 15:
persist = min(setting_tuple[2], 15)
self._set_bits(
_APDS9960_PERS, _BIT_POS_PERS_PPERS, _BIT_MASK_PERS_PPERS, persist
)
@property
def proximity_gain(self) -> int:
"""Proximity sensor gain value.
This sets the gain multiplier for the ADC during proximity engine operations.
.. csv-table::
:header: "``proximity_gain``", "Gain Multiplier", "Note"
0, "1x", "Power-on Default"
1, "2x", ""
2, "4x", ""
3, "8x", ""
"""
return self._get_bits(
_APDS9960_CONTROL, _BIT_POS_CONTROL_PGAIN, _BIT_MASK_CONTROL_PGAIN
)
@proximity_gain.setter
def proximity_gain(self, value: int) -> None:
self._set_bits(
_APDS9960_CONTROL, _BIT_POS_CONTROL_PGAIN, _BIT_MASK_CONTROL_PGAIN, value
)
[docs]
def clear_interrupt(self) -> None:
"""Clears all non-gesture interrupts.
This includes all of the following internal interrupts:
* **Proximity Interrupt** (``PINT``)
* **Proximity Saturation Interrupt** (``STATUS<PGSAT>``)
* **Color/Light Interrupt** (``STATUS<AINT>``)
* **Color/Light Clear Saturation Interrupt** (``STATUS<CPSAT>``)
"""
self._writecmdonly(_APDS9960_AICLEAR)
## Gesture Properties
@property
def enable_gesture(self) -> bool:
"""If ``True``, the sensor's gesture engine is enabled.
.. note:: The gesture engine will only operate if `enable_proximity` is also set to ``True``
"""
return self._get_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_GESTURE)
@enable_gesture.setter
def enable_gesture(self, value: bool) -> None:
self._set_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_GESTURE, value)
@property
def gesture_gain(self) -> int:
"""Gesture mode gain value.
This sets the gain multiplier for the ADC during gesture engine operations.
.. csv-table::
:header: "``gesture_gain``", "Gain Multiplier", "Note"
0, "1x", "Power-on Default"
1, "2x", ""
2, "4x", "Driver Default"
3, "8x", ""
"""
return self._get_bits(
_APDS9960_GCONF2, _BIT_POS_GCONF2_GGAIN, _BIT_MASK_GCONF2_GGAIN
)
@gesture_gain.setter
def gesture_gain(self, value: int) -> None:
self._set_bits(
_APDS9960_GCONF2, _BIT_POS_GCONF2_GGAIN, _BIT_MASK_GCONF2_GGAIN, value
)
@property
def rotation(self) -> int:
"""Clock-wise offset to apply to gesture results.
Acceptable values are ``0``, ``90``, ``180``, ``270``.
.. tip:: The sensor's "top" end is the one with the larger of the two circular windows.
Some rotation examples for various boards with the APS-9960 built in:
.. csv-table::
:header: "Board", "Rotation"
"CLUE", 270
"Feather nRF52840 Sense, with USB port to the left", 270
"Proximity Trinkey, plugged into right-side laptop USB port", 270
"Proximity Trinkey, plugged into left-side laptop USB port", 90
"""
return self._rotation
@rotation.setter
def rotation(self, new_rotation: int) -> None:
if new_rotation in [0, 90, 180, 270]:
self._rotation = new_rotation
else:
raise ValueError("Rotation value must be one of: 0, 90, 180, 270")
## Color/Light Properties
@property
def enable_color(self) -> bool:
"""If ``True``, the sensor's color/light engine is enabled"""
return self._get_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_COLOR)
@enable_color.setter
def enable_color(self, value: bool) -> None:
self._set_bit(_APDS9960_ENABLE, _BIT_MASK_ENABLE_COLOR, value)
@property
def color_data_ready(self) -> int:
"""Color data ready flag.
Returns ``0`` if no new data is ready, ``1`` if new data is ready.
This flag is reset when `color_data` is read."""
return self._get_bit(_APDS9960_STATUS, _BIT_MASK_STATUS_AVALID)
@property
def color_gain(self) -> int:
"""Color/light sensor gain value.
This sets the gain multiplier for the ADC during color/light engine operations.
.. csv-table::
:header: "``color_gain``", "Gain Multiplier", "Note"
0, "1x", "Power-on Default"
1, "4x", "Driver Default"
2, "16x", ""
3, "64x", ""
.. tip:: To get useful, predictable `color_data` results it is important to tune this,
along with `color_integration_time`, to accommodate different lighting conditions, sensor
placements, material transparencies, expected object reflectivity, and environmental
conditions.
For instance, measuring color of objects close to the sensor with bright, nearby
illumination (such as the white LEDs on the `Adafruit CLUE
<https://www.adafruit.com/product/4500>`_) may work well with a `color_gain` of ``0``
and a `color_integration_time` of ``72`` or lower.
However, measuring the intensity and color temperature of ambient light through
difusion glass or plastic is likely to require experimenting with a wide range of
integration time and gain settings before useful data can be obtained."""
return self._get_bits(
_APDS9960_CONTROL, _BIT_POS_CONTROL_AGAIN, _BIT_MASK_CONTROL_AGAIN
)
@color_gain.setter
def color_gain(self, value: int) -> None:
self._set_bits(
_APDS9960_CONTROL, _BIT_POS_CONTROL_AGAIN, _BIT_MASK_CONTROL_AGAIN, value
)
@property
def color_integration_time(self) -> int:
"""Color/light sensor gain.
Represents the integration time in number of 2.78 ms cycles for the ADC during color/light
engine operations. This also effectively sets the maxmium value returned for each channel
by `color_data`.
.. csv-table::
:header: "``color_integration_time``", "Time", "Max Count", "Note"
1, "2.78 ms", 1025, "Power-on Default"
10, "27.8 ms", 10241, ""
37, "103 ms", 37889, ""
72, "200 ms", 65535, ""
256, "712 ms", 65535, "Driver Default"
"""
return 256 - self._read8(_APDS9960_ATIME)
@color_integration_time.setter
def color_integration_time(self, value: int) -> None:
self._write8(_APDS9960_ATIME, 256 - value)
## PROXIMITY
@property
def proximity(self) -> int:
"""Proximity sensor data.
The proximity engine returns a number between ``0`` and ``255`` which represents the
intensity of the reflected IR light detected from the sensor's internal LEDs, which pulse
continously during proximity engine operation.
A value of ``0`` indicates no reflected IR light was received. This typically indicates
that no object(s) were in the sensor's line of sight and within detectable range of its IR
LED pulses.
A value of ``255`` indicates that the maximum detectable amount of reflected IR light was
received. This typically indicates that an object was detected very close to the sensor.
.. caution:: Will always return ``0`` if `enable_proximity` is not set ``True``.
.. note:: The sensor itself offers a very wide variety of configuration options for tuning
the proximity engine, such as the LEDs (pulse count/length, drive power) and the
photosensors (gain, offsets, masking). However, this driver does not make those readily
available in order to keep file size and memory footprint to a minimum, which is
critical for its use on more constrained platforms.
"""
return self._read8(_APDS9960_PDATA)
## GESTURE DETECTION
# pylint: disable-msg=too-many-branches,too-many-locals,too-many-statements
[docs]
def gesture(self) -> int:
"""Gesture sensor data.
This checks the sensor for new gesture engine results and, if they are present, retrieves
and processes the results to determine what, if any, gesture can be deduced from the sensor
data.
Returns a gesture code indicating the direction of the gesture. Before returning the code,
the `rotation` value is used to "rotate" the result as intended.
.. csv-table::
:header: "Code", "Direction"
0, "No gesture detected"
1, "Up"
2, "Down"
3, "Left"
4, "Right"
.. caution:: Will always return ``0`` if `enable_proximity` and `enable_gesture` are not set
to ``True``.
The data returned by the sensor is a continuous stream of four proximtiy measurements
constrained to up/down/left/right dimensions by using four directionally-aligned sensors.
The sensor itself doesn't include any logic to determine the gesture, leaving that work to
the implementer.
This driver implements an algorithm that reliably detects simple gestures in most scenarios
while remaining small and efficient enough to work within the resource constraints of as
many CircuitPython boards/platforms as possible.
.. tip:: Detecting gestures with this driver's algorithm requires actively, continously
polling for a gesture, with as little time as possible between `gesture()` calls. Even
with continous polling, however, its possible that gestures may go undetected by
`gesture()` calls if they occurred between or on the edges of `gesture()` method
execution.
.. warning:: If gesture data becomes available from the sensor, this driver will
continuously pull in that new data and analyze it until the sensor's gesture engine
exits and the sensor's FIFO buffers are clear. This allows for much more reliable
gesture detection by comparing the "first" to the "last" detected state at the cost of
blocking until the FIFOs are all clear. This will only happen if all four gesture values
drop below ``30``.
As a result, if an object is close to the sensor when `gesture()` is called, the method
will not return until it moves away.
.. note:: The sensor itself offers a very wide variety of configuration options for tuning
the gesture engine, such as the LEDs (pulse count/length, drive power), the photosensors
(gain, offsets, masking), the gesture engine's entry/exit thresholds, wait time, and
more. However, this driver does not make those readily available in order to keep file
size and memory footprint to a minimum, which is critical for its use on more
constrained platforms."""
# If FIFOs have overflowed we're already way too late, so clear those FIFOs and wait
if self._get_bit(_APDS9960_GSTATUS, _BIT_MASK_GSTATUS_GFOV):
self._set_bit(_APDS9960_GCONF4, _BIT_MASK_GCONF4_GFIFO_CLR, True)
wait_cycles = 0
# Don't wait forever though, just enough to see if a gesture is happening
while (
not self._get_bit(_APDS9960_STATUS, _BIT_MASK_STATUS_GINT)
and wait_cycles <= 30
):
time.sleep(0.003)
wait_cycles += 1
# Only start retrieval if there are datasets to retrieve
frame = []
datasets_available = self._read8(_APDS9960_GFLVL)
if (
self._get_bit(_APDS9960_STATUS, _BIT_MASK_STATUS_GINT)
and datasets_available > 0
):
if not self.buf129:
self.buf129 = bytearray(129)
buffer = self.buf129
buffer[0] = _APDS9960_GFIFO_U
if not self.buf4:
self.buf4 = bytearray(4)
buffer_dataset = self.buf4
# Retrieve new data until our FIFOs are truly empty
while True:
dataset_count = self._read8(_APDS9960_GFLVL)
if dataset_count == 0:
break
with self.i2c_device as i2c:
i2c.write_then_readinto(
buffer,
buffer,
out_end=1,
in_start=1,
in_end=min(129, 1 + (dataset_count * 4)),
)
# Unpack data stream into more usable U/D/L/R datasets for analysis
idx = 0
for i in range(dataset_count):
rec = i + 1
idx = 1 + ((rec - 1) * 4)
buffer_dataset[0] = buffer[idx]
buffer_dataset[1] = buffer[idx + 1]
buffer_dataset[2] = buffer[idx + 2]
buffer_dataset[3] = buffer[idx + 3]
# Drop fully-saturated and fully-zero to conserve memory
# Filter to remove useless (saturated, empty, low-count) datasets
if (
(not all(val == 255 for val in buffer_dataset))
and (not all(val == 0 for val in buffer_dataset))
and (all(val >= 30 for val in buffer_dataset))
):
if len(frame) < 2:
frame.append(tuple(buffer_dataset))
else:
frame[1] = tuple(buffer_dataset)
# Wait a very short time to see if new FIFO data has arrived before we drop out
time.sleep(0.03)
# If we only got one useful frame, that's not enough to make a solid guess
if len(frame) < 2:
return 0
# We should have a dataframe with two tuples now, a "first" and "last" entry.
# Time to process the dataframe!
# Determine our up/down and left/right ratios along with our first/last deltas
f_r_ud = ((frame[0][0] - frame[0][1]) * 100) // (frame[0][0] + frame[0][1])
f_r_lr = ((frame[0][2] - frame[0][3]) * 100) // (frame[0][2] + frame[0][3])
l_r_ud = ((frame[1][0] - frame[1][1]) * 100) // (frame[1][0] + frame[1][1])
l_r_lr = ((frame[1][2] - frame[1][3]) * 100) // (frame[1][2] + frame[1][3])
delta_ud = l_r_ud - f_r_ud
delta_lr = l_r_lr - f_r_lr
# Make our first guess at what gesture we saw, if any
state_ud = 0
state_lr = 0
if delta_ud >= 30:
state_ud = 1
elif delta_ud <= -30:
state_ud = -1
if delta_lr >= 30:
state_lr = 1
elif delta_lr <= -30:
state_lr = -1
# Make our final decision based on our first guess and, if required, the delta data
gesture_found = 0
# Easy cases
if state_ud == -1 and state_lr == 0:
gesture_found = 1
elif state_ud == 1 and state_lr == 0:
gesture_found = 2
elif state_ud == 0 and state_lr == -1:
gesture_found = 3
elif state_ud == 0 and state_lr == 1:
gesture_found = 4
# Not so easy cases
if gesture_found == 0:
if state_ud == -1 and state_lr == 1:
if abs(delta_ud) > abs(delta_lr):
gesture_found = 1
else:
gesture_found = 4
elif state_ud == 1 and state_lr == -1:
if abs(delta_ud) > abs(delta_lr):
gesture_found = 2
else:
gesture_found = 3
elif state_ud == -1 and state_lr == -1:
if abs(delta_ud) > abs(delta_lr):
gesture_found = 1
else:
gesture_found = 3
elif state_ud == 1 and state_lr == 1:
if abs(delta_ud) > abs(delta_lr):
gesture_found = 2
else:
gesture_found = 3
if gesture_found != 0:
if self._rotation != 0:
# If we need to rotate our gesture, lets do that before returning
dir_lookup = [1, 4, 2, 3]
idx = (dir_lookup.index(gesture_found) + self._rotation // 90) % 4
return dir_lookup[idx]
return gesture_found
## COLOR
@property
def color_data(self) -> Tuple[int, int, int, int]:
"""Tuple containing red, green, blue, and clear light intensity values detected by the
sensor during the latest color/light engine run.
Each value is a 16-bit integer with a possible value of ``0`` to ``65535``.
.. caution:: Will always return ``(0, 0, 0, 0)`` if `enable_color` is not set to ``True``.
.. tip:: To get useful, predictable `color_data` results it is important to tune
`color_gain` and `color_integration_time`, to accommodate different lighting conditions,
sensor placements, plastic/glass transparencies, expected object reflectivity, and
environmental conditions.
For instance, measuring color of objects close to the sensor with bright, nearby
illumination (such as the white LEDs on the `Adafruit Clue
<https://www.adafruit.com/product/4500>`_) may work well with a `color_gain` of ``0``
and a `color_integration_time` of ``72``.
However, measuring the intensity and color temperature of ambient light through
difusion glass or plastic is likely to require experimenting with a wide range of
`color_gain` and `color_integration_time` settings before useful data can be obtained.
"""
return (
self._color_data16(_APDS9960_CDATAL + 2),
self._color_data16(_APDS9960_CDATAL + 4),
self._color_data16(_APDS9960_CDATAL + 6),
self._color_data16(_APDS9960_CDATAL),
)
# method for reading and writing to I2C
def _write8(self, command: int, abyte: int) -> None:
"""Write a command and 1 byte of data to the I2C device"""
buf = self.buf2
buf[0] = command
buf[1] = abyte
with self.i2c_device as i2c:
i2c.write(buf)
def _writecmdonly(self, command: int) -> None:
"""Writes a command and 0 bytes of data to the I2C device"""
buf = self.buf2
buf[0] = command
with self.i2c_device as i2c:
i2c.write(buf, end=1)
def _read8(self, command: int) -> int:
"""Sends a command and reads 1 byte of data from the I2C device"""
buf = self.buf2
buf[0] = command
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1, in_end=1)
return buf[0]
def _get_bit(self, register: int, mask: int) -> bool:
"""Gets a single bit value from the I2C device's register"""
buf = self.buf2
buf[0] = register
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1, in_start=1)
return bool(buf[1] & mask)
def _set_bit(self, register: int, mask: int, value: bool) -> None:
"""Sets a single bit value in the I2C device's register"""
buf = self.buf2
buf[0] = register
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1, in_start=1)
if value:
buf[1] |= mask
else:
buf[1] &= ~mask
with self.i2c_device as i2c:
i2c.write(buf, end=2)
def _get_bits(self, register: int, pos: int, mask: int) -> int:
"""Sets a multi-bit value in the I2C device's register"""
buf = self.buf2
buf[0] = register
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1, in_start=1)
return (buf[1] & mask) >> pos
def _set_bits(self, register: int, pos: int, mask: int, value: int) -> None:
"""Sets a multi-bit value in the I2C device's register"""
buf = self.buf2
buf[0] = register
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1, in_start=1)
buf[1] = (buf[1] & ~mask) | (value << pos)
with self.i2c_device as i2c:
i2c.write(buf, end=2)
def _color_data16(self, command: int) -> int:
"""Sends a command and reads 2 bytes of data from the I2C device
The returned data is low byte first followed by high byte"""
buf = self.buf2
buf[0] = command
with self.i2c_device as i2c:
i2c.write_then_readinto(buf, buf, out_end=1)
return buf[1] << 8 | buf[0]