From ec6227640045ad131070dfb0d09a2fed8db8ced7 Mon Sep 17 00:00:00 2001 From: "I. A. Naval" <790279+ianonavy@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:41:53 -0400 Subject: [PATCH] feat: Auto choose team from robot and match number See !108 for the original version of this feature. This PR adds a new field type TBA-assigned-station and updates the default config to include the new field. We update the schema for TBA-team-and-robot robotPosition to be constrained to the values generated by the current TBATeamAndRobotInput, which is descended from the "Robot" field from previous year's default config. These values are not end user configurable, so this should not break any preexisting configs. I also added a new pattern for defaulting choices for types that extend select, including the labels. The gen schema script was not working for me, so I switched it to npx tsx. If you'd prefer I keep that in a separate PR, happy to do that. There was some preexisting drift in the schema relating to "year" that I decided to leave in. I can also revert that if it's an issue. Testing methodology: 1. Reset to default config 2. Edit config to change to a team that attended a week 0 event (e.g. 3467) 3. Prefill match data from a past event 4. Select an assigned station; observe the Team and Robot is auto-selected 5. Update the match number; observe the same Robot and new Team is auto-selected --- config/2026/config.json | 11 +- package.json | 2 +- public/schema.json | 114 +++++++++++++++++- src/assets/schema.json | 114 +++++++++++++++++- src/components/inputs/BaseInputProps.ts | 34 +++++- src/components/inputs/ConfigurableInput.tsx | 2 + .../inputs/TBATeamAndRobotInput.tsx | 22 ++-- 7 files changed, 277 insertions(+), 22 deletions(-) diff --git a/config/2026/config.json b/config/2026/config.json index 9b0ef6306..914b74ec7 100644 --- a/config/2026/config.json +++ b/config/2026/config.json @@ -74,6 +74,14 @@ "formResetBehavior": "preserve", "defaultValue": "" }, + { + "title": "Assigned Station", + "description": "The station assigned to the scout (e.g., R1, R2, R3, B1, B2, B3). Use this to automatically assign the team and robot position based on the match number and station.", + "type": "TBA-assigned-station", + "required": false, + "code": "assignedStation", + "formResetBehavior": "preserve" + }, { "title": "Match Number", "description": "Select the match number.", @@ -90,7 +98,8 @@ "required": true, "code": "robot", "formResetBehavior": "preserve", - "defaultValue": null + "defaultValue": null, + "autoAssignFromFieldCode": "assignedStation" }, { "title": "Starting Position", diff --git a/package.json b/package.json index 20fb5a02b..8f4e31b2f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dev": "vite", "build": "tsc && vite build ", "preview": "vite preview", - "schema": "ts-node src/scripts/generateJsonSchema.ts src/assets/schema.json && cp src/assets/schema.json public/schema.json" + "schema": "npx tsx src/scripts/generateJsonSchema.ts src/assets/schema.json && cp src/assets/schema.json public/schema.json" }, "dependencies": { "@headlessui/react": "^1.7.19", diff --git a/public/schema.json b/public/schema.json index fa58f2281..84cc3413a 100644 --- a/public/schema.json +++ b/public/schema.json @@ -11,7 +11,7 @@ }, "year": { "type": "number", - "description": "The year this scouting config is relevant for." + "description": "The year this scouting config is relevant for. Defaults to the current year if not provided." }, "delimiter": { "type": "string", @@ -294,6 +294,45 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "title": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title" + }, + "description": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description" + }, + "type": { + "type": "string", + "const": "multi-counter" + }, + "required": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required" + }, + "code": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code" + }, + "disabled": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled" + }, + "formResetBehavior": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior" + }, + "defaultValue": { + "type": "number", + "default": 0, + "description": "The default value" + } + }, + "required": [ + "title", + "type", + "required", + "code" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -741,6 +780,10 @@ "timerDuration": { "type": "number", "description": "Expected duration in seconds (for UI reference, e.g., 15 for auto, 135 for teleop)" + }, + "autoStopSeconds": { + "type": "number", + "description": "Automatically stop the timer after this many seconds. Useful to prevent the timer from running past the match phase duration." } }, "required": [ @@ -786,7 +829,16 @@ "type": "number" }, "robotPosition": { - "type": "string" + "type": "string", + "enum": [ + "R1", + "R2", + "R3", + "B1", + "B2", + "B3" + ], + "description": "The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance." } }, "required": [ @@ -801,6 +853,10 @@ ], "default": null, "description": "The default team and robot position" + }, + "autoAssignFromFieldCode": { + "type": "string", + "description": "Optional code of another field to auto-assign" } }, "required": [ @@ -857,6 +913,59 @@ "code" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "title": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title" + }, + "description": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description" + }, + "type": { + "type": "string", + "const": "TBA-assigned-station" + }, + "required": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required" + }, + "code": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code" + }, + "disabled": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled" + }, + "formResetBehavior": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior" + }, + "defaultValue": { + "type": "string", + "default": "", + "description": "If assigning by driver station, the driver station to use for selecting team and robot position across matches" + }, + "choices": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "R1": "Red 1", + "R2": "Red 2", + "R3": "Red 3", + "B1": "Blue 1", + "B2": "Blue 2", + "B3": "Blue 3" + } + } + }, + "required": [ + "title", + "type", + "required", + "code" + ], + "additionalProperties": false } ] } @@ -876,7 +985,6 @@ "required": [ "title", "page_title", - "year", "delimiter", "teamNumber", "sections" diff --git a/src/assets/schema.json b/src/assets/schema.json index fa58f2281..84cc3413a 100644 --- a/src/assets/schema.json +++ b/src/assets/schema.json @@ -11,7 +11,7 @@ }, "year": { "type": "number", - "description": "The year this scouting config is relevant for." + "description": "The year this scouting config is relevant for. Defaults to the current year if not provided." }, "delimiter": { "type": "string", @@ -294,6 +294,45 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "title": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title" + }, + "description": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description" + }, + "type": { + "type": "string", + "const": "multi-counter" + }, + "required": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required" + }, + "code": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code" + }, + "disabled": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled" + }, + "formResetBehavior": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior" + }, + "defaultValue": { + "type": "number", + "default": 0, + "description": "The default value" + } + }, + "required": [ + "title", + "type", + "required", + "code" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -741,6 +780,10 @@ "timerDuration": { "type": "number", "description": "Expected duration in seconds (for UI reference, e.g., 15 for auto, 135 for teleop)" + }, + "autoStopSeconds": { + "type": "number", + "description": "Automatically stop the timer after this many seconds. Useful to prevent the timer from running past the match phase duration." } }, "required": [ @@ -786,7 +829,16 @@ "type": "number" }, "robotPosition": { - "type": "string" + "type": "string", + "enum": [ + "R1", + "R2", + "R3", + "B1", + "B2", + "B3" + ], + "description": "The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance." } }, "required": [ @@ -801,6 +853,10 @@ ], "default": null, "description": "The default team and robot position" + }, + "autoAssignFromFieldCode": { + "type": "string", + "description": "Optional code of another field to auto-assign" } }, "required": [ @@ -857,6 +913,59 @@ "code" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "title": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/title" + }, + "description": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/description" + }, + "type": { + "type": "string", + "const": "TBA-assigned-station" + }, + "required": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/required" + }, + "code": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/code" + }, + "disabled": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/disabled" + }, + "formResetBehavior": { + "$ref": "#/properties/sections/items/properties/fields/items/anyOf/0/properties/formResetBehavior" + }, + "defaultValue": { + "type": "string", + "default": "", + "description": "If assigning by driver station, the driver station to use for selecting team and robot position across matches" + }, + "choices": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "default": { + "R1": "Red 1", + "R2": "Red 2", + "R3": "Red 3", + "B1": "Blue 1", + "B2": "Blue 2", + "B3": "Blue 3" + } + } + }, + "required": [ + "title", + "type", + "required", + "code" + ], + "additionalProperties": false } ] } @@ -876,7 +985,6 @@ "required": [ "title", "page_title", - "year", "delimiter", "teamNumber", "sections" diff --git a/src/components/inputs/BaseInputProps.ts b/src/components/inputs/BaseInputProps.ts index f5a33d5fe..32f8dfa99 100644 --- a/src/components/inputs/BaseInputProps.ts +++ b/src/components/inputs/BaseInputProps.ts @@ -15,6 +15,7 @@ export const inputTypeSchema = z 'action-tracker', 'TBA-team-and-robot', 'TBA-match-number', + 'TBA-assigned-station', ]) .describe('The type of input'); @@ -150,16 +151,35 @@ export const actionTrackerInputSchema = inputBaseSchema.extend({ ), }); +export const robotPosition = z + .enum(['R1', 'R2', 'R3', 'B1', 'B2', 'B3']) + .describe( + 'The robot position in the alliance. R = Red alliance, B = Blue alliance, 1/2/3 = position on the alliance.', + ); + +export const robotPositionLabels = { + R1: 'Red 1', + R2: 'Red 2', + R3: 'Red 3', + B1: 'Blue 1', + B2: 'Blue 2', + B3: 'Blue 3', +}; + export const tbaTeamAndRobotInputSchema = inputBaseSchema.extend({ type: z.literal('TBA-team-and-robot'), defaultValue: z .object({ teamNumber: z.number(), - robotPosition: z.string(), + robotPosition, }) .nullable() .default(null) .describe('The default team and robot position'), + autoAssignFromFieldCode: z + .string() + .optional() + .describe('Optional code of another field to auto-assign'), }); export const tbaMatchNumberInputSchema = inputBaseSchema.extend({ @@ -169,6 +189,17 @@ export const tbaMatchNumberInputSchema = inputBaseSchema.extend({ defaultValue: z.number().default(0).describe('The default value'), }); +export const tbaAssignedStationInputSchema = selectInputSchema.extend({ + type: z.literal('TBA-assigned-station'), + choices: z.record(z.string()).default(robotPositionLabels), + defaultValue: z + .string() + .default('') + .describe( + 'If assigning by driver station, the driver station to use for selecting team and robot position across matches', + ), +}); + export const sectionSchema = z.object({ name: z.string(), fields: z.array( @@ -186,6 +217,7 @@ export const sectionSchema = z.object({ actionTrackerInputSchema, tbaTeamAndRobotInputSchema, tbaMatchNumberInputSchema, + tbaAssignedStationInputSchema, ]), ), }); diff --git a/src/components/inputs/ConfigurableInput.tsx b/src/components/inputs/ConfigurableInput.tsx index 9b4d4a07d..f5dad2656 100644 --- a/src/components/inputs/ConfigurableInput.tsx +++ b/src/components/inputs/ConfigurableInput.tsx @@ -46,5 +46,7 @@ export default function ConfigurableInput(props: ConfigurableInputProps) { return ; case 'TBA-match-number': return ; + case 'TBA-assigned-station': + return ; } } diff --git a/src/components/inputs/TBATeamAndRobotInput.tsx b/src/components/inputs/TBATeamAndRobotInput.tsx index 0d66643d2..85e2b8a73 100644 --- a/src/components/inputs/TBATeamAndRobotInput.tsx +++ b/src/components/inputs/TBATeamAndRobotInput.tsx @@ -35,17 +35,11 @@ export default function TBATeamAndRobotInput(props: ConfigurableInputProps) { // (e.g. Red 1, Red 2, Red 3, Blue 1, Blue 2, Blue 3) across matches. To save time // and reduce errors, we can automatically select the team and robot position based // on the selected match number and driver station. - // - // By temporary convention, we assume the field for driver station selection is - // called "driverStation" and contains values like "R1", "R2", "R3", "B1", "B2", - // "B3". If the driver station field is present and has a valid value, we will - // automatically select the corresponding team and robot position for the scout. - // - // This is an optional feature that can be used by teams who want it, and it will - // eventually graduate into explicit config. const driverStation = useQRScoutState(() => { - return getFieldValue("driverStation"); - }) + return data?.autoAssignFromFieldCode + ? getFieldValue(data.autoAssignFromFieldCode) + : undefined; + }); if (!data) { return
Invalid input
; @@ -108,15 +102,17 @@ export default function TBATeamAndRobotInput(props: ConfigurableInputProps) { // Automatically select team and robot based on selected driver station and match number useEffect(() => { if (driverStation !== '') { - const teamNumber = teamOptions.find((team) => team.robotPosition == driverStation)?.teamNumber; + const teamNumber = teamOptions.find( + team => team.robotPosition == driverStation, + )?.teamNumber; if (teamNumber !== undefined) { setValue({ teamNumber, robotPosition: driverStation, - }) + }); } } - }, [teamOptions, selectedMatchNumber, driverStation]) + }, [teamOptions, selectedMatchNumber, driverStation]); const resetState = useCallback( ({ force }: { force: boolean }) => {