diff --git a/frontend/src/components/StatusBar.tsx b/frontend/src/components/StatusBar.tsx index ee55c98..eb97f3f 100644 --- a/frontend/src/components/StatusBar.tsx +++ b/frontend/src/components/StatusBar.tsx @@ -17,36 +17,10 @@ import { BATTERY_DISPLAY_CHANGE_EVENT, getShowBatteryPercent, getShowBatteryVoltage, + mvToPercent, } from '../utils/batteryDisplay'; import { cn } from '@/lib/utils'; -// Meshtastic default OCV table (meshtastic/firmware src/power.h) -const OCV_TABLE: [number, number][] = [ - [4190, 100], - [4050, 90], - [3990, 80], - [3890, 70], - [3800, 60], - [3720, 50], - [3630, 40], - [3530, 30], - [3420, 20], - [3300, 10], - [3100, 0], -]; - -function mvToPercent(mv: number): number { - if (mv >= OCV_TABLE[0][0]) return 100; - if (mv <= OCV_TABLE[OCV_TABLE.length - 1][0]) return 0; - for (let i = 0; i < OCV_TABLE.length - 1; i++) { - const [highMv, highPct] = OCV_TABLE[i]; - const [lowMv, lowPct] = OCV_TABLE[i + 1]; - if (mv >= lowMv) - return Math.round(lowPct + ((mv - lowMv) / (highMv - lowMv)) * (highPct - lowPct)); - } - return 0; -} - interface StatusBarProps { health: HealthStatus | null; config: RadioConfig | null; diff --git a/frontend/src/test/batteryDisplay.test.ts b/frontend/src/test/batteryDisplay.test.ts new file mode 100644 index 0000000..c79eca6 --- /dev/null +++ b/frontend/src/test/batteryDisplay.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { mvToPercent, formatBatteryLabel } from '../utils/batteryDisplay'; + +describe('mvToPercent', () => { + it('clamps to 100 above table ceiling', () => { + expect(mvToPercent(4500)).toBe(100); + expect(mvToPercent(4190)).toBe(100); + }); + + it('clamps to 0 below table floor', () => { + expect(mvToPercent(3100)).toBe(0); + expect(mvToPercent(2800)).toBe(0); + }); + + it('returns exact table values at boundaries', () => { + expect(mvToPercent(4050)).toBe(90); + expect(mvToPercent(3630)).toBe(40); + }); + + it('interpolates between table entries', () => { + // Midpoint between 3630 (40%) and 3720 (50%) = 3675 → ~45% + const mid = mvToPercent(3675); + expect(mid).toBeGreaterThan(40); + expect(mid).toBeLessThan(50); + }); +}); + +describe('formatBatteryLabel', () => { + it('returns null when both toggles are off', () => { + expect(formatBatteryLabel(4050, false, false)).toBeNull(); + }); + + it('returns percentage only', () => { + expect(formatBatteryLabel(4050, true, false)).toBe('90%'); + }); + + it('returns voltage only', () => { + expect(formatBatteryLabel(4050, false, true)).toBe('4050mV'); + }); + + it('returns combined when both enabled', () => { + expect(formatBatteryLabel(4050, true, true)).toBe('90% (4050mV)'); + }); +}); diff --git a/frontend/src/utils/batteryDisplay.ts b/frontend/src/utils/batteryDisplay.ts index 3617bf0..a5c12ec 100644 --- a/frontend/src/utils/batteryDisplay.ts +++ b/frontend/src/utils/batteryDisplay.ts @@ -1,5 +1,44 @@ export const BATTERY_DISPLAY_CHANGE_EVENT = 'remoteterm-battery-display-change'; +// Meshtastic default OCV table (meshtastic/firmware src/power.h) +const OCV_TABLE: [number, number][] = [ + [4190, 100], + [4050, 90], + [3990, 80], + [3890, 70], + [3800, 60], + [3720, 50], + [3630, 40], + [3530, 30], + [3420, 20], + [3300, 10], + [3100, 0], +]; + +export function mvToPercent(mv: number): number { + if (mv >= OCV_TABLE[0][0]) return 100; + if (mv <= OCV_TABLE[OCV_TABLE.length - 1][0]) return 0; + for (let i = 0; i < OCV_TABLE.length - 1; i++) { + const [highMv, highPct] = OCV_TABLE[i]; + const [lowMv, lowPct] = OCV_TABLE[i + 1]; + if (mv >= lowMv) + return Math.round(lowPct + ((mv - lowMv) / (highMv - lowMv)) * (highPct - lowPct)); + } + return 0; +} + +export function formatBatteryLabel( + mv: number, + showPercent: boolean, + showVoltage: boolean +): string | null { + if (!showPercent && !showVoltage) return null; + const pct = mvToPercent(mv); + if (showPercent && showVoltage) return `${pct}% (${mv}mV)`; + if (showPercent) return `${pct}%`; + return `${mv}mV`; +} + const PERCENT_KEY = 'remoteterm-show-battery-percent'; const VOLTAGE_KEY = 'remoteterm-show-battery-voltage';