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