Skip to content

Commit 0816789

Browse files
committed
dotbot/console-web: render swarmit's battery reading per robot
AI-assisted: Claude Opus 5
1 parent 07c53e3 commit 0816789

10 files changed

Lines changed: 74 additions & 29 deletions

File tree

‎dotbot/console-web/src/Footer.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ const StateDot: React.FC<{ state: string; glow?: boolean; size?: number }> = ({
6363
/>
6464
);
6565

66-
const BatteryBar: React.FC<{ volts: number }> = ({ volts }) => {
67-
const pct = batteryPct(volts);
66+
const BatteryBar: React.FC<{ bot: UnifiedBot }> = ({ bot }) => {
67+
const volts = bot.battery;
68+
const pct = batteryPct(bot);
6869
return (
6970
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
7071
<div style={{ width: 58, height: 6, background: "var(--elevated)", borderRadius: 3, overflow: "hidden" }}>
71-
<div style={{ width: `${pct}%`, height: "100%", background: batteryColor(volts) }} />
72+
<div style={{ width: `${pct}%`, height: "100%", background: batteryColor(bot) }} />
7273
</div>
7374
<span style={{ ...mono, fontSize: 12 }}>{volts.toFixed(2)} V</span>
7475
</div>
@@ -436,7 +437,7 @@ export const Footer: React.FC<FooterProps> = (props) => {
436437
</div>
437438
)}
438439
<div style={{ display: "flex", alignItems: "center", gap: 18 }}>
439-
<BatteryBar volts={one.battery} />
440+
<BatteryBar bot={one} />
440441
<span style={{ ...mono, fontSize: 11, color: "var(--muted)" }}>
441442
{one.position ? `${Math.round(one.position.x)}, ${Math.round(one.position.y)} mm` : "— unknown"}
442443
</span>

‎dotbot/console-web/src/GridView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const GridView: React.FC<GridViewProps> = ({ bots, selection, onSelect })
103103
<span style={{ fontSize: 11, color: "var(--muted)" }}>{b.state}</span>
104104
</div>
105105
<div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--muted)" }}>{b.id}</div>
106-
<BatteryCell volts={b.battery} fill />
106+
<BatteryCell bot={b} fill />
107107
<div style={{ fontSize: 11, color: "var(--muted)" }}>
108108
Device <span style={{ fontFamily: "var(--font-mono)", color: "var(--text)" }}>{b.deviceType}</span>
109109
</div>

‎dotbot/console-web/src/ListView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export const ListView: React.FC<ListViewProps> = ({ bots, selection, onSelect })
200200
{b.image ?? "—"}
201201
</td>
202202
<td style={{ padding: "9px 12px" }}>
203-
<BatteryCell volts={b.battery} />
203+
<BatteryCell bot={b} />
204204
</td>
205205
<td style={{ padding: "9px 12px" }}>
206206
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>

‎dotbot/console-web/src/MapView.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export const MapView: React.FC<MapViewProps> = (props) => {
331331
const hovered = hoverId === b.id;
332332
const led = ledCss(b);
333333
const stc = `var(--s-${b.state})`;
334-
const pct = batteryPct(b.battery);
334+
const pct = batteryPct(b);
335335
const blink = b.state === "Programming" || b.state === "Resetting";
336336
return (
337337
<div
@@ -395,7 +395,7 @@ export const MapView: React.FC<MapViewProps> = (props) => {
395395
borderRadius: 2,
396396
}}
397397
>
398-
<div style={{ height: "100%", width: `${pct}%`, background: batteryColor(b.battery), borderRadius: 2 }} />
398+
<div style={{ height: "100%", width: `${pct}%`, background: batteryColor(b), borderRadius: 2 }} />
399399
</div>
400400
)}
401401
{/* body: state-colored circle */}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { batteryColor, batteryPct } from "./viewChrome";
4+
5+
// The scale is a robot fact and lives in swarmit, per device type. These cover
6+
// that the console renders what it is served and degrades visibly, not
7+
// confidently, for a bot swarmit does not know.
8+
describe("battery rendering", () => {
9+
it("renders the served percentage verbatim", () => {
10+
expect(batteryPct({ batteryPct: 57, battery: 2.3 })).toBe(57);
11+
expect(batteryPct({ batteryPct: 0, battery: 0.6 })).toBe(0);
12+
});
13+
14+
it("clamps a nonsense served value rather than overflowing the bar", () => {
15+
expect(batteryPct({ batteryPct: 140, battery: 3.0 })).toBe(100);
16+
expect(batteryPct({ batteryPct: -5, battery: 0 })).toBe(0);
17+
});
18+
19+
it("falls back only for a bot swarmit does not know", () => {
20+
expect(batteryPct({ batteryPct: null, battery: 3.0 })).toBe(100);
21+
expect(batteryPct({ batteryPct: null, battery: 1.5 })).toBe(50);
22+
});
23+
24+
it("colours by the served band, matching the bootloader LED", () => {
25+
expect(batteryColor({ batteryLevel: "full" })).toBe("var(--s-Full)");
26+
expect(batteryColor({ batteryLevel: "ok" })).toBe("var(--s-Running)");
27+
expect(batteryColor({ batteryLevel: "low" })).toBe("var(--s-Stopping)");
28+
});
29+
30+
it("shows no band when there is none to show", () => {
31+
expect(batteryColor({ batteryLevel: null })).toBe("var(--muted)");
32+
});
33+
});

‎dotbot/console-web/src/inspector.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const bot = (over: Partial<UnifiedBot> = {}): UnifiedBot => ({
1919
image: null,
2020
resetCause: null,
2121
crashed: false,
22+
batteryPct: null,
23+
batteryLevel: null,
2224
swarmit: null,
2325
...over,
2426
});

‎dotbot/console-web/src/missions.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const bot = (id: string, over: Partial<UnifiedBot> = {}): UnifiedBot => ({
1919
image: null,
2020
resetCause: null,
2121
crashed: false,
22+
batteryPct: null,
23+
batteryLevel: null,
2224
swarmit: null,
2325
...over,
2426
});

‎dotbot/console-web/src/types.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export interface SwarmitNode {
8484
fault?: number; // latched fault type, 0 = none
8585
reset_cause?: string; // swarmit's friendly label for the last reset
8686
fault_name?: string; // the latched FaultType's name
87+
battery_pct?: number; // 0-100 on this robot's own battery profile
88+
battery_level?: string; // full | ok | low, the bootloader's LED bands
8789
from_ns?: number; // the fault came from the non-secure world
8890
pc?: number; // program counter at the fault
8991
lr?: number;
@@ -111,6 +113,8 @@ export interface UnifiedBot {
111113
image: string | null; // firmware image the bot reports running
112114
resetCause: string | null; // why it last booted, swarmit's vocabulary
113115
crashed: boolean; // last reset was a fault, a crash deadman, or a lockup
116+
batteryPct: number | null; // served by swarmit; null for a bot it does not know
117+
batteryLevel: string | null; // full | ok | low
114118
swarmit: SwarmitNode | null; // the orchestration record, for the inspector
115119
}
116120

‎dotbot/console-web/src/useFleet.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export function merge(
7676
image: sw?.info?.image_name || null,
7777
resetCause: sw?.reset_cause ?? null,
7878
crashed: isCrashed(sw),
79+
batteryPct: sw?.battery_pct ?? null,
80+
batteryLevel: sw?.battery_level ?? null,
7981
swarmit: sw ?? null,
8082
});
8183
}

