slopcurrent

This commit is contained in:
legop3
2026-07-17 14:20:06 -04:00
parent 1cd0e05b4a
commit d7fb15d891
17 changed files with 1054 additions and 469 deletions
@@ -1,63 +1,72 @@
// Overcurrent Overlay
// Purpose: Defines the Overcurrent Overlay module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import React from 'react';
import { useMemo } from 'react';
// Purpose: Shows server-authoritative motor limiting, stop, recovery, and administrator-bypass status.
// Scope: Renders protection state only; it never calculates stress or changes motor commands.
import React, { useMemo } from 'react';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useVisualTelemetrySelector } from '../../../context/TelemetryContext.jsx';
import { overcurrentFlagsEqual, selectOvercurrentFlags } from '../../../context/telemetryViews.js';
import { useOvercurrentLimiter } from '../../../controls/index.js';
import { OVERCURRENT_LABELS } from './constants.js';
function OvercurrentOverlay({ roverId = null, sensors, overcurrentLimiter = null, compact = false }) {
function OvercurrentOverlay({ roverId = null, overcurrentLimiter = null, compact = false }) {
const assignedRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const effectiveRoverId = roverId ?? assignedRoverId;
const selectedOvercurrents = useVisualTelemetrySelector(effectiveRoverId, selectOvercurrentFlags, overcurrentFlagsEqual);
const internalLimiter = useOvercurrentLimiter(effectiveRoverId);
const resolvedOvercurrents = sensors?.wheelOvercurrents ?? selectedOvercurrents;
const resolvedOvercurrentLimiter = overcurrentLimiter ?? internalLimiter ?? null;
const overcurrentMotors = useMemo(
() =>
resolvedOvercurrents == null
? []
: Object.entries(resolvedOvercurrents)
.filter(([, active]) => Boolean(active))
.map(([key]) => key),
[resolvedOvercurrents],
const protection = overcurrentLimiter ?? internalLimiter;
const status = protection?.status || 'idle';
const motors = protection?.motors || {};
const activeMotors = useMemo(
() => Object.entries(motors)
.filter(([, motor]) => Boolean(motor?.overcurrent) || Number(motor?.stress) > 0)
.map(([key]) => key),
[motors],
);
const limiterCaps = resolvedOvercurrentLimiter?.caps || null;
const limiterFill = useMemo(() => {
if (!limiterCaps) return null;
const driveCap = Number.isFinite(limiterCaps?.drive?.cap) ? limiterCaps.drive.cap : 1;
const auxCap = Number.isFinite(limiterCaps?.aux?.cap) ? limiterCaps.aux.cap : 1;
return Math.max(0, Math.min(1, 1 - Math.min(driveCap, auxCap)));
}, [limiterCaps]);
const limiterActive = Boolean(resolvedOvercurrentLimiter?.isActive);
const motors = useMemo(
() => (overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : []),
[overcurrentMotors, limiterActive],
);
const fill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
if (!motors?.length) return null;
const safeLabels = motors.map((name) => OVERCURRENT_LABELS[name] || name);
const containerClass = compact ? 'w-[12rem] h-[3.5rem]' : 'w-[20rem] h-[7rem]';
const padClass = compact ? 'px-2 py-1' : 'px-4 py-2';
const textClass = compact ? 'text-lg' : 'text-4xl';
const subTextClass = compact ? 'text-xs' : 'text-xl';
const safeFill = Math.max(0, Math.min(1, fill));
const fillWidth = `${Math.round(safeFill * 100)}%`;
if (status === 'idle') return null;
const stopReason = protection?.drive?.stopReason;
const displayMotors = stopReason ? [stopReason] : activeMotors;
const labels = displayMotors.map((name) => OVERCURRENT_LABELS[name] || name);
const highestStress = displayMotors.reduce(
(highest, name) => Math.max(highest, Number(motors?.[name]?.stress) || 0),
0,
);
const driveCap = Number.isFinite(protection?.drive?.cap) ? protection.drive.cap : 1;
const fillWidth = `${Math.round(Math.max(0, Math.min(1, highestStress)) * 100)}%`;
const bypassed = status === 'bypassed';
const stopped = status === 'stopped';
const title = bypassed
? 'Overcurrent detected'
: stopped
? 'Drive stopped'
: status === 'recovering'
? 'Protection recovering'
: 'Overcurrent limiting';
const detail = bypassed
? 'Admin bypass'
: stopped && protection?.drive?.requiresNeutral
? `${labels.join(', ') || 'Wheel stall'} · release controls to resume`
: status === 'limiting'
? `${labels.join(', ')} · output ${Math.round(driveCap * 100)}%`
: labels.join(', ');
const containerClass = bypassed
? 'h-[3.5rem] w-[14rem]'
: compact
? 'h-[3.5rem] w-[14rem]'
: 'h-[7rem] w-[22rem]';
const titleClass = compact || bypassed ? 'text-base' : 'text-3xl';
const detailClass = compact || bypassed ? 'text-xs' : 'text-base';
const backgroundClass = bypassed ? 'bg-amber-950/75' : 'bg-red-950/70';
const fillClass = bypassed ? 'bg-amber-700/50' : 'bg-red-700/60';
return (
<div
className={`pointer-events-none absolute flex items-center justify-center bg-red-900/50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ${containerClass}`}
className={`pointer-events-none absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center ${backgroundClass} ${containerClass}`}
>
<div className="relative h-full w-full">
<div className="absolute inset-0 overflow-hidden">
<div className="h-full bg-red-700/60" style={{ width: fillWidth }} />
</div>
<div className={`relative z-10 flex h-full w-full flex-col items-center justify-center text-center font-semibold text-white animate-pulse ${textClass} ${padClass}`}>
<div>OVERCURRENT</div>
<div className={`mt-0 font-medium text-white ${subTextClass}`}>{safeLabels.join(', ')}</div>
<div className="relative h-full w-full overflow-hidden">
<div className={`absolute inset-y-0 left-0 ${fillClass}`} style={{ width: fillWidth }} />
<div className="relative z-10 flex h-full flex-col items-center justify-center px-2 text-center font-semibold text-white">
<div className={titleClass}>{title}</div>
<div className={`font-medium ${detailClass}`}>{detail}</div>
</div>
</div>
</div>