Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c47757c
feat: ftc codesim
BrandonPacewic Jul 31, 2026
2e219a2
feat: unify codesim connection status indicator
BrandonPacewic Jul 31, 2026
bdc07d5
feat: hide wiring panel for ftc codesim
BrandonPacewic Jul 31, 2026
4abc646
feat: disable auto testing - reason not implemented
BrandonPacewic Jul 31, 2026
3dc5d2c
fix: only care if ftc brain is active
BrandonPacewic Jul 31, 2026
1ad7d74
fix: magic configured title in ftc sim
BrandonPacewic Jul 31, 2026
236f6f2
chore: merge branch `dev`
BrandonPacewic Jul 31, 2026
5ef0fec
docs: ftc codesim doc
BrandonPacewic Jul 31, 2026
9a42972
chore: update comments
BrandonPacewic Jul 31, 2026
55bc43e
fix: remove unused components
BrandonPacewic Jul 31, 2026
77d4d0d
feat: combine ws workers
BrandonPacewic Aug 4, 2026
5cb6d52
chore: remove obnoxious comments
BrandonPacewic Aug 4, 2026
681f334
chore: remove old code
BrandonPacewic Aug 4, 2026
06d8304
feat: move fission specific ftc code to java and mock ftc to frc 1:1 …
BrandonPacewic Aug 6, 2026
1ba7a24
chore: linting
BrandonPacewic Aug 6, 2026
25eae3b
feat: autonomous codesim
RoushilS Aug 6, 2026
c27aa07
chore: remove outdated codesim string
BrandonPacewic Aug 6, 2026
2cf1599
Align FTC codesim to FRC paths (#1482)
BrandonPacewic Aug 6, 2026
db822a5
chore: merge branch `dev`
BrandonPacewic Aug 6, 2026
320e563
feat: add driver station state
RoushilS Aug 6, 2026
c8fddc8
chore: update doc comments and java formatting
BrandonPacewic Aug 7, 2026
88ae164
chore: lint
BrandonPacewic Aug 7, 2026
010bf9c
feat: remove CAN and PWM device language from ftc codesim
BrandonPacewic Aug 7, 2026
f6e8edf
chore: remove dead port argument from ftc sim project
BrandonPacewic Aug 7, 2026
9bfeab3
feat: add runtime and finish behavior
RoushilS Aug 7, 2026
7c791d6
fix: mistakes
RoushilS Aug 7, 2026
1c043e4
Merge branch 'branp/295/ftc-codesim' into ftc-codesim-autonomous-support
RoushilS Aug 7, 2026
21ba384
feat: improve example
RoushilS Aug 11, 2026
9b862d9
fix: auto testing broken
RoushilS Aug 18, 2026
57c5695
chore: update gradle
RoushilS Aug 19, 2026
2aaab02
Merge branch 'dev' into branp/295/ftc-codesim
RoushilS Aug 19, 2026
10b2398
fix: code review
RoushilS Aug 19, 2026
1a20ff9
Ftc codesim autonomous support (#1491)
PepperLola Aug 19, 2026
1e3233f
refactor(codesim examples): make dozer auto drive nicer.
RoushilS Aug 20, 2026
dff52da
Merge branch 'dev' into branp/295/ftc-codesim
RoushilS Aug 20, 2026
0fee50a
chore: format
RoushilS Aug 20, 2026
7e9db08
fix: code review
RoushilS Aug 20, 2026
92fe030
chore: remove entire old comment
azaleacolburn Aug 21, 2026
9b1de06
Merge branch 'dev' into branp/295/ftc-codesim
azaleacolburn Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/CODE_SIMULATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,30 @@ You should see updates both in Fission and in the readouts in the code simulator
The robot code will control the movement of the robot in Fission, and you can expand each of the devices in the robot code simulator GUI to see if their fields are being updated by Fission.
For example, running the JavaAutoSample should cause the `ADXL362[4]` and `SYN AHRS[0]` devices to update with rotation and acceleration data from Fission
(note that the names may differ if the code changes).

## FTC Code Simulation

To run:

```
cd simulation/SyntheSimFTC
./gradlew run --args="--src <path-to-teamcode-src> [--opmode <name>] [--port <port>]"
```

`--src` points at a plain directory of `.java` files, rooted so package folders (`org/firstinspires/ftc/teamcode/...`) hang off it. `--port` defaults to `3301`. Make sure to start the simulation before starting the dev server.

`--opmode` picks which discovered OpMode to run, matching a class name (`ExampleDozerArcadeDrive`), or the annotation's display name (`"Dozer Arcade Drive"`).

The runner logs every OpMode it found and which one it selected, e.g.:

```
[OpModeRunner] Discovered [TeleOp] Dozer Arcade Drive (org.firstinspires.ftc.teamcode.examples.ExampleDozerArcadeDrive)
[OpModeRunner] Running [TeleOp] Dozer Arcade Drive (org.firstinspires.ftc.teamcode.examples.ExampleDozerArcadeDrive)
```

Running the Dozer sample at `simulation/samples/FTCDozerArcadeDriveSample`:

```
cd simulation/SyntheSimFTC
./gradlew run --args="--src ../samples/FTCDozerArcadeDriveSample --opmode ExampleDozerArcadeDrive"
```
2 changes: 1 addition & 1 deletion fission/src/mirabuf/MirabufSceneObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
this.nameOverride ??
(this._brain?.isSynthesis()
? this._brain.inputSchemeName
: this._brain?.isWPILib()
: this._brain?.isWPILib() || this._brain?.isFTC()
? "Magic"
: "Not Configured!")
if (World.multiplayerSystem != null) {
Expand Down
5 changes: 4 additions & 1 deletion fission/src/systems/simulation/Brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type Mechanism from "../physics/Mechanism"
import type SynthesisBrain from "@/systems/simulation/synthesis_brain/SynthesisBrain.ts"
import type WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain.ts"

export type BrainType = "synthesis" | "wpilib" | "unknown"
export type BrainType = "synthesis" | "wpilib" | "ftc" | "unknown"

abstract class Brain {
protected _mechanism: Mechanism
Expand All @@ -24,6 +24,9 @@ abstract class Brain {
public isWPILib(): this is WPILibBrain {
return this.brainType == "wpilib"
}
public isFTC(): this is WPILibBrain {
return this.brainType == "ftc"
}
}

export default Brain
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Mutex } from "async-mutex"

// Generic WebSocket worker shared by WPILib and FTC codesim connections.

let socket: WebSocket | undefined = undefined

const connectMutex = new Mutex()

let intervalHandle: NodeJS.Timeout | undefined = undefined
let reconnect = false
let connectionUrl: string | undefined = undefined
const RECONNECT_INTERVAL = 1000

function socketOpen(): boolean {
Expand All @@ -16,13 +19,14 @@ function socketConnecting(): boolean {
return (socket && socket.readyState == WebSocket.CONNECTING) ?? false
}

async function tryConnect(port?: number): Promise<void> {
async function tryConnect(): Promise<void> {
await connectMutex.runExclusive(() => {
if (!connectionUrl) return
if ((socket?.readyState ?? WebSocket.CLOSED) == WebSocket.OPEN) {
return
}

socket = new WebSocket(`ws://localhost:${port ?? 3300}/wpilibws`)
socket = new WebSocket(connectionUrl)

socket.addEventListener("open", () => {
self.postMessage({ status: "open" })
Expand All @@ -47,15 +51,14 @@ async function tryDisconnect(): Promise<void> {
})
}

// Posts incoming messages
function onMessage(event: MessageEvent) {
self.postMessage(event.data)
}

// Sends outgoing messages
self.addEventListener("message", e => {
switch (e.data.command) {
case "enable": {
connectionUrl = e.data.url ?? connectionUrl
reconnect = e.data.reconnect ?? false
const intervalFunc = () => {
if (intervalHandle != undefined && !socketOpen() && !socketConnecting()) {
Expand Down
9 changes: 6 additions & 3 deletions fission/src/systems/simulation/wpilib_brain/WPILibBrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,17 @@ class WPILibBrain extends Brain {
return this._assembly.assemblyId
}

private _brainType: "wpilib" | "ftc"

public override get brainType() {
return "wpilib" as const
return this._brainType
}

constructor(assembly: MirabufSceneObject) {
constructor(assembly: MirabufSceneObject, brainType: "wpilib" | "ftc" = "wpilib") {
super(assembly.mechanism)

this._assembly = assembly
this._brainType = brainType

this._simLayer = World.simulationSystem.getSimulationLayer(this._mechanism)!

Expand All @@ -107,7 +110,7 @@ class WPILibBrain extends Brain {
this.loadSimConfig()

World.sceneRenderer.mirabufSceneObjects.getRobots().forEach(v => {
if (v.brain?.isWPILib()) {
if (v.brain?.isWPILib() || v.brain?.isFTC()) {
v.brain = new SynthesisBrain(v)
}
})
Expand Down
35 changes: 2 additions & 33 deletions fission/src/systems/simulation/wpilib_brain/WPILibState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
import type WPILibBrain from "./WPILibBrain"
import { type SimMap, worker } from "./WPILibTypes"
import { FTC_WS_URL, type SimMap, WPILIB_WS_URL, worker } from "./WPILibTypes"

export const simMaps = new Map<string, SimMap>()

Expand All @@ -14,6 +14,7 @@ export function setSimBrain(brain: WPILibBrain | undefined) {
if (simBrain)
worker.getValue().postMessage({
command: "enable",
url: simBrain.brainType === "ftc" ? FTC_WS_URL : WPILIB_WS_URL,
reconnect: PreferencesSystem.getUserPreference("SimAutoReconnect"),
})
}
Expand All @@ -40,35 +41,3 @@ export function setConnected(connected: boolean) {
export function getIsConnected() {
return isConnected
}

/*
export const supplierTypeMap: { [k in SimType]: NoraTypes | undefined } = {
[SimType.PWM]: NoraTypes.NUMBER,
[SimType.SIM_DEVICE]: undefined,
[SimType.CAN_MOTOR]: NoraTypes.NUMBER,
[SimType.SOLENOID]: NoraTypes.NUMBER,
[SimType.CAN_ENCODER]: undefined,
[SimType.GYRO]: undefined,
[SimType.ACCELEROMETER]: undefined,
[SimType.DIO]: NoraTypes.NUMBER, // ?
[SimType.AI]: undefined,
[SimType.AO]: NoraTypes.NUMBER,
[SimType.DRIVERS_STATION]: undefined,
[SimType.CAMERA]: undefined,
}

export const receiverTypeMap: { [k in SimType]: NoraTypes | undefined } = {
[SimType.PWM]: undefined,
[SimType.SIM_DEVICE]: undefined,
[SimType.CAN_MOTOR]: undefined,
[SimType.SOLENOID]: undefined,
[SimType.CAN_ENCODER]: NoraTypes.NUMBER2,
[SimType.GYRO]: NoraTypes.GYRO,
[SimType.ACCELEROMETER]: NoraTypes.ACCEL,
[SimType.DIO]: NoraTypes.NUMBER, // ?
[SimType.AI]: NoraTypes.NUMBER,
[SimType.AO]: undefined,
[SimType.DRIVERS_STATION]: undefined,
[SimType.CAMERA]: undefined,
}
*/
8 changes: 6 additions & 2 deletions fission/src/systems/simulation/wpilib_brain/WPILibTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Lazy from "@/util/Lazy.ts"
import WPILibWSWorker from "./WPILibWSWorker?worker"
import WSWorker from "../shared/WSWorker?worker"

export const WPILIB_WS_URL = "ws://localhost:3300/wpilibws"
export const FTC_WS_URL = "ws://localhost:3301/ftcsimws"

export type DeviceName = string
export type DeviceData = Map<string, number | boolean | string>
Expand All @@ -18,6 +21,7 @@ export enum SimType {
AI = "AI",
AO = "AO",
DRIVERS_STATION = "DriverStation",
GAMEPAD = "Gamepad",
CAMERA = "Camera",
}

Expand Down Expand Up @@ -63,4 +67,4 @@ export const CAMERA_HEIGHT = "<height"
export const CAMERA_FPS = "<fps"
export const CAMERA_CONNECTED = "<connected"

export const worker: Lazy<Worker> = new Lazy<Worker>(() => new WPILibWSWorker())
export const worker: Lazy<Worker> = new Lazy<Worker>(() => new WSWorker())
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export default class SimDriverStation {
const enabled = mode != RobotSimMode.DISABLED
const autonomous = mode == RobotSimMode.AUTO
SimGeneric.set<boolean>(SimType.DRIVERS_STATION, "", ">ds", true)
SimGeneric.set<boolean>(SimType.DRIVERS_STATION, "", ">enabled", enabled)
SimGeneric.set<boolean>(SimType.DRIVERS_STATION, "", ">autonomous", autonomous)
SimGeneric.set<boolean>(SimType.DRIVERS_STATION, "", ">enabled", enabled)
}

public static setStation(station: AllianceStation) {
Expand Down
83 changes: 83 additions & 0 deletions fission/src/systems/simulation/wpilib_brain/sim/SimGamepad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import InputSystem from "@/systems/input/InputSystem"
import type { KeyCode } from "@/systems/input/KeyboardTypes"
import { SimInput } from "../SimInput"
import { SimType, worker } from "../WPILibTypes"

const GAMEPAD_AXIS = { LEFT_X: 0, LEFT_Y: 1, RIGHT_X: 2, RIGHT_Y: 3 }
const GAMEPAD_BUTTON = {
A: 0,
B: 1,
X: 2,
Y: 3,
LEFT_BUMPER: 4,
RIGHT_BUMPER: 5,
LEFT_TRIGGER: 6,
RIGHT_TRIGGER: 7,
BACK: 8,
START: 9,
DPAD_UP: 12,
DPAD_DOWN: 13,
DPAD_LEFT: 14,
DPAD_RIGHT: 15,
}

export class SimGamepadInput extends SimInput {
public update(_deltaT: number) {
const data = InputSystem.gamepad ? this.readPhysicalGamepad() : this.readKeyboardGamepad()

worker.getValue().postMessage({
command: "update",
data: { type: SimType.GAMEPAD, device: this._device, data },
})
}

private readPhysicalGamepad() {
const gamepad = InputSystem.gamepad!
return {
left_stick_x: InputSystem.getGamepadAxis(GAMEPAD_AXIS.LEFT_X),
left_stick_y: InputSystem.getGamepadAxis(GAMEPAD_AXIS.LEFT_Y),
right_stick_x: InputSystem.getGamepadAxis(GAMEPAD_AXIS.RIGHT_X),
right_stick_y: InputSystem.getGamepadAxis(GAMEPAD_AXIS.RIGHT_Y),
left_trigger: gamepad.buttons[GAMEPAD_BUTTON.LEFT_TRIGGER]?.value ?? 0,
right_trigger: gamepad.buttons[GAMEPAD_BUTTON.RIGHT_TRIGGER]?.value ?? 0,
a: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.A),
b: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.B),
x: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.X),
y: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.Y),
left_bumper: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.LEFT_BUMPER),
right_bumper: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.RIGHT_BUMPER),
dpad_up: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.DPAD_UP),
dpad_down: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.DPAD_DOWN),
dpad_left: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.DPAD_LEFT),
dpad_right: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.DPAD_RIGHT),
start: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.START),
back: InputSystem.isGamepadButtonPressed(GAMEPAD_BUTTON.BACK),
}
}

private readKeyboardGamepad() {
const axis = (positiveKey: KeyCode, negativeKey: KeyCode) =>
(InputSystem.isKeyPressed(positiveKey) ? 1 : 0) - (InputSystem.isKeyPressed(negativeKey) ? 1 : 0)

return {
left_stick_x: 0,
left_stick_y: axis("KeyS", "KeyW"),
right_stick_x: axis("KeyD", "KeyA"),
right_stick_y: 0,
left_trigger: 0,
right_trigger: 0,
a: false,
b: false,
x: false,
y: false,
left_bumper: false,
right_bumper: false,
dpad_up: false,
dpad_down: false,
dpad_left: false,
dpad_right: false,
start: false,
back: false,
}
}
}
4 changes: 2 additions & 2 deletions fission/src/ui/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { TopBarFitProvider } from "@/ui/components/topbar/TopBarFitProvider"
import { TopBarIcon } from "@/ui/components/topbar/TopBarIcons"
import { useAssemblySelection } from "@/ui/components/topbar/UseConfigureAssembly"
import UserIcon from "@/ui/components/UserIcon"
import { hasSimBrain } from "@/systems/simulation/wpilib_brain/WPILibState"
import { getIsConnected, hasSimBrain } from "@/systems/simulation/wpilib_brain/WPILibState"
import { useTourAnchor } from "@/ui/tour/TourProviderHelpers"

const TUTORIALS_URL = "https://synthesis.autodesk.com/tutorials"
Expand Down Expand Up @@ -183,7 +183,7 @@ const TopBar: React.FC = () => {

{hasSimBrain() && (
<>
<CodeConnectionIndicator />
<CodeConnectionIndicator label="Code connection" getIsConnected={getIsConnected} />
<Box sx={TOP_BAR_DIVIDER_SX} />
</>
)}
Expand Down
14 changes: 9 additions & 5 deletions fission/src/ui/components/topbar/CodeConnectionIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Box, Tooltip } from "@mui/material"
import type React from "react"
import { useEffect, useState } from "react"
import { getIsConnected } from "@/systems/simulation/wpilib_brain/WPILibState"
import { SynthesisIcons } from "@/ui/components/StyledComponents"
import { TOP_BAR_GLYPH_SX } from "@/ui/components/topbar/TopBarConfig"

/** small status glyph on the codesim menu reflecting wpilib code connection */
const CodeConnectionIndicator: React.FC = () => {
interface CodeConnectionIndicatorProps {
label: string
getIsConnected: () => boolean
}

/** small status glyph on the codesim menu reflecting code connection */
const CodeConnectionIndicator: React.FC<CodeConnectionIndicatorProps> = ({ label, getIsConnected }) => {
const [connected, setConnected] = useState<boolean>(false)

useEffect(() => {
const handle = setInterval(() => setConnected(getIsConnected()), 500)
return () => clearInterval(handle)
}, [])
}, [getIsConnected])

const tooltip = connected ? "Code connection: connected" : "Code connection: not connected"
const tooltip = connected ? `${label}: connected` : `${label}: not connected`

return (
// not a TopBarIcon since it is a stateful component
Expand Down
11 changes: 6 additions & 5 deletions fission/src/ui/components/topbar/CodesimControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,22 @@ type CodesimButton = {
label: string
mode: ConfigMode
icon: React.ReactNode
requiresWpilibBrain?: boolean
requiresCodesimBrain?: boolean
}

const CODESIM_BUTTONS: CodesimButton[] = [
{ label: "Brain", mode: ConfigMode.BRAIN, icon: <SynthesisIcons.BRAIN /> },
{ label: "Simulation", mode: ConfigMode.SIM, icon: <SynthesisIcons.MICROCHIP />, requiresWpilibBrain: true },
{ label: "Simulation", mode: ConfigMode.SIM, icon: <SynthesisIcons.MICROCHIP />, requiresCodesimBrain: true },
]

const CodesimControls: React.FC<{ selectedAssembly?: MirabufSceneObject }> = ({ selectedAssembly }) => {
const { isField, isWpilibBrain, openConfig, disabledMessage } = useConfigureAssembly(selectedAssembly)
const { isField, isCodesimBrain, openConfig, disabledMessage } = useConfigureAssembly(selectedAssembly)

// codesim is robot only
const disabledTooltip = ({ requiresWpilibBrain }: CodesimButton) => {
const disabledTooltip = ({ requiresCodesimBrain }: CodesimButton) => {
if (!selectedAssembly) return "Spawn an assembly first"
if (isField) return "Select a robot to configure"
if (requiresWpilibBrain && !isWpilibBrain) return "Set this robot's brain to WPILib first"
if (requiresCodesimBrain && !isCodesimBrain) return "Set this robot's brain to WPILib or FTC first"
return undefined
}

Expand Down
Loading
Loading