@horizon36596/zenith-mcp 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +18 -0
  3. package/README.md +129 -0
  4. package/dist/editIo.d.ts +15 -0
  5. package/dist/editIo.d.ts.map +1 -0
  6. package/dist/editIo.js +33 -0
  7. package/dist/editIo.js.map +1 -0
  8. package/dist/findings.d.ts +35 -0
  9. package/dist/findings.d.ts.map +1 -0
  10. package/dist/findings.js +76 -0
  11. package/dist/findings.js.map +1 -0
  12. package/dist/index.d.ts +11 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +21 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/project.d.ts +46 -0
  17. package/dist/project.d.ts.map +1 -0
  18. package/dist/project.js +106 -0
  19. package/dist/project.js.map +1 -0
  20. package/dist/resources.d.ts +3 -0
  21. package/dist/resources.d.ts.map +1 -0
  22. package/dist/resources.js +37 -0
  23. package/dist/resources.js.map +1 -0
  24. package/dist/result.d.ts +14 -0
  25. package/dist/result.d.ts.map +1 -0
  26. package/dist/result.js +27 -0
  27. package/dist/result.js.map +1 -0
  28. package/dist/schemas.d.ts +576 -0
  29. package/dist/schemas.d.ts.map +1 -0
  30. package/dist/schemas.js +197 -0
  31. package/dist/schemas.js.map +1 -0
  32. package/dist/server.d.ts +13 -0
  33. package/dist/server.d.ts.map +1 -0
  34. package/dist/server.js +27 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/spawnCli.d.ts +28 -0
  37. package/dist/spawnCli.d.ts.map +1 -0
  38. package/dist/spawnCli.js +57 -0
  39. package/dist/spawnCli.js.map +1 -0
  40. package/dist/tools/analysis.d.ts +4 -0
  41. package/dist/tools/analysis.d.ts.map +1 -0
  42. package/dist/tools/analysis.js +128 -0
  43. package/dist/tools/analysis.js.map +1 -0
  44. package/dist/tools/auto.d.ts +4 -0
  45. package/dist/tools/auto.d.ts.map +1 -0
  46. package/dist/tools/auto.js +43 -0
  47. package/dist/tools/auto.js.map +1 -0
  48. package/dist/tools/edit.d.ts +3 -0
  49. package/dist/tools/edit.d.ts.map +1 -0
  50. package/dist/tools/edit.js +346 -0
  51. package/dist/tools/edit.js.map +1 -0
  52. package/dist/tools/project.d.ts +4 -0
  53. package/dist/tools/project.d.ts.map +1 -0
  54. package/dist/tools/project.js +58 -0
  55. package/dist/tools/project.js.map +1 -0
  56. package/dist/version.d.ts +3 -0
  57. package/dist/version.d.ts.map +1 -0
  58. package/dist/version.js +4 -0
  59. package/dist/version.js.map +1 -0
  60. package/package.json +67 -0
  61. package/spec/checks.md +313 -0
  62. package/spec/file-format.md +672 -0
