@horizon36596/zenith-core 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.
- package/LICENSE +21 -0
- package/NOTICE +18 -0
- package/README.md +16 -0
- package/dist/canonicalize.d.ts +25 -0
- package/dist/canonicalize.d.ts.map +1 -0
- package/dist/canonicalize.js +199 -0
- package/dist/canonicalize.js.map +1 -0
- package/dist/check.d.ts +26 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +231 -0
- package/dist/check.js.map +1 -0
- package/dist/checks/heading.d.ts +17 -0
- package/dist/checks/heading.d.ts.map +1 -0
- package/dist/checks/heading.js +78 -0
- package/dist/checks/heading.js.map +1 -0
- package/dist/checks/intake.d.ts +28 -0
- package/dist/checks/intake.d.ts.map +1 -0
- package/dist/checks/intake.js +131 -0
- package/dist/checks/intake.js.map +1 -0
- package/dist/checks/motion.d.ts +28 -0
- package/dist/checks/motion.d.ts.map +1 -0
- package/dist/checks/motion.js +252 -0
- package/dist/checks/motion.js.map +1 -0
- package/dist/checks/schedule.d.ts +29 -0
- package/dist/checks/schedule.d.ts.map +1 -0
- package/dist/checks/schedule.js +166 -0
- package/dist/checks/schedule.js.map +1 -0
- package/dist/checks/season.d.ts +6 -0
- package/dist/checks/season.d.ts.map +1 -0
- package/dist/checks/season.js +126 -0
- package/dist/checks/season.js.map +1 -0
- package/dist/continuity.d.ts +38 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +69 -0
- package/dist/continuity.js.map +1 -0
- package/dist/diff.d.ts +37 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +258 -0
- package/dist/diff.js.map +1 -0
- package/dist/edit/command.d.ts +4 -0
- package/dist/edit/command.d.ts.map +1 -0
- package/dist/edit/command.js +16 -0
- package/dist/edit/command.js.map +1 -0
- package/dist/edit/errors.d.ts +9 -0
- package/dist/edit/errors.d.ts.map +1 -0
- package/dist/edit/errors.js +12 -0
- package/dist/edit/errors.js.map +1 -0
- package/dist/edit/fields.d.ts +40 -0
- package/dist/edit/fields.d.ts.map +1 -0
- package/dist/edit/fields.js +150 -0
- package/dist/edit/fields.js.map +1 -0
- package/dist/edit/finish.d.ts +15 -0
- package/dist/edit/finish.d.ts.map +1 -0
- package/dist/edit/finish.js +36 -0
- package/dist/edit/finish.js.map +1 -0
- package/dist/edit/groups.d.ts +66 -0
- package/dist/edit/groups.d.ts.map +1 -0
- package/dist/edit/groups.js +277 -0
- package/dist/edit/groups.js.map +1 -0
- package/dist/edit/ids.d.ts +39 -0
- package/dist/edit/ids.d.ts.map +1 -0
- package/dist/edit/ids.js +76 -0
- package/dist/edit/ids.js.map +1 -0
- package/dist/edit/index.d.ts +22 -0
- package/dist/edit/index.d.ts.map +1 -0
- package/dist/edit/index.js +22 -0
- package/dist/edit/index.js.map +1 -0
- package/dist/edit/meta.d.ts +15 -0
- package/dist/edit/meta.d.ts.map +1 -0
- package/dist/edit/meta.js +13 -0
- package/dist/edit/meta.js.map +1 -0
- package/dist/edit/path.d.ts +36 -0
- package/dist/edit/path.d.ts.map +1 -0
- package/dist/edit/path.js +135 -0
- package/dist/edit/path.js.map +1 -0
- package/dist/edit/steps.d.ts +53 -0
- package/dist/edit/steps.d.ts.map +1 -0
- package/dist/edit/steps.js +0 -0
- package/dist/edit/steps.js.map +1 -0
- package/dist/edit/template.d.ts +18 -0
- package/dist/edit/template.d.ts.map +1 -0
- package/dist/edit/template.js +30 -0
- package/dist/edit/template.js.map +1 -0
- package/dist/edit/timing.d.ts +11 -0
- package/dist/edit/timing.d.ts.map +1 -0
- package/dist/edit/timing.js +23 -0
- package/dist/edit/timing.js.map +1 -0
- package/dist/edit/tree.d.ts +32 -0
- package/dist/edit/tree.d.ts.map +1 -0
- package/dist/edit/tree.js +90 -0
- package/dist/edit/tree.js.map +1 -0
- package/dist/estimate.d.ts +48 -0
- package/dist/estimate.d.ts.map +1 -0
- package/dist/estimate.js +443 -0
- package/dist/estimate.js.map +1 -0
- package/dist/findingHelp.d.ts +18 -0
- package/dist/findingHelp.d.ts.map +1 -0
- package/dist/findingHelp.js +114 -0
- package/dist/findingHelp.js.map +1 -0
- package/dist/fix.d.ts +21 -0
- package/dist/fix.d.ts.map +1 -0
- package/dist/fix.js +180 -0
- package/dist/fix.js.map +1 -0
- package/dist/footprint.d.ts +33 -0
- package/dist/footprint.d.ts.map +1 -0
- package/dist/footprint.js +71 -0
- package/dist/footprint.js.map +1 -0
- package/dist/geometry/angle.d.ts +13 -0
- package/dist/geometry/angle.d.ts.map +1 -0
- package/dist/geometry/angle.js +21 -0
- package/dist/geometry/angle.js.map +1 -0
- package/dist/geometry/curve.d.ts +24 -0
- package/dist/geometry/curve.d.ts.map +1 -0
- package/dist/geometry/curve.js +78 -0
- package/dist/geometry/curve.js.map +1 -0
- package/dist/geometry/path.d.ts +29 -0
- package/dist/geometry/path.d.ts.map +1 -0
- package/dist/geometry/path.js +61 -0
- package/dist/geometry/path.js.map +1 -0
- package/dist/geometry/quadrature.d.ts +13 -0
- package/dist/geometry/quadrature.d.ts.map +1 -0
- package/dist/geometry/quadrature.js +57 -0
- package/dist/geometry/quadrature.js.map +1 -0
- package/dist/geometry/sat.d.ts +40 -0
- package/dist/geometry/sat.d.ts.map +1 -0
- package/dist/geometry/sat.js +86 -0
- package/dist/geometry/sat.js.map +1 -0
- package/dist/geometry/vec.d.ts +27 -0
- package/dist/geometry/vec.d.ts.map +1 -0
- package/dist/geometry/vec.js +30 -0
- package/dist/geometry/vec.js.map +1 -0
- package/dist/heading.d.ts +77 -0
- package/dist/heading.d.ts.map +1 -0
- package/dist/heading.js +164 -0
- package/dist/heading.js.map +1 -0
- package/dist/headingRanges.d.ts +48 -0
- package/dist/headingRanges.d.ts.map +1 -0
- package/dist/headingRanges.js +132 -0
- package/dist/headingRanges.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/kinematics.d.ts +63 -0
- package/dist/kinematics.d.ts.map +1 -0
- package/dist/kinematics.js +71 -0
- package/dist/kinematics.js.map +1 -0
- package/dist/ledger.d.ts +49 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +136 -0
- package/dist/ledger.js.map +1 -0
- package/dist/load.d.ts +12 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +12 -0
- package/dist/load.js.map +1 -0
- package/dist/mirror.d.ts +81 -0
- package/dist/mirror.d.ts.map +1 -0
- package/dist/mirror.js +282 -0
- package/dist/mirror.js.map +1 -0
- package/dist/plan.d.ts +29 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +159 -0
- package/dist/plan.js.map +1 -0
- package/dist/prBody.d.ts +24 -0
- package/dist/prBody.d.ts.map +1 -0
- package/dist/prBody.js +112 -0
- package/dist/prBody.js.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +46 -0
- package/dist/provenance.js.map +1 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +464 -0
- package/dist/render.js.map +1 -0
- package/dist/resolve.d.ts +39 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +172 -0
- package/dist/resolve.js.map +1 -0
- package/dist/season.d.ts +65 -0
- package/dist/season.d.ts.map +1 -0
- package/dist/season.js +16 -0
- package/dist/season.js.map +1 -0
- package/dist/sim/commands.d.ts +97 -0
- package/dist/sim/commands.d.ts.map +1 -0
- package/dist/sim/commands.js +185 -0
- package/dist/sim/commands.js.map +1 -0
- package/dist/sim/curves.d.ts +37 -0
- package/dist/sim/curves.d.ts.map +1 -0
- package/dist/sim/curves.js +187 -0
- package/dist/sim/curves.js.map +1 -0
- package/dist/sim/follower.d.ts +124 -0
- package/dist/sim/follower.d.ts.map +1 -0
- package/dist/sim/follower.js +472 -0
- package/dist/sim/follower.js.map +1 -0
- package/dist/sim/ideal.d.ts +103 -0
- package/dist/sim/ideal.d.ts.map +1 -0
- package/dist/sim/ideal.js +393 -0
- package/dist/sim/ideal.js.map +1 -0
- package/dist/sim/index.d.ts +11 -0
- package/dist/sim/index.d.ts.map +1 -0
- package/dist/sim/index.js +10 -0
- package/dist/sim/index.js.map +1 -0
- package/dist/sim/params.d.ts +91 -0
- package/dist/sim/params.d.ts.map +1 -0
- package/dist/sim/params.js +195 -0
- package/dist/sim/params.js.map +1 -0
- package/dist/sim/pedroPath.d.ts +41 -0
- package/dist/sim/pedroPath.d.ts.map +1 -0
- package/dist/sim/pedroPath.js +191 -0
- package/dist/sim/pedroPath.js.map +1 -0
- package/dist/sim/plant.d.ts +59 -0
- package/dist/sim/plant.d.ts.map +1 -0
- package/dist/sim/plant.js +128 -0
- package/dist/sim/plant.js.map +1 -0
- package/dist/sim/residuals.d.ts +36 -0
- package/dist/sim/residuals.d.ts.map +1 -0
- package/dist/sim/residuals.js +86 -0
- package/dist/sim/residuals.js.map +1 -0
- package/dist/sim/simulate.d.ts +27 -0
- package/dist/sim/simulate.d.ts.map +1 -0
- package/dist/sim/simulate.js +924 -0
- package/dist/sim/simulate.js.map +1 -0
- package/dist/sim/types.d.ts +209 -0
- package/dist/sim/types.d.ts.map +1 -0
- package/dist/sim/types.js +2 -0
- package/dist/sim/types.js.map +1 -0
- package/dist/tokens.d.ts +93 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +93 -0
- package/dist/tokens.js.map +1 -0
- package/dist/trace.d.ts +89 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +187 -0
- package/dist/trace.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unimplemented.d.ts +8 -0
- package/dist/unimplemented.d.ts.map +1 -0
- package/dist/unimplemented.js +11 -0
- package/dist/unimplemented.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import { commandSeconds } from "../estimate.js";
|
|
2
|
+
import { robotLimits } from "../kinematics.js";
|
|
3
|
+
import { ConditionalCommand, DeadlineCommand, InstantCommand, ParallelAllCommand, RaceCommand, SequenceCommand, TimedCommand, } from "./commands.js";
|
|
4
|
+
import { copyFollowerState, PedroFollower } from "./follower.js";
|
|
5
|
+
import { followerParams, plantParams, runParams } from "./params.js";
|
|
6
|
+
import { buildSimPath, normalizeAngle, normalizeSigned } from "./pedroPath.js";
|
|
7
|
+
import { copyPlantState, MecanumPlant } from "./plant.js";
|
|
8
|
+
/**
|
|
9
|
+
* The instant sim (site/docs/simulation.md): the routine run tick by tick through a
|
|
10
|
+
* port of the command tree the robot builds, Pedro's ForesightV3 follower and the Gradle sim's
|
|
11
|
+
* mecanum plant. Pure and deterministic: the same inputs give byte-identical trace JSON, because
|
|
12
|
+
* nothing here reads a clock, draws a random number or iterates anything unordered.
|
|
13
|
+
*
|
|
14
|
+
* One tick, in the order the robot's loop runs it (`CommandScheduler.run` runs subsystem
|
|
15
|
+
* `periodic()` first, and biobuzz's `Drive.periodic()` is where the follower updates):
|
|
16
|
+
*
|
|
17
|
+
* 1. The follower reads the localizer and computes its powers; the drivetrain writes them.
|
|
18
|
+
* 2. The command tree executes: steps finish, the next ones start, paths are handed to the follower.
|
|
19
|
+
* 3. The pose at the start of the tick is recorded, stamped with the tick's time, which is what the
|
|
20
|
+
* Gradle harness records too.
|
|
21
|
+
* 4. The plant advances one tick.
|
|
22
|
+
*/
|
|
23
|
+
/** Pedro reports millimetres from the OctoQuad; the Gradle `FakeOctoQuad` rounds to them. */
|
|
24
|
+
const MM_PER_IN = 25.4;
|
|
25
|
+
/** `FakeOctoQuad`'s heading resolution in the robot repository: 1/5000 rad. */
|
|
26
|
+
const HEADING_STEPS_PER_RAD = 5000;
|
|
27
|
+
const round4 = (value) => {
|
|
28
|
+
const rounded = Math.round(value * 1e4) / 1e4;
|
|
29
|
+
return rounded === 0 ? 0 : rounded;
|
|
30
|
+
};
|
|
31
|
+
const roundTime = (value) => Math.round(value * 1e6) / 1e6;
|
|
32
|
+
const sumHolds = (holds) => {
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const key of Object.keys(holds).sort())
|
|
35
|
+
total += holds[key];
|
|
36
|
+
return total;
|
|
37
|
+
};
|
|
38
|
+
class Engine {
|
|
39
|
+
ctx;
|
|
40
|
+
plan;
|
|
41
|
+
tick = 0;
|
|
42
|
+
tickS;
|
|
43
|
+
fidelity;
|
|
44
|
+
plant;
|
|
45
|
+
follower;
|
|
46
|
+
/** Bumped every time a command gives the follower a new job, so a settle watch can tell. */
|
|
47
|
+
generation = 0;
|
|
48
|
+
pendingSettle = null;
|
|
49
|
+
records = [];
|
|
50
|
+
events = [];
|
|
51
|
+
poses = [];
|
|
52
|
+
truthPoses = [];
|
|
53
|
+
velocities = [];
|
|
54
|
+
ledgerRows = [];
|
|
55
|
+
checkpoints = [];
|
|
56
|
+
/** Filled by `TopSequence` before step i starts; null entries are not recorded. */
|
|
57
|
+
prefixKeys = [];
|
|
58
|
+
/** Checkpoints at or below this index are already known and not taken again. */
|
|
59
|
+
skipCheckpointsThrough = 0;
|
|
60
|
+
constructor(ctx, plan, tickS, fidelity, plant, follower) {
|
|
61
|
+
this.ctx = ctx;
|
|
62
|
+
this.plan = plan;
|
|
63
|
+
this.tickS = tickS;
|
|
64
|
+
this.fidelity = fidelity;
|
|
65
|
+
this.plant = plant;
|
|
66
|
+
this.follower = follower;
|
|
67
|
+
}
|
|
68
|
+
get timeS() {
|
|
69
|
+
return roundTime(this.tick * this.tickS);
|
|
70
|
+
}
|
|
71
|
+
/** What the localizer reports: the truth, or the Gradle harness's quantised pose and no velocity. */
|
|
72
|
+
belief() {
|
|
73
|
+
const motion = this.plant.motion();
|
|
74
|
+
if (this.fidelity !== "gradle")
|
|
75
|
+
return motion;
|
|
76
|
+
const st = this.plant.state;
|
|
77
|
+
return {
|
|
78
|
+
x: Math.round(st.x * MM_PER_IN) / MM_PER_IN,
|
|
79
|
+
y: Math.round(st.y * MM_PER_IN) / MM_PER_IN,
|
|
80
|
+
heading: normalizeAngle(Math.round(st.heading * HEADING_STEPS_PER_RAD) / HEADING_STEPS_PER_RAD),
|
|
81
|
+
vx: 0,
|
|
82
|
+
vy: 0,
|
|
83
|
+
omega: 0,
|
|
84
|
+
twistX: 0,
|
|
85
|
+
twistY: 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
follow(path, speedFraction) {
|
|
89
|
+
this.generation += 1;
|
|
90
|
+
this.follower.follow(path, speedFraction);
|
|
91
|
+
}
|
|
92
|
+
holdHere() {
|
|
93
|
+
this.generation += 1;
|
|
94
|
+
const b = this.belief();
|
|
95
|
+
this.follower.hold({ x: b.x, y: b.y, heading: b.heading });
|
|
96
|
+
}
|
|
97
|
+
event(kind, extra) {
|
|
98
|
+
this.events.push({ timeS: this.timeS, kind, ...extra });
|
|
99
|
+
}
|
|
100
|
+
startRecord(step, parentId, stepCtx) {
|
|
101
|
+
const record = {
|
|
102
|
+
id: step.id,
|
|
103
|
+
startS: this.timeS,
|
|
104
|
+
endS: this.timeS,
|
|
105
|
+
interrupted: false,
|
|
106
|
+
kind: step.kind,
|
|
107
|
+
parentId,
|
|
108
|
+
timedOut: false,
|
|
109
|
+
conditionFired: false,
|
|
110
|
+
unknown: stepCtx.unknownReasons.length > 0,
|
|
111
|
+
};
|
|
112
|
+
if (stepCtx.unknownReasons.length > 0)
|
|
113
|
+
record.unknownReason = stepCtx.unknownReasons.join(" ");
|
|
114
|
+
this.records.push(record);
|
|
115
|
+
return record;
|
|
116
|
+
}
|
|
117
|
+
endRecord(record, interrupted) {
|
|
118
|
+
record.endS = this.timeS;
|
|
119
|
+
record.interrupted = interrupted;
|
|
120
|
+
const view = this.ctx.holds.get(record.id);
|
|
121
|
+
const entry = view?.entry;
|
|
122
|
+
if (entry !== undefined && entry.row !== null) {
|
|
123
|
+
const row = { timeS: record.endS };
|
|
124
|
+
if (view?.after !== null && view?.after !== undefined)
|
|
125
|
+
row.held = sumHolds(view.after);
|
|
126
|
+
if (entry.launched > 0)
|
|
127
|
+
row.launches = entry.launched;
|
|
128
|
+
this.ledgerRows.push(row);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Called by the top-level sequence just before step `index` starts. */
|
|
132
|
+
beforeTopLevelStep(index) {
|
|
133
|
+
if (index === 0 || index <= this.skipCheckpointsThrough)
|
|
134
|
+
return;
|
|
135
|
+
const key = this.prefixKeys[index];
|
|
136
|
+
if (key === undefined)
|
|
137
|
+
return;
|
|
138
|
+
const pending = this.pendingSettle;
|
|
139
|
+
const state = {
|
|
140
|
+
tick: this.tick,
|
|
141
|
+
plant: copyPlantState(this.plant.state),
|
|
142
|
+
follower: copyFollowerState(this.follower.state),
|
|
143
|
+
generation: this.generation,
|
|
144
|
+
records: this.records.length,
|
|
145
|
+
events: this.events.length,
|
|
146
|
+
ledger: this.ledgerRows.length,
|
|
147
|
+
pendingSettle: pending === null ? null : this.records.indexOf(pending.record),
|
|
148
|
+
};
|
|
149
|
+
this.checkpoints.push({ stepIndex: index, prefixKey: key, state });
|
|
150
|
+
}
|
|
151
|
+
/** Phase 1: the follower and the drivetrain. */
|
|
152
|
+
driveTick() {
|
|
153
|
+
const powers = this.follower.update(this.belief(), this.timeS);
|
|
154
|
+
if (powers !== null)
|
|
155
|
+
this.plant.drive(powers);
|
|
156
|
+
const pending = this.pendingSettle;
|
|
157
|
+
if (pending !== null) {
|
|
158
|
+
if (pending.generation !== this.generation || this.follower.state.mode !== "HOLD") {
|
|
159
|
+
if (pending.generation !== this.generation || this.follower.state.mode === "IDLE") {
|
|
160
|
+
this.pendingSettle = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (this.follower.endConstraintsMet()) {
|
|
164
|
+
pending.record.settledS = this.timeS;
|
|
165
|
+
this.event("settled", { stepId: pending.record.id });
|
|
166
|
+
this.pendingSettle = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Phase 3: record the pose the tick started from. */
|
|
171
|
+
recordTick() {
|
|
172
|
+
const t = this.timeS;
|
|
173
|
+
const b = this.belief();
|
|
174
|
+
this.poses.push([t, round4(b.x), round4(b.y), round4(normalizeSigned(b.heading))]);
|
|
175
|
+
const m = this.plant.motion();
|
|
176
|
+
if (this.fidelity === "gradle") {
|
|
177
|
+
this.truthPoses.push([t, round4(m.x), round4(m.y), round4(normalizeSigned(m.heading))]);
|
|
178
|
+
}
|
|
179
|
+
this.velocities.push([t, round4(m.vx), round4(m.vy), round4(m.omega)]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// ------------------------------------------------------------------------------------------------
|
|
183
|
+
// Commands that need the engine
|
|
184
|
+
// ------------------------------------------------------------------------------------------------
|
|
185
|
+
/** `AutoStep`: records the step's start and end around its body. */
|
|
186
|
+
class StepCommand {
|
|
187
|
+
engine;
|
|
188
|
+
step;
|
|
189
|
+
parentId;
|
|
190
|
+
stepCtx;
|
|
191
|
+
body;
|
|
192
|
+
constructor(engine, step, parentId, stepCtx, body) {
|
|
193
|
+
this.engine = engine;
|
|
194
|
+
this.step = step;
|
|
195
|
+
this.parentId = parentId;
|
|
196
|
+
this.stepCtx = stepCtx;
|
|
197
|
+
this.body = body;
|
|
198
|
+
}
|
|
199
|
+
initialize() {
|
|
200
|
+
this.stepCtx.record = this.engine.startRecord(this.step, this.parentId, this.stepCtx);
|
|
201
|
+
this.body.initialize();
|
|
202
|
+
}
|
|
203
|
+
execute() {
|
|
204
|
+
this.body.execute();
|
|
205
|
+
}
|
|
206
|
+
isFinished() {
|
|
207
|
+
return this.body.isFinished();
|
|
208
|
+
}
|
|
209
|
+
end(interrupted) {
|
|
210
|
+
this.body.end(interrupted);
|
|
211
|
+
if (this.stepCtx.record !== null)
|
|
212
|
+
this.engine.endRecord(this.stepCtx.record, interrupted);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** The routine's own `SequentialCommandGroup`, which also takes the checkpoints. */
|
|
216
|
+
class TopSequence {
|
|
217
|
+
engine;
|
|
218
|
+
members;
|
|
219
|
+
offset;
|
|
220
|
+
index = 0;
|
|
221
|
+
constructor(engine, members, offset) {
|
|
222
|
+
this.engine = engine;
|
|
223
|
+
this.members = members;
|
|
224
|
+
this.offset = offset;
|
|
225
|
+
}
|
|
226
|
+
start() {
|
|
227
|
+
const member = this.members[this.index];
|
|
228
|
+
if (member === undefined)
|
|
229
|
+
return;
|
|
230
|
+
this.engine.beforeTopLevelStep(this.index + this.offset);
|
|
231
|
+
member.initialize();
|
|
232
|
+
}
|
|
233
|
+
initialize() {
|
|
234
|
+
this.index = 0;
|
|
235
|
+
this.start();
|
|
236
|
+
}
|
|
237
|
+
execute() {
|
|
238
|
+
const current = this.members[this.index];
|
|
239
|
+
if (current === undefined)
|
|
240
|
+
return;
|
|
241
|
+
current.execute();
|
|
242
|
+
if (current.isFinished()) {
|
|
243
|
+
current.end(false);
|
|
244
|
+
this.index += 1;
|
|
245
|
+
this.start();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
isFinished() {
|
|
249
|
+
return this.index >= this.members.length;
|
|
250
|
+
}
|
|
251
|
+
end(interrupted) {
|
|
252
|
+
if (interrupted)
|
|
253
|
+
this.members[this.index]?.end(true);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** `FollowPath`, built when it starts the way the runtime's `DeferredCommand` builds a `current` leg. */
|
|
257
|
+
class FollowCommand {
|
|
258
|
+
engine;
|
|
259
|
+
step;
|
|
260
|
+
stepCtx;
|
|
261
|
+
speedFraction;
|
|
262
|
+
path = null;
|
|
263
|
+
constructor(engine, step, stepCtx, speedFraction) {
|
|
264
|
+
this.engine = engine;
|
|
265
|
+
this.step = step;
|
|
266
|
+
this.stepCtx = stepCtx;
|
|
267
|
+
this.speedFraction = speedFraction;
|
|
268
|
+
}
|
|
269
|
+
initialize() {
|
|
270
|
+
const authored = this.step.step;
|
|
271
|
+
const polygons = (this.step.segments ?? []).map((segment) => segment.pointsIn.map((point) => ({ x: point.xIn, y: point.yIn })));
|
|
272
|
+
const first = polygons[0];
|
|
273
|
+
if (first !== undefined && authored.segments[0]?.from === "current") {
|
|
274
|
+
const b = this.engine.belief();
|
|
275
|
+
first[0] = { x: b.x, y: b.y };
|
|
276
|
+
}
|
|
277
|
+
this.path = buildSimPath(polygons, authored.heading, this.step.startPose.headingRad);
|
|
278
|
+
this.engine.follow(this.path, this.speedFraction);
|
|
279
|
+
}
|
|
280
|
+
execute() { }
|
|
281
|
+
isFinished() {
|
|
282
|
+
return !this.engine.follower.following;
|
|
283
|
+
}
|
|
284
|
+
end(interrupted) {
|
|
285
|
+
if (interrupted) {
|
|
286
|
+
this.engine.holdHere();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const record = this.stepCtx.record;
|
|
290
|
+
if (record !== null && this.engine.follower.holdEnd) {
|
|
291
|
+
this.engine.pendingSettle = { record, generation: this.engine.generation };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** The `WaitUntilCommand` an `endCondition` races the path against, fired by arc length. */
|
|
296
|
+
class ConditionTrigger {
|
|
297
|
+
engine;
|
|
298
|
+
follow;
|
|
299
|
+
fraction;
|
|
300
|
+
stepCtx;
|
|
301
|
+
condition;
|
|
302
|
+
fired = false;
|
|
303
|
+
constructor(engine, follow, fraction, stepCtx, condition) {
|
|
304
|
+
this.engine = engine;
|
|
305
|
+
this.follow = follow;
|
|
306
|
+
this.fraction = fraction;
|
|
307
|
+
this.stepCtx = stepCtx;
|
|
308
|
+
this.condition = condition;
|
|
309
|
+
}
|
|
310
|
+
initialize() {
|
|
311
|
+
this.fired = false;
|
|
312
|
+
}
|
|
313
|
+
execute() { }
|
|
314
|
+
isFinished() {
|
|
315
|
+
if (this.fired)
|
|
316
|
+
return true;
|
|
317
|
+
const path = this.follow.path;
|
|
318
|
+
if (this.fraction === null || path === null || !this.engine.follower.following)
|
|
319
|
+
return false;
|
|
320
|
+
if (this.engine.follower.distanceAlongIn() + 1e-9 >= this.fraction * path.runtimeTotalIn) {
|
|
321
|
+
this.fired = true;
|
|
322
|
+
if (this.stepCtx.record !== null)
|
|
323
|
+
this.stepCtx.record.conditionFired = true;
|
|
324
|
+
this.engine.event("conditionFired", {
|
|
325
|
+
...(this.stepCtx.record === null ? {} : { stepId: this.stepCtx.record.id }),
|
|
326
|
+
detail: this.condition,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return this.fired;
|
|
330
|
+
}
|
|
331
|
+
end() { }
|
|
332
|
+
}
|
|
333
|
+
/** A `WaitUntilCommand` on a `wait` step: the resolved seconds, or never. */
|
|
334
|
+
class WaitUntilCommand extends TimedCommand {
|
|
335
|
+
engine;
|
|
336
|
+
stepCtx;
|
|
337
|
+
condition;
|
|
338
|
+
announced = false;
|
|
339
|
+
constructor(engine, seconds, stepCtx, condition) {
|
|
340
|
+
super(engine, seconds);
|
|
341
|
+
this.engine = engine;
|
|
342
|
+
this.stepCtx = stepCtx;
|
|
343
|
+
this.condition = condition;
|
|
344
|
+
}
|
|
345
|
+
initialize() {
|
|
346
|
+
this.announced = false;
|
|
347
|
+
super.initialize();
|
|
348
|
+
}
|
|
349
|
+
isFinished() {
|
|
350
|
+
const finished = super.isFinished();
|
|
351
|
+
if (finished && !this.announced) {
|
|
352
|
+
this.announced = true;
|
|
353
|
+
if (this.stepCtx.record !== null)
|
|
354
|
+
this.stepCtx.record.conditionFired = true;
|
|
355
|
+
this.engine.event("conditionFired", {
|
|
356
|
+
...(this.stepCtx.record === null ? {} : { stepId: this.stepCtx.record.id }),
|
|
357
|
+
detail: this.condition,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return finished;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** `RobotTimeout`: a race against `WaitRobotTime`, flagging the step when the clock wins. */
|
|
364
|
+
class TimeoutRace extends RaceCommand {
|
|
365
|
+
engine;
|
|
366
|
+
body;
|
|
367
|
+
timer;
|
|
368
|
+
stepCtx;
|
|
369
|
+
constructor(engine, body, timer, stepCtx) {
|
|
370
|
+
super([body, timer]);
|
|
371
|
+
this.engine = engine;
|
|
372
|
+
this.body = body;
|
|
373
|
+
this.timer = timer;
|
|
374
|
+
this.stepCtx = stepCtx;
|
|
375
|
+
}
|
|
376
|
+
end() {
|
|
377
|
+
const timedOut = this.timer.isFinished() && !this.body.isFinished();
|
|
378
|
+
super.end();
|
|
379
|
+
if (timedOut && this.stepCtx.record !== null) {
|
|
380
|
+
this.stepCtx.record.timedOut = true;
|
|
381
|
+
this.engine.event("timedOut", { stepId: this.stepCtx.record.id });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/** `PathMarkers.Runner`: fires each marker's command once the follower has come far enough. */
|
|
386
|
+
class MarkerRunner {
|
|
387
|
+
engine;
|
|
388
|
+
follow;
|
|
389
|
+
markers;
|
|
390
|
+
stepCtx;
|
|
391
|
+
fired = [];
|
|
392
|
+
sorted = [];
|
|
393
|
+
running = [];
|
|
394
|
+
furthestIn = 0;
|
|
395
|
+
seenFollowing = false;
|
|
396
|
+
constructor(engine, follow, markers, stepCtx) {
|
|
397
|
+
this.engine = engine;
|
|
398
|
+
this.follow = follow;
|
|
399
|
+
this.markers = markers;
|
|
400
|
+
this.stepCtx = stepCtx;
|
|
401
|
+
}
|
|
402
|
+
total() {
|
|
403
|
+
return this.follow.path?.runtimeTotalIn ?? 0;
|
|
404
|
+
}
|
|
405
|
+
initialize() {
|
|
406
|
+
const total = this.total();
|
|
407
|
+
const withDistance = this.markers.map((spec) => ({ distanceIn: spec.distanceOf(total), spec }));
|
|
408
|
+
// A stable insertion sort, as the runtime's, so ties fire in file order.
|
|
409
|
+
const sorted = [];
|
|
410
|
+
for (const item of withDistance) {
|
|
411
|
+
let at = sorted.length;
|
|
412
|
+
while (at > 0 && sorted[at - 1].distanceIn > item.distanceIn)
|
|
413
|
+
at -= 1;
|
|
414
|
+
sorted.splice(at, 0, item);
|
|
415
|
+
}
|
|
416
|
+
this.sorted = sorted;
|
|
417
|
+
this.fired = sorted.map(() => false);
|
|
418
|
+
this.running = [];
|
|
419
|
+
this.furthestIn = 0;
|
|
420
|
+
this.seenFollowing = false;
|
|
421
|
+
this.fireDue();
|
|
422
|
+
}
|
|
423
|
+
distanceAlongIn() {
|
|
424
|
+
if (this.engine.follower.following)
|
|
425
|
+
this.seenFollowing = true;
|
|
426
|
+
else if (this.seenFollowing)
|
|
427
|
+
return this.total();
|
|
428
|
+
return this.engine.follower.distanceAlongIn();
|
|
429
|
+
}
|
|
430
|
+
fireDue() {
|
|
431
|
+
for (let i = 0; i < this.sorted.length; i += 1) {
|
|
432
|
+
if (this.fired[i])
|
|
433
|
+
continue;
|
|
434
|
+
const item = this.sorted[i];
|
|
435
|
+
if (this.furthestIn + 1e-9 < item.distanceIn)
|
|
436
|
+
return;
|
|
437
|
+
this.fired[i] = true;
|
|
438
|
+
this.engine.event("markerFired", {
|
|
439
|
+
...(this.stepCtx.record === null ? {} : { stepId: this.stepCtx.record.id }),
|
|
440
|
+
command: item.spec.name,
|
|
441
|
+
});
|
|
442
|
+
const command = new TimedCommand(this.engine, item.spec.seconds);
|
|
443
|
+
command.initialize();
|
|
444
|
+
if (command.isFinished())
|
|
445
|
+
command.end(false);
|
|
446
|
+
else
|
|
447
|
+
this.running.push(command);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
execute() {
|
|
451
|
+
this.furthestIn = Math.max(this.furthestIn, this.distanceAlongIn());
|
|
452
|
+
this.fireDue();
|
|
453
|
+
for (let i = this.running.length - 1; i >= 0; i -= 1) {
|
|
454
|
+
const command = this.running[i];
|
|
455
|
+
command.execute();
|
|
456
|
+
if (command.isFinished()) {
|
|
457
|
+
command.end(false);
|
|
458
|
+
this.running.splice(i, 1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
isFinished() {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
end() {
|
|
466
|
+
for (let i = this.running.length - 1; i >= 0; i -= 1)
|
|
467
|
+
this.running[i].end(true);
|
|
468
|
+
this.running = [];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function conditionClass(ctx, condition) {
|
|
472
|
+
if (ctx.fullConditions.includes(condition))
|
|
473
|
+
return "full";
|
|
474
|
+
if (ctx.emptyConditions.includes(condition))
|
|
475
|
+
return "empty";
|
|
476
|
+
return "unknown";
|
|
477
|
+
}
|
|
478
|
+
/** The declared conditions whose `ledger` key reads `kind`, when the robot file marks any. */
|
|
479
|
+
function markedConditions(robot, kind) {
|
|
480
|
+
const conditions = robot["conditions"];
|
|
481
|
+
if (!Array.isArray(conditions))
|
|
482
|
+
return [];
|
|
483
|
+
const names = [];
|
|
484
|
+
for (const entry of conditions) {
|
|
485
|
+
if (typeof entry !== "object" || entry === null)
|
|
486
|
+
continue;
|
|
487
|
+
const record = entry;
|
|
488
|
+
if (record["ledger"] === kind && typeof record["name"] === "string")
|
|
489
|
+
names.push(record["name"]);
|
|
490
|
+
}
|
|
491
|
+
return names;
|
|
492
|
+
}
|
|
493
|
+
const NOT_SEEN = (condition) => `The instant sim cannot see "${condition}", so it is taken as never reading true.`;
|
|
494
|
+
const NO_LEDGER = (condition) => `"${condition}" depends on what the robot holds, and no ledger with a capacity was given, so it is taken as never reading true.`;
|
|
495
|
+
/** The ledger's view of a full or empty condition against a hold count, or null when it cannot say. */
|
|
496
|
+
function readsTrue(ctx, cls, holds) {
|
|
497
|
+
if (holds === null || cls === "unknown")
|
|
498
|
+
return null;
|
|
499
|
+
const count = sumHolds(holds);
|
|
500
|
+
if (cls === "empty")
|
|
501
|
+
return count === 0;
|
|
502
|
+
const max = ctx.robot.capacity?.max;
|
|
503
|
+
if (max === undefined)
|
|
504
|
+
return null;
|
|
505
|
+
return count >= max;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Where along a path its `endCondition` fires, as a fraction of arc length, or null for never.
|
|
509
|
+
* APPROX: the ledger says how many pieces a step collects or releases, not where; they are taken
|
|
510
|
+
* as evenly spaced, each at the middle of its share of the path.
|
|
511
|
+
*/
|
|
512
|
+
function endConditionFraction(ctx, step, condition, stepCtx) {
|
|
513
|
+
const view = ctx.holds.get(step.id) ?? null;
|
|
514
|
+
const query = {
|
|
515
|
+
kind: "endCondition",
|
|
516
|
+
stepId: step.id,
|
|
517
|
+
condition,
|
|
518
|
+
holdsBefore: view?.before ?? null,
|
|
519
|
+
holdsAfter: view?.after ?? null,
|
|
520
|
+
};
|
|
521
|
+
const custom = ctx.options.conditions?.(query);
|
|
522
|
+
if (custom !== undefined)
|
|
523
|
+
return typeof custom === "number" ? custom : null;
|
|
524
|
+
const cls = conditionClass(ctx, condition);
|
|
525
|
+
if (cls === "unknown") {
|
|
526
|
+
stepCtx.unknownReasons.push(NOT_SEEN(condition));
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const before = view?.before ?? null;
|
|
530
|
+
const after = view?.after ?? null;
|
|
531
|
+
const atStart = readsTrue(ctx, cls, before);
|
|
532
|
+
if (atStart === null || after === null) {
|
|
533
|
+
stepCtx.unknownReasons.push(NO_LEDGER(condition));
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
if (atStart)
|
|
537
|
+
return 0;
|
|
538
|
+
if (readsTrue(ctx, cls, after) !== true)
|
|
539
|
+
return null;
|
|
540
|
+
const h0 = sumHolds(before);
|
|
541
|
+
const h1 = sumHolds(after);
|
|
542
|
+
const pieces = Math.abs(h1 - h0);
|
|
543
|
+
if (pieces === 0)
|
|
544
|
+
return null;
|
|
545
|
+
const needed = cls === "full" ? (ctx.robot.capacity?.max ?? h1) - h0 : h0;
|
|
546
|
+
return Math.max(0, Math.min(1, (needed - 0.5) / pieces));
|
|
547
|
+
}
|
|
548
|
+
/** Seconds after a `wait until` starts that it ends, or null for never. */
|
|
549
|
+
function waitSeconds(ctx, step, condition, stepCtx) {
|
|
550
|
+
const holds = ctx.holds.get(step.id)?.before ?? null;
|
|
551
|
+
const custom = ctx.options.conditions?.({ kind: "wait", stepId: step.id, condition, holds });
|
|
552
|
+
if (custom !== undefined)
|
|
553
|
+
return typeof custom === "number" ? custom : null;
|
|
554
|
+
const cls = conditionClass(ctx, condition);
|
|
555
|
+
if (cls === "unknown") {
|
|
556
|
+
stepCtx.unknownReasons.push(NOT_SEEN(condition));
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const value = readsTrue(ctx, cls, holds);
|
|
560
|
+
if (value === null) {
|
|
561
|
+
stepCtx.unknownReasons.push(NO_LEDGER(condition));
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return value ? 0 : null;
|
|
565
|
+
}
|
|
566
|
+
/** Which side a branch takes. With nothing to go on, the `then` side, and the step says so. */
|
|
567
|
+
function branchSide(ctx, step, condition, stepCtx) {
|
|
568
|
+
const holds = ctx.holds.get(step.id)?.before ?? null;
|
|
569
|
+
const custom = ctx.options.conditions?.({ kind: "branch", stepId: step.id, condition, holds });
|
|
570
|
+
if (custom !== undefined && custom !== null)
|
|
571
|
+
return Boolean(custom);
|
|
572
|
+
const value = readsTrue(ctx, conditionClass(ctx, condition), holds);
|
|
573
|
+
if (value === null) {
|
|
574
|
+
stepCtx.unknownReasons.push(`The instant sim cannot tell how "${condition}" reads, so it takes the then side.`);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
// ------------------------------------------------------------------------------------------------
|
|
580
|
+
// Building the command tree
|
|
581
|
+
// ------------------------------------------------------------------------------------------------
|
|
582
|
+
function commandTiming(ctx, name, args) {
|
|
583
|
+
const spec = ctx.robot.commands.find((command) => command.name === name);
|
|
584
|
+
if (spec === undefined) {
|
|
585
|
+
return { seconds: ctx.unknownCommandS, reason: `"${name}" is not in the command registry.` };
|
|
586
|
+
}
|
|
587
|
+
const seconds = commandSeconds(name, args, ctx.robot);
|
|
588
|
+
if (seconds === null) {
|
|
589
|
+
return {
|
|
590
|
+
seconds: ctx.unknownCommandS,
|
|
591
|
+
reason: `"${name}" has no estimateS, so it is taken as ${String(ctx.unknownCommandS)} s.`,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (spec.movesRobot === true) {
|
|
595
|
+
return { seconds, reason: `"${name}" moves the robot in ways the instant sim does not drive.` };
|
|
596
|
+
}
|
|
597
|
+
return { seconds, reason: null };
|
|
598
|
+
}
|
|
599
|
+
function buildStep(engine, step, parentId) {
|
|
600
|
+
const ctx = engine.ctx;
|
|
601
|
+
const stepCtx = { record: null, unknownReasons: [] };
|
|
602
|
+
const authored = step.step;
|
|
603
|
+
let body;
|
|
604
|
+
switch (authored.kind) {
|
|
605
|
+
case "path": {
|
|
606
|
+
const follow = new FollowCommand(engine, step, stepCtx, authored.speedFraction ?? ctx.defaultSpeedFraction);
|
|
607
|
+
let drive = follow;
|
|
608
|
+
if (authored.endCondition !== undefined) {
|
|
609
|
+
const condition = authored.endCondition.condition;
|
|
610
|
+
const fraction = endConditionFraction(ctx, step, condition, stepCtx);
|
|
611
|
+
drive = new RaceCommand([follow, new ConditionTrigger(engine, follow, fraction, stepCtx, condition)]);
|
|
612
|
+
}
|
|
613
|
+
const markers = authored.markers ?? [];
|
|
614
|
+
if (markers.length > 0) {
|
|
615
|
+
const specs = markers.map((marker) => {
|
|
616
|
+
const at = marker.at;
|
|
617
|
+
const distanceOf = "t" in at
|
|
618
|
+
? (total) => at.t * total
|
|
619
|
+
: "distanceIn" in at
|
|
620
|
+
? () => at.distanceIn
|
|
621
|
+
: (total) => total - at.distanceFromEndIn;
|
|
622
|
+
return {
|
|
623
|
+
distanceOf,
|
|
624
|
+
name: marker.command.name,
|
|
625
|
+
seconds: commandTiming(ctx, marker.command.name, marker.command.args).seconds,
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
drive = new DeadlineCommand(drive, [new MarkerRunner(engine, follow, specs, stepCtx)]);
|
|
629
|
+
}
|
|
630
|
+
body = drive;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "command": {
|
|
634
|
+
const timing = commandTiming(ctx, authored.name, authored.args);
|
|
635
|
+
if (timing.reason !== null)
|
|
636
|
+
stepCtx.unknownReasons.push(timing.reason);
|
|
637
|
+
body = new TimedCommand(engine, timing.seconds);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case "wait": {
|
|
641
|
+
if (authored.until !== undefined) {
|
|
642
|
+
const seconds = waitSeconds(ctx, step, authored.until, stepCtx);
|
|
643
|
+
body = new WaitUntilCommand(engine, seconds, stepCtx, authored.until);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
// `WaitRobotTime(Math.round(seconds * 1000))`: whole milliseconds.
|
|
647
|
+
body = new TimedCommand(engine, Math.round((authored.seconds ?? 0) * 1000) / 1000);
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "sequence": {
|
|
652
|
+
body = new SequenceCommand((step.children ?? []).map((child) => buildStep(engine, child, step.id)));
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case "parallel": {
|
|
656
|
+
const children = step.children ?? [];
|
|
657
|
+
const members = children.map((child) => buildStep(engine, child, step.id));
|
|
658
|
+
if (authored.mode === "all")
|
|
659
|
+
body = new ParallelAllCommand(members);
|
|
660
|
+
else if (authored.mode === "race")
|
|
661
|
+
body = new RaceCommand(members);
|
|
662
|
+
else {
|
|
663
|
+
const at = children.findIndex((child) => child.id === authored.deadline || child.step.id === authored.deadline);
|
|
664
|
+
if (at < 0) {
|
|
665
|
+
stepCtx.unknownReasons.push("Its deadline names none of its own steps, which the runtime refuses; the sim waits for all of them.");
|
|
666
|
+
body = new ParallelAllCommand(members);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
body = new DeadlineCommand(members[at], members.filter((_, index) => index !== at));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case "branch": {
|
|
675
|
+
const children = step.children ?? [];
|
|
676
|
+
const thenCount = authored.then.length;
|
|
677
|
+
const thenSide = new SequenceCommand(children.slice(0, thenCount).map((child) => buildStep(engine, child, step.id)));
|
|
678
|
+
const elseChildren = children.slice(thenCount);
|
|
679
|
+
const elseSide = authored.else === undefined || authored.else.length === 0
|
|
680
|
+
? new InstantCommand()
|
|
681
|
+
: new SequenceCommand(elseChildren.map((child) => buildStep(engine, child, step.id)));
|
|
682
|
+
const condition = authored.condition;
|
|
683
|
+
const side = branchSide(ctx, step, condition, stepCtx);
|
|
684
|
+
body = new ConditionalCommand(() => side, thenSide, elseSide);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if ("timeoutS" in authored && authored.timeoutS !== undefined) {
|
|
689
|
+
const timer = new TimedCommand(engine, Math.round(authored.timeoutS * 1000) / 1000);
|
|
690
|
+
body = new TimeoutRace(engine, body, timer, stepCtx);
|
|
691
|
+
}
|
|
692
|
+
return new StepCommand(engine, step, parentId, stepCtx, body);
|
|
693
|
+
}
|
|
694
|
+
// ------------------------------------------------------------------------------------------------
|
|
695
|
+
// The ledger's holds, by step
|
|
696
|
+
// ------------------------------------------------------------------------------------------------
|
|
697
|
+
function holdsByStep(plan, options) {
|
|
698
|
+
const views = new Map();
|
|
699
|
+
const ledger = options.ledger;
|
|
700
|
+
if (ledger === undefined)
|
|
701
|
+
return views;
|
|
702
|
+
const season = options.season;
|
|
703
|
+
const tracked = season?.holds !== undefined;
|
|
704
|
+
let previous = tracked
|
|
705
|
+
? (season?.holds?.(ledger.initialState) ?? null)
|
|
706
|
+
: (plan.auto.start.holds ?? null);
|
|
707
|
+
for (const entry of ledger.entries) {
|
|
708
|
+
const known = tracked || Object.keys(entry.holds).length > 0;
|
|
709
|
+
const before = tracked && season?.holds !== undefined ? season.holds(entry.before) : previous;
|
|
710
|
+
const after = known ? entry.holds : null;
|
|
711
|
+
views.set(entry.stepId, { before, after, entry });
|
|
712
|
+
previous = after ?? previous;
|
|
713
|
+
}
|
|
714
|
+
return views;
|
|
715
|
+
}
|
|
716
|
+
// ------------------------------------------------------------------------------------------------
|
|
717
|
+
// Keys for re-simulation
|
|
718
|
+
// ------------------------------------------------------------------------------------------------
|
|
719
|
+
/** cyrb53, a small well-mixed 53-bit string hash. Only ever compared for equality. */
|
|
720
|
+
function hash(text) {
|
|
721
|
+
let h1 = 0xdeadbeef;
|
|
722
|
+
let h2 = 0x41c6ce57;
|
|
723
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
724
|
+
const ch = text.charCodeAt(i);
|
|
725
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
726
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
727
|
+
}
|
|
728
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
729
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
730
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
731
|
+
}
|
|
732
|
+
function stepKey(step, holds) {
|
|
733
|
+
const view = holds.get(step.id);
|
|
734
|
+
return JSON.stringify({
|
|
735
|
+
id: step.id,
|
|
736
|
+
step: step.step,
|
|
737
|
+
start: step.startPose,
|
|
738
|
+
segments: step.segments?.map((segment) => segment.pointsIn) ?? null,
|
|
739
|
+
before: view?.before ?? null,
|
|
740
|
+
after: view?.after ?? null,
|
|
741
|
+
children: step.children?.map((child) => stepKey(child, holds)) ?? null,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function prefixKeys(plan, robot, field, options, holds) {
|
|
745
|
+
const { ledger: _ledger, season: _season, conditions: _conditions, ...plain } = options;
|
|
746
|
+
let key = hash(JSON.stringify({ robot, field, options: plain, start: plan.startPose, auto: plan.auto.start }));
|
|
747
|
+
const keys = [key];
|
|
748
|
+
for (const step of plan.steps) {
|
|
749
|
+
key = hash(key + stepKey(step, holds));
|
|
750
|
+
keys.push(key);
|
|
751
|
+
}
|
|
752
|
+
return keys;
|
|
753
|
+
}
|
|
754
|
+
function setup(plan, robot, field, options) {
|
|
755
|
+
const run = runParams(field, options);
|
|
756
|
+
const limits = robotLimits(robot);
|
|
757
|
+
const plant = plantParams(robot, field, run.fidelity, options);
|
|
758
|
+
const follower = new PedroFollower(followerParams(robot, options), plant.maxForwardVelInPerS, plant.maxStrafeVelInPerS, limits.holdEnd);
|
|
759
|
+
const start = plan.startPose;
|
|
760
|
+
const holds = holdsByStep(plan, options);
|
|
761
|
+
const ctx = {
|
|
762
|
+
robot,
|
|
763
|
+
options,
|
|
764
|
+
fidelity: run.fidelity,
|
|
765
|
+
unknownCommandS: options.unknownCommandS ?? 0,
|
|
766
|
+
defaultSpeedFraction: limits.defaultPathSpeedFraction,
|
|
767
|
+
fullConditions: options.fullConditions ??
|
|
768
|
+
(markedConditions(robot, "full").length > 0 ? markedConditions(robot, "full") : ["hopperFull"]),
|
|
769
|
+
emptyConditions: options.emptyConditions ??
|
|
770
|
+
(markedConditions(robot, "empty").length > 0 ? markedConditions(robot, "empty") : ["hopperEmpty"]),
|
|
771
|
+
holds,
|
|
772
|
+
};
|
|
773
|
+
const engine = new Engine(ctx, plan, run.tickS, run.fidelity, new MecanumPlant(plant, start.xIn, start.yIn, start.headingRad), follower);
|
|
774
|
+
engine.prefixKeys = prefixKeys(plan, robot, field, options, holds);
|
|
775
|
+
const members = plan.steps.map((step) => buildStep(engine, step, null));
|
|
776
|
+
return { engine, maxTimeS: run.maxTimeS, periodS: run.periodS, members };
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Runs ticks until the routine finishes or the time runs out. `midTick` resumes a tick that a
|
|
780
|
+
* checkpoint interrupted just after the command tree ran.
|
|
781
|
+
*/
|
|
782
|
+
function runLoop(engine, root, maxTimeS, midTick) {
|
|
783
|
+
let resume = midTick;
|
|
784
|
+
for (;;) {
|
|
785
|
+
if (!resume) {
|
|
786
|
+
engine.driveTick();
|
|
787
|
+
root.execute();
|
|
788
|
+
}
|
|
789
|
+
resume = false;
|
|
790
|
+
const done = root.isFinished();
|
|
791
|
+
if (done)
|
|
792
|
+
root.end(false);
|
|
793
|
+
engine.recordTick();
|
|
794
|
+
if (done)
|
|
795
|
+
return true;
|
|
796
|
+
if ((engine.tick + 1) * engine.tickS > maxTimeS + 1e-9) {
|
|
797
|
+
root.end(true);
|
|
798
|
+
engine.event("periodEnded", { detail: "The routine had not finished when the time ran out." });
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
engine.plant.update(engine.tickS);
|
|
802
|
+
engine.tick += 1;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function finish(engine, plan, completed, periodS) {
|
|
806
|
+
const capabilities = ["steps", "poses", "velocities", "events"];
|
|
807
|
+
if (engine.ctx.options.ledger !== undefined)
|
|
808
|
+
capabilities.push("ledger");
|
|
809
|
+
if (engine.fidelity === "gradle")
|
|
810
|
+
capabilities.push("truthPoses");
|
|
811
|
+
return {
|
|
812
|
+
formatVersion: 1,
|
|
813
|
+
auto: plan.auto.name,
|
|
814
|
+
sha: null,
|
|
815
|
+
simTimeS: engine.timeS,
|
|
816
|
+
tickS: engine.tickS,
|
|
817
|
+
capabilities,
|
|
818
|
+
steps: engine.records,
|
|
819
|
+
poses: engine.poses,
|
|
820
|
+
truthPoses: engine.truthPoses,
|
|
821
|
+
structureContacts: [],
|
|
822
|
+
ledger: engine.ledgerRows,
|
|
823
|
+
events: engine.events,
|
|
824
|
+
source: "instant",
|
|
825
|
+
fidelity: engine.fidelity,
|
|
826
|
+
velocities: engine.velocities,
|
|
827
|
+
completed,
|
|
828
|
+
periodS,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/** Runs the routine from the start and keeps the checkpoints `resimulate` needs. */
|
|
832
|
+
export function simulateRun(plan, robot, field, options = {}) {
|
|
833
|
+
const { engine, maxTimeS, periodS, members } = setup(plan, robot, field, options);
|
|
834
|
+
const root = new TopSequence(engine, members, 0);
|
|
835
|
+
root.initialize();
|
|
836
|
+
const completed = runLoop(engine, root, maxTimeS, false);
|
|
837
|
+
return { trace: finish(engine, plan, completed, periodS), checkpoints: engine.checkpoints, resumedFromStep: 0 };
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* The instant sim: the routine as the robot would run it, tick by tick, as a `Trace` with the
|
|
841
|
+
* sim's additions (`SimTrace`). The default fidelity is `robot`; pass `fidelity: "gradle"` to
|
|
842
|
+
* reproduce the robot repository's headless harness instead.
|
|
843
|
+
*/
|
|
844
|
+
export function simulate(plan, robot, field, options = {}) {
|
|
845
|
+
return simulateRun(plan, robot, field, options).trace;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Re-simulates after an edit, from the latest checkpoint whose every earlier top-level step, and
|
|
849
|
+
* every input, is unchanged. `fromStepIndex`, when given, is the first step the caller knows was
|
|
850
|
+
* edited; the sim never resumes later than it. The result is identical to `simulateRun` on the
|
|
851
|
+
* same inputs, only faster. A `conditions` resolver is not part of the key: pass the same one.
|
|
852
|
+
*/
|
|
853
|
+
export function resimulate(previous, plan, robot, field, options = {}, fromStepIndex) {
|
|
854
|
+
const { engine, maxTimeS, periodS, members } = setup(plan, robot, field, options);
|
|
855
|
+
const limit = Math.min(fromStepIndex ?? plan.steps.length, plan.steps.length - 1);
|
|
856
|
+
let checkpoint = null;
|
|
857
|
+
for (const candidate of previous.checkpoints) {
|
|
858
|
+
if (candidate.stepIndex > limit)
|
|
859
|
+
break;
|
|
860
|
+
if (engine.prefixKeys[candidate.stepIndex] === candidate.prefixKey)
|
|
861
|
+
checkpoint = candidate;
|
|
862
|
+
}
|
|
863
|
+
if (checkpoint === null)
|
|
864
|
+
return simulateRun(plan, robot, field, options);
|
|
865
|
+
const state = checkpoint.state;
|
|
866
|
+
const trace = previous.trace;
|
|
867
|
+
engine.tick = state.tick;
|
|
868
|
+
engine.plant.state = copyPlantState(state.plant);
|
|
869
|
+
engine.follower.state = copyFollowerState(state.follower);
|
|
870
|
+
engine.generation = state.generation;
|
|
871
|
+
engine.records = trace.steps.slice(0, state.records).map((record) => ({ ...record }));
|
|
872
|
+
engine.events = trace.events.slice(0, state.events);
|
|
873
|
+
engine.ledgerRows = trace.ledger.slice(0, state.ledger);
|
|
874
|
+
engine.poses = trace.poses.slice(0, state.tick);
|
|
875
|
+
engine.truthPoses = trace.truthPoses.slice(0, state.tick);
|
|
876
|
+
engine.velocities = trace.velocities.slice(0, state.tick);
|
|
877
|
+
if (state.pendingSettle !== null) {
|
|
878
|
+
const record = engine.records[state.pendingSettle];
|
|
879
|
+
delete record.settledS;
|
|
880
|
+
engine.pendingSettle = { record, generation: state.generation };
|
|
881
|
+
}
|
|
882
|
+
engine.checkpoints = previous.checkpoints.filter((c) => c.stepIndex <= checkpoint.stepIndex);
|
|
883
|
+
engine.skipCheckpointsThrough = checkpoint.stepIndex;
|
|
884
|
+
const index = checkpoint.stepIndex;
|
|
885
|
+
const root = new TopSequence(engine, members.slice(index), index);
|
|
886
|
+
root.initialize();
|
|
887
|
+
const completed = runLoop(engine, root, maxTimeS, true);
|
|
888
|
+
return {
|
|
889
|
+
trace: finish(engine, plan, completed, periodS),
|
|
890
|
+
checkpoints: engine.checkpoints,
|
|
891
|
+
resumedFromStep: index,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** The recorded pose at `timeS`, interpolated between ticks, with the heading taken the short way. */
|
|
895
|
+
export function simPoseAt(trace, timeS) {
|
|
896
|
+
const poses = trace.poses;
|
|
897
|
+
const first = poses[0];
|
|
898
|
+
const last = poses[poses.length - 1];
|
|
899
|
+
if (first === undefined || last === undefined)
|
|
900
|
+
return null;
|
|
901
|
+
if (timeS <= first[0])
|
|
902
|
+
return { xIn: first[1], yIn: first[2], headingRad: first[3] };
|
|
903
|
+
if (timeS >= last[0])
|
|
904
|
+
return { xIn: last[1], yIn: last[2], headingRad: last[3] };
|
|
905
|
+
let lo = 0;
|
|
906
|
+
let hi = poses.length - 1;
|
|
907
|
+
while (hi - lo > 1) {
|
|
908
|
+
const mid = (lo + hi) >> 1;
|
|
909
|
+
if (poses[mid][0] <= timeS)
|
|
910
|
+
lo = mid;
|
|
911
|
+
else
|
|
912
|
+
hi = mid;
|
|
913
|
+
}
|
|
914
|
+
const a = poses[lo];
|
|
915
|
+
const b = poses[hi];
|
|
916
|
+
const span = b[0] - a[0];
|
|
917
|
+
const u = span <= 0 ? 0 : (timeS - a[0]) / span;
|
|
918
|
+
return {
|
|
919
|
+
xIn: a[1] + (b[1] - a[1]) * u,
|
|
920
|
+
yIn: a[2] + (b[2] - a[2]) * u,
|
|
921
|
+
headingRad: normalizeSigned(a[3] + normalizeSigned(b[3] - a[3]) * u),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
//# sourceMappingURL=simulate.js.map
|