lidar reasons

This commit is contained in:
legop3
2026-05-01 18:26:16 -04:00
parent 3bb16c140e
commit 458199ad11
5 changed files with 99 additions and 23 deletions
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
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-Wmnr23kh.js"></script> <script type="module" crossorigin src="/assets/index-BQtp08e2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ddqa5PDb.css"> <link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -72,6 +72,16 @@ function createLidarRuntime({ logger, host, port = 6053, key, logFile = '', shou
requestInFlight: false, requestInFlight: false,
requestStartedAt: 0, requestStartedAt: 0,
logStream: null, logStream: null,
stats: {
headersSeen: 0,
rotationsSeen: 0,
scansOk: 0,
scansTimedOut: 0,
scansRestarted: 0,
rotationsWithoutScan: 0,
parseablePayloads: 0,
pointPayloads: 0,
},
}; };
function emitStatus() { function emitStatus() {
@@ -119,25 +129,56 @@ function createLidarRuntime({ logger, host, port = 6053, key, logFile = '', shou
state.requestStartedAt = 0; state.requestStartedAt = 0;
} }
function finalizeScan() { function buildPoints(scan) {
return Array.from(scan?.points?.values?.() || []).sort((a, b) => a.angleDeg - b.angleDeg);
}
function emitFrame({ status, reason, scan = state.currentScan }) {
const points = buildPoints(scan);
const payload = {
points,
rotationSpeed: scan?.rotationSpeed ?? null,
status,
debug: {
reason,
pointsReceived: points.length,
validPoints: points.filter((point) => point?.valid).length,
requestInFlight: state.requestInFlight,
requestStartedAt: state.requestStartedAt || null,
stats: { ...state.stats },
},
};
events.emit('scan', payload);
return payload;
}
function finalizeScan(reason = 'rotation_complete') {
if (!state.currentScan) { if (!state.currentScan) {
state.stats.rotationsWithoutScan += 1;
emitFrame({ status: 'error', reason: 'rotation_without_scan', scan: null });
resetScanState(); resetScanState();
return; return;
} }
const points = Array.from(state.currentScan.points.values()).sort((a, b) => a.angleDeg - b.angleDeg); state.stats.scansOk += 1;
const payload = { const payload = emitFrame({ status: 'ok', reason });
points, logger.info('Neato lidar scan parsed', {
rotationSpeed: state.currentScan.rotationSpeed, points: payload.points.length,
}; rotationSpeed: payload.rotationSpeed,
logger.info('Neato lidar scan parsed', { points: points.length, rotationSpeed: payload.rotationSpeed }); reason,
});
resetScanState(); resetScanState();
events.emit('scan', payload);
triggerPollSoon(); triggerPollSoon();
} }
function handlePayload(payload) { function handlePayload(payload) {
if (!payload) return; if (!payload) return;
state.stats.parseablePayloads += 1;
if (payload === 'AngleInDegrees,DistInMM,Intensity,ErrorCodeHEX') { if (payload === 'AngleInDegrees,DistInMM,Intensity,ErrorCodeHEX') {
state.stats.headersSeen += 1;
if (state.currentScan) {
state.stats.scansRestarted += 1;
emitFrame({ status: 'error', reason: 'header_restart' });
}
state.currentScan = { state.currentScan = {
points: new Map(), points: new Map(),
rotationSpeed: null, rotationSpeed: null,
@@ -146,12 +187,16 @@ function createLidarRuntime({ logger, host, port = 6053, key, logFile = '', shou
} }
const rotationSpeed = parseRotationSpeed(payload); const rotationSpeed = parseRotationSpeed(payload);
if (rotationSpeed != null) { if (rotationSpeed != null) {
if (state.currentScan) state.currentScan.rotationSpeed = rotationSpeed; state.stats.rotationsSeen += 1;
finalizeScan(); if (state.currentScan) {
state.currentScan.rotationSpeed = rotationSpeed;
}
finalizeScan('rotation_complete');
return; return;
} }
const point = parsePointPayload(payload); const point = parsePointPayload(payload);
if (!point || !state.currentScan) return; if (!point || !state.currentScan) return;
state.stats.pointPayloads += 1;
state.currentScan.points.set(point.angleDeg, point); state.currentScan.points.set(point.angleDeg, point);
} }
@@ -274,6 +319,8 @@ function createLidarRuntime({ logger, host, port = 6053, key, logFile = '', shou
clearRequestTimeoutTimer(); clearRequestTimeoutTimer();
state.requestTimeoutTimer = setTimeout(() => { state.requestTimeoutTimer = setTimeout(() => {
logger.warn('Neato lidar scan timed out; resetting parser state'); logger.warn('Neato lidar scan timed out; resetting parser state');
state.stats.scansTimedOut += 1;
emitFrame({ status: 'error', reason: 'scan_timeout' });
resetScanState(); resetScanState();
triggerPollSoon(); triggerPollSoon();
}, SCAN_TIMEOUT_MS); }, SCAN_TIMEOUT_MS);
+30 -1
View File
@@ -67,6 +67,7 @@ export default function VipNeatoCard({
}) { }) {
const [working, setWorking] = useState(''); const [working, setWorking] = useState('');
const [lidarFlash, setLidarFlash] = useState(false); const [lidarFlash, setLidarFlash] = useState(false);
const [showLidarDebug, setShowLidarDebug] = useState(false);
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl'; const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
const configured = Boolean(neato?.configured); const configured = Boolean(neato?.configured);
@@ -84,6 +85,10 @@ export default function VipNeatoCard({
const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--'; const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--';
const lidarPoints = Array.isArray(lidar?.points) ? lidar.points : []; const lidarPoints = Array.isArray(lidar?.points) ? lidar.points : [];
const lidarDots = buildLidarDots(lidarPoints); const lidarDots = buildLidarDots(lidarPoints);
const lidarStatus = normalizeState(lidar?.status) || '--';
const lidarDebug = lidar?.debug && typeof lidar.debug === 'object' ? lidar.debug : null;
const lidarReason = normalizeState(lidarDebug?.reason) || '--';
const lidarStats = lidarDebug?.stats && typeof lidarDebug.stats === 'object' ? lidarDebug.stats : null;
const controls = neato?.controls || {}; const controls = neato?.controls || {};
const canStart = Boolean(controls?.start?.available); const canStart = Boolean(controls?.start?.available);
@@ -236,7 +241,13 @@ export default function VipNeatoCard({
</div> </div>
<div className={`rounded-md px-1 py-0.5 ${lidarFlash ? 'bg-emerald-900' : 'bg-slate-800'}`}> <div className={`rounded-md px-1 py-0.5 ${lidarFlash ? 'bg-emerald-900' : 'bg-slate-800'}`}>
<div className="text-[0.72rem] text-slate-300">Lidar</div> <button
type="button"
onClick={() => setShowLidarDebug((value) => !value)}
className="w-full text-left text-[0.72rem] text-slate-300"
>
Lidar
</button>
<div className="mt-0.25 aspect-square rounded-md bg-slate-900 p-0.25"> <div className="mt-0.25 aspect-square rounded-md bg-slate-900 p-0.25">
{lidarDots ? ( {lidarDots ? (
<svg viewBox="0 0 220 220" className="h-full w-full"> <svg viewBox="0 0 220 220" className="h-full w-full">
@@ -261,6 +272,24 @@ export default function VipNeatoCard({
</div> </div>
)} )}
</div> </div>
{showLidarDebug ? (
<div className="mt-0.5 rounded-md bg-slate-900 px-0.75 py-0.5 font-mono text-[0.68rem] text-slate-300">
<div>Status: {lidarStatus}</div>
<div>Reason: {lidarReason}</div>
<div>Points: {lidarPoints.length}</div>
<div>Rotation: {Number.isFinite(lidar?.rotationSpeed) ? lidar.rotationSpeed.toFixed(2) : '--'}</div>
<div>Request in flight: {lidarDebug?.requestInFlight ? 'yes' : 'no'}</div>
<div>Started at: {lidarDebug?.requestStartedAt || '--'}</div>
<div>Headers: {lidarStats?.headersSeen ?? '--'}</div>
<div>Rotations: {lidarStats?.rotationsSeen ?? '--'}</div>
<div>Scans ok: {lidarStats?.scansOk ?? '--'}</div>
<div>Timeouts: {lidarStats?.scansTimedOut ?? '--'}</div>
<div>Restarts: {lidarStats?.scansRestarted ?? '--'}</div>
<div>Rotation no scan: {lidarStats?.rotationsWithoutScan ?? '--'}</div>
<div>Parseable payloads: {lidarStats?.parseablePayloads ?? '--'}</div>
<div>Point payloads: {lidarStats?.pointPayloads ?? '--'}</div>
</div>
) : null}
</div> </div>
</div> </div>
</div> </div>