ESPHome 2026.10.2
Loading...
Searching...
No Matches
mk2pvrouter.cpp
Go to the documentation of this file.
1#include "mk2pvrouter.h"
2#include "esphome/core/log.h"
3#include <cstddef>
4#include <cstring>
5
6namespace esphome::mk2pvrouter {
7
8static const char *const TAG = "mk2pvrouter";
9
10constexpr uint8_t START_FRAME = 0x2;
11constexpr uint8_t END_FRAME = 0x3;
12constexpr uint8_t LINE_FEED = 0xa;
13constexpr uint8_t CARRIAGE_RETURN = 0xd;
14constexpr uint8_t TAB = 0x9;
15constexpr uint8_t MAX_ITERATIONS = 128;
16constexpr uint8_t CRC_MASK = 0x3F;
17constexpr uint8_t CRC_OFFSET = 0x20;
18
19// Extracts a TAB-delimited field from [buf_start, buf_end) into dest.
20// Returns the field length, or 0 if no TAB was found, or the (uncopied) field
21// length if it's >= max_len.
22static size_t get_field(char *dest, const char *buf_start, const char *buf_end, size_t max_len) {
23 const auto *const field_end = static_cast<const char *>(memchr(buf_start, TAB, buf_end - buf_start));
24 if (!field_end)
25 return 0;
26 const size_t len = field_end - buf_start;
27 if (len >= max_len) {
28 ESP_LOGE(TAG, "Field too long: %zu bytes (max %zu)", len, max_len);
29 return len;
30 }
31
32 memcpy(dest, buf_start, len);
33 dest[len] = '\0'; // Null-terminate
34 return len;
35}
36
37// Calculates the CRC (checksum) for a given group of characters.
38uint8_t Mk2PVRouter::calculate_crc_(const char *grp, size_t grp_len) {
39 uint8_t crc_tmp{0};
40 const auto effective_len = grp_len - CRC_SUFFIX_LEN;
41 for (size_t i = 0; i < effective_len; i++) {
42 crc_tmp += grp[i];
43 }
44 crc_tmp &= CRC_MASK;
45 crc_tmp += CRC_OFFSET;
46 return crc_tmp;
47}
48
49// Verifies the CRC of a group against its trailing CRC byte.
50bool Mk2PVRouter::check_crc_(const char *grp, const char *grp_end) {
51 const auto grp_len = grp_end - grp;
52 if (grp_len < static_cast<std::ptrdiff_t>(CRC_SUFFIX_LEN)) {
53 ESP_LOGE(TAG, "Empty or too short group");
54 return false;
55 }
56 const auto raw_crc = grp[grp_len - 1];
57
58 const auto calculated_crc = this->calculate_crc_(grp, grp_len);
59
60 if (raw_crc != calculated_crc) {
61 ESP_LOGE(TAG, "CRC mismatch: expected %d, got %d", calculated_crc, raw_crc);
62 return false;
63 }
64 return true;
65}
66
67// Validates, parses, and publishes a single tag/value group.
68void Mk2PVRouter::process_group_(const char *grp, const char *grp_end) {
69 if (!this->check_crc_(grp, grp_end))
70 return;
71
72 size_t field_len = get_field(this->tag_, grp, grp_end, MAX_TAG_SIZE);
73 if (!field_len || field_len >= MAX_TAG_SIZE) {
74 ESP_LOGE(TAG, "Invalid tag");
75 return;
76 }
77 const auto *val_start = grp + field_len + 1; // Skip tag + TAB.
78
79 field_len = get_field(this->val_, val_start, grp_end, MAX_VAL_SIZE);
80 if (!field_len || field_len >= MAX_VAL_SIZE) {
81 ESP_LOGE(TAG, "Invalid value for tag %s", this->tag_);
82 return;
83 }
84
85 this->publish_value_(this->tag_, this->val_);
86}
87
88// Reads characters until `c` is found or the internal buffer is full.
89bool Mk2PVRouter::read_chars_until_(bool drop, uint8_t c) {
90 size_t j{0};
91
92 while (this->available() > 0 && j++ < MAX_ITERATIONS) {
93 const auto received = this->read();
94 if (received < 0)
95 continue;
96 if (received == c)
97 return true;
98 if (drop)
99 continue;
100 if (this->buf_index_ >= (sizeof(this->buf_) - 1)) {
101 ESP_LOGW(TAG, "Internal buffer full");
102 this->buf_index_ = 0;
104 return false;
105 }
106 this->buf_[this->buf_index_++] = received;
107 }
108
109 return false;
110}
111
113 switch (this->state_) {
115 ESP_LOGVV(TAG, "State: WAITING_FOR_START");
116 if (this->read_chars_until_(true, START_FRAME))
118 break;
120 ESP_LOGVV(TAG, "State: START_FRAME_RECEIVED");
121 if (this->read_chars_until_(false, END_FRAME))
123 break;
125 ESP_LOGVV(TAG, "State: END_FRAME_RECEIVED -> processing");
126
127 if (this->buf_index_ == 0) {
129 break;
130 }
131
132 auto *buf_finger = this->buf_;
133 auto *buf_end = this->buf_ + this->buf_index_;
134
135 // Each group: 0xa(LF) | Tag | 0x9(TAB) | Data | 0x9(TAB) | CRC | 0xd(CR)
136 // CRC is computed over "Tag | TAB | Data | TAB".
137 while ((buf_finger = static_cast<char *>(memchr(buf_finger, LINE_FEED, buf_end - buf_finger))) != nullptr) {
138 ++buf_finger; // Skip LF to the start of the group.
139
140 auto *const grp_end = static_cast<char *>(memchr(buf_finger, CARRIAGE_RETURN, buf_end - buf_finger));
141 if (!grp_end) {
142 ESP_LOGE(TAG, "No group found");
143 break;
144 }
145
146 this->process_group_(buf_finger, grp_end);
147
148 buf_finger = grp_end; // grp_end is always < buf_end, so this stays in bounds.
149 }
150 this->buf_index_ = 0;
152 break;
153 }
154 }
155}
156
157void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
158#ifdef MK2PVROUTER_LISTENER_COUNT
159 for (auto *element : this->mk2pvrouter_listeners_) {
160 if (strcmp(tag, element->get_tag()) != 0)
161 continue;
162 element->publish_val(val);
163 }
164#endif
165}
166
167void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); }
168
169#ifdef MK2PVROUTER_LISTENER_COUNT
173#endif
174
175} // namespace esphome::mk2pvrouter
bool check_crc_(const char *grp, const char *grp_end)
static constexpr size_t CRC_SUFFIX_LEN
Definition mk2pvrouter.h:45
void process_group_(const char *grp, const char *grp_end)
void publish_value_(const char *tag, const char *val)
bool read_chars_until_(bool drop, uint8_t c)
uint8_t calculate_crc_(const char *grp, size_t grp_len)
void register_mk2pvrouter_listener(Mk2PVRouterListener *listener)
StaticVector< Mk2PVRouterListener *, MK2PVROUTER_LISTENER_COUNT > mk2pvrouter_listeners_
Definition mk2pvrouter.h:54
mopeka_std_values val[3]
constexpr uint8_t MAX_ITERATIONS
constexpr uint8_t CARRIAGE_RETURN
constexpr uint8_t START_FRAME
constexpr uint8_t CRC_OFFSET
constexpr uint8_t LINE_FEED
constexpr uint8_t TAB
constexpr uint8_t END_FRAME
constexpr uint8_t CRC_MASK
const void size_t len
Definition hal.h:64