File format reference¶
Zenith keeps everything in plain JSON files inside your robot repository. Five kinds of file:
Keep the editor, CLI and runtime on compatible versions
These formats describe the source version of this guide. A development branch can write newer
formats than the latest published runtime accepts. Use the documentation and tools from the
same release when running an auto on the robot. An older runtime can reject a file saved by a
newer editor; changing formatVersion by hand does not convert it.
| file | what it holds | current formatVersion |
JSON Schema |
|---|---|---|---|
zenith.json |
the link file: where everything else lives, how to run the sim | 1 | link.json |
robot.json |
one robot: footprint, speeds, intake mouths, commands, conditions | 1 | robot.json |
*.field.json |
one season's field: walls, obstacles, zones, game elements, start rules | 3 | field.json |
waypoints.json |
named poses shared by every auto | 1 | waypoints.json |
*.auto.json |
one autonomous routine | 4 | auto.json |
Two more kinds of file travel between teams instead of living in your repository. They are described at the end of this page:
| file | what it holds | current formatVersion |
JSON Schema |
|---|---|---|---|
*.projection.json |
what another team receives when you share an auto | 1 | projection.json |
*.proposal.json |
the changes a team proposes to an auto shared with it | 1 | proposal.json |
A typical layout:
<robot repo>/
zenith.json link file
autos/
robot.json this robot
field/biobuzz.field.json the season's field
waypoints.json named poses
first-auto.auto.json one routine
collect-and-score.auto.json another routine
The starter example, examples/starter in the Zenith repository, is laid out the same way. The
examples on this page are its files.
Some code blocks show one property, such as "sim": { ... }, rather than a whole file. Insert that
property into the surrounding JSON object, keeping its other properties and the commas between
them. Complete file examples include the outer braces and formatVersion.
zenith init writes zenith.json, a skeleton robot.json and waypoints.json, and a copy of the
BIOBUZZ field. zenith new <name> adds an auto.
Units¶
Files use inches, radians and seconds. Every numeric key names its unit in a suffix:
| suffix | unit | examples |
|---|---|---|
In |
inches | xIn, widthIn, distanceIn |
Rad |
radians | headingRad, fromRad, minRad |
S |
seconds | timeoutS, settleS, autoS |
InPerS |
inches per second | maxForwardVelInPerS |
InPerS2 |
inches per second squared | accelInPerS2 |
RadPerS |
radians per second | maxAngularVelRadPerS |
The editor shows degrees and converts at the boundary. Files never hold degrees, with one exception:
the field image's rotationDeg, which is a quarter-turn setting, not a measurement.
The field frame¶
All poses are in one frame, fixed by the field file:
- The origin is the centre of the field.
- +X points to the audience's right.
- +Y points away from the audience.
- Heading 0 points along +X, and a positive heading turns counter-clockwise.
- Headings are wrapped to (−π, π].
An auto's poses are in the frame of its own alliance: a RED file's poses are where the robot
drives as RED, and a BLUE file's poses are where it drives as BLUE. The editor draws a file as its
own alliance by default, so what you draw is what the robot drives. Autos are usually written for
the RED alliance, the canonical alliance, and Zenith produces the BLUE version by mirroring. For BIOBUZZ the mirror is a point symmetry through the field centre:
a RED pose (x, y, h) becomes the BLUE pose (−x, −y, h + π). Named field elements swap to their
other-alliance counterparts, so a mirrored routine expects the hive and flowers it will really face.
Headings mirror with the poses. A linear sweep keeps its size and the way it turns, so its toRad
can land outside (−π, π]; under a reflection the sweep turns the other way. The robot runtime mirrors a
file run as the other alliance the same way.
In the robot frame, the nose points along +x and the robot's left along +y.
Canonical form¶
Zenith writes every file the same way, byte for byte. A file loaded and saved without changes comes back identical. That keeps git diffs of an auto small and pull requests readable.
- UTF-8, LF line endings, a trailing newline.
- Two-space indent.
- Object keys in schema order, not alphabetical:
namebeforesteps,kindbeforesegments. The order in the examples below is the canonical order. - Numbers rounded by the unit of the key that holds them:
| key ends in | decimals | examples |
|---|---|---|
Rad |
4 | headingRad, fromRad |
In |
3 | xIn, lengthIn |
S or S2 |
3 | timeoutS, maxForwardVelInPerS, accelInPerS2, maxAngularVelRadPerS |
The suffix at the very end decides, so maxAngularVelRadPerS ends in S and rounds to 3 decimals.
Rounding applies only to the key that names the unit. An array under such a key rounds its elements the same way. A number inside an object under such a key names no unit of its own and keeps full precision. So in
value is written as 60.12345, not rounded to 3 decimals. A measured constant keeps every digit
it was measured to.
A number that must be over 0, such as a timeoutS, never rounds down to 0: a timeoutS of
0.0004 is written as 0.001, so the saved file still loads. A number that may be 0, such as a
coordinate, rounds to 0 as usual.
Provenance labels¶
Every constant in robot.json and field.json should say where it came from. Poses in
waypoints.json should too. The label goes first, then the reason:
| label | use it for | trust |
|---|---|---|
MEASURED |
measured on the real robot or field | measured |
SPEC |
taken from the game manual or a part's datasheet | measured |
CALIBRATED FROM ROBOT |
fitted by zenith calibrate from real-robot recordings |
measured |
SET BY HAND |
chosen deliberately, such as a speed limit | derived |
SET FROM EDITOR |
placed by dragging or typing in the editor | derived |
SET FROM SIM |
read off a simulator run | derived |
CALIBRATED FROM SIM |
fitted by zenith calibrate from simulator traces |
derived |
CARRIED OVER |
copied from an older robot or season | derived |
NEEDS MEASUREMENT |
known to be wrong or missing; measure it | unverified |
PLACEHOLDER |
a stand-in so the file works | unverified |
APPROX |
an estimate, close but not measured | unverified |
The editor shows each label as a chip and sorts it into one of the three trust tiers. A label that
names two tiers, such as SET FROM SIM; PLACEHOLDER until measured, counts as the weaker one. A
measured or derived label counts only as whole words, so UNMEASURED and SPECIFICATION are not
MEASURED or SPEC, and NOT MEASURED is unverified. A number with no label, or with text that
names no label, is treated as NEEDS MEASUREMENT.
Format versions and migration¶
Every file carries a formatVersion. Each kind of file is versioned on its own, and the version
goes up only for a change that breaks older readers.
| file | current | history |
|---|---|---|
*.auto.json |
4 | 2 added the sequence step. 3 added the piecewise heading mode. 4 added the planner-only transfer step. |
*.field.json |
3 | 2 added the optional image. 3 added the strut obstacle. |
robot.json |
1 | unchanged |
waypoints.json |
1 | unchanged |
zenith.json |
1 | unchanged |
*.projection.json |
1 | new. The optional commands list was added without a new version, since every earlier projection is still valid. |
*.proposal.json |
1 | new |
- Zenith loads older versions and migrates them in memory. The editor, the CLI and the MCP server all do this.
- Zenith always writes the current version. A version 1, 2 or 3 auto comes back from its first save as version 4, and a version 1 or 2 field as version 3, otherwise unchanged. The editor says so when it opens one, since an older Zenith cannot open the new version.
- The robot runtime refuses a newer version than it knows, at init, and the message names both versions. Update the runtime when you update the app.
The $schema URLs keep v1 in their path. The URL names the kind of file; formatVersion says
which revision of that kind it is.
JSON Schemas¶
The schemas are JSON Schema draft 2020-12, served from the docs site:
zenith.json: https://libraries.horizon36596.org/zenith/schema/v1/link.jsonrobot.json: https://libraries.horizon36596.org/zenith/schema/v1/robot.json*.field.json: https://libraries.horizon36596.org/zenith/schema/v1/field.jsonwaypoints.json: https://libraries.horizon36596.org/zenith/schema/v1/waypoints.json*.auto.json: https://libraries.horizon36596.org/zenith/schema/v1/auto.json*.projection.json: https://libraries.horizon36596.org/zenith/schema/v1/projection.json*.proposal.json: https://libraries.horizon36596.org/zenith/schema/v1/proposal.json*.trajectory.json: https://libraries.horizon36596.org/zenith/schema/v1/trajectory.json, the interchange trajectory (see Bring paths from other planners)*.path.json: https://libraries.horizon36596.org/zenith/schema/v1/path.json, the interchange path (see Bring paths from other planners)
Put the URL in a file's $schema key and editors such as VS Code will complete and check it as you
type. $schema is optional when reading; Zenith writes it on every save. The same schemas ship in
the @horizon36596/zenith-schema npm package, with TypeScript types; the two interchange schemas
ship in @horizon36596/zenith-import.
zenith.json¶
The link file at the root of the robot repository. It tells Zenith where the other files are. This
is the starter example's, examples/starter/zenith.json:
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/link.json",
"formatVersion": 1,
"autosDir": "autos",
"robot": "autos/robot.json",
"field": "autos/field/biobuzz.field.json",
"waypoints": "autos/waypoints.json",
"deploy": {
"kind": "androidAssets",
"dir": "TeamCode/src/main/assets/autos",
"commandLibrary": "solverslib",
"robotClass": "org.firstinspires.ftc.teamcode.zenith.MyRobot"
},
"sim": {
"command": "./gradlew :TeamCode:testDebugUnitTest --tests \"*ZenithAutoHeadlessTest*\" -Dzenith.auto={auto}",
"trace": "TeamCode/build/sim/{auto}.trace.json"
},
"codegen": { "package": "org.firstinspires.ftc.teamcode.zenith", "dir": "TeamCode/src/main/java" }
}
| key | required | meaning |
|---|---|---|
autosDir |
yes | the folder holding *.auto.json files |
robot |
yes | path to robot.json |
field |
yes | path to the field file |
waypoints |
no | path to waypoints.json |
deploy |
no | where zenith deploy copies autos. kind is androidAssets (the runtime reads them from the APK's assets) or directory. commandLibrary is ivy or solverslib: which runtime the generated OpModes and zenith codegen classes use (Choosing a command library). It has no default, and zenith deploy, zenith codegen and the desktop app's Deploy refuse to write Java until it is set; the schema accepts a file without it, so an older zenith.json still opens and validates. robotClass is the robot class the generated OpModes build. opModeBase, optional, is your own abstract subclass of your library's AutoFromFile, fully qualified, which the generated OpModes extend in place of AutoFromFile. |
sim |
no | how to run the high-fidelity sim: command, trace and, optionally, server. sim below has the keys. |
codegen |
no | the Java package and source folder zenith codegen writes to |
javaDir |
no | where the robot's Java or Kotlin is: one folder, or a list of them. When it is set, zenith validate also checks robot.json's commands against the NamedCommands registrations there, with no --java flag, and zenith import-commands and the editor's Import commands from Java scan it by default. It may point outside this folder, such as ../robot/TeamCode/src/main/java, since it is only read; a folder that is not there is a warning. |
All paths are relative to the folder holding zenith.json.
sim¶
"sim": {
"command": "./gradlew :TeamCode:testDebugUnitTest --tests \"*ZenithAutoHeadlessTest*\" -Dzenith.auto={auto}",
"trace": "TeamCode/build/sim/{auto}.trace.json",
"server": {
"prepare": "./gradlew <your prepare task>",
"args": "TeamCode/build/sim/server.args",
"cwd": "TeamCode",
"watch": ["TeamCode/src", "TeamCode/build.gradle"]
}
}
| key | required | meaning |
|---|---|---|
command |
yes | the command that runs one auto in the sim. {auto} is replaced with the auto's name. It is split into words and run with no shell; ./gradlew works on Windows too. |
trace |
yes | where that run writes its trace. {auto} is replaced with the auto's name. |
server |
no | a sim server for the desktop app's Full sim speed. Without it, a project runs only Basic. |
server.prepare |
yes, in server |
builds what the server needs and writes the args file. It runs like command, in the folder zenith.json is in, and must exit with 0. |
server.args |
yes, in server |
the Java argument file the server is started with: java @<args>. |
server.cwd |
no | the server's working folder. The folder zenith.json is in when it is not set. |
server.watch |
no | files and folders whose change means the robot code changed. The server is then stopped, and the next run prepares and starts it again. . is the whole repository. |
Every server path is relative to the folder zenith.json is in, written with /, and must stay
inside it: zenith validate refuses one that starts with / or a drive letter, or has a .. part.
The desktop app checks again before it runs anything. ./ and . parts are ignored, so ./TeamCode/
is TeamCode, and on Windows case does not matter.
Some changes inside a watched folder never count as robot code: the args file's folder, the folder
trace writes into, the folder the server says it wrote a trace into, and autosDir. Nor does
anything prepare writes while it runs. A file you save while prepare runs is not noticed either; save
it again once prepare is done. The protocol the server speaks is
The sim server protocol.
robot.json¶
Everything the planner needs to know about one robot. This example is the starter example's
autos/robot.json, a generic robot: a plain mecanum drive, one front intake, a fixed launcher.
Every number is a round placeholder. Replace them with your own robot's values as you measure them.
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/robot.json",
"formatVersion": 1,
"name": "Starter robot",
"frame": { "forward": "+x", "left": "+y", "headingZero": "+x", "headingPositive": "ccw" },
"footprint": {
"startIn": { "lengthIn": 18, "widthIn": 18, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
"expandedIn": { "lengthIn": 18, "widthIn": 18, "provenance": "PLACEHOLDER: starter example, nothing sticks out past the frame" },
"centreOfRotationIn": { "xIn": 0, "yIn": 0, "provenance": "PLACEHOLDER: starter example, the middle of the frame" }
},
"heightIn": { "value": 18, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
"kinematics": {
"maxForwardVelInPerS": { "value": 60, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's velocity tuner" },
"maxStrafeVelInPerS": { "value": 50, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's velocity tuner" },
"forwardDecelInPerS2": { "value": 60, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's deceleration tuner" },
"strafeDecelInPerS2": { "value": 40, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's deceleration tuner" },
"accelInPerS2": { "value": 60, "provenance": "PLACEHOLDER: starter example, the same as the forward deceleration until you calibrate it" },
"maxAngularVelRadPerS": { "value": 6, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
"defaultPathSpeedFraction": { "value": 0.8, "provenance": "SET BY HAND: starter example, 80 percent of full speed on every path" },
"follower": { "library": "pedro", "version": "3.0.0-20260828.185437-17", "holdEnd": true }
},
"mouths": [
{
"id": "front",
"side": "FRONT",
"offsetIn": { "xIn": 7, "yIn": 0 },
"widthIn": 16,
"depthIn": 4,
"provenance": "PLACEHOLDER: starter example, one intake across the front of the frame, flush with the front edge"
}
],
"capacity": { "elementKind": "pollen", "max": 4, "provenance": "SPEC (G407): the most pollen a robot may hold in BIOBUZZ" },
"shooter": { "kind": "fixed", "settleS": { "value": 0.25, "provenance": "PLACEHOLDER: starter example, time for the robot to settle before it launches" } },
"commands": [
{
"name": "intakeOn",
"summary": "Runs the front intake inward and keeps it running.",
"params": { "state": { "type": "enum", "values": ["ON"], "default": "ON" } },
"estimateS": "0",
"requires": ["intake"],
"stationary": false
},
{
"name": "intakeOff",
"summary": "Stops the front intake.",
"params": { "state": { "type": "enum", "values": ["OFF"], "default": "OFF" } },
"estimateS": "0",
"requires": ["intake"],
"stationary": false
},
{
"name": "spinUp",
"summary": "Brings the launcher up to speed so the next score starts sooner. Ends once the launcher is at speed.",
"estimateS": "0.5",
"requires": ["launcher"],
"stationary": false
},
{
"name": "score",
"summary": "Launches count pollen into the alliance's up hive cell, then waits for the launcher to settle. The robot must stand still while it runs.",
"params": { "count": { "type": "integer", "min": 1, "max": 4, "default": 1 } },
"estimateS": "0.5 + count * 0.5",
"requires": ["launcher", "intake"],
"stationary": true,
"ledger": { "launches": "count" }
}
],
"conditions": [
{ "name": "holdingPiece", "summary": "At least one pollen is aboard. A sensor in the intake reads it." },
{ "name": "hopperFull", "summary": "The robot holds as much pollen as it may: four in BIOBUZZ.", "ledger": "full" },
{ "name": "hopperEmpty", "summary": "The robot holds no pollen.", "ledger": "empty" }
]
}
Top-level keys¶
name- A name for people. Shown in the editor.
frame- How the robot frame is laid out. For almost every robot, leave it as shown.
footprintstartInis the robot's box at the start of the match.expandedInis the box with every mechanism out, which the wall and structure checks use.lengthInruns along the nose,widthInacross it.centreOfRotationInis the offset of the turning centre from the box centre.footprint.heightInis also accepted; the top-levelheightInwins when both are set.footprint.clearanceInis optional: how close, in inches, the robot may pass a structure before theCLEARANCEwarning, as{ "value", "provenance" }. It is 1 in when left out, and 0 turns the warning off. Canonical form writes it last infootprint, afterheightIn. Settings, Robot writes it with Set, labelled SET BY HAND.heightIn- How tall the robot is, so the
STRUCTUREcheck knows what it can drive under. 18 in when left out. mouths- Each intake opening:
id,side(FRONT,BACK,LEFTorRIGHT),offsetInfrom the robot centre,widthInanddepthIn. The wall and structure checks sweep the mouths with the body, andMOUTH_LEADINGusesside. capacity- What the robot collects (
elementKind) and how many it can hold (max). shooter- Optional.
kindis free text. A turret addsturretRangeRadwithminRadandmaxRad, used byTURRET_RANGE.cadenceSandsettleSmay describe firing rhythm. commands- The named-command registry. See commands.
conditions- The named conditions a
wait,branchorendConditionmay read. The optionalledgersays how the preview reads the condition from the ledger, so a branch on it draws the side the ledger picks."full"means the robot holds itscapacity.max,"empty"that it holds nothing.{ "quantity": ..., "min": ..., "max": ... }means a number the season's ledger keeps lies frommintomax, both inclusive, either one optional: a place's piece count (robot,hiveRedUpCell,flower0, see transfers) or a counter such ashiveRed.tips.OwnandOpponentin a quantity stand for the routine's alliance and the other one, so{ "quantity": "hiveOwn.tips", "min": 1 }reads "our hive has tipped at least once" for RED and BLUE alike. It is planner-only: on the robot the condition is always your ownBooleanSupplier.
kinematics¶
| key | meaning |
|---|---|
maxForwardVelInPerS |
top speed driving nose-first |
maxStrafeVelInPerS |
top speed driving sideways |
forwardDecelInPerS2 |
how fast the robot slows when coasting forward |
strafeDecelInPerS2 |
how fast it slows when coasting sideways |
accelInPerS2 |
how fast it speeds up |
maxAngularVelRadPerS |
top turning rate |
defaultPathSpeedFraction |
the speedFraction a path uses when it sets none |
settleS |
optional: time added at the end of a path for the follower to settle. 0.25 s when left out. |
strafeFractionWarn |
optional: the STRAFE_FRACTION threshold. 0.2 when left out. |
sweepSpeedFraction |
optional: the SWEEP_SPEED limit. 0.4 when left out. |
calibration |
optional: written back after calibration. band is the estimate's error band as a fraction, with samples and provenance. |
follower |
the path follower: library (pedro), version and holdEnd |
drivetrain |
optional: trackWidthIn, wheelBaseIn, wheelResponseRatePerS for the preview sim |
Each value is an object, { "value": ..., "provenance": "..." }, so it can say where it came from.
follower may also carry the Pedro follower's gains and tolerances, each an optional
{ value, provenance }, for the instant preview sim:
forwardTranslationalPowerPerIn, strafeTranslationalPowerPerIn, headingPowerPerRad,
headingStaticPower, coastPowerPerInPerS, coastFeedforwardPowerPerInPerS,
brakeFeedforwardPowerPerInPerS, maxBrakingPower, headingDriveRatio, brakeAggression,
brakeLinearForwardS, brakeQuadraticForwardS2PerIn, brakeLinearStrafeS,
brakeQuadraticStrafeS2PerIn, headingBrakeLinearS, headingBrakeQuadraticS2PerRad,
endParametricT, endHeadingToleranceRad, endTranslationalToleranceIn,
endVelocityToleranceInPerS, holdTimeoutS, headingDeviationToleranceRad,
translationalDeviationToleranceIn and minCorrectionDistanceIn. Two switches, cosineScale and
turnBeforeDriving, take a boolean or { value, provenance }. A key left out falls back to
Pedro's default or a value derived from the rest of the file. Copy these from your Pedro constants
when you want the preview to follow paths the way your robot does.
Commands¶
Each entry describes one command your robot code registers with NamedCommands.
| key | meaning |
|---|---|
name |
must match the name registered in the robot code |
summary |
shown in the editor: the whole of it on a long hover, its first sentence in a list (see below) |
params |
the arguments, each with a type: integer, number (with optional min, max, default), enum (with values), boolean or string |
estimateS |
how long it takes: an expression over the params using numbers, + - * /, max and min, or "unknown" |
requires |
the subsystems it uses |
stationary |
true if the robot must stand still; a marker firing it raises STATIONARY_MARKER |
movesRobot |
true if it drives the robot by itself; the next path must start from "current" |
ledger |
how it changes what the robot holds, such as { "launches": "count" } |
provenance |
optional: where the entry came from. zenith import-commands writes SET FROM <file>; estimateS NEEDS MEASUREMENT on a command it reads from the robot's Java |
A param, and a condition, may also carry an optional provenance; zenith import-commands writes
SET FROM <file> on each one it adds.
An estimateS of "unknown" draws a hatched block in the timeline, and the routine's total becomes a
lower bound.
Writing a summary¶
Write the first sentence for the person building the auto, and say what the robot does: "Runs the front
intake inward and keeps it running." The editor cuts a summary at its first sentence where a list has
room for one line, in the Insert menu and in Settings with descriptions switched on, and shows the whole
summary when someone hovers the name for a moment. Put anything a programmer needs, such as the Java class
or a default, after that first sentence, where it is still one hover away and never in the way. The cut
is at a sentence end, so Zenith adds no ellipsis. A summary of one sentence is shown whole. A condition's
summary follows the same rule.
*.field.json¶
One season's field, as vector geometry in the field frame. Zenith ships the BIOBUZZ field. Copy it into your repository so you can correct measurements locally.
This is the BIOBUZZ field from the starter example, shortened to one obstacle, one zone, two elements (a pollen in the RED garden and a flower) and one target. The shipped file has them all.
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/field.json",
"formatVersion": 3,
"season": "biobuzz",
"name": "BIOBUZZ presented by RTX (FTC 2026-27)",
"frame": {
"origin": "centre",
"xAxis": "audienceRight",
"yAxis": "awayFromAudience",
"headingZero": "+x",
"headingPositive": "ccw",
"units": "in",
"canonicalAlliance": "RED",
"mirror": "pointSymmetry",
"view": { "audienceAt": "bottom" }
},
"sizeIn": { "xIn": 144, "yIn": 144 },
"periods": { "autoS": 30, "teleopS": 120 },
"obstacles": [
{
"id": "hiveRedPivotBar",
"kind": "box",
"minXIn": -22.75,
"maxXIn": -2.75,
"minYIn": -18.58,
"maxYIn": 18.58,
"minZIn": 25.5,
"maxZIn": 65.6,
"solidToRobot": true,
"provenance": "SPEC plus APPROX (BIOBUZZ Competition Manual V1 §9.6, Figs 9-9 to 9-11). Pivot at x = -12.75: the two hives are 25.5 in centre to centre (Fig 9-10) and the pair is centred on the field (Fig 9-2; FIRST's BIOBUZZ field CAD (STEP v26-27.2) agrees). Cell outer face 18.58 in from the pivot in plan: the cells' inner faces are 18.84 in apart and each cell is 12.04 in deep (Fig 9-9), so the outer face is 21.46 in along the bar, times cos 30 degrees for the 30 degree tilt (Fig 9-10). Opening 20 in wide (§9.6.2, Fig 9-11). z from 25.5 in, the bottom of the down hive, to 65.6 in, the top of the up-cell opening (Fig 9-10). The box is an envelope over both cells' full swing footprint, because either cell may be up. APPROX on the 25.5 in floor: FIRST's BIOBUZZ field CAD (STEP v26-27.2) puts the down-cell floor at 31.981 in and the lowest hive structure at rest at 30.652 in, see TRANSCRIPTION.md."
}
],
"zones": [
{
"id": "loadingZoneRed",
"kind": "rect",
"minXIn": -72,
"maxXIn": -61,
"minYIn": 24,
"maxYIn": 48,
"rule": "G304: no start here",
"provenance": "APPROX (BIOBUZZ Competition Manual V1 §9.3, Figs 9-2 and 9-3; FIRST's Event Field Setup Guide §8.3): 11 in deep off the x = -72 wall, between the y = +24 and y = +48 tile seams, tape included. FIRST's BIOBUZZ field CAD (STEP v26-27.2), on its 23.528 in tile pitch, puts it at x in [-70.674, -59.101], y in [23.907, 46.599], see TRANSCRIPTION.md."
}
],
"elements": [
{
"id": "gardenRed0",
"kind": "pollen",
"container": null,
"xIn": -70.6,
"yIn": -70.6,
"radiusIn": 1.4,
"provenance": "APPROX (BIOBUZZ Competition Manual V1 §10.3.1, Fig 10-2): the red garden's four pollen lie in a line along the audience wall starting in the red corner, each touching the next. Pollen are 2.8 in across (§9.8), so ball i sits at x = -(72 - 1.4 - 2.8 i), y = -(72 - 1.4)."
},
{
"id": "flower0",
"kind": "container",
"container": "flower",
"xIn": -69.46,
"yIn": -24,
"holds": ["pollen", "pollen", "pollen", "pollen"],
"provenance": "SPEC plus APPROX (BIOBUZZ Competition Manual V1 §9.7, Figs 9-2, 9-4 and 9-12). On the x = -72 wall at the y = -24 tile seam, one tile off centre. The ring centre 2.54 in off the wall face (x = -69.46) is APPROX, derived from the public field drawings: FIRST's BIOBUZZ field CAD (STEP v26-27.2) puts it 2.629 in off the wall. Retrieval opening 3.55 in tall by 3.57 in deep at the bottom (Fig 9-12), taken as 6 in wide, the width of the foot. Holds 4 pollen at the start of the match (§10.3.1, Fig 10-2).",
"retrieval": { "side": "field", "openingIn": { "widthIn": 6, "heightIn": 3.55, "depthIn": 3.57 }, "takes": ["pollen"] }
}
],
"targets": [
{
"id": "hiveRedUpCell",
"kind": "cell",
"provenance": "SPEC plus SET BY HAND. Aim at 59.5 in, the middle of the up-cell opening at 53.5 to 65.6 in (BIOBUZZ Competition Manual V1 Fig 9-10). A launch enters the cell's open outer end (§9.6.2, Fig 9-11), so it approaches along y from outboard of the outer face. The 6 in margin outboard of that face is SET BY HAND: a planning choice, not a game figure.",
"hive": "hiveRed",
"aimZIn": 59.5,
"legalApproach": { "outboardOfOuterFaceByIn": 6, "alongAxis": "y" }
}
],
"startRules": { "touchingWall": true, "ownHalf": true, "notInZones": ["loadingZoneRed"], "notTouchingContainers": ["flower"], "holds": { "pollen": 4 } },
"rules": { "plugin": "season-biobuzz", "tipTable": [8, 7, 6, 3, 1, 0], "capacity": 4 }
}
Note
The example is formatted for reading. Canonical form writes each object's keys one per line.
| key | meaning |
|---|---|
season, name |
the season id and its display name |
frame |
the field frame. mirror is pointSymmetry, mirrorX, mirrorY or none. view.audienceAt is where the editor draws the audience by default. |
sizeIn |
the field's size |
image |
optional picture drawn under the geometry; see field image |
periods |
autoS and teleopS; TIME_BUDGET reads autoS |
obstacles |
boxes with a z range, and slanted bars (struts); see obstacles. solidToRobot: true makes STRUCTURE check them. |
zones |
rectangles a rule talks about. appliesInAuto: true makes KEEPOUT check them. ledger: "loadingZone" makes the ledger count the pieces in it as that alliance's LOADING ZONE, where the human player enters pieces. |
elements |
game pieces and containers. The season plugin reads extra keys it put there. |
targets |
what the robot aims at. The season plugin reads extra keys it put there. |
startRules |
what START_ILLEGAL checks, and what the robot holds at the start |
rules.plugin |
the season plugin that tracks game state; see Seasons |
Obstacles¶
An obstacle is one of two kinds, named by kind:
box: an axis-aligned box,minXIn,maxXIn,minYIn,maxYIn,minZInandmaxZIn.strut(added in field format version 3): a straight bar between two points, such as a slanted leg of a hive frame.fromInandtoInare the centres of its two end faces, each withxIn,yInandzIn(the height above the tiles).radiusInis the radius of a round envelope that holds the whole part. Canonical form writes its keys in the orderid,kind,fromIn,toIn,radiusIn,solidToRobot,provenance.
{
"id": "frameLeg",
"kind": "strut",
"fromIn": { "xIn": 23.81, "yIn": 18.56, "zIn": 0.53 },
"toIn": { "xIn": 12.74, "yIn": 0.51, "zIn": 41.09 },
"radiusIn": 0.78,
"solidToRobot": true,
"provenance": "PLACEHOLDER: an example leg, not a measured one"
}
Both kinds take solidToRobot and provenance. What a strut blocks depends on how tall the robot
is: STRUCTURE tests only the part below the robot's top, so
a slanted leg blocks more of the floor for a taller robot. A Zenith build older than field format
version 3 refuses a field file that declares version 3.
Field image¶
An optional picture, added in field format version 2. It is only a picture: no check reads it, and the vector geometry stays the truth. The BIOBUZZ field names none: the editor draws the whole field from its geometry. A field file of your own can name a picture, for example a photo of your practice field, and the editor then offers the picture, the picture with outlines, or the outlines alone.
"image": {
"src": "img/practice-field.webp",
"credit": "Photo by Horizon (FTC 36596)",
"pxBoundsIn": { "left": 40, "top": 32, "right": 1040, "bottom": 1032 },
"rotationDeg": 0,
"provenance": "SET BY HAND: perimeter corners read off the photo"
}
| key | meaning |
|---|---|
src |
the picture, as a path relative to the field file or app:<path> for an image the app ships |
credit |
who made the image; shown wherever the image is |
pxBoundsIn |
"fullBleed" when the field wall is the image's edge, or { "left", "top", "right", "bottom" } in image pixels |
rotationDeg |
0, 90, 180 or 270, clockwise as seen on screen. 0 when left out. |
variants |
optional other looks of the same picture, each { "name", "src" } |
provenance |
optional: where the pixel box came from |
waypoints.json¶
Named poses shared by every auto. An auto points at one with { "ref": "scoreSouth" }. Change the
waypoint and every auto that uses it changes too, so you measure a pose on the field once.
This excerpt contains the three waypoints used by first-auto. The starter's other autos share
the same file and add their own waypoint names.
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/waypoints.json",
"formatVersion": 1,
"waypoints": {
"start": { "xIn": -13, "yIn": -63, "headingRad": 1.5708, "provenance": "SET FROM EDITOR: starter example, against the south wall in RED's half, 4 in west of the centre line, facing the hive, so it drives straight north to scoreSouth; measure your own start tile" },
"scoreSouth": { "xIn": -13, "yIn": -36, "headingRad": 1.5708, "provenance": "SET FROM EDITOR: starter example, south of the RED hive on x = -13, outside the legal approach margin, facing the up cell; 4 in clear of the centre line, so the robot can turn on the spot here without a corner crossing it" },
"park": { "xIn": -40, "yIn": -36, "headingRad": 3.1416, "provenance": "PLACEHOLDER: starter example, an open spot in RED's half to finish in" }
}
}
Each waypoint needs xIn, yIn and headingRad. provenance is optional but recommended. Poses
are for the RED alliance. A name the editor gives a waypoint starts with a letter and holds only
letters, digits, _ and -, at most 64 characters; step ids follow the same rule.
*.auto.json¶
One routine. The file name is <name>.auto.json. This is the starter example's
first-auto.auto.json; collect-and-score.auto.json beside it adds a curve, a marker, a parallel
group and a wait until a condition, and Step kinds shows each of those.
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/auto.json",
"formatVersion": 4,
"name": "first-auto",
"title": "First auto",
"description": "The smallest useful auto: drive out from the start tile, score the four preloaded pollen, and park. Three steps: a path, a command and another path. The getting-started guide walks through it.",
"robot": "autos/robot.json",
"field": "autos/field/biobuzz.field.json",
"alliance": "RED",
"authors": ["Horizon (FTC 36596)"],
"created": "2026-09-23",
"start": { "pose": { "ref": "start" }, "holds": { "pollen": 4 } },
"steps": [
{
"id": "driveOut",
"kind": "path",
"segments": [
{ "kind": "line", "from": { "ref": "start" }, "to": { "ref": "scoreSouth" } }
],
"heading": { "mode": "tangent" },
"notes": "A straight line north to the scoring spot. Tangent heading keeps the front of the robot pointing the way it drives."
},
{
"id": "scorePreload",
"kind": "command",
"name": "score",
"args": { "count": 4 },
"notes": "Launches the four preloaded pollen. score is stationary in robot.json, so it runs as its own step while the robot stands still."
},
{
"id": "park",
"kind": "path",
"segments": [
{ "kind": "line", "from": "current", "to": { "ref": "park" } }
],
"heading": { "mode": "tangent" },
"notes": "from current starts where the last step ended, so the park leg follows on from wherever the robot scored."
}
]
}
Top-level keys¶
| key | required | meaning |
|---|---|---|
name |
yes | the routine's id; matches the file name |
title, description |
no | for people; shown in the editor |
robot, field |
no | paths to the robot and field files, when they differ from zenith.json |
alliance |
yes | RED or BLUE: which alliance the file's poses are written for |
authors, created |
no | who wrote it and when |
start |
yes | pose (a waypoint ref or an inline pose) and holds, what the robot carries at the start |
steps |
yes | the steps, at least one; see Step kinds |
Step ids¶
A step's id is optional. When a step has one, it follows the same rule as a waypoint's name: it
starts with a letter and holds only letters, digits, _ and -, at most 64 characters, for example
driveOut, score_preload or park-2. The editor, zenith.edit.addStep, zenith.edit.insertAfter,
zenith.edit.renameStep and the Inspector's id field refuse any other id and say why. A file written before the rule, with an id such as
shoot preload, still loads; zenith validate and the editor give it a
STEP_ID warning with a one-click rename that also updates the
deadline of a parallel group that named it. A step without an id goes by its place, step3 or
park.2, and that positional id is not subject to the rule.
Segments¶
In the editor a path step is a path chain and each of its segments is a path (Path 1, Path 2, …). Only the editor's words changed: the file keys, the MCP tool names and the generated Java keep "segment".
| kind | fields | notes |
|---|---|---|
line |
from, to |
a straight line |
bezier |
from, control[], to |
a curve with one to three control points |
from and to are an inline pose, a waypoint { "ref": name }, or the string "current", the pose
the previous step ended at. The runtime reads "current" from the follower when the step starts;
the planner takes it from the previous step's end. Segments in one path must meet within 0.5 in.
A later segment's from may be "current" too: there it is the pose the segment before it ended
at, so the two cannot come apart. The editor writes it that way when it adds or removes a path in
a chain.
Heading modes¶
| mode | editor label | Pedro call | fields | the robot faces | use it when |
|---|---|---|---|---|---|
tangent |
Tangent | path.tangent() |
along the direction of travel | the default on mecanum: nose first, front intake leading | |
tangentReversed |
Reverse tangent | path.reverseTangent() |
opposite the direction of travel | a back mechanism should lead | |
constant |
Constant | path.constant(heading) |
headingRad |
one fixed heading | a short, deliberate strafe, such as a sweep along a wall |
linear |
Linear | path.linear(start, end) |
fromRad, toRad |
turning steadily from one heading to the other | the robot must turn during the leg |
facePoint |
Facing point | path.facingPoint(point) |
xIn, yIn, offsetRad? |
toward a point on the field | a camera or launcher should stay on a target while moving |
piecewise |
Piecewise | Interpolator.piecewise().until(t, ...) |
ranges[] |
a different mode on each stretch of the path | hold a heading out of a wall, then turn to score, in one leg |
The editor labels are Pedro Pathing's own names for these heading interpolators, checked against the
Pedro build the robot runtime compiles against. The file keeps its own mode names.
For linear, the sweep is shared over the path's segments by length, and each segment's share turns
the short way round, as Pedro does. On a curve the share turns steadily in the curve's own
parameter rather than its length, as Pedro turns it, so the turn can run a little ahead of or
behind the halfway point; the editor, the estimate and the playback show it where the robot does it.
A share over half a turn goes the other way and raises the
HEADING warning. The robot runtime ignores facePoint's
offsetRad for now.
Piecewise heading¶
A piecewise heading splits the path into ranges of t, the fraction of the path's length from 0
at the start to 1 at the end, and gives each range its own mode:
"heading": {
"mode": "piecewise",
"ranges": [
{ "startT": 0, "endT": 0.4, "heading": { "mode": "constant", "headingRad": 1.5708 } },
{ "startT": 0.4, "endT": 1, "heading": { "mode": "linear", "fromRad": 1.5708, "toRad": 3.1416 } }
]
}
| key | meaning |
|---|---|
ranges[] |
at least one range, in order along the path |
startT, endT |
where the range starts and ends, from 0 to 1; t has no unit and is not rounded |
heading |
the range's own mode: tangent, tangentReversed, constant, linear or facePoint, with that mode's fields; never piecewise |
- The ranges must cover the whole path: the first starts at 0, each starts where the one before it
ends, and the last ends at 1. A gap, an overlap or a range that does not end after it starts is a
HEADING_RANGESerror, because Pedro stops the auto on the robot when the ranges do not cover the path. - A point on a boundary belongs to the range that ends there, as it does in Pedro.
- A
linearrange turns fromfromRadat its start totoRadat its end, the short way round, over that range alone, and on a curve, likelinear, steadily in the curve's own parameter. A range over half a turn raises theHEADINGwarning.tangent,tangentReversedandfacePointread the path wherever the range is. - The robot runtime builds Pedro's
Interpolator.piecewise()for each segment of the path, from the ranges that cross it.
Markers¶
A marker fires a command part way along a path without stopping it. at is one of:
{ "t": 0.5 }: a fraction of the whole path's length, from 0 to 1;{ "distanceIn": 12 }: inches from the start;{ "distanceFromEndIn": 6 }: inches before the end.
command is { "name": ..., "args": ... }, like a command step. The runtime fires markers on
distance travelled, so the editor and the robot agree on where they happen.
expect¶
Optional notes for the planner's ledger, on a path or command step. The robot ignores them.
| key | meaning |
|---|---|
collectFrom, count |
this step collects count pieces from the named container |
launchesInto |
this step launches into the named target, count pieces of them; a name that is not a target on the field raises LAUNCH_TARGET |
tip |
own or opponent: this step tips a hive |
The ledger uses them to track what the robot holds and the season state after each step. count
is a launch count only on a step that launches: beside launchesInto, or on a command whose
registry entry has a launches hint, where it overrides the hint. Beside collectFrom alone it is
how many the step collects. A path's markers launch through their commands' own launches hints.
Transfers¶
A transfer step (format version 4) tells the planner that game pieces move between two places at
this point in the routine, when nothing this robot does moves them: the alliance partner scoring
into your hive, the human player entering pieces through the loading zone, a tip spilling onto a
quarter of the floor.
{ "id": "partnerScores", "kind": "transfer", "from": "partner", "to": "hiveRedUpCell", "expectedCount": 4, "actualCount": 0 }
| key | meaning |
|---|---|
from, to |
two places the season's ledger counts |
expectedCount |
how many pieces the routine is written for, a whole number |
actualCount |
optional: how many the ledger moves instead. Left out, it is expectedCount |
notes |
optional, for people |
actualCount is how a team previews an edge case without touching the routine: set it to 0 and the
ledger behaves as if the partner failed to score, so a branch on a condition that reads the ledger
takes its other side in the preview. The ledger moves at most what the source holds, and the
TRANSFER check says when it could not move them all.
The robot does nothing for a transfer. The runtime runs it as a wait of zero seconds, the estimate gives it no time, and the generated Java carries it only as a comment, so editing either count never changes the robot's code.
The BIOBUZZ places are robot, partner (it starts with the field's preload, startRules.holds),
each hive's …UpCell and …DownCell, flower0 to flower3, the garden lines gardenRed and
gardenBlue (a source only), the four quarters of the floor floorRedGardenSide,
floorRedLoadingSide, floorBlueGardenSide and floorBlueLoadingSide (split at the field's centre
lines; each alliance's garden side is the quarter of its half its garden lies in), every zone marked
ledger: "loadingZone" by its own id (loadingZoneRed, loadingZoneBlue), loose for pieces
whose place is unknown, such as a tip's spill, humanPlayerRed and humanPlayerBlue, which bring
that alliance's nectar into its own loading zone and nowhere else, and offField, which is never
counted. Own and Opponent may stand in for Red and Blue in any of them. Mirroring a routine
for the other alliance swaps the colour in each place.
The BIOBUZZ ledger counts nectar and pollen apart in every place. A from that ends in .nectar or
.pollen (robot.nectar) moves only that kind; without one, pollen moves first. The same suffix
works on expect.collectFrom and on a condition's quantity. A human player brings in as many
nectar as the transfer says: no source gives the real limit.
envelope¶
An envelope says where the robot stays while a step runs, when nobody can say exactly where it goes:
a command that drives the robot by itself, such as a vision collect, or a wait for a condition. It
is optional, it can go on a path, command or wait step, and the robot ignores it.
"envelope": {
"polygon": [
{ "xIn": -72, "yIn": -48 },
{ "xIn": -36, "yIn": -48 },
{ "xIn": -36, "yIn": -12 },
{ "xIn": -72, "yIn": -12 }
],
"maxDurationS": 6,
"provenance": "SET BY HAND: vision collect stays in the south-west quarter"
}
| key | meaning |
|---|---|
polygon |
3 to 32 corners, { "xIn", "yIn" } in field inches, in order round the outside |
maxDurationS |
the longest the step runs, in seconds: over 0 and at most 30 |
actualDurationS |
optional, a what-if: how long the step runs this time, in seconds, over 0 and at most maxDurationS |
provenance |
required: where the shape and the time came from, as a provenance label |
While the step runs, for at most maxDurationS, the robot's bounding box is declared to stay inside
the polygon.
- The polygon must be one simple shape. Edges that cross or fold back, corners that all lie on one
line, or fewer than three distinct corners raise the
ENVELOPE_SHAPEerror. - A command or wait with no estimate of its own, such as a command whose
estimateSis"unknown"or a wait onuntil, is timed bymaxDurationSinstead of being hatched on the timeline. A step that has its own estimate keeps it. actualDurationSpreviews a run that finishes sooner than the window, or fails early. Where the window times the step (the estimate, the timeline, the Ideal playback and the Instant sim), the planner uses it instead ofmaxDurationS. A partner's projection keepsmaxDurationS, the promise. A value longer thanmaxDurationSis refused, saying so. Leave it out for a run of the whole window. The robot ignores it.- A
sequence,parallelorbranchcannot carry an envelope; put it on the step inside. - In the file it comes after
expect(on await, aftertimeoutS) and beforenotes.
In the editor, select the step and use its Envelope section, or drag the envelope's corners on the field.
rejoinFrom¶
After a command that robot.json marks movesRobot, nobody knows exactly where the robot is, so the
next path starts from "current" and the planner cannot draw it. rejoinFrom tells the planner
where to assume the robot is when that path starts. It is optional and goes on a path step.
{
"id": "backToScore",
"kind": "path",
"rejoinFrom": { "xIn": -48, "yIn": -30, "headingRad": 1.5708, "provenance": "SET BY HAND: the collect ends near here" },
"segments": [ { "kind": "line", "from": "current", "to": { "ref": "scoreSouth" } } ],
"heading": { "mode": "tangent" }
}
| key | meaning |
|---|---|
xIn, yIn, headingRad |
the pose the planner starts the path from, in field inches and radians |
provenance |
optional: where the pose came from |
- It is used only when the path's first segment starts from
"current"and the last step before it that moves the robot is amovesRobotcommand (or asequencethat ends in one). Steps that hold the robot still may come between them: await, atransferor a command that does not drive. For example, amovesRobotcommand, then a 1 swait, then the path still uses it. The planner then starts the path at this pose and takes it as where that command, and the steps that held still after it, end. - Anywhere else it is not used, and the
MOVES_ROBOTwarning says so. - The robot ignores it: on the robot,
"current"is always the follower's own pose. - In the file it comes after
timeoutSand beforesegments.
envelope and rejoinFrom are optional additions, so adding them did not change the format
version, and nor did actualDurationS inside envelope: an older reader never meets it.
*.projection.json¶
A projection is what another team receives when you give it a share file of an auto: a summary of
the routine made on your own computer, never the auto file. The editor's Download share file,
zenith projection and the MCP server's zenith.projection
make one, all byte for byte the same, and so does the Share dialog. A partner loads it in the Partners
panel or passes it to zenith alliance. A downloaded one is
named <auto>.<level>.projection.json. What the sharing settings mean is in
Teams and sharing.
A projection is built from an allowlist: each key below is copied into a new object from the one
place that should supply it, so a key added to the auto format later can never leak into a
projection by default. It is read from another team, so it is strict, unlike the files a team
writes for itself: an unknown key at any depth is refused rather than dropped, every list and every
string has a limit, and $schema is required. This is the starter example's first-auto shared at
the view level with a title and with command names included, cut to its first three trajectory rows
(the full projection has 53). Its timing comes from the instant model, not a recorded robot run:
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/projection.json",
"formatVersion": 1,
"kind": "projection",
"level": "view",
"season": "biobuzz",
"alliance": "RED",
"title": "Horizon first auto",
"startPose": { "xIn": -13, "yIn": -63, "headingRad": 1.5708 },
"boundingBox": { "lengthIn": 18, "widthIn": 18, "centreOffsetIn": { "xIn": 0, "yIn": 0 } },
"trajectory": {
"rateHz": 10,
"positionQuantumIn": 0.5,
"headingQuantumRad": 0.0175,
"poses": [[-13, -63, 1.5708], [-13, -62.5, 1.5708], [-13, -61, 1.5708]]
},
"phases": [
{ "startS": 0, "endS": 1.14, "label": "drive" },
{ "startS": 1.14, "endS": 3.64, "label": "score" },
{ "startS": 3.64, "endS": 5.29, "label": "park" }
],
"claims": [{ "elementId": "hiveRedUpCell", "action": "score", "atS": 1.14 }],
"envelopes": [],
"totalS": 5.29,
"commands": [
{ "token": "cmd1", "kind": "command", "name": "score", "args": { "count": 4 }, "startS": 1.14, "endS": 3.64 }
]
}
The example is written compactly to fit; in canonical form each trajectory row is written one number to a line, like every other array.
Regenerate the complete projection from the starter project directory with a matching CLI build:
Top-level keys¶
| key | required | meaning |
|---|---|---|
$schema |
yes | exactly https://libraries.horizon36596.org/zenith/schema/v1/projection.json |
formatVersion |
yes | 1 |
kind |
yes | "projection" |
level |
yes | view or propose: how much the other team sees |
season |
yes | the season id the field file names, such as biobuzz; 1 to 200 characters |
alliance |
yes | RED or BLUE: the alliance the poses are written for. A partner on the other alliance mirrors them with the field's own mirror. |
title |
no | the title the other team sees, 1 to 200 characters. Only there when the sharer typed one; the auto's own title is never copied. |
startPose |
yes | xIn, yIn, headingRad: the start pose, rounded like the trajectory |
boundingBox |
yes | one box around the whole robot; see below |
trajectory |
yes | where the robot is over time; see below |
phases |
yes | up to 200 time windows, each labelled from a fixed list |
claims |
yes | up to 200 field elements the auto takes from or scores into |
envelopes |
yes | up to 50 areas the robot stays inside for a time window |
totalS |
yes | the length of the run in the instant sim, in seconds, zero or more |
structure |
no | the steps, at the propose level only; see The structure |
commands |
no | what each command token stands for, only when the sharer ticked Include command names; see Command names |
Every pose is in the season's frame, which for every shipped season is the field file's own frame, in inches and radians.
boundingBox, trajectory, phases, claims and envelopes¶
| key | meaning |
|---|---|
boundingBox.lengthIn, widthIn |
the size of one axis-aligned box in the robot frame, over 0: the union of the robot's expanded footprint and every intake mouth, with no mouth layout. It is rounded outward to 0.5 in, even when the rest is coarsened. |
boundingBox.centreOffsetIn |
xIn, yIn: where the box's centre sits from the point the path follows, in the robot frame |
trajectory.rateHz |
always 10: one row every 0.1 s |
trajectory.positionQuantumIn |
what positions are rounded to, in inches: 0.5, or 2 when coarsened |
trajectory.headingQuantumRad |
what headings are rounded to, in radians: one degree (0.0175), or five degrees (0.0873) when coarsened |
trajectory.poses |
1 to 3000 rows (300 s), each [xIn, yIn, headingRad], from t = 0 to the end of the run |
phases[] |
startS, endS (not before startS) and label: one of drive, score, collect, wait, park or other. Neighbouring steps with the same label are joined. |
claims[] |
elementId, an element, container, target or zone id from the public field file (or the start of an element id, for a row of them); action, take or score; atS, when the step that does it starts |
envelopes[] |
startS, endS and polygon, 3 to 200 corners { "xIn", "yIn" }: a step's envelope, from when the step starts for its maxDurationS (never its what-if actualDurationS), rounded outward. Its provenance is not carried. |
Times are rounded to the millisecond, or to 0.25 s when coarsened. A phase is score when a launch
happens in it and collect when a collection does, both from the ledger; otherwise a wait is wait,
a step that drives is drive, and anything else is other. The last drive becomes park when its
step's id says park and nothing after it drives, scores or collects.
The structure¶
At the propose level, structure is { "steps": [...] }: the auto's steps with every waypoint
and "current" resolved to a literal pose, so the other team can propose changes.
Poses in it are not coarsened, because a proposal edits the real poses.
- Steps are named
s1tosN, depth first, a group before its members and a branch'sthenarm before itselsearm. Your own step ids are never sent; your app maps these back. - Every command is a token,
cmd1,cmd2and on, counted in step order, and every condition is{ "token": "cond1" }and on, counted apart. Neither says what it stands for. - Every list in it holds at most 200 entries, and steps nest at most 32 levels deep (a top-level step is level 1). A deeper projection is refused before it is read.
| kind | keys |
|---|---|
path |
id; timeoutS, over 0, optional; segments, 1 or more line (from, to) or bezier (from, 1 to 3 control points, to) segments, every pose literal; heading, optional, any heading mode; speedFraction, over 0 and at most 1, optional; markers, optional, each { "at", "token" } with at as on an auto's marker; endCondition, a condition token, optional |
command |
id; token; estimateS, the seconds robot.json estimates for this call, or null when it is unknown; stationary and movesRobot, the command's two flags from robot.json; timeoutS, optional |
wait |
id; seconds, zero or more, or until, a condition token; timeoutS, optional |
sequence |
id; steps |
parallel |
id; mode, all, race or deadline; deadline, the opaque id of the member that ends the group, optional; steps |
branch |
id; condition, a condition token; then; else, optional |
Command names¶
commands is there only when the sharer ticks Include command names, which is off by default, at either level. It
names what each command token stands for and nothing else: one entry per command token, in token
order. A projection without it is byte for byte what it would be if the option did not exist, and
condition tokens are never named.
| key | required | meaning |
|---|---|---|
token |
yes | the command token, cmd1 and on |
kind |
yes | command for a command step, marker for a marker on a path |
name |
yes | the command's name as the auto calls it, 1 to 200 characters |
args |
no | the arguments exactly as the auto passes them, at most 200, each a string of up to 200 characters, a number or true/false; absent when the call has none |
startS, endS |
no | the span of the step the token belongs to in the instant sim: a command step's own run, or the whole path a marker fires on. Both or neither; absent for a step that did not run, such as one in a branch arm the sim did not take. |
At most 1000 entries. Nothing from robot.json's command table is carried: no summary, no
parameter table, no estimate formula, no code.
What a projection leaves out¶
A projection never holds:
- Java or any generated code;
robot.json: its kinematics, measured constants, follower gains, provenance and mouth layout. Only the bounding box, each command's estimated time and its two flags come from it;- condition names, at any level;
- command names and arguments, unless the sharer includes them in
commands; - the author's step ids, waypoint names, notes, the description, authors and dates;
- the auto's own title, unless the sharer types one;
- file paths,
zenith.jsonor the repository's name.
It still shows how fast the robot drives and in what order it scores, which is what a partner needs and what anyone watching a match sees. Coarsen rounds the timed layer more.
*.proposal.json¶
A proposal is the changes a team suggests to an auto another team shared with it at the propose
level. The proposing team only ever holds the projection, so a proposal names every step by the
projection's opaque id (s3) and every command and condition by its token (cmd2, cond1), and it
has no way to name, add or change a command. The editor's Save proposal… writes one as
<share>.proposal.json, and
zenith proposal validate and
zenith proposal apply check and apply one. How the owning
team reviews it is in Alliance partners, and every way it can be
refused in Proposal findings.
Like a projection, it is strict: an unknown key at any depth is refused, every list and string has a
limit, and $schema is required.
{
"$schema": "https://libraries.horizon36596.org/zenith/schema/v1/proposal.json",
"formatVersion": 1,
"kind": "proposal",
"shareRef": { "shareId": "k3f9x2Qm7TzL", "revision": 3 },
"baseStructureHash": "sha256:913f927447d1b42cc2e8a9a5ead0ab93de5b04b26836be97a4182b380f6da7f5",
"changes": [
{ "op": "insertWait", "afterStepId": "s1", "seconds": 0.5, "comment": "Let our robot cross in front of you first." },
{ "op": "movePose", "stepId": "s3", "segmentIndex": 0, "point": "to", "pose": { "xIn": -40, "yIn": -30 } },
{ "op": "setSpeedFraction", "stepId": "s3", "speedFraction": 0.8, "comment": "Slower on the way to park." }
]
}
Top-level keys¶
| key | required | meaning |
|---|---|---|
$schema |
yes | exactly https://libraries.horizon36596.org/zenith/schema/v1/proposal.json |
formatVersion |
yes | 1 |
kind |
yes | "proposal" |
shareRef |
yes | shareId, the share's id, 1 to 200 letters, digits, _ or -; and revision, a whole number zero or more, the version of the auto the share was made from |
baseStructureHash |
yes | sha256: and 64 lowercase hex digits: the SHA-256 of the projection's structure it was made against, so a proposal against an older version is refused as a whole |
changes |
yes | up to 200 changes, applied in order |
Whether the share is live, revoked or expired is not in any file: the team library knows it.
Changes¶
Each change has an op from the closed list below, and an optional comment, one line of 1 to 200
characters. stepId is an opaque id from the projection. Indexes count from 0.
op |
keys | does |
|---|---|---|
movePose |
stepId, segmentIndex, point, pose |
moves one point of a path's segment: point is "from", "to" or { "controlIndex": n } for a Bézier control point. pose is xIn, yIn and an optional headingRad; left out, the heading stays as it was, and a control point takes only xIn and yIn. |
setHeading |
stepId, heading |
gives a path a new heading mode |
setSpeedFraction |
stepId, speedFraction |
sets a path's speed fraction, over 0 and at most 1 |
setTimeout |
stepId, timeoutS |
sets a path, command or wait step's timeout, in seconds over 0, or removes it with null |
insertWait |
afterStepId or atStart: true; seconds or until |
adds a wait straight after a step, in the list that holds it, or at the start of the auto. It waits seconds, zero or more, or until a condition token the share holds; exactly one of each pair. |
setWait |
stepId; seconds or until |
changes what a wait step waits for; exactly one of the two |
removeStep |
stepId |
removes a path or wait step. A command, a group, a parallel group's deadline or the only step of a group cannot be removed. |
addPath |
afterStepId or atStart: true; segments, heading; optional timeoutS, speedFraction, markers, endCondition |
adds a path step, placed like an inserted wait, with every pose literal as in the structure. Its markers and end condition can only use tokens the share holds and that the rest of the proposal frees, so a token is moved, never copied. |
reorder |
stepId, newIndex |
moves a step to newIndex in the list that holds it, counted after it is taken out |
moveMarker |
stepId, markerIndex, at |
slides one marker of a path to a new at, as on an auto's marker |
setEnvelope |
stepId, envelope |
gives a path, command or wait step an envelope (polygon of 3 to 32 corners, maxDurationS over 0 and at most 30, an optional actualDurationS over 0 and at most maxDurationS, provenance of 1 to 200 characters), or removes it with null |
A step the proposal adds gets the next opaque id after the share's highest (s13, s14, ...), in
the order the changes add them, so a later change can name it. A token must end up held exactly once:
a change that holds a token the share did not hold, or copies one, is refused. Applied poses carry no
provenance, exactly like a hand edit, so they read as SET FROM EDITOR.