From 424e72cd30ae3279f82480ca599626f79fa7995e Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sun, 14 Nov 2021 18:38:13 -0600 Subject: [PATCH 1/8] Reducing memory footprint by using _naming for purely internal constants --- adafruit_apds9960/apds9960.py | 104 +++++++++++++++++----------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/adafruit_apds9960/apds9960.py b/adafruit_apds9960/apds9960.py index edea299..216e0b0 100644 --- a/adafruit_apds9960/apds9960.py +++ b/adafruit_apds9960/apds9960.py @@ -56,23 +56,23 @@ # GAIN_DEF = const(0x01) # APDS9960_RAM = const(0x00) -APDS9960_ENABLE = const(0x80) -APDS9960_ATIME = const(0x81) +_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_PILT = const(0x89) +_APDS9960_PIHT = const(0x8B) +_APDS9960_PERS = const(0x8C) # APDS9960_CONFIG1 = const(0x8D) # APDS9960_PPULSE = const(0x8E) -APDS9960_CONTROL = const(0x8F) +_APDS9960_CONTROL = const(0x8F) # APDS9960_CONFIG2 = const(0x90) -APDS9960_ID = const(0x92) -APDS9960_STATUS = const(0x93) -APDS9960_CDATAL = const(0x94) +_APDS9960_ID = const(0x92) +_APDS9960_STATUS = const(0x93) +_APDS9960_CDATAL = const(0x94) # APDS9960_CDATAH = const(0x95) # APDS9960_RDATAL = const(0x96) # APDS9960_RDATAH = const(0x97) @@ -80,28 +80,28 @@ # APDS9960_GDATAH = const(0x99) # APDS9960_BDATAL = const(0x9A) # APDS9960_BDATAH = const(0x9B) -APDS9960_PDATA = const(0x9C) +_APDS9960_PDATA = const(0x9C) # APDS9960_POFFSET_UR = const(0x9D) # APDS9960_POFFSET_DL = const(0x9E) # APDS9960_CONFIG3 = const(0x9F) -APDS9960_GPENTH = const(0xA0) +_APDS9960_GPENTH = const(0xA0) # APDS9960_GEXTH = const(0xA1) -APDS9960_GCONF1 = const(0xA2) -APDS9960_GCONF2 = const(0xA3) +_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_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_AICLEAR = const(0xE7) +_APDS9960_GFIFO_U = const(0xFC) # APDS9960_GFIFO_D = const(0xFD) # APDS9960_GFIFO_L = const(0xFE) # APDS9960_GFIFO_R = const(0xFF) @@ -144,10 +144,10 @@ class APDS9960: """ - _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) + _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, @@ -164,7 +164,7 @@ def __init__( self.i2c_device = I2CDevice(i2c, address) - if self._read8(APDS9960_ID) != 0xAB: + if self._read8(_APDS9960_ID) != 0xAB: raise RuntimeError() self.enable_gesture = False @@ -188,7 +188,7 @@ def __init__( self._reset_counts() # gesture pulse length=0x2 pulse count=0x3 - self._write8(APDS9960_GPULSE, (0x2 << 6) | 0x3) + self._write8(_APDS9960_GPULSE, (0x2 << 6) | 0x3) ## BOARD def _reset_counts(self) -> None: @@ -198,20 +198,20 @@ def _reset_counts(self) -> None: self._saw_left_start = 0 self._saw_right_start = 0 - enable = RWBit(APDS9960_ENABLE, 0) + enable = RWBit(_APDS9960_ENABLE, 0) """Board enable. True to enable, False to disable""" - enable_color = RWBit(APDS9960_ENABLE, 1) + 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_proximity = RWBit(_APDS9960_ENABLE, 2) """Enable of proximity mode""" - gesture_fifo_threshold = RWBits(2, APDS9960_GCONF1, 6) + gesture_fifo_threshold = RWBits(2, _APDS9960_GCONF1, 6) """Gesture fifo threshold value: range 0-3""" - gesture_gain = RWBits(2, APDS9960_GCONF2, 5) + gesture_gain = RWBits(2, _APDS9960_GCONF2, 5) """Gesture gain value: range 0-3""" - color_gain = RWBits(2, APDS9960_CONTROL, 0) + color_gain = RWBits(2, _APDS9960_CONTROL, 0) """Color gain value""" - enable_proximity_interrupt = RWBit(APDS9960_ENABLE, 5) + enable_proximity_interrupt = RWBit(_APDS9960_ENABLE, 5) """Proximity interrupt enable flag. True if enabled, False to disable""" @@ -256,7 +256,7 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches self.buf129 = bytearray(129) buffer = self.buf129 - buffer[0] = APDS9960_GFIFO_U + buffer[0] = _APDS9960_GFIFO_U if not self._gesture_valid: return 0 @@ -269,7 +269,7 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches gesture_received = 0 time.sleep(0.030) # 30 ms - n_recs = self._read8(APDS9960_GFLVL) + n_recs = self._read8(_APDS9960_GFLVL) if n_recs: with self.i2c_device as i2c: @@ -336,25 +336,25 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches @property def gesture_dimensions(self) -> int: """Gesture dimension value: range 0-3""" - return self._read8(APDS9960_GCONF3) + return self._read8(_APDS9960_GCONF3) @gesture_dimensions.setter def gesture_dimensions(self, dims: int) -> None: - self._write8(APDS9960_GCONF3, dims & 0x03) + self._write8(_APDS9960_GCONF3, dims & 0x03) @property def color_data_ready(self) -> int: """Color data ready flag. zero if not ready, 1 is ready""" - return self._read8(APDS9960_STATUS) & 0x01 + return self._read8(_APDS9960_STATUS) & 0x01 @property def color_data(self) -> Tuple[int, int, int, int]: """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), + self._color_data16(_APDS9960_CDATAL + 2), + self._color_data16(_APDS9960_CDATAL + 4), + self._color_data16(_APDS9960_CDATAL + 6), + self._color_data16(_APDS9960_CDATAL), ) ### PROXIMITY @@ -366,17 +366,17 @@ def proximity_interrupt_threshold(self) -> Tuple[int, int, int]: 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._read8(_APDS9960_PILT), + self._read8(_APDS9960_PIHT), self._proximity_persistance, ) @proximity_interrupt_threshold.setter def proximity_interrupt_threshold(self, setting_tuple: Tuple[int, ...]) -> None: if setting_tuple: - self._write8(APDS9960_PILT, setting_tuple[0]) + self._write8(_APDS9960_PILT, setting_tuple[0]) if len(setting_tuple) > 1: - self._write8(APDS9960_PIHT, 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) @@ -385,29 +385,29 @@ def proximity_interrupt_threshold(self, setting_tuple: Tuple[int, ...]) -> None: @property def gesture_proximity_threshold(self) -> int: """Proximity threshold value: range 0-255""" - return self._read8(APDS9960_GPENTH) + return self._read8(_APDS9960_GPENTH) @gesture_proximity_threshold.setter def gesture_proximity_threshold(self, thresh: int) -> None: - self._write8(APDS9960_GPENTH, thresh & 0xFF) + self._write8(_APDS9960_GPENTH, thresh & 0xFF) @property def proximity(self) -> int: """Proximity value: range 0-255""" - return self._read8(APDS9960_PDATA) + return self._read8(_APDS9960_PDATA) def clear_interrupt(self) -> None: """Clear all interrupts""" - self._writecmdonly(APDS9960_AICLEAR) + self._writecmdonly(_APDS9960_AICLEAR) @property def integration_time(self) -> int: """Proximity integration time: range 0-255""" - return self._read8(APDS9960_ATIME) + return self._read8(_APDS9960_ATIME) @integration_time.setter def integration_time(self, int_time: int) -> None: - self._write8(APDS9960_ATIME, int_time & 0xFF) + self._write8(_APDS9960_ATIME, int_time & 0xFF) # method for reading and writing to I2C def _write8(self, command: int, abyte: int) -> None: From 97932e168081734aee01a0b3823dcf2f8f044eb6 Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Thu, 18 Nov 2021 20:27:20 -0600 Subject: [PATCH 2/8] Broad register accessor refactor, adding optional sensible defaults --- adafruit_apds9960/apds9960.py | 644 +++++++++++++++++++++++++--------- 1 file changed, 477 insertions(+), 167 deletions(-) diff --git a/adafruit_apds9960/apds9960.py b/adafruit_apds9960/apds9960.py index 216e0b0..e198623 100644 --- a/adafruit_apds9960/apds9960.py +++ b/adafruit_apds9960/apds9960.py @@ -37,7 +37,8 @@ """ import time from adafruit_register.i2c_bits import RWBits -from adafruit_register.i2c_bit import RWBit +from adafruit_register.i2c_bit import RWBit, ROBit +from adafruit_register.i2c_struct import UnaryStruct, ROUnaryStruct from adafruit_bus_device.i2c_device import I2CDevice from micropython import const @@ -51,10 +52,6 @@ __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) @@ -67,9 +64,9 @@ _APDS9960_PIHT = const(0x8B) _APDS9960_PERS = const(0x8C) # APDS9960_CONFIG1 = const(0x8D) -# APDS9960_PPULSE = const(0x8E) +_APDS9960_PPULSE = const(0x8E) _APDS9960_CONTROL = const(0x8F) -# APDS9960_CONFIG2 = const(0x90) +_APDS9960_CONFIG2 = const(0x90) _APDS9960_ID = const(0x92) _APDS9960_STATUS = const(0x93) _APDS9960_CDATAL = const(0x94) @@ -81,25 +78,25 @@ # 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_POFFSET_UR = const(0x9D) +# _APDS9960_POFFSET_DL = const(0x9E) +# _APDS9960_CONFIG3 = const(0x9F) _APDS9960_GPENTH = const(0xA0) -# APDS9960_GEXTH = const(0xA1) +_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_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_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_PICLEAR = const(0xE5) +_APDS9960_CICLEAR = const(0xE6) _APDS9960_AICLEAR = const(0xE7) _APDS9960_GFIFO_U = const(0xFC) # APDS9960_GFIFO_D = const(0xFD) @@ -107,7 +104,7 @@ # APDS9960_GFIFO_R = const(0xFF) -# pylint: disable-msg=too-many-instance-attributes +# pylint: disable-msg=too-many-instance-attributes, too-many-public-methods class APDS9960: """ APDS9900 provide basic driver services for the ASDS9960 breakout board @@ -144,19 +141,14 @@ class APDS9960: """ - _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: I2C, *, address: int = 0x39, - integration_time: int = 0x01, - gain: int = 0x01, - rotation: int = 0 + rotation: int = 0, + reset: bool = True, + set_defaults: bool = True ): self.buf129 = None @@ -164,58 +156,472 @@ def __init__( self.i2c_device = I2CDevice(i2c, address) - if self._read8(_APDS9960_ID) != 0xAB: + if self.device_id != 0xAB: raise RuntimeError() - self.enable_gesture = False - self.enable_proximity = False - self.enable_color = False self._rotation = rotation + + if reset: + self.reset() + + if set_defaults: + self.defaults() + + self._reset_counts() + + def defaults(self): + """Apply sensible defaults to fit most use cases""" + self.proximity_interrupt_threshold = (20, 255, 5) + self.proximity_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA, 1x + self.proximity_gain = 1 + + self.gesture_engine_config = (5, 100, 2, 2) + self.gesture_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA, 1x + self.gesture_gain = 1 + + def reset(self): + """Reset device registers to power-on defaults""" + self.enable_proximity = False self.enable_proximity_interrupt = False - self.clear_interrupt() + self.proximity_interrupt_threshold = (0, 0, 0) + self.proximity_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA, 1x + self.proximity_gain = 0 + + self.enable_gesture = False + self.enable_gesture_interrupt = False + self.gmode = False + self.clear_gesture_fifo() + self.gesture_engine_config = (0, 0, 0, 0) + self.gesture_led_config = (0, 1, 0, 0) + self.gesture_gain = 0 + self.gesture_fifo_threshold = 0 + + self.enable_color = False + self.color_integration_time = 1 + + self.clear_all_interrupts() 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) + # Device Configuration + device_id = UnaryStruct(_APDS9960_ID, " None: - """Gesture detection internal counts""" - self._saw_down_start = 0 - self._saw_up_start = 0 - self._saw_left_start = 0 - self._saw_right_start = 0 + If false, the sensor will enter a sleep state until re-enabled - 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""" + While in sleep state the internal oscillator and other circuits are disabled, resulting in + very low power consumption but I2C messages will still be handled""" 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""" + """Enables operation of proximity engine""" + enable_gesture = RWBit(_APDS9960_ENABLE, 6) + """Enables operation of gesture engine""" + enable_color = RWBit(_APDS9960_ENABLE, 1) + """Enables operation of color/light engine""" + + def clear_all_interrupts(self): + """Clears all non-gesture internal interrupts""" + self._writecmdonly(_APDS9960_AICLEAR) + + # _wtime = UnaryStruct(_APDS9960_WTIME, " int: + # """Number of 2.78ms cycles to wait between sensor-wide cycles + + # Multiplied by 12x if `cycle_wait_time_long` is true""" + # return 256 - self._wtime + + # @cycle_wait_time.setter + # def cycle_wait_time(self, wait_time: int) -> None: + # """Number of 2.78ms cycles to wait between sensor-wide cycles + + # Multiplied by 12x if `cycle_wait_time_long` is true""" + # if 1 <= wait_time <= 256: + # self._atime = 256 - self._wtime + # else: + # raise ValueError + + # Proximity Data + proximity = ROUnaryStruct(_APDS9960_PDATA, " Tuple[int, int, int, int]: + """Tuple representing LED configuration for proximity measurements + + Pulse count (0-63): 0 = 1 pulse, 63 = 64 pulses (default: 0) + Pulse length (0-3): 0 = 4 usec, 1 = 8 usec, 2 = 16 usec, 3 = 32 usec (default: 1) + LED drive (0-3): 0 = 100 mA, 1 = 50 mA, 2 = 25 mA, 3 = 12.5 mA (default: 0) + LED boost (0-3): 0 = 100%, 1 = 150%, 2 = 200%, 3 = 300% (default: 0)""" + return (self._ppulse, self._pplen, self._ldrive, self._ledboost) + + @proximity_led_config.setter + def proximity_led_config(self, led_config: Tuple[int, int, int, int]) -> None: + """Tuple representing LED configuration for proximity measurements + + Pulse count (0-63): 0 = 1 pulse, 63 = 64 pulses (default: 0) + Pulse length (0-3): 0 = 4 usec, 1 = 8 usec, 2 = 16 usec, 3 = 32 usec (default: 1) + LED drive (0-3): 0 = 100 mA, 1 = 50 mA, 2 = 25 mA, 3 = 12.5 mA (default: 0) + LED boost (0-3): 0 = 100%, 1 = 150%, 2 = 200%, 3 = 300% (default: 0)""" + if 0 <= led_config[0] <= 63: + self._ppulse = led_config[0] + else: + raise ValueError + + if 0 <= led_config[1] <= 3: + self._pplen = led_config[1] + else: + raise ValueError + + if 0 <= led_config[2] <= 3: + self._ldrive = led_config[2] + else: + raise ValueError + + if 0 <= led_config[3] <= 3: + self._ledboost = led_config[3] + else: + raise ValueError + + # Proximity Sensor Configuration + _pgain = RWBits(2, _APDS9960_CONTROL, 2) + + @property + def proximity_gain(self) -> int: + """Proximity sensor gain multipler + + Proximity Gain (0-3): 0 = 1x, 1 = 2x, 2 = 4x, 3 = 8x (default: 0)""" + return self._pgain + + @proximity_gain.setter + def proximity_gain(self, gain: int) -> None: + """Proximity sensor gain multipler + + Proximity Gain (0-3): 0 = 1x, 1 = 2x, 2 = 4x, 3 = 8x (default: 0)""" + if 0 <= gain <= 3: + self._pgain = gain + else: + raise ValueError + + ## Proximity masking, offsets, gain compensation + # _pmsk_u = RWBit(_APDS9960_CONFIG3, 3) + # _pmsk_d = RWBit(_APDS9960_CONFIG3, 2) + # _pmsk_l = RWBit(_APDS9960_CONFIG3, 1) + # _pmsk_r = RWBit(_APDS9960_CONFIG3, 0) + # _poffset_ur_sign = RWBit(APDS9960_POFFSET_UR, 1) + # _poffset_ur = RWBits(7, APDS9960_POFFSET_UR) + # _poffset_dl_sign = RWBit(APDS9960_POFFSET_DL, 1) + # _poffset_dl = RWBits(7, APDS9960_POFFSET_DL) + # _pcmp = RWBit(_APDS9960_CONFIG3, 5) + + # Proximity Interrupts and Interrupt Configuration + proximity_interrupt = ROBit(_APDS9960_STATUS, 5) + """Asserted when persistence threshold is met by sequential valid proximity measurements + + Cleared by manual 'proximity_interrupt_clear' or 'all_interrupt_clear' + + Can assert external interrupt pin if 'enable_proximity_interrupt' is true""" + enable_proximity_interrupt = RWBit(_APDS9960_ENABLE, 5) + """If true, internal proximity interrupt asserts interrupt pin""" + + # proximity_saturation_interrupt = ROBit(_APDS9960_STATUS, 6) + # """Asserted if an analog saturation event occurs during proximity or gesture measurements + + # Cleared by 'proximity_interrupt_clear' or by disabling proximity engine + + # Can assert external interrupt pin if 'enable_proximity_interrupt' is true""" + # enable_proximity_saturation_interrupt = RWBit(_APDS9960_CONFIG2, 5) + # """If true, proximity saturation interrupt asserts interrupt pin""" + + def clear_proximity_interrupt(self): + """Clears internal proximity interrupt""" + self._writecmdonly(_APDS9960_PICLEAR) + + _pilt = UnaryStruct(_APDS9960_PILT, " Tuple[int, int, int]: + """Tuple representing proximity engnie low/high threshold (0-255) and persistence (0-15) + + Controls assertion of internal proximty interrupt + + Internal interrupt is only asserted when the number of valid, in-threshold measurements is + equal to or greater than the persistence setting""" + return ( + self._pilt, + self._piht, + self._ppers, + ) + + @proximity_interrupt_threshold.setter + def proximity_interrupt_threshold( + self, setting_tuple: Tuple[int, int, int] + ) -> None: + """Tuple representing proximity engnie low/high threshold (0-255) and persistence (0-15) + + Controls assertion of internal proximty interrupt + + Internal interrupt is only asserted when the number of valid, in-threshold measurements is + equal to or greater than the persistence setting""" + if 0 <= setting_tuple[0] <= 255: + self._pilt = setting_tuple[0] + else: + raise ValueError + + if 0 <= setting_tuple[1] <= 255: + self._piht = setting_tuple[1] + else: + raise ValueError + + if 0 <= setting_tuple[2] <= 15: + self._ppers = setting_tuple[2] + else: + raise ValueError + + # Gesture Data and Status + gesture_valid = ROBit(_APDS9960_GSTATUS, 0) + _gflvl = ROUnaryStruct(_APDS9960_GFLVL, " bool: + """True if the sensor's gesture engine is currently looping""" + return self._gmode + + def force_gesture_loop_entry(self): + """Force gesture engine to enter regardless of proximity entry threshold status""" + self._gmode = True + + def force_gesture_loop_exit(self): + """Forces gesture engine to halt if it is currently looping regardless of exit threshold""" + self._gmode = False + + _gfifo_clr = RWBit(_APDS9960_GCONF4, 2) + + def clear_gesture_fifo(self): + """Clears gestire FIFO, interrupt, overflow flag, and resets FIFO level""" + self._gfifo_clr = True + + # Gesture Core Engine Configuration + _gpenth = UnaryStruct(_APDS9960_GPENTH, " Tuple[int, int, int, int, int]: + """Tuple representing configuration for gesture engine + + Proximity entry threshold (0-255): Minimum proximity value for gesture engine entrance + Exit threshold (0-255): Minimum proximity value for gesture engine loop persistence + Persistence (0-3): Number of out-of-thresold loops to continue recording gesture data + 0 = 1 cycle, 1 = 2 cycles, 2 = 4 cycles, 3 = 7 cycles + Wait time (0-7): Number of 2.78ms cycles to wait between gesture 1.39 ms engine loops""" + return (self._gpenth, self._gexth, self._gpers, self._gwtime) + + @gesture_engine_config.setter + def gesture_engine_config( + self, gesture_config: Tuple[int, int, int, int, int] + ) -> None: + """Tuple representing configuration for gesture engine + + Proximity entry threshold (0-255): Minimum proximity value for gesture engine entrance + Exit threshold (0-255): Minimum proximity value for gesture engine loop persistence + Persistence (0-3): Number of out-of-thresold loops to continue recording gesture data + 0 = 1 cycle, 1 = 2 cycles, 2 = 4 cycles, 3 = 7 cycles + Wait time (0-7): Number of 2.78ms cycles to wait between gesture 1.39 ms engine loops""" + if 0 <= gesture_config[0] <= 255: + self._gpenth = gesture_config[0] + else: + raise ValueError + + if 0 <= gesture_config[1] <= 255: + self._gexth = gesture_config[1] + else: + raise ValueError + + if 0 <= gesture_config[2] <= 3: + self._gpers = gesture_config[2] + else: + raise ValueError + + if 0 <= gesture_config[3] <= 7: + self._gwtime = gesture_config[3] + else: + raise ValueError + + # Gesture LED/Sensor Configuration + _gpulse = RWBits(6, _APDS9960_GPULSE, 0) + _gplen = RWBits(2, _APDS9960_GPULSE, 6) + _gldrive = RWBits(2, _APDS9960_GCONF2, 3) + + @property + def gesture_led_config(self) -> Tuple[int, int, int, int]: + """Tuple representing LED configuration for gesture measurements + + Pulse count (0-63): 0 = 1 pulse, 63 = 64 pulses (default: 0) + Pulse length (0-3): 0 = 4 usec, 1 = 8 usec, 2 = 16 usec, 3 = 32 usec (default: 1) + LED drive (0-3): 0 = 100 mA, 1 = 50 mA, 2 = 25 mA, 3 = 12.5 mA (default: 0) + LED boost (0-3): 0 = 100%, 1 = 150%, 2 = 200%, 3 = 300% (default: 0)""" + return (self._gpulse, self._gplen, self._gldrive, self._ledboost) + + @gesture_led_config.setter + def gesture_led_config(self, led_config: Tuple[int, int, int, int]) -> None: + """Tuple representing LED configuration for gesture measurements + + Pulse count (0-63): 0 = 1 pulse, 63 = 64 pulses (default: 0) + Pulse length (0-3): 0 = 4 usec, 1 = 8 usec, 2 = 16 usec, 3 = 32 usec (default: 1) + LED drive (0-3): 0 = 100 mA, 1 = 50 mA, 2 = 25 mA, 3 = 12.5 mA (default: 0) + LED boost (0-3): 0 = 100%, 1 = 150%, 2 = 200%, 3 = 300% (default: 0)""" + if 0 <= led_config[0] <= 63: + self._gpulse = led_config[0] + else: + raise ValueError + + if 0 <= led_config[1] <= 3: + self._gplen = led_config[1] + else: + raise ValueError + + if 0 <= led_config[2] <= 3: + self._gldrive = led_config[2] + else: + raise ValueError + + if 0 <= led_config[3] <= 3: + self._ledboost = led_config[3] + else: + raise ValueError + + # Proximity Sensor Configuration + _ggain = RWBits(2, _APDS9960_GCONF2, 5) + + @property + def gesture_gain(self) -> int: + """Gesture sensor gain multipler + + Gesture Gain (0-3): 0 = 1x, 1 = 2x, 2 = 4x, 3 = 8x (default: 0)""" + return self._ggain + + @gesture_gain.setter + def gesture_gain(self, gain: int) -> None: + """Gesture sensor gain multipler + + Gesture Gain (0-3): 0 = 1x, 1 = 2x, 2 = 4x, 3 = 8x (default: 0)""" + if 0 <= gain <= 3: + self._ggain = gain + else: + raise ValueError + + # _gdims = RWBits(2, _APDS9960_GCONF3, 0) + # _goffset_u_sign = RWBit(_APDS9960_GOFFSET_U, 1) + # _goffset_u = RWBits(7, _APDS9960_GOFFSET_U) + # _goffset_d_sign = RWBit(_APDS9960_GOFFSET_D, 1) + # _goffset_d = RWBits(7, _APDS9960_GOFFSET_D) + # _goffset_l_sign = RWBit(_APDS9960_GOFFSET_L, 1) + # _goffset_l = RWBits(7, _APDS9960_GOFFSET_L) + # _goffset_r_sign = RWBit(_APDS9960_GOFFSET_R, 1) + # _goffset_r = RWBits(7, _APDS9960_GOFFSET_R) + + # Gesture Interrupts and Interrupt Configuration + gesture_interrupt = ROBit(_APDS9960_STATUS, 2) + """Asserted when gesture FIFO threshold is met by sequential valid gesture measurements + + Cleared by full read of gesture FIFOs or manual gesture FIFO Clear""" + enable_gesture_interrupt = RWBit(_APDS9960_GCONF4, 1) + """If true, internal gesture interrupt asserts interrupt pin""" + _gfifoth = RWBits(2, _APDS9960_GCONF1, 6) + + @property + def gesture_fifo_threshold(self) -> int: + """Minimum gesture FIFO depth to reach before triggering internal gesture interrupt + + FIFO Threshold (0-3): 0 = 1 dataset, 1 = 4 datasets, 2 = 8 datasets, 3 = 16 datasets""" + return self._gfifoth + + @gesture_fifo_threshold.setter + def gesture_fifo_threshold(self, threshold: int) -> None: + """Minimum gesture FIFO depth to reach before triggering internal gesture interrupt + + FIFO Threshold (0-3): 0 = 1 dataset, 1 = 4 datasets, 2 = 8 datasets, 3 = 16 datasets""" + if 0 <= threshold <= 3: + self._gwtime = threshold + else: + raise ValueError + + # Color Data and Status + @property + def color_data(self) -> Tuple[int, int, int, int]: + """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), + ) + + color_valid = ROBit(_APDS9960_STATUS, 0) + + # Color Engine Configuration + + _atime = UnaryStruct(_APDS9960_ATIME, " int: + """Number of 2.78ms cycles to wait for ADC integration during color operations""" + return 256 - self._atime + + @color_integration_time.setter + def color_integration_time(self, integration_time: int) -> None: + """Number of 2.78ms cycles to wait for ADC integration during color operations""" + if 1 <= integration_time <= 256: + self._atime = 256 - integration_time + else: + raise ValueError + + # _ailtl = UnaryStruct(_APDS_AILTL, " None: + """Clears internal color interrupt""" + self._writecmdonly(_APDS9960_CICLEAR) + + @property + def color_data_ready(self) -> int: + """Color data ready flag. zero if not ready, 1 is ready""" + return self.color_valid + + # Gesture processing configuration @property def rotation(self) -> int: """Gesture rotation offset. Acceptable values are 0, 90, 180, 270.""" @@ -228,19 +634,15 @@ def rotation(self, new_rotation: int) -> None: else: raise ValueError("Rotation value must be one of: 0, 90, 180, 270") - ## GESTURE DETECTION - @property - def enable_gesture(self) -> bool: - """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: bool) -> None: - if not enable_flag: - self._gesture_mode = False - self._gesture_enable = enable_flag + # Gesture processing globals + def _reset_counts(self) -> None: + """Reset internal gesture detection state""" + self._saw_down_start = 0 + self._saw_up_start = 0 + self._saw_left_start = 0 + self._saw_right_start = 0 + # Gesture processing def rotated_gesture(self, original_gesture: int) -> int: """Applies rotation offset to the given gesture direction and returns the result""" directions = [1, 4, 2, 3] @@ -257,7 +659,7 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches buffer = self.buf129 buffer[0] = _APDS9960_GFIFO_U - if not self._gesture_valid: + if not self.gesture_valid: return 0 time_mark = 0.0 @@ -269,7 +671,7 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches gesture_received = 0 time.sleep(0.030) # 30 ms - n_recs = self._read8(_APDS9960_GFLVL) + n_recs = self._gflvl if n_recs: with self.i2c_device as i2c: @@ -333,91 +735,7 @@ def gesture(self) -> int: # pylint: disable-msg=too-many-branches return self.rotated_gesture(gesture_received) return gesture_received - @property - def gesture_dimensions(self) -> int: - """Gesture dimension value: range 0-3""" - return self._read8(_APDS9960_GCONF3) - - @gesture_dimensions.setter - def gesture_dimensions(self, dims: int) -> None: - self._write8(_APDS9960_GCONF3, dims & 0x03) - - @property - def color_data_ready(self) -> int: - """Color data ready flag. zero if not ready, 1 is ready""" - return self._read8(_APDS9960_STATUS) & 0x01 - - @property - def color_data(self) -> Tuple[int, int, int, int]: - """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[int, int, int]: - """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: Tuple[int, ...]) -> None: - 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) -> int: - """Proximity threshold value: range 0-255""" - return self._read8(_APDS9960_GPENTH) - - @gesture_proximity_threshold.setter - def gesture_proximity_threshold(self, thresh: int) -> None: - self._write8(_APDS9960_GPENTH, thresh & 0xFF) - - @property - def proximity(self) -> int: - """Proximity value: range 0-255""" - return self._read8(_APDS9960_PDATA) - - def clear_interrupt(self) -> None: - """Clear all interrupts""" - self._writecmdonly(_APDS9960_AICLEAR) - - @property - def integration_time(self) -> int: - """Proximity integration time: range 0-255""" - return self._read8(_APDS9960_ATIME) - - @integration_time.setter - def integration_time(self, int_time: int) -> None: - self._write8(_APDS9960_ATIME, int_time & 0xFF) - # 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 @@ -425,14 +743,6 @@ def _writecmdonly(self, command: int) -> None: 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 _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""" From 0f2fa939d959dce510ebbbfe5797ea38d7b3d19f Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Thu, 18 Nov 2021 22:48:52 -0600 Subject: [PATCH 3/8] Gesture engine refactor --- adafruit_apds9960/apds9960.py | 268 ++++++++++++++++++++++------------ 1 file changed, 173 insertions(+), 95 deletions(-) diff --git a/adafruit_apds9960/apds9960.py b/adafruit_apds9960/apds9960.py index e198623..02c6b33 100644 --- a/adafruit_apds9960/apds9960.py +++ b/adafruit_apds9960/apds9960.py @@ -44,7 +44,7 @@ try: # Only used for typing - from typing import Tuple + from typing import Tuple, List from busio import I2C except ImportError: pass @@ -103,6 +103,8 @@ # APDS9960_GFIFO_L = const(0xFE) # APDS9960_GFIFO_R = const(0xFF) +_GESTURE_NAMES = ["None", "Up", "Down", "Left", "Right"] +_CYCLE_TIME = 0.00278 # Sensor internal cycle time in milliseconds # pylint: disable-msg=too-many-instance-attributes, too-many-public-methods class APDS9960: @@ -148,7 +150,10 @@ def __init__( address: int = 0x39, rotation: int = 0, reset: bool = True, - set_defaults: bool = True + set_defaults: bool = True, + max_gesture_dataframe: int = 256, + data_stream_persist_cycles: int = 5, + data_stream_low_threshold: int = 30 ): self.buf129 = None @@ -167,16 +172,20 @@ def __init__( if set_defaults: self.defaults() - self._reset_counts() + self._max_dataframe_size = max_gesture_dataframe + self._data_stream_low_threshold = data_stream_low_threshold + self._data_stream_persist_cycles = data_stream_persist_cycles + self._data_stream_persist_sleep = self._data_stream_persist_cycles * _CYCLE_TIME def defaults(self): """Apply sensible defaults to fit most use cases""" - self.proximity_interrupt_threshold = (20, 255, 5) - self.proximity_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA, 1x + self.proximity_interrupt_threshold = (20, 150, 5) + self.proximity_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA x1 self.proximity_gain = 1 self.gesture_engine_config = (5, 100, 2, 2) - self.gesture_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA, 1x + self.gesture_led_config = (5, 2, 0, 2) # 4 pulses, 32us, 100mA x2 + self.gesture_fifo_threshold = 1 self.gesture_gain = 1 def reset(self): @@ -184,7 +193,7 @@ def reset(self): self.enable_proximity = False self.enable_proximity_interrupt = False self.proximity_interrupt_threshold = (0, 0, 0) - self.proximity_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA, 1x + self.proximity_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA x1 self.proximity_gain = 0 self.enable_gesture = False @@ -192,7 +201,7 @@ def reset(self): self.gmode = False self.clear_gesture_fifo() self.gesture_engine_config = (0, 0, 0, 0) - self.gesture_led_config = (0, 1, 0, 0) + self.gesture_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA x1 self.gesture_gain = 0 self.gesture_fifo_threshold = 0 @@ -206,8 +215,6 @@ def reset(self): self.enable = True time.sleep(0.010) - self._reset_counts() - # Device Configuration device_id = UnaryStruct(_APDS9960_ID, " None: """Color gain value""" color_interrupt = ROBit(_APDS9960_STATUS, 4) + """Asserted when color clear channel threshold is met or exceeded by sequential color results + + Cleared by 'clear_color_interrupt' or 'clear_all_interrupts'""" # color_saturation_interrupt = ROBit(_APDS9960_STATUS, 7) def clear_color_interrupt(self) -> None: @@ -621,7 +631,7 @@ def color_data_ready(self) -> int: """Color data ready flag. zero if not ready, 1 is ready""" return self.color_valid - # Gesture processing configuration + # Gesture processing rotation @property def rotation(self) -> int: """Gesture rotation offset. Acceptable values are 0, 90, 180, 270.""" @@ -634,106 +644,174 @@ def rotation(self, new_rotation: int) -> None: else: raise ValueError("Rotation value must be one of: 0, 90, 180, 270") - # Gesture processing globals - def _reset_counts(self) -> None: - """Reset internal gesture detection state""" - self._saw_down_start = 0 - self._saw_up_start = 0 - self._saw_left_start = 0 - self._saw_right_start = 0 - - # Gesture processing def rotated_gesture(self, original_gesture: int) -> int: """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] - def gesture(self) -> int: # 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) + def gesture_string(self) -> str: + """Gather and process gesture data, returns string representing gesture direction""" + gesture_number = self.gesture() + + return _GESTURE_NAMES[gesture_number] + + def gesture(self) -> int: + """Gather and process gesture data, returns int representing gesture direction + + 0 = None, 1 = Up, 2 = Down, 3 = Left, 4 = Right""" + dataframe = self._get_gesture_data() - buffer = self.buf129 - buffer[0] = _APDS9960_GFIFO_U - if not self.gesture_valid: - return 0 + if len(dataframe) > 0: + processed_gesture = self._process_gesture_data(dataframe) + if processed_gesture > 0: + return self.rotated_gesture(processed_gesture) - time_mark = 0.0 - gesture_received = 0 - while True: + return 0 - up_down_diff = 0 - left_right_diff = 0 - gesture_received = 0 - time.sleep(0.030) # 30 ms + def _get_gesture_data(self) -> List[Tuple[int, int, int, int]]: + """Retrieves sequential gesture datasets from FIFO, if any are available""" + dataframe = [] - n_recs = self._gflvl - if n_recs: + # If we've already overflowed, clear out the stale FIFOs right away and wait for fresh data + if self._gfov: + self.clear_gesture_fifo() + while not self.gesture_interrupt: + time.sleep(0.001) + # Only start retrieval if gvalid and if there are datasets to retrieve + dataset_count = self._gflvl + if self.gesture_valid and dataset_count > 0: + if self.buf129 is None: + self.buf129 = bytearray(129) + + # Stack new gesture datasets into our dataframe + # Also, keep stacking new datasets if they show up while we're reading in FIFO data + while True: + # Acquire all available data + dataset_count = self._gflvl + self.buf129[0] = _APDS9960_GFIFO_U with self.i2c_device as i2c: i2c.write_then_readinto( - buffer, - buffer, + self.buf129, + self.buf129, out_end=1, in_start=1, - in_end=min(129, 1 + n_recs * 4), + in_end=min(129, 1 + (dataset_count * 4)), + ) + + idx = 0 + # Unpack data stream into more usable U/D/L/R datasets + for i in range(dataset_count): + rec = i + 1 + idx = 1 + ((rec - 1) * 4) + + dataset_tuple = ( + self.buf129[idx], + self.buf129[idx + 1], + self.buf129[idx + 2], + self.buf129[idx + 3], ) - 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 + + # Drop fully-saturated and fully-zero to conserve memory + # Low-pass filter to remove potentially spurious very-low-count entries + if self._filter_dataset( + dataset_tuple, self._data_stream_low_threshold + ): + dataframe.append(dataset_tuple) + + # Break out of our loop if we have way too many datasets + if len(dataframe) > self._max_dataframe_size: + # print("Get: Halting, gflvl: {}, len: {}".format(self._gflvl, len(dataframe))) + break + + # Wait a very short time to see if new FIFO data has arrived before we drop out + time.sleep(self._data_stream_persist_sleep) + if self._gflvl == 0: + # print("Get: Halting, gflvl: {}, len: {}".format(self._gflvl, len(dataframe))) + break + # else: + # print("Get: Continuing, gflvl: {}, len: {}".format(self._gflvl, len(dataframe))) + return dataframe + + # pylint: disable-msg=no-else-return, too-many-return-statements, too-many-branches + def _process_gesture_data(self, dataframe: List[Tuple[int, int, int, int]]) -> int: + _gesture_delta_threshold = 30 + + first_dataset = dataframe[0] + last_dataset = dataframe[len(dataframe) - 1] + + # print("f: {}, l: {}".format(first_dataset, last_dataset)) + + ratios_first = self._dataset_ratios(first_dataset) + ratios_last = self._dataset_ratios(last_dataset) + + delta_ud = ratios_last[0] - ratios_first[0] + delta_lr = ratios_last[1] - ratios_first[1] + + state_ud = 0 + state_lr = 0 + + if delta_ud >= _gesture_delta_threshold: + state_ud = 1 + elif delta_ud <= -_gesture_delta_threshold: + state_ud = -1 + + if delta_lr >= _gesture_delta_threshold: + state_lr = 1 + elif delta_lr <= -_gesture_delta_threshold: + state_lr = -1 + + # Easy cases + if state_ud == -1 and state_lr == 0: + return 1 + elif state_ud == 1 and state_lr == 0: + return 2 + elif state_ud == 0 and state_lr == -1: + return 3 + elif state_ud == 0 and state_lr == 1: + return 4 + + # Not so easy cases + if state_ud == -1 and state_lr == 1: + if abs(delta_ud) > abs(delta_lr): + return 1 + else: + return 4 + elif state_ud == 1 and state_lr == -1: + if abs(delta_ud) > abs(delta_lr): + return 2 + else: + return 3 + elif state_ud == -1 and state_lr == -1: + if abs(delta_ud) > abs(delta_lr): + return 1 + else: + return 3 + elif state_ud == 1 and state_lr == 1: + if abs(delta_ud) > abs(delta_lr): + return 2 + else: + return 3 + + return 0 + + @staticmethod + def _dataset_ratios(dataset: Tuple[int, int, int, int]) -> Tuple[int, int]: + ratio_ud = ((dataset[0] - dataset[1]) * 100) // (dataset[0] + dataset[1]) + ratio_lr = ((dataset[2] - dataset[3]) * 100) // (dataset[2] + dataset[3]) + return ratio_ud, ratio_lr + + @staticmethod + def _filter_dataset(dataset: Tuple[int, int, int, int], low_thresh: int) -> bool: + if all(val == 255 for val in dataset): + return False + elif all(val == 0 for val in dataset): + return False + elif not all(val >= low_thresh for val in dataset): + return False + else: + return True # method for reading and writing to I2C def _writecmdonly(self, command: int) -> None: From 69b3c5d160c80467e7e6575865847eb248a9755a Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sat, 20 Nov 2021 12:47:05 -0600 Subject: [PATCH 4/8] Docstring and variable naming cleanup --- adafruit_apds9960/apds9960.py | 105 ++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/adafruit_apds9960/apds9960.py b/adafruit_apds9960/apds9960.py index 02c6b33..7dca2cf 100644 --- a/adafruit_apds9960/apds9960.py +++ b/adafruit_apds9960/apds9960.py @@ -113,9 +113,12 @@ class APDS9960: :param ~busio.I2C i2c: The I2C bus the ASDS9960 is connected to :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` + :param int rotation: Rotation of the device. Defaults to :const:`0` + :param bool reset: If true, resets device registers during init. Defaults to :const:`True` + :param bool set_defaults: If true, set sensible defaults during init. Defaults to :const:`True` + :param int gesture_max_dataframes: Maxmium size gesture dataframe. Defaults to :const:`64` + :param int gesture_persist_cycles: Cycles to wait for new gesture data. Defaults to :const:`5` + :param int gesture_high_pass_threshold: Minimum value for gesture data. Defaults to :const:`30` **Quickstart: Importing and using the APDS9960** @@ -135,10 +138,11 @@ class APDS9960: i2c = board.I2C() # uses board.SCL and board.SDA apds = APDS9960(i2c) - Now you have access to the :attr:`sensor.proximity` attribute + Now you have access to the :attr:`proximity_enable` and :attr:`sensor.proximity` attributes .. code-block:: python + apds.proximity_enable = True proximity = apds.proximity """ @@ -151,13 +155,13 @@ def __init__( rotation: int = 0, reset: bool = True, set_defaults: bool = True, - max_gesture_dataframe: int = 256, - data_stream_persist_cycles: int = 5, - data_stream_low_threshold: int = 30 + gesture_max_dataframes: int = 64, + gesture_persist_cycles: int = 5, + gesture_high_pass_threshold: int = 30 ): - self.buf129 = None - self.buf2 = bytearray(2) + self.gesture_buffer = None + self.msg_buffer = bytearray(2) self.i2c_device = I2CDevice(i2c, address) @@ -172,18 +176,20 @@ def __init__( if set_defaults: self.defaults() - self._max_dataframe_size = max_gesture_dataframe - self._data_stream_low_threshold = data_stream_low_threshold - self._data_stream_persist_cycles = data_stream_persist_cycles - self._data_stream_persist_sleep = self._data_stream_persist_cycles * _CYCLE_TIME + self._gesture_max_dataframes = gesture_max_dataframes + self._data_gesture_persist_cycles = gesture_persist_cycles + self._data_gesture_high_pass_threshold = gesture_high_pass_threshold + self._data_stream_persist_sleep = ( + self._data_gesture_high_pass_threshold * _CYCLE_TIME + ) def defaults(self): """Apply sensible defaults to fit most use cases""" - self.proximity_interrupt_threshold = (20, 150, 5) + self.proximity_interrupt_threshold = (0, 150, 5) # 0 near, 150 far, 5 cycles self.proximity_led_config = (7, 1, 0, 0) # 8 pulses, 8us, 100mA x1 self.proximity_gain = 1 - self.gesture_engine_config = (5, 100, 2, 2) + self.gesture_engine_config = (5, 100, 2, 2) # ent: 5, ex: 100, per: 2, wait: 2 self.gesture_led_config = (5, 2, 0, 2) # 4 pulses, 32us, 100mA x2 self.gesture_fifo_threshold = 1 self.gesture_gain = 1 @@ -192,7 +198,7 @@ def reset(self): """Reset device registers to power-on defaults""" self.enable_proximity = False self.enable_proximity_interrupt = False - self.proximity_interrupt_threshold = (0, 0, 0) + self.proximity_interrupt_threshold = (0, 0, 0) # 0 near, 0 far, 0 cycles self.proximity_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA x1 self.proximity_gain = 0 @@ -200,7 +206,7 @@ def reset(self): self.enable_gesture_interrupt = False self.gmode = False self.clear_gesture_fifo() - self.gesture_engine_config = (0, 0, 0, 0) + self.gesture_engine_config = (0, 0, 0, 0) # ent: 0, ex: 0, per: 0, wait: 0 self.gesture_led_config = (0, 1, 0, 0) # 1 pulse, 8us, 100mA x1 self.gesture_gain = 0 self.gesture_fifo_threshold = 0 @@ -650,16 +656,23 @@ def rotated_gesture(self, original_gesture: int) -> int: new_index = (directions.index(original_gesture) + self._rotation // 90) % 4 return directions[new_index] - def gesture_string(self) -> str: + def gesture_string(self, blocking=False) -> str: """Gather and process gesture data, returns string representing gesture direction""" - gesture_number = self.gesture() + gesture_number = self.gesture(blocking) return _GESTURE_NAMES[gesture_number] - def gesture(self) -> int: + def gesture(self, blocking=False) -> int: """Gather and process gesture data, returns int representing gesture direction - 0 = None, 1 = Up, 2 = Down, 3 = Left, 4 = Right""" + Blocking: If true, wait for 'gesture_interrupt' before reading in gesture data. + + Return: 0 = None, 1 = Up, 2 = Down, 3 = Left, 4 = Right""" + if blocking: + # If we want to block until gesture data comes in we'll wait on our FIFO threshold + while not self.gesture_interrupt: + pass + dataframe = self._get_gesture_data() if len(dataframe) > 0: @@ -673,55 +686,60 @@ def _get_gesture_data(self) -> List[Tuple[int, int, int, int]]: """Retrieves sequential gesture datasets from FIFO, if any are available""" dataframe = [] - # If we've already overflowed, clear out the stale FIFOs right away and wait for fresh data + # If FIFOs have overflowed we're already way too late, so clear those FIFOs and wait if self._gfov: self.clear_gesture_fifo() - while not self.gesture_interrupt: - time.sleep(0.001) + # Don't wait forever though, just enough to see if a gesture is happening + wait_cycles = 0 + while not self.gesture_interrupt and wait_cycles <= 30: + time.sleep(_CYCLE_TIME) + wait_cycles += 1 - # Only start retrieval if gvalid and if there are datasets to retrieve + # Only start retrieval if there are datasets to retrieve dataset_count = self._gflvl - if self.gesture_valid and dataset_count > 0: - if self.buf129 is None: - self.buf129 = bytearray(129) + if dataset_count > 0: + if self.gesture_buffer is None: + self.gesture_buffer = bytearray(129) + + buffer = self.gesture_buffer # Stack new gesture datasets into our dataframe # Also, keep stacking new datasets if they show up while we're reading in FIFO data while True: # Acquire all available data dataset_count = self._gflvl - self.buf129[0] = _APDS9960_GFIFO_U + buffer[0] = _APDS9960_GFIFO_U with self.i2c_device as i2c: i2c.write_then_readinto( - self.buf129, - self.buf129, + buffer, + buffer, out_end=1, in_start=1, in_end=min(129, 1 + (dataset_count * 4)), ) idx = 0 - # Unpack data stream into more usable U/D/L/R datasets + # Unpack data stream into more usable U/D/L/R dataset tuples for i in range(dataset_count): rec = i + 1 idx = 1 + ((rec - 1) * 4) dataset_tuple = ( - self.buf129[idx], - self.buf129[idx + 1], - self.buf129[idx + 2], - self.buf129[idx + 3], + buffer[idx], + buffer[idx + 1], + buffer[idx + 2], + buffer[idx + 3], ) # Drop fully-saturated and fully-zero to conserve memory # Low-pass filter to remove potentially spurious very-low-count entries if self._filter_dataset( - dataset_tuple, self._data_stream_low_threshold + dataset_tuple, self._data_gesture_high_pass_threshold ): dataframe.append(dataset_tuple) - # Break out of our loop if we have way too many datasets - if len(dataframe) > self._max_dataframe_size: + # Break out of our loop ASAP if we have way too many datasets + if len(dataframe) > self._gesture_max_dataframes: # print("Get: Halting, gflvl: {}, len: {}".format(self._gflvl, len(dataframe))) break @@ -736,12 +754,15 @@ def _get_gesture_data(self) -> List[Tuple[int, int, int, int]]: # pylint: disable-msg=no-else-return, too-many-return-statements, too-many-branches def _process_gesture_data(self, dataframe: List[Tuple[int, int, int, int]]) -> int: + """Processes gesture dataframes to determine gesture + + This assumes that the dataframe has already been high-pass filtered""" _gesture_delta_threshold = 30 first_dataset = dataframe[0] last_dataset = dataframe[len(dataframe) - 1] - # print("f: {}, l: {}".format(first_dataset, last_dataset)) + # print("Process: f: {}, l: {}".format(first_dataset, last_dataset)) ratios_first = self._dataset_ratios(first_dataset) ratios_last = self._dataset_ratios(last_dataset) @@ -816,7 +837,7 @@ def _filter_dataset(dataset: Tuple[int, int, int, int], low_thresh: int) -> bool # method for reading and writing to I2C def _writecmdonly(self, command: int) -> None: """Writes a command and 0 bytes of data to the I2C device""" - buf = self.buf2 + buf = self.msg_buffer buf[0] = command with self.i2c_device as i2c: i2c.write(buf, end=1) @@ -824,7 +845,7 @@ def _writecmdonly(self, command: int) -> None: 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 = self.msg_buffer buf[0] = command with self.i2c_device as i2c: i2c.write_then_readinto(buf, buf, out_end=1) From 6e0322f8c09ba6ab5f3e6702ccdf69732a33c37d Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sat, 20 Nov 2021 12:51:47 -0600 Subject: [PATCH 5/8] Updating proximtiy_simpletest example with basic configuration information --- examples/apds9960_proximity_simpletest.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/apds9960_proximity_simpletest.py b/examples/apds9960_proximity_simpletest.py index cc70f59..3f75200 100644 --- a/examples/apds9960_proximity_simpletest.py +++ b/examples/apds9960_proximity_simpletest.py @@ -2,19 +2,32 @@ # SPDX-License-Identifier: MIT import board -import digitalio +from digitalio import DigitalInOut, Pull from adafruit_apds9960.apds9960 import APDS9960 i2c = board.I2C() -int_pin = digitalio.DigitalInOut(board.D5) + +# Replace "D5" with the GPIO connected to the sensor's interrupt pin +int_pin = DigitalInOut(board.D5) +int_pin.switch_to_input(pull=Pull.UP) apds = APDS9960(i2c) -apds.enable_proximity = True -apds.proximity_interrupt_threshold = (0, 175) +# LED settings can be tuned to quickly increase/decrease range and sensitivity +# This configures the IR LED for 8 pulses, 8us each, 100mA @ x1 boost +apds.proximity_led_config = (7, 1, 0, 0) + +# Interrupt thresholds can be tuned to decide when the interrupt pin gets asserted +# This configures the interrupt with 0 far threshold, 175 near threshold, 5 cycle persistence +apds.proximity_interrupt_threshold = (0, 175, 5) + +# This enables assertion of the interrupt pin (pulling it to ground) when the thresholds are met apds.enable_proximity_interrupt = True +# This starts running continously running proximity engine loops on the sensor +apds.enable_proximity = True + while True: - # print the proximity reading when the interrupt pin goes low + # Print the proximity reading when the interrupt pin goes low if not int_pin.value: print(apds.proximity) From ccd8452029ed3d01c4d2a1e708ab3646ea928574 Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sat, 20 Nov 2021 12:55:48 -0600 Subject: [PATCH 6/8] Adding new proximity_keypad example --- examples/apds9960_proximity_keypad.py | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 examples/apds9960_proximity_keypad.py diff --git a/examples/apds9960_proximity_keypad.py b/examples/apds9960_proximity_keypad.py new file mode 100644 index 0000000..c8867c1 --- /dev/null +++ b/examples/apds9960_proximity_keypad.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2021 fivesixzero +# SPDX-License-Identifier: MIT +# +# Proximity 'keypad key' Demo +# +# The APDS-9960's proximity function can be used like any other button or keyswitch input! +# +# In this example we'll be using the 'keypad' library, which is useful for cases where your inputs +# need to be responsive even when your main loop has unpredictable timing. +# +# Adding additional buttons or keyswitches is as easy as adding them to the 'keys' tuple and making +# sure they're accounted for in the event handling function. +# +# Once the proximity engine is enabled it'll keep running on its own. The interrupt pin will be +# asserted (pulled to ground, or 'False'), only when the proximity stays within the 'window' +# established by the high/low thresholds for enough of its internal measurement cycles, defined +# by the 'persistence' variable. + +import time +import board +import keypad +from adafruit_apds9960.apds9960 import APDS9960 + +i2c = board.I2C() +apds = APDS9960(i2c) + +# Interrupt will trigger only if proximity value is >50 for at least 2 consecutive cycles +apds.proximity_interrupt_threshold = (0, 50, 1) +apds.enable_proximity = True +apds.enable_proximity_interrupt = True + +# Clue: https://www.adafruit.com/product/4500 +# +# The two buttons available on the Clue are easy to include for scanning with the keypad +# +keys = (board.PROXIMITY_LIGHT_INTERRUPT, board.BUTTON_A, board.BUTTON_B) + +# Bluefruit Sense: https://www.adafruit.com/product/4516 +# keys = ( +# board.PROXIMITY_LIGHT_INTERRUPT, +# ) + +# Breakout board sensor: https://www.adafruit.com/product/3595 +# +# The breakout's "INT" pin will need to be attached to a GPIO pin on your device +# +# keys = ( +# board.D5, +# ) + +# Proximity Trinkey: https://www.adafruit.com/product/5022 +# +# The Proximity Trinky firmware doesn't include the 'keypad' module by default so this demo will +# require a custom firmware build with the 'keypad' module manually included. +# +# Note: Getting the ~3.6k module to fit in the Trinkey's flash storage will likely require +# removing other modules, such as usb_midi (~1.9k) and/or usb_hid (~2k). +# +# Details on building customized firmware can be found in the Building CircuitPython guide: +# https://learn.adafruit.com/building-circuitpython +# +# keys = ( +# board.INTERRUPT, +# ) + +prox_keypad = keypad.Keys(keys, value_when_pressed=False, pull=True) + +# Keypad constants/buffer to save memory +PROX_PRESS = keypad.Event(0, True) +PROX_RELEASE = keypad.Event(0, False) +KEY_BUFFER = keypad.Event() + +# Simple function to see if we had a press or release come in since our last check-in +def handle_key_events(keys_to_check: keypad.Keys, currently_held: bool): + new_press = False + new_release = False + + # Get new events and quickly compare them against our buffers + while keys_to_check.events.get_into(KEY_BUFFER): + if KEY_BUFFER == PROX_PRESS: + new_press = True + currently_held = True + if KEY_BUFFER == PROX_RELEASE: + new_release = True + currently_held = False + + # We should clear the proximity interrupt every time we check on it + apds.clear_proximity_interrupt() + + return new_press, new_release, currently_held + + +held = False +while True: + pressed, released, held = handle_key_events(prox_keypad, held) + + if pressed: + print("A thing got pretty close!") + elif released: + print("That thing moved away!") + elif held: + print("That thing is still there!") + else: + print("Nothing nearby...") + + time.sleep(1) From 30322a0da03cd6754ac65355088571d10137ba20 Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sat, 20 Nov 2021 12:57:09 -0600 Subject: [PATCH 7/8] Updating .gitignore to include .vscode local configuration directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9647e71..5fb597b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ bundles .eggs dist **/*.egg-info +.vscode From 064fd99f6006ce146eefa74eff5944af1d716b2e Mon Sep 17 00:00:00 2001 From: Erik Hess Date: Sat, 20 Nov 2021 14:04:19 -0600 Subject: [PATCH 8/8] Docstring fix in gesture_engine_config --- adafruit_apds9960/apds9960.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_apds9960/apds9960.py b/adafruit_apds9960/apds9960.py index 7dca2cf..8b9c8ba 100644 --- a/adafruit_apds9960/apds9960.py +++ b/adafruit_apds9960/apds9960.py @@ -449,7 +449,7 @@ def gesture_engine_config(self) -> Tuple[int, int, int, int, int]: Proximity entry threshold (0-255): Minimum proximity value for gesture engine entrance Exit threshold (0-255): Minimum proximity value for gesture engine loop persistence Persistence (0-3): Number of out-of-thresold loops to continue recording gesture data - 0 = 1 cycle, 1 = 2 cycles, 2 = 4 cycles, 3 = 7 cycles + 0 = 1 cycle, 1 = 2 cycles, 2 = 4 cycles, 3 = 7 cycles Wait time (0-7): Number of 2.78ms cycles to wait between gesture 1.39 ms engine loops""" return (self._gpenth, self._gexth, self._gpers, self._gwtime)