ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
dfrobot_sen0465_base.cpp
Go to the documentation of this file.
2#include "esphome/core/log.h"
3
4#include <cmath>
5
7
8ESPHOME_LOG_TAG(TAG, "dfrobot_sen0465");
9
10// Protocol from https://github.com/DFRobot/DFRobot_MultiGasSensor
11// Request: FF 01 command argument 00 00 00 00 checksum
12// Response: FF command conc_high conc_low gas_type decimals temp_high temp_low checksum
13static constexpr uint8_t FRAME_ADDRESS = 0x01;
14
15static constexpr uint8_t NO_COMMAND = 0x00;
16static constexpr uint8_t CMD_CHANGE_ACQUIRE_MODE = 0x78;
17static constexpr uint8_t CMD_GET_ALL_DATA = 0x88;
18static constexpr uint8_t CMD_SET_THRESHOLD_ALARM = 0x89;
19static constexpr uint8_t ACQUIRE_MODE_PASSIVE = 0x04;
20static constexpr uint8_t ALARM_SWITCH_OFF = 0x00;
21static constexpr uint8_t ALARM_SWITCH_ON = 0x01;
22static constexpr uint8_t ALARM_LOW_THRESHOLD = 0x00;
23static constexpr uint8_t ALARM_HIGH_THRESHOLD = 0x01;
24// Byte 2 of the answer to a setting command is 1 when the sensor accepted it
25static constexpr uint8_t RESULT_OK = 0x01;
26
27// The DFRobot examples wait this long after changing the acquire mode
28static constexpr uint32_t MODE_CHANGE_DELAY_MS = 1000;
29static constexpr uint32_t RETRY_DELAY_MS = 1000;
30static constexpr uint8_t MAX_ATTEMPTS = 3;
31
32// Like FucCheckSum() in the DFRobot library, the checksum covers bytes 1 to 6 only, not byte 7
33static uint8_t frame_checksum(const uint8_t *frame) {
34 // Two's complement of the byte sum between the head and the checksum
35 return static_cast<uint8_t>(~sum16_bytes(frame + 1, FRAME_SIZE - 3) + 1);
36}
37
38static const LogString *step_name(SetupStep step) {
39 return step == SetupStep::SETUP_STEP_ACQUIRE_MODE ? LOG_STR("Passive mode") : LOG_STR("Alarm setting");
40}
41
42void DFRobotSEN0465Component::setup() {
43 // Answer only when asked, instead of sending a reading every second
45 if (!this->send_step_request_())
46 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
47}
48
49void DFRobotSEN0465Component::update() {
50 // The first reading is requested when the setup steps are done
52 return;
53 // A new request would replace the wait for the answer to the previous one
54 if (this->pending_command_ != NO_COMMAND) {
55 ESP_LOGV(TAG, "Previous request still waiting for an answer, skipping update");
56 return;
57 }
58 if (!this->send_command_(CMD_GET_ALL_DATA)) {
59 ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
60 this->status_set_warning();
61 }
62}
63
64void DFRobotSEN0465Component::dump_config() {
65 ESP_LOGCONFIG(TAG,
66 "DFRobot SEN0465 series gas sensor:\n"
67 " Gas: %s (type 0x%02X)",
68 this->gas_name_, this->gas_type_);
69#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
70 ESP_LOGCONFIG(TAG, " Temperature compensation: %s", YESNO(!this->compensation_.empty()));
71#endif
73 ESP_LOGCONFIG(TAG, " Alarm: off");
74 } else {
75 float scale = 1.0f;
76 for (uint8_t i = 0; i < this->alarm_decimals_; i++)
77 scale *= 10.0f;
78 const float threshold = this->alarm_threshold_ / scale;
79 ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
80 this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
81 this->alarm_decimals_, threshold);
82 }
83 if (this->setup_incomplete_) {
84 ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
85 }
86 if (this->is_failed()) {
87 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
88 }
89 LOG_UPDATE_INTERVAL(this);
90 LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
91 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
92}
93
94bool DFRobotSEN0465Component::send_command_(uint8_t command, uint8_t arg0, uint8_t arg1, uint8_t arg2, uint8_t arg3) {
95 uint8_t frame[FRAME_SIZE] = {FRAME_HEAD, FRAME_ADDRESS, command, arg0, arg1, arg2, arg3};
96 frame[FRAME_SIZE - 1] = frame_checksum(frame);
97 if (!this->write_frame(frame))
98 return false;
99 this->pending_command_ = command;
100 this->read_response();
101 return true;
102}
103
106 return this->send_command_(CMD_CHANGE_ACQUIRE_MODE, ACQUIRE_MODE_PASSIVE);
108 // Switch the alarm off explicitly, the sensor may still have one from an earlier configuration
109 return this->send_command_(CMD_SET_THRESHOLD_ALARM, ALARM_SWITCH_OFF);
110 }
111 return this->send_command_(
112 CMD_SET_THRESHOLD_ALARM, ALARM_SWITCH_ON, static_cast<uint8_t>(this->alarm_threshold_ >> 8),
113 static_cast<uint8_t>(this->alarm_threshold_),
114 this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? ALARM_HIGH_THRESHOLD : ALARM_LOW_THRESHOLD);
115}
116
118 if (!this->send_step_request_())
119 this->step_failed_(LOG_STR("request not sent"));
120}
121
122void DFRobotSEN0465Component::step_failed_(const LogString *reason) {
123 this->attempts_++;
124 ESP_LOGW(TAG, "%s failed: %s (attempt %u of %u)", LOG_STR_ARG(step_name(this->step_)), LOG_STR_ARG(reason),
125 this->attempts_, MAX_ATTEMPTS);
126 if (this->attempts_ < MAX_ATTEMPTS) {
127 this->set_timeout(STEP_TIMEOUT_ID, RETRY_DELAY_MS, [this]() { this->retry_step_request_(); });
128 return;
129 }
130 // Carry on: readings may still work, but the warning stays because the sensor is not set up as configured
131 this->setup_incomplete_ = true;
132 this->status_set_warning();
133 this->next_step_();
134}
135
137 this->attempts_ = 0;
140 this->set_timeout(STEP_TIMEOUT_ID, MODE_CHANGE_DELAY_MS, [this]() { this->retry_step_request_(); });
141 return;
142 }
144 this->update();
145}
146
148 this->pending_command_ = NO_COMMAND;
149 if (this->step_ != SetupStep::SETUP_STEP_DONE) {
150 this->step_failed_(LOG_STR("no response"));
151 return;
152 }
153 ESP_LOGW(TAG, "No response from the sensor");
154 this->fail_reading_();
155}
156
158 // A frozen gas reading would hide a rising concentration, so the entities go unknown
159 if (this->concentration_sensor_ != nullptr)
160 this->concentration_sensor_->publish_state(NAN);
161 if (this->temperature_sensor_ != nullptr)
162 this->temperature_sensor_->publish_state(NAN);
163 this->status_set_warning();
164}
165
167 // Reject a late answer to an earlier request or a reading sent before passive mode was set
168 const bool expected = frame[0] == FRAME_HEAD && frame[1] == this->pending_command_;
169 this->pending_command_ = NO_COMMAND;
170
171 if (this->step_ == SetupStep::SETUP_STEP_DONE) {
172 if (!expected) {
173 char hex[format_hex_pretty_size(FRAME_SIZE)];
174 ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex, frame, FRAME_SIZE));
175 this->fail_reading_();
176 return;
177 }
178 this->handle_data_(frame);
179 return;
180 }
181
182 ESP_LOGD_HEX(TAG, frame, FRAME_SIZE, FRAME_SIZE, "%s response: %s", LOG_STR_ARG(step_name(this->step_)));
183 if (!expected) {
184 this->step_failed_(LOG_STR("unexpected response"));
185 return;
186 }
187 // Like the DFRobot library, the answer to the mode change is accepted without a checksum check
188 if (this->step_ == SetupStep::SETUP_STEP_ALARM && frame[FRAME_SIZE - 1] != frame_checksum(frame)) {
189 this->step_failed_(LOG_STR("invalid checksum"));
190 return;
191 }
192 if (frame[2] != RESULT_OK) {
193 this->step_failed_(LOG_STR("not confirmed"));
194 return;
195 }
196 ESP_LOGD(TAG, "%s confirmed", LOG_STR_ARG(step_name(this->step_)));
197 this->next_step_();
198}
199
200void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
201 if (frame[FRAME_SIZE - 1] != frame_checksum(frame)) {
202 char hex[format_hex_pretty_size(FRAME_SIZE)];
203 ESP_LOGW(TAG, "Invalid checksum: %s", format_hex_pretty_to(hex, frame, FRAME_SIZE));
204 this->fail_reading_();
205 return;
206 }
207
208 // On-board 10 kOhm NTC (B = 3380) read by a 10-bit ADC at 3 V, as converted by the DFRobot library
209 float temperature = NAN;
210 const uint16_t temperature_raw = encode_uint16(frame[6], frame[7]);
211 if (temperature_raw > 0 && temperature_raw < 1024) {
212 const float voltage = 3.0f * temperature_raw / 1024.0f;
213 const float resistance = voltage * 10000.0f / (3.0f - voltage);
214 temperature = 1.0f / (1.0f / 298.15f + std::log(resistance / 10000.0f) / 3380.13f) - 273.15f;
215 } else {
216 // 0 is a disconnected thermistor, 1024 and above a bad frame
217 ESP_LOGW(TAG, "Temperature reading %u is out of range", temperature_raw);
218 }
219 if (this->temperature_sensor_ != nullptr)
220 this->temperature_sensor_->publish_state(temperature);
221
222 if (frame[4] != this->gas_type_) {
223 ESP_LOGW(TAG, "Probe reports gas type 0x%02X, expected %s (0x%02X); check the configured model", frame[4],
224 this->gas_name_, this->gas_type_);
225 if (this->concentration_sensor_ != nullptr)
226 this->concentration_sensor_->publish_state(NAN);
227 this->status_set_warning();
228 return;
229 }
230
231 float concentration = encode_uint16(frame[2], frame[3]);
232 switch (frame[5]) {
233 case 0:
234 break;
235 case 1:
236 concentration *= 0.1f;
237 break;
238 case 2:
239 concentration *= 0.01f;
240 break;
241 default:
242 ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
243 if (this->concentration_sensor_ != nullptr)
244 this->concentration_sensor_->publish_state(NAN);
245 this->status_set_warning();
246 return;
247 }
248
249#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
250 if (!this->compensation_.empty())
251 concentration = this->compensate_(concentration, temperature);
252#endif
253 if (concentration < 0.0f)
254 concentration = 0.0f;
255
256 if (this->concentration_sensor_ != nullptr)
257 this->concentration_sensor_->publish_state(concentration);
258 this->status_comm_result(!std::isnan(concentration) && !std::isnan(temperature) && !this->setup_incomplete_, nullptr);
259}
260
261#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
262float DFRobotSEN0465Component::compensate_(float concentration, float temperature) const {
263 for (const auto &range : this->compensation_) {
264 if (temperature > range.low && temperature <= range.high)
265 return concentration / (range.a * temperature + range.b) - (range.c * temperature + range.d);
266 }
267 ESP_LOGW(TAG, "Temperature %.1f C is outside the compensation range", temperature);
268 return NAN;
269}
270#endif
271
272} // namespace esphome::dfrobot_sen0465_base
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
bool status_comm_result(bool ok)
Clears the warning when ok, else sets it with message (default "Communication failed"); returns ok.
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
StaticVector< CompensationRange, DFROBOT_SEN0465_COMPENSATION_RANGES > compensation_
bool send_step_request_()
Sends the request of the current step.
virtual void read_response()=0
Waits for the answer to a request, then calls either handle_response_() or handle_no_response_(),...
void retry_step_request_()
Sends the request again from a scheduled callback; a failed write counts as a failed attempt.
bool setup_incomplete_
A setup step gave up, so the alarm or the mode may not be what was configured.
virtual bool write_frame(const uint8_t *frame)=0
Sends one complete request frame.
uint8_t pending_command_
Command whose answer is awaited, 0 when no request is outstanding.
bool send_command_(uint8_t command, uint8_t arg0=0, uint8_t arg1=0, uint8_t arg2=0, uint8_t arg3=0)
Sends a request and waits for its answer; returns false when the request could not be sent.
float compensate_(float concentration, float temperature) const
Returns NAN when the temperature is outside every range.
void fail_reading_()
Publish unknown on both sensors and set the warning when a reading cannot be used.
Range range
Definition msa3xx.h:0
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0465")
SetupStep
Settings sent once after boot, before the first reading.
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:498
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1626
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:1015
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12