From 03f1c733fe3db1899fbe2a3625e05c6ff26c560b Mon Sep 17 00:00:00 2001 From: Soren Zaiser Date: Wed, 25 Sep 2024 16:44:00 -0400 Subject: [PATCH 1/2] add force push button --- front-end/src/api/use-socket.ts | 8 +- .../nexus-sheets/nexus-scoresheet.tsx | 213 ++++++++++++++---- 2 files changed, 177 insertions(+), 44 deletions(-) diff --git a/front-end/src/api/use-socket.ts b/front-end/src/api/use-socket.ts index 864e9ca3..7b37b475 100644 --- a/front-end/src/api/use-socket.ts +++ b/front-end/src/api/use-socket.ts @@ -1,5 +1,5 @@ import { createSocket } from '@toa-lib/client'; -import { Match, MatchKey, MatchSocketEvent } from '@toa-lib/models'; +import { FieldControlUpdatePacket, Match, MatchKey, MatchSocketEvent } from '@toa-lib/models'; import { Socket } from 'socket.io-client'; import { useRecoilCallback, useRecoilState } from 'recoil'; @@ -212,4 +212,10 @@ export async function sendUpdateFrcFmsSettings( socket?.emit('frc-fms:settings-update', { hwFingerprint }); } +export async function sendFCSPacket( + packet: FieldControlUpdatePacket +): Promise { + socket?.emit('fcs:update', packet); +} + export default socket; diff --git a/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx b/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx index 4046e685..faaa4da5 100644 --- a/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx +++ b/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx @@ -1,7 +1,13 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import Box from '@mui/material/Box'; -import { Alliance, MatchState } from '@toa-lib/models'; -import { Checkbox, Grid, Stack, Typography } from '@mui/material'; +import { + Alliance, + applySetpointToMotors, + FieldControlUpdatePacket, + MatchState, + Motor +} from '@toa-lib/models'; +import { Button, Checkbox, Grid, Stack, Typography } from '@mui/material'; import styled from '@emotion/styled'; import { AllianceNexusGoalState, @@ -10,6 +16,8 @@ import { } from '@toa-lib/models/build/seasons/FeedingTheFuture'; import { useRecoilValue } from 'recoil'; import { matchStateAtom } from 'src/stores/recoil'; +import { MotorA } from '@toa-lib/models/build/fcs/FeedingTheFutureFCS'; +import { sendFCSPacket } from 'src/api/use-socket'; interface NexusScoresheetProps { state?: AllianceNexusGoalState; @@ -26,6 +34,7 @@ interface NexusScoresheetProps { ) => void; side: 'near' | 'far' | 'both'; scorekeeperView?: boolean; + allowForcePush?: boolean; } const StairGoal = styled(Box)((props: { alliance: Alliance }) => ({ @@ -52,8 +61,30 @@ const NexusScoresheet: React.FC = ({ onChange, onOpposingChange, side, - scorekeeperView + scorekeeperView, + allowForcePush }) => { + const cancelQueue = useRef([]); + + useEffect(() => { + const interval = setInterval(() => { + cancelQueue.current = cancelQueue.current.filter((c) => { + if (c.time < Date.now()) { + c.callback(); + return false; + } + return true; + }); + }, 1000); + + return () => { + clearInterval(interval); + // cancel anything in the queue + cancelQueue.current.forEach((c) => c.callback()); + cancelQueue.current = []; + }; + }); + // If we're not passed in a state, we'll use the default state and disable the sheet if (!state) { state = { ...defaultNexusGoalState }; @@ -78,6 +109,48 @@ const NexusScoresheet: React.FC = ({ } }; + const onForceRelease = ( + alliance: Alliance, + goal: keyof AllianceNexusGoalState + ) => { + if (!allowForcePush) return; + // create packet to send to FCS + const packetOn: FieldControlUpdatePacket = { hubs: {}, wleds: {} }; + const packetOff: FieldControlUpdatePacket = { hubs: {}, wleds: {} }; + + // get motors for the goal + let Motors: Motor[] = []; + if (alliance === 'red') { + // If the alliance is red and we're updaing the side goals, return the blue side goals. otherwise, return the red center goals + // this is because the red ref is scoring for the blue side goals and the red center goals. + Motors = goal.startsWith('CW') + ? MotorA.BLUE_SIDE_GOALS + : MotorA.RED_CENTER_GOALS; + } else { + // opposite of above + Motors = goal.startsWith('CW') + ? MotorA.RED_SIDE_GOALS + : MotorA.BLUE_CENTER_GOALS; + } + + // get number off end of motor + const motorNumber = parseInt(goal.slice(2)) - 1; // -1 because soren indexed these stupid goals at 1 + const motor = Motors[motorNumber]; + + // apply setpoint to motor + applySetpointToMotors(1, [motor], packetOn); + applySetpointToMotors(0, [motor], packetOff); + + // send on packet to FCS + sendFCSPacket(packetOn); + + // add packetoff socket request to cancel queue + cancelQueue.current.push({ + time: Date.now() + 3000, + callback: () => sendFCSPacket(packetOff) + }); + }; + return ( <> {!scorekeeperView && ( @@ -90,6 +163,9 @@ const NexusScoresheet: React.FC = ({ state={opposingState} onGoalChange={onGoalChange} alliance={alliance === 'red' ? 'blue' : 'red'} // intentionally inverted + onForceRelease={(g) => + onForceRelease(alliance ? 'blue' : 'red', g) + } /> = ({ alliance={alliance} side={side} fullWidth={scorekeeperView} + onForceRelease={(g) => onForceRelease(alliance, g)} /> {/* Placeholder for better alignment */}   @@ -144,6 +221,7 @@ interface GoalGridProps { state: NexusGoalState ) => void; alliance: Alliance; + onForceRelease?: (goal: keyof AllianceNexusGoalState) => void; } interface CenterGoalGridProps extends GoalGridProps { @@ -155,8 +233,14 @@ const StepGoalGrid: React.FC = ({ disabled, state, onGoalChange, - alliance + alliance, + onForceRelease }) => { + const onForceReleaseLocal = (goal: keyof AllianceNexusGoalState) => { + if (!onForceRelease) return; + onForceRelease(goal); + }; + /* * Stair-step goals * Blue steps down, red steps up. We'll reverse the row to handle that. CSS hax @@ -172,6 +256,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW1} onChange={(s) => onGoalChange('CW1', s)} + onForceRelease={() => onForceReleaseLocal('CW1')} /> @@ -179,6 +264,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW2} onChange={(s) => onGoalChange('CW2', s)} + onForceRelease={() => onForceReleaseLocal('CW2')} /> @@ -188,6 +274,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW3} onChange={(s) => onGoalChange('CW3', s)} + onForceRelease={() => onForceReleaseLocal('CW3')} /> @@ -195,6 +282,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW4} onChange={(s) => onGoalChange('CW4', s)} + onForceRelease={() => onForceReleaseLocal('CW4')} /> @@ -204,6 +292,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW5} onChange={(s) => onGoalChange('CW5', s)} + onForceRelease={() => onForceReleaseLocal('CW5')} /> @@ -211,6 +300,7 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW6} onChange={(s) => onGoalChange('CW6', s)} + onForceRelease={() => onForceReleaseLocal('CW6')} /> @@ -223,7 +313,8 @@ const CenterGoalGrid: React.FC = ({ onGoalChange, alliance, side, - fullWidth + fullWidth, + onForceRelease }) => { /* * Center-field 3x2 goal. @@ -234,6 +325,12 @@ const CenterGoalGrid: React.FC = ({ */ const directionBlue = side === 'far' ? 'row' : 'row-reverse'; const directionRed = side === 'far' ? 'row-reverse' : 'row'; + + const onForceReleaseLocal = (goal: keyof AllianceNexusGoalState) => { + if (!onForceRelease) return; + onForceRelease(goal); + }; + return ( = ({ disabled={disabled} state={state.EC1} onChange={(s) => onGoalChange('EC1', s)} + onForceRelease={() => onForceReleaseLocal('EC1')} /> @@ -254,6 +352,7 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC2} onChange={(s) => onGoalChange('EC2', s)} + onForceRelease={() => onForceReleaseLocal('EC2')} /> @@ -261,6 +360,7 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC3} onChange={(s) => onGoalChange('EC3', s)} + onForceRelease={() => onForceReleaseLocal('EC3')} /> @@ -273,6 +373,7 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC6} onChange={(s) => onGoalChange('EC6', s)} + onForceRelease={() => onForceReleaseLocal('EC6')} /> @@ -280,6 +381,7 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC5} onChange={(s) => onGoalChange('EC5', s)} + onForceRelease={() => onForceReleaseLocal('EC5')} /> @@ -287,6 +389,7 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC4} onChange={(s) => onGoalChange('EC4', s)} + onForceRelease={() => onForceReleaseLocal('EC4')} /> @@ -300,6 +403,7 @@ interface GoalToggleProps { state: NexusGoalState; onChange?: (goal: NexusGoalState) => void; single?: boolean; + onForceRelease?: () => void; } const BallCheckbox = styled(Checkbox)( @@ -324,7 +428,8 @@ const GoalToggle: React.FC = ({ disabled, state, onChange, - single + single, + onForceRelease }) => { const matchState = useRecoilValue(matchStateAtom); @@ -373,44 +478,66 @@ const GoalToggle: React.FC = ({ } }; + const onForceReleaseLocal = () => { + if (!onForceRelease) return; + onForceRelease(); + }; + return ( - + {NexusGoalState.Produced === state && ( + + + + )} + - - - + ? '5px dashed orange' + : undefined + }} + direction={single ? 'row' : 'column'} + flexGrow={1} + justifyContent={'center'} + > + + + + ); }; From 5e3fddf24f6c938b1140d88c3153175bcf02ee2e Mon Sep 17 00:00:00 2001 From: Soren Zaiser Date: Thu, 26 Sep 2024 01:00:46 -0400 Subject: [PATCH 2/2] post-testing updates --- front-end/src/api/use-socket.ts | 7 +- .../nexus-sheets/nexus-scoresheet.tsx | 69 ++++++++++++------- .../src/seasons/fgc-2024/referee/HRExtra.tsx | 2 + .../fgc-2024/referee/TeleOpScoreSheet.tsx | 1 + 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/front-end/src/api/use-socket.ts b/front-end/src/api/use-socket.ts index 7b37b475..1aa14e6e 100644 --- a/front-end/src/api/use-socket.ts +++ b/front-end/src/api/use-socket.ts @@ -1,5 +1,10 @@ import { createSocket } from '@toa-lib/client'; -import { FieldControlUpdatePacket, Match, MatchKey, MatchSocketEvent } from '@toa-lib/models'; +import { + FieldControlUpdatePacket, + Match, + MatchKey, + MatchSocketEvent +} from '@toa-lib/models'; import { Socket } from 'socket.io-client'; import { useRecoilCallback, useRecoilState } from 'recoil'; diff --git a/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx b/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx index faaa4da5..0bf684a7 100644 --- a/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx +++ b/front-end/src/seasons/fgc-2024/nexus-sheets/nexus-scoresheet.tsx @@ -34,7 +34,7 @@ interface NexusScoresheetProps { ) => void; side: 'near' | 'far' | 'both'; scorekeeperView?: boolean; - allowForcePush?: boolean; + allowForceRelease?: boolean; } const StairGoal = styled(Box)((props: { alliance: Alliance }) => ({ @@ -62,7 +62,7 @@ const NexusScoresheet: React.FC = ({ onOpposingChange, side, scorekeeperView, - allowForcePush + allowForceRelease }) => { const cancelQueue = useRef([]); @@ -113,7 +113,8 @@ const NexusScoresheet: React.FC = ({ alliance: Alliance, goal: keyof AllianceNexusGoalState ) => { - if (!allowForcePush) return; + console.log(allowForceRelease); + if (!allowForceRelease) return; // create packet to send to FCS const packetOn: FieldControlUpdatePacket = { hubs: {}, wleds: {} }; const packetOff: FieldControlUpdatePacket = { hubs: {}, wleds: {} }; @@ -163,9 +164,8 @@ const NexusScoresheet: React.FC = ({ state={opposingState} onGoalChange={onGoalChange} alliance={alliance === 'red' ? 'blue' : 'red'} // intentionally inverted - onForceRelease={(g) => - onForceRelease(alliance ? 'blue' : 'red', g) - } + onForceRelease={(g) => onForceRelease(alliance, g)} + allowForceRelease={!!allowForceRelease} /> = ({ side={side} fullWidth={scorekeeperView} onForceRelease={(g) => onForceRelease(alliance, g)} + allowForceRelease={!!allowForceRelease} /> {/* Placeholder for better alignment */}   @@ -222,6 +223,7 @@ interface GoalGridProps { ) => void; alliance: Alliance; onForceRelease?: (goal: keyof AllianceNexusGoalState) => void; + allowForceRelease: boolean; } interface CenterGoalGridProps extends GoalGridProps { @@ -234,7 +236,8 @@ const StepGoalGrid: React.FC = ({ state, onGoalChange, alliance, - onForceRelease + onForceRelease, + allowForceRelease }) => { const onForceReleaseLocal = (goal: keyof AllianceNexusGoalState) => { if (!onForceRelease) return; @@ -257,6 +260,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW1} onChange={(s) => onGoalChange('CW1', s)} onForceRelease={() => onForceReleaseLocal('CW1')} + allowForceRelease={allowForceRelease} /> @@ -265,6 +269,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW2} onChange={(s) => onGoalChange('CW2', s)} onForceRelease={() => onForceReleaseLocal('CW2')} + allowForceRelease={allowForceRelease} /> @@ -275,6 +280,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW3} onChange={(s) => onGoalChange('CW3', s)} onForceRelease={() => onForceReleaseLocal('CW3')} + allowForceRelease={allowForceRelease} /> @@ -283,6 +289,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW4} onChange={(s) => onGoalChange('CW4', s)} onForceRelease={() => onForceReleaseLocal('CW4')} + allowForceRelease={allowForceRelease} /> @@ -293,6 +300,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW5} onChange={(s) => onGoalChange('CW5', s)} onForceRelease={() => onForceReleaseLocal('CW5')} + allowForceRelease={allowForceRelease} /> @@ -301,6 +309,7 @@ const StepGoalGrid: React.FC = ({ state={state.CW6} onChange={(s) => onGoalChange('CW6', s)} onForceRelease={() => onForceReleaseLocal('CW6')} + allowForceRelease={allowForceRelease} /> @@ -314,7 +323,8 @@ const CenterGoalGrid: React.FC = ({ alliance, side, fullWidth, - onForceRelease + onForceRelease, + allowForceRelease }) => { /* * Center-field 3x2 goal. @@ -345,6 +355,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC1} onChange={(s) => onGoalChange('EC1', s)} onForceRelease={() => onForceReleaseLocal('EC1')} + allowForceRelease={allowForceRelease} /> @@ -353,6 +364,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC2} onChange={(s) => onGoalChange('EC2', s)} onForceRelease={() => onForceReleaseLocal('EC2')} + allowForceRelease={allowForceRelease} /> @@ -361,6 +373,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC3} onChange={(s) => onGoalChange('EC3', s)} onForceRelease={() => onForceReleaseLocal('EC3')} + allowForceRelease={allowForceRelease} /> @@ -374,6 +387,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC6} onChange={(s) => onGoalChange('EC6', s)} onForceRelease={() => onForceReleaseLocal('EC6')} + allowForceRelease={allowForceRelease} /> @@ -382,6 +396,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC5} onChange={(s) => onGoalChange('EC5', s)} onForceRelease={() => onForceReleaseLocal('EC5')} + allowForceRelease={allowForceRelease} /> @@ -390,6 +405,7 @@ const CenterGoalGrid: React.FC = ({ state={state.EC4} onChange={(s) => onGoalChange('EC4', s)} onForceRelease={() => onForceReleaseLocal('EC4')} + allowForceRelease={allowForceRelease} /> @@ -404,6 +420,7 @@ interface GoalToggleProps { onChange?: (goal: NexusGoalState) => void; single?: boolean; onForceRelease?: () => void; + allowForceRelease: boolean; } const BallCheckbox = styled(Checkbox)( @@ -429,7 +446,8 @@ const GoalToggle: React.FC = ({ state, onChange, single, - onForceRelease + onForceRelease, + allowForceRelease }) => { const matchState = useRecoilValue(matchStateAtom); @@ -485,21 +503,24 @@ const GoalToggle: React.FC = ({ return ( <> - {NexusGoalState.Produced === state && ( - - - - )} + {NexusGoalState.Produced === state && + allowForceRelease && + matchState < MatchState.MATCH_COMPLETE && ( + + + + )} { } scorekeeperView side='near' + allowForceRelease /> @@ -183,6 +184,7 @@ const HeadRefereeExtra: React.FC = () => { } scorekeeperView side='near' + allowForceRelease /> diff --git a/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx b/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx index 5faeda00..4a6e9a3c 100644 --- a/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx +++ b/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx @@ -219,6 +219,7 @@ const TeleScoreSheet: FC = ({ onOpposingChange={updateOpposingNexusState} alliance={alliance} side={'far'} + allowForceRelease /> {participants?.map((p) => {