Turn FRC robot telemetry into Rerun recordings and live streams.
moneyshift has four surfaces that all speak Rerun 0.41.0:
| Surface | What it does |
|---|---|
rerun-importer-moneyshift |
Rerun external importer: *.wpilog in, RRD out on stdout |
rerun-importer-moneyshift --nt |
Live bridge: NetworkTables topics in, Rerun stream out |
moneyshift/ |
Java SDK that publishes Rerun data to NetworkTables from robot code |
web/ |
Static browser importer (the wasm build of the same *.wpilog importer) |
Build and put the binary on PATH (the Rerun viewer discovers executables named
rerun-importer-*):
cargo build --release --bin rerun-importer-moneyshift
install -m 755 target/release/rerun-importer-moneyshift ~/.local/bin/Then open a log in the viewer as usual - rerun match.wpilog - and the importer handles it. The
contract is Rerun's external-importer protocol: RRD on stdout, logs on stderr, exit code 66
when the file is not a WPI log, 2 for bad arguments. --static, --entity-path-prefix,
--time_sequence and --time_duration_nanos (and the legacy --sequence/--time aliases) are
honored.
A log that was cut off while being written, or has a scrambled stretch, imports up to the damage and continues at the next intact record, with a warning naming the bytes that had to be dropped. A struct type that never publishes a schema is dropped with a single warning: Rerun has no way to render a struct whose layout was never published.
You can also import without a viewer:
rerun-importer-moneyshift match.wpilog > match.rrd
rerun-importer-moneyshift match.wpilog --entity-path-prefix robot --static > scene.rrdThe live bridge subscribes to every topic on the team's NetworkTables server and maps:
/Rerun/...topics published by the JavaNtRecorderthrough Rerun's own type reflection;- everything else through the same WPILib-struct heuristics the WPILog importer uses, so plain robot code, AdvantageKit and dashboard topics are graphed without extra work.
Robot code can also publish one /Moneyshift/layout string (see
FirstMetadata). It declares which topics
carry each robot's pose, swerve module states and planned paths, and what the field and cameras
look like; the bridge then adds the carpet, field lines, tags, robot models, swerve
vectors, camera frustums, paths and a 2D top-down mirror under first/ and first2d/ - no
/Rerun topics required on the robot.
A camera can also name the MJPEG URL PhotonVision serves for it
(http://<coprocessor>:1181/stream.mjpg). The bridge opens that multipart/x-mixed-replace stream
itself over a plain socket and logs one EncodedImage per JPEG frame on
first/robots/<team>/cameras/<name>/image, so everything the camera sends is recorded and there is
nothing extra to install. Only plain http:// is supported: a URL that needs TLS, redirects or a
proxy is reported and retried rather than followed.
A camera's 2D view has to be rooted at that image entity: Rerun only shows a pinhole's 2D content
inside that content's own 2D space, and a view rooted at the field reports that its target frame
has no pinhole root. The bridge sends a viewer layout with a 2D view per camera, whether or not
robot code declared a layout of its own, so a live session always opens with a pane that works.
The same frames also land on the pinhole plane in the 3D view, image_plane_distance in front of
the lens. That plane is only a couple of meters across, so on a field-wide view the picture is a
small patch next to the robot rather than a full-size pane; zoom in on the robot to read it.
Robot code decides what the Rerun window opens with. FirstMetadata takes an optional blueprint
section: container(Container.GRID | TABS | HORIZONTAL | VERTICAL) and any number of
view(kind, name, origin, entities…) calls, where kind is a ViewKind of THREE_D, TWO_D or
DATAFRAME.
A dataframe view lists exactly the entity paths you name, so tabulating an opmode string is a
decision on the robot, not a guess in the bridge; with none named it shows the recording's
timeline alone.
Declare nothing and the bridge sends its default: a 3D view of the field, a 2D view of it, a
dataframe of the timeline, and — when the declaration has cameras — a grid of their 2D views.
Declared views replace that default, but the cameras keep a 2D view either way. The camera panes
follow the order you declared them in, so a layout never shuffles between runs; when two robots
mount a camera with the same name, the title carries the team (camera front (6391)).
The bridge also turns the viewer's automatic views off, because its data spans several independent
trees (first/, first2d/ and every NetworkTables path). Rerun's automatic layout answers that
with a view rooted at /, which sweeps in the cameras and reports that a pinhole cannot be drawn
in it. Topics no view names are still recorded and browsable in the entity tree; name the ones you
want in a view to get a pane for them.
cargo run --example first_field -- <model.glb> <out.rrd> [clip.h264] renders a field with two
swerve robots, camera video and a planned path for checking the overlays without a robot. Without
a clip it renders a synthetic one with ffmpeg, and skips video when ffmpeg is missing.
examples/wpilib-2027 is a runnable WPILib 2027 robot with
AdvantageKit, PhotonVision and a simulated swerve drive that publishes the layout, so
--nt --save records a real robot end to end.
rerun-importer-moneyshift --nt --team 254 --spawn
rerun-importer-moneyshift --nt --nt-addr 10.0.0.2 --save /tmp/live.rrd
rerun-importer-moneyshift --nt --robot-time-topic /FMSInfo/MatchTime --connect rerun+http://127.0.0.1:9876/proxySinks are mutually exclusive: --spawn (default, needs rerun on PATH), --connect <url>,
--save <path.rrd>, --stdout. robot_time comes from the NT server timestamp unless
--robot-time-topic is set (double seconds or int microseconds); log_time is wall clock.
Ctrl-C flushes and exits 0. The bridge reconnects both sides and keeps the entity metadata cache
across reconnects: NetworkTables is dialed again with backoff, and a Rerun viewer that was closed
is reopened (--spawn) or retried (--connect) and gets the field, robots, cameras and layout
again. --save and --stdout have no connection to lose and are never restarted.
--field-model <path> and --robot-model <path> select your own glTF/GLB/OBJ/STL models
instead of waiting for the declaration to name a field model or using the bundled kitbot
drivebase. The field model is drawn in the field frame; a robot model is drawn in the robot frame
(+x forward, +y left, +z up) with its origin at the footprint center on the carpet, and
replaces the kitbot on every declared robot. A robot model that cannot be read only falls back to
the kitbot.
Everything robot code can declare can also come from a file. --nt reads moneyshift.toml from the
working directory when it exists, and --config <path> names another file. The importer reads it
too: rerun-importer-moneyshift <log> looks for moneyshift.toml next to the log and then in each
parent directory, so one file at the top of a log folder lays out every log below it. The [layout] table is
the same declaration /Moneyshift/layout carries, so the field, robots, cameras, paths and the
viewer blueprint read the same either way:
# moneyshift.toml: draw the field and the viewer layout without any robot code.
override = true
field_model = "models/field.glb" # replaces the model the layout names
robot_model = "models/robot.glb" # replaces the bundled kitbot
[layout]
version = 1 # optional; the bridge speaks version 1
[layout.field]
carpet = true
lines = [
{ points = [[0.0, 0.0], [16.54175, 0.0], [16.54175, 8.069326], [0.0, 8.069326]], closed = true },
{ points = [[8.270875, 0.0], [8.270875, 8.069326]], closed = false },
]
[[layout.field.tags]]
id = 3
x = 3.62
y = 4.03
z = 1.35
yaw = 3.1416
size = 0.1651
[[layout.robots]]
team = 254
pose_topic = "/AdvantageKit/RealOutputs/Drive/Pose2d"
half_extents = [0.45, 0.45, 0.3]
swerve = { states_topic = "/AdvantageKit/RealOutputs/Drive/SwerveStates" }
[[layout.robots.cameras]]
name = "front"
x = 0.3
y = 0.0
z = 0.5
pitch = -10.0
fov = 70.0
stream = "http://10.25.4.11:1181/stream.mjpg"
[[layout.paths]]
name = "auto"
topic = "/Auto/Path"
[layout.blueprint]
container = "tabs"
[[layout.blueprint.views]]
kind = "three_d"
[[layout.blueprint.views]]
kind = "dataframe"
name = "State"
entities = ["/AdvantageKit/RealOutputs/Drive/Pose2d"]The importer and the live bridge draw a declaration through the same overlay and decode the topics
it names with the same code, so a topic means the same thing in a recording as it does live. The
file's declaration is drawn as soon as the bridge starts, so the field, the robot models, the
camera streams and the viewer layout appear even with the robot off and no robot code running. A
/Moneyshift/layout published later replaces it, exactly as it replaces the default layout; set
override = true (or pass --override) to ignore that topic and keep the file authoritative.
Command-line flags win over the file for --field-model and --robot-model, and a [layout]
declared in the file is validated exactly like one robot code publishes.
On the robot side, see moneyshift/README.md:
NtRecorder rec = NtRecorder.getInstance();
rec.log("world/points", Points3D.builder()
.withPositions(Position3D.of(1, 2, 3))
.withColors(0xFF00E5FFL)
.build());web/ loads the wasm build of the WPILog importer and Rerun's web viewer. Build the artifact with:
cargo build --release --lib --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir web/dist target/wasm32-unknown-unknown/release/moneyshift.wasmcargo test # Rust: importer, decoder, live logic
cargo test --test importer_cli # external-importer protocol smoke tests
cargo test --test wire_fixture # decodes the Java SDK's wire fixture
cd moneyshift && ./gradlew :lib:test :example:test # Java
cargo run --example decode_rrd -- /tmp/live.rrd # decode any RRD with Rerun's decodertest_data/FRC_TBD_d225b5377c70a88d.wpilog is a real AdvantageKit log used by the tests; treat it
as read-only.
The Java type surface is generated: edit xtask/src/codegen/java.rs and run
cargo xtask java moneyshift/lib/src/main/java. Generated files are checked in and marked
DO NOT EDIT.
The /Rerun wire format is shared by the Java writer and the Rust bridge, so it is pinned by a
checked-in fixture: moneyshift/lib/src/test/resources/rerun_wire_fixture.json is produced by the
Java WireFixtureTest and decoded by tests/wire_fixture.rs. Regenerate it after an intentional
layout change with ./gradlew :lib:test --tests '*WireFixtureTest' -Dmoneyshift.writeFixture=true
and commit it together with the codegen output.
| Path | Purpose |
|---|---|
src/importer.rs, src/cli.rs, src/main.rs |
Console importer and CLI dispatch |
src/nt/protocol.rs |
/Rerun NetworkTables column decoder |
src/nt/native.rs |
Plain NT topic adapter |
src/live.rs |
Live subscription, debounce, timestamps, sinks, reconnect |
src/config.rs |
The live bridge's moneyshift.toml (field, robots, cameras, blueprint) |
src/conv/, src/log.rs, src/values/, src/wpilog/ |
WPILog parsing and Rerun conversion heuristics |
moneyshift/ |
Java SDK and example |
web/ |
Browser importer |
xtask/ |
Java code generator |