ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ld2460.cpp
Go to the documentation of this file.
1#include "ld2460.h"
2
3#include "esphome/core/hal.h"
4
5#include <cmath>
6#include <cstring>
7#include <numbers>
8
9namespace esphome::ld2460 {
10
11ESPHOME_LOG_TAG(TAG, "ld2460");
12
13static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
14// PROGMEM keeps the patterns out of RAM on the ESP8266; they are copied out for each compare
15static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] PROGMEM = {0xFD, 0xFC, 0xFB, 0xFA};
16static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] PROGMEM = {0x04, 0x03, 0x02, 0x01};
17static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] PROGMEM = {0xF4, 0xF3, 0xF2, 0xF1};
18static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] PROGMEM = {0xF8, 0xF7, 0xF6, 0xF5};
19
20static constexpr uint8_t CMD_REPORT_DATA = 0x04;
21static constexpr uint8_t CMD_QUERY_INSTALLATION_MODE = 0x0A;
22static constexpr uint8_t ACK_FRAME_LENGTH = 12;
23static constexpr uint8_t MAX_QUERY_ATTEMPTS = 3;
24// Header, command, length (12, little endian), payload 0x01, footer
25static constexpr uint8_t QUERY_INSTALLATION_MODE_FRAME[] PROGMEM = {
26 0xFD, 0xFC, 0xFB, 0xFA, CMD_QUERY_INSTALLATION_MODE, 0x0C, 0x00, 0x01, 0x04, 0x03, 0x02, 0x01,
27};
28
29static bool matches(const uint8_t *bytes, const uint8_t *pattern) {
30 uint8_t copy[HEADER_FOOTER_SIZE];
31 progmem_memcpy(copy, pattern, sizeof(copy));
32 return std::memcmp(bytes, copy, sizeof(copy)) == 0;
33}
34
35// Header (4), command (1), length (2, little endian); the length counts the whole frame
36static constexpr uint8_t FRAME_PREFIX_SIZE = 7;
37static uint16_t frame_length(const uint8_t *frame) { return encode_uint16(frame[6], frame[5]); }
38
39static constexpr float RADIANS_TO_DEGREES = 180.0f / std::numbers::pi_v<float>;
40
41void LD2460Component::setup() { this->send_query_(); }
42
44 uint8_t frame[sizeof(QUERY_INSTALLATION_MODE_FRAME)];
45 progmem_memcpy(frame, QUERY_INSTALLATION_MODE_FRAME, sizeof(frame));
46 this->write_array(frame, sizeof(frame));
47}
48
49void LD2460Component::dump_config() {
50 ESP_LOGCONFIG(TAG,
51 "LD2460:\n"
52 " Installation mode: %s",
53 this->query_attempts_ != 0 ? LOG_STR_LITERAL("unknown")
54 : this->installation_mode_ == InstallationMode::INSTALLATION_MODE_TOP ? LOG_STR_LITERAL("top")
55 : LOG_STR_LITERAL("side"));
56#ifdef USE_SENSOR
57 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetCount", this->target_count_sensor_);
58 for (const auto &t : this->target_sensors_) {
59 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetX", t.x);
60 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetY", t.y);
61 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetDistance", t.distance);
62 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetAngle", t.angle);
63 }
64#endif
65}
66
67void LD2460Component::loop() {
68 size_t avail = this->available();
69 uint8_t buf[MAX_LINE_LENGTH];
70 while (avail > 0) {
71 size_t to_read = std::min(avail, sizeof(buf));
72 if (!this->read_array(buf, to_read)) {
73 break;
74 }
75 avail -= to_read;
76
77 for (size_t i = 0; i < to_read; i++) {
78 this->readline_(buf[i]);
79 }
80 }
81}
82
84 const uint8_t *data = this->buffer_data_;
85 if (!this->align_to_header_(DATA_FRAME_HEADER) || data[4] != CMD_REPORT_DATA) {
86 ESP_LOGW(TAG, "Invalid periodic data header");
87 return;
88 }
89 const uint8_t packet_len = this->buffer_pos_;
90 if (packet_len < 11 || (packet_len - 11) % 4 != 0) {
91 ESP_LOGW(TAG, "Periodic packet length %u is not a whole number of targets", packet_len);
92 return;
93 }
94
95 if (this->query_attempts_ != 0) {
96 if (this->query_attempts_ < MAX_QUERY_ATTEMPTS) {
97 // The module may have booted after us and missed the earlier query
98 this->query_attempts_++;
99 this->send_query_();
100 } else if (this->query_attempts_ == MAX_QUERY_ATTEMPTS) {
101 this->query_attempts_++;
102 ESP_LOGW(TAG, "No installation mode reply after %u queries; assuming side mount", MAX_QUERY_ATTEMPTS);
103 }
104 }
105
106#ifdef USE_SENSOR
107 uint8_t num_targets = std::min<uint8_t>((packet_len - 11) / 4, MAX_TARGETS);
108 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
109 TargetSensors &t = this->target_sensors_[i];
110 if (i >= num_targets) {
111 t.last_x = INT16_MIN;
112 t.last_y = INT16_MIN;
113 SAFE_PUBLISH_SENSOR_UNKNOWN(t.x);
114 SAFE_PUBLISH_SENSOR_UNKNOWN(t.y);
115 SAFE_PUBLISH_SENSOR_UNKNOWN(t.distance);
116 SAFE_PUBLISH_SENSOR_UNKNOWN(t.angle);
117 continue;
118 }
119 const uint8_t *p = &data[7 + i * 4];
120 auto raw_x = static_cast<int16_t>(encode_uint16(p[1], p[0]));
121 auto raw_y = static_cast<int16_t>(encode_uint16(p[3], p[2]));
122 if (raw_x == t.last_x && raw_y == t.last_y) {
123 continue;
124 }
125 t.last_x = raw_x;
126 t.last_y = raw_y;
127
128 // Coordinates are reported in 0.1 m steps
129 float x = raw_x * 0.1f;
130 float y = raw_y * 0.1f;
131 SAFE_PUBLISH_SENSOR(t.x, x);
132 SAFE_PUBLISH_SENSOR(t.y, y);
133 if (t.distance.has_sensor()) {
134 SAFE_PUBLISH_SENSOR(t.distance, std::sqrt(x * x + y * y));
135 }
136 if (t.angle.has_sensor()) {
137 float angle;
139 angle = std::atan2(y, x) * RADIANS_TO_DEGREES;
140 if (angle < 0.0f) {
141 angle += 360.0f;
142 }
143 } else {
144 angle = std::atan2(x, y) * RADIANS_TO_DEGREES;
145 }
146 SAFE_PUBLISH_SENSOR(t.angle, angle);
147 }
148 }
149 SAFE_PUBLISH_SENSOR(this->target_count_sensor_, num_targets);
150#endif
151
152 this->data_callback_.call();
153}
154
155void LD2460Component::readline_(uint8_t readch) {
156 if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
157 ESP_LOGW(TAG, "Max frame length exceeded; ignoring");
158 this->buffer_pos_ = 0;
159 }
160 this->buffer_data_[this->buffer_pos_++] = readch;
161
162 if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
163 return;
164 }
165 const uint8_t *tail = &this->buffer_data_[this->buffer_pos_ - HEADER_FOOTER_SIZE];
166 bool is_data = readch == progmem_read_byte(&DATA_FRAME_FOOTER[3]) && matches(tail, DATA_FRAME_FOOTER);
167 if (!is_data && (readch != progmem_read_byte(&CMD_FRAME_FOOTER[3]) || !matches(tail, CMD_FRAME_FOOTER))) {
168 return;
169 }
170
171#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
172 char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)];
173 ESP_LOGV(TAG, "Frame: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_));
174#endif
175
176 if (is_data) {
177 this->handle_periodic_data_();
178 } else {
179 this->handle_ack_();
180 }
181 this->buffer_pos_ = 0;
182}
183
184// Once a footer is matched, shift the buffer to the first offset holding a header whose length
185// field ends the frame exactly at the footer. Stray bytes, truncated frames and header bytes inside
186// a payload are skipped this way. Returns false when no such offset exists.
187bool LD2460Component::align_to_header_(const uint8_t *header) {
188 for (uint8_t i = 0; i + FRAME_PREFIX_SIZE <= this->buffer_pos_; i++) {
189 const uint8_t *frame = &this->buffer_data_[i];
190 if (matches(frame, header) && frame_length(frame) == this->buffer_pos_ - i) {
191 if (i != 0) {
192 this->buffer_pos_ -= i;
193 std::memmove(this->buffer_data_, frame, this->buffer_pos_);
194 }
195 return true;
196 }
197 }
198 return false;
199}
200
202 const uint8_t *data = this->buffer_data_;
203 if (!this->align_to_header_(CMD_FRAME_HEADER) || this->buffer_pos_ != ACK_FRAME_LENGTH) {
204 ESP_LOGW(TAG, "Dropping malformed command frame");
205 return;
206 }
207 if (data[4] != CMD_QUERY_INSTALLATION_MODE) {
208 ESP_LOGD(TAG, "Ignoring reply to command 0x%02X", data[4]);
209 return;
210 }
211 const uint8_t mode = data[7];
212 if (mode != static_cast<uint8_t>(InstallationMode::INSTALLATION_MODE_SIDE) &&
213 mode != static_cast<uint8_t>(InstallationMode::INSTALLATION_MODE_TOP)) {
214 ESP_LOGW(TAG, "Unexpected installation mode %u", mode);
215 return;
216 }
217 this->installation_mode_ = static_cast<InstallationMode>(mode);
218 this->query_attempts_ = 0;
219 ESP_LOGD(TAG, "Installation mode: %s",
220 mode == static_cast<uint8_t>(InstallationMode::INSTALLATION_MODE_TOP) ? LOG_STR_LITERAL("top")
221 : LOG_STR_LITERAL("side"));
222#ifdef USE_SENSOR
223 // Angles depend on the mode, so republish every target on the next frame
224 for (auto &t : this->target_sensors_) {
225 t.last_x = INT16_MIN;
226 }
227#endif
228}
229
230} // namespace esphome::ld2460
BedjetMode mode
BedJet operating mode.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
InstallationMode installation_mode_
Definition ld2460.h:62
uint8_t buffer_data_[MAX_LINE_LENGTH]
Definition ld2460.h:59
void readline_(uint8_t readch)
Definition ld2460.cpp:155
bool align_to_header_(const uint8_t *header)
Definition ld2460.cpp:187
std::array< TargetSensors, MAX_TARGETS > target_sensors_
Definition ld2460.h:55
LazyCallbackManager< void()> data_callback_
Definition ld2460.h:58
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:40
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:26
ESPHOME_LOG_TAG(TAG, "ld2460")
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
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:1533
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:980
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6