Files
linuxcncWebWASM/web/worker/sim-task.mjs

193 lines
8.7 KiB
JavaScript

import { fnv1a64 } from "./replay.mjs";
export const TASK_SCHEMA = "sim_task.v1";
export const TASK_STATES = Object.freeze(["NEW", "READY", "RUNNING", "PAUSED", "ESTOP", "FAULT", "STOPPED", "DESTROYED"]);
export const TASK_MODES = Object.freeze(["MANUAL", "MDI", "AUTO"]);
export const PROGRAM_STATES = Object.freeze(["idle", "loaded", "running", "paused", "completed", "aborted", "fault"]);
const CORE_STATES = Object.freeze({ NEW: 0, READY: 1, RUNNING: 2, PAUSED: 3, ESTOP: 4, FAULT: 5, STOPPED: 6, DESTROYED: 7 });
const CORE_MODES = Object.freeze({ MANUAL: 0, MDI: 1, AUTO: 2 });
const CORE_PROGRAMS = Object.freeze({ idle: 0, loaded: 1, running: 2, paused: 3, completed: 4, aborted: 5, fault: 6 });
function clone(value) { return structuredClone(value); }
function safeString(value) { return typeof value === "string" && value.length > 0 ? value : null; }
function validInteger(value, minimum = 0) { return Number.isSafeInteger(value) && value >= minimum; }
function coreCall(core, name, returnType, argTypes, args) {
if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args);
const direct = core?.[`_${name}`] ?? core?.[name];
if (typeof direct === "function") return direct(...args);
throw new Error(`missing core export: ${name}`);
}
function readCoreString(core, pointer, length) {
const bytes = core?.HEAPU8;
if (!bytes || !pointer || !length) throw new Error("task_core_memory_unavailable");
const view = bytes.subarray(pointer, pointer + length);
const end = view.indexOf(0);
return new TextDecoder().decode(view.subarray(0, end < 0 ? view.length : end));
}
export class SimTask {
constructor(core, { contextId, sessionId = null } = {}) {
if (!safeString(contextId)) throw new Error("task_context_required");
this.core = core;
this.contextId = contextId;
this.sessionId = safeString(sessionId);
this.coreTask = 0;
this.coreSize = 0;
this.lastError = 0;
this.state = {
schema: TASK_SCHEMA,
context_id: contextId,
session_id: this.sessionId,
state: "NEW",
mode: "MANUAL",
program_status: "idle",
seq: 0,
simulation_tick: 0,
current_line: null,
};
}
bindSession(sessionId) {
if (!safeString(sessionId)) throw new Error("task_session_required");
if (this.state.seq !== 0 && this.sessionId !== sessionId) throw new Error("task_session_immutable");
this.sessionId = sessionId;
this.state.session_id = sessionId;
this.ensureCore();
}
ensureCore() {
if (this.coreTask || typeof this.core?._sim_task_size !== "function") return;
if (typeof this.core?._malloc !== "function" || typeof this.core?._free !== "function") throw new Error("task_core_memory_unavailable");
if (!this.sessionId) return;
this.coreSize = coreCall(this.core, "sim_task_size", "number", [], []);
this.coreTask = this.core._malloc(this.coreSize);
if (!this.coreTask) throw new Error("task_core_allocation_failed");
const result = coreCall(this.core, "sim_task_init", "number", ["number", "string", "string"], [this.coreTask, this.contextId, this.sessionId]);
if (result !== 0) throw new Error(`task_core_init_${result}`);
}
mirror(name, args = [], code = "task_core_error") {
this.ensureCore();
if (!this.coreTask) return;
const result = coreCall(this.core, name, "number", ["number", ...args.map(() => "number")], [this.coreTask, ...args]);
if (result !== 0) throw new Error(`${code}_${result}`);
}
mirrorCommand(seq, tick) {
this.ensureCore();
if (!this.coreTask) return;
const result = coreCall(this.core, "sim_task_set_command", "number", ["number", "number", "number"], [this.coreTask, seq, tick]);
if (result !== 0) throw new Error(`task_command_${result}`);
}
setCommand(seq, tick) {
if (!validInteger(seq, 1)) throw new Error("task_sequence_invalid");
if (!validInteger(tick, 0)) throw new Error("task_tick_invalid");
if (seq !== this.state.seq + 1) throw new Error("task_sequence_gap");
if (tick < this.state.simulation_tick) throw new Error("task_tick_regression");
this.mirrorCommand(seq, tick);
this.state.seq = seq;
this.state.simulation_tick = tick;
this.lastError = 0;
}
setTick(tick) {
if (!validInteger(tick, 0)) throw new Error("task_tick_invalid");
if (tick < this.state.simulation_tick) throw new Error("task_tick_regression");
this.mirror("sim_task_set_tick", [tick], "task_tick");
this.state.simulation_tick = tick;
}
transition(next) {
if (!TASK_STATES.includes(next)) throw new Error("task_state_invalid");
this.mirror("sim_task_transition", [CORE_STATES[next]], "task_transition");
this.state.state = next;
}
setMode(mode) {
if (!TASK_MODES.includes(mode)) throw new Error("task_mode_invalid");
this.mirror("sim_task_set_mode", [CORE_MODES[mode]], "task_mode");
this.state.mode = mode;
}
setProgram(programStatus, currentLine = null) {
if (!PROGRAM_STATES.includes(programStatus)) throw new Error("task_program_invalid");
if (currentLine !== null && !validInteger(currentLine, 0)) throw new Error("task_line_invalid");
this.mirror("sim_task_set_program", [CORE_PROGRAMS[programStatus], currentLine ?? 0xffffffff], "task_program");
this.state.program_status = programStatus;
this.state.current_line = currentLine;
}
fault(errorCode) {
this.mirror("sim_task_fault", [errorCode], "task_fault");
this.state.state = "FAULT";
this.lastError = errorCode;
}
estop(errorCode) {
this.mirror("sim_task_estop", [errorCode], "task_estop");
this.state.state = "ESTOP";
this.lastError = errorCode;
}
recover() {
this.mirror("sim_task_recover", [], "task_recover");
this.state.state = "READY";
this.lastError = 0;
}
export() {
const unsigned = clone(this.state);
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
hash() { return this.export().state_hash; }
assertCoreCanonical() {
if (!this.coreTask) return true;
const required = coreCall(this.core, "sim_task_canonical", "number", ["number", "number", "number"], [this.coreTask, 0, 0]);
if (!required || required > 4096) throw new Error("task_core_canonical_invalid");
const pointer = this.core._malloc(required);
if (!pointer) throw new Error("task_core_canonical_allocation_failed");
try {
const written = coreCall(this.core, "sim_task_canonical", "number", ["number", "number", "number"], [this.coreTask, pointer, required]);
const actual = readCoreString(this.core, pointer, written);
const expected = canonicalTask(this.state);
if (actual !== expected) throw new Error("task_core_canonical_mismatch");
return true;
} finally {
this.core._free(pointer);
}
}
restore(value) {
if (!value || value.schema !== TASK_SCHEMA || value.context_id !== this.contextId || value.session_id !== this.sessionId) throw new Error("task_restore_schema");
const unsigned = { ...value };
delete unsigned.state_hash;
if (value.state_hash !== fnv1a64(unsigned)) throw new Error("task_restore_hash");
if (!TASK_STATES.includes(value.state) || !TASK_MODES.includes(value.mode) || !PROGRAM_STATES.includes(value.program_status)) throw new Error("task_restore_enum");
if (!validInteger(value.seq, 1) || !validInteger(value.simulation_tick, 0) || (value.current_line !== null && !validInteger(value.current_line, 0))) throw new Error("task_restore_range");
this.ensureCore();
if (this.coreTask) {
const result = coreCall(this.core, "sim_task_restore", "number", ["number", "number", "number", "number", "number", "number", "number", "number"], [this.coreTask, CORE_STATES[value.state], CORE_MODES[value.mode], CORE_PROGRAMS[value.program_status], value.seq, value.simulation_tick, value.current_line ?? 0xffffffff, this.lastError]);
if (result !== 0) throw new Error(`task_restore_core_${result}`);
}
this.state = { schema: TASK_SCHEMA, context_id: value.context_id, session_id: value.session_id, state: value.state, mode: value.mode, program_status: value.program_status, seq: value.seq, simulation_tick: value.simulation_tick, current_line: value.current_line };
this.assertCoreCanonical();
return this.export();
}
destroy() {
if (this.coreTask && typeof this.core?._free === "function") this.core._free(this.coreTask);
this.coreTask = 0;
this.state.state = "DESTROYED";
}
}
export function canonicalTask(state) {
return `{"context_id":${JSON.stringify(state.context_id)},"current_line":${state.current_line === null ? "null" : state.current_line},"mode":${JSON.stringify(state.mode)},"program_status":${JSON.stringify(state.program_status)},"schema":"sim_task.v1","seq":${state.seq},"session_id":${JSON.stringify(state.session_id)},"simulation_tick":${state.simulation_tick},"state":${JSON.stringify(state.state)}}`;
}