ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
pmsx003.cpp
Go to the documentation of this file.
1#include "pmsx003.h"
2#include "esphome/core/log.h"
4
5namespace esphome::pmsx003 {
6
7ESPHOME_LOG_TAG(TAG, "pmsx003");
8
9static const uint8_t START_CHARACTER_1 = 0x42;
10static const uint8_t START_CHARACTER_2 = 0x4D;
11
12static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
13
14static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE =
15 0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement
16static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
17static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
18static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
19
21
23 ESP_LOGCONFIG(TAG, "PMSX003:");
24 LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_);
25 LOG_SENSOR(" ", "PM2.5STD", this->pm_2_5_std_sensor_);
26 LOG_SENSOR(" ", "PM10.0STD", this->pm_10_0_std_sensor_);
27
28 LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_);
29 LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
30 LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_);
31
32 LOG_SENSOR(" ", "PM0.3um", this->pm_particles_03um_sensor_);
33 LOG_SENSOR(" ", "PM0.5um", this->pm_particles_05um_sensor_);
34 LOG_SENSOR(" ", "PM1.0um", this->pm_particles_10um_sensor_);
35 LOG_SENSOR(" ", "PM2.5um", this->pm_particles_25um_sensor_);
36 LOG_SENSOR(" ", "PM5.0um", this->pm_particles_50um_sensor_);
37 LOG_SENSOR(" ", "PM10.0um", this->pm_particles_100um_sensor_);
38
39 LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_);
40
41 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
42 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
43
44 if (this->update_interval_ <= STABILISING_MS) {
45 ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
46 } else {
47 ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
48 }
49}
50
53
54 // Initialize sensor mode on first loop
55 if (!this->initialised_) {
56 if (this->update_interval_ > STABILISING_MS) {
57 // Long update interval: use passive mode with sleep/wake cycles
58 this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE);
59 this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
60 } else {
61 // Short/zero update interval: use active continuous mode
62 this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE);
63 }
64 this->initialised_ = true;
65 }
66
67 // If we update less often than it takes the device to stabilise, spin the fan down
68 // rather than running it constantly. It does take some time to stabilise, so we
69 // need to keep track of what state we're in.
70 if (this->update_interval_ > STABILISING_MS) {
71 switch (this->state_) {
72 case State::IDLE:
73 // Power on the sensor now so it'll be ready when we hit the update time
74 if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS))
75 return;
76
78 this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
79 this->fan_on_time_ = now;
80 return;
82 // wait for the sensor to be stable
83 if (now - this->fan_on_time_ < STABILISING_MS)
84 return;
85 // consume any command responses that are in the serial buffer
86 this->discard_available();
87 // Trigger a new read
89 this->state_ = State::WAITING;
90 break;
91 case State::WAITING:
92 // Just go ahead and read stuff
93 break;
94 }
95 }
96
97 if (now - this->last_transmission_ >= 500) {
98 // last transmission too long ago. Reset RX index.
99 this->data_index_ = 0;
100 }
101
102 const size_t avail = this->available();
103 if (avail == 0)
104 return;
105
106 this->last_transmission_ = now;
107 this->read_available(
108 [this, now](uint8_t byte) {
109 this->data_[this->data_index_] = byte;
110 auto check = this->check_byte_();
111 if (!check.has_value()) {
112 if (this->update_interval_ > STABILISING_MS || now - this->last_update_ >= this->update_interval_) {
113 this->parse_data_();
114 this->last_update_ = now;
115 }
116 this->data_index_ = 0;
117 } else if (!*check) {
118 // wrong data
119 this->data_index_ = 0;
120 } else {
121 // next byte
122 this->data_index_++;
123 }
124 },
125 avail);
126}
127
129 const uint8_t index = this->data_index_;
130 const uint8_t byte = this->data_[index];
131
132 if (index == 0 || index == 1) {
133 const uint8_t start_char = index == 0 ? START_CHARACTER_1 : START_CHARACTER_2;
134 if (byte == start_char) {
135 return true;
136 }
137
138 ESP_LOGW(TAG, "Start character %u mismatch: 0x%02X != 0x%02X", index + 1, byte, start_char);
139 return false;
140 }
141
142 if (index == 2) {
143 return true;
144 }
145
146 const uint16_t payload_length = this->get_16_bit_uint_(2);
147 if (index == 3) {
148 if (this->check_payload_length_(payload_length)) {
149 return true;
150 } else {
151 ESP_LOGW(TAG, "Payload length %u doesn't match. Are you using the correct PMSX003 type?", payload_length);
152 return false;
153 }
154 }
155
156 // start (16bit) + length (16bit) + DATA (payload_length - 16bit) + checksum (16bit)
157 const uint16_t total_size = 4 + payload_length;
158
159 if (index < total_size - 1) {
160 return true;
161 }
162
163 // checksum is without checksum bytes
164 const uint16_t checksum = sum16_bytes(this->data_, total_size - 2);
165
166 const uint16_t check = this->get_16_bit_uint_(total_size - 2);
167 if (checksum != check) {
168 ESP_LOGW(TAG, "PMSX003 checksum mismatch! 0x%02X != 0x%02X", checksum, check);
169 return false;
170 }
171
172 return {};
173}
174
175bool PMSX003Component::check_payload_length_(uint16_t payload_length) {
176 // https://avaldebe.github.io/PyPMS/sensors/Plantower/
177 switch (this->type_) {
178 case Type::PMS1003:
179 return payload_length == 28; // 2*13+2
180 case Type::PMS3003: // Data 7/8/9 not set/reserved
181 return payload_length == 20; // 2*9+2
182 case Type::PMSX003: // Data 13 not set/reserved
183 // Deprecated: Length 20 is for PMS3003 backwards compatibility
184 return payload_length == 28 || payload_length == 20; // 2*13+2
185 case Type::PMS5003S:
186 case Type::PMS5003T: // Data 13 not set/reserved
187 return payload_length == 28; // 2*13+2
188 case Type::PMS5003ST: // Data 16 not set/reserved
189 return payload_length == 36; // 2*17+2
190 case Type::PMS9003M:
191 case Type::LUFTMY:
192 return payload_length == 28; // 2*13+2
193 }
194 return false;
195}
196
197void PMSX003Component::send_command_(Command cmd, uint16_t data) {
198 uint8_t send_data[7] = {
199 START_CHARACTER_1, // Start Byte 1
200 START_CHARACTER_2, // Start Byte 2
201 static_cast<uint8_t>(cmd), // Command
202 uint8_t((data >> 8) & 0xFF), // Data 1
203 uint8_t((data >> 0) & 0xFF), // Data 2
204 0, // Verify Byte 1
205 0, // Verify Byte 2
206 };
207
208 // Calculate checksum
209 const uint16_t checksum = sum16_bytes(send_data, 5);
210 send_data[5] = (checksum >> 8) & 0xFF; // Verify Byte 1
211 send_data[6] = (checksum >> 0) & 0xFF; // Verify Byte 2
212
213 for (auto send_byte : send_data) {
214 this->write_byte(send_byte);
215 }
216}
217
219 // PM mass concentrations
220 const uint16_t pm_1_0_std_concentration = this->get_16_bit_uint_(4);
221 const uint16_t pm_2_5_std_concentration = this->get_16_bit_uint_(6);
222 const uint16_t pm_10_0_std_concentration = this->get_16_bit_uint_(8);
223
224 if (this->pm_1_0_std_sensor_ != nullptr)
225 this->pm_1_0_std_sensor_->publish_state(pm_1_0_std_concentration);
226 if (this->pm_2_5_std_sensor_ != nullptr)
227 this->pm_2_5_std_sensor_->publish_state(pm_2_5_std_concentration);
228 if (this->pm_10_0_std_sensor_ != nullptr)
229 this->pm_10_0_std_sensor_->publish_state(pm_10_0_std_concentration);
230 ESP_LOGD(TAG,
231 "Got PM1.0 Standard Concentration: %u µg/m³, PM2.5 Standard Concentration %u µg/m³, PM10.0 Standard "
232 "Concentration: %u µg/m³",
233 pm_1_0_std_concentration, pm_2_5_std_concentration, pm_10_0_std_concentration);
234
235 // The Luftmy LD10, LD11, LD13, LD15 and LD16 report no atmospheric PM, so their particle counts start at byte 10.
236 // All PMS types have atmospheric PM at bytes 10-15, so their particle counts start at byte 16.
237 const bool is_luftmy = this->type_ == Type::LUFTMY;
238 const uint8_t pc_offset = is_luftmy ? 10 : 16;
239
240 if (!is_luftmy) {
241 const uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10);
242 const uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12);
243 const uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14);
244
245 ESP_LOGD(TAG, "Got PM1.0 Concentration: %u µg/m³, PM2.5 Concentration %u µg/m³, PM10.0 Concentration: %u µg/m³",
246 pm_1_0_concentration, pm_2_5_concentration, pm_10_0_concentration);
247
248 if (this->pm_1_0_sensor_ != nullptr)
249 this->pm_1_0_sensor_->publish_state(pm_1_0_concentration);
250 if (this->pm_2_5_sensor_ != nullptr)
251 this->pm_2_5_sensor_->publish_state(pm_2_5_concentration);
252 if (this->pm_10_0_sensor_ != nullptr)
253 this->pm_10_0_sensor_->publish_state(pm_10_0_concentration);
254 }
255
256 // Particle counts at pc_offset: >0.3, >0.5, >1.0, >2.5µm
257 const uint16_t pm_particles_03um = this->get_16_bit_uint_(pc_offset);
258 const uint16_t pm_particles_05um = this->get_16_bit_uint_(pc_offset + 2);
259 const uint16_t pm_particles_10um = this->get_16_bit_uint_(pc_offset + 4);
260 const uint16_t pm_particles_25um = this->get_16_bit_uint_(pc_offset + 6);
261
262 if (this->pm_particles_03um_sensor_ != nullptr)
263 this->pm_particles_03um_sensor_->publish_state(pm_particles_03um);
264 if (this->pm_particles_05um_sensor_ != nullptr)
265 this->pm_particles_05um_sensor_->publish_state(pm_particles_05um);
266 if (this->pm_particles_10um_sensor_ != nullptr)
267 this->pm_particles_10um_sensor_->publish_state(pm_particles_10um);
268 if (this->pm_particles_25um_sensor_ != nullptr)
269 this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
270 ESP_LOGD(TAG,
271 "Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "
272 "PM2.5 Particles: %u Count/0.1L",
273 pm_particles_03um, pm_particles_05um, pm_particles_10um, pm_particles_25um);
274
275 // PMS5003T uses pc_offset+8 and pc_offset+10 for temperature/humidity, not particle counts
276 if (this->type_ != Type::PMS5003T) {
277 const uint16_t pm_particles_50um = this->get_16_bit_uint_(pc_offset + 8);
278 const uint16_t pm_particles_100um = this->get_16_bit_uint_(pc_offset + 10);
279
280 ESP_LOGD(TAG, "Got PM5.0 Particles: %u Count/0.1L, PM10.0 Particles: %u Count/0.1L", pm_particles_50um,
281 pm_particles_100um);
282
283 if (this->pm_particles_50um_sensor_ != nullptr)
284 this->pm_particles_50um_sensor_->publish_state(pm_particles_50um);
285 if (this->pm_particles_100um_sensor_ != nullptr)
286 this->pm_particles_100um_sensor_->publish_state(pm_particles_100um);
287 }
288
289 // Formaldehyde
290 if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) {
291 const uint16_t formaldehyde = this->get_16_bit_uint_(28);
292
293 ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde);
294
295 if (this->formaldehyde_sensor_ != nullptr)
296 this->formaldehyde_sensor_->publish_state(formaldehyde);
297 }
298
299 // Temperature and Humidity
300 if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) {
301 const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30;
302
303 const float temperature = static_cast<int16_t>(this->get_16_bit_uint_(temperature_offset)) / 10.0f;
304 const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f;
305
306 ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%%", temperature, humidity);
307
308 if (this->temperature_sensor_ != nullptr)
309 this->temperature_sensor_->publish_state(temperature);
310 if (this->humidity_sensor_ != nullptr)
311 this->humidity_sensor_->publish_state(humidity);
312 }
313
314 // Firmware Version and Error Code
315 if (this->type_ == Type::PMS1003 || this->type_ == Type::PMS5003ST || this->type_ == Type::PMS9003M) {
316 const uint8_t firmware_error_code_offset = (this->type_ == Type::PMS5003ST) ? 36 : 28;
317 const uint8_t firmware_version = this->data_[firmware_error_code_offset];
318 const uint8_t error_code = this->data_[firmware_error_code_offset + 1];
319
320 ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code);
321 }
322
323 // Spin down the sensor again if we aren't going to need it until more time has
324 // passed than it takes to stabilise
325 if (this->update_interval_ > STABILISING_MS) {
326 this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP);
327 this->state_ = State::IDLE;
328 }
329
330 this->status_clear_warning();
331}
332
333} // namespace esphome::pmsx003
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.
void status_clear_warning()
Definition component.h:296
sensor::Sensor * pm_10_0_sensor_
Definition pmsx003.h:105
sensor::Sensor * pm_1_0_std_sensor_
Definition pmsx003.h:98
sensor::Sensor * pm_particles_100um_sensor_
Definition pmsx003.h:113
sensor::Sensor * pm_2_5_sensor_
Definition pmsx003.h:104
sensor::Sensor * pm_particles_03um_sensor_
Definition pmsx003.h:108
sensor::Sensor * formaldehyde_sensor_
Definition pmsx003.h:116
sensor::Sensor * pm_2_5_std_sensor_
Definition pmsx003.h:99
sensor::Sensor * humidity_sensor_
Definition pmsx003.h:120
sensor::Sensor * pm_1_0_sensor_
Definition pmsx003.h:103
sensor::Sensor * pm_particles_10um_sensor_
Definition pmsx003.h:110
sensor::Sensor * pm_10_0_std_sensor_
Definition pmsx003.h:100
sensor::Sensor * pm_particles_25um_sensor_
Definition pmsx003.h:111
sensor::Sensor * pm_particles_05um_sensor_
Definition pmsx003.h:109
sensor::Sensor * temperature_sensor_
Definition pmsx003.h:119
uint16_t get_16_bit_uint_(uint8_t start_index) const
Definition pmsx003.h:83
void send_command_(Command cmd, uint16_t data)
Definition pmsx003.cpp:197
sensor::Sensor * pm_particles_50um_sensor_
Definition pmsx003.h:112
bool check_payload_length_(uint16_t payload_length)
Definition pmsx003.cpp:175
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
void discard_available()
Read and drop every byte waiting in the receive buffer.
Definition uart.cpp:36
void write_byte(uint8_t data)
Definition uart.h:22
bool read_available(F &&on_byte)
Same as read_available_chunks(), calling on_byte(uint8_t) once per byte.
Definition uart.h:77
ESPHOME_LOG_TAG(TAG, "pmsx003")
Application App
Global storage of Application pointer - only one Application can exist.
constexpr uint16_t sum16_bytes(const uint8_t *data, size_t len, uint16_t sum=0)
Add up the bytes of data with size len, truncated to 16 bits.
Definition helpers.h:899
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12