# 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
Implementation Notes
--------------------
**Hardware:**
* Adafruit `APDS9960 Proximity, Light, RGB, and Gesture Sensor
<https://www.adafruit.com/product/3595>`_ (Product ID: 3595)
**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
import digitalio
from adafruit_register.i2c_bits import RWBits
from adafruit_register.i2c_bit import RWBit
from adafruit_bus_device.i2c_device import I2CDevice
from micropython import const
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_APDS9960.git"
# ADDRESS_DEF = const(0x39)
# INTEGRATION_TIME_DEF = const(0x01)
# GAIN_DEF = const(0x01)
# 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)
# pylint: disable-msg=too-many-instance-attributes
[docs]class APDS9960:
"""
APDS9900 provide basic driver services for the ASDS9960 breakout board
:param ~busio.I2C i2c: The I2C bus the BME280 is connected to
:param ~microcontroller.Pin interrupt_pin: Interrupt pin. Defaults to `None`
:param int address: The I2C device address. Defaults to :const:`0x39`
:param int integration_time: integration time. Defaults to :const:`0x01`
:param int gain: Device gain. Defaults to :const:`0x01`
:param int rotation: rotation of the device. Defaults to :const:`0`
**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:`sensor.proximity` attribute
.. code-block:: python
proximity = apds.proximity
"""
_gesture_enable = RWBit(APDS9960_ENABLE, 6)
_gesture_valid = RWBit(APDS9960_GSTATUS, 0)
_gesture_mode = RWBit(APDS9960_GCONF4, 0)
_proximity_persistance = RWBits(4, APDS9960_PERS, 4)
def __init__(
self,
i2c,
*,
interrupt_pin=None,
address=0x39,
integration_time=0x01,
gain=0x01,
rotation=0
):
self.buf129 = None
self.buf2 = bytearray(2)
self.i2c_device = I2CDevice(i2c, address)
self._interrupt_pin = interrupt_pin
if interrupt_pin:
self._interrupt_pin.switch_to_input(pull=digitalio.Pull.UP)
if self._read8(APDS9960_ID) != 0xAB:
raise RuntimeError()
self.enable_gesture = False
self.enable_proximity = False
self.enable_color = False
self._rotation = rotation
self.enable_proximity_interrupt = False
self.clear_interrupt()
self.enable = False
time.sleep(0.010)
self.enable = True
time.sleep(0.010)
self.color_gain = gain
self.integration_time = integration_time
self.gesture_dimensions = 0x00 # all
self.gesture_fifo_threshold = 0x01 # fifo 4
self.gesture_gain = 0x02 # gain 4
self.gesture_proximity_threshold = 50
self._reset_counts()
# gesture pulse length=0x2 pulse count=0x3
self._write8(APDS9960_GPULSE, (0x2 << 6) | 0x3)
## BOARD
def _reset_counts(self):
"""Gesture detection internal counts"""
self._saw_down_start = 0
self._saw_up_start = 0
self._saw_left_start = 0
self._saw_right_start = 0
enable = RWBit(APDS9960_ENABLE, 0)
"""Board enable. True to enable, False to disable"""
enable_color = RWBit(APDS9960_ENABLE, 1)
"""Color detection enable flag.
True when color detection is enabled, else False"""
enable_proximity = RWBit(APDS9960_ENABLE, 2)
"""Enable of proximity mode"""
gesture_fifo_threshold = RWBits(2, APDS9960_GCONF1, 6)
"""Gesture fifo threshold value: range 0-3"""
gesture_gain = RWBits(2, APDS9960_GCONF2, 5)
"""Gesture gain value: range 0-3"""
color_gain = RWBits(2, APDS9960_CONTROL, 0)
"""Color gain value"""
enable_proximity_interrupt = RWBit(APDS9960_ENABLE, 5)
"""Proximity interrupt enable flag. True if enabled,
False to disable"""
## GESTURE ROTATION
@property
def rotation(self):
"""Gesture rotation offset. Acceptable values are 0, 90, 180, 270."""
return self._rotation
@rotation.setter
def rotation(self, new_rotation):
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")
## GESTURE DETECTION
@property
def enable_gesture(self):
"""Gesture detection enable flag. True to enable, False to disable.
Note that when disabled, gesture mode is turned off"""
return self._gesture_enable
@enable_gesture.setter
def enable_gesture(self, enable_flag):
if not enable_flag:
self._gesture_mode = False
self._gesture_enable = enable_flag
[docs] def rotated_gesture(self, original_gesture):
"""Applies rotation offset to the given gesture direction and returns the result"""
directions = [1, 4, 2, 3]
new_index = (directions.index(original_gesture) + self._rotation // 90) % 4
return directions[new_index]
[docs] def gesture(self): # pylint: disable-msg=too-many-branches
"""Returns gesture code if detected. =0 if no gesture detected
=1 if an UP, =2 if a DOWN, =3 if an LEFT, =4 if a RIGHT
"""
# buffer to read of contents of device FIFO buffer
if not self.buf129:
self.buf129 = bytearray(129)
buffer = self.buf129
buffer[0] = APDS9960_GFIFO_U
if not self._gesture_valid:
return 0
time_mark = 0
gesture_received = 0
while True:
up_down_diff = 0
left_right_diff = 0
gesture_received = 0
time.sleep(0.030) # 30 ms
n_recs = self._read8(APDS9960_GFLVL)
if n_recs:
with self.i2c_device as i2c:
i2c.write_then_readinto(
buffer,
buffer,
out_end=1,
in_start=1,
in_end=min(129, 1 + n_recs * 4),
)
upp, down, left, right = buffer[1:5]
if abs(upp - down) > 13:
up_down_diff = upp - down
if abs(left - right) > 13:
left_right_diff = left - right
if up_down_diff != 0:
if up_down_diff < 0:
# either leading edge of down movement
# or trailing edge of up movement
if self._saw_up_start:
gesture_received = 0x01 # up
else:
self._saw_down_start += 1
elif up_down_diff > 0:
# either leading edge of up movement
# or trailing edge of down movement
if self._saw_down_start:
gesture_received = 0x02 # down
else:
self._saw_up_start += 1
if left_right_diff != 0:
if left_right_diff < 0:
# either leading edge of right movement
# trailing edge of left movement
if self._saw_left_start:
gesture_received = 0x03 # left
else:
self._saw_right_start += 1
elif left_right_diff > 0:
# either leading edge of left movement
# trailing edge of right movement
if self._saw_right_start:
gesture_received = 0x04 # right
else:
self._saw_left_start += 1
# saw a leading or trailing edge; start timer
if up_down_diff or left_right_diff:
time_mark = time.monotonic()
# finished when a gesture is detected or ran out of time (300ms)
if gesture_received or time.monotonic() - time_mark > 0.300:
self._reset_counts()
break
if gesture_received != 0:
if self._rotation != 0:
return self.rotated_gesture(gesture_received)
return gesture_received
@property
def gesture_dimensions(self):
"""Gesture dimension value: range 0-3"""
return self._read8(APDS9960_GCONF3)
@gesture_dimensions.setter
def gesture_dimensions(self, dims):
self._write8(APDS9960_GCONF3, dims & 0x03)
@property
def color_data_ready(self):
"""Color data ready flag. zero if not ready, 1 is ready"""
return self._read8(APDS9960_STATUS) & 0x01
@property
def color_data(self):
"""Tuple containing r, g, b, c values"""
return (
self._color_data16(APDS9960_CDATAL + 2),
self._color_data16(APDS9960_CDATAL + 4),
self._color_data16(APDS9960_CDATAL + 6),
self._color_data16(APDS9960_CDATAL),
)
### PROXIMITY
@property
def proximity_interrupt_threshold(self):
"""Tuple containing low and high threshold
followed by the proximity interrupt persistance.
When setting the proximity interrupt threshold values using a tuple of
zero to three values: low threshold, high threshold, persistance.
persistance defaults to 4 if not provided"""
return (
self._read8(APDS9960_PILT),
self._read8(APDS9960_PIHT),
self._proximity_persistance,
)
@proximity_interrupt_threshold.setter
def proximity_interrupt_threshold(self, setting_tuple):
if setting_tuple:
self._write8(APDS9960_PILT, setting_tuple[0])
if len(setting_tuple) > 1:
self._write8(APDS9960_PIHT, setting_tuple[1])
persist = 4 # default 4
if len(setting_tuple) > 2:
persist = min(setting_tuple[2], 7)
self._proximity_persistance = persist
@property
def gesture_proximity_threshold(self):
"""Proximity threshold value: range 0-255"""
return self._read8(APDS9960_GPENTH)
@gesture_proximity_threshold.setter
def gesture_proximity_threshold(self, thresh):
self._write8(APDS9960_GPENTH, thresh & 0xFF)
@property
def proximity(self):
"""Proximity value: range 0-255"""
return self._read8(APDS9960_PDATA)
[docs] def clear_interrupt(self):
"""Clear all interrupts"""
self._writecmdonly(APDS9960_AICLEAR)
@property
def integration_time(self):
"""Proximity integration time: range 0-255"""
return self._read8(APDS9960_ATIME)
@integration_time.setter
def integration_time(self, int_time):
self._write8(APDS9960_ATIME, int_time & 0xFF)
# method for reading and writing to I2C
def _write8(self, command, abyte):
"""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):
"""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):
"""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 _color_data16(self, command):
"""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]