ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
sps30.cpp
Go to the documentation of this file.
1#include "esphome/core/hal.h"
2#include "esphome/core/log.h"
3#include "sps30.h"
4
5#include <cinttypes>
6
7namespace esphome {
8namespace sps30 {
9
10static const char *const TAG = "sps30";
11
12static const uint16_t SPS30_CMD_GET_ARTICLE_CODE = 0xD025;
13static const uint16_t SPS30_CMD_GET_SERIAL_NUMBER = 0xD033;
14static const uint16_t SPS30_CMD_GET_FIRMWARE_VERSION = 0xD100;
15static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010;
16static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG = 0x0300;
17static const uint16_t SPS30_CMD_GET_DATA_READY_STATUS = 0x0202;
18static const uint16_t SPS30_CMD_READ_MEASUREMENT = 0x0300;
19static const uint16_t SPS30_CMD_STOP_MEASUREMENTS = 0x0104;
20static const uint16_t SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS = 0x8004;
21static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607;
22static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304;
23static const size_t SERIAL_NUMBER_LENGTH = 8;
24static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5;
25static const uint32_t SPS30_WARM_UP_SEC = 30;
26
28 this->write_command(SPS30_CMD_SOFT_RESET);
30 this->set_timeout(500, [this]() {
32 if (!this->get_register(SPS30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version_, 1)) {
33 this->error_code_ = FIRMWARE_VERSION_READ_FAILED;
34 this->mark_failed();
35 return;
36 }
38 uint16_t raw_serial_number[8];
39 if (!this->get_register(SPS30_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 1)) {
40 this->error_code_ = SERIAL_NUMBER_READ_FAILED;
41 this->mark_failed();
42 return;
43 }
44
45 for (size_t i = 0; i < 8; ++i) {
46 this->serial_number_[i * 2] = static_cast<char>(raw_serial_number[i] >> 8);
47 this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF));
48 }
49 ESP_LOGV(TAG, " Serial number: %s", this->serial_number_);
50
51 bool result;
52 if (this->fan_interval_.has_value()) {
53 // override default value
54 result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
55 } else {
56 result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS);
57 }
58
59 this->set_timeout(20, [this, result]() {
60 if (result) {
61 uint16_t secs[2];
62 if (this->read_data(secs, 2)) {
63 this->fan_interval_ = secs[0] << 16 | secs[1];
64 }
65 }
69 this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
70 this->next_state_ = READ;
71 this->setup_complete_ = true;
72 });
73 });
74}
75
77 ESP_LOGCONFIG(TAG, "SPS30:");
78 LOG_I2C_DEVICE(this);
79 if (this->is_failed()) {
80 switch (this->error_code_) {
82 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
83 break;
85 ESP_LOGW(TAG, "Measurement Initialization failed");
86 break;
88 ESP_LOGW(TAG, "Unable to request serial number");
89 break;
91 ESP_LOGW(TAG, "Unable to read serial number");
92 break;
94 ESP_LOGW(TAG, "Unable to request firmware version");
95 break;
97 ESP_LOGW(TAG, "Unable to read firmware version");
98 break;
99 default:
100 ESP_LOGW(TAG, "Unknown setup error");
101 break;
102 }
103 }
104 LOG_UPDATE_INTERVAL(this);
105 ESP_LOGCONFIG(TAG,
106 " Serial number: %s\n"
107 " Firmware version v%0d.%0d",
108 this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF);
109 if (this->idle_interval_.has_value()) {
110 ESP_LOGCONFIG(TAG, " Idle interval: %" PRIu32 "s", this->idle_interval_.value() / 1000);
111 }
112 LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_);
113 LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_);
114 LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_);
115 LOG_SENSOR(" ", "PM10 Weight Concentration", this->pm_10_0_sensor_);
116 LOG_SENSOR(" ", "PM1.0 Number Concentration", this->pmc_1_0_sensor_);
117 LOG_SENSOR(" ", "PM2.5 Number Concentration", this->pmc_2_5_sensor_);
118 LOG_SENSOR(" ", "PM4 Number Concentration", this->pmc_4_0_sensor_);
119 LOG_SENSOR(" ", "PM10 Number Concentration", this->pmc_10_0_sensor_);
120 LOG_SENSOR(" ", "PM typical size", this->pm_size_sensor_);
121}
122
124 if (!this->setup_complete_)
125 return;
127 if (this->status_has_warning()) {
128 ESP_LOGD(TAG, "Reconnecting");
129 if (this->write_command(SPS30_CMD_SOFT_RESET)) {
130 ESP_LOGD(TAG, "Soft-reset successful; waiting 500 ms");
131 this->set_timeout(500, [this]() {
134 this->status_clear_warning();
136 ESP_LOGD(TAG, "Reconnected; resuming continuous measurement");
137 });
138 } else {
139 ESP_LOGD(TAG, "Soft-reset failed");
140 }
141 return;
142 }
143
144 // If its not time to take an action, do nothing.
145 const uint32_t update_start_ms = millis();
146 if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) {
147 ESP_LOGD(TAG, "Sensor waiting for %" PRIu32 "ms before transitioning to state %d.",
148 (this->next_state_ms_ - update_start_ms), this->next_state_);
149 return;
150 }
151
152 switch (this->next_state_) {
153 case WAKE:
154 this->start_measurement();
155 return;
156 case NONE:
157 return;
158 case READ:
159 // Read logic continues below
160 break;
161 }
162
164 if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) {
165 this->status_set_warning();
166 return;
167 }
168
169 uint16_t raw_read_status;
170 if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) {
171 ESP_LOGD(TAG, "Not ready");
175 if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) {
176 ESP_LOGD(TAG, "Exceeded max attempts; will reinitialize");
177 this->status_set_warning();
178 }
179 return;
180 }
181
182 if (!this->write_command(SPS30_CMD_READ_MEASUREMENT)) {
183 ESP_LOGW(TAG, "Error reading status");
184 this->status_set_warning();
185 return;
186 }
187
188 this->set_timeout(50, [this]() {
189 uint16_t raw_data[20];
190 if (!this->read_data(raw_data, 20)) {
191 ESP_LOGW(TAG, "Error reading data");
192 this->status_set_warning();
193 return;
194 }
195
196 union uint32_float_t {
197 uint32_t uint32;
198 float value;
199 };
200
202 uint32_float_t pm_1_0{.uint32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])))};
203 uint32_float_t pm_2_5{.uint32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])))};
204 uint32_float_t pm_4_0{.uint32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])))};
205 uint32_float_t pm_10_0{.uint32 = (((uint32_t(raw_data[6])) << 16) | (uint32_t(raw_data[7])))};
206
208 uint32_float_t pmc_0_5{.uint32 = (((uint32_t(raw_data[8])) << 16) | (uint32_t(raw_data[9])))};
209 uint32_float_t pmc_1_0{.uint32 = (((uint32_t(raw_data[10])) << 16) | (uint32_t(raw_data[11])))};
210 uint32_float_t pmc_2_5{.uint32 = (((uint32_t(raw_data[12])) << 16) | (uint32_t(raw_data[13])))};
211 uint32_float_t pmc_4_0{.uint32 = (((uint32_t(raw_data[14])) << 16) | (uint32_t(raw_data[15])))};
212 uint32_float_t pmc_10_0{.uint32 = (((uint32_t(raw_data[16])) << 16) | (uint32_t(raw_data[17])))};
213
215 uint32_float_t pm_size{.uint32 = (((uint32_t(raw_data[18])) << 16) | (uint32_t(raw_data[19])))};
216
217 if (this->pm_1_0_sensor_ != nullptr)
218 this->pm_1_0_sensor_->publish_state(pm_1_0.value);
219 if (this->pm_2_5_sensor_ != nullptr)
220 this->pm_2_5_sensor_->publish_state(pm_2_5.value);
221 if (this->pm_4_0_sensor_ != nullptr)
222 this->pm_4_0_sensor_->publish_state(pm_4_0.value);
223 if (this->pm_10_0_sensor_ != nullptr)
224 this->pm_10_0_sensor_->publish_state(pm_10_0.value);
225
226 if (this->pmc_0_5_sensor_ != nullptr)
227 this->pmc_0_5_sensor_->publish_state(pmc_0_5.value);
228 if (this->pmc_1_0_sensor_ != nullptr)
229 this->pmc_1_0_sensor_->publish_state(pmc_1_0.value);
230 if (this->pmc_2_5_sensor_ != nullptr)
231 this->pmc_2_5_sensor_->publish_state(pmc_2_5.value);
232 if (this->pmc_4_0_sensor_ != nullptr)
233 this->pmc_4_0_sensor_->publish_state(pmc_4_0.value);
234 if (this->pmc_10_0_sensor_ != nullptr)
235 this->pmc_10_0_sensor_->publish_state(pmc_10_0.value);
236
237 if (this->pm_size_sensor_ != nullptr)
238 this->pm_size_sensor_->publish_state(pm_size.value);
239
240 this->status_clear_warning();
241 this->skipped_data_read_cycles_ = 0;
242
243 // Stop measurements and wait if we have an idle interval. If not using idle mode, let the next state just execute
244 // on next update.
245 if (this->idle_interval_.has_value()) {
246 this->stop_measurement();
247 this->next_state_ms_ = millis() + this->idle_interval_.value();
248 this->next_state_ = WAKE;
249 } else {
250 this->next_state_ms_ = millis();
251 }
252 });
253}
254
256 if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) {
257 ESP_LOGE(TAG, "Error initiating measurements");
258 return false;
259 }
260 ESP_LOGD(TAG, "Started measurements");
261
262 // Notify the state machine to wait the warm up interval before reading
263 this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
264 this->next_state_ = READ;
265 return true;
266}
267
269
271 if (!write_command(SPS30_CMD_STOP_MEASUREMENTS)) {
272 ESP_LOGE(TAG, "Error stopping measurements");
273 return false;
274 } else {
275 ESP_LOGD(TAG, "Stopped measurements");
276 // Exit the state machine if measurement is stopped.
277 this->next_state_ms_ = 0;
278 this->next_state_ = NONE;
279 }
280 return true;
281}
282
284 if (!this->write_command(SPS30_CMD_START_FAN_CLEANING)) {
285 this->status_set_warning();
286 ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_);
287 return false;
288 } else {
289 ESP_LOGD(TAG, "Fan auto clean started");
290 }
291 return true;
292}
293
294} // namespace sps30
295} // namespace esphome
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:284
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:510
bool status_has_warning() const
Definition component.h:290
void status_clear_warning()
Definition component.h:306
i2c::ErrorCode last_error_
last error code from I2C operation
bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay=0)
get data words from I2C register.
bool write_command(T i2c_register)
Write a command to the I2C device.
bool read_data(uint16_t *data, uint8_t len)
Read data words from I2C device.
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
sensor::Sensor * pm_2_5_sensor_
Definition sps30.h:57
optional< uint32_t > fan_interval_
Definition sps30.h:66
sensor::Sensor * pmc_4_0_sensor_
Definition sps30.h:63
sensor::Sensor * pmc_2_5_sensor_
Definition sps30.h:62
sensor::Sensor * pm_10_0_sensor_
Definition sps30.h:59
uint8_t skipped_data_read_cycles_
Terminating NULL character.
Definition sps30.h:39
enum esphome::sps30::SPS30Component::NextState NONE
sensor::Sensor * pm_1_0_sensor_
Definition sps30.h:56
sensor::Sensor * pm_size_sensor_
Definition sps30.h:65
void dump_config() override
Definition sps30.cpp:76
sensor::Sensor * pmc_0_5_sensor_
Definition sps30.h:60
sensor::Sensor * pm_4_0_sensor_
Definition sps30.h:58
optional< uint32_t > idle_interval_
Definition sps30.h:67
sensor::Sensor * pmc_10_0_sensor_
Definition sps30.h:64
sensor::Sensor * pmc_1_0_sensor_
Definition sps30.h:61
const char *const TAG
Definition spi.cpp:7
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
if(written< 0)
Definition helpers.h:1091
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
static void uint32_t