ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
apds9960.cpp
Go to the documentation of this file.
1#include "apds9960.h"
2#include "esphome/core/log.h"
3#include "esphome/core/hal.h"
4
6
7ESPHOME_LOG_TAG(TAG, "apds9960");
8
9#define APDS9960_ERROR_CHECK(func) \
10 if (!(func)) { \
11 this->mark_failed(LOG_STR("Setting up APDS9960 registers failed")); \
12 return; \
13 }
14#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
15
16void APDS9960::setup() {
17 uint8_t id;
18 if (!this->read_byte(0x92, &id)) { // ID register
19 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
20 return;
21 }
22
23 if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
24 this->mark_failed(LOG_STR("APDS9960 has invalid id"));
25 return;
26 }
27
28 // ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
29 APDS9960_WRITE_BYTE(0x81, 0xDB);
30 // WTime (Wait time, 0x83) -> 0xF6 (27ms)
31 APDS9960_WRITE_BYTE(0x83, 0xF6);
32 // PPulse (0x8E) -> 0x87 (16us, 8 pulses)
33 APDS9960_WRITE_BYTE(0x8E, 0x87);
34 // POffset UR (0x9D) -> 0 (no offset)
35 APDS9960_WRITE_BYTE(0x9D, 0x00);
36 // POffset DL (0x9E) -> 0 (no offset)
37 APDS9960_WRITE_BYTE(0x9E, 0x00);
38 // Config 1 (0x8D) -> 0x60 (no wtime factor)
39 APDS9960_WRITE_BYTE(0x8D, 0x60);
40
41 // Control (0x8F) ->
42 uint8_t val = 0;
43 APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val));
44 val &= 0b00111111;
45 // led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
46 val |= (this->led_drive_ & 0b11) << 6;
47
48 val &= 0b11110011;
49 // proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X
50 val |= (this->proximity_gain_ & 0b11) << 2;
51
52 val &= 0b11111100;
53 // ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x
54 val |= (this->ambient_gain_ & 0b11) << 0;
55 APDS9960_WRITE_BYTE(0x8F, val);
56
57 // Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
58 APDS9960_WRITE_BYTE(0x8C, 0x11);
59 // Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
60 APDS9960_WRITE_BYTE(0x90, 0x01);
61 // Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
62 APDS9960_WRITE_BYTE(0x9F, 0x00);
63 // GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
64 APDS9960_WRITE_BYTE(0xA0, 0x28);
65 // GPexTh (0xA1, gesture exit threshold) -> 0x1E
66 APDS9960_WRITE_BYTE(0xA1, 0x1E);
67
68 // GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
69 APDS9960_WRITE_BYTE(0xA2, 0x40);
70
71 // GConf 2 (0xA3, gesture config 2) ->
72 APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val));
73 val &= 0b10011111;
74 // gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x
75 val |= (this->gesture_gain_ & 0b11) << 5;
76
77 val &= 0b11100111;
78 // gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
79 val |= (this->gesture_led_drive_ & 0b11) << 3;
80
81 val &= 0b11111000;
82 // gesture wait time
83 // 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms
84 // 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms
85 val |= (this->gesture_wait_time_ & 0b111) << 0;
86 APDS9960_WRITE_BYTE(0xA3, val);
87
88 // GOffsetU (0xA4) -> 0x00 (no offset)
89 APDS9960_WRITE_BYTE(0xA4, 0x00);
90 // GOffsetD (0xA5) -> 0x00 (no offset)
91 APDS9960_WRITE_BYTE(0xA5, 0x00);
92 // GOffsetL (0xA7) -> 0x00 (no offset)
93 APDS9960_WRITE_BYTE(0xA7, 0x00);
94 // GOffsetR (0xA9) -> 0x00 (no offset)
95 APDS9960_WRITE_BYTE(0xA9, 0x00);
96 // GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
97 APDS9960_WRITE_BYTE(0xA6, 0xC9);
98
99 // GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
100 // 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
101 APDS9960_WRITE_BYTE(0xAA, 0x00);
102
103 // Enable (0x80) ->
104 val = 0;
105 val |= (0b1) << 0; // power on
106 val |= (this->is_color_enabled_() & 0b1) << 1;
107 val |= (this->is_proximity_enabled_() & 0b1) << 2;
108 val |= 0b0 << 3; // wait timer disabled
109 val |= 0b0 << 4; // color interrupt disabled
110 val |= 0b0 << 5; // proximity interrupt disabled
111 val |= (this->is_gesture_enabled_() & 0b1) << 6; // proximity is required for gestures
112 APDS9960_WRITE_BYTE(0x80, val);
113}
115#ifdef USE_SENSOR
116 return this->red_sensor_ != nullptr || this->green_sensor_ != nullptr || this->blue_sensor_ != nullptr ||
117 this->clear_sensor_ != nullptr;
118#else
119 return false;
120#endif
121}
122
123void APDS9960::dump_config() {
124 ESP_LOGCONFIG(TAG, "APDS9960:");
125 LOG_I2C_DEVICE(this);
126
127 LOG_UPDATE_INTERVAL(this);
128
129#ifdef USE_SENSOR
130 LOG_SENSOR(" ", "Red channel", this->red_sensor_);
131 LOG_SENSOR(" ", "Green channel", this->green_sensor_);
132 LOG_SENSOR(" ", "Blue channel", this->blue_sensor_);
133 LOG_SENSOR(" ", "Clear channel", this->clear_sensor_);
134 LOG_SENSOR(" ", "Proximity", this->proximity_sensor_);
135#endif
136}
137
138#define APDS9960_WARNING_CHECK(func, warning) \
139 if (!(func)) { \
140 ESP_LOGW(TAG, warning); \
141 this->status_set_warning(); \
142 return; \
143 }
144
145void APDS9960::update() {
146 uint8_t status;
147 APDS9960_WARNING_CHECK(this->read_byte(0x93, &status), "Reading status bit failed.");
148 this->status_clear_warning();
149
150 this->read_color_data_(status);
151 this->read_proximity_data_(status);
152}
153
154void APDS9960::loop() { this->read_gesture_data_(); }
155
156void APDS9960::read_color_data_(uint8_t status) {
157 if (!this->is_color_enabled_())
158 return;
159
160 if ((status & 0x01) == 0x00) {
161 // color data not ready yet.
162 return;
163 }
164
165 uint16_t raw[4];
166 APDS9960_WARNING_CHECK(this->read_bytes_16_le(0x94, raw, 4), "Reading color values failed.");
167
168 uint16_t uint_clear = raw[0];
169 uint16_t uint_red = raw[1];
170 uint16_t uint_green = raw[2];
171 uint16_t uint_blue = raw[3];
172
173 float clear_perc = (uint_clear / float(UINT16_MAX)) * 100.0f;
174 float red_perc = (uint_red / float(UINT16_MAX)) * 100.0f;
175 float green_perc = (uint_green / float(UINT16_MAX)) * 100.0f;
176 float blue_perc = (uint_blue / float(UINT16_MAX)) * 100.0f;
177
178 ESP_LOGD(TAG, "Got clear=%.1f%% red=%.1f%% green=%.1f%% blue=%.1f%%", clear_perc, red_perc, green_perc, blue_perc);
179#ifdef USE_SENSOR
180 if (this->clear_sensor_ != nullptr)
181 this->clear_sensor_->publish_state(clear_perc);
182 if (this->red_sensor_ != nullptr)
183 this->red_sensor_->publish_state(red_perc);
184 if (this->green_sensor_ != nullptr)
185 this->green_sensor_->publish_state(green_perc);
186 if (this->blue_sensor_ != nullptr)
187 this->blue_sensor_->publish_state(blue_perc);
188#endif
189}
190void APDS9960::read_proximity_data_(uint8_t status) {
191#ifndef USE_SENSOR
192 return;
193#else
194 if (this->proximity_sensor_ == nullptr)
195 return;
196
197 if ((status & 0b10) == 0x00) {
198 // proximity data not ready yet.
199 return;
200 }
201
202 uint8_t prox;
203 APDS9960_WARNING_CHECK(this->read_byte(0x9C, &prox), "Reading proximity values failed.");
204
205 float prox_perc = (prox / float(UINT8_MAX)) * 100.0f;
206 ESP_LOGD(TAG, "Got proximity=%.1f%%", prox_perc);
207 this->proximity_sensor_->publish_state(prox_perc);
208#endif
209}
211 if (!this->is_gesture_enabled_())
212 return;
213
214 uint8_t status;
215 APDS9960_WARNING_CHECK(this->read_byte(0xAF, &status), "Reading gesture status failed.");
216
217 if ((status & 0b01) == 0) {
218 // GVALID is false
219 return;
220 }
221
222 if ((status & 0b10) == 0b10) {
223 ESP_LOGV(TAG, "FIFO buffer has filled to capacity!");
224 }
225
226 uint8_t fifo_level;
227 APDS9960_WARNING_CHECK(this->read_byte(0xAE, &fifo_level), "Reading FIFO level failed.");
228 if (fifo_level == 0) {
229 // no data to process
230 return;
231 }
232
233 APDS9960_WARNING_CHECK(fifo_level <= 32, "FIFO level has invalid value.")
234
235 uint8_t buf[128];
236 for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
237 // Read in 32-byte chunks due to ESP8266 I2C buffer limit.
238 // Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
239 // and advances its internal pointer after every 4th byte.
240 uint8_t read = std::min(32, fifo_level * 4 - pos);
241 APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
242 }
243
244 if (millis() - this->gesture_start_ > 500) {
245 this->gesture_up_started_ = false;
246 this->gesture_down_started_ = false;
247 this->gesture_left_started_ = false;
248 this->gesture_right_started_ = false;
249 }
250
251 for (uint32_t i = 0; i < fifo_level * 4; i += 4) {
252 const int up = buf[i + 0]; // NOLINT
253 const int down = buf[i + 1];
254 const int left = buf[i + 2];
255 const int right = buf[i + 3];
256 this->process_dataset_(up, down, left, right);
257 }
258}
259void APDS9960::report_gesture_(int gesture) {
260#ifdef USE_BINARY_SENSOR
262 switch (gesture) {
263 case 1:
264 bin = this->up_direction_binary_sensor_;
265 this->gesture_up_started_ = false;
266 this->gesture_down_started_ = false;
267 ESP_LOGD(TAG, "Got gesture UP");
268 break;
269 case 2:
270 bin = this->down_direction_binary_sensor_;
271 this->gesture_up_started_ = false;
272 this->gesture_down_started_ = false;
273 ESP_LOGD(TAG, "Got gesture DOWN");
274 break;
275 case 3:
276 bin = this->left_direction_binary_sensor_;
277 this->gesture_left_started_ = false;
278 this->gesture_right_started_ = false;
279 ESP_LOGD(TAG, "Got gesture LEFT");
280 break;
281 case 4:
282 bin = this->right_direction_binary_sensor_;
283 this->gesture_left_started_ = false;
284 this->gesture_right_started_ = false;
285 ESP_LOGD(TAG, "Got gesture RIGHT");
286 break;
287 default:
288 return;
289 }
290
291 if (bin != nullptr) {
292 bin->publish_state(true);
293 bin->publish_state(false);
294 }
295#endif
296}
297void APDS9960::process_dataset_(int up, int down, int left, int right) {
298 /* Algorithm: (see Figure 11 in datasheet)
299 *
300 * Observation: When a gesture is started, we will see a short amount of time where
301 * the photodiode in the direction of the motion has a much higher count value
302 * than where the gesture originates.
303 *
304 * In this algorithm we continually check the difference between the count values of opposing
305 * directions. For example in the down/up direction we continually look at the difference of the
306 * up count and down count. When DOWN gesture begins, this difference will be positive with a
307 * high magnitude for a short amount of time (magic value here is the difference is at least 13).
308 *
309 * If we see such a pattern, we store that we saw the first part of a gesture (the leading edge).
310 * After that some time can pass during which the difference is zero again (though the count values
311 * are not zero). At the end of a gesture, we will see this difference go into the opposite direction
312 * for a short period of time.
313 *
314 * If a gesture is not ended within 500 milliseconds, we consider the initial trailing edge invalid
315 * and reset the state.
316 *
317 * This algorithm does work, but not too well. Some good signal processing algorithms could
318 * probably improve this a lot, especially since the incoming signal has such a characteristic
319 * and quite noise-free pattern.
320 */
321 const int up_down_delta = up - down;
322 const int left_right_delta = left - right;
323 const bool up_down_significant = abs(up_down_delta) > 13;
324 const bool left_right_significant = abs(left_right_delta) > 13;
325
326 if (up_down_significant) {
327 if (up_down_delta < 0) {
328 if (this->gesture_up_started_) {
329 // trailing edge of gesture up
330 this->report_gesture_(1); // UP
331 } else {
332 // leading edge of gesture down
333 this->gesture_down_started_ = true;
334 this->gesture_start_ = millis();
335 }
336 } else {
337 if (this->gesture_down_started_) {
338 // trailing edge of gesture down
339 this->report_gesture_(2); // DOWN
340 } else {
341 // leading edge of gesture up
342 this->gesture_up_started_ = true;
343 this->gesture_start_ = millis();
344 }
345 }
346 }
347
348 if (left_right_significant) {
349 if (left_right_delta < 0) {
350 if (this->gesture_left_started_) {
351 // trailing edge of gesture left
352 this->report_gesture_(3); // LEFT
353 } else {
354 // leading edge of gesture right
355 this->gesture_right_started_ = true;
356 this->gesture_start_ = millis();
357 }
358 } else {
359 if (this->gesture_right_started_) {
360 // trailing edge of gesture right
361 this->report_gesture_(4); // RIGHT
362 } else {
363 // leading edge of gesture left
364 this->gesture_left_started_ = true;
365 this->gesture_start_ = millis();
366 }
367 }
368 }
369}
371 return
372#ifdef USE_SENSOR
373 this->proximity_sensor_ != nullptr
374#else
375 false
376#endif
377 || this->is_gesture_enabled_();
378}
380#ifdef USE_BINARY_SENSOR
381 return this->up_direction_binary_sensor_ != nullptr || this->left_direction_binary_sensor_ != nullptr ||
382 this->down_direction_binary_sensor_ != nullptr || this->right_direction_binary_sensor_ != nullptr;
383#else
384 return false;
385#endif
386}
387
388} // namespace esphome::apds9960
uint8_t raw[35]
Definition bl0939.h:0
uint8_t status
Definition bl0942.h:8
void mark_failed()
Mark this component as failed.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
void status_clear_warning()
Definition component.h:296
void read_proximity_data_(uint8_t status)
Definition apds9960.cpp:190
bool is_gesture_enabled_() const
Definition apds9960.cpp:379
void report_gesture_(int gesture)
Definition apds9960.cpp:259
void read_color_data_(uint8_t status)
Definition apds9960.cpp:156
void process_dataset_(int up, int down, int left, int right)
Definition apds9960.cpp:297
bool is_proximity_enabled_() const
Definition apds9960.cpp:370
Base class for all binary_sensor-type classes.
void publish_state(bool new_state)
Publish a new state to the front-end.
bool read_bytes_16_le(uint8_t a_register, uint16_t *data, uint8_t len)
Read len 16-bit values stored least significant byte first; no conversion is compiled on little-endia...
Definition i2c.h:284
ErrorCode read(uint8_t *data, size_t len) const
reads an array of bytes from the device using an I2CBus
Definition i2c.h:175
bool read_byte(uint8_t a_register, uint8_t *data)
Definition i2c.h:272
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len)
Compat APIs All methods below have been added for compatibility reasons.
Definition i2c.h:249
uint16_t id
mopeka_std_values val[3]
ESPHOME_LOG_TAG(TAG, "apds9960")
size_t size_t pos
Definition helpers.h:1272
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t