Ground fake-fms output against the full real double-elim playoff + finals + alliance selection captured at MIMAS1 (via audience-display log sync): - Alliance pick clock now ticks over the wire as TimerChanged/AllianceSelectionTimer (pause-aware on real FMS), not just the break clocks - displays read the wire value. - Emit PLC_ALLIANCESELECTION_STATUS_Changed console-button pulses (StartButton before each pick, Break2/Break8 for the breaks), matching the captured StartButton->PickTimer ->StartButton-release ordering. - MatchStatusChanged now fires the Played state with FinalScoreRed/Blue at commit (previously only NotStarted + InProgress). - Finals results endpoint returns tiebreaker:null (capture: /GetMatchResultsDoubleElim FinalData returns literal null, NOT "None" - the playoff endpoint uses "Unknown"). - EnergizedThreshold 100 -> 240 (real 2026 REBUILT value from RedScoreChanged). - Emit LastCycleTimeCalculated (TimeSpan) once per posted match for FTA-Buddy cycle stats. Verified live: pick clock ticks 45/44/43, StartButton pulse brackets PickTimer, Played carries final scores, cycle time formats as a .NET TimeSpan. |
||
|---|---|---|
| packages | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| .mcp.json | ||
| .prettierrc | ||
| bun.lock | ||
| CLAUDE.md | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| NETWORKING.md | ||
| package.json | ||
| README.md | ||
| tsconfig.base.json | ||
Fake FMS
An FRC Field Management System emulator for testing FTA-Buddy and the audience-display
package without a real field. It speaks the same SignalR Core hubs + REST API the real FMS
exposes at 10.0.100.5, and gives you a React control panel to drive an event: create teams,
generate WPA keys, run matches with timers, bypass / e-stop / a-stop robots, edit game-specific
scores, and click a field-monitor grid to cycle each robot's connection status. Every change
fans out the correct SignalR events and updates the REST responses.
How it works
- Both consumers use the
@microsoft/signalrclient, i.e. the ASP.NET Core SignalR JSON protocol over WebSocket. The server hand-rolls that protocol (packages/server/src/signalr), so no .NET is involved. - A single in-memory
FmsStoreis the source of truth. Mutations emit typed domain events;fanout.tsmaps each to the right SignalR broadcast; the control UI gets a full-state push over a websocket. - Game-specific scoring is a swappable module (
packages/shared/src/games); the default targets the 2026 season.
Packages
| Package | What |
|---|---|
packages/shared |
Wire types (byte-compatible with both consumers), game modules |
packages/server |
The emulator: SignalR hubs + REST on :80, control API on :3010 |
packages/ui |
React + Vite control panel (served from :3010 in production) |
Develop
bun install
# run server (FMS :80 needs root locally; override the ports for dev):
FMS_PORT=18080 CONTROL_PORT=3010 bun run server
# in another shell, the UI with hot reload (proxies control API to :3010):
bun run ui # http://localhost:5173
# or run both together:
bun run dev
Smoke test
Connects with the real @microsoft/signalr client and drives a full match flow:
FMS_PORT=18080 CONTROL_PORT=3010 bun run server &
SMOKE_FMS_PORT=18080 SMOKE_CONTROL_PORT=3010 bun run smoke
Run it on your laptop (no field network)
If you just want a working FMS at 10.0.100.5 on your own machine to develop against (Linux or
macOS), you do not need the macvlan/field-network setup. You need Docker installed, then:
git clone <this repo> && cd fake-fms
./scripts/dev-up.sh # adds a 10.0.100.5 loopback alias (one sudo prompt), builds, starts
That's it. The emulator is now reachable from the same laptop at:
- FMS API + SignalR:
http://10.0.100.5 - Control console:
http://10.0.100.5:3010
Point FTA-Buddy / audience-display at 10.0.100.5 as usual and drive matches from the control
console. Stop it with ./scripts/dev-down.sh. No .env or TBA key is required (it seeds realistic
demo teams + schedule); set TBA_API_KEY in a gitignored .env to pull a real event instead.
How it works: docker-compose.dev.yml publishes the container's ports onto a 10.0.100.5 loopback
alias the script adds, so traffic to 10.0.100.5 stays on your machine. The alias is not persistent
across reboots, so re-run dev-up.sh after restarting.
Production deploy (the real field network)
For the home-server deploy where the container owns the real 10.0.100.5 on the field VLAN:
bun run build # builds the UI into packages/ui/dist
docker compose up -d --build # macvlan; see NETWORKING.md for the host VLAN + UniFi setup
- FMS API + SignalR:
http://10.0.100.5:80 - Control UI:
http://10.0.100.5:3010
Drive it from Claude (MCP)
The emulator ships an MCP server so an AI agent can run matches, score, cycle the field monitor, and read live state while you develop. Two ways to connect:
Networked (no local repo). The running container hosts the MCP server over Streamable HTTP at
/mcp on the control port, so a remote Claude Code just points at the URL:
claude mcp add --transport http fake-fms http://10.0.100.5:3010/mcp
That's all the laptop needs, no clone, no bun. (For a local laptop deploy via dev-up.sh, the URL
is the same http://10.0.100.5:3010/mcp.)
Local (stdio). A clone of the repo also exposes the server over stdio; the repo's .mcp.json
auto-registers it for Claude Code run inside the repo. Point it at any emulator with
FAKE_FMS_CONTROL_URL (default the deployed field box).
Ports / env
| Env | Default | Meaning |
|---|---|---|
FMS_PORT |
80 |
FMS REST + SignalR port |
CONTROL_PORT |
3010 |
Control API + UI + state ws + /mcp port |
GAME_ID |
rebuilt2026 |
Active game scoring module |
FAKE_FMS_CONTROL_URL |
(control URL) | MCP server: which emulator control API to drive |