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