zelpi 0.1.0
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/README.md +149 -0
- package/bin/pi-os.mjs +37 -0
- package/cli/client.mjs +148 -0
- package/cli/commands.mjs +406 -0
- package/cli/config.mjs +58 -0
- package/cli/daemon.mjs +75 -0
- package/cli/dispatch.mjs +70 -0
- package/cli/embedded.mjs +106 -0
- package/cli/help.mjs +44 -0
- package/cli/hub.mjs +76 -0
- package/cli/kernel.mjs +63 -0
- package/cli/mcp.mjs +144 -0
- package/cli/repl.mjs +100 -0
- package/cli/ui.mjs +109 -0
- package/dist/pios-engine.mjs +948 -0
- package/package.json +37 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
// lib/intent.ts
|
|
2
|
+
var COLORS = ["red", "blue", "green", "yellow", "orange", "purple"];
|
|
3
|
+
function st(label, agent, kind) {
|
|
4
|
+
return { label, agent, kind };
|
|
5
|
+
}
|
|
6
|
+
function parseIntent(raw, items) {
|
|
7
|
+
const text = raw.toLowerCase().trim();
|
|
8
|
+
const aisle = matchAisle(text);
|
|
9
|
+
const color = COLORS.find((c) => text.includes(c));
|
|
10
|
+
const wantsLocate = /\b(locate|find|identify|where|search)\b/.test(text);
|
|
11
|
+
const wantsPick = /\b(pick|grab|retrieve|grasp|fetch|lift|move the|bring)\b/.test(text);
|
|
12
|
+
const wantsInventory = /\b(inventory|count|catalog|stock|audit|scan)\b/.test(text);
|
|
13
|
+
const wantsGo = /\b(go|walk|drive|navigate|patrol|head)\b/.test(text);
|
|
14
|
+
let target;
|
|
15
|
+
if (color) {
|
|
16
|
+
const pool = items.filter(
|
|
17
|
+
(it) => it.color === hex(color) && (aisle ? it.aisle === aisle : true)
|
|
18
|
+
);
|
|
19
|
+
target = pool[0];
|
|
20
|
+
}
|
|
21
|
+
if (wantsInventory && aisle) {
|
|
22
|
+
return {
|
|
23
|
+
summary: `Inventory Aisle ${aisle}: traverse the lane, perceive every shelf face, and catalog detected items.`,
|
|
24
|
+
load: 0.55,
|
|
25
|
+
targetAisle: aisle,
|
|
26
|
+
subtasks: [
|
|
27
|
+
st(`Navigate to head of Aisle ${aisle}`, "spatial", "navigate"),
|
|
28
|
+
st(`Sweep-scan all shelf faces in Aisle ${aisle}`, "vision", "perceive"),
|
|
29
|
+
st("Reconcile detections against spatial map", "spatial", "map"),
|
|
30
|
+
st("Catalog item manifest", "vision", "perceive"),
|
|
31
|
+
st("Settle inventory job & aggregate to global model", "settlement", "settle")
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (wantsPick && (color || target)) {
|
|
36
|
+
const what = color ? `the ${color} box` : "the target object";
|
|
37
|
+
return {
|
|
38
|
+
summary: `Retrieve ${what}${aisle ? ` from Aisle ${aisle}` : ""}: locate, plan a grasp, and execute under safety supervision.`,
|
|
39
|
+
load: 0.8,
|
|
40
|
+
targetAisle: aisle ?? target?.aisle,
|
|
41
|
+
targetItemId: target?.id,
|
|
42
|
+
subtasks: [
|
|
43
|
+
st(`Navigate toward ${what}`, "spatial", "navigate"),
|
|
44
|
+
st(`Visually confirm ${what}`, "vision", "perceive"),
|
|
45
|
+
st("Plan whole-body grasp trajectory", "kinematic", "manipulate"),
|
|
46
|
+
st("Execute grasp (Safety Broker gated)", "kinematic", "manipulate"),
|
|
47
|
+
st("Settle handling job", "settlement", "settle")
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (wantsLocate || color) {
|
|
52
|
+
const what = color ? `the ${color} box` : "the requested object";
|
|
53
|
+
return {
|
|
54
|
+
summary: `Locate ${what}${aisle ? ` in Aisle ${aisle}` : ""}: walk to the shelf and visually identify it.`,
|
|
55
|
+
load: 0.5,
|
|
56
|
+
targetAisle: aisle ?? target?.aisle,
|
|
57
|
+
targetItemId: target?.id,
|
|
58
|
+
subtasks: [
|
|
59
|
+
st(`Navigate toward ${aisle ? `Aisle ${aisle}` : "candidate shelves"}`, "spatial", "navigate"),
|
|
60
|
+
st(`Visually scan for ${what}`, "vision", "perceive"),
|
|
61
|
+
st(`Identify & flag ${what}`, "vision", "perceive")
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (wantsGo) {
|
|
66
|
+
return {
|
|
67
|
+
summary: `Navigate the floor${aisle ? ` to Aisle ${aisle}` : ""} and hold position for further intent.`,
|
|
68
|
+
load: 0.3,
|
|
69
|
+
targetAisle: aisle,
|
|
70
|
+
subtasks: [
|
|
71
|
+
st(`Plan path${aisle ? ` to Aisle ${aisle}` : " across floor"}`, "spatial", "navigate"),
|
|
72
|
+
st("Drive route with live obstacle avoidance", "spatial", "navigate")
|
|
73
|
+
]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
summary: `Interpret "${raw}" as an exploratory directive: survey the nearest aisle and report findings.`,
|
|
78
|
+
load: 0.4,
|
|
79
|
+
targetAisle: aisle,
|
|
80
|
+
subtasks: [
|
|
81
|
+
st("Plan exploratory route", "spatial", "navigate"),
|
|
82
|
+
st("Survey environment", "vision", "perceive"),
|
|
83
|
+
st("Update spatial map", "spatial", "map")
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function matchAisle(text) {
|
|
88
|
+
const m = text.match(/aisle\s*(\d+)/) || text.match(/\b(?:lane|row)\s*(\d+)/);
|
|
89
|
+
if (m) return clampAisle(parseInt(m[1], 10));
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
function clampAisle(n) {
|
|
93
|
+
return Math.min(4, Math.max(1, n));
|
|
94
|
+
}
|
|
95
|
+
function hex(color) {
|
|
96
|
+
return {
|
|
97
|
+
red: "#ef4444",
|
|
98
|
+
blue: "#3b82f6",
|
|
99
|
+
green: "#22c55e",
|
|
100
|
+
yellow: "#eab308",
|
|
101
|
+
orange: "#f97316",
|
|
102
|
+
purple: "#a855f7"
|
|
103
|
+
}[color];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// lib/types.ts
|
|
107
|
+
var PROFILE_META = {
|
|
108
|
+
balanced: {
|
|
109
|
+
label: "Balanced (Kernel default)",
|
|
110
|
+
glyph: "\u25C7",
|
|
111
|
+
desc: "Neutral routing \u2014 the kernel's baseline scheduler.",
|
|
112
|
+
speed: 1,
|
|
113
|
+
scrutiny: 1,
|
|
114
|
+
color: "#94a3b8"
|
|
115
|
+
},
|
|
116
|
+
robustness: {
|
|
117
|
+
label: "Robustness",
|
|
118
|
+
glyph: "\u25A3",
|
|
119
|
+
desc: "Handle many SOCs, buses & hardware; multi-system spread, extra checks.",
|
|
120
|
+
speed: 0.95,
|
|
121
|
+
scrutiny: 1.2,
|
|
122
|
+
color: "#60a5fa"
|
|
123
|
+
},
|
|
124
|
+
optimisation: {
|
|
125
|
+
label: "Optimisation",
|
|
126
|
+
glyph: "\u26A1",
|
|
127
|
+
desc: "Fastest response for routine tasks where speed beats deliberation.",
|
|
128
|
+
speed: 1.4,
|
|
129
|
+
scrutiny: 0.5,
|
|
130
|
+
color: "#34d399"
|
|
131
|
+
},
|
|
132
|
+
"system-critical": {
|
|
133
|
+
label: "System-Critical",
|
|
134
|
+
glyph: "\u25C6",
|
|
135
|
+
desc: "Precise, high-integrity work \u2014 maximum Safety Broker scrutiny.",
|
|
136
|
+
speed: 0.82,
|
|
137
|
+
scrutiny: 1.8,
|
|
138
|
+
color: "#f87171"
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// lib/world.ts
|
|
143
|
+
var FLOOR = { w: 100, h: 60 };
|
|
144
|
+
var COLOR_POOL = [
|
|
145
|
+
{ name: "red", hex: "#ef4444" },
|
|
146
|
+
{ name: "blue", hex: "#3b82f6" },
|
|
147
|
+
{ name: "green", hex: "#22c55e" },
|
|
148
|
+
{ name: "yellow", hex: "#eab308" },
|
|
149
|
+
{ name: "orange", hex: "#f97316" },
|
|
150
|
+
{ name: "purple", hex: "#a855f7" }
|
|
151
|
+
];
|
|
152
|
+
var ITEM_LABELS = ["pallet", "tote", "carton", "drum", "crate", "bin"];
|
|
153
|
+
function buildWorld(seed = 7) {
|
|
154
|
+
const rng = mulberry32(seed);
|
|
155
|
+
const shelves = [];
|
|
156
|
+
const items = [];
|
|
157
|
+
const aisles = 4;
|
|
158
|
+
for (let a = 1; a <= aisles; a++) {
|
|
159
|
+
const x = 14 + (a - 1) * 24;
|
|
160
|
+
const perAisle = 4;
|
|
161
|
+
for (let s = 0; s < perAisle; s++) {
|
|
162
|
+
const y = 8 + s * 12;
|
|
163
|
+
const shelf = { id: `sh-${a}-${s}`, aisle: a, pos: { x, y }, w: 10, h: 7 };
|
|
164
|
+
shelves.push(shelf);
|
|
165
|
+
const count = 1 + Math.floor(rng() * 2);
|
|
166
|
+
for (let i = 0; i < count; i++) {
|
|
167
|
+
const c = COLOR_POOL[Math.floor(rng() * COLOR_POOL.length)];
|
|
168
|
+
const label = ITEM_LABELS[Math.floor(rng() * ITEM_LABELS.length)];
|
|
169
|
+
items.push({
|
|
170
|
+
id: `it-${a}-${s}-${i}`,
|
|
171
|
+
label: `${c.name} ${label}`,
|
|
172
|
+
color: c.hex,
|
|
173
|
+
pos: { x: x + (i === 0 ? -2.5 : 2.5), y: y + 1 },
|
|
174
|
+
aisle: a,
|
|
175
|
+
found: false
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { shelves, items };
|
|
181
|
+
}
|
|
182
|
+
function buildFleet() {
|
|
183
|
+
const base = (i) => ({ x: 4, y: 8 + i * 14 });
|
|
184
|
+
return [
|
|
185
|
+
mkRobot("rb-01", "Atlas-01", "Humanoid biped", "EtherCAT", base(0)),
|
|
186
|
+
mkRobot("rb-02", "Mule-02", "AMR cart", "CAN", base(1)),
|
|
187
|
+
mkRobot("rb-03", "Arm-03", "6-DoF industrial arm", "CAN", base(2)),
|
|
188
|
+
mkRobot("rb-04", "Scout-04", "Quadruped", "EtherCAT", base(3))
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
function mkRobot(id, name, chassis, bus, pos) {
|
|
192
|
+
return {
|
|
193
|
+
id,
|
|
194
|
+
name,
|
|
195
|
+
chassis,
|
|
196
|
+
bus,
|
|
197
|
+
pos,
|
|
198
|
+
heading: 0,
|
|
199
|
+
battery: 70 + Math.random() * 28,
|
|
200
|
+
status: "idle",
|
|
201
|
+
joints: Array.from({ length: 7 }, () => 0.5),
|
|
202
|
+
torque: 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function dist(a, b) {
|
|
206
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
207
|
+
}
|
|
208
|
+
function angle(from, to) {
|
|
209
|
+
return Math.atan2(to.y - from.y, to.x - from.x);
|
|
210
|
+
}
|
|
211
|
+
function mulberry32(seed) {
|
|
212
|
+
let a = seed >>> 0;
|
|
213
|
+
return function() {
|
|
214
|
+
a |= 0;
|
|
215
|
+
a = a + 1831565813 | 0;
|
|
216
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
217
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
218
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// lib/engine.ts
|
|
223
|
+
var LOG_CAP = 170;
|
|
224
|
+
var MOVE_SPEED = 13;
|
|
225
|
+
var SAFETY_TRIGGER_P = 0.45;
|
|
226
|
+
var TRAIN_SECONDS = 26;
|
|
227
|
+
var TRAIN_EPOCHS = 60;
|
|
228
|
+
var BASELINE_Q = 0.42;
|
|
229
|
+
var PiOsEngine = class {
|
|
230
|
+
constructor() {
|
|
231
|
+
this.tasks = [];
|
|
232
|
+
this.logs = [];
|
|
233
|
+
this.models = [];
|
|
234
|
+
this.safety = null;
|
|
235
|
+
this.paused = false;
|
|
236
|
+
/** Active Kernel execution profile (the diagram's QoS router). Biases Safety
|
|
237
|
+
* Broker scrutiny + execution speed. Set via the CLI/console kernel layer. */
|
|
238
|
+
this.profile = "balanced";
|
|
239
|
+
/** When true (Phase 4), an external robot (ROS 2 via rosbridge) owns the pose;
|
|
240
|
+
* the engine stops integrating motion internally and accepts setRobotPose(). */
|
|
241
|
+
this.externalControl = false;
|
|
242
|
+
/** Bumped on reset / fleet change so the ROS bridge can re-seed robot poses. */
|
|
243
|
+
this.fleetGeneration = 0;
|
|
244
|
+
/** Demo aid: force the next gated manipulation to trip the Safety/HITL gate. */
|
|
245
|
+
this.forceSafetyNext = false;
|
|
246
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
247
|
+
this.logId = 1;
|
|
248
|
+
this.taskSeq = 1;
|
|
249
|
+
this.safetyId = 1;
|
|
250
|
+
this.modelSeq = 1;
|
|
251
|
+
this.robotSeq = 5;
|
|
252
|
+
// factory fleet uses 01..04
|
|
253
|
+
this.clock = 0;
|
|
254
|
+
this.version = 0;
|
|
255
|
+
this.rr = 0;
|
|
256
|
+
// round-robin pointer for robot assignment
|
|
257
|
+
this.sub = /* @__PURE__ */ new Map();
|
|
258
|
+
this.acc = { s0: 0, s1: 0, s2: 0, s3: 0 };
|
|
259
|
+
// ambient log accumulators
|
|
260
|
+
this.settlements = 0;
|
|
261
|
+
this.dataAggregated = 0;
|
|
262
|
+
this.snap = null;
|
|
263
|
+
// ── subscription API ───────────────────────────────────────────────────────
|
|
264
|
+
this.subscribe = (fn) => {
|
|
265
|
+
this.listeners.add(fn);
|
|
266
|
+
return () => this.listeners.delete(fn);
|
|
267
|
+
};
|
|
268
|
+
this.getSnapshot = () => {
|
|
269
|
+
if (!this.snap) this.rebuildSnapshot();
|
|
270
|
+
return this.snap;
|
|
271
|
+
};
|
|
272
|
+
const w = buildWorld();
|
|
273
|
+
this.shelves = w.shelves;
|
|
274
|
+
this.items = w.items;
|
|
275
|
+
this.robots = buildFleet();
|
|
276
|
+
this.models = this.seedModels();
|
|
277
|
+
this.log("S3", "info", "Fleet & Economic Orchestrator online. 4 agents enrolled with verifiable M2M identity.");
|
|
278
|
+
this.log("S3", "info", "Factory models deployed: VLA v1, ACT v1, M-HAL v1. Awaiting operator intent.");
|
|
279
|
+
}
|
|
280
|
+
/** Ship one baseline (factory) model per layer, already deployed. */
|
|
281
|
+
seedModels() {
|
|
282
|
+
const mk = (kind, name) => ({
|
|
283
|
+
id: `M${this.modelSeq++}`,
|
|
284
|
+
name,
|
|
285
|
+
kind,
|
|
286
|
+
version: 1,
|
|
287
|
+
embodiment: "universal",
|
|
288
|
+
status: "deployed",
|
|
289
|
+
quality: BASELINE_Q,
|
|
290
|
+
trainProgress: 1,
|
|
291
|
+
epoch: TRAIN_EPOCHS,
|
|
292
|
+
totalEpochs: TRAIN_EPOCHS,
|
|
293
|
+
loss: 0.31,
|
|
294
|
+
targetLoss: 0.31,
|
|
295
|
+
lossHistory: [2.4, 1.6, 1, 0.66, 0.45, 0.31],
|
|
296
|
+
datasetMB: 1200,
|
|
297
|
+
createdAt: 0
|
|
298
|
+
});
|
|
299
|
+
return [mk("vla", "vla-omni"), mk("act", "act-chunk"), mk("mhal", "mhal-prior")];
|
|
300
|
+
}
|
|
301
|
+
emit() {
|
|
302
|
+
this.rebuildSnapshot();
|
|
303
|
+
this.listeners.forEach((l) => l());
|
|
304
|
+
}
|
|
305
|
+
rebuildSnapshot() {
|
|
306
|
+
this.snap = {
|
|
307
|
+
robots: this.robots,
|
|
308
|
+
items: this.items,
|
|
309
|
+
shelves: this.shelves,
|
|
310
|
+
tasks: this.tasks,
|
|
311
|
+
logs: this.logs,
|
|
312
|
+
models: this.models,
|
|
313
|
+
safety: this.safety,
|
|
314
|
+
paused: this.paused,
|
|
315
|
+
profile: this.profile,
|
|
316
|
+
metrics: this.metrics(),
|
|
317
|
+
version: this.version
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// ── operator commands ──────────────────────────────────────────────────────
|
|
321
|
+
/** Conversational Command Center entry point (offline deterministic parse). */
|
|
322
|
+
submitIntent(raw) {
|
|
323
|
+
const text = raw.trim();
|
|
324
|
+
if (!text) return null;
|
|
325
|
+
return this.submitPlannedIntent(text, parseIntent(text, this.items), "fallback");
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Create + dispatch a task from an already-computed plan. Lets the backend
|
|
329
|
+
* supply a plan from a real System-2 model (LLM/VLA) while sharing one code
|
|
330
|
+
* path with the offline deterministic parser.
|
|
331
|
+
*/
|
|
332
|
+
submitPlannedIntent(raw, parsed, source = "llm") {
|
|
333
|
+
const text = raw.trim();
|
|
334
|
+
if (!text) return null;
|
|
335
|
+
const robot = this.pickRobot();
|
|
336
|
+
if (!robot) {
|
|
337
|
+
this.log("S3", "warn", "No fleet capacity available \u2014 all agents engaged.");
|
|
338
|
+
this.emit();
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
this.log("OP", "info", `Operator \u2192 "${text}"`);
|
|
342
|
+
this.log(
|
|
343
|
+
"S2",
|
|
344
|
+
source === "llm" ? "ok" : "info",
|
|
345
|
+
`VLA${source === "llm" ? " (model)" : ""} parsed intent: ${parsed.summary}`
|
|
346
|
+
);
|
|
347
|
+
const task = {
|
|
348
|
+
id: `T${this.taskSeq++}`,
|
|
349
|
+
intent: text,
|
|
350
|
+
summary: parsed.summary,
|
|
351
|
+
robotId: robot.id,
|
|
352
|
+
state: "running",
|
|
353
|
+
createdAt: this.clock,
|
|
354
|
+
targetAisle: parsed.targetAisle,
|
|
355
|
+
targetItemId: parsed.targetItemId,
|
|
356
|
+
subtasks: parsed.subtasks.map((s, i) => ({
|
|
357
|
+
...s,
|
|
358
|
+
id: `${this.taskSeq}-${i}`,
|
|
359
|
+
state: "queued",
|
|
360
|
+
progress: 0
|
|
361
|
+
}))
|
|
362
|
+
};
|
|
363
|
+
this.tasks.unshift(task);
|
|
364
|
+
robot.taskId = task.id;
|
|
365
|
+
this.log("S3", "act", `Deployed ${task.subtasks.length}-step plan to ${robot.name} (${task.id}).`);
|
|
366
|
+
this.activateNext(task, robot);
|
|
367
|
+
this.emit();
|
|
368
|
+
return task;
|
|
369
|
+
}
|
|
370
|
+
/** Operator resolves a Safety Broker / HITL prompt. */
|
|
371
|
+
resolveSafety(option) {
|
|
372
|
+
const p = this.safety;
|
|
373
|
+
if (!p) return;
|
|
374
|
+
const robot = this.robot(p.robotId);
|
|
375
|
+
const task = this.task(p.taskId);
|
|
376
|
+
const sub = task?.subtasks.find((s) => s.id === p.subTaskId);
|
|
377
|
+
this.log("OP", "act", `Operator authorization: "${option}".`);
|
|
378
|
+
this.safety = null;
|
|
379
|
+
if (!robot || !task || !sub) {
|
|
380
|
+
this.emit();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const opt = option.toLowerCase();
|
|
384
|
+
if (opt.includes("abort")) {
|
|
385
|
+
task.state = "aborted";
|
|
386
|
+
sub.state = "blocked";
|
|
387
|
+
robot.status = "idle";
|
|
388
|
+
robot.taskId = void 0;
|
|
389
|
+
robot.waypoint = void 0;
|
|
390
|
+
this.log("S3", "warn", `${task.id} aborted by operator. ${robot.name} returning to standby.`);
|
|
391
|
+
} else if (opt.includes("reroute")) {
|
|
392
|
+
this.sub.get(sub.id).gateResolved = true;
|
|
393
|
+
this.log("S1", "act", `Re-planning trajectory with new constraint envelope (operator reroute).`);
|
|
394
|
+
task.state = "running";
|
|
395
|
+
robot.status = "manipulating";
|
|
396
|
+
} else {
|
|
397
|
+
this.sub.get(sub.id).gateResolved = true;
|
|
398
|
+
this.log("SAFETY", "ok", `Override accepted. Releasing trajectory to System 0 (M-HAL).`);
|
|
399
|
+
task.state = "running";
|
|
400
|
+
robot.status = "manipulating";
|
|
401
|
+
}
|
|
402
|
+
this.emit();
|
|
403
|
+
}
|
|
404
|
+
togglePause() {
|
|
405
|
+
this.paused = !this.paused;
|
|
406
|
+
this.log("OP", "info", this.paused ? "Global execution paused." : "Global execution resumed.");
|
|
407
|
+
this.emit();
|
|
408
|
+
}
|
|
409
|
+
/** Force the next gated manipulation to raise the Safety/HITL gate (demos). */
|
|
410
|
+
forceSafety() {
|
|
411
|
+
this.forceSafetyNext = true;
|
|
412
|
+
}
|
|
413
|
+
reset() {
|
|
414
|
+
const w = buildWorld();
|
|
415
|
+
this.shelves = w.shelves;
|
|
416
|
+
this.items = w.items;
|
|
417
|
+
this.robots = buildFleet();
|
|
418
|
+
this.tasks = [];
|
|
419
|
+
this.logs = [];
|
|
420
|
+
this.safety = null;
|
|
421
|
+
this.sub.clear();
|
|
422
|
+
this.settlements = 0;
|
|
423
|
+
this.dataAggregated = 0;
|
|
424
|
+
this.clock = 0;
|
|
425
|
+
this.modelSeq = 1;
|
|
426
|
+
this.robotSeq = 5;
|
|
427
|
+
this.models = this.seedModels();
|
|
428
|
+
this.fleetGeneration++;
|
|
429
|
+
this.log("S3", "info", "Runtime reset. Fleet re-enrolled, factory models redeployed.");
|
|
430
|
+
this.emit();
|
|
431
|
+
}
|
|
432
|
+
// ── Neural Model Foundry (System 3 trains global models) ───────────────────
|
|
433
|
+
/** Kick off a training run; fits a new model on the aggregated edge experience.
|
|
434
|
+
* When `external`, a real training job (LeRobot/GPU) drives the loss curve and
|
|
435
|
+
* the engine's internal simulator leaves this model alone. */
|
|
436
|
+
createModel(kind, name, embodiment, external = false) {
|
|
437
|
+
const dataset = Math.max(200, this.dataAggregated);
|
|
438
|
+
const dataFactor = Math.min(1, dataset / 3200);
|
|
439
|
+
const quality = Math.min(0.98, 0.5 + dataFactor * 0.38 + Math.random() * 0.08);
|
|
440
|
+
const version = this.models.filter((m) => m.kind === kind).length + 1;
|
|
441
|
+
const model = {
|
|
442
|
+
id: `M${this.modelSeq++}`,
|
|
443
|
+
name: name.trim() || `${kind}-v${version}`,
|
|
444
|
+
kind,
|
|
445
|
+
version,
|
|
446
|
+
embodiment: embodiment || "universal",
|
|
447
|
+
status: "training",
|
|
448
|
+
quality,
|
|
449
|
+
trainProgress: 0,
|
|
450
|
+
epoch: 0,
|
|
451
|
+
totalEpochs: TRAIN_EPOCHS,
|
|
452
|
+
loss: 2.6,
|
|
453
|
+
targetLoss: 0.02 + (1 - quality) * 0.55,
|
|
454
|
+
lossHistory: [2.6],
|
|
455
|
+
datasetMB: dataset,
|
|
456
|
+
createdAt: this.clock,
|
|
457
|
+
external
|
|
458
|
+
};
|
|
459
|
+
this.models.unshift(model);
|
|
460
|
+
this.log(
|
|
461
|
+
"S3",
|
|
462
|
+
"act",
|
|
463
|
+
external ? `Training ${model.name} v${version} (${kind.toUpperCase()}) on a real dataset via external job\u2026` : `Training ${model.name} v${version} (${kind.toUpperCase()}) on ${(dataset / 1024).toFixed(2)} GB edge experience\u2026`
|
|
464
|
+
);
|
|
465
|
+
this.emit();
|
|
466
|
+
return model;
|
|
467
|
+
}
|
|
468
|
+
/** External training job → push a real training step (loss) into a model. */
|
|
469
|
+
setModelTrainingProgress(id, progress, loss, step) {
|
|
470
|
+
const m = this.models.find((x) => x.id === id);
|
|
471
|
+
if (!m) return;
|
|
472
|
+
m.trainProgress = Math.min(1, Math.max(0, progress));
|
|
473
|
+
m.loss = loss;
|
|
474
|
+
m.epoch = step ?? Math.floor(m.trainProgress * m.totalEpochs);
|
|
475
|
+
m.lossHistory.push(loss);
|
|
476
|
+
if (m.lossHistory.length > 60) m.lossHistory.splice(0, m.lossHistory.length - 60);
|
|
477
|
+
this.emit();
|
|
478
|
+
}
|
|
479
|
+
/** External training job → finished. Record real final quality/loss. */
|
|
480
|
+
completeModelTraining(id, quality, loss) {
|
|
481
|
+
const m = this.models.find((x) => x.id === id);
|
|
482
|
+
if (!m || m.status !== "training") return;
|
|
483
|
+
m.trainProgress = 1;
|
|
484
|
+
m.status = "ready";
|
|
485
|
+
m.quality = Math.min(0.99, Math.max(0, quality));
|
|
486
|
+
if (loss !== void 0) {
|
|
487
|
+
m.loss = loss;
|
|
488
|
+
m.targetLoss = loss;
|
|
489
|
+
m.lossHistory.push(loss);
|
|
490
|
+
}
|
|
491
|
+
this.log("S3", "ok", `${m.name} v${m.version} trained on real data \u2014 final loss ${m.loss.toFixed(3)}, quality ${m.quality * 100 | 0}%. Ready to deploy.`);
|
|
492
|
+
this.emit();
|
|
493
|
+
}
|
|
494
|
+
/** Promote a ready model to live; demote the previously-deployed peer. */
|
|
495
|
+
deployModel(id) {
|
|
496
|
+
const model = this.models.find((m) => m.id === id);
|
|
497
|
+
if (!model || model.status === "training") return;
|
|
498
|
+
for (const m of this.models) {
|
|
499
|
+
if (m.kind === model.kind && m.status === "deployed") m.status = "ready";
|
|
500
|
+
}
|
|
501
|
+
model.status = "deployed";
|
|
502
|
+
this.log("S3", "ok", `Rollout: ${model.name} v${model.version} now live on ${model.kind.toUpperCase()} (quality ${model.quality * 100 | 0}%). Global model pushed to fleet.`);
|
|
503
|
+
this.emit();
|
|
504
|
+
}
|
|
505
|
+
/** Enroll a new hardware embodiment; the M-HAL adapts its universal prior. */
|
|
506
|
+
enrollEmbodiment(spec) {
|
|
507
|
+
const idx = this.robots.length;
|
|
508
|
+
const robot = {
|
|
509
|
+
id: `rb-${String(this.robotSeq).padStart(2, "0")}`,
|
|
510
|
+
name: spec.name.trim() || `Unit-${this.robotSeq}`,
|
|
511
|
+
chassis: spec.chassis,
|
|
512
|
+
bus: spec.bus,
|
|
513
|
+
pos: { x: 4, y: 6 + idx * 11 % 50 },
|
|
514
|
+
heading: 0,
|
|
515
|
+
battery: 88 + Math.random() * 10,
|
|
516
|
+
status: "idle",
|
|
517
|
+
joints: Array.from({ length: Math.max(2, Math.min(28, spec.joints)) }, () => 0.5),
|
|
518
|
+
torque: 0
|
|
519
|
+
};
|
|
520
|
+
this.robotSeq++;
|
|
521
|
+
this.robots.push(robot);
|
|
522
|
+
this.fleetGeneration++;
|
|
523
|
+
this.log("S3", "ok", `Embodiment "${robot.name}" enrolled \u2014 verifiable M2M identity issued. M-HAL adapting universal prior to ${robot.chassis} over ${robot.bus}.`);
|
|
524
|
+
this.emit();
|
|
525
|
+
return robot;
|
|
526
|
+
}
|
|
527
|
+
/** Deployed-model quality for a layer (falls back to factory baseline). */
|
|
528
|
+
q(kind) {
|
|
529
|
+
const m = this.models.find((x) => x.kind === kind && x.status === "deployed");
|
|
530
|
+
return m ? m.quality : BASELINE_Q;
|
|
531
|
+
}
|
|
532
|
+
// ── Kernel: QoS profile router ───────────────────────────────────────────────
|
|
533
|
+
/** Switch the active execution profile (Robustness / Optimisation / System-Critical). */
|
|
534
|
+
setProfile(profile) {
|
|
535
|
+
if (!PROFILE_META[profile] || this.profile === profile) return;
|
|
536
|
+
this.profile = profile;
|
|
537
|
+
const p = PROFILE_META[profile];
|
|
538
|
+
this.log(
|
|
539
|
+
"S3",
|
|
540
|
+
"act",
|
|
541
|
+
`Kernel profile \u2192 ${p.label}. Execution ${p.speed >= 1 ? "+" : ""}${Math.round((p.speed - 1) * 100)}% \xB7 Safety scrutiny \xD7${p.scrutiny.toFixed(2)}.`
|
|
542
|
+
);
|
|
543
|
+
this.emit();
|
|
544
|
+
}
|
|
545
|
+
/** Speed multiplier for the active profile. */
|
|
546
|
+
get profSpeed() {
|
|
547
|
+
return PROFILE_META[this.profile].speed;
|
|
548
|
+
}
|
|
549
|
+
/** Safety-Broker scrutiny multiplier for the active profile. */
|
|
550
|
+
get profScrutiny() {
|
|
551
|
+
return PROFILE_META[this.profile].scrutiny;
|
|
552
|
+
}
|
|
553
|
+
// ── persistence (Phase 1: server-side durable state) ───────────────────────
|
|
554
|
+
/** Serialize the durable display state (no transient runtime maps). */
|
|
555
|
+
exportState() {
|
|
556
|
+
return {
|
|
557
|
+
v: 1,
|
|
558
|
+
clock: this.clock,
|
|
559
|
+
robots: this.robots,
|
|
560
|
+
items: this.items,
|
|
561
|
+
tasks: this.tasks,
|
|
562
|
+
logs: this.logs,
|
|
563
|
+
models: this.models,
|
|
564
|
+
settlements: this.settlements,
|
|
565
|
+
dataAggregated: this.dataAggregated,
|
|
566
|
+
profile: this.profile,
|
|
567
|
+
seqs: {
|
|
568
|
+
logId: this.logId,
|
|
569
|
+
taskSeq: this.taskSeq,
|
|
570
|
+
safetyId: this.safetyId,
|
|
571
|
+
modelSeq: this.modelSeq,
|
|
572
|
+
robotSeq: this.robotSeq
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** Restore from a previously exported state (best-effort; rebuilds runtime). */
|
|
577
|
+
importState(s) {
|
|
578
|
+
if (!s || s.v !== 1) return;
|
|
579
|
+
this.clock = s.clock ?? 0;
|
|
580
|
+
if (s.robots?.length) this.robots = s.robots;
|
|
581
|
+
if (s.items?.length) this.items = s.items;
|
|
582
|
+
this.tasks = s.tasks ?? [];
|
|
583
|
+
this.logs = s.logs ?? [];
|
|
584
|
+
if (s.models?.length) this.models = s.models;
|
|
585
|
+
this.settlements = s.settlements ?? 0;
|
|
586
|
+
this.dataAggregated = s.dataAggregated ?? 0;
|
|
587
|
+
if (s.profile && PROFILE_META[s.profile]) this.profile = s.profile;
|
|
588
|
+
if (s.seqs) {
|
|
589
|
+
this.logId = s.seqs.logId;
|
|
590
|
+
this.taskSeq = s.seqs.taskSeq;
|
|
591
|
+
this.safetyId = s.seqs.safetyId;
|
|
592
|
+
this.modelSeq = s.seqs.modelSeq;
|
|
593
|
+
this.robotSeq = s.seqs.robotSeq;
|
|
594
|
+
}
|
|
595
|
+
this.sub.clear();
|
|
596
|
+
for (const t of this.tasks) {
|
|
597
|
+
for (const st2 of t.subtasks) if (st2.state === "active") this.sub.set(st2.id, {});
|
|
598
|
+
}
|
|
599
|
+
this.log("S3", "info", "State restored from durable store.");
|
|
600
|
+
this.emit();
|
|
601
|
+
}
|
|
602
|
+
// ── Phase 4: external robot control (ROS 2 via rosbridge) ───────────────────
|
|
603
|
+
/** Toggle whether an external robot owns pose integration. */
|
|
604
|
+
setExternalControl(on) {
|
|
605
|
+
if (this.externalControl === on) return;
|
|
606
|
+
this.externalControl = on;
|
|
607
|
+
this.log(
|
|
608
|
+
"S0",
|
|
609
|
+
on ? "ok" : "warn",
|
|
610
|
+
on ? "M-HAL bridged to external robot (ROS 2 / rosbridge). Pose now driven by hardware odometry." : "External robot link lost \u2014 reverting to internal kinematic model."
|
|
611
|
+
);
|
|
612
|
+
this.emit();
|
|
613
|
+
}
|
|
614
|
+
/** Inject a pose from external odometry (called by the rosbridge client). */
|
|
615
|
+
setRobotPose(id, x, y, heading) {
|
|
616
|
+
const r = this.robot(id);
|
|
617
|
+
if (!r) return;
|
|
618
|
+
r.pos = { x, y };
|
|
619
|
+
if (heading !== void 0) r.heading = heading;
|
|
620
|
+
}
|
|
621
|
+
// ── main tick ──────────────────────────────────────────────────────────────
|
|
622
|
+
tick(dtRaw) {
|
|
623
|
+
const dt = Math.min(dtRaw, 0.05);
|
|
624
|
+
if (this.paused) {
|
|
625
|
+
this.version++;
|
|
626
|
+
this.emit();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
this.trainModels(dt);
|
|
630
|
+
if (this.safety) {
|
|
631
|
+
this.version++;
|
|
632
|
+
this.emit();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
this.clock += dt;
|
|
636
|
+
this.version++;
|
|
637
|
+
for (const task of this.tasks) {
|
|
638
|
+
if (task.state !== "running") continue;
|
|
639
|
+
const robot = this.robot(task.robotId);
|
|
640
|
+
if (!robot) continue;
|
|
641
|
+
this.stepTask(task, robot, dt);
|
|
642
|
+
}
|
|
643
|
+
this.driveActuation(dt);
|
|
644
|
+
this.ambientLogs(dt);
|
|
645
|
+
this.batteries(dt);
|
|
646
|
+
this.emit();
|
|
647
|
+
}
|
|
648
|
+
/** Advance every in-flight *simulated* training run; finalize when it converges.
|
|
649
|
+
* Externally-trained models are driven by the real job, not here. */
|
|
650
|
+
trainModels(dt) {
|
|
651
|
+
for (const m of this.models) {
|
|
652
|
+
if (m.status !== "training" || m.external) continue;
|
|
653
|
+
const before = m.trainProgress;
|
|
654
|
+
m.trainProgress = Math.min(1, m.trainProgress + dt / TRAIN_SECONDS);
|
|
655
|
+
m.epoch = Math.floor(m.trainProgress * m.totalEpochs);
|
|
656
|
+
const decayed = m.targetLoss + (2.6 - m.targetLoss) * Math.exp(-3.2 * m.trainProgress);
|
|
657
|
+
m.loss = Math.max(m.targetLoss, decayed + (Math.random() - 0.5) * 0.05);
|
|
658
|
+
if (Math.floor(m.trainProgress * 20) > Math.floor(before * 20)) {
|
|
659
|
+
m.lossHistory.push(m.loss);
|
|
660
|
+
}
|
|
661
|
+
if (m.trainProgress >= 1) {
|
|
662
|
+
m.status = "ready";
|
|
663
|
+
m.loss = m.targetLoss;
|
|
664
|
+
m.lossHistory.push(m.targetLoss);
|
|
665
|
+
this.log("S3", "ok", `${m.name} v${m.version} converged \u2014 loss ${m.targetLoss.toFixed(3)}, quality ${m.quality * 100 | 0}%. Ready to deploy.`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// ── task execution state machine ───────────────────────────────────────────
|
|
670
|
+
stepTask(task, robot, dt) {
|
|
671
|
+
let active = task.subtasks.find((s) => s.state === "active");
|
|
672
|
+
if (!active) {
|
|
673
|
+
active = this.activateNext(task, robot);
|
|
674
|
+
if (!active) {
|
|
675
|
+
this.completeTask(task, robot);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const rt = this.sub.get(active.id);
|
|
680
|
+
switch (active.kind) {
|
|
681
|
+
case "navigate": {
|
|
682
|
+
const wp = robot.waypoint;
|
|
683
|
+
if (!wp) {
|
|
684
|
+
active.progress = 1;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
const d = dist(robot.pos, wp);
|
|
688
|
+
rt.initialDist = rt.initialDist ?? Math.max(d, 1e-3);
|
|
689
|
+
active.progress = Math.min(1, 1 - d / rt.initialDist);
|
|
690
|
+
this.moveToward(robot, wp, dt);
|
|
691
|
+
if (d < 1.6) {
|
|
692
|
+
active.progress = 1;
|
|
693
|
+
robot.pos = { ...wp };
|
|
694
|
+
}
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
case "perceive": {
|
|
698
|
+
robot.status = "scanning";
|
|
699
|
+
robot.scanCone = Math.min(1, (robot.scanCone ?? 0) + dt * 0.5 * (0.7 + this.q("vla") * 0.7));
|
|
700
|
+
active.progress = robot.scanCone;
|
|
701
|
+
if (task.targetItemId && robot.scanCone > 0.55) {
|
|
702
|
+
const it = this.items.find((i) => i.id === task.targetItemId);
|
|
703
|
+
if (it && !it.found) {
|
|
704
|
+
it.found = true;
|
|
705
|
+
this.log("S2", "ok", `Vision Agent: ${it.label} identified and flagged on the spatial map.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case "map": {
|
|
711
|
+
active.progress = Math.min(1, active.progress + dt * 0.7 * (0.7 + this.q("vla") * 0.5));
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case "manipulate": {
|
|
715
|
+
robot.status = "manipulating";
|
|
716
|
+
const gated = /grasp|execute/i.test(active.label);
|
|
717
|
+
if (gated && !rt.gateResolved) {
|
|
718
|
+
this.runSafetyBroker(task, robot, active);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
active.progress = Math.min(1, active.progress + dt * 0.45 * (0.7 + this.q("act") * 0.8));
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "settle": {
|
|
725
|
+
active.progress = Math.min(1, active.progress + dt * 0.9);
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (active.progress >= 1 && active.state === "active") {
|
|
730
|
+
this.finishSubtask(task, robot, active);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
activateNext(task, robot) {
|
|
734
|
+
const next = task.subtasks.find((s) => s.state === "queued");
|
|
735
|
+
if (!next) return void 0;
|
|
736
|
+
next.state = "active";
|
|
737
|
+
this.sub.set(next.id, {});
|
|
738
|
+
this.onEnterSubtask(task, robot, next);
|
|
739
|
+
return next;
|
|
740
|
+
}
|
|
741
|
+
onEnterSubtask(task, robot, sub) {
|
|
742
|
+
robot.scanCone = 0;
|
|
743
|
+
switch (sub.kind) {
|
|
744
|
+
case "navigate": {
|
|
745
|
+
robot.status = "navigating";
|
|
746
|
+
robot.waypoint = this.resolveWaypoint(task);
|
|
747
|
+
this.log("S2", "info", `Spatial Mapping Agent: Sense \u2192 Reason \u2192 Plan \xB7 ${sub.label}.`);
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "perceive":
|
|
751
|
+
robot.status = "scanning";
|
|
752
|
+
this.log("S2", "info", `Vision Agent engaged: ${sub.label}.`);
|
|
753
|
+
break;
|
|
754
|
+
case "map":
|
|
755
|
+
this.log("S1", "info", `Spatial Mapping Agent: ${sub.label}.`);
|
|
756
|
+
break;
|
|
757
|
+
case "manipulate":
|
|
758
|
+
robot.status = "manipulating";
|
|
759
|
+
this.log("S1", "act", `Kinematic Agent: ${sub.label} \u2014 ACT generating action chunks.`);
|
|
760
|
+
break;
|
|
761
|
+
case "settle":
|
|
762
|
+
this.log("S3", "info", `Settlement Agent: ${sub.label}.`);
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
finishSubtask(task, robot, sub) {
|
|
767
|
+
sub.state = "done";
|
|
768
|
+
sub.progress = 1;
|
|
769
|
+
if (sub.kind === "settle") {
|
|
770
|
+
this.settlements++;
|
|
771
|
+
this.dataAggregated += 40 + Math.random() * 120;
|
|
772
|
+
this.log("S3", "ok", `M2M settlement closed on Peaq network (#${this.settlements}). Edge experience aggregated to global model.`);
|
|
773
|
+
} else if (sub.kind === "perceive") {
|
|
774
|
+
this.log("S2", "ok", `${sub.label} complete.`);
|
|
775
|
+
} else {
|
|
776
|
+
this.log("S1", "ok", `${sub.label} complete.`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
completeTask(task, robot) {
|
|
780
|
+
task.state = "complete";
|
|
781
|
+
robot.status = "idle";
|
|
782
|
+
robot.taskId = void 0;
|
|
783
|
+
robot.waypoint = void 0;
|
|
784
|
+
robot.scanCone = 0;
|
|
785
|
+
this.log("S3", "ok", `${task.id} complete \u2014 ${robot.name} returned to standby.`);
|
|
786
|
+
}
|
|
787
|
+
// ── Safety Broker + Human-in-the-Loop gate (Section 4) ─────────────────────
|
|
788
|
+
runSafetyBroker(task, robot, sub) {
|
|
789
|
+
this.log("SAFETY", "info", `Validating trajectory for ${robot.name}: collision check + joint-limit constraints\u2026`);
|
|
790
|
+
const triggerP = Math.min(0.95, SAFETY_TRIGGER_P * (1 - this.q("mhal") * 0.7) * this.profScrutiny);
|
|
791
|
+
if (this.forceSafetyNext || Math.random() < triggerP) {
|
|
792
|
+
this.forceSafetyNext = false;
|
|
793
|
+
const scenario = this.pickAnomaly();
|
|
794
|
+
task.state = "paused";
|
|
795
|
+
robot.status = "paused";
|
|
796
|
+
sub.state = "blocked";
|
|
797
|
+
this.safety = {
|
|
798
|
+
id: this.safetyId++,
|
|
799
|
+
robotId: robot.id,
|
|
800
|
+
taskId: task.id,
|
|
801
|
+
subTaskId: sub.id,
|
|
802
|
+
kind: scenario.kind,
|
|
803
|
+
message: scenario.message,
|
|
804
|
+
options: scenario.options,
|
|
805
|
+
t: this.clock
|
|
806
|
+
};
|
|
807
|
+
this.log("SAFETY", "alert", `HALT \u2014 ${scenario.message}`);
|
|
808
|
+
this.log("HITL", "alert", `Escalated to Live Observation Deck. Operator decision required.`);
|
|
809
|
+
} else {
|
|
810
|
+
this.sub.get(sub.id).gateResolved = true;
|
|
811
|
+
this.log("SAFETY", "ok", `Trajectory cleared \u2014 collision-free, within physical limits. Releasing to System 0.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
pickAnomaly() {
|
|
815
|
+
const pool = [
|
|
816
|
+
{
|
|
817
|
+
kind: "anomaly",
|
|
818
|
+
message: "Unidentified biological object blocking heavy payload. Reroute or abort?",
|
|
819
|
+
options: ["Authorize", "Reroute", "Abort"]
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
kind: "collision",
|
|
823
|
+
message: "Predicted gripper path intersects an unmapped obstacle within 4 cm. Override?",
|
|
824
|
+
options: ["Override", "Reroute", "Abort"]
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
kind: "constraint",
|
|
828
|
+
message: "Estimated payload exceeds rated wrist torque by 12%. Authorize high-stakes lift?",
|
|
829
|
+
options: ["Authorize", "Reroute", "Abort"]
|
|
830
|
+
}
|
|
831
|
+
];
|
|
832
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
833
|
+
}
|
|
834
|
+
// ── low-level actuation: System 1 → System 0 (M-HAL) ───────────────────────
|
|
835
|
+
driveActuation(dt) {
|
|
836
|
+
for (const r of this.robots) {
|
|
837
|
+
const busy = r.status === "navigating" || r.status === "scanning" || r.status === "manipulating";
|
|
838
|
+
const amp = r.status === "manipulating" ? 0.42 : busy ? 0.22 : 0.05;
|
|
839
|
+
const freq = r.status === "manipulating" ? 6.5 : 3;
|
|
840
|
+
for (let j = 0; j < r.joints.length; j++) {
|
|
841
|
+
r.joints[j] = 0.5 + amp * Math.sin(this.clock * freq + j * 0.9);
|
|
842
|
+
}
|
|
843
|
+
const targetTorque = busy ? 0.35 + amp + Math.random() * 0.15 : 0.02;
|
|
844
|
+
r.torque += (targetTorque - r.torque) * Math.min(1, dt * 6);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
moveToward(robot, wp, dt) {
|
|
848
|
+
if (this.externalControl) return;
|
|
849
|
+
const d = dist(robot.pos, wp);
|
|
850
|
+
if (d < 0.01) return;
|
|
851
|
+
robot.heading = angle(robot.pos, wp);
|
|
852
|
+
const step = Math.min(d, MOVE_SPEED * dt * (0.6 + 0.4 * (robot.battery / 100)) * (0.75 + this.q("act") * 0.5) * this.profSpeed);
|
|
853
|
+
robot.pos = {
|
|
854
|
+
x: robot.pos.x + Math.cos(robot.heading) * step,
|
|
855
|
+
y: robot.pos.y + Math.sin(robot.heading) * step
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
resolveWaypoint(task) {
|
|
859
|
+
if (task.targetItemId) {
|
|
860
|
+
const it = this.items.find((i) => i.id === task.targetItemId);
|
|
861
|
+
if (it) return { x: it.pos.x - 4, y: it.pos.y };
|
|
862
|
+
}
|
|
863
|
+
if (task.targetAisle) {
|
|
864
|
+
const x = 14 + (task.targetAisle - 1) * 24;
|
|
865
|
+
return { x: x - 5, y: FLOOR.h / 2 };
|
|
866
|
+
}
|
|
867
|
+
return { x: FLOOR.w / 2, y: FLOOR.h / 2 };
|
|
868
|
+
}
|
|
869
|
+
// ── ambient telemetry, batteries, metrics ──────────────────────────────────
|
|
870
|
+
ambientLogs(dt) {
|
|
871
|
+
const active = this.robots.filter(
|
|
872
|
+
(r) => ["navigating", "scanning", "manipulating"].includes(r.status)
|
|
873
|
+
);
|
|
874
|
+
if (active.length === 0) return;
|
|
875
|
+
this.acc.s1 += dt;
|
|
876
|
+
this.acc.s0 += dt;
|
|
877
|
+
this.acc.s2 += dt;
|
|
878
|
+
if (this.acc.s1 > 0.8) {
|
|
879
|
+
this.acc.s1 = 0;
|
|
880
|
+
const r = active[Math.floor(Math.random() * active.length)];
|
|
881
|
+
this.log("S1", "info", `ACT \u2192 ${r.name}: action chunk of 40 joint targets streamed @ ~50 Hz.`);
|
|
882
|
+
}
|
|
883
|
+
if (this.acc.s0 > 1.2) {
|
|
884
|
+
this.acc.s0 = 0;
|
|
885
|
+
const r = active[Math.floor(Math.random() * active.length)];
|
|
886
|
+
this.log("S0", "info", `M-HAL \u2192 ${r.bus}: whole-body control @ 1000 Hz \xB7 \u03C4=${(r.torque * 14).toFixed(1)} N\xB7m.`);
|
|
887
|
+
}
|
|
888
|
+
if (this.acc.s2 > 2.6) {
|
|
889
|
+
this.acc.s2 = 0;
|
|
890
|
+
this.log("S2", "info", `VLA deliberating @ ~1 Hz \xB7 cognitive bandwidth ${this.metrics().cognitiveBandwidth * 100 | 0}%.`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
batteries(dt) {
|
|
894
|
+
const drain = dt * 0.22 * (1 - this.q("mhal") * 0.35);
|
|
895
|
+
for (const r of this.robots) {
|
|
896
|
+
const busy = r.status !== "idle" && r.status !== "charging";
|
|
897
|
+
if (busy) r.battery = Math.max(2, r.battery - drain);
|
|
898
|
+
else if (r.battery < 100) r.battery = Math.min(100, r.battery + dt * 0.4);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
metrics() {
|
|
902
|
+
const running = this.tasks.filter((t) => t.state === "running" || t.state === "paused");
|
|
903
|
+
const load = running.length;
|
|
904
|
+
const cognitive = Math.min(1, 0.04 + load * 0.34 * (1 - this.q("vla") * 0.4) + (this.safety ? 0.1 : 0));
|
|
905
|
+
const j = (base, spread) => base + (Math.random() - 0.5) * spread;
|
|
906
|
+
const dep = (kind) => {
|
|
907
|
+
const m = this.models.find((x) => x.kind === kind && x.status === "deployed");
|
|
908
|
+
return m ? { name: m.name, version: m.version, quality: m.quality } : null;
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
clock: this.clock,
|
|
912
|
+
cognitiveBandwidth: cognitive,
|
|
913
|
+
activeTasks: running.length,
|
|
914
|
+
fleetOnline: this.robots.length,
|
|
915
|
+
settlements: this.settlements,
|
|
916
|
+
dataAggregated: this.dataAggregated,
|
|
917
|
+
hz: {
|
|
918
|
+
S3: load || this.models.some((m) => m.status === "training") ? j(1.4, 0.6) : 0.2,
|
|
919
|
+
S2: load ? j(1, 0.2) : 0,
|
|
920
|
+
S1: load ? j(52, 18) : 0,
|
|
921
|
+
S0: load ? j(1e3, 22) : 0,
|
|
922
|
+
SAFETY: this.safety ? 0 : load ? j(20, 6) : 0,
|
|
923
|
+
HITL: this.safety ? 1 : 0
|
|
924
|
+
},
|
|
925
|
+
deployed: { vla: dep("vla"), act: dep("act"), mhal: dep("mhal") }
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
929
|
+
pickRobot() {
|
|
930
|
+
const idle = this.robots.filter((r) => !r.taskId && r.status === "idle");
|
|
931
|
+
if (idle.length) return idle[this.rr++ % idle.length];
|
|
932
|
+
const free = this.robots.filter((r) => !r.taskId);
|
|
933
|
+
return free[0];
|
|
934
|
+
}
|
|
935
|
+
robot(id) {
|
|
936
|
+
return this.robots.find((r) => r.id === id);
|
|
937
|
+
}
|
|
938
|
+
task(id) {
|
|
939
|
+
return this.tasks.find((t) => t.id === id);
|
|
940
|
+
}
|
|
941
|
+
log(system, level, text) {
|
|
942
|
+
this.logs.push({ id: this.logId++, t: this.clock, system, level, text });
|
|
943
|
+
if (this.logs.length > LOG_CAP) this.logs.splice(0, this.logs.length - LOG_CAP);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
export {
|
|
947
|
+
PiOsEngine
|
|
948
|
+
};
|