sensor stream throttle and new roster panel

This commit is contained in:
legop3
2025-11-20 14:55:59 -05:00
parent ff0bf30eac
commit bfdfa33b62
10 changed files with 111 additions and 69 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+8
View File
@@ -14,6 +14,7 @@ import (
const (
sensorHeader = 19
sensorReadTimeout = 150 * time.Millisecond
sensorThrottleMinimum = 80 * time.Millisecond
)
type SensorStreamer struct {
@@ -29,6 +30,7 @@ func NewSensorStreamer(r io.Reader, rawOut chan<- []byte, parsed chan<- SensorSa
func (s *SensorStreamer) Run(ctx context.Context) {
reader := bufio.NewReader(s.r)
var nextSend time.Time
for {
select {
case <-ctx.Done():
@@ -62,6 +64,12 @@ func (s *SensorStreamer) Run(ctx context.Context) {
continue
}
now := time.Now()
if !nextSend.IsZero() && now.Before(nextSend) {
continue
}
nextSend = now.Add(sensorThrottleMinimum)
select {
case s.rawOut <- frame:
default:
+2 -2
View File
@@ -10,9 +10,9 @@
- video does not load in spectator page in admin mode
## todo
1. battery manager
<!-- 1. battery manager -->
2. pi-side sensor throttle (1/5th)
3. convert rover roster to a reusable component, unbake it from telemetry and admin panels
<!-- 3. convert rover roster to a reusable component, unbake it from telemetry and admin panels -->
4. room cameras
5. home assistant controls
6. nicknames
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-tDRd6DNO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DP0zs3Oi.css">
<script type="module" crossorigin src="/assets/index-BqC-rKak.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C4U-e7FZ.css">
</head>
<body>
<div id="root"></div>
+7 -15
View File
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import { useSession } from '../context/SessionContext.jsx';
import RoverRoster from './RoverRoster.jsx';
const MODES = [
{ key: 'open', label: 'Open' },
@@ -69,13 +70,9 @@ export default function AdminPanel() {
</select>
</div>
<div className="space-y-0.5 text-sm">
{roster.map((rover) => (
<div key={rover.id} className="surface flex flex-wrap items-center justify-between gap-0.5">
<div>
<p className="text-slate-100">{rover.name}</p>
<p className="text-xs text-slate-400">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
</div>
<RoverRoster
roster={roster}
renderActions={(rover) => (
<div className="flex flex-wrap gap-0.5 text-xs">
<button
type="button"
@@ -84,17 +81,12 @@ export default function AdminPanel() {
>
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
</button>
<button
type="button"
onClick={() => handleForceControl(rover.id)}
className="button-dark"
>
<button type="button" onClick={() => handleForceControl(rover.id)} className="button-dark">
Force
</button>
</div>
</div>
))}
</div>
)}
/>
</section>
);
}
+56
View File
@@ -0,0 +1,56 @@
function classNames(...values) {
return values.filter(Boolean).join(' ');
}
function formatBattery(rover) {
const percent = rover?.batteryState?.percentDisplay;
if (percent == null) return '--';
return `${percent}%`;
}
function batteryClass(rover) {
if (!rover?.batteryState) return 'text-slate-400';
if (rover.batteryState.urgentActive) return 'text-red-400';
if (rover.batteryState.warnActive) return 'text-amber-300';
return 'text-emerald-300';
}
export default function RoverRoster({
title,
roster = [],
emptyText = 'No roster data.',
renderActions = null,
}) {
return (
<div className="space-y-0.5 text-sm">
{title && <p className="text-sm text-slate-400">{title}</p>}
{roster.length === 0 ? (
<p className="text-sm text-slate-500">{emptyText}</p>
) : (
<ul className="space-y-0.5 text-sm">
{roster.map((rover) => (
<li
key={rover.id}
className={classNames(
'surface flex flex-wrap items-center justify-between gap-0.5',
rover.locked && 'bg-red-900/40',
)}
>
<div>
<p className="text-slate-200">{rover.name}</p>
<p className="text-xs text-slate-500 flex flex-wrap items-center gap-1">
<span>{rover.locked ? 'locked' : 'free'}</span>
{rover.lockReason && <span className="rounded bg-black/30 px-1">{rover.lockReason}</span>}
<span className={classNames('font-semibold', batteryClass(rover))}>
Battery {formatBattery(rover)}
</span>
</p>
</div>
{renderActions ? renderActions(rover) : null}
</li>
))}
</ul>
)}
</div>
);
}
+9 -23
View File
@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import { useSession } from '../context/SessionContext.jsx';
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
import RoverRoster from './RoverRoster.jsx';
function formatMetric(value, fallback = '--') {
if (value == null || value === '') return fallback;
@@ -97,23 +98,11 @@ export default function TelemetryPanel() {
{rawSnippet}
</pre>
)}
<div className="space-y-0.5">
<p className="text-sm text-slate-400">Rovers</p>
{roster.length === 0 ? (
// if there ARE NO rovers
<p className="text-sm text-slate-500">No roster data.</p>
) : (
// if there ARE rovers
<ul className="space-y-0.5 text-sm">
{roster.map((rover) => (
<li key={rover.id} className="surface flex items-center justify-between gap-0.5">
<div>
<p className="text-slate-200">{rover.name}</p>
<p className="text-xs text-slate-500">
{rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'}
</p>
</div>
{isAdmin && (
<RoverRoster
title="Rovers"
roster={roster}
renderActions={(rover) =>
isAdmin ? (
<button
type="button"
onClick={() => handleRequest(rover.id)}
@@ -122,12 +111,9 @@ export default function TelemetryPanel() {
>
{pending[rover.id] ? '...' : 'request'}
</button>
)}
</li>
))}
</ul>
)}
</div>
) : null
}
/>
</section>
);
}