ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
apc1.cpp
Go to the documentation of this file.
1#include "apc1.h"
3#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
7
8#include <cinttypes>
9#include <cmath>
10
11namespace esphome::apc1 {
12
13ESPHOME_LOG_TAG(TAG, "apc1");
14
15static constexpr uint8_t APC1_MEASUREMENT_OFFSET_AQI = 58;
16static constexpr uint8_t APC1_MEASUREMENT_OFFSET_ERROR_CODE = 61;
17
18static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_MODULE = 4;
19static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_SERIAL = 10;
20static constexpr uint8_t APC1_DEVICE_INFO_OFFSET_FW = 19;
21
22// Give a freshly powered sensor time to boot before the first retry
23static constexpr uint32_t APC1_COLD_BOOT_INIT_DELAY_MS = 2000;
24// Re-send the init commands at most this often while no frames arrive
25static constexpr uint32_t APC1_INIT_RETRY_INTERVAL_MS = 10000;
26// Active mode streams a frame every second; this long without one means the sensor stopped
27static constexpr uint32_t APC1_NO_DATA_TIMEOUT_MS = 15000;
28// Drop a partial frame when the bytes stop mid-way
29static constexpr uint32_t APC1_RX_IDLE_TIMEOUT_MS = 200;
30
31// Frames and commands end with the 16-bit sum of every byte before it
32static uint16_t apc1_checksum(const uint8_t *data, size_t len) {
33 uint16_t sum = 0;
34 for (size_t i = 0; i < len; i++) {
35 sum += data[i];
36 }
37 return sum;
38}
39
41 if (this->set_pin_ != nullptr) {
42 this->set_pin_->setup();
43 this->set_pin_->digital_write(true);
44 }
45 if (this->reset_pin_ != nullptr) {
46 this->reset_pin_->setup();
47 this->reset_pin_->digital_write(true);
48 }
49
50 // Attempt initial communication immediately for warm restarts
51 this->initialize_device_();
52
53 // Schedule delayed initialization to catch sensors during cold power-on bootup
54 this->set_timeout(APC1_COLD_BOOT_INIT_DELAY_MS, [this]() { this->initialize_device_(); });
55}
56
59 if (this->active_mode_ && !this->idle_) {
60 this->set_active_mode();
61 }
63}
64
65void APC1Component::dump_config() {
66 ESP_LOGCONFIG(TAG, "APC1:");
67 LOG_PIN(" Set Pin: ", this->set_pin_);
68 LOG_PIN(" Reset Pin: ", this->reset_pin_);
69 LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
70 LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
71 LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_);
72 LOG_SENSOR(" ", "PM1.0 Standard", this->pm_1_0_std_sensor_);
73 LOG_SENSOR(" ", "PM2.5 Standard", this->pm_2_5_std_sensor_);
74 LOG_SENSOR(" ", "PM10.0 Standard", this->pm_10_0_std_sensor_);
75 LOG_SENSOR(" ", "PM >0.3um", this->pm_0_3um_sensor_);
76 LOG_SENSOR(" ", "PM >0.5um", this->pm_0_5um_sensor_);
77 LOG_SENSOR(" ", "PM >1.0um", this->pm_1_0um_sensor_);
78 LOG_SENSOR(" ", "PM >2.5um", this->pm_2_5um_sensor_);
79 LOG_SENSOR(" ", "PM >5.0um", this->pm_5_0um_sensor_);
80 LOG_SENSOR(" ", "PM >10.0um", this->pm_10_0um_sensor_);
81 LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
82 LOG_SENSOR(" ", "eCO2", this->eco2_sensor_);
83 LOG_SENSOR(" ", "AQI", this->aqi_sensor_);
84 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
85 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
86 LOG_SENSOR(" ", "Raw Temperature", this->raw_temperature_sensor_);
87 LOG_SENSOR(" ", "Raw Humidity", this->raw_humidity_sensor_);
88 LOG_SENSOR(" ", "RS0", this->rs0_sensor_);
89 LOG_SENSOR(" ", "RS2", this->rs2_sensor_);
90 LOG_SENSOR(" ", "RS3", this->rs3_sensor_);
91 LOG_SENSOR(" ", "Error Code", this->error_code_sensor_);
92}
93
94void APC1Component::loop() {
96
97 // If in active mode and not idle, retry periodically if no valid data received
98 if (this->active_mode_ && !this->idle_) {
99 if (this->last_valid_frame_ == 0) {
100 if (now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
101 ESP_LOGD(TAG, "Waiting for APC1 sensor to respond, sending init commands...");
102 this->initialize_device_();
103 }
104 } else if (now - this->last_valid_frame_ > APC1_NO_DATA_TIMEOUT_MS &&
105 now - this->last_init_attempt_ >= APC1_INIT_RETRY_INTERVAL_MS) {
106 ESP_LOGW(TAG, "No data received from APC1 for %" PRIu32 " ms, re-initializing...", now - this->last_valid_frame_);
107 this->initialize_device_();
108 }
109 }
110
111 if (this->rx_index_ > 0 && now - this->last_transmission_ > APC1_RX_IDLE_TIMEOUT_MS) {
112 ESP_LOGV(TAG, "Timed out waiting for data, resetting buffer");
113 this->rx_index_ = 0;
114 }
115
116 while (this->available()) {
117 this->last_transmission_ = now;
118 uint8_t byte;
119 this->read_byte(&byte);
120
121 if (this->rx_index_ == 0) {
122 if (byte == APC1_HEADER_1) {
123 this->rx_buffer_[this->rx_index_++] = byte;
124 }
125 continue;
126 }
127
128 if (this->rx_index_ == 1) {
129 if (byte == APC1_HEADER_2) {
130 this->rx_buffer_[this->rx_index_++] = byte;
131 } else if (byte != APC1_HEADER_1) {
132 this->rx_index_ = 0;
133 }
134 continue;
135 }
136
137 this->rx_buffer_[this->rx_index_++] = byte;
138 if (this->rx_index_ < APC1_FRAME_HEADER_SIZE) {
139 continue;
140 }
141
142 // The payload carries at least the checksum and must fit the buffer; checking it before the sum keeps a
143 // corrupt length from wrapping the total and walking past the buffer
144 const uint16_t payload_len = encode_uint16(this->rx_buffer_[2], this->rx_buffer_[3]);
145 if (payload_len < 2 || payload_len > this->rx_buffer_.size() - APC1_FRAME_HEADER_SIZE) {
146 ESP_LOGW(TAG, "Invalid frame length: %u", payload_len);
147 this->rx_index_ = 0;
148 continue;
149 }
150 const uint8_t total_len = payload_len + APC1_FRAME_HEADER_SIZE;
151 if (this->rx_index_ == total_len) {
152 this->parse_frame_(total_len);
153 this->rx_index_ = 0;
154 }
155 }
156}
157
158void APC1Component::parse_frame_(uint16_t total_len) {
159 uint16_t expected_checksum = encode_uint16(this->rx_buffer_[total_len - 2], this->rx_buffer_[total_len - 1]);
160 uint16_t calculated_checksum = apc1_checksum(this->rx_buffer_.data(), total_len - 2);
161 if (expected_checksum != calculated_checksum) {
162 ESP_LOGW(TAG, "APC1 checksum mismatch: calculated 0x%04X != expected 0x%04X", calculated_checksum,
163 expected_checksum);
164 return;
165 }
166
167 if (total_len == APC1_FRAME_SIZE_MEASUREMENT) {
169 } else if (total_len == APC1_FRAME_SIZE_DEVICE_INFO) {
171 } else if (total_len == APC1_FRAME_SIZE_COMMAND_RESPONSE) {
172 ESP_LOGD(TAG, "APC1 command response: cmd=0x%02X, data=0x%02X", this->rx_buffer_[4], this->rx_buffer_[5]);
173 } else {
174 ESP_LOGD(TAG, "Received frame of unknown length %u", total_len);
175 }
176}
177
178// One message per error bit, bit 0 first
179PROGMEM_STRING_TABLE(ErrorStrings, "Fan restart error (too many restarts)", "Fan speed low", "Photodiode fault",
180 "Fan stopped / startup error", "Laser fault", "VOC sensor fault", "RHT sensor fault");
181
184 this->last_valid_frame_ = now;
185
186 uint8_t error_code = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_ERROR_CODE];
187 if (error_code != this->last_error_code_) {
188 if (error_code != 0) {
189 for (uint8_t bit = 0; bit < ErrorStrings::COUNT; bit++) {
190 if (error_code & (1 << bit)) {
191 ESP_LOGW(TAG, "APC1: %s", LOG_STR_ARG(ErrorStrings::get_log_str(bit, ErrorStrings::LAST_INDEX)));
192 }
193 }
194 this->status_set_warning(LOG_STR("Sensor reported hardware error"));
195 } else {
196 ESP_LOGI(TAG, "APC1: Hardware error cleared");
197 this->status_clear_warning();
198 }
199 this->last_error_code_ = error_code;
200 if (this->error_code_sensor_ != nullptr) {
201 this->error_code_sensor_->publish_state(error_code);
202 }
203 }
204
205 uint8_t aqi = this->rx_buffer_[APC1_MEASUREMENT_OFFSET_AQI];
206 if (aqi == 0) {
207 if (!this->gas_warming_up_) {
208 this->gas_warming_up_ = true;
209 ESP_LOGI(TAG, "APC1: Gas sensor warming up (AQI=0)");
210 }
211 } else if (this->gas_warming_up_) {
212 this->gas_warming_up_ = false;
213 ESP_LOGI(TAG, "APC1: Gas sensor warm-up complete (AQI=%u)", aqi);
214 }
215
216 // Measurement payload data (bytes 4-57): big-endian sensor readings.
217 // The values below feed the debug summary, so they are always decoded; every
218 // other reading is only decoded when its sensor is configured.
219 const auto u16 = [this](uint8_t offset) {
220 return encode_uint16(this->rx_buffer_[offset], this->rx_buffer_[offset + 1]);
221 };
222 const auto u32 = [this](uint8_t offset) {
223 return encode_uint32(this->rx_buffer_[offset], this->rx_buffer_[offset + 1], this->rx_buffer_[offset + 2],
224 this->rx_buffer_[offset + 3]);
225 };
226 uint16_t pm_1_0 = u16(10);
227 uint16_t pm_2_5 = u16(12);
228 uint16_t pm_10_0 = u16(14);
229 uint16_t tvoc = u16(28);
230 uint16_t eco2 = u16(30);
231 float temp_comp = static_cast<int16_t>(u16(34)) / 10.0f;
232 float hum_comp = u16(36) / 10.0f;
233
234 ESP_LOGD(TAG, "APC1: PM1.0: %u, PM2.5: %u, PM10: %u, TVOC: %u, eCO2: %u, AQI: %u, Temp: %.1f°C, Hum: %.1f%%", pm_1_0,
235 pm_2_5, pm_10_0, tvoc, eco2, aqi, temp_comp, hum_comp);
236
237 if (this->pm_1_0_std_sensor_ != nullptr)
238 this->pm_1_0_std_sensor_->publish_state(u16(4));
239 if (this->pm_2_5_std_sensor_ != nullptr)
240 this->pm_2_5_std_sensor_->publish_state(u16(6));
241 if (this->pm_10_0_std_sensor_ != nullptr)
242 this->pm_10_0_std_sensor_->publish_state(u16(8));
243
244 if (this->pm_1_0_sensor_ != nullptr)
245 this->pm_1_0_sensor_->publish_state(pm_1_0);
246 if (this->pm_2_5_sensor_ != nullptr)
247 this->pm_2_5_sensor_->publish_state(pm_2_5);
248 if (this->pm_10_0_sensor_ != nullptr)
249 this->pm_10_0_sensor_->publish_state(pm_10_0);
250
251 if (this->pm_0_3um_sensor_ != nullptr)
252 this->pm_0_3um_sensor_->publish_state(u16(16));
253 if (this->pm_0_5um_sensor_ != nullptr)
254 this->pm_0_5um_sensor_->publish_state(u16(18));
255 if (this->pm_1_0um_sensor_ != nullptr)
256 this->pm_1_0um_sensor_->publish_state(u16(20));
257 if (this->pm_2_5um_sensor_ != nullptr)
258 this->pm_2_5um_sensor_->publish_state(u16(22));
259 if (this->pm_5_0um_sensor_ != nullptr)
260 this->pm_5_0um_sensor_->publish_state(u16(24));
261 if (this->pm_10_0um_sensor_ != nullptr)
262 this->pm_10_0um_sensor_->publish_state(u16(26));
263
264 if (this->tvoc_sensor_ != nullptr)
265 this->tvoc_sensor_->publish_state(tvoc);
266 if (this->eco2_sensor_ != nullptr)
267 this->eco2_sensor_->publish_state(eco2);
268 if (this->aqi_sensor_ != nullptr)
269 this->aqi_sensor_->publish_state(aqi > 0 ? aqi : NAN);
270
271 if (this->temperature_sensor_ != nullptr)
272 this->temperature_sensor_->publish_state(temp_comp);
273 if (this->humidity_sensor_ != nullptr)
274 this->humidity_sensor_->publish_state(hum_comp);
275
276 if (this->raw_temperature_sensor_ != nullptr) {
277 this->raw_temperature_sensor_->publish_state(static_cast<int16_t>(u16(38)) / 10.0f);
278 }
279 if (this->raw_humidity_sensor_ != nullptr)
280 this->raw_humidity_sensor_->publish_state(u16(40) / 10.0f);
281
282 if (this->rs0_sensor_ != nullptr) {
283 this->rs0_sensor_->publish_state(u32(42));
284 }
285 if (this->rs2_sensor_ != nullptr) {
286 this->rs2_sensor_->publish_state(u32(50));
287 }
288 if (this->rs3_sensor_ != nullptr) {
289 this->rs3_sensor_->publish_state(u32(54));
290 }
291}
292
294 const uint8_t *serial = this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_SERIAL;
295 uint16_t fw_version =
296 encode_uint16(this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW], this->rx_buffer_[APC1_DEVICE_INFO_OFFSET_FW + 1]);
297 ESP_LOGI(TAG, "APC1: Module: %.6s, Serial: %08" PRIX32 "%08" PRIX32 ", Firmware: 0x%04X (%u)",
298 reinterpret_cast<const char *>(this->rx_buffer_.data() + APC1_DEVICE_INFO_OFFSET_MODULE),
299 encode_uint32(serial[0], serial[1], serial[2], serial[3]),
300 encode_uint32(serial[4], serial[5], serial[6], serial[7]), fw_version, fw_version);
301}
302
307
308void APC1Component::set_active_mode() {
309 if (!this->active_mode_) {
310 this->arm_watchdog_();
311 }
312 this->active_mode_ = true;
313 this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_ACTIVE);
314}
315
316void APC1Component::set_passive_mode() {
317 this->active_mode_ = false;
318 this->send_command_(APC1Command::APC1_COMMAND_MEASUREMENT_MODE, APC1_MEASUREMENT_MODE_PASSIVE);
319}
320
321void APC1Component::request_measurement() {
323}
324
325void APC1Component::set_idle_mode() {
326 this->idle_ = true;
327 this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_IDLE);
328}
329
330void APC1Component::set_measurement_mode() {
331 this->idle_ = false;
332 this->arm_watchdog_();
333 this->send_command_(APC1Command::APC1_COMMAND_OPERATION_MODE, APC1_OPERATING_MODE_STANDARD);
334}
335
337 std::array<uint8_t, APC1_COMMAND_FRAME_SIZE> buf{
338 APC1_HEADER_1,
339 APC1_HEADER_2,
340 static_cast<uint8_t>(cmd),
341 static_cast<uint8_t>((data >> 8) & 0xFF),
342 static_cast<uint8_t>(data & 0xFF),
343 0,
344 0,
345 };
346 uint16_t checksum = apc1_checksum(buf.data(), 5);
347 buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
348 buf[6] = static_cast<uint8_t>(checksum & 0xFF);
349 this->write_array(buf);
350}
351
352} // namespace esphome::apc1
uint8_t checksum
Definition bl0906.h:3
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.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:291
virtual void setup()=0
virtual void digital_write(bool value)=0
std::array< uint8_t, APC1_FRAME_SIZE_MEASUREMENT > rx_buffer_
Definition apc1.h:91
uint32_t last_transmission_
Definition apc1.h:88
void parse_frame_(uint16_t total_len)
Definition apc1.cpp:158
void send_command_(APC1Command cmd, uint16_t data)
Definition apc1.cpp:336
uint32_t last_init_attempt_
Definition apc1.h:90
void arm_watchdog_()
Restarts the no-data watchdog so a mode change gets the same grace period as a cold boot.
Definition apc1.cpp:303
bool read_byte(uint8_t *data)
Definition uart.h:36
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
ESPHOME_LOG_TAG(TAG, "apc1")
PROGMEM_STRING_TABLE(ErrorStrings, "Fan restart error (too many restarts)", "Fan speed low", "Photodiode fault", "Fan stopped / startup error", "Laser fault", "VOC sensor fault", "RHT sensor fault")
const void size_t len
Definition hal.h:68
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:988
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
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t