mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-12 11:43:03 +02:00
Add battery indicator to status bar
This commit is contained in:
@@ -1,12 +1,50 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Menu, Moon, Sun } from 'lucide-react';
|
import {
|
||||||
|
BatteryFull,
|
||||||
|
BatteryLow,
|
||||||
|
BatteryMedium,
|
||||||
|
BatteryWarning,
|
||||||
|
Menu,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
} from 'lucide-react';
|
||||||
import type { HealthStatus, RadioConfig } from '../types';
|
import type { HealthStatus, RadioConfig } from '../types';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { toast } from './ui/sonner';
|
import { toast } from './ui/sonner';
|
||||||
import { handleKeyboardActivate } from '../utils/a11y';
|
import { handleKeyboardActivate } from '../utils/a11y';
|
||||||
import { applyTheme, getSavedTheme, THEME_CHANGE_EVENT } from '../utils/theme';
|
import { applyTheme, getSavedTheme, THEME_CHANGE_EVENT } from '../utils/theme';
|
||||||
|
import { getShowBatteryPercent, getShowBatteryVoltage } from '../utils/batteryDisplay';
|
||||||
import { cn } from '@/lib/utils';
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BATTERY_DISPLAY_CHANGE_EVENT = 'remoteterm-battery-display-change';
|
||||||
|
|
||||||
interface StatusBarProps {
|
interface StatusBarProps {
|
||||||
health: HealthStatus | null;
|
health: HealthStatus | null;
|
||||||
config: RadioConfig | null;
|
config: RadioConfig | null;
|
||||||
@@ -22,6 +60,18 @@ export function StatusBar({
|
|||||||
onSettingsClick,
|
onSettingsClick,
|
||||||
onMenuClick,
|
onMenuClick,
|
||||||
}: StatusBarProps) {
|
}: StatusBarProps) {
|
||||||
|
const [showBatteryPercent, setShowBatteryPercent] = useState(getShowBatteryPercent);
|
||||||
|
const [showBatteryVoltage, setShowBatteryVoltage] = useState(getShowBatteryVoltage);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
setShowBatteryPercent(getShowBatteryPercent());
|
||||||
|
setShowBatteryVoltage(getShowBatteryVoltage());
|
||||||
|
};
|
||||||
|
window.addEventListener(BATTERY_DISPLAY_CHANGE_EVENT, handler);
|
||||||
|
return () => window.removeEventListener(BATTERY_DISPLAY_CHANGE_EVENT, handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const radioState =
|
const radioState =
|
||||||
health?.radio_state ??
|
health?.radio_state ??
|
||||||
(health?.radio_initializing
|
(health?.radio_initializing
|
||||||
@@ -119,6 +169,42 @@ export function StatusBar({
|
|||||||
<span className="hidden lg:inline text-muted-foreground">{statusLabel}</span>
|
<span className="hidden lg:inline text-muted-foreground">{statusLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(showBatteryPercent || showBatteryVoltage) &&
|
||||||
|
connected &&
|
||||||
|
health?.radio_stats?.battery_mv != null &&
|
||||||
|
health.radio_stats.battery_mv > 0 &&
|
||||||
|
(() => {
|
||||||
|
const mv = health.radio_stats.battery_mv!;
|
||||||
|
const pct = mvToPercent(mv);
|
||||||
|
const Icon =
|
||||||
|
pct >= 80
|
||||||
|
? BatteryFull
|
||||||
|
: pct >= 40
|
||||||
|
? BatteryMedium
|
||||||
|
: pct >= 15
|
||||||
|
? BatteryLow
|
||||||
|
: BatteryWarning;
|
||||||
|
const color =
|
||||||
|
pct >= 40 ? 'text-status-connected' : pct >= 15 ? 'text-warning' : 'text-destructive';
|
||||||
|
const label =
|
||||||
|
showBatteryPercent && showBatteryVoltage
|
||||||
|
? `${pct}% (${mv}mV)`
|
||||||
|
: showBatteryPercent
|
||||||
|
? `${pct}%`
|
||||||
|
: `${mv}mV`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex items-center gap-1', color)}
|
||||||
|
title={`Battery: ${pct}% (${(mv / 1000).toFixed(2)}V)`}
|
||||||
|
role="status"
|
||||||
|
aria-label={`Battery ${pct} percent`}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
<span className="hidden sm:inline text-[0.6875rem]">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{config && (
|
{config && (
|
||||||
<div className="hidden lg:flex items-center gap-2 text-muted-foreground">
|
<div className="hidden lg:flex items-center gap-2 text-muted-foreground">
|
||||||
<span className="text-foreground font-medium">{config.name || 'Unnamed'}</span>
|
<span className="text-foreground font-medium">{config.name || 'Unnamed'}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user