mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef73dbd15 | ||
|
|
e426e33427 | ||
|
|
0628e4e492 | ||
|
|
d5c6b01e19 | ||
|
|
abbf3a6960 | ||
|
|
a97d4241bd | ||
|
|
f1031bc64d | ||
|
|
57e14c1b54 | ||
|
|
71064efc25 | ||
|
|
f5ea2d9f01 | ||
|
|
11a228569b | ||
|
|
5d69784adc | ||
|
|
3b4deefd4c | ||
|
|
22a0440348 | ||
|
|
46379d8cbc | ||
|
|
f00328f45d | ||
|
|
ddfcc0bc8b | ||
|
|
61bd5e2657 | ||
|
|
7a1dc1279d | ||
|
|
04165e289f | ||
|
|
965b26dab6 | ||
|
|
a7e5a2a621 | ||
|
|
8824610f82 |
No files matched your search
@@ -216,9 +216,10 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
|
||||
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "dfrobot_sen0465_uart.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
using dfrobot_sen0465_base::FRAME_HEAD;
|
||||
using dfrobot_sen0465_base::FRAME_SIZE;
|
||||
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
|
||||
|
||||
@@ -18,30 +15,21 @@ void DFRobotSEN0465UARTComponent::setup() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0465UARTComponent::loop() {
|
||||
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
|
||||
uint8_t byte;
|
||||
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
|
||||
this->read_byte(&byte);
|
||||
const size_t available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
|
||||
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
|
||||
return;
|
||||
this->rx_count_ += len;
|
||||
if (this->rx_count_ < FRAME_SIZE)
|
||||
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
|
||||
const uint8_t *frame = nullptr;
|
||||
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
|
||||
if (frame == nullptr)
|
||||
return;
|
||||
|
||||
this->rx_count_ = 0;
|
||||
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
|
||||
this->disable_loop();
|
||||
this->handle_response_(this->rx_buffer_);
|
||||
this->handle_response_(frame);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
|
||||
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
|
||||
this->discard_available();
|
||||
this->rx_count_ = 0;
|
||||
this->reader_.reset();
|
||||
this->write_array(frame, FRAME_SIZE);
|
||||
return true;
|
||||
}
|
||||
@@ -50,7 +38,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
|
||||
this->enable_loop();
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
|
||||
this->disable_loop();
|
||||
this->rx_count_ = 0;
|
||||
this->reader_.reset();
|
||||
this->handle_no_response_();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
@@ -17,8 +18,7 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
|
||||
bool write_frame(const uint8_t *frame) override;
|
||||
void read_response() override;
|
||||
|
||||
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
|
||||
uint8_t rx_count_{0};
|
||||
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_uart
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "ds1603l.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -10,35 +8,28 @@ namespace esphome::ds1603l {
|
||||
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
|
||||
|
||||
void DS1603L::loop() {
|
||||
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
|
||||
uint8_t byte;
|
||||
while (this->available() > 0 && this->read_byte(&byte)) {
|
||||
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
continue;
|
||||
this->read_available([this](uint8_t byte) {
|
||||
const uint8_t *frame = this->reader_.feed(byte);
|
||||
if (frame == nullptr) {
|
||||
if (this->reader_.length() == 0) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_count_++] = byte;
|
||||
if (this->rx_count_ < FRAME_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->parse_data_()) {
|
||||
this->rx_count_ = 0;
|
||||
} else {
|
||||
if (!this->parse_data_(frame)) {
|
||||
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
|
||||
this->resync_();
|
||||
this->reader_.resync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
|
||||
|
||||
bool DS1603L::parse_data_() {
|
||||
uint8_t header = this->rx_buffer_[0];
|
||||
uint8_t data_h = this->rx_buffer_[1];
|
||||
uint8_t data_l = this->rx_buffer_[2];
|
||||
uint8_t checksum = this->rx_buffer_[3];
|
||||
bool DS1603L::parse_data_(const uint8_t *frame) {
|
||||
uint8_t header = frame[0];
|
||||
uint8_t data_h = frame[1];
|
||||
uint8_t data_l = frame[2];
|
||||
uint8_t checksum = frame[3];
|
||||
|
||||
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
|
||||
|
||||
@@ -53,16 +44,4 @@ bool DS1603L::parse_data_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void DS1603L::resync_() {
|
||||
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
|
||||
size_t start = 1;
|
||||
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
|
||||
start++;
|
||||
}
|
||||
this->rx_count_ -= start;
|
||||
if (this->rx_count_ > 0) {
|
||||
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::ds1603l {
|
||||
|
||||
@@ -18,13 +19,10 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
|
||||
static constexpr uint8_t HEADER_BYTE = 0xFF;
|
||||
static constexpr size_t FRAME_SIZE = 4;
|
||||
|
||||
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_();
|
||||
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
|
||||
void resync_();
|
||||
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_(const uint8_t *frame);
|
||||
|
||||
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
|
||||
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
|
||||
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -1,33 +1,27 @@
|
||||
#include "sm300d2.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sm300d2 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sm300d2");
|
||||
static const uint8_t SM300D2_RESPONSE_LENGTH = 17;
|
||||
static constexpr uint8_t SM300D2_RESPONSE_LENGTH = 17;
|
||||
|
||||
void SM300D2Sensor::update() {
|
||||
uint8_t response[SM300D2_RESPONSE_LENGTH];
|
||||
uint8_t peeked;
|
||||
// Takes the first frame already received; only one cut off at the end of the buffer is waited for
|
||||
FrameReader<SM300D2_RESPONSE_LENGTH, 0x3C, 0x02> reader;
|
||||
const uint8_t *response = nullptr;
|
||||
this->read_available<1>([&](uint8_t c) { return (response = reader.feed(c)) == nullptr; });
|
||||
for (uint8_t c; response == nullptr && reader.length() > 0 && this->read_byte(&c);)
|
||||
response = reader.feed(c);
|
||||
|
||||
while (this->available() > 0 && this->peek_byte(&peeked) && peeked != 0x3C)
|
||||
this->read();
|
||||
|
||||
bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH);
|
||||
|
||||
if (!read_success) {
|
||||
if (response == nullptr) {
|
||||
ESP_LOGW(TAG, "Reading data from SM300D2 failed!");
|
||||
status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response[0] != 0x3C || response[1] != 0x02) {
|
||||
ESP_LOGW(TAG, "Invalid preamble for SM300D2 response!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t calculated_checksum = this->sm300d2_checksum_(response);
|
||||
uint16_t calculated_checksum = sum_bytes(response, SM300D2_RESPONSE_LENGTH - 1);
|
||||
// Occasionally the checksum has a +/- 0x80 offset. Negative temperatures are
|
||||
// responsible for some of these. The rest are unknown/undocumented.
|
||||
if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) &&
|
||||
@@ -83,8 +77,6 @@ void SM300D2Sensor::update() {
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
uint16_t SM300D2Sensor::sm300d2_checksum_(uint8_t *ptr) { return sum_bytes(ptr, SM300D2_RESPONSE_LENGTH - 1); }
|
||||
|
||||
void SM300D2Sensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SM300D2:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
|
||||
@@ -20,8 +20,6 @@ class SM300D2Sensor final : public PollingComponent, public uart::UARTDevice {
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
uint16_t sm300d2_checksum_(uint8_t *ptr);
|
||||
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *formaldehyde_sensor_{nullptr};
|
||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||
|
||||
@@ -44,8 +44,6 @@ static constexpr std::array<uint8_t, 7> GET_SOFTWARE_VERSION = {0xFE, 0xFE, 0x11
|
||||
static constexpr size_t FRAME_DEVICE_ID_INDEX = 2;
|
||||
static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
|
||||
static constexpr size_t FRAME_STOP_INDEX = 31;
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2_WITH_ID = 0x96;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
@@ -120,9 +118,8 @@ static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t exp
|
||||
|
||||
static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
|
||||
const uint8_t sum = sum_bytes(data, FRAME_CHECKSUM_INDEX);
|
||||
return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
|
||||
check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
|
||||
check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
|
||||
// FrameReader already matched the start bytes
|
||||
return check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
|
||||
check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
|
||||
check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
|
||||
check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
|
||||
@@ -215,7 +212,7 @@ static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
|
||||
|
||||
void UFM01Component::flush_rx_() {
|
||||
this->discard_available();
|
||||
this->read_index_ = 0;
|
||||
this->active_reader_.reset();
|
||||
}
|
||||
|
||||
void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
|
||||
@@ -423,7 +420,7 @@ bool UFM01Component::service_software_version_(uint32_t now) {
|
||||
}
|
||||
#endif // USE_UFM01_SOFTWARE_VERSION
|
||||
|
||||
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
|
||||
void UFM01Component::on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now) {
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
this->publish_meter_id_from_frame_(data);
|
||||
#endif
|
||||
@@ -461,40 +458,26 @@ void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
|
||||
bool UFM01Component::process_active_stream_(uint32_t now) {
|
||||
bool got_valid_frame = false;
|
||||
|
||||
while (this->available()) {
|
||||
if (!this->read_byte(&this->data_[this->read_index_])) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
|
||||
(this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
|
||||
ESP_LOGD(TAG, "not start of data at %" PRId32 " (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
|
||||
continue;
|
||||
|
||||
if (validate_active_frame(this->data_)) {
|
||||
this->on_active_frame_(this->data_, now);
|
||||
this->read_index_ = 0;
|
||||
got_valid_frame = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
log_hex(this->data_, sizeof(this->data_));
|
||||
ESP_LOGW(TAG, "unable to read data");
|
||||
for (int32_t i = 2;
|
||||
i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
|
||||
if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
|
||||
for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
|
||||
this->data_[j - i] = this->data_[j];
|
||||
this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
|
||||
const bool read_ok = this->read_available([this, now, &got_valid_frame](uint8_t byte) {
|
||||
const uint8_t *frame = this->active_reader_.feed(byte);
|
||||
if (frame == nullptr) {
|
||||
if (this->active_reader_.length() == 0) {
|
||||
ESP_LOGD(TAG, "not start of data (is 0x%02X)", byte);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
|
||||
this->read_index_ = 0;
|
||||
if (validate_active_frame(frame)) {
|
||||
this->on_active_frame_(frame, now);
|
||||
got_valid_frame = true;
|
||||
return;
|
||||
}
|
||||
log_hex(frame, FRAME_SIZE);
|
||||
ESP_LOGW(TAG, "unable to read data");
|
||||
this->active_reader_.resync();
|
||||
});
|
||||
if (!read_ok) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
this->active_reader_.reset();
|
||||
}
|
||||
|
||||
return got_valid_frame;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
@@ -25,6 +26,8 @@ class TestableUFM01;
|
||||
} // namespace testing
|
||||
|
||||
static constexpr size_t FRAME_SIZE = 32;
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr size_t PASSIVE_FRAME_SIZE = 23;
|
||||
static constexpr size_t PASSIVE_FRAME_WITH_ID_SIZE = 39;
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
@@ -108,7 +111,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
bool consume_ack_();
|
||||
void flush_rx_();
|
||||
bool process_active_stream_(uint32_t now);
|
||||
void on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now);
|
||||
void on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now);
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
void publish_meter_id_from_frame_(const uint8_t data[FRAME_SIZE]);
|
||||
#endif
|
||||
@@ -145,7 +148,6 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#ifdef USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
|
||||
uint32_t pending_clear_start_ms_{0};
|
||||
#endif
|
||||
int32_t read_index_{0};
|
||||
|
||||
OperatingMode operating_mode_{OperatingMode::STARTUP};
|
||||
StartupPhase startup_phase_{StartupPhase::WAIT};
|
||||
@@ -170,7 +172,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#endif
|
||||
|
||||
uint8_t passive_frame_[PASSIVE_FRAME_MAX_SIZE];
|
||||
uint8_t data_[FRAME_SIZE];
|
||||
FrameReader<FRAME_SIZE, FRAME_START_BYTE_1, FRAME_START_BYTE_2> active_reader_;
|
||||
|
||||
friend class testing::TestableUFM01;
|
||||
};
|
||||
|
||||
@@ -4,70 +4,38 @@
|
||||
|
||||
namespace esphome::wts01 {
|
||||
|
||||
constexpr uint8_t HEADER_1 = 0x55;
|
||||
constexpr uint8_t HEADER_2 = 0x01;
|
||||
constexpr uint8_t HEADER_3 = 0x01;
|
||||
constexpr uint8_t HEADER_4 = 0x04;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "wts01");
|
||||
|
||||
void WTS01Sensor::loop() {
|
||||
this->read_available([this](uint8_t c) { this->handle_char_(c); });
|
||||
this->read_available([this](uint8_t c) {
|
||||
if (const uint8_t *packet = this->reader_.feed(c)) {
|
||||
this->process_packet_(packet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void WTS01Sensor::dump_config() { LOG_SENSOR("", "WTS01 Sensor", this); }
|
||||
|
||||
void WTS01Sensor::handle_char_(uint8_t c) {
|
||||
// State machine for processing the header. Reset if something doesn't match.
|
||||
if (this->buffer_pos_ == 0 && c != HEADER_1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 1 && c != HEADER_2) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 2 && c != HEADER_3) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 3 && c != HEADER_4) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add byte to buffer
|
||||
this->buffer_[this->buffer_pos_++] = c;
|
||||
|
||||
// Process complete packet
|
||||
if (this->buffer_pos_ >= PACKET_SIZE) {
|
||||
this->process_packet_();
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void WTS01Sensor::process_packet_() {
|
||||
void WTS01Sensor::process_packet_(const uint8_t *packet) {
|
||||
// Based on Tasmota implementation
|
||||
// Format: 55 01 01 04 01 11 16 12 95
|
||||
// header T Td Ck - T = Temperature, Td = Temperature decimal, Ck = Checksum
|
||||
const uint8_t calculated_checksum = sum_bytes(this->buffer_, PACKET_SIZE - 1);
|
||||
const uint8_t calculated_checksum = sum_bytes(packet, PACKET_SIZE - 1);
|
||||
|
||||
uint8_t received_checksum = this->buffer_[PACKET_SIZE - 1];
|
||||
uint8_t received_checksum = packet[PACKET_SIZE - 1];
|
||||
if (calculated_checksum != received_checksum) {
|
||||
ESP_LOGW(TAG, "WTS01 Checksum doesn't match: 0x%02X != 0x%02X", received_checksum, calculated_checksum);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract temperature value
|
||||
const uint8_t raw = this->buffer_[6];
|
||||
const uint8_t raw = packet[6];
|
||||
|
||||
// WTS01 encodes sign in bit 7, magnitude in bits 0-6
|
||||
const bool negative = (raw & 0x80) != 0;
|
||||
const uint8_t magnitude = raw & 0x7F;
|
||||
|
||||
const float decimal = static_cast<float>(this->buffer_[7]) / 100.0f;
|
||||
const float decimal = static_cast<float>(packet[7]) / 100.0f;
|
||||
|
||||
float temperature = static_cast<float>(magnitude) + decimal;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -14,11 +15,9 @@ class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
uint8_t buffer_[PACKET_SIZE];
|
||||
uint8_t buffer_pos_{0};
|
||||
void process_packet_(const uint8_t *packet);
|
||||
|
||||
void handle_char_(uint8_t c);
|
||||
void process_packet_();
|
||||
FrameReader<PACKET_SIZE, 0x55, 0x01, 0x01, 0x04> reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::wts01
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Collects fixed length binary frames of N bytes that start with the header bytes HEADER...; the binary twin of
|
||||
/// LineReader. Bytes before a header are skipped; checksums are left to the caller.
|
||||
template<size_t N, uint8_t... HEADER> class FrameReader {
|
||||
using pos_t = std::conditional_t<(N <= 0xFF), uint8_t, uint16_t>;
|
||||
static constexpr size_t HEADER_LEN = sizeof...(HEADER);
|
||||
static_assert(HEADER_LEN >= 1 && HEADER_LEN <= 8, "FrameReader takes 1 to 8 header bytes"); // bounds replay_() depth
|
||||
static_assert(HEADER_LEN < N && N <= 0xFFFF, "FrameReader size out of range");
|
||||
|
||||
public:
|
||||
/// Feed one byte; returns the completed frame, header included, valid until the next call, or nullptr.
|
||||
const uint8_t *feed(uint8_t c) {
|
||||
pos_t pos = this->pos_;
|
||||
if (pos < HEADER_LEN && c != header_byte(pos)) {
|
||||
// A later byte of the broken header, or c, may start the real one
|
||||
this->buf_[pos] = c;
|
||||
this->replay_(pos);
|
||||
return nullptr;
|
||||
}
|
||||
this->buf_[pos++] = c;
|
||||
if (pos < N) {
|
||||
this->pos_ = pos;
|
||||
return nullptr;
|
||||
}
|
||||
this->pos_ = 0;
|
||||
return this->buf_;
|
||||
}
|
||||
|
||||
/// Number of bytes of the partial frame; 0 after a frame is returned and while looking for a header.
|
||||
size_t length() const { return this->pos_; }
|
||||
|
||||
/// Forget the partial frame and look for a header from the next byte.
|
||||
void reset() { this->pos_ = 0; }
|
||||
|
||||
/// For a frame the caller rejected (bad checksum): drop its first byte and realign on a header in the rest.
|
||||
/// Call only right after feed() returned that frame; use reset() otherwise.
|
||||
void resync() { this->replay_(N - 1); }
|
||||
|
||||
protected:
|
||||
// A fold, not an array, so no header table lands in RAM on ESP8266
|
||||
static constexpr uint8_t header_byte(size_t i) {
|
||||
size_t j = 0;
|
||||
uint8_t r = 0;
|
||||
((r = (j++ == i) ? HEADER : r), ...);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Start over and feed buf_[1..last] again; never completes a frame as last < N
|
||||
void replay_(size_t last) {
|
||||
this->pos_ = 0;
|
||||
for (size_t i = 1; i <= last; i++) {
|
||||
this->feed(this->buf_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf_[N];
|
||||
pos_t pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,143 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Frames = std::vector<Bytes>;
|
||||
|
||||
// Feeds bytes and collects every completed frame.
|
||||
template<size_t N, uint8_t... H> static Frames feed_all(FrameReader<N, H...> &reader, const Bytes &input) {
|
||||
Frames frames;
|
||||
for (uint8_t c : input) {
|
||||
const uint8_t *frame = reader.feed(c);
|
||||
if (frame != nullptr) {
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
frames.emplace_back(frame, frame + N);
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
static_assert(sizeof(FrameReader<4, 0xFF>) == 5);
|
||||
static_assert(sizeof(FrameReader<9, 0x55, 0x01, 0x01, 0x04>) == 10);
|
||||
static_assert(sizeof(FrameReader<0xFF, 0xAA>) == 0x100);
|
||||
static_assert(sizeof(FrameReader<0x100, 0xAA>) == 0x102);
|
||||
|
||||
TEST(FrameReaderTest, SingleByteHeader) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0x01, 0x02, 0x02, 0xFF, 0x03, 0x04, 0x06}),
|
||||
(Frames{{0xFF, 0x01, 0x02, 0x02}, {0xFF, 0x03, 0x04, 0x06}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SkipsBytesBeforeHeader) {
|
||||
FrameReader<3, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x01, 0x02, 0xFF, 0x10, 0x20}), (Frames{{0xFF, 0x10, 0x20}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, HeaderByteInsidePayloadIsData) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0xFF, 0xFF, 0x00}), (Frames{{0xFF, 0xFF, 0xFF, 0x00}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, LengthTracksPartialFrame) {
|
||||
FrameReader<4, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0x00);
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0xAA);
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
reader.feed(0xBB);
|
||||
reader.feed(0x01);
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
reader.reset();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0xAA, 0xBB, 0x03, 0x04}), (Frames{{0xAA, 0xBB, 0x03, 0x04}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MultiByteHeaderMismatchRestarts) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x02, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MismatchedByteCanStartHeader) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
// The second 0x55 breaks the first header and starts the real one
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, RepeatedHeaderPrefixIsKept) {
|
||||
// AA AA AA BB: the mismatch at the third byte still leaves AA AA matched
|
||||
FrameReader<4, 0xAA, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xAA, 0xAA, 0xAA, 0xBB, 0x01}), (Frames{{0xAA, 0xAA, 0xBB, 0x01}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, OverlappingHeaderPrefix) {
|
||||
// A1 B2 A1 B2: the mismatch on the last B2 keeps the trailing A1 B2 as the start of the header
|
||||
FrameReader<5, 0xA1, 0xB2, 0xA1, 0xC3> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xA1, 0xB2, 0xA1, 0xB2, 0xA1, 0xC3, 0x07}), (Frames{{0xA1, 0xB2, 0xA1, 0xC3, 0x07}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncFindsHeaderInRejectedFrame) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
// Started mid-frame: the 0xFF taken as a header was payload; the real frame starts at the second 0xFF
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x12, 0xFF, 0x01}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 2u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0x02}), (Frames{{0xFF, 0x01, 0x02, 0x02}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncWithoutHeaderStartsOver) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x01, 0x02, 0x03}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncKeepsPartialHeaderAtEnd) {
|
||||
FrameReader<5, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x00, 0x00, 0x3C}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
EXPECT_EQ(feed_all(reader, {0x32, 0x01, 0x02, 0x03}), (Frames{{0x3C, 0x32, 0x01, 0x02, 0x03}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncMultiByteHeader) {
|
||||
FrameReader<6, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x3C, 0x3C, 0x32, 0x09}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
EXPECT_EQ(feed_all(reader, {0x0A, 0x0B, 0x0C}), (Frames{{0x3C, 0x32, 0x09, 0x0A, 0x0B, 0x0C}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SixteenBitIndex) {
|
||||
FrameReader<300, 0xAA, 0x55> reader;
|
||||
Bytes input{0x00, 0xAA, 0x55};
|
||||
input.resize(input.size() + 298, 0x42);
|
||||
Frames frames = feed_all(reader, input);
|
||||
ASSERT_EQ(frames.size(), 1u);
|
||||
EXPECT_EQ(frames[0][0], 0xAA);
|
||||
EXPECT_EQ(frames[0][1], 0x55);
|
||||
EXPECT_EQ(frames[0][299], 0x42);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -20,8 +20,6 @@
|
||||
|
||||
namespace esphome::ufm01::testing {
|
||||
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2_WITH_ID = 0x96;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
@@ -131,7 +129,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
|
||||
StartupPhase startup_phase() const { return this->startup_phase_; }
|
||||
|
||||
int32_t read_index() const { return this->read_index_; }
|
||||
int32_t read_index() const { return this->active_reader_.length(); }
|
||||
|
||||
uint8_t passive_index() const { return this->passive_index_; }
|
||||
|
||||
@@ -165,7 +163,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
this->operating_mode_ = OperatingMode::ACTIVE_STREAM;
|
||||
this->last_valid_frame_ms_ = now();
|
||||
this->passive_read_pending_ = false;
|
||||
this->read_index_ = 0;
|
||||
this->active_reader_.reset();
|
||||
}
|
||||
|
||||
void begin_active_wait_frame() {
|
||||
@@ -182,7 +180,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
this->read_index_ = 0;
|
||||
this->active_reader_.reset();
|
||||
this->last_valid_frame_ms_ = 0;
|
||||
this->passive_index_ = 0;
|
||||
this->passive_read_pending_ = false;
|
||||
|
||||
@@ -30,6 +30,15 @@ TEST_F(UFM01Test, InvalidActiveFrameChecksumRejected) {
|
||||
EXPECT_EQ(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, RejectedActiveFrameResyncsOnStartBytesInside) {
|
||||
// A cut off frame start makes the first 32 bytes invalid; the real frame starts inside them
|
||||
this->mock_uart_.enqueue({FRAME_START_BYTE_1, FRAME_START_BYTE_2, 0x00, 0x00});
|
||||
this->mock_uart_.enqueue(make_active_frame());
|
||||
|
||||
EXPECT_TRUE(this->ufm01_.process_active_stream());
|
||||
EXPECT_EQ(this->ufm01_.read_index(), 0);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, ValidPassiveFrameReadSuccess) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue(frame);
|
||||
|
||||
Reference in new issue
Block a user