‎dotbot/console-web/src/viewChrome.tsx‎

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -170,33 +170,34 @@ export const LedDot: React.FC<{ bot: UnifiedBot }> = ({ bot }) => (
170170
/>
171171
);
172172

173-
// Battery, on the same scale and thresholds the CLI uses (swarmit's
174-
// battery_level_color and its `V (%)` cell). The pack is a 3.0 V supercap, not
175-
// a 4.2 V LiPo: reading it on a LiPo scale showed a healthy 2.3 V bot as
176-
// nearly flat. Full voltage and the thresholds are hardware facts, so they
177-
// live here once and every battery readout goes through them.
178-
export const VOLTAGE_FULL_V = 2.9;
179-
export const VOLTAGE_WARNING_V = 1.5;
180-
const VOLTAGE_SCALE_V = 3.0; // 100% reference
181-
182-
/** Percent 0..100, matching the CLI's `int(battery / 3000 * 100)`. */
183-
export function batteryPct(volts: number): number {
184-
return Math.max(0, Math.min(100, Math.trunc((volts / VOLTAGE_SCALE_V) * 100)));
173+
// Battery. The percentage and the band are computed by swarmit, per robot:
174+
// the v3 pack is a 3.0 V supercapacitor that browns out at 0.6 V, and reading
175+
// it on a naive voltage ratio showed a healthy 2.3 V bot as nearly flat. Those
176+
// numbers are robot facts and a v2 pack differs, so this renders what it is
177+
// told rather than keeping a second scale here.
178+
//
179+
// The fallbacks below only cover a bot swarmit does not know (a control-plane
180+
// only bot) and are deliberately crude - a bar with no band rather than a
181+
// confident wrong number.
182+
export function batteryPct(bot: { batteryPct: number | null; battery: number }): number {
183+
if (bot.batteryPct !== null) return Math.max(0, Math.min(100, bot.batteryPct));
184+
return Math.max(0, Math.min(100, Math.trunc((bot.battery / 3.0) * 100)));
185185
}
186186

187-
/** Bar colour, mirroring swarmit's cyan / green / red bands. */
188-
export function batteryColor(volts: number): string {
189-
if (volts > VOLTAGE_FULL_V) return "var(--s-Full)";
190-
if (volts > VOLTAGE_WARNING_V) return "var(--s-Running)";
191-
return "var(--s-Stopping)";
187+
export function batteryColor(bot: { batteryLevel: string | null }): string {
188+
if (bot.batteryLevel === "full") return "var(--s-Full)";
189+
if (bot.batteryLevel === "low") return "var(--s-Stopping)";
190+
if (bot.batteryLevel === "ok") return "var(--s-Running)";
191+
return "var(--muted)"; // unknown to swarmit: no band to show
192192
}
193193

194-
export const BatteryCell: React.FC<{ volts: number; width?: number; fill?: boolean }> = ({
195-
volts,
194+
export const BatteryCell: React.FC<{ bot: UnifiedBot; width?: number; fill?: boolean }> = ({
195+
bot,
196196
width = 54,
197197
fill = false,
198198
}) => {
199-
const pct = batteryPct(volts);
199+
const volts = bot.battery;
200+
const pct = batteryPct(bot);
200201
return (
201202
<div style={{ display: "flex", alignItems: "center", gap: 8, ...(fill ? { width: "100%" } : {}) }}>
202203
<div
@@ -212,7 +213,7 @@ export const BatteryCell: React.FC<{ volts: number; width?: number; fill?: boole
212213
style={{
213214
width: `${pct}%`,
214215
height: "100%",
215-
background: batteryColor(volts),
216+
background: batteryColor(bot),
216217
}}
217218
/>
218219
</div>

0 commit comments

Comments
 (0)