ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
vevor_dtx.cpp
Go to the documentation of this file.
1#include "vevor_dtx.h"
2
3#include <algorithm>
4#include <cinttypes>
5#include <cstdlib>
6
8#include "esphome/core/log.h"
9
11
12ESPHOME_LOG_TAG(TAG, "vevor_dtx");
13
14// The five sync bytes and the two constant header bytes that start every frame, matched bit by bit
15static constexpr uint64_t SYNC_AND_HEADER = 0xAAAACACA54AA00ULL;
16static constexpr uint64_t SYNC_AND_HEADER_MASK = (1ULL << 56) - 1;
17static constexpr uint8_t HEADER_SIZE = 2;
18// Wind arrives in 1/8.333 km/h steps and gust in 1/1.25 km/h (0.8 km/h) steps; both are published in m/s
19static constexpr float WIND_SPEED_MS_PER_RAW = 1.0f / (8.333f * 3.6f);
20static constexpr float WIND_GUST_MS_PER_RAW = 1.0f / (1.25f * 3.6f);
21static constexpr float RAIN_MM_PER_RAW = 0.233f;
22
23// Wind, direction, rain and light arrive with an offset of one on each byte; 0x00 wraps to 0xFF
24static uint16_t offset_word(uint8_t high, uint8_t low) {
25 return encode_uint16(static_cast<uint8_t>(high - 1), static_cast<uint8_t>(low - 1));
26}
27
29 ESP_LOGCONFIG(TAG,
30 "Vevor DTX:\n"
31 " Bit time: %u us",
32 this->bit_time_us_);
33 if (this->station_id_ != 0) {
34 ESP_LOGCONFIG(TAG, " Station ID: 0x%04X", this->station_id_);
35 }
36#ifdef USE_SENSOR
37 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
38 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
39 LOG_SENSOR(" ", "Wind speed", this->wind_speed_sensor_);
40 LOG_SENSOR(" ", "Wind gust", this->wind_gust_sensor_);
41 LOG_SENSOR(" ", "Wind direction", this->wind_direction_sensor_);
42 LOG_SENSOR(" ", "Rain", this->rain_sensor_);
43 LOG_SENSOR(" ", "UV index", this->uv_index_sensor_);
44 LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
45 LOG_SENSOR(" ", "TX counter", this->tx_counter_sensor_);
46#endif
47#ifdef USE_TEXT_SENSOR
48 LOG_TEXT_SENSOR(" ", "Sensor ID", this->sensor_id_text_sensor_);
49#endif
50#ifdef USE_BINARY_SENSOR
51 LOG_BINARY_SENSOR(" ", "Low battery", this->low_battery_binary_sensor_);
52#endif
53}
54
55bool VevorDtxComponent::on_receive(remote_base::RemoteReceiveData data) {
56 Payload payload{};
57 if (!this->decode_timings_(data.get_raw_data(), payload)) {
58 ESP_LOGVV(TAG, "No Vevor DTX frame in %" PRId32 " timings", data.size());
59 return false;
60 }
61 return this->publish_(payload);
62}
63
64// The frame is NRZ at a fixed bit time: a high run of n bit times is n one bits. The bits are shifted
65// through a register until the sync word appears, then the payload is collected and its checksum tested.
66// A run is never treated as a gap, since a payload may hold long runs of zeros; a gap simply shifts zeros
67// through the register, and one longer than a frame is cut to a frame's worth, which has the same effect.
69 uint64_t stream = 0;
70 uint16_t collected = 0; // payload bits gathered since the sync word, 0 while still searching for it
71
72 for (const int32_t timing : timings) {
73 const uint32_t duration = std::abs(timing);
74 const uint32_t cells =
75 std::clamp<uint32_t>((duration + this->bit_time_us_ / 2) / this->bit_time_us_, 1, FRAME_BITS);
76 const uint64_t bit = timing > 0 ? 1 : 0;
77 for (uint32_t i = 0; i < cells; i++) {
78 stream = (stream << 1) | bit;
79 if (collected == 0) {
80 if ((stream & SYNC_AND_HEADER_MASK) != SYNC_AND_HEADER) {
81 continue;
82 }
83 payload[0] = uint8_t(SYNC_AND_HEADER >> 8);
84 payload[1] = uint8_t(SYNC_AND_HEADER);
85 collected = HEADER_SIZE * 8;
86 continue;
87 }
88 payload[collected / 8] = uint8_t((payload[collected / 8] << 1) | bit);
89 if (++collected < FRAME_BITS) {
90 continue;
91 }
92 // The byte after the checksum repeats the counter plus one; it rejects frames whose sum matches by chance
93 if (sum_bytes(payload.data(), CHECKSUM_INDEX) == payload[CHECKSUM_INDEX] &&
94 payload[TX_COUNTER_INDEX + 2] == uint8_t(payload[TX_COUNTER_INDEX] + 1)) {
95 return true;
96 }
97 ESP_LOGV(TAG, "Rejected frame: sum 0x%02X received 0x%02X, counter %u followed by %u",
98 sum_bytes(payload.data(), CHECKSUM_INDEX), payload[CHECKSUM_INDEX], payload[TX_COUNTER_INDEX],
99 payload[TX_COUNTER_INDEX + 2]);
100 collected = 0;
101 }
102 }
103 return false;
104}
105
107 const uint16_t sensor_id = encode_uint16(payload[2], payload[3]);
108 if (this->station_id_ != 0 && sensor_id != this->station_id_) {
109 ESP_LOGV(TAG, "Ignoring station 0x%04X", sensor_id);
110 return false;
111 }
112 const float temperature = (float(encode_uint16(payload[5], payload[6])) - 500.0f) / 10.0f;
113 const uint8_t humidity = payload[7];
114 const float wind_speed = float(offset_word(payload[8], payload[9])) * WIND_SPEED_MS_PER_RAW;
115 const float wind_gust = float(payload[10]) * WIND_GUST_MS_PER_RAW;
116 const float wind_direction = float(offset_word(payload[11], payload[12]) & 0x0FFF);
117 const float rain = float(offset_word(payload[13], payload[14])) * RAIN_MM_PER_RAW;
118 const int8_t uv_index = int8_t(payload[15] & 0x1F) - 1;
119 // The top bit of the light word selects a tenfold range
120 const uint16_t lux_raw = offset_word(payload[16], payload[17]);
121 const float illuminance = (lux_raw & 0x8000) != 0 ? float(lux_raw & 0x7FFF) * 10.0f : float(lux_raw);
122 const uint8_t tx_counter = payload[18];
123
124 ESP_LOGD(TAG,
125 "Vevor DTX id=0x%04X status=0x%02X temp=%.1fC hum=%u%% wind=%.1fm/s gust=%.1fm/s dir=%.0f "
126 "rain=%.1f uv=%d raw=0x%02X lux=%.0f raw=0x%04X tx=%u",
127 sensor_id, payload[4], temperature, humidity, wind_speed, wind_gust, wind_direction, rain, uv_index,
128 payload[15], illuminance, lux_raw, tx_counter);
129
130#ifdef USE_SENSOR
131 const auto publish = [](sensor::Sensor *sensor, float value) {
132 if (sensor != nullptr)
133 sensor->publish_state(value);
134 };
135 publish(this->temperature_sensor_, temperature);
136 publish(this->humidity_sensor_, humidity);
137 publish(this->wind_speed_sensor_, wind_speed);
138 publish(this->wind_gust_sensor_, wind_gust);
139 publish(this->wind_direction_sensor_, wind_direction);
140 publish(this->rain_sensor_, rain);
141 publish(this->uv_index_sensor_, uv_index);
142 publish(this->illuminance_sensor_, illuminance);
143 publish(this->tx_counter_sensor_, tx_counter);
144#endif
145#ifdef USE_TEXT_SENSOR
146 if (this->sensor_id_text_sensor_ != nullptr) {
147 char text[7];
148 snprintf(text, sizeof(text), "0x%04X", sensor_id);
149 this->sensor_id_text_sensor_->publish_state(text);
150 }
151#endif
152#ifdef USE_BINARY_SENSOR
153 if (this->low_battery_binary_sensor_ != nullptr)
154 this->low_battery_binary_sensor_->publish_state((payload[4] & 0x80) != 0);
155#endif
156 return true;
157}
158
159} // namespace esphome::vevor_dtx
virtual void dump_config()
Base-class for all sensors.
Definition sensor.h:47
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
bool decode_timings_(const remote_base::RawTimings &timings, Payload &payload) const
Definition vevor_dtx.cpp:68
bool publish_(const Payload &payload)
static constexpr uint8_t TX_COUNTER_INDEX
Definition vevor_dtx.h:49
static constexpr uint8_t CHECKSUM_INDEX
Definition vevor_dtx.h:48
static constexpr uint16_t FRAME_BITS
Definition vevor_dtx.h:52
std::array< uint8_t, PAYLOAD_SIZE > Payload
Definition vevor_dtx.h:54
uint8_t duration
Definition msa3xx.h:0
std::span< const uint8_t > data
std::vector< int32_t > RawTimings
Definition remote_base.h:20
ESPHOME_LOG_TAG(TAG, "vevor_dtx")
constexpr uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t sum=0)
Add up the bytes of data with size len, truncated to 8 bits: the checksum many serial and IR protocol...
Definition helpers.h:911
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