@@ -0,0 +1,672 @@
1
+ # File format reference
2
+
3
+ Zenith keeps everything in plain JSON files inside your robot repository. Five kinds of file:
4
+
5
+ | file | what it holds | current `formatVersion` | JSON Schema |
6
+ |---|---|---|---|
7
+ | `zenith.json` | the link file: where everything else lives, how to run the sim | 1 | [`link.json`](https://libraries.horizon36596.org/zenith/schema/v1/link.json) |
8
+ | `robot.json` | one robot: footprint, speeds, intake mouths, commands, conditions | 1 | [`robot.json`](https://libraries.horizon36596.org/zenith/schema/v1/robot.json) |
9
+ | `*.field.json` | one season's field: walls, obstacles, zones, game elements, start rules | 2 | [`field.json`](https://libraries.horizon36596.org/zenith/schema/v1/field.json) |
10
+ | `waypoints.json` | named poses shared by every auto | 1 | [`waypoints.json`](https://libraries.horizon36596.org/zenith/schema/v1/waypoints.json) |
11
+ | `*.auto.json` | one autonomous routine | 3 | [`auto.json`](https://libraries.horizon36596.org/zenith/schema/v1/auto.json) |
12
+
13
+ A typical layout:
14
+
15
+ ```text
16
+ <robot repo>/
17
+ zenith.json link file
18
+ autos/
19
+ robot.json this robot
20
+ field/biobuzz.field.json the season's field
21
+ waypoints.json named poses
22
+ first-auto.auto.json one routine
23
+ collect-and-score.auto.json another routine
24
+ ```
25
+
26
+ The starter example, `examples/starter` in the Zenith repository, is laid out the same way. The
27
+ examples on this page are its files.
28
+
29
+ `zenith init` writes `zenith.json`, a skeleton `robot.json` and `waypoints.json`, and a copy of the
30
+ BIOBUZZ field. `zenith new <name>` adds an auto.
31
+
32
+ ## Units
33
+
34
+ Files use **inches, radians and seconds**. Every numeric key names its unit in a suffix:
35
+
36
+ | suffix | unit | examples |
37
+ |---|---|---|
38
+ | `In` | inches | `xIn`, `widthIn`, `distanceIn` |
39
+ | `Rad` | radians | `headingRad`, `fromRad`, `minRad` |
40
+ | `S` | seconds | `timeoutS`, `settleS`, `autoS` |
41
+ | `InPerS` | inches per second | `maxForwardVelInPerS` |
42
+ | `InPerS2` | inches per second squared | `accelInPerS2` |
43
+ | `RadPerS` | radians per second | `maxAngularVelRadPerS` |
44
+
45
+ The editor shows degrees and converts at the boundary. Files never hold degrees, with one exception:
46
+ the field image's `rotationDeg`, which is a quarter-turn setting, not a measurement.
47
+
48
+ ## The field frame
49
+
50
+ All poses are in one frame, fixed by the field file:
51
+
52
+ - The origin is the **centre** of the field.
53
+ - **+X** points to the audience's right.
54
+ - **+Y** points away from the audience.
55
+ - Heading 0 points along +X, and a positive heading turns **counter-clockwise**.
56
+ - Headings are wrapped to (−π, π].
57
+
58
+ An auto's poses are in the frame of its own `alliance`: a RED file's poses are where the robot
59
+ drives as RED, and a BLUE file's poses are where it drives as BLUE. The editor draws a file as its
60
+ own alliance by default, so what you draw is what the robot drives. Autos are usually written for
61
+ 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:
62
+ a RED pose (x, y, h) becomes the BLUE pose (−x, −y, h + π). Named field elements swap to their
63
+ other-alliance counterparts, so a mirrored routine expects the hive and flowers it will really face.
64
+ Headings mirror with the poses. A `linear` sweep keeps its size and the way it turns, so its `toRad`
65
+ can land outside (−π, π]; under a reflection the sweep turns the other way. The robot runtime mirrors a
66
+ file run as the other alliance the same way.
67
+
68
+ In the robot frame, the nose points along +x and the robot's left along +y.
69
+
70
+ ## Canonical form
71
+
72
+ Zenith writes every file the same way, byte for byte. A file loaded and saved without changes comes
73
+ back identical. That keeps git diffs of an auto small and pull requests readable.
74
+
75
+ - UTF-8, LF line endings, a trailing newline.
76
+ - Two-space indent.
77
+ - Object keys in **schema order**, not alphabetical: `name` before `steps`, `kind` before
78
+ `segments`. The order in the examples below is the canonical order.
79
+ - Numbers rounded by the unit of the key that holds them:
80
+
81
+ | key ends in | decimals | examples |
82
+ |---|---|---|
83
+ | `Rad` | 4 | `headingRad`, `fromRad` |
84
+ | `In` | 3 | `xIn`, `lengthIn` |
85
+ | `S` or `S2` | 3 | `timeoutS`, `maxForwardVelInPerS`, `accelInPerS2`, `maxAngularVelRadPerS` |
86
+
87
+ The suffix at the very end decides, so `maxAngularVelRadPerS` ends in `S` and rounds to 3 decimals.
88
+
89
+ Rounding applies **only to the key that names the unit**. An array under such a key rounds its
90
+ elements the same way. A number inside an object under such a key names no unit of its own and keeps
91
+ full precision. So in
92
+
93
+ ```json
94
+ "maxForwardVelInPerS": { "value": 60.12345, "provenance": "MEASURED: velocity tuner" }
95
+ ```
96
+
97
+ `value` is written as `60.12345`, not rounded to 3 decimals. A measured constant keeps every digit
98
+ it was measured to.
99
+
100
+ ## Provenance labels
101
+
102
+ Every constant in `robot.json` and `field.json` should say where it came from. Poses in
103
+ `waypoints.json` should too. The label goes first, then the reason:
104
+
105
+ ```json
106
+ "accelInPerS2": { "value": 60, "provenance": "PLACEHOLDER: until calibrated" }
107
+ ```
108
+
109
+ | label | use it for | trust |
110
+ |---|---|---|
111
+ | `MEASURED` | measured on the real robot or field | measured |
112
+ | `SPEC` | taken from the game manual or a part's datasheet | measured |
113
+ | `CALIBRATED FROM ROBOT` | fitted by `zenith calibrate` from real-robot recordings | measured |
114
+ | `SET BY HAND` | chosen deliberately, such as a speed limit | derived |
115
+ | `SET FROM EDITOR` | placed by dragging or typing in the editor | derived |
116
+ | `SET FROM SIM` | read off a simulator run | derived |
117
+ | `CALIBRATED FROM SIM` | fitted by `zenith calibrate` from simulator traces | derived |
118
+ | `CARRIED OVER` | copied from an older robot or season | derived |
119
+ | `NEEDS MEASUREMENT` | known to be wrong or missing; measure it | unverified |
120
+ | `PLACEHOLDER` | a stand-in so the file works | unverified |
121
+ | `APPROX` | an estimate, close but not measured | unverified |
122
+
123
+ The editor shows each label as a chip and sorts it into one of the three trust tiers. A label that
124
+ names two tiers, such as `SET FROM SIM; PLACEHOLDER until measured`, counts as the weaker one. A
125
+ number with no label is treated as `NEEDS MEASUREMENT`.
126
+
127
+ ## Format versions and migration
128
+
129
+ Every file carries a `formatVersion`. Each kind of file is versioned on its own, and the version
130
+ goes up only for a change that breaks older readers.
131
+
132
+ | file | current | history |
133
+ |---|---|---|
134
+ | `*.auto.json` | 3 | 2 added the [`sequence`](step-kinds.md#sequence) step. 3 added the [`piecewise`](#piecewise-heading) heading mode. |
135
+ | `*.field.json` | 2 | 2 added the optional [`image`](#field-image). |
136
+ | `robot.json` | 1 | unchanged |
137
+ | `waypoints.json` | 1 | unchanged |
138
+ | `zenith.json` | 1 | unchanged |
139
+
140
+ - Zenith **loads older versions** and migrates them in memory. The editor, the CLI and the MCP
141
+ server all do this.
142
+ - Zenith **always writes the current version**. A version 1 or 2 auto comes back from its first save
143
+ as version 3, and otherwise unchanged.
144
+ - The robot runtime **refuses a newer version** than it knows, at init, and the message names both
145
+ versions. Update the runtime when you update the app.
146
+
147
+ The `$schema` URLs keep `v1` in their path. The URL names the kind of file; `formatVersion` says
148
+ which revision of that kind it is.
149
+
150
+ ## JSON Schemas
151
+
152
+ The schemas are JSON Schema draft 2020-12, served from the docs site:
153
+
154
+ - `zenith.json`: <https://libraries.horizon36596.org/zenith/schema/v1/link.json>
155
+ - `robot.json`: <https://libraries.horizon36596.org/zenith/schema/v1/robot.json>
156
+ - `*.field.json`: <https://libraries.horizon36596.org/zenith/schema/v1/field.json>
157
+ - `waypoints.json`: <https://libraries.horizon36596.org/zenith/schema/v1/waypoints.json>
158
+ - `*.auto.json`: <https://libraries.horizon36596.org/zenith/schema/v1/auto.json>
159
+
160
+ Put the URL in a file's `$schema` key and editors such as VS Code will complete and check it as you
161
+ type. `$schema` is optional when reading; Zenith writes it on every save. The same schemas ship in
162
+ the `@horizon36596/zenith-schema` npm package, with TypeScript types.
163
+
164
+ ## `zenith.json`
165
+
166
+ The link file at the root of the robot repository. It tells Zenith where the other files are. This
167
+ is the starter example's, `examples/starter/zenith.json`:
168
+
169
+ ```json
170
+ {
171
+ "$schema": "https://libraries.horizon36596.org/zenith/schema/v1/link.json",
172
+ "formatVersion": 1,
173
+ "autosDir": "autos",
174
+ "robot": "autos/robot.json",
175
+ "field": "autos/field/biobuzz.field.json",
176
+ "waypoints": "autos/waypoints.json",
177
+ "deploy": {
178
+ "kind": "androidAssets",
179
+ "dir": "TeamCode/src/main/assets/autos",
180
+ "commandLibrary": "solverslib",
181
+ "robotClass": "org.firstinspires.ftc.teamcode.zenith.MyRobot"
182
+ },
183
+ "sim": {
184
+ "command": "./gradlew :TeamCode:testDebugUnitTest --tests \"*ZenithAutoHeadlessTest*\" -Dzenith.auto={auto}",
185
+ "trace": "TeamCode/build/sim/{auto}.trace.json"
186
+ },
187
+ "codegen": { "package": "org.firstinspires.ftc.teamcode.zenith", "dir": "TeamCode/src/main/java" }
188
+ }
189
+ ```
190
+
191
+ | key | required | meaning |
192
+ |---|---|---|
193
+ | `autosDir` | yes | the folder holding `*.auto.json` files |
194
+ | `robot` | yes | path to `robot.json` |
195
+ | `field` | yes | path to the field file |
196
+ | `waypoints` | no | path to `waypoints.json` |
197
+ | `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](command-libraries.md)). 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. |
198
+ | `sim` | no | how to run the [high-fidelity sim](simulation.md#high-fidelity-sim). `{auto}` is replaced with the auto's name. |
199
+ | `codegen` | no | the Java package and source folder `zenith codegen` writes to |
200
+
201
+ All paths are relative to the folder holding `zenith.json`.
202
+
203
+ ## `robot.json`
204
+
205
+ Everything the planner needs to know about one robot. This example is the starter example's
206
+ `autos/robot.json`, a generic robot: a plain mecanum drive, one front intake, a fixed launcher.
207
+ Every number is a round placeholder. Replace them with your own robot's values as you measure them.
208
+
209
+ ```json
210
+ {
211
+ "$schema": "https://libraries.horizon36596.org/zenith/schema/v1/robot.json",
212
+ "formatVersion": 1,
213
+ "name": "Starter robot",
214
+ "frame": { "forward": "+x", "left": "+y", "headingZero": "+x", "headingPositive": "ccw" },
215
+ "footprint": {
216
+ "startIn": { "lengthIn": 18, "widthIn": 18, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
217
+ "expandedIn": { "lengthIn": 18, "widthIn": 18, "provenance": "PLACEHOLDER: starter example, nothing sticks out past the frame" },
218
+ "centreOfRotationIn": { "xIn": 0, "yIn": 0, "provenance": "PLACEHOLDER: starter example, the middle of the frame" }
219
+ },
220
+ "heightIn": { "value": 18, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
221
+ "kinematics": {
222
+ "maxForwardVelInPerS": { "value": 60, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's velocity tuner" },
223
+ "maxStrafeVelInPerS": { "value": 50, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's velocity tuner" },
224
+ "forwardDecelInPerS2": { "value": 60, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's deceleration tuner" },
225
+ "strafeDecelInPerS2": { "value": 40, "provenance": "PLACEHOLDER: starter example, measure your own robot with Pedro's deceleration tuner" },
226
+ "accelInPerS2": { "value": 60, "provenance": "PLACEHOLDER: starter example, the same as the forward deceleration until you calibrate it" },
227
+ "maxAngularVelRadPerS": { "value": 6, "provenance": "PLACEHOLDER: starter example, measure your own robot" },
228
+ "defaultPathSpeedFraction": { "value": 0.8, "provenance": "SET BY HAND: starter example, 80 percent of full speed on every path" },
229
+ "follower": { "library": "pedro", "version": "3.0.0-20260828.185437-17", "holdEnd": true }
230
+ },
231
+ "mouths": [
232
+ {
233
+ "id": "front",
234
+ "side": "FRONT",
235
+ "offsetIn": { "xIn": 7, "yIn": 0 },
236
+ "widthIn": 16,
237
+ "depthIn": 4,
238
+ "provenance": "PLACEHOLDER: starter example, one intake across the front of the frame, flush with the front edge"
239
+ }
240
+ ],
241
+ "capacity": { "elementKind": "pollen", "max": 4, "provenance": "SPEC (G407): the most pollen a robot may hold in BIOBUZZ" },
242
+ "shooter": { "kind": "fixed", "settleS": { "value": 0.25, "provenance": "PLACEHOLDER: starter example, time for the robot to settle before it launches" } },
243
+ "commands": [
244
+ {
245
+ "name": "intakeOn",
246
+ "summary": "Runs the front intake inward and keeps it running. The state parameter has one value and a default, so an auto calls intakeOn with no arguments; it tells Zenith's intake checks that this command runs the intake.",
247
+ "params": { "state": { "type": "enum", "values": ["ON"], "default": "ON" } },
248
+ "estimateS": "0",
249
+ "requires": ["intake"],
250
+ "stationary": false
251
+ },
252
+ {
253
+ "name": "intakeOff",
254
+ "summary": "Stops the front intake. Like intakeOn, it takes no arguments in an auto.",
255
+ "params": { "state": { "type": "enum", "values": ["OFF"], "default": "OFF" } },
256
+ "estimateS": "0",
257
+ "requires": ["intake"],
258
+ "stationary": false
259
+ },
260
+ {
261
+ "name": "spinUp",
262
+ "summary": "Brings the launcher up to speed so the next score starts sooner. Ends once the launcher is at speed.",
263
+ "estimateS": "0.5",
264
+ "requires": ["launcher"],
265
+ "stationary": false
266
+ },
267
+ {
268
+ "name": "score",
269
+ "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.",
270
+ "params": { "count": { "type": "integer", "min": 1, "max": 4, "default": 1 } },
271
+ "estimateS": "0.5 + count * 0.5",
272
+ "requires": ["launcher", "intake"],
273
+ "stationary": true,
274
+ "ledger": { "launches": "count" }
275
+ }
276
+ ],
277
+ "conditions": [
278
+ { "name": "holdingPiece", "summary": "At least one pollen is aboard. A sensor in the intake reads it." },
279
+ { "name": "hopperFull", "summary": "The robot holds as much pollen as it may: four in BIOBUZZ.", "ledger": "full" },
280
+ { "name": "hopperEmpty", "summary": "The robot holds no pollen.", "ledger": "empty" }
281
+ ]
282
+ }
283
+ ```
284
+
285
+ ### Top-level keys
286
+
287
+ `name`
288
+ : A name for people. Shown in the editor.
289
+
290
+ `frame`
291
+ : How the robot frame is laid out. For almost every robot, leave it as shown.
292
+
293
+ `footprint`
294
+ : `startIn` is the robot's box at the start of the match. `expandedIn` is the box with every
295
+ mechanism out, which the wall and structure checks use. `lengthIn` runs along the nose,
296
+ `widthIn` across it. `centreOfRotationIn` is the offset of the turning centre from the box
297
+ centre. `footprint.heightIn` is also accepted; the top-level `heightIn` wins when both are set.
298
+
299
+ `heightIn`
300
+ : How tall the robot is, so the [`STRUCTURE`](checks-and-findings.md#structure) check knows what
301
+ it can drive under. 18 in when left out.
302
+
303
+ `mouths`
304
+ : Each intake opening: `id`, `side` (`FRONT`, `BACK`, `LEFT` or `RIGHT`), `offsetIn` from the robot
305
+ centre, `widthIn` and `depthIn`. The wall and structure checks sweep the mouths with the body,
306
+ and [`MOUTH_LEADING`](checks-and-findings.md#mouth_leading) uses `side`.
307
+
308
+ `capacity`
309
+ : What the robot collects (`elementKind`) and how many it can hold (`max`).
310
+
311
+ `shooter`
312
+ : Optional. `kind` is free text. A turret adds `turretRangeRad` with `minRad` and `maxRad`, used by
313
+ [`TURRET_RANGE`](checks-and-findings.md#turret_range). `cadenceS` and `settleS` may describe
314
+ firing rhythm.
315
+
316
+ `commands`
317
+ : The named-command registry. See [commands](#commands).
318
+
319
+ `conditions`
320
+ : The named conditions a `wait`, `branch` or `endCondition` may read. `ledger` is `"full"` or
321
+ `"empty"` when the condition means "at capacity" or "holding nothing", so the preview sim can
322
+ answer it from what the robot holds.
323
+
324
+ ### `kinematics`
325
+
326
+ | key | meaning |
327
+ |---|---|
328
+ | `maxForwardVelInPerS` | top speed driving nose-first |
329
+ | `maxStrafeVelInPerS` | top speed driving sideways |
330
+ | `forwardDecelInPerS2` | how fast the robot slows when coasting forward |
331
+ | `strafeDecelInPerS2` | how fast it slows when coasting sideways |
332
+ | `accelInPerS2` | how fast it speeds up |
333
+ | `maxAngularVelRadPerS` | top turning rate |
334
+ | `defaultPathSpeedFraction` | the `speedFraction` a path uses when it sets none |
335
+ | `settleS` | optional: time added at the end of a path for the follower to settle. 0.25 s when left out. |
336
+ | `strafeFractionWarn` | optional: the [`STRAFE_FRACTION`](checks-and-findings.md#strafe_fraction) threshold. 0.2 when left out. |
337
+ | `sweepSpeedFraction` | optional: the [`SWEEP_SPEED`](checks-and-findings.md#sweep_speed) limit. 0.4 when left out. |
338
+ | `calibration` | optional: written back after calibration. `band` is the estimate's error band as a fraction, with `samples` and `provenance`. |
339
+ | `follower` | the path follower: `library` (`pedro`), `version` and `holdEnd` |
340
+ | `drivetrain` | optional: `trackWidthIn`, `wheelBaseIn`, `wheelResponseRatePerS` for the preview sim |
341
+
342
+ Each value is an object, `{ "value": ..., "provenance": "..." }`, so it can say where it came from.
343
+
344
+ `follower` may also carry the Pedro follower's gains and tolerances, each an optional
345
+ `{ value, provenance }`, for the [instant preview sim](simulation.md#instant-preview-sim):
346
+ `forwardTranslationalPowerPerIn`, `strafeTranslationalPowerPerIn`, `headingPowerPerRad`,
347
+ `headingStaticPower`, `coastPowerPerInPerS`, `coastFeedforwardPowerPerInPerS`,
348
+ `brakeFeedforwardPowerPerInPerS`, `maxBrakingPower`, `headingDriveRatio`, `brakeAggression`,
349
+ `brakeLinearForwardS`, `brakeQuadraticForwardS2PerIn`, `brakeLinearStrafeS`,
350
+ `brakeQuadraticStrafeS2PerIn`, `headingBrakeLinearS`, `headingBrakeQuadraticS2PerRad`,
351
+ `endParametricT`, `endHeadingToleranceRad`, `endTranslationalToleranceIn`,
352
+ `endVelocityToleranceInPerS`, `holdTimeoutS`, `headingDeviationToleranceRad`,
353
+ `translationalDeviationToleranceIn` and `minCorrectionDistanceIn`. Two switches, `cosineScale` and
354
+ `turnBeforeDriving`, take a boolean or `{ value, provenance }`. A key left out falls back to
355
+ Pedro's default or a value derived from the rest of the file. Copy these from your Pedro constants
356
+ when you want the preview to follow paths the way your robot does.
357
+
358
+ ### Commands
359
+
360
+ Each entry describes one command your robot code registers with `NamedCommands`.
361
+
362
+ | key | meaning |
363
+ |---|---|
364
+ | `name` | must match the name registered in the robot code |
365
+ | `summary` | shown in the editor |
366
+ | `params` | the arguments, each with a `type`: `integer`, `number` (with optional `min`, `max`, `default`), `enum` (with `values`), `boolean` or `string` |
367
+ | `estimateS` | how long it takes: an expression over the params using numbers, `+ - * /`, `max` and `min`, or `"unknown"` |
368
+ | `requires` | the subsystems it uses |
369
+ | `stationary` | `true` if the robot must stand still; a marker firing it raises [`STATIONARY_MARKER`](checks-and-findings.md#stationary_marker) |
370
+ | `movesRobot` | `true` if it drives the robot by itself; the next path must start from `"current"` |
371
+ | `ledger` | how it changes what the robot holds, such as `{ "launches": "count" }` |
372
+
373
+ An `estimateS` of `"unknown"` draws a hatched block in the timeline, and the routine's total becomes a
374
+ lower bound.
375
+
376
+ ## `*.field.json`
377
+
378
+ One season's field, as vector geometry in the field frame. Zenith ships the BIOBUZZ field. Copy it
379
+ into your repository so you can correct measurements locally.
380
+
381
+ This is the BIOBUZZ field from the starter example, shortened to one obstacle, one zone, two
382
+ elements (a pollen in the RED garden and a flower) and one target. The shipped file has them all.
383
+
384
+ ```json
385
+ {
386
+ "$schema": "https://libraries.horizon36596.org/zenith/schema/v1/field.json",
387
+ "formatVersion": 2,
388
+ "season": "biobuzz",
389
+ "name": "BIOBUZZ presented by RTX (FTC 2026-27)",
390
+ "frame": {
391
+ "origin": "centre",
392
+ "xAxis": "audienceRight",
393
+ "yAxis": "awayFromAudience",
394
+ "headingZero": "+x",
395
+ "headingPositive": "ccw",
396
+ "units": "in",
397
+ "canonicalAlliance": "RED",
398
+ "mirror": "pointSymmetry",
399
+ "view": { "audienceAt": "bottom" }
400
+ },
401
+ "sizeIn": { "xIn": 144, "yIn": 144 },
402
+ "image": {
403
+ "src": "app:fields/biobuzz/biobuzz-dark.webp",
404
+ "credit": "Field image by Team Juice 16236",
405
+ "pxBoundsIn": "fullBleed",
406
+ "rotationDeg": 90,
407
+ "variants": [{ "name": "dark", "src": "app:fields/biobuzz/biobuzz-dark.webp" }, { "name": "black", "src": "app:fields/biobuzz/biobuzz-black.webp" }, { "name": "light", "src": "app:fields/biobuzz/biobuzz-light.webp" }],
408
+ "provenance": "SET BY HAND 2026-09-22: full bleed (1080 px spans 144 in, grid every 180 px = one tile) and a 90 degree clockwise turn, checked against the RED loading zone, the four flowers and hiveRed (apps/web/public/fields/biobuzz/SOURCE.md)"
409
+ },
410
+ "periods": { "autoS": 30, "teleopS": 120 },
411
+ "obstacles": [
412
+ {
413
+ "id": "hiveRedPivotBar",
414
+ "kind": "box",
415
+ "minXIn": -22.75,
416
+ "maxXIn": -2.75,
417
+ "minYIn": -18.58,
418
+ "maxYIn": 18.58,
419
+ "minZIn": 25.5,
420
+ "maxZIn": 65.6,
421
+ "solidToRobot": true,
422
+ "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."
423
+ }
424
+ ],
425
+ "zones": [
426
+ {
427
+ "id": "loadingZoneRed",
428
+ "kind": "rect",
429
+ "minXIn": -72,
430
+ "maxXIn": -61,
431
+ "minYIn": 24,
432
+ "maxYIn": 48,
433
+ "rule": "G304: no start here",
434
+ "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."
435
+ }
436
+ ],
437
+ "elements": [
438
+ {
439
+ "id": "gardenRed0",
440
+ "kind": "pollen",
441
+ "container": null,
442
+ "xIn": -70.6,
443
+ "yIn": -70.6,
444
+ "radiusIn": 1.4,
445
+ "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)."
446
+ },
447
+ {
448
+ "id": "flower0",
449
+ "kind": "container",
450
+ "container": "flower",
451
+ "xIn": -69.46,
452
+ "yIn": -24,
453
+ "holds": ["pollen", "pollen", "pollen", "pollen"],
454
+ "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).",
455
+ "retrieval": { "side": "field", "openingIn": { "widthIn": 6, "heightIn": 3.55, "depthIn": 3.57 }, "takes": ["pollen"] }
456
+ }
457
+ ],
458
+ "targets": [
459
+ {
460
+ "id": "hiveRedUpCell",
461
+ "kind": "cell",
462
+ "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.",
463
+ "hive": "hiveRed",
464
+ "aimZIn": 59.5,
465
+ "legalApproach": { "outboardOfOuterFaceByIn": 6, "alongAxis": "y" }
466
+ }
467
+ ],
468
+ "startRules": { "touchingWall": true, "ownHalf": true, "notInZones": ["loadingZoneRed"], "notTouchingContainers": ["flower"], "holds": { "pollen": 4 } },
469
+ "rules": { "plugin": "season-biobuzz", "tipTable": [8, 7, 6, 3, 1, 0], "capacity": 4 }
470
+ }
471
+ ```
472
+
473
+ !!! note
474
+ The example is formatted for reading. Canonical form writes each object's keys one per line.
475
+
476
+ | key | meaning |
477
+ |---|---|
478
+ | `season`, `name` | the season id and its display name |
479
+ | `frame` | the [field frame](#the-field-frame). `mirror` is `pointSymmetry`, `mirrorX`, `mirrorY` or `none`. `view.audienceAt` is where the editor draws the audience by default. |
480
+ | `sizeIn` | the field's size |
481
+ | `image` | optional picture drawn under the geometry; see [field image](#field-image) |
482
+ | `periods` | `autoS` and `teleopS`; [`TIME_BUDGET`](checks-and-findings.md#time_budget) reads `autoS` |
483
+ | `obstacles` | axis-aligned boxes with a z range. `solidToRobot: true` makes [`STRUCTURE`](checks-and-findings.md#structure) check them. |
484
+ | `zones` | rectangles a rule talks about. `appliesInAuto: true` makes [`KEEPOUT`](checks-and-findings.md#keepout) check them. |
485
+ | `elements` | game pieces and containers. The season plugin reads extra keys it put there. |
486
+ | `targets` | what the robot aims at. The season plugin reads extra keys it put there. |
487
+ | `startRules` | what [`START_ILLEGAL`](checks-and-findings.md#start_illegal) checks, and what the robot holds at the start |
488
+ | `rules.plugin` | the season plugin that tracks game state; see [Seasons](seasons.md) |
489
+
490
+ ### Field image
491
+
492
+ An optional picture, added in field format version 2. It is only a picture: no check reads it, and
493
+ the vector geometry stays the truth.
494
+
495
+ | key | meaning |
496
+ |---|---|
497
+ | `src` | the picture, as a path relative to the field file or `app:<path>` for an image the app ships |
498
+ | `credit` | who made the image; shown wherever the image is |
499
+ | `pxBoundsIn` | `"fullBleed"` when the field wall is the image's edge, or `{ "left", "top", "right", "bottom" }` in image pixels |
500
+ | `rotationDeg` | `0`, `90`, `180` or `270`, clockwise as seen on screen. `0` when left out. |
501
+ | `variants` | optional other looks of the same picture, each `{ "name", "src" }` |
502
+ | `provenance` | optional: where the pixel box came from |
503
+
504
+ The BIOBUZZ field images are by Team Juice 16236.
505
+
506
+ ## `waypoints.json`
507
+
508
+ Named poses shared by every auto. An auto points at one with `{ "ref": "scoreSouth" }`. Change the
509
+ waypoint and every auto that uses it changes too, so you measure a pose on the field once.
510
+
511
+ ```json
512
+ {
513
+ "$schema": "https://libraries.horizon36596.org/zenith/schema/v1/waypoints.json",
514
+ "formatVersion": 1,
515
+ "waypoints": {
516
+ "start": { "xIn": -12, "yIn": -63, "headingRad": 1.5708, "provenance": "PLACEHOLDER: starter example, against the south wall in RED's half, facing the hive; measure your own start tile" },
517
+ "scoreSouth": { "xIn": -12, "yIn": -36, "headingRad": 1.5708, "provenance": "PLACEHOLDER: starter example, south of the RED hive, outside the legal approach margin, facing the up cell" },
518
+ "gardenApproach": { "xIn": -61, "yIn": -44, "headingRad": -1.5708, "provenance": "PLACEHOLDER: starter example, above the RED garden row, intake facing the south wall" },
519
+ "gardenPickup": { "xIn": -61, "yIn": -62, "headingRad": -1.5708, "provenance": "PLACEHOLDER: starter example, the intake over the RED garden row, frame 1 in off the south wall" },
520
+ "park": { "xIn": -40, "yIn": -36, "headingRad": 3.1416, "provenance": "PLACEHOLDER: starter example, an open spot in RED's half to finish in" }
521
+ }
522
+ }
523
+ ```
524
+
525
+ Each waypoint needs `xIn`, `yIn` and `headingRad`. `provenance` is optional but recommended. Poses
526
+ are for the RED alliance.
527
+
528
+ ## `*.auto.json`
529
+
530
+ One routine. The file name is `<name>.auto.json`. This is the starter example's
531
+ `first-auto.auto.json`; `collect-and-score.auto.json` beside it adds a curve, a marker, a parallel
532
+ group and a wait until a condition, and [Step kinds](step-kinds.md) shows each of those.
533
+
534
+ ```json
535
+ {
536
+ "$schema": "https://libraries.horizon36596.org/zenith/schema/v1/auto.json",
537
+ "formatVersion": 3,
538
+ "name": "first-auto",
539
+ "title": "First auto",
540
+ "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.",
541
+ "robot": "autos/robot.json",
542
+ "field": "autos/field/biobuzz.field.json",
543
+ "alliance": "RED",
544
+ "authors": ["Horizon (FTC 36596)"],
545
+ "created": "2026-09-23",
546
+ "start": { "pose": { "ref": "start" }, "holds": { "pollen": 4 } },
547
+ "steps": [
548
+ {
549
+ "id": "driveOut",
550
+ "kind": "path",
551
+ "segments": [
552
+ { "kind": "line", "from": { "ref": "start" }, "to": { "ref": "scoreSouth" } }
553
+ ],
554
+ "heading": { "mode": "tangent" },
555
+ "notes": "A straight line north to the scoring spot. Tangent heading keeps the front of the robot pointing the way it drives."
556
+ },
557
+ {
558
+ "id": "scorePreload",
559
+ "kind": "command",
560
+ "name": "score",
561
+ "args": { "count": 4 },
562
+ "notes": "Launches the four preloaded pollen. score is stationary in robot.json, so it runs as its own step while the robot stands still."
563
+ },
564
+ {
565
+ "id": "park",
566
+ "kind": "path",
567
+ "segments": [
568
+ { "kind": "line", "from": "current", "to": { "ref": "park" } }
569
+ ],
570
+ "heading": { "mode": "tangent" },
571
+ "notes": "from current starts where the last step ended, so the park leg follows on from wherever the robot scored."
572
+ }
573
+ ]
574
+ }
575
+ ```
576
+
577
+ ### Top-level keys
578
+
579
+ | key | required | meaning |
580
+ |---|---|---|
581
+ | `name` | yes | the routine's id; matches the file name |
582
+ | `title`, `description` | no | for people; shown in the editor |
583
+ | `robot`, `field` | no | paths to the robot and field files, when they differ from `zenith.json` |
584
+ | `alliance` | yes | `RED` or `BLUE`: which alliance the file's poses are written for |
585
+ | `authors`, `created` | no | who wrote it and when |
586
+ | `start` | yes | `pose` (a waypoint `ref` or an inline pose) and `holds`, what the robot carries at the start |
587
+ | `steps` | yes | the steps, at least one; see [Step kinds](step-kinds.md) |
588
+
589
+ ### Segments
590
+
591
+ | kind | fields | notes |
592
+ |---|---|---|
593
+ | `line` | `from`, `to` | a straight line |
594
+ | `bezier` | `from`, `control[]`, `to` | a curve with one to three control points |
595
+
596
+ `from` and `to` are an inline pose, a waypoint `{ "ref": name }`, or the string `"current"`, the pose
597
+ the previous step ended at. The runtime reads `"current"` from the follower when the step starts;
598
+ the planner takes it from the previous step's end. Segments in one path must meet within 0.5 in.
599
+
600
+ ### Heading modes
601
+
602
+ | mode | editor label | Pedro call | fields | the robot faces | use it when |
603
+ |---|---|---|---|---|---|
604
+ | `tangent` | Tangent | `path.tangent()` | | along the direction of travel | the default on mecanum: nose first, front intake leading |
605
+ | `tangentReversed` | Reverse tangent | `path.reverseTangent()` | | opposite the direction of travel | a back mechanism should lead |
606
+ | `constant` | Constant | `path.constant(heading)` | `headingRad` | one fixed heading | a short, deliberate strafe, such as a sweep along a wall |
607
+ | `linear` | Linear | `path.linear(start, end)` | `fromRad`, `toRad` | turning steadily from one heading to the other | the robot must turn during the leg |
608
+ | `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 |
609
+ | `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 |
610
+
611
+ The editor labels are Pedro Pathing's own names for these heading interpolators, checked against the
612
+ Pedro build the robot runtime compiles against. The file keeps its own `mode` names.
613
+
614
+ For `linear`, the sweep is shared over the path's segments by length, and each segment's share turns
615
+ the short way round, as Pedro does. A share over half a turn goes the other way and raises the
616
+ [`HEADING`](checks-and-findings.md#heading) warning. The robot runtime ignores `facePoint`'s
617
+ `offsetRad` for now.
618
+
619
+ #### Piecewise heading
620
+
621
+ A `piecewise` heading splits the path into ranges of `t`, the fraction of the path's length from 0
622
+ at the start to 1 at the end, and gives each range its own mode:
623
+
624
+ ```json
625
+ "heading": {
626
+ "mode": "piecewise",
627
+ "ranges": [
628
+ { "startT": 0, "endT": 0.4, "heading": { "mode": "constant", "headingRad": 1.5708 } },
629
+ { "startT": 0.4, "endT": 1, "heading": { "mode": "linear", "fromRad": 1.5708, "toRad": 3.1416 } }
630
+ ]
631
+ }
632
+ ```
633
+
634
+ | key | meaning |
635
+ |---|---|
636
+ | `ranges[]` | at least one range, in order along the path |
637
+ | `startT`, `endT` | where the range starts and ends, from 0 to 1; `t` has no unit and is not rounded |
638
+ | `heading` | the range's own mode: `tangent`, `tangentReversed`, `constant`, `linear` or `facePoint`, with that mode's fields; never `piecewise` |
639
+
640
+ - The ranges must cover the whole path: the first starts at 0, each starts where the one before it
641
+ ends, and the last ends at 1. A gap, an overlap or a range that does not end after it starts is a
642
+ [`HEADING_RANGES`](checks-and-findings.md#heading_ranges) error, because Pedro stops the auto on
643
+ the robot when the ranges do not cover the path.
644
+ - A point on a boundary belongs to the range that ends there, as it does in Pedro.
645
+ - A `linear` range turns from `fromRad` at its start to `toRad` at its end, the short way round, over
646
+ that range alone. A range over half a turn raises the `HEADING` warning. `tangent`,
647
+ `tangentReversed` and `facePoint` read the path wherever the range is.
648
+ - The robot runtime builds Pedro's `Interpolator.piecewise()` for each segment of the path, from the
649
+ ranges that cross it.
650
+
651
+ ### Markers
652
+
653
+ A marker fires a command part way along a path without stopping it. `at` is one of:
654
+
655
+ - `{ "t": 0.5 }`: a fraction of the whole path's length, from 0 to 1;
656
+ - `{ "distanceIn": 12 }`: inches from the start;
657
+ - `{ "distanceFromEndIn": 6 }`: inches before the end.
658
+
659
+ `command` is `{ "name": ..., "args": ... }`, like a command step. The runtime fires markers on
660
+ distance travelled, so the editor and the robot agree on where they happen.
661
+
662
+ ### `expect`
663
+
664
+ Optional notes for the planner's ledger, on a `path` or `command` step. The robot ignores them.
665
+
666
+ | key | meaning |
667
+ |---|---|
668
+ | `collectFrom`, `count` | this step collects `count` pieces from the named container |
669
+ | `launchesInto` | this step launches into the named target |
670
+ | `tip` | `own` or `opponent`: this step tips a hive |
671
+
672
+ The ledger uses them to track what the robot holds and the season state after each step.