mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
sensor stream throttle and new roster panel
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -12,8 +12,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sensorHeader = 19
|
sensorHeader = 19
|
||||||
sensorReadTimeout = 150 * time.Millisecond
|
sensorReadTimeout = 150 * time.Millisecond
|
||||||
|
sensorThrottleMinimum = 80 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type SensorStreamer struct {
|
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) {
|
func (s *SensorStreamer) Run(ctx context.Context) {
|
||||||
reader := bufio.NewReader(s.r)
|
reader := bufio.NewReader(s.r)
|
||||||
|
var nextSend time.Time
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -62,6 +64,12 @@ func (s *SensorStreamer) Run(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if !nextSend.IsZero() && now.Before(nextSend) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextSend = now.Add(sensorThrottleMinimum)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case s.rawOut <- frame:
|
case s.rawOut <- frame:
|
||||||
default:
|
default:
|
||||||
|
|||||||
+2
-2
@@ -10,9 +10,9 @@
|
|||||||
- video does not load in spectator page in admin mode
|
- video does not load in spectator page in admin mode
|
||||||
|
|
||||||
## todo
|
## todo
|
||||||
1. battery manager
|
<!-- 1. battery manager -->
|
||||||
2. pi-side sensor throttle (1/5th)
|
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
|
4. room cameras
|
||||||
5. home assistant controls
|
5. home assistant controls
|
||||||
6. nicknames
|
6. nicknames
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>webui</title>
|
<title>webui</title>
|
||||||
<script type="module" crossorigin src="/assets/index-tDRd6DNO.js"></script>
|
<script type="module" crossorigin src="/assets/index-BqC-rKak.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DP0zs3Oi.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C4U-e7FZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSession } from '../context/SessionContext.jsx';
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
import RoverRoster from './RoverRoster.jsx';
|
||||||
|
|
||||||
const MODES = [
|
const MODES = [
|
||||||
{ key: 'open', label: 'Open' },
|
{ key: 'open', label: 'Open' },
|
||||||
@@ -69,32 +70,23 @@ export default function AdminPanel() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0.5 text-sm">
|
<RoverRoster
|
||||||
{roster.map((rover) => (
|
roster={roster}
|
||||||
<div key={rover.id} className="surface flex flex-wrap items-center justify-between gap-0.5">
|
renderActions={(rover) => (
|
||||||
<div>
|
<div className="flex flex-wrap gap-0.5 text-xs">
|
||||||
<p className="text-slate-100">{rover.name}</p>
|
<button
|
||||||
<p className="text-xs text-slate-400">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
|
type="button"
|
||||||
</div>
|
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
||||||
<div className="flex flex-wrap gap-0.5 text-xs">
|
className="button-dark"
|
||||||
<button
|
>
|
||||||
type="button"
|
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
||||||
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
</button>
|
||||||
className="button-dark"
|
<button type="button" onClick={() => handleForceControl(rover.id)} className="button-dark">
|
||||||
>
|
Force
|
||||||
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleForceControl(rover.id)}
|
|
||||||
className="button-dark"
|
|
||||||
>
|
|
||||||
Force
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSession } from '../context/SessionContext.jsx';
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||||
|
import RoverRoster from './RoverRoster.jsx';
|
||||||
|
|
||||||
function formatMetric(value, fallback = '--') {
|
function formatMetric(value, fallback = '--') {
|
||||||
if (value == null || value === '') return fallback;
|
if (value == null || value === '') return fallback;
|
||||||
@@ -97,37 +98,22 @@ export default function TelemetryPanel() {
|
|||||||
{rawSnippet}
|
{rawSnippet}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-0.5">
|
<RoverRoster
|
||||||
<p className="text-sm text-slate-400">Rovers</p>
|
title="Rovers"
|
||||||
{roster.length === 0 ? (
|
roster={roster}
|
||||||
// if there ARE NO rovers
|
renderActions={(rover) =>
|
||||||
<p className="text-sm text-slate-500">No roster data.</p>
|
isAdmin ? (
|
||||||
) : (
|
<button
|
||||||
// if there ARE rovers
|
type="button"
|
||||||
<ul className="space-y-0.5 text-sm">
|
onClick={() => handleRequest(rover.id)}
|
||||||
{roster.map((rover) => (
|
disabled={pending[rover.id]}
|
||||||
<li key={rover.id} className="surface flex items-center justify-between gap-0.5">
|
className="button-dark disabled:opacity-40"
|
||||||
<div>
|
>
|
||||||
<p className="text-slate-200">{rover.name}</p>
|
{pending[rover.id] ? '...' : 'request'}
|
||||||
<p className="text-xs text-slate-500">
|
</button>
|
||||||
{rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'}
|
) : null
|
||||||
</p>
|
}
|
||||||
</div>
|
/>
|
||||||
{isAdmin && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleRequest(rover.id)}
|
|
||||||
disabled={pending[rover.id]}
|
|
||||||
className="button-dark disabled:opacity-40"
|
|
||||||
>
|
|
||||||
{pending[rover.id] ? '...' : 'request'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user