Skip to content

Commit 49a435e

Browse files
committed
feat(estop): enhance battery monitoring and UI display for E-STOP functionality
1 parent b3a8850 commit 49a435e

8 files changed

Lines changed: 421 additions & 56 deletions

File tree

esp_firmware/data/estop.html

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
<div class="topnav">
1313
<h1>ESP-Daemon E-STOP</h1>
1414
<div class="topnav-meta" aria-label="Device">
15+
<div class="topnav-meta__chip topnav-meta__chip--battery" aria-label="Battery capacity">
16+
<span class="topnav-meta__label">BAT:</span>
17+
<span id="topBatteryCapacity" class="topnav-meta__value topnav-meta__value--muted">--</span>
18+
</div>
1519
<div class="topnav-meta__chip">
1620
<span class="topnav-meta__label">MAC:</span>
1721
<span id="deviceMac" class="topnav-meta__value">00-00-00-00-00-00</span>
@@ -41,21 +45,25 @@ <h3>E-STOP Status</h3>
4145
<span class="status-item__label">ESP-NOW Channel</span>
4246
<span class="status-pill" id="espNowChannelRuntime">-</span>
4347
</div>
44-
<div class="status-item status-item--split">
45-
<span class="status-item__label">Peers</span>
46-
<span class="status-pill" id="peerStatus">not configured</span>
47-
</div>
4848
<div class="status-item status-item--split">
4949
<span class="status-item__label">Routes</span>
5050
<span class="status-pill" id="routeStatus">0/0 active</span>
5151
</div>
52+
<div class="status-item status-item--split">
53+
<span class="status-item__label">Packets Sent</span>
54+
<span class="status-pill" id="packetCount">0</span>
55+
</div>
5256
<div class="status-item status-item--split">
5357
<span class="status-item__label">WLED</span>
5458
<span class="status-pill" id="wledStatus">disabled</span>
5559
</div>
5660
<div class="status-item status-item--split">
57-
<span class="status-item__label">Packets Sent</span>
58-
<span class="status-pill" id="packetCount">0</span>
61+
<span class="status-item__label">Battery Voltage</span>
62+
<span class="status-pill" id="batteryVoltage">--</span>
63+
</div>
64+
<div class="status-item status-item--split">
65+
<span class="status-item__label">Battery Status</span>
66+
<span class="status-pill" id="batteryState">unknown</span>
5967
</div>
6068
</div>
6169
</div>

esp_firmware/data/estop.js

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,55 @@ function mapWledStatusText(statusRaw) {
245245
}
246246
}
247247

248+
function mapBatteryStatusText(statusRaw) {
249+
const status = String(statusRaw || '').trim().toUpperCase();
250+
switch (status) {
251+
case 'NORMAL':
252+
return 'normal';
253+
case 'LOW':
254+
return 'low';
255+
case 'DISCONNECTED':
256+
return 'disconnected';
257+
default:
258+
return status ? status.toLowerCase() : 'unknown';
259+
}
260+
}
261+
262+
function formatBatteryVoltage(voltageRaw, disconnected) {
263+
if (disconnected) {
264+
return '--';
265+
}
266+
const voltage = Number(voltageRaw);
267+
if (!Number.isFinite(voltage) || voltage <= 0) {
268+
return '--';
269+
}
270+
return voltage.toFixed(3) + ' V';
271+
}
272+
273+
function updateTopBatteryCapacity(percentRaw, disconnected, low, forceOffline) {
274+
const valueEl = document.getElementById('topBatteryCapacity');
275+
if (!valueEl) {
276+
return;
277+
}
278+
279+
let percent = NaN;
280+
if (Number.isFinite(percentRaw)) {
281+
percent = Math.max(0, Math.min(100, Math.round(percentRaw)));
282+
}
283+
284+
const hasPercent = !forceOffline && !disconnected && Number.isFinite(percent);
285+
const displayText = hasPercent ? String(percent) + '%' : '--';
286+
valueEl.textContent = displayText;
287+
valueEl.classList.remove('topnav-meta__value--ok', 'topnav-meta__value--warn', 'topnav-meta__value--muted');
288+
if (!hasPercent) {
289+
valueEl.classList.add('topnav-meta__value--muted');
290+
} else if (low) {
291+
valueEl.classList.add('topnav-meta__value--warn');
292+
} else {
293+
valueEl.classList.add('topnav-meta__value--ok');
294+
}
295+
}
296+
248297
function parseJsonSafe(res) {
249298
return res.text().then(function(t) {
250299
if (!t) {
@@ -598,15 +647,19 @@ function refreshEStopStatus() {
598647
const switchState = document.getElementById('switchState');
599648
const rawEl = document.getElementById('switchRaw');
600649
const channelEl = document.getElementById('espNowChannelRuntime');
601-
const peerStatusEl = document.getElementById('peerStatus');
602650
const routeStatusEl = document.getElementById('routeStatus');
603651
const packetEl = document.getElementById('packetCount');
604652
const wledEl = document.getElementById('wledStatus');
653+
const batteryVoltageEl = document.getElementById('batteryVoltage');
654+
const batteryStateEl = document.getElementById('batteryState');
605655

606656
const pressed = !!data.pressed;
607657
const routeCount = Number(data.routeCount || 0);
608658
const pressedRouteCount = Number(data.pressedRouteCount || 0);
609-
const configuredPeerCount = Number(data.configuredPeerCount || 0);
659+
const batteryStatusRaw = String(data.batteryStatus || '').toUpperCase();
660+
const batteryDisconnected = batteryStatusRaw === 'DISCONNECTED';
661+
const batteryLow = batteryStatusRaw === 'LOW';
662+
const batteryPercentRaw = Number(data.batteryPercent);
610663

611664
setPillState(switchState, pressed ? 'PRESSED (STOP)' : 'RELEASED', !pressed);
612665

@@ -629,27 +682,26 @@ function refreshEStopStatus() {
629682
setPillState(routeStatusEl, String(pressedRouteCount) + '/' + String(routeCount) + ' active', routeOk);
630683
}
631684

632-
if (routeCount === 0) {
633-
setPillState(peerStatusEl, 'not configured', false);
634-
} else if (configuredPeerCount === routeCount) {
635-
setPillState(peerStatusEl, String(configuredPeerCount) + '/' + String(routeCount) + ' online', true);
636-
} else if (configuredPeerCount > 0) {
637-
setPillState(peerStatusEl, String(configuredPeerCount) + '/' + String(routeCount) + ' online', false);
638-
} else {
639-
setPillState(peerStatusEl, 'waiting', false);
640-
}
641-
642685
const wledText = mapWledStatusText(data.wledStatus);
643686
const wledOk = wledText === 'ready' || wledText === 'pressed preset active' || wledText === 'released preset active' || wledText === 'restored';
644687
setPillState(wledEl, wledText, wledOk);
645688

689+
const batteryVoltageText = formatBatteryVoltage(data.batteryVoltage, batteryDisconnected);
690+
const batteryStatusText = mapBatteryStatusText(batteryStatusRaw);
691+
const batteryOk = !batteryDisconnected && !batteryLow;
692+
setPillState(batteryVoltageEl, batteryVoltageText, batteryOk);
693+
setPillState(batteryStateEl, batteryStatusText, batteryOk);
694+
updateTopBatteryCapacity(batteryPercentRaw, batteryDisconnected, batteryLow, false);
695+
646696
applyRouteRuntimeStatus(data.routes);
647697
})
648698
.catch(function() {
649699
setPillState(document.getElementById('switchState'), 'offline', false);
650-
setPillState(document.getElementById('peerStatus'), 'offline', false);
651700
setPillState(document.getElementById('routeStatus'), 'offline', false);
652701
setPillState(document.getElementById('wledStatus'), 'offline', false);
702+
setPillState(document.getElementById('batteryVoltage'), 'offline', false);
703+
setPillState(document.getElementById('batteryState'), 'offline', false);
704+
updateTopBatteryCapacity(NaN, true, false, true);
653705
});
654706
}
655707

esp_firmware/data/style.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,33 @@ textarea:focus-visible {
225225
text-overflow: ellipsis;
226226
}
227227

228+
.topnav-meta__chip--battery {
229+
align-items: center;
230+
gap: 0.34rem;
231+
padding-left: 0.56rem;
232+
padding-right: 0.56rem;
233+
}
234+
235+
.topnav-meta__value--ok {
236+
color: #30d158;
237+
}
238+
239+
.topnav-meta__value--warn {
240+
color: #ff9f0a;
241+
}
242+
243+
.topnav-meta__value--muted {
244+
color: var(--topnav-meta-label);
245+
}
246+
247+
.light-mode .topnav-meta__value--ok {
248+
color: #248a3d;
249+
}
250+
251+
.light-mode .topnav-meta__value--warn {
252+
color: #c76800;
253+
}
254+
228255
/* Prevent iOS data detectors from forcing blue link styles on MAC-like placeholders. */
229256
.topnav-meta__value a,
230257
a[x-apple-data-detectors],
@@ -2004,6 +2031,11 @@ button.modern-floating-btn {
20042031
padding: 0.4rem 0.5rem 0.4rem 0.62rem;
20052032
}
20062033

2034+
.topnav-meta__chip--battery {
2035+
padding-left: 0.5rem;
2036+
padding-right: 0.5rem;
2037+
}
2038+
20072039
.topnav-meta__label,
20082040
.topnav-meta__value {
20092041
font-size: 0.7rem;
@@ -2098,6 +2130,11 @@ button.modern-floating-btn {
20982130
padding: 0.38rem 0.42rem 0.38rem 0.52rem;
20992131
}
21002132

2133+
.topnav-meta__chip--battery {
2134+
padding-left: 0.44rem;
2135+
padding-right: 0.44rem;
2136+
}
2137+
21012138
.topnav h1 {
21022139
font-size: clamp(1.06rem, 0.42rem + 4.2vw, 1.42rem);
21032140
letter-spacing: 0;

esp_firmware/include/config.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
#endif
6767
#ifndef ENABLE_SENSOR_TASK
6868
#if APP_MODE == APP_MODE_ESTOP
69-
#define ENABLE_SENSOR_TASK 0
69+
#define ENABLE_SENSOR_TASK 1
7070
#else
7171
#define ENABLE_SENSOR_TASK 1
7272
#endif

esp_firmware/lib/app_settings/app_settings.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ void updateCalibrationFromDivider() {
163163
(g_settings.voltage_divider_r1 + g_settings.voltage_divider_r2) / g_settings.voltage_divider_r2;
164164
}
165165

166+
void applyEStopBatteryDefaults() {
167+
// Seeed XIAO ESP32C3 battery monitor reference:
168+
// A0 + 1/2 divider (200k / 200k), then recover pack voltage by calibration ratio 2.0.
169+
g_settings.voltmeter_pin = 2; // A0 on XIAO ESP32C3
170+
g_settings.voltage_divider_r1 = 200000.0f;
171+
g_settings.voltage_divider_r2 = 200000.0f;
172+
g_settings.voltmeter_calibration = 2.0f;
173+
g_settings.voltmeter_offset = 0.0f;
174+
g_settings.sliding_window_size = 16;
175+
g_settings.timer_period_us = 1000000;
176+
g_settings.battery_disconnect_threshold = 2.7f;
177+
g_settings.battery_low_threshold = 3.45f;
178+
updateCalibrationFromDivider();
179+
}
180+
166181
void applyDefaults() {
167182
g_settings.device_name = defaultDeviceNameFromMac();
168183

@@ -173,7 +188,7 @@ void applyDefaults() {
173188
g_settings.runtime_espnow_enabled = true;
174189
g_settings.runtime_microros_enabled = false;
175190
g_settings.runtime_led_enabled = false;
176-
g_settings.runtime_sensor_enabled = false;
191+
g_settings.runtime_sensor_enabled = true;
177192
#else
178193
g_settings.runtime_espnow_enabled = true;
179194
g_settings.runtime_microros_enabled = true;
@@ -202,6 +217,9 @@ void applyDefaults() {
202217
g_settings.led_brightness = 200;
203218
g_settings.led_override_duration_ms = 1000;
204219

220+
#if APP_MODE == APP_MODE_ESTOP
221+
applyEStopBatteryDefaults();
222+
#else
205223
g_settings.voltmeter_pin = 2;
206224
g_settings.voltage_divider_r1 = 47000.0f;
207225
g_settings.voltage_divider_r2 = 4700.0f;
@@ -212,6 +230,7 @@ void applyDefaults() {
212230
g_settings.battery_disconnect_threshold = 3.0f;
213231
g_settings.battery_low_threshold = 17.5f;
214232
updateCalibrationFromDivider();
233+
#endif
215234

216235
g_settings.ros_node_name = "esp_daemon";
217236
g_settings.ros_domain_id = 0;
@@ -407,6 +426,10 @@ void loadFromJson(const JsonObjectConst& json) {
407426
loadFloat(json, "batteryDisconnectThreshold", g_settings.battery_disconnect_threshold);
408427
loadFloat(json, "batteryLowThreshold", g_settings.battery_low_threshold);
409428
updateCalibrationFromDivider();
429+
#if APP_MODE == APP_MODE_ESTOP
430+
// Keep E-STOP battery sensing always active (UI does not expose this runtime toggle).
431+
g_settings.runtime_sensor_enabled = true;
432+
#endif
410433

411434
loadString(json, "rosNodeName", g_settings.ros_node_name);
412435
if (g_settings.ros_node_name.length() == 0) {

0 commit comments

Comments
 (0)