ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
as734x.cpp
Go to the documentation of this file.
1#include "as734x.h"
2#include "color_helpers.h"
5#include "esphome/core/log.h"
7
8#include <algorithm>
9#include <cstdio>
10
11namespace esphome::as734x {
12
13ESPHOME_LOG_TAG(TAG, "as734x");
14
15// Registers and bits shared by both chips
16static constexpr uint8_t REG_ENABLE = 0x80;
17static constexpr uint8_t REG_ATIME = 0x81;
18static constexpr uint8_t REG_ASTATUS = 0x94;
19static constexpr uint8_t ENABLE_PON = 0x01;
20static constexpr uint8_t ENABLE_SP_EN = 0x02;
21static constexpr uint8_t ENABLE_SMUX_EN = 0x10;
22static constexpr uint8_t CFG0_REG_BANK = 0x10;
23static constexpr uint8_t BANK_SPLIT = 0x80; // registers below this need REG_BANK set
24static constexpr uint8_t STATUS2_AVALID = 0x40;
25static constexpr uint8_t ASTATUS_ASAT = 0x80;
26static constexpr uint8_t LED_ACT = 0x80; // LED driver on, in the chip's LED register
27static constexpr uint8_t CONFIG_LED_SEL = 0x08;
28
29static constexpr uint8_t AS7341_CFG6 = 0xAF;
30static constexpr uint8_t AS7341_SMUX_CMD_WRITE = 0x10; // copy the SMUX image from RAM to the chain
31static constexpr uint8_t AS7341_SMUX_RAM = 0x00;
32static constexpr uint8_t AS7343_CHAIN_CMD = 0xE4;
33static constexpr uint8_t AS7343_CHAIN_SMUX = 0xE7;
34static constexpr uint8_t AS7343_CHAINS = 3;
35
36// Read one field of a ChipInfo kept in flash
37template<typename T> static T pgm(const T &field) {
38 T value;
39 progmem_memcpy(&value, &field, sizeof(T));
40 return value;
41}
42
43static constexpr uint8_t NO_SLOT = 0xFF;
44static constexpr uint8_t CLEAR_BAND = NUM_BANDS - 1;
45static constexpr uint8_t MAX_SMUX_LEN = 20;
46static constexpr uint8_t MAX_ADC_SLOTS = 18;
47static constexpr uint32_t MIN_COLLECTION_TIMEOUT_MS = 30 * 1000;
48static constexpr uint32_t MAX_ADC_COUNT = 65535;
49
50PROGMEM_STRING_TABLE(BandNames, "F1", "F2", "FZ", "F3", "F4", "FY", "F5", "FXL", "F6", "F7", "F8", "NIR", "Clear");
51
52#ifdef USE_AS7341
53static const char AS7341_NAME[] PROGMEM = "AS7341";
54static constexpr uint8_t AS7341_ADC_SLOTS = 6;
55static constexpr uint8_t AS7341_SMUX_LEN = 20;
56static constexpr uint8_t AS7341_SMUX_STEPS = 2;
57static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
58static const uint8_t AS7341_SMUX[] PROGMEM = {
59 0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
60 0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
61 0x00, 0x00, 0x00, 0x40, 0x02, 0x00, 0x10, 0x03, 0x50, 0x10, // step 1: F5 to F8, NIR, Clear
62 0x03, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x50, 0x00, 0x06, // step 1, continued
63};
64static_assert(sizeof(AS7341_SMUX) == AS7341_SMUX_STEPS * AS7341_SMUX_LEN && AS7341_SMUX_LEN <= MAX_SMUX_LEN);
65static constexpr uint8_t N = NO_SLOT;
66static const uint8_t AS7341_READOUT[] PROGMEM = {
67 0, 1, N, 2, 3, N, N, N, N, N, N, N, N, // step 0
68 N, N, N, N, N, N, 0, N, 1, 2, 3, 5, 4, // step 1: SMUX routes clear to ADC4 and NIR to ADC5
69};
70static_assert(sizeof(AS7341_READOUT) == AS7341_SMUX_STEPS * NUM_BANDS && AS7341_ADC_SLOTS <= MAX_ADC_SLOTS);
71// This chip has no FZ, FY or FXL band, so those slots are marked absent.
72// clang-format off
73static const uint16_t AS7341_WAVELENGTHS[] PROGMEM = {
74 415, 445, ABSENT_NM, 480, 515, ABSENT_NM, 555, ABSENT_NM, 590, 630, 680, 910, WIDEBAND_NM,
75};
76// clang-format on
77static_assert(sizeof(AS7341_WAVELENGTHS) == NUM_BANDS * sizeof(uint16_t));
78// Datasheet values for the golden device: gain correction per gain step, the same for every band.
79// The last two entries stay at zero because this chip has no 1024x or 2048x gain.
80static const float AS7341_GAIN_CORRECTION[] PROGMEM = {1.0577f, 1.0491f, 1.0479f, 1.0491f, 1.0207f, 1.0158f, 1.0109f,
81 1.0000f, 1.0003f, 0.9873f, 0.9593f, 0.0f, 0.0f};
82static_assert(sizeof(AS7341_GAIN_CORRECTION) == GAIN_COUNT * sizeof(float));
83// clang-format off
84// The three bands this chip does not have contribute nothing to the weighted sums.
85static const ChannelContribution AS7341_CONTRIBUTIONS[] PROGMEM = {
86 {0.005261294f, 5.936230324f, 0.020518273f}, // F1
87 {0.242615252f, 3.572528495f, 0.013525490f}, // F2
88 {0.0f, 0.0f, 0.0f}, // FZ, absent
89 {0.174482552f, 2.914113437f, 0.011585145f}, // F3
90 {1.479499075f, 2.452851227f, 0.010604986f}, // F4
91 {0.0f, 0.0f, 0.0f}, // FY, absent
92 {1.906818555f, 1.940656433f, 0.008890531f}, // F5
93 {0.0f, 0.0f, 0.0f}, // FXL, absent
94 {1.162058142f, 1.533976606f, 0.007441544f}, // F6
95 {0.462374845f, 1.819838250f, 0.009393590f}, // F7
96 {-0.017794572f, 1.232942274f, 0.006890812f}, // F8
97 {-0.260730901f, -1.102368000f, -0.004647911f}, // NIR
98 {-0.022604075f, -0.033304813f, -0.000129106f}, // Clear
99};
100static const ChannelTristimulus AS7341_TRISTIMULUS[] PROGMEM = {
101 {0.39814f, 0.01396f, 1.95010f}, // F1
102 {1.29540f, 0.16748f, 6.45490f}, // F2
103 {0.0f, 0.0f, 0.0f}, // FZ, absent
104 {0.36956f, 0.23538f, 2.78010f}, // F3
105 {0.10902f, 1.42750f, 0.18501f}, // F4
106 {0.0f, 0.0f, 0.0f}, // FY, absent
107 {0.71942f, 1.88670f, 0.15325f}, // F5
108 {0.0f, 0.0f, 0.0f}, // FXL, absent
109 {1.78180f, 1.14200f, 0.09539f}, // F6
110 {1.10110f, 0.46497f, 0.10563f}, // F7
111 {-0.03991f, -0.02702f, 0.08866f}, // F8
112 {-0.27597f, -0.24468f, -0.61140f}, // NIR
113 {-0.02347f, -0.01993f, -0.00938f}, // Clear
114};
115// clang-format on
116static_assert(sizeof(AS7341_CONTRIBUTIONS) == NUM_BANDS * sizeof(ChannelContribution));
117static_assert(sizeof(AS7341_TRISTIMULUS) == NUM_BANDS * sizeof(ChannelTristimulus));
118const ChipInfo AS7341_CHIP PROGMEM = {
119 .name = AS7341_NAME,
120 .init = AS7341_INIT,
121 .smux = AS7341_SMUX,
122 .readout = AS7341_READOUT,
123 .wavelengths = AS7341_WAVELENGTHS,
124 .gain_correction = AS7341_GAIN_CORRECTION,
125 .contributions = AS7341_CONTRIBUTIONS,
126 .tristimulus = AS7341_TRISTIMULUS,
127 .init_count = 1,
128 .astep_reg = 0xCA,
129 .cfg0_reg = 0xA9,
130 .cfg1_reg = 0xAA,
131 .status2_reg = 0xA3,
132 .id_reg = 0x92,
133 .id_mask = 0xFC,
134 .id_value = 0x09 << 2,
135 .led_reg = 0x74,
136 .led_sel_reg = 0x70, // CONFIG, whose LED_SEL bit connects the pin to the driver
137 .adc_slots = AS7341_ADC_SLOTS,
138 .smux_steps = AS7341_SMUX_STEPS,
139 .integration_cycles = 1,
140 .smux_len = AS7341_SMUX_LEN,
141 .auto_smux = false,
142 .gain_correction_per_band = false,
143 .extra_clear_slot = NO_SLOT,
144};
145#endif
146
147#ifdef USE_AS7343
148static const char AS7343_NAME[] PROGMEM = "AS7343";
149static constexpr uint8_t AS7343_ADC_SLOTS = 18;
150static constexpr uint8_t AS7343_SMUX_LEN = 10;
151static const char TCS3448_NAME[] PROGMEM = "TCS3448";
152// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
153static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
154 {0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
155static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
156 0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
157 0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
158// Clear is the average of the cycle 1 (slot 4) and cycle 2 (slot 10) readings; cycle 3 always reads 0.
159static const uint8_t AS7343_READOUT[] PROGMEM = {12, 6, 0, 7, 8, 1, 15, 2, 9, 13, 14, 3, 10};
160static_assert(sizeof(AS7343_SMUX) == AS7343_CHAINS * AS7343_SMUX_LEN && AS7343_SMUX_LEN <= MAX_SMUX_LEN);
161static_assert(sizeof(AS7343_READOUT) == NUM_BANDS && AS7343_ADC_SLOTS <= MAX_ADC_SLOTS);
162// clang-format off
163static const uint16_t AS7343_WAVELENGTHS[] PROGMEM = {
164 405, 425, 450, 475, 515, 555, 550, 600, 640, 690, 745, 855, WIDEBAND_NM,
165};
166// clang-format on
167static_assert(sizeof(AS7343_WAVELENGTHS) == NUM_BANDS * sizeof(uint16_t));
168// Datasheet values for the golden device: gain correction per gain step, per band.
169// clang-format off
170static const float AS7343_GAIN_CORRECTION[] PROGMEM = {
171 1.149f, 1.100f, 1.060f, 1.070f, 1.063f, 1.051f, 1.062f, 1.056f, 1.049f, 1.040f, 1.080f, 1.038f, 1.065f, // 0.5x
172 1.090f, 1.128f, 1.064f, 1.071f, 1.063f, 1.050f, 1.068f, 1.055f, 1.047f, 1.039f, 1.075f, 1.038f, 1.085f, // 1x
173 1.083f, 1.086f, 1.062f, 1.070f, 1.062f, 1.049f, 1.057f, 1.053f, 1.045f, 1.038f, 1.063f, 1.037f, 1.069f, // 2x
174 1.059f, 1.068f, 1.056f, 1.066f, 1.058f, 1.046f, 1.051f, 1.051f, 1.044f, 1.036f, 1.059f, 1.035f, 1.053f, // 4x
175 1.100f, 1.109f, 1.096f, 1.108f, 1.099f, 1.089f, 1.091f, 1.092f, 1.082f, 1.078f, 1.100f, 1.076f, 1.088f, // 8x
176 1.099f, 1.109f, 1.096f, 1.108f, 1.099f, 1.089f, 1.091f, 1.092f, 1.082f, 1.078f, 1.100f, 1.075f, 1.087f, // 16x
177 1.088f, 1.096f, 1.085f, 1.097f, 1.087f, 1.078f, 1.079f, 1.080f, 1.071f, 1.067f, 1.087f, 1.064f, 1.076f, // 32x
178 1.083f, 1.091f, 1.078f, 1.090f, 1.079f, 1.072f, 1.072f, 1.073f, 1.064f, 1.062f, 1.080f, 1.057f, 1.069f, // 64x
179 1.076f, 1.084f, 1.072f, 1.085f, 1.074f, 1.066f, 1.062f, 1.067f, 1.055f, 1.056f, 1.074f, 1.051f, 1.061f, // 128x
180 1.067f, 1.074f, 1.063f, 1.075f, 1.064f, 1.059f, 1.055f, 1.058f, 1.049f, 1.051f, 1.064f, 1.044f, 1.053f, // 256x
181 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, // 512x
182 1.033303857f, 1.034763813f, 1.029855013f, 1.036911249f, 0.988150537f, 1.029003501f, 1.021363258f,
183 0.98762089f, 1.021713376f, 1.040108204f, 0.987417698f, 1.019481421f, 0.987270057f, // 1024x
184 0.986859143f, 0.980877221f, 0.9469769f, 0.985535324f, 0.926243961f, 0.959814787f, 1.007963896f,
185 0.942685187f, 1.007630587f, 1.025460005f, 0.928696275f, 1.001296639f, 1.041275263f, // 2048x
186};
187// clang-format on
188static_assert(sizeof(AS7343_GAIN_CORRECTION) == GAIN_COUNT * NUM_BANDS * sizeof(float));
189// clang-format off
190static const ChannelContribution AS7343_CONTRIBUTIONS[] PROGMEM = {
191 {-0.028559089f, 0.450293995f, 0.001266925f}, // F1
192 {-0.042150948f, 3.566379984f, 0.012515925f}, // F2
193 {0.000661721f, 2.877424672f, 0.010243005f}, // FZ
194 {-0.108791278f, 2.273837581f, 0.009376157f}, // F3
195 {-0.167172180f, 1.519298503f, 0.006017556f}, // F4
196 {5.726660294f, 4.199345859f, 0.019357905f}, // FY
197 {0.0f, 0.0f, 0.0f}, // F5
198 {-0.241492014f, 1.185259160f, 0.006516325f}, // FXL
199 {-0.047665296f, 1.759469826f, 0.009780714f}, // F6
200 {-0.012516990f, 1.702006611f, 0.009950228f}, // F7
201 {-0.026424974f, -1.059196137f, -0.005425656f}, // F8
202 {-0.007565221f, -0.064827881f, -0.000232267f}, // NIR
203 {0.0f, 0.0f, 0.0f}, // Clear
204};
205static const ChannelTristimulus AS7343_TRISTIMULUS[] PROGMEM = {
206 {-0.07879f, -0.03269f, -0.31295f}, // F1
207 {-0.12235f, -0.01297f, -0.57885f}, // F2
208 {1.99879f, -0.04011f, 10.00197f}, // FZ
209 {-0.33364f, -0.06889f, -0.31281f}, // F3
210 {-0.06795f, -0.17453f, -0.19657f}, // F4
211 {-0.45419f, 5.69083f, -0.11077f}, // FY
212 {0.0f, 0.0f, 0.0f}, // F5
213 {5.27242f, -0.22956f, -0.06132f}, // FXL
214 {-0.05072f, -0.04762f, -0.03536f}, // F6
215 {-0.04666f, -0.01295f, -0.06025f}, // F7
216 {-0.03931f, -0.02797f, -0.12174f}, // F8
217 {0.0f, 0.0f, 0.0f}, // NIR
218 {0.0f, 0.0f, 0.0f}, // Clear
219};
220// clang-format on
221static_assert(sizeof(AS7343_CONTRIBUTIONS) == NUM_BANDS * sizeof(ChannelContribution));
222static_assert(sizeof(AS7343_TRISTIMULUS) == NUM_BANDS * sizeof(ChannelTristimulus));
223static constexpr ChipInfo as7343_chip(const char *name) {
224 return {
225 .name = name,
226 .init = AS7343_INIT,
227 .smux = AS7343_SMUX,
228 .readout = AS7343_READOUT,
229 .wavelengths = AS7343_WAVELENGTHS,
230 .gain_correction = AS7343_GAIN_CORRECTION,
231 .contributions = AS7343_CONTRIBUTIONS,
232 .tristimulus = AS7343_TRISTIMULUS,
233 .init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
234 .astep_reg = 0xD4,
235 .cfg0_reg = 0xBF,
236 .cfg1_reg = 0xC6,
237 .status2_reg = 0x90,
238 .id_reg = 0x5A,
239 .id_mask = 0xFF,
240 .id_value = 0x81,
241 .led_reg = 0xCD,
242 .led_sel_reg = 0, // the LED register drives the pin on its own
243 .adc_slots = AS7343_ADC_SLOTS,
244 .smux_steps = 1,
245 .integration_cycles = 3,
246 .smux_len = AS7343_SMUX_LEN,
247 .auto_smux = true,
248 .gain_correction_per_band = true,
249 .extra_clear_slot = 4,
250 };
251}
252const ChipInfo AS7343_CHIP PROGMEM = as7343_chip(AS7343_NAME);
253const ChipInfo TCS3448_CHIP PROGMEM = as7343_chip(TCS3448_NAME); // AS7343 register map, address 0x59
254#endif
255
256// Registers below 0x80 are only reachable with the REG_BANK bit of CFG0 set; CFG0 itself always answers.
258 const bool low = reg < BANK_SPLIT;
259 if (low == this->bank_low_) {
260 return true;
261 }
262 this->bank_low_ = low;
263 return this->write_byte(pgm(this->chip_->cfg0_reg), low ? CFG0_REG_BANK : 0);
264}
265
266bool AS734XComponent::update_bits_(uint8_t reg, uint8_t mask, bool value) {
267 uint8_t data = 0;
268 if (!this->select_bank_(reg) || !this->read_byte(reg, &data)) {
269 return false;
270 }
271 data = value ? (data | mask) : (data & ~mask);
272 return this->write_byte(reg, data);
273}
274
275// The exposure is applied before every measurement, so a gain, ATIME or ASTEP changed at runtime
276// takes effect on the next reading rather than only after a restart.
278 const ChipInfo *chip = this->chip_;
279 if (!this->select_bank_(REG_ATIME) || !this->write_byte(REG_ATIME, this->atime_) ||
280 !this->write_byte_16(pgm(chip->astep_reg), byteswap(this->astep_)) ||
281 !this->write_byte(pgm(chip->cfg1_reg), this->gain_)) {
282 return false;
283 }
284 this->reading_gain_ = this->gain_;
285 this->reading_atime_ = this->atime_;
286 this->reading_astep_ = this->astep_;
287 // One integration cycle lasts (ATIME + 1) * (ASTEP + 1) * 2.78 us.
288 const uint32_t cycle_ms = (this->atime_ + 1UL) * (this->astep_ + 1UL) * 139 / 50000;
289 this->integration_ms_ = cycle_ms * pgm(chip->integration_cycles);
290 this->timeout_ms_ = std::max(MIN_COLLECTION_TIMEOUT_MS, 2 * this->integration_ms_);
291 return true;
292}
293
295 const ChipInfo *chip = this->chip_;
296 const uint8_t id_reg = pgm(chip->id_reg);
297 uint8_t id = 0;
298 // The sensor keeps its bank across an MCU reset, so the first access always writes CFG0
299 bool ok = this->select_bank_(REG_ENABLE) && this->write_byte(REG_ENABLE, ENABLE_PON);
300 delayMicroseconds(200); // oscillator start-up
301 ok = ok && this->select_bank_(id_reg) && this->read_byte(id_reg, &id);
302 if (ok && (id & pgm(chip->id_mask)) != pgm(chip->id_value)) {
303 this->mark_failed(LOG_STR("Wrong chip ID"));
304 return;
305 }
306 const RegValue *init = pgm(chip->init);
307 for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
308 RegValue rv;
309 progmem_memcpy(&rv, &init[i], sizeof(rv));
310 ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
311 }
312 // Everything from here on, including the measurement registers, is in the high bank.
313 ok = ok && this->write_exposure_();
314 if (pgm(chip->auto_smux)) {
315 const uint8_t *smux = pgm(chip->smux);
316 const uint8_t len = pgm(chip->smux_len);
317 for (uint8_t i = 0; ok && i < AS7343_CHAINS * len; i++) {
318 ok = this->write_byte(AS7343_CHAIN_SMUX, progmem_read_byte(&smux[i])) &&
319 this->write_byte(AS7343_CHAIN_CMD, 0x46 + (i / len) * 0x10);
320 }
321 }
322 if (!ok) {
323 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
324 return;
325 }
326 this->disable_loop();
327}
328
330 const float gain = this->gain_ == GAIN_0_5X ? 0.5f : static_cast<float>(1 << (this->gain_ - 1));
331 ESP_LOGCONFIG(TAG,
332 "AS734x:\n"
333 " Chip: %s\n"
334 " Gain: %gx\n"
335 " ATIME: %u\n"
336 " ASTEP: %u\n"
337 " Glass attenuation factor: %.3f",
338 LOG_STR_ARG(pgm(this->chip_->name)), gain, this->atime_, this->astep_, this->glass_attenuation_factor_);
339 LOG_I2C_DEVICE(this);
340 LOG_UPDATE_INTERVAL(this);
341 for (uint8_t band = 0; band < NUM_BANDS; band++) {
342 char label[24];
343 const char *name = LOG_STR_ARG(BandNames::get_log_str(band, BandNames::LAST_INDEX));
344 snprintf(label, sizeof(label), "Counts %s", name);
345 sensor::log_sensor(TAG, " ", label, this->sensors_[band]);
346 snprintf(label, sizeof(label), "Basic counts %s", name);
347 sensor::log_sensor(TAG, " ", label, this->basic_counts_sensors_[band]);
348 }
349 LOG_SENSOR(" ", "Saturation level", this->saturation_level_sensor_);
350 LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
351 LOG_SENSOR(" ", "Irradiance photopic", this->irradiance_photopic_sensor_);
352 LOG_SENSOR(" ", "Irradiance PAR", this->irradiance_par_sensor_);
353 LOG_SENSOR(" ", "PPFD", this->ppfd_sensor_);
354 LOG_SENSOR(" ", "Color temperature", this->color_temperature_sensor_);
355#ifdef USE_AS734X_RGB
356 LOG_TEXT_SENSOR(" ", "RGB hex", this->rgb_hex_sensor_);
357#endif
358}
359
361 const uint8_t led_reg = pgm(this->chip_->led_reg);
362 if (led_reg == 0) {
363 return;
364 }
365 // The LED register only drives the LED while LED_SEL is set, so the pin is handed over before
366 // switching on and released only after switching off. The other order would leave the LED lit
367 // with nothing able to turn it off.
368 const uint8_t led_sel_reg = pgm(this->chip_->led_sel_reg);
369 bool ok = true;
370 if (enable && led_sel_reg != 0) {
371 ok = this->update_bits_(led_sel_reg, CONFIG_LED_SEL, true);
372 }
373 ok = this->update_bits_(led_reg, LED_ACT, enable) && ok;
374 if (!enable && led_sel_reg != 0) {
375 ok = this->update_bits_(led_sel_reg, CONFIG_LED_SEL, false) && ok;
376 }
377 // The measurement path assumes the high bank, so leave the chip there for a reading in flight
378 ok = this->select_bank_(REG_ENABLE) && ok;
379 if (!ok) {
380 ESP_LOGW(TAG, "Failed to switch the LED");
381 }
382}
383
385 if (this->state_ != State::STATE_IDLE) {
386 ESP_LOGW(TAG, "Skipping update, previous measurement still running");
387 return;
388 }
389 if (!this->write_exposure_()) {
390 this->abort_measurement_(LOG_STR("Failed to apply measurement settings"));
391 return;
392 }
393 this->step_ = 0;
394 this->peak_raw_count_ = 0;
395 this->raw_counts_.fill(0);
396 this->enable_loop();
397 if (!this->start_step_()) {
398 this->abort_measurement_(LOG_STR("Failed to start measurement"));
399 }
400}
401
404 if (pgm(this->chip_->auto_smux)) {
405 this->state_ = State::STATE_READ_DATA;
406 return this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SP_EN);
407 }
408 // Load this step's image into SMUX RAM; SMUX_EN copies it to the chain and clears itself when done.
409 std::array<uint8_t, MAX_SMUX_LEN> image{};
410 const uint8_t len = pgm(this->chip_->smux_len);
411 progmem_memcpy(image.data(), pgm(this->chip_->smux) + this->step_ * len, len);
412 this->state_ = State::STATE_WAIT_SMUX;
413 return this->write_byte(AS7341_CFG6, AS7341_SMUX_CMD_WRITE) &&
414 this->write_bytes(AS7341_SMUX_RAM, image.data(), len) &&
415 this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SMUX_EN);
416}
417
420 uint8_t value = 0;
421 switch (this->state_) {
423 // A failed read counts as busy, so the timeout below catches a chip that stops answering.
424 if (this->read_byte(REG_ENABLE, &value) && !(value & ENABLE_SMUX_EN)) {
425 this->start_ms_ = now;
426 this->state_ = State::STATE_READ_DATA;
427 if (!this->write_byte(REG_ENABLE, ENABLE_PON | ENABLE_SP_EN)) {
428 this->abort_measurement_(LOG_STR("Failed to start integration"));
429 }
430 return;
431 }
432 break;
434 if (now - this->start_ms_ < this->integration_ms_) {
435 return;
436 }
437 if (this->read_byte(pgm(this->chip_->status2_reg), &value) && (value & STATUS2_AVALID)) {
438 this->read_step_();
439 return;
440 }
441 break;
443 this->disable_loop();
444 return;
445 }
446 if (now - this->start_ms_ > this->timeout_ms_) {
447 this->abort_measurement_(LOG_STR("Measurement timeout"));
448 }
449}
450
452 // Reading ASTATUS latches the data, which is only consistent within one burst starting there.
453 std::array<uint8_t, 1 + 2 * MAX_ADC_SLOTS> frame{};
454 if (!this->read_bytes(REG_ASTATUS, frame.data(), 1 + 2 * pgm(this->chip_->adc_slots)) ||
455 !this->write_byte(REG_ENABLE, ENABLE_PON)) {
456 this->abort_measurement_(LOG_STR("Failed to read channel data"));
457 return;
458 }
459 if (frame[0] & ASTATUS_ASAT) {
460 ESP_LOGV(TAG, "Step %u saturated", this->step_);
461 }
462 auto slot_value = [&frame](uint8_t slot) { return encode_uint16(frame[2 + 2 * slot], frame[1 + 2 * slot]); };
463 const uint8_t *readout = pgm(this->chip_->readout) + this->step_ * NUM_BANDS;
464 const uint8_t extra_clear = pgm(this->chip_->extra_clear_slot);
465 for (uint8_t band = 0; band < NUM_BANDS; band++) {
466 const uint8_t slot = progmem_read_byte(&readout[band]);
467 if (slot == NO_SLOT) {
468 continue;
469 }
470 const uint16_t raw = slot_value(slot);
471 this->peak_raw_count_ = std::max(this->peak_raw_count_, raw);
472 uint16_t counts = raw;
473 if (band == CLEAR_BAND && extra_clear != NO_SLOT) {
474 const uint16_t extra = slot_value(extra_clear);
475 this->peak_raw_count_ = std::max(this->peak_raw_count_, extra);
476 counts = (static_cast<uint32_t>(raw) + extra) / 2;
477 }
478 this->raw_counts_[band] = counts;
479 }
480 if (++this->step_ < pgm(this->chip_->smux_steps)) {
481 if (!this->start_step_()) {
482 this->abort_measurement_(LOG_STR("Failed to configure SMUX"));
483 }
484 return;
485 }
486 this->publish_measurement_();
487 this->status_clear_warning();
488 this->state_ = State::STATE_IDLE;
489 this->disable_loop();
490}
491
492// A basic count is the raw count scaled to one unit of gain and one millisecond of integration
493// time, which makes readings taken with different settings comparable. The manufacturer's
494// conversion factors are calibrated against that definition.
496 this->basic_counts_.fill(0.0f);
497 this->max_basic_count_ = 0.0f;
498 this->max_band_basic_count_ = 0.0f;
499 this->clear_basic_count_ = 0.0f;
500
501 // The gain enum counts in powers of two starting at 0.5x, so the multiplier is 2^gain / 2.
502 const float gain_x = static_cast<float>(1 << this->reading_gain_) / 2.0f;
503 const float integration_ms = (1.0f + this->reading_atime_) * (1.0f + this->reading_astep_) * 2.78e-3f;
504 if (gain_x <= 0.0f || integration_ms <= 0.0f) {
505 return;
506 }
507 const float inv_exposure = 1.0f / (gain_x * integration_ms);
508 const ChipInfo *chip = this->chip_;
509 const uint16_t *wavelengths = pgm(chip->wavelengths);
510 const float *gain_correction = pgm(chip->gain_correction);
511 const bool per_band = pgm(chip->gain_correction_per_band);
512 const uint8_t correction_base = per_band ? this->reading_gain_ * NUM_BANDS : this->reading_gain_;
513
514 for (uint8_t band = 0; band < NUM_BANDS; band++) {
515 const uint16_t wavelength = pgm(wavelengths[band]);
516 if (wavelength == ABSENT_NM) {
517 continue;
518 }
519 float basic_count = this->raw_counts_[band] * inv_exposure;
520 basic_count -= this->dark_current_[band];
521 basic_count = std::max(basic_count, 0.0f);
522 basic_count *= pgm(gain_correction[per_band ? correction_base + band : correction_base]);
523 basic_count *= this->channel_correction_[band];
524 basic_count *= this->glass_attenuation_factor_;
525
526 this->basic_counts_[band] = basic_count;
527 this->max_basic_count_ = std::max(this->max_basic_count_, basic_count);
528 if (wavelength == WIDEBAND_NM) {
529 this->clear_basic_count_ = basic_count;
530 } else if (wavelength >= VISIBLE_MIN_NM && wavelength <= VISIBLE_MAX_NM) {
531 this->max_band_basic_count_ = std::max(this->max_band_basic_count_, basic_count);
532 }
533 }
534}
535
537 float divisor = 1.0f;
538 switch (this->normalization_) {
540 divisor = this->max_basic_count_;
541 break;
543 divisor = this->max_band_basic_count_;
544 break;
546 divisor = this->clear_basic_count_;
547 break;
549 return 1.0f;
550 }
551 // In the dark the reference can be zero, and dividing by that would publish infinities.
552 return divisor > 0.0f ? divisor : 1.0f;
553}
554
556 // Normalization describes the shape of the spectrum rather than its absolute level. Channels
557 // outside the chosen reference can exceed 1.0 and are left that way rather than clipped: with
558 // BANDS the near infrared channel legitimately sits well above the visible bands.
559 const float scale = 1.0f / this->normalization_divisor_();
560 for (uint8_t band = 0; band < NUM_BANDS; band++) {
561 if (this->basic_counts_sensors_[band] != nullptr) {
562 this->basic_counts_sensors_[band]->publish_state(this->basic_counts_[band] * scale);
563 }
564 }
565}
566
567// Each band contributes a fixed amount per basic count to every integrated quantity, so the
568// totals are a weighted sum over the bands.
569// The weighted sums can go negative under light the tables were not fitted for, such as pure blue or
570// far red, so the published values are floored at zero.
572 float irradiance_photopic = 0.0f;
573 float irradiance_par = 0.0f;
574 float ppfd = 0.0f;
575
576 const ChannelContribution *contributions = pgm(this->chip_->contributions);
577 for (uint8_t band = 0; band < NUM_BANDS; band++) {
578 const float basic_count = this->basic_counts_[band];
579 const ChannelContribution contribution = pgm(contributions[band]);
580
581 irradiance_photopic += contribution.irradiance_photopic * basic_count;
582 irradiance_par += contribution.irradiance_par * basic_count;
583 ppfd += contribution.ppfd * basic_count;
584 }
585 irradiance_photopic = std::max(irradiance_photopic, 0.0f);
586 irradiance_par = std::max(irradiance_par, 0.0f);
587 ppfd = std::max(ppfd, 0.0f) * 1e3f;
588 const float illuminance = irradiance_photopic * LUMENS_PER_WATT;
589
590 ESP_LOGV(TAG, "Photopic irradiance %.4f, PAR %.4f, PPFD %.4f, illuminance %.2f lx", irradiance_photopic,
591 irradiance_par, ppfd, illuminance);
592 if (this->illuminance_sensor_ != nullptr) {
593 this->illuminance_sensor_->publish_state(illuminance);
594 }
595 if (this->irradiance_photopic_sensor_ != nullptr) {
596 this->irradiance_photopic_sensor_->publish_state(irradiance_photopic);
597 }
598 if (this->irradiance_par_sensor_ != nullptr) {
599 this->irradiance_par_sensor_->publish_state(irradiance_par);
600 }
601 if (this->ppfd_sensor_ != nullptr) {
602 this->ppfd_sensor_->publish_state(ppfd);
603 }
604}
605
607 float tri_x = 0.0f;
608 float tri_y = 0.0f;
609 float tri_z = 0.0f;
610
611 const ChannelTristimulus *table = pgm(this->chip_->tristimulus);
612 for (uint8_t band = 0; band < NUM_BANDS; band++) {
613 const float basic_count = this->basic_counts_[band];
614 const ChannelTristimulus tristimulus = pgm(table[band]);
615
616 tri_x += tristimulus.x * basic_count;
617 tri_y += tristimulus.y * basic_count;
618 tri_z += tristimulus.z * basic_count;
619 }
620
621 const float color_temperature = tristimulus_to_cct(tri_x, tri_y, tri_z);
622 ESP_LOGV(TAG, "XYZ %.4f, %.4f, %.4f -> CCT %.0f K", tri_x, tri_y, tri_z, color_temperature);
623 if (this->color_temperature_sensor_ != nullptr) {
624 this->color_temperature_sensor_->publish_state(color_temperature);
625 }
626
627#ifdef USE_AS734X_RGB
628 if (this->rgb_hex_sensor_ == nullptr) {
629 return;
630 }
631 // Convert to chromaticity first, so the colour describes the hue rather than the brightness.
632 // Absolute tristimulus values clamp to white under anything but dim light.
633 char rgb_hex[7] = "000000"; // no usable chromaticity reads as black rather than an empty string
634 const float sum = tri_x + tri_y + tri_z;
635 if (sum > MIN_TRISTIMULUS_SUM) {
636 uint8_t rgb[3];
637 tristimulus_to_rgb(tri_x / sum, tri_y / sum, tri_z / sum, rgb[0], rgb[1], rgb[2]);
638 format_hex_to(rgb_hex, rgb, sizeof(rgb));
639 }
640 this->rgb_hex_sensor_->publish_state(rgb_hex);
641#endif
642}
643
646 for (uint8_t band = 0; band < NUM_BANDS; band++) {
647 if (this->sensors_[band] != nullptr) {
648 this->sensors_[band]->publish_state(this->raw_counts_[band]);
649 }
650 }
651 this->publish_basic_counts_();
653 this->publish_color_();
654 if (this->saturation_level_sensor_ != nullptr) {
655 const uint32_t max_adc =
656 std::min<uint32_t>((this->reading_atime_ + 1UL) * (this->reading_astep_ + 1UL), MAX_ADC_COUNT);
657 const float level = max_adc == 0 ? 0.0f : 100.0f * this->peak_raw_count_ / max_adc;
658 ESP_LOGV(TAG, "Highest ADC count %u of %u (%.1f%%)", this->peak_raw_count_, static_cast<unsigned>(max_adc), level);
659 this->saturation_level_sensor_->publish_state(level);
660 }
661}
662
663void AS734XComponent::abort_measurement_(const LogString *reason) {
664 ESP_LOGW(TAG, "%s", LOG_STR_ARG(reason));
665 this->write_byte(REG_ENABLE, ENABLE_PON);
666 this->status_set_warning(reason);
667 this->state_ = State::STATE_IDLE;
668 this->disable_loop();
669}
670
671} // namespace esphome::as734x
uint8_t raw[35]
Definition bl0939.h:0
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
void status_clear_warning()
Definition component.h:291
void enable_led(bool enable)
Turns the sensor's LED driver on or off. Meant for lambdas.
Definition as734x.cpp:360
text_sensor::TextSensor * rgb_hex_sensor_
Definition as734x.h:195
std::array< float, NUM_BANDS > channel_correction_
Definition as734x.h:170
std::array< float, NUM_BANDS > dark_current_
Definition as734x.h:169
void abort_measurement_(const LogString *reason)
Definition as734x.cpp:663
std::array< sensor::Sensor *, NUM_BANDS > basic_counts_sensors_
Definition as734x.h:168
bool select_bank_(uint8_t reg)
Definition as734x.cpp:257
bool update_bits_(uint8_t reg, uint8_t mask, bool value)
Definition as734x.cpp:266
std::array< uint16_t, NUM_BANDS > raw_counts_
Definition as734x.h:172
float normalization_divisor_() const
Definition as734x.cpp:536
std::array< float, NUM_BANDS > basic_counts_
Definition as734x.h:171
std::array< sensor::Sensor *, NUM_BANDS > sensors_
Definition as734x.h:167
bool write_byte(uint8_t a_register, uint8_t data) const
Definition i2c.h:265
bool read_byte(uint8_t a_register, uint8_t *data)
Definition i2c.h:240
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:152
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) const
Definition i2c.h:251
bool write_byte_16(uint8_t a_register, uint16_t data) const
Definition i2c.h:267
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:217
void publish_state(const std::string &state)
Definition text_sensor.h:41
AlsGain501 gain
ESPHOME_LOG_TAG(TAG, "as734x")
const ChipInfo TCS3448_CHIP
void tristimulus_to_rgb(float x, float y, float z, uint8_t &r, uint8_t &g, uint8_t &b)
PROGMEM_STRING_TABLE(BandNames, "F1", "F2", "FZ", "F3", "F4", "FY", "F5", "FXL", "F6", "F7", "F8", "NIR", "Clear")
const ChipInfo AS7341_CHIP
float tristimulus_to_cct(float x, float y, float z)
constexpr float MIN_TRISTIMULUS_SUM
const ChipInfo AS7343_CHIP
FLAG_HAS_TRANSITION float
void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj)
Definition sensor.cpp:12
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
const void size_t len
Definition hal.h:68
void IRAM_ATTR HOT delayMicroseconds(uint32_t us)
Definition hal.cpp:48
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:980
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).
Definition helpers.cpp:428
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t
Per-chip constants, stored in flash. The pointed-to tables are in flash too.
Definition as734x.h:77
const uint16_t * wavelengths
Definition as734x.h:82
const uint8_t * smux
Definition as734x.h:80
const ChannelTristimulus * tristimulus
Definition as734x.h:85
const char * name
Definition as734x.h:78
const RegValue * init
Definition as734x.h:79
const uint8_t * readout
Definition as734x.h:81
const ChannelContribution * contributions
Definition as734x.h:84
const float * gain_correction
Definition as734x.h:83
void byteswap()