mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
slop
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
|||||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||||
<title>Roomba Rover</title>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-ylLtL6Of.js"></script>
|
<script type="module" crossorigin src="/assets/index-U2WtZsRF.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BzceGRD0.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BTjcgOin.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -23,13 +23,7 @@ const FRAME_ROOM = 'balance-board-viewers';
|
|||||||
const TARE_SAMPLE_COUNT = 20;
|
const TARE_SAMPLE_COUNT = 20;
|
||||||
const MAX_AUTOMATIC_TARE_KG = 2;
|
const MAX_AUTOMATIC_TARE_KG = 2;
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const ALERT_COLORS = {
|
const ALERT_COLOR = '#38bdf8';
|
||||||
connected: '#10b981',
|
|
||||||
sleeping: '#64748b',
|
|
||||||
warning: '#f59e0b',
|
|
||||||
error: '#ef4444',
|
|
||||||
info: '#38bdf8',
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadStore() {
|
function loadStore() {
|
||||||
try {
|
try {
|
||||||
@@ -89,49 +83,25 @@ let lastAlertKey = '';
|
|||||||
let unpairing = false;
|
let unpairing = false;
|
||||||
|
|
||||||
function sendStatusAlert(workerState, message = '') {
|
function sendStatusAlert(workerState, message = '') {
|
||||||
let alert = null;
|
const shouldAlert =
|
||||||
if (workerState === 'connected') {
|
workerState === 'connected' ||
|
||||||
alert = {
|
workerState === 'sleeping' ||
|
||||||
color: ALERT_COLORS.connected,
|
workerState === 'connection-failed' ||
|
||||||
title: 'Balance Board connected',
|
workerState === 'error' ||
|
||||||
message: 'Live weight is available.',
|
(workerState === 'waiting' && previousWorkerState === 'connected');
|
||||||
};
|
|
||||||
} else if (workerState === 'sleeping') {
|
|
||||||
alert = {
|
|
||||||
color: ALERT_COLORS.sleeping,
|
|
||||||
title: 'Balance Board sleeping',
|
|
||||||
message: 'Press the front power button when you are ready to use it.',
|
|
||||||
};
|
|
||||||
} else if (workerState === 'waiting' && previousWorkerState === 'connected') {
|
|
||||||
alert = {
|
|
||||||
color: ALERT_COLORS.warning,
|
|
||||||
title: 'Balance Board disconnected',
|
|
||||||
message: message || 'Press the front power button to reconnect it.',
|
|
||||||
};
|
|
||||||
} else if (workerState === 'connection-failed') {
|
|
||||||
alert = {
|
|
||||||
color: ALERT_COLORS.error,
|
|
||||||
title: 'Balance Board connection failed',
|
|
||||||
message: message || 'The Bluetooth connection did not complete.',
|
|
||||||
};
|
|
||||||
} else if (workerState === 'error') {
|
|
||||||
alert = {
|
|
||||||
color: ALERT_COLORS.error,
|
|
||||||
title: 'Balance Board needs attention',
|
|
||||||
message: message || 'The hardware worker stopped.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
previousWorkerState = workerState;
|
previousWorkerState = workerState;
|
||||||
if (!alert) return;
|
if (!shouldAlert) return;
|
||||||
|
|
||||||
// Native reconnect retries may repeat the same result while hardware is out
|
// Keep the alert at the same system-level boundary as the worker protocol:
|
||||||
// of range. Collapse identical status/message pairs so feed alerts remain
|
// state first, followed by its exact detail when one exists. The service does
|
||||||
// meaningful state changes instead of turning into a transport log.
|
// not reinterpret failures as friendlier product copy, but still collapses
|
||||||
const key = `${workerState}:${alert.message}`;
|
// identical retries so a failing reconnect cannot flood the activity feed.
|
||||||
|
const rawMessage = message ? `${workerState}: ${message}` : workerState;
|
||||||
|
const key = rawMessage;
|
||||||
if (key === lastAlertKey) return;
|
if (key === lastAlertKey) return;
|
||||||
lastAlertKey = key;
|
lastAlertKey = key;
|
||||||
sendAlert(alert);
|
sendAlert({ color: ALERT_COLOR, title: 'Balance Board', message: rawMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getState() {
|
function getState() {
|
||||||
@@ -197,9 +167,9 @@ function handleWorkerMessage(message = {}) {
|
|||||||
hardware?.setAddress(address);
|
hardware?.setAddress(address);
|
||||||
lastAlertKey = 'paired';
|
lastAlertKey = 'paired';
|
||||||
sendAlert({
|
sendAlert({
|
||||||
color: ALERT_COLORS.info,
|
color: ALERT_COLOR,
|
||||||
title: 'Balance Board paired',
|
title: 'Balance Board',
|
||||||
message: 'Bluetooth setup completed.',
|
message: 'paired',
|
||||||
});
|
});
|
||||||
updateStatus('connecting', 'Paired. Connecting to the board now.');
|
updateStatus('connecting', 'Paired. Connecting to the board now.');
|
||||||
return;
|
return;
|
||||||
@@ -291,9 +261,9 @@ io.on('connection', (socket) => {
|
|||||||
hardware?.restart();
|
hardware?.restart();
|
||||||
updateStatus('starting', 'Starting Bluetooth discovery.');
|
updateStatus('starting', 'Starting Bluetooth discovery.');
|
||||||
sendAlert({
|
sendAlert({
|
||||||
color: ALERT_COLORS.warning,
|
color: ALERT_COLOR,
|
||||||
title: 'Balance Board unpaired',
|
title: 'Balance Board',
|
||||||
message: 'Press the red Sync button underneath the board to pair it again.',
|
message: 'unpaired',
|
||||||
});
|
});
|
||||||
cb({ success: true, warning: bluetoothWarning || null });
|
cb({ success: true, warning: bluetoothWarning || null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function centerOfPressure(corners) {
|
|||||||
// An empty board naturally has tiny load-cell noise. Keep the marker centered
|
// An empty board naturally has tiny load-cell noise. Keep the marker centered
|
||||||
// and subdued until there is enough weight for its position to mean anything;
|
// and subdued until there is enough weight for its position to mean anything;
|
||||||
// once loaded, map the normalized left/right and top/bottom balance into the
|
// once loaded, map the normalized left/right and top/bottom balance into the
|
||||||
// safe interior of the illustrated board.
|
// safe interior of the load map.
|
||||||
if (total < 0.5) return { left: 50, top: 50, active: false };
|
if (total < 0.5) return { left: 50, top: 50, active: false };
|
||||||
const horizontal = ((topRight + bottomRight) - (topLeft + bottomLeft)) / total;
|
const horizontal = ((topRight + bottomRight) - (topLeft + bottomLeft)) / total;
|
||||||
const vertical = ((bottomLeft + bottomRight) - (topLeft + topRight)) / total;
|
const vertical = ((bottomLeft + bottomRight) - (topLeft + topRight)) / total;
|
||||||
@@ -49,7 +49,7 @@ function centerOfPressure(corners) {
|
|||||||
|
|
||||||
function CornerReading({ className, label, value }) {
|
function CornerReading({ className, label, value }) {
|
||||||
return (
|
return (
|
||||||
<div className={`absolute rounded bg-slate-950/80 px-1 py-0.5 text-center shadow ${className}`}>
|
<div className={`surface absolute min-w-[5.5rem] text-center ${className}`}>
|
||||||
<div className="text-[0.62rem] text-slate-400">{label}</div>
|
<div className="text-[0.62rem] text-slate-400">{label}</div>
|
||||||
<div className="font-mono text-sm font-semibold text-slate-100">{formatWeight(value)}</div>
|
<div className="font-mono text-sm font-semibold text-slate-100">{formatWeight(value)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +70,6 @@ function BalanceBoardPanelContent() {
|
|||||||
const role = useSessionSelector((state) => state.session?.role || null);
|
const role = useSessionSelector((state) => state.session?.role || null);
|
||||||
const [frame, setFrame] = useState(EMPTY_FRAME);
|
const [frame, setFrame] = useState(EMPTY_FRAME);
|
||||||
const [unpairing, setUnpairing] = useState(false);
|
const [unpairing, setUnpairing] = useState(false);
|
||||||
const [adminMessage, setAdminMessage] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket) return undefined;
|
if (!socket) return undefined;
|
||||||
@@ -115,82 +114,71 @@ function BalanceBoardPanelContent() {
|
|||||||
if (unpairing || !board?.paired) return;
|
if (unpairing || !board?.paired) return;
|
||||||
if (!window.confirm('Unpair this Balance Board and require the red Sync button to pair it again?')) return;
|
if (!window.confirm('Unpair this Balance Board and require the red Sync button to pair it again?')) return;
|
||||||
setUnpairing(true);
|
setUnpairing(true);
|
||||||
setAdminMessage('');
|
|
||||||
socket.emit('balanceBoard:unpair', {}, (response = {}) => {
|
socket.emit('balanceBoard:unpair', {}, (response = {}) => {
|
||||||
setUnpairing(false);
|
setUnpairing(false);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
setAdminMessage(response.error);
|
window.alert(response.error);
|
||||||
} else if (response.warning) {
|
} else if (response.warning) {
|
||||||
setAdminMessage('Board forgotten locally, but BlueZ reported a bond-removal warning.');
|
window.alert('Board forgotten locally, but BlueZ reported a bond-removal warning.');
|
||||||
} else {
|
|
||||||
setAdminMessage('Board unpaired. Press the red Sync button to pair it again.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{battery != null ? (
|
||||||
|
<span className="font-mono text-xs text-slate-300">{Math.round(battery)}% battery</span>
|
||||||
|
) : null}
|
||||||
|
{isAdmin ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-dark text-xs disabled:opacity-50"
|
||||||
|
disabled={!board?.paired || unpairing}
|
||||||
|
onClick={unpair}
|
||||||
|
>
|
||||||
|
{unpairing ? 'Unpairing…' : 'Unpair'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardFrame
|
<CardFrame
|
||||||
title="Balance Board"
|
title="Balance Board"
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
bodyClassName="text-sm text-slate-200"
|
bodyClassName="text-sm text-slate-200"
|
||||||
actions={battery != null ? (
|
actions={actions}
|
||||||
<span className="font-mono text-xs text-slate-300">{Math.round(battery)}% battery</span>
|
|
||||||
) : null}
|
|
||||||
>
|
>
|
||||||
{sleeping ? (
|
{sleeping ? (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-md bg-slate-950/85 px-2 text-center">
|
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-md bg-slate-950/85 px-2 text-center">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-lg font-semibold text-slate-100">Balance Board is asleep</p>
|
<p className="text-lg font-semibold text-slate-100">The Balance Board is asleep</p>
|
||||||
<p className="text-sm text-slate-300">Press the front power button on the board to wake it.</p>
|
<p className="text-sm text-slate-300">Press the front power button on the board to wake it.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-0.5 md:grid-cols-[minmax(0,1.25fr)_minmax(9rem,0.75fr)]">
|
<div className="panel-section relative h-40 overflow-hidden">
|
||||||
<section className="surface-muted p-1">
|
<CornerReading className="left-0.5 top-0.5" label="Top left" value={corners.topLeft} />
|
||||||
<div className="relative mx-auto aspect-[1.35/1] w-full max-w-md overflow-hidden rounded-[1.5rem] border-2 border-slate-500 bg-gradient-to-b from-slate-700 to-slate-800 shadow-inner">
|
<CornerReading className="right-0.5 top-0.5" label="Top right" value={corners.topRight} />
|
||||||
<div className="absolute inset-[12%] rounded-[1rem] border border-slate-500/70 bg-slate-900/35" />
|
<CornerReading className="bottom-0.5 left-0.5" label="Bottom left" value={corners.bottomLeft} />
|
||||||
<CornerReading className="left-1 top-1" label="Top left" value={corners.topLeft} />
|
<CornerReading className="bottom-0.5 right-0.5" label="Bottom right" value={corners.bottomRight} />
|
||||||
<CornerReading className="right-1 top-1" label="Top right" value={corners.topRight} />
|
|
||||||
<CornerReading className="bottom-1 left-1" label="Bottom left" value={corners.bottomLeft} />
|
|
||||||
<CornerReading className="bottom-1 right-1" label="Bottom right" value={corners.bottomRight} />
|
|
||||||
<div
|
<div
|
||||||
aria-label="Center of pressure"
|
aria-label="Center of pressure"
|
||||||
className={`absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 shadow-lg transition-all duration-100 ${
|
className={`absolute z-10 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border transition-all duration-100 ${
|
||||||
center.active
|
center.active
|
||||||
? 'border-cyan-100 bg-cyan-400 shadow-cyan-400/50'
|
? 'border-sky-200 bg-sky-500'
|
||||||
: 'border-slate-400 bg-slate-600 opacity-50'
|
: 'border-neutral-500 bg-neutral-600 opacity-50'
|
||||||
}`}
|
}`}
|
||||||
style={{ left: `${center.left}%`, top: `${center.top}%` }}
|
style={{ left: `${center.left}%`, top: `${center.top}%` }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-0.5 left-1/2 h-1.5 w-8 -translate-x-1/2 rounded-full bg-sky-400/70" />
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
</div>
|
<div className="surface px-1 py-0.5 text-center">
|
||||||
</section>
|
<div className="text-[0.65rem] text-slate-400">Total weight</div>
|
||||||
|
<div className="font-mono text-3xl font-bold leading-none text-white">
|
||||||
<div className="grid content-start gap-0.5">
|
|
||||||
<section className="surface-muted px-1 py-1.5 text-center">
|
|
||||||
<div className="text-xs text-slate-400">Total weight</div>
|
|
||||||
<div className="mt-0.5 font-mono text-4xl font-bold leading-none text-white">
|
|
||||||
{formatWeight(liveFrame.totalKg)}
|
{formatWeight(liveFrame.totalKg)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
{isAdmin ? (
|
|
||||||
<section className="surface-muted grid gap-0.5 p-0.5">
|
|
||||||
{board?.address ? (
|
|
||||||
<div className="truncate text-center font-mono text-[0.65rem] text-slate-500">{board.address}</div>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-dark w-full text-xs disabled:opacity-50"
|
|
||||||
disabled={!board?.paired || unpairing}
|
|
||||||
onClick={unpair}
|
|
||||||
>
|
|
||||||
{unpairing ? 'Unpairing…' : 'Unpair board'}
|
|
||||||
</button>
|
|
||||||
{adminMessage ? <p className="text-center text-xs text-slate-400">{adminMessage}</p> : null}
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFrame>
|
</CardFrame>
|
||||||
|
|||||||
Reference in New Issue
Block a user