Skip to content

Field-test readiness: real-robot example, field check, calibration, dashboard for the field - #148

Open
isaac0804 wants to merge 27 commits into
mainfrom
docs-roadmap-refresh
Open

isaac0804 wants to merge 27 commits into
mainfrom
docs-roadmap-refresh

Conversation

@isaac0804

Copy link
Copy Markdown
Contributor

Everything for the first real-robot field test (2026-10-22): an attacker-vs-defender example, field checks with auto-HALT, field-test recording and calibration, and a dashboard that works from a phone beside the field. Also a refreshed roadmap and a few engine and tactic fixes found along the way.

Field test

  • examples/real_attacker_defender.py: one attacker against one defender, on real robots, grSim or rsim. Per-robot hardware switches (no kicker, no dribbler) and a RULES switch. Its docstring is the test-day checklist:
    • calibrate each robot twice
    • hand rolls for floor friction
    • 5+ kicks per robot
    • occlusion clips
  • Field check (run/field_check.py): on camera runs it checks each robot is seen, inside the area in play and has a live radio link, plus the ball, the camera and the geometry. On real robots any failure forces HALT, judged before the referee steps so no tick of motion gets through.
  • Robot profiles: without a kicker, the robot pushes the ball; without a dribbler, it carries it gently.
  • Recording and measuring: field tests are recorded as SSL game logs (replay/game_log_writer.py). analysis/robot_calibration.py measures robots from them, and physics_ranges measures floor friction.
  • Examples run as documented (pixi run python examples/<name>.py). They used to fail at their first import, because utama_core is not installed.

Ball filter

  • An unseen ball now rolls to a stop at the measured deceleration (BALL_ROLL_DECEL, 0.29 m/s²) and stays inside vision's area. It used to keep its last velocity forever: the rsim "ball escape", hundreds of metres away after a long match.
  • With smoothing off, a second loss starts from where the ball was last seen.

Dashboard

  • Match timeline in the Replay and Live tabs: referee band, each robot's tactic, possession, distance to the ball, events, shots.
  • Play list in Replay: one row per situation, with its third, length, shots and how it ended. It replaces the raw event and intention logs, which stay available, folded.
  • Speed: replays are sent as columns of whole numbers. A 13-minute match went from 65 MB to 21 MB (4.3 MB gzipped) and its build time from 4.2 s to 1.5 s. The Live page draws once per screen frame and refreshes the side panel at 4 Hz.
  • Ball placement: right-click, or long-press including on iPhone, opens a menu to place the ball for either team. Targets outside the field, or outside the camera's area in play, are refused.
  • Phone layout: panels stack, and Halt floats in the bottom-right corner.
  • Safety fixes:
    • A client that stops reading (a locked phone, a sleeping laptop) is dropped and disconnected; it used to block the control loop, radio and HALT included.
    • The auto-HALT banner clears once play resumes.
    • The command and banner are drawn first on every frame.
    • Refused or failed commands are shown under the buttons.
    • "No update for N s" appears when the stream stalls.
  • Smaller fixes: robot headings point the right way in mirrored views; the last Play-list row has the right length.

Engine and tactics

  • A pinned goalkeeper that is not on the field is skipped, with no goalkeeper row in debug_status.
  • The shot aim in LeadAndSupport and GiveAndGo now has hysteresis.

Testing

  • pixi run lint: clean.
  • pixi run test: 12947 passed, 83 xfailed.
  • Every fix has a regression test that fails before it and passes after.
  • The dashboard was checked in headless Chrome at laptop and phone widths.
  • Not yet checked on real hardware: placement with real robots, the phone over WiFi, the iPhone long-press.

isaac0804 and others added 27 commits October 21, 2026 12:05
…ints at

The roadmap still described the pre-evaluate.py evaluation plan, kept a
mirror_swap xfail that no longer exists, and listed several items that had
already shipped. Rewritten along its own convention (a resolved item becomes a
one-line pointer under "Done"), with the remaining items numbered in sequence:

- the strategy-evaluation items -> Done: evaluate.py and pool_v1 replaced the
  situation bench and ladder.py. The two genuinely open parts moved instead of
  disappearing: the CI stall gate into the stalls entry, the tactic contracts
  into the ball-holding entry.
- directional tube and Replay overlays -> Done; fpp mirror_swap -> Done: the
  0.52 m was the defense-area keep-out clamp, not a local planner minimum, and
  the xfail went in 5f97199.
- stalls -> the 2026-10-17 full round-robin figures; sim realism -> Phases 0-2
  built, with what remains named.
- bang-bang, trajsample, testing gaps and convergence revised for what has
  landed since; the unrecorded Sumatra findings (#4-7) and the shared-Sticky
  item removed (Sticky exists and is used).

Also fixes the stale notes that pointed into the roadmap: obstacles.py's class
docstring still described a growing circle, FORK_NOTES referenced two deleted
roadmap sections, sim_realism's status line read as if nothing had been built,
and testing_gaps #6 now records the one sim case for
ball_placement_interference.
Corrections to the previous commit, checked against the code:
- stalls: 1731e90 fixed only the kick-off that waited the 15 s cap; the
  three COMMITTED_FROZEN stalls of the 2026-10-17 round-robin are not yet
  diagnosed (docs/strategies.md says the same).
- sim realism: P1's value model is fitted (passes 1-3) but not yet a better
  ranker than goals; only P2 is not started.
- Shared hysteresis helper: Sticky's users are PassAndShoot, ClearBall and
  PressAndContain; pickers.friendly_closer_to_ball keeps its own hysteresis.
- grsim entry: drop the note that FORK_NOTES points at a missing section
  (the previous commit fixed it); "like 10c" names its entry; pool_v1 holds
  the v3 ranges, v3_seed2 is the round-robin's seeded run of them.

The roadmap now asks to cite entries by title, since open items renumber.
Every comment, docstring and doc that cited a roadmap number used numbers
long gone ("roadmap item 15/16", "item 5", "item 1"); each now names its
entry. fastpathplanner_corner_test's docstring said seeds were pinned as
stuck; the list has been empty since #147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…field

A team with no robot of goalkeeper_id (default 0) crashed on its first tick
with KeyError in the goalkeeper tactic. Real tests often run without robot 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
Against a centred keeper the two corners are near-equal, and the aim flipped
between them every tick; a lone carrier turned back and forth ~11 s before
shooting. Uses find_best_shot's switch margin, as _pass_and_score does.
Changes strategy code: cached matches rerun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
Field, camera half, vision/firmware IDs, IR trust and tactics in one config
block; operator-only referee driven from the dashboard; starts in HALT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
A team without the goalkeeper's robot showed a phantom goalkeeper (e.g. ID 15)
in the dashboard's tactics list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
FieldCheck judges raw vision, the geometry packet and radio feedback: each
driven robot seen, under its ID, inside field_bounds; camera sending; field
size; radio still answering; unconfigured robots in play. The Live tab shows
the rows, dims everything outside field_bounds and draws the field as
ssl-vision sees it. On real robots any failing row forces HALT. The dashboard
opens on Live when a match is attached; Space only halts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…t a dribbler

RobotProfile(kicker, dribbler, ir) per robot, passed to StrategyRunner as
{yellow,blue}_robot_profiles and applied in every mode, so a test with a
broken robot can be rehearsed in a simulator. ProfiledRobotController wraps
the team's robot controller: a kick becomes a 0.3 s forward push at 1 m/s
(a stop command ends it); a dribble request without a dribbler never backs
up and is scaled to 0.5 m/s and 2 rad/s together. A robot without a ball
sensor is left out of the trusted-IR set. A profile for a robot that is not
driven, or ball sensors given both ways, stops the run at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
… Live page

The attacker/defender example takes one TestRobot per robot (vision ID,
firmware ID, RobotProfile) and RULES = "off" | "on" ("on": the human
referee profile, nothing auto-advances). The Live page lists only failing
and warning field-check rows, folds the passing ones into an "N ok" line,
hides the restart buttons when no rule sets up a restart, and drops the
robot list the field check already covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…ts from them

StrategyRunner(game_log_path=...) records a camera run in the league's
game-log format: every vision packet as received, the referee's state,
the radio's bytes both ways and a run-info record (ID mappings,
profiles), all on this machine's clock. tools/convert_ssl_log.py reads
it unchanged. The attacker/defender example records every camera run to
replays/field/, and --calibrate plays fixed moves (ScriptedMovesTactic:
rest, steps at 0.3/0.6/1 m/s, sideways, turns; a pause restarts the
interrupted move).

tools/robot_calibration.py measures each robot from such a log: loop
delay, speed gain and acceleration per axis (the sim's move model fitted
to the camera's positions), vision noise at rest, missed detections and
kick speed. On a synthetic robot it recovers delay within 10 ms,
acceleration within 15 % and gain within 1 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…ler replay payload

timeline.js draws tools/match_timeline.py's swimlanes in the browser:
the referee command band, one lane per friendly robot coloured by its
tactic with a strip where it had the ball, each robot's distance to the
ball, and the event markers. In the Replay tab it follows the cursor and
seeks on click or drag (30 s / 60 s / 3 min / whole match); its tactic
spans match match_timeline.tactic_changes exactly on a tournament match.
In the Live tab each streamed state extends the lanes, stamped with the
frame's game time (now sent with the state), keeping the last 5 minutes.

A 13-minute replay's frames were 108 MB of JSON (full 17-digit floats)
against a 16 MB .npz. Positions now go to the millimetre, velocities to
the centimetre, and responses over 64 KB are gzipped: 7.4 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…ks, shots

The Replay side panel's raw event and intention logs were hard to read. A Play list replaces
them: one row per situation (a restart going live or a side winning the ball, from
goal_ledger), with its third, length, shots and how it ended (the stoppage's reason, or who
won the ball and how). Click a row to seek; the raw intention log stays, folded.

The timeline gains a possession band, tactic switch ticks, shots and hover tooltips. The slow
analysis (ball losses, shots) is kept beside the replay in <stem>.timeline.json, and npz
frames are built straight from the columns, so a replay serves in ~3 s instead of ~16 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
Unseen, the ball filter carried the ball on at its last velocity, and the velocity refiner kept
that velocity alive from the guessed positions: a ball hidden by a robot drifted away, and one
that left vision's area flew off for good (an rsim attacker/defender run had it 600 m out after
15 minutes). Now it rolls on from where it was lost, slowing at BALL_ROLL_DECEL (0.29 m/s^2,
official Div B logs) until it stops, and never leaves the area vision accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…riction, kicks, occlusions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
utama_core is not installed in the pixi environment, so every example failed at its first
import when run by path, as their docstrings say to (the field-test example included). Each
now puts the repo root on sys.path first, as the scripts in tools/ do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
… Live page

- Right-click (long-press on a phone) on the Live field: ball placement to that spot by Yellow
  or Blue, as the old referee GUI's God Mode had. In a simulator the ball is moved there.
- Below 760 px wide the panels stack, the page scrolls and buttons are bigger.
- The field shows the 0.3 m run-off; a ball beyond the view is drawn hollow at its edge.
- States arrive at 60 Hz: the field and timeline draw once per screen frame, the side panel's
  text 4 times a second (it was rebuilt on every state, so a click could miss).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
One JSON object per tick, every key repeated, made a match 65 MB (7.4 MB gzipped) and took 4 s
to build. Columns of mm, cm/s and mrad with `seen` masks are 21 MB (4.3 MB gzipped) and build
in 1.5 s; the page rebuilds the same per-tick frames in about 0.2 s, so nothing else changes.
Pickled replays keep the old shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…trol loop

`notify` writes each state to every stream client on the control loop (`CustomReferee.step`),
on blocking sockets with no timeout. A client that stopped reading (a phone locked with the
page open, a laptop asleep, WiFi gone) filled its buffers and blocked the loop for good: no
radio commands, no HALT, no auto-HALT. Stream sockets are now non-blocking: once a client's
buffers are full its write fails at once, it is dropped and its browser reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
The field check ran after the referee stepped, so the tick that forced HALT still carried the
command it replaced: pressing Force Start (or a placement) while a check failed sent one tick
of motion. Vision is now taken and judged before the referee steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…s drawn first

The runner kept the last auto-HALT's reason for good, so the red banner stayed up with the
robots playing, and the Live page, which skipped its 4 Hz panel throttle while a reason was
set, rebuilt the side panel on every state from then on. The reason now lasts as long as the
HALT it forced. The command and the banner are drawn first on every frame, before the field
and timeline, so neither waits on the throttle or fails with a drawing error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
The end was read from `frames`, which a columnar replay leaves empty since its ticks are sent
as columns, so the last row ended at 0 s and read "-29 s" with no shots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
With our goal on the right in a view that mirrors x (rsim, replays), the heading dash was drawn
from the raw orientation, so a robot facing +x pointed the wrong way. It now goes through the
same field-to-screen transform as positions. The test runs field_canvas.js in node with a 2D
context that records lines, skipped where node is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…onnects

d9e6f2e dropped a client that stopped reading from the list but left its connection open:
its keep-alives kept it alive, so a phone woken up got keep-alives and no states, its page
frozen with the dot green, and a failed write could leave half a frame in the stream. A
client whose write fails is now shut down and its browser reconnects (after 0.5 s: the stream
sets `retry`). States and keep-alives, written from two threads, share a lock per client so
neither cuts into the other's frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…a in play

Any point on the Live page was accepted, the run-off and the half the camera does not see
included, so the placing robots could drive out of vision. The command handler now refuses a
target outside the field lines or, with a camera, outside the field check's area in play: a
400 with the reason, and nothing reaches the referee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…lt pinned on a phone

- A command the server refused (a placement outside the area in play) or never got shows
  under the buttons for 6 s; it was only logged to the browser console, so a Halt that did
  not go through looked the same as one that did.
- No state for half a second turns the dot grey and shows "no update for N s" beside the
  command: the page was showing the past with the dot green.
- On a phone, Halt floats in the bottom-right corner, in reach wherever the page is scrolled;
  it sat below the field, the timeline and the field check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
iPhone Safari fires no contextmenu on a long press, so the menu never opened there. A touch
held still for 0.6 s opens it too; a click within 0.4 s of opening (the lifted finger) no
longer closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
…e ball was last seen

Smoothing off and imputation on, the ball filter runs only while the ball is unseen, so it
never heard the ball had been seen again: a second loss carried on the first one's roll from
where that one started. A seen ball now resets the filter when it is not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJGHdb8Dht67hn4Lohndf5
@isaac0804 isaac0804 added the release:minor Minor changes to main label Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Minor changes to main

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant