Skip to content

Commit 2941198

Browse files
committed
feat(esp-daemon): implement LTTB downsampling for battery telemetry data export
1 parent c704143 commit 2941198

2 files changed

Lines changed: 111 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ On press/release edges, firmware calls `POST {baseUrl}/json/state` with `{"on":
275275
| **Persistence** | Settings are stored in NVS (`Preferences`). Export/import uses structured JSON (`esp-daemon.settings-export` / `esp-estop.settings-export` schemas). |
276276
| **UI actions** | **EXPORT** / **IMPORT** — backup and restore. **RESTORE DEFAULTS** — firmware defaults, keeps NVS layout. **FACTORY RESET** — erases NVS including Wi-Fi credentials; device reboots. |
277277
| **PIN (Daemon only)** | When PIN protection is enabled, `POST /settings/*` bodies must include a valid `authPin`; use `POST /settings/unlock` from the UI flow. E-STOP settings endpoints do not use PIN. |
278-
| **Battery telemetry** | `GET /telemetry` returns a discharge session sampled at 1 Hz while connected (max ~7200 points, ~2 h). Default responses are downsampled for UI performance; use `?full=1` for full export, or `?maxPoints=<n>` to cap response points. |
278+
| **Battery telemetry** | `GET /telemetry` returns a discharge session sampled at 1 Hz while connected (max ~7200 points, ~2 h). Default UI responses are downsampled with **LTTB**; use `?full=1` for full export, or `?maxPoints=<n>` to cap response points. |
279279

280280
---
281281

esp_firmware/lib/telemetry_log/telemetry_log.cpp

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,6 @@ size_t decimatedPointCount(size_t count, bool full, size_t max_points) {
5151
return (count > maxPts) ? maxPts : count;
5252
}
5353

54-
size_t decimatedIndex(size_t out_idx, size_t out_count, size_t src_count) {
55-
if (src_count <= 1u || out_count <= 1u) {
56-
return 0u;
57-
}
58-
const uint64_t num = static_cast<uint64_t>(out_idx) * static_cast<uint64_t>(src_count - 1u);
59-
const uint64_t den = static_cast<uint64_t>(out_count - 1u);
60-
return static_cast<size_t>((num + (den / 2u)) / den);
61-
}
62-
6354
void appendBoolJson(String& out, bool value) {
6455
out += (value ? "true" : "false");
6556
}
@@ -78,6 +69,111 @@ void appendFloatJson(String& out, float value) {
7869
}
7970
}
8071

72+
void appendSeriesJson(String& out, const float* series, size_t count, size_t points, bool downsampled) {
73+
out += ",\"v\":[";
74+
if (count == 0 || points == 0) {
75+
out += "]";
76+
return;
77+
}
78+
79+
if (!downsampled || points >= count) {
80+
for (size_t i = 0; i < count; i++) {
81+
if (i > 0) {
82+
out += ",";
83+
}
84+
appendFloatJson(out, series[i]);
85+
}
86+
out += "]";
87+
return;
88+
}
89+
90+
if (points == 1u) {
91+
appendFloatJson(out, series[0]);
92+
out += "]";
93+
return;
94+
}
95+
96+
if (points == 2u) {
97+
appendFloatJson(out, series[0]);
98+
out += ",";
99+
appendFloatJson(out, series[count - 1u]);
100+
out += "]";
101+
return;
102+
}
103+
104+
// Largest-Triangle-Three-Buckets (LTTB):
105+
// preserve first/last, pick representative points that maximize local area.
106+
appendFloatJson(out, series[0]);
107+
const double every = static_cast<double>(count - 2u) / static_cast<double>(points - 2u);
108+
size_t a = 0u;
109+
110+
for (size_t i = 0; i < points - 2u; i++) {
111+
size_t avgRangeStart = static_cast<size_t>(floor((static_cast<double>(i) + 1.0) * every)) + 1u;
112+
size_t avgRangeEnd = static_cast<size_t>(floor((static_cast<double>(i) + 2.0) * every)) + 1u;
113+
if (avgRangeStart >= count) {
114+
avgRangeStart = count - 1u;
115+
}
116+
if (avgRangeEnd > count) {
117+
avgRangeEnd = count;
118+
}
119+
if (avgRangeEnd <= avgRangeStart) {
120+
avgRangeEnd = (avgRangeStart + 1u <= count) ? (avgRangeStart + 1u) : count;
121+
}
122+
123+
double avgX = 0.0;
124+
double avgY = 0.0;
125+
size_t avgRangeLen = 0u;
126+
for (size_t j = avgRangeStart; j < avgRangeEnd; j++) {
127+
avgX += static_cast<double>(j);
128+
avgY += static_cast<double>(series[j]);
129+
avgRangeLen++;
130+
}
131+
if (avgRangeLen > 0u) {
132+
avgX /= static_cast<double>(avgRangeLen);
133+
avgY /= static_cast<double>(avgRangeLen);
134+
} else {
135+
avgX = static_cast<double>(avgRangeStart);
136+
avgY = static_cast<double>(series[avgRangeStart]);
137+
}
138+
139+
size_t rangeOffs = static_cast<size_t>(floor(static_cast<double>(i) * every)) + 1u;
140+
size_t rangeTo = static_cast<size_t>(floor((static_cast<double>(i) + 1.0) * every)) + 1u;
141+
142+
const size_t lastIndex = count - 1u;
143+
if (rangeOffs >= lastIndex) {
144+
rangeOffs = lastIndex - 1u;
145+
}
146+
if (rangeTo > lastIndex) {
147+
rangeTo = lastIndex;
148+
}
149+
if (rangeTo <= rangeOffs) {
150+
rangeTo = rangeOffs + 1u;
151+
}
152+
153+
const double ax = static_cast<double>(a);
154+
const double ay = static_cast<double>(series[a]);
155+
156+
size_t nextA = rangeOffs;
157+
double maxArea = -1.0;
158+
for (size_t j = rangeOffs; j < rangeTo; j++) {
159+
const double area = fabs((ax - avgX) * (static_cast<double>(series[j]) - ay)
160+
- (ax - static_cast<double>(j)) * (avgY - ay));
161+
if (area > maxArea) {
162+
maxArea = area;
163+
nextA = j;
164+
}
165+
}
166+
167+
out += ",";
168+
appendFloatJson(out, series[nextA]);
169+
a = nextA;
170+
}
171+
172+
out += ",";
173+
appendFloatJson(out, series[count - 1u]);
174+
out += "]";
175+
}
176+
81177
} // namespace
82178

83179
void telemetryLogInit() {
@@ -198,19 +294,15 @@ String telemetryLogGetJson(bool full, size_t max_points) {
198294
appendBoolJson(out, connected_now);
199295
out += ",\"downsampled\":";
200296
appendBoolJson(out, downsampled);
297+
if (downsampled) {
298+
out += ",\"sampleMethod\":\"lttb\"";
299+
}
201300
if (!full) {
202301
out += ",\"maxPoints\":";
203302
out += String(maxPointsUsed);
204303
}
205-
out += ",\"v\":[";
206-
for (size_t i = 0; i < points; i++) {
207-
if (i > 0) {
208-
out += ",";
209-
}
210-
const size_t srcIndex = downsampled ? decimatedIndex(i, points, count) : i;
211-
appendFloatJson(out, s_copy[srcIndex]);
212-
}
213-
out += "]}";
304+
appendSeriesJson(out, s_copy, count, points, downsampled);
305+
out += "}";
214306

215307
return out;
216308
}

0 commit comments

Comments
 (0)