diff --git a/front-end/src/api/use-socket.ts b/front-end/src/api/use-socket.ts index 864e9ca3..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 { 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 +217,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..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 @@ -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; + allowForceRelease?: boolean; } const StairGoal = styled(Box)((props: { alliance: Alliance }) => ({ @@ -52,8 +61,30 @@ const NexusScoresheet: React.FC = ({ onChange, onOpposingChange, side, - scorekeeperView + scorekeeperView, + allowForceRelease }) => { + 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,49 @@ const NexusScoresheet: React.FC = ({ } }; + const onForceRelease = ( + alliance: Alliance, + goal: keyof AllianceNexusGoalState + ) => { + console.log(allowForceRelease); + if (!allowForceRelease) 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 +164,8 @@ const NexusScoresheet: React.FC = ({ state={opposingState} onGoalChange={onGoalChange} alliance={alliance === 'red' ? 'blue' : 'red'} // intentionally inverted + onForceRelease={(g) => onForceRelease(alliance, g)} + allowForceRelease={!!allowForceRelease} /> = ({ alliance={alliance} side={side} fullWidth={scorekeeperView} + onForceRelease={(g) => onForceRelease(alliance, g)} + allowForceRelease={!!allowForceRelease} /> {/* Placeholder for better alignment */}   @@ -144,6 +222,8 @@ interface GoalGridProps { state: NexusGoalState ) => void; alliance: Alliance; + onForceRelease?: (goal: keyof AllianceNexusGoalState) => void; + allowForceRelease: boolean; } interface CenterGoalGridProps extends GoalGridProps { @@ -155,8 +235,15 @@ const StepGoalGrid: React.FC = ({ disabled, state, onGoalChange, - alliance + alliance, + onForceRelease, + allowForceRelease }) => { + 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 +259,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW1} onChange={(s) => onGoalChange('CW1', s)} + onForceRelease={() => onForceReleaseLocal('CW1')} + allowForceRelease={allowForceRelease} /> @@ -179,6 +268,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW2} onChange={(s) => onGoalChange('CW2', s)} + onForceRelease={() => onForceReleaseLocal('CW2')} + allowForceRelease={allowForceRelease} /> @@ -188,6 +279,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW3} onChange={(s) => onGoalChange('CW3', s)} + onForceRelease={() => onForceReleaseLocal('CW3')} + allowForceRelease={allowForceRelease} /> @@ -195,6 +288,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW4} onChange={(s) => onGoalChange('CW4', s)} + onForceRelease={() => onForceReleaseLocal('CW4')} + allowForceRelease={allowForceRelease} /> @@ -204,6 +299,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW5} onChange={(s) => onGoalChange('CW5', s)} + onForceRelease={() => onForceReleaseLocal('CW5')} + allowForceRelease={allowForceRelease} /> @@ -211,6 +308,8 @@ const StepGoalGrid: React.FC = ({ disabled={disabled} state={state.CW6} onChange={(s) => onGoalChange('CW6', s)} + onForceRelease={() => onForceReleaseLocal('CW6')} + allowForceRelease={allowForceRelease} /> @@ -223,7 +322,9 @@ const CenterGoalGrid: React.FC = ({ onGoalChange, alliance, side, - fullWidth + fullWidth, + onForceRelease, + allowForceRelease }) => { /* * Center-field 3x2 goal. @@ -234,6 +335,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')} + allowForceRelease={allowForceRelease} /> @@ -254,6 +363,8 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC2} onChange={(s) => onGoalChange('EC2', s)} + onForceRelease={() => onForceReleaseLocal('EC2')} + allowForceRelease={allowForceRelease} /> @@ -261,6 +372,8 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC3} onChange={(s) => onGoalChange('EC3', s)} + onForceRelease={() => onForceReleaseLocal('EC3')} + allowForceRelease={allowForceRelease} /> @@ -273,6 +386,8 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC6} onChange={(s) => onGoalChange('EC6', s)} + onForceRelease={() => onForceReleaseLocal('EC6')} + allowForceRelease={allowForceRelease} /> @@ -280,6 +395,8 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC5} onChange={(s) => onGoalChange('EC5', s)} + onForceRelease={() => onForceReleaseLocal('EC5')} + allowForceRelease={allowForceRelease} /> @@ -287,6 +404,8 @@ const CenterGoalGrid: React.FC = ({ disabled={disabled} state={state.EC4} onChange={(s) => onGoalChange('EC4', s)} + onForceRelease={() => onForceReleaseLocal('EC4')} + allowForceRelease={allowForceRelease} /> @@ -300,6 +419,8 @@ interface GoalToggleProps { state: NexusGoalState; onChange?: (goal: NexusGoalState) => void; single?: boolean; + onForceRelease?: () => void; + allowForceRelease: boolean; } const BallCheckbox = styled(Checkbox)( @@ -324,7 +445,9 @@ const GoalToggle: React.FC = ({ disabled, state, onChange, - single + single, + onForceRelease, + allowForceRelease }) => { const matchState = useRecoilValue(matchStateAtom); @@ -373,44 +496,69 @@ const GoalToggle: React.FC = ({ } }; + const onForceReleaseLocal = () => { + if (!onForceRelease) return; + onForceRelease(); + }; + return ( - + {NexusGoalState.Produced === state && + allowForceRelease && + matchState < MatchState.MATCH_COMPLETE && ( + + + + )} + - - - + ? '5px dashed orange' + : undefined + }} + direction={single ? 'row' : 'column'} + flexGrow={1} + justifyContent={'center'} + > + + + + ); }; diff --git a/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx b/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx index 6079dbb2..98270c02 100644 --- a/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx +++ b/front-end/src/seasons/fgc-2024/referee/HRExtra.tsx @@ -189,6 +189,7 @@ const HeadRefereeExtra: React.FC = () => { } scorekeeperView side='near' + allowForceRelease /> @@ -200,6 +201,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 9913e067..a30622a9 100644 --- a/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx +++ b/front-end/src/seasons/fgc-2024/referee/TeleOpScoreSheet.tsx @@ -176,6 +176,7 @@ const TeleScoreSheet: FC = ({ onOpposingChange={updateOpposingNexusState} alliance={alliance} side={'far'} + allowForceRelease /> {participants?.map((p) => {