Make feed override control the WASM trajectory
This commit is contained in:
@@ -22,7 +22,7 @@ class MotionRuntimeTests(unittest.TestCase):
|
|||||||
if not module.is_file():
|
if not module.is_file():
|
||||||
self.skipTest("WASM build has not been generated")
|
self.skipTest("WASM build has not been generated")
|
||||||
text = module.read_text(encoding="utf-8")
|
text = module.read_text(encoding="utf-8")
|
||||||
for symbol in ("sim_motion_init", "sim_motion_enqueue", "sim_motion_step", "sim_motion_state_hash", "sim_motion_canonical"):
|
for symbol in ("sim_motion_init", "sim_motion_enqueue", "sim_motion_step", "sim_motion_set_feed_override", "sim_motion_state_hash", "sim_motion_canonical"):
|
||||||
self.assertIn(symbol, text)
|
self.assertIn(symbol, text)
|
||||||
|
|
||||||
def test_report_contract(self):
|
def test_report_contract(self):
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ if(EMSCRIPTEN)
|
|||||||
string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}")
|
string(REPLACE "_sim_io_init" "_sim_io_size','_sim_io_init" _sim_module_link_flags "${_sim_module_link_flags}")
|
||||||
string(REPLACE "_sim_io_set_estop','_sim_io_recover" "_sim_io_set_estop','_sim_io_set_power','_sim_io_recover" _sim_module_link_flags "${_sim_module_link_flags}")
|
string(REPLACE "_sim_io_set_estop','_sim_io_recover" "_sim_io_set_estop','_sim_io_set_power','_sim_io_recover" _sim_module_link_flags "${_sim_module_link_flags}")
|
||||||
string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}")
|
string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}")
|
||||||
string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}")
|
string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}")
|
||||||
string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
|
string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
|
||||||
set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}")
|
set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ typedef struct sim_motion_command {
|
|||||||
sim_posemath_segment segment;
|
sim_posemath_segment segment;
|
||||||
uint32_t id;
|
uint32_t id;
|
||||||
uint32_t tag;
|
uint32_t tag;
|
||||||
|
uint32_t flags;
|
||||||
} sim_motion_command;
|
} sim_motion_command;
|
||||||
|
|
||||||
|
#define SIM_MOTION_COMMAND_FLAG_RAPID 1u
|
||||||
|
|
||||||
typedef struct sim_motion_state {
|
typedef struct sim_motion_state {
|
||||||
sim_posemath_pose position;
|
sim_posemath_pose position;
|
||||||
double velocity;
|
double velocity;
|
||||||
@@ -65,6 +68,7 @@ int sim_motion_step(sim_motion_queue *queue);
|
|||||||
int sim_motion_set_profile(sim_motion_queue *queue, int profile);
|
int sim_motion_set_profile(sim_motion_queue *queue, int profile);
|
||||||
int sim_motion_set_lookahead(sim_motion_queue *queue, uint32_t depth);
|
int sim_motion_set_lookahead(sim_motion_queue *queue, uint32_t depth);
|
||||||
int sim_motion_set_dynamics(sim_motion_queue *queue, double acceleration_limit, double jerk_limit);
|
int sim_motion_set_dynamics(sim_motion_queue *queue, double acceleration_limit, double jerk_limit);
|
||||||
|
int sim_motion_set_feed_override(sim_motion_queue *queue, double feed_override);
|
||||||
int sim_motion_set_paused(sim_motion_queue *queue, uint32_t paused);
|
int sim_motion_set_paused(sim_motion_queue *queue, uint32_t paused);
|
||||||
int sim_motion_peek(const sim_motion_queue *queue, uint32_t offset, sim_motion_command *command);
|
int sim_motion_peek(const sim_motion_queue *queue, uint32_t offset, sim_motion_command *command);
|
||||||
int sim_motion_read_state(const sim_motion_queue *queue, sim_motion_state *state);
|
int sim_motion_read_state(const sim_motion_queue *queue, sim_motion_state *state);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ enum sim_task_gate_command {
|
|||||||
SIM_TASK_GATE_SET_MODE = 13,
|
SIM_TASK_GATE_SET_MODE = 13,
|
||||||
SIM_TASK_GATE_PROGRAM_LOAD = 14,
|
SIM_TASK_GATE_PROGRAM_LOAD = 14,
|
||||||
SIM_TASK_GATE_STEP = 15,
|
SIM_TASK_GATE_STEP = 15,
|
||||||
|
SIM_TASK_GATE_OVERRIDE = 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sim_task_gate_result {
|
enum sim_task_gate_result {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ typedef struct motion_meta {
|
|||||||
uint32_t lookahead;
|
uint32_t lookahead;
|
||||||
double acceleration_limit;
|
double acceleration_limit;
|
||||||
double jerk_limit;
|
double jerk_limit;
|
||||||
|
double feed_override;
|
||||||
double current_velocity;
|
double current_velocity;
|
||||||
double current_acceleration;
|
double current_acceleration;
|
||||||
uint32_t paused;
|
uint32_t paused;
|
||||||
@@ -34,6 +35,7 @@ static motion_meta *meta_for(const sim_motion_queue *queue, int create) {
|
|||||||
free_slot->lookahead = 1;
|
free_slot->lookahead = 1;
|
||||||
free_slot->acceleration_limit = 1e9;
|
free_slot->acceleration_limit = 1e9;
|
||||||
free_slot->jerk_limit = 1e12;
|
free_slot->jerk_limit = 1e12;
|
||||||
|
free_slot->feed_override = 1.0;
|
||||||
free_slot->paused = 0;
|
free_slot->paused = 0;
|
||||||
return free_slot;
|
return free_slot;
|
||||||
}
|
}
|
||||||
@@ -106,6 +108,7 @@ int sim_motion_reset(sim_motion_queue *queue) {
|
|||||||
meta->lookahead = 1;
|
meta->lookahead = 1;
|
||||||
meta->acceleration_limit = 1e9;
|
meta->acceleration_limit = 1e9;
|
||||||
meta->jerk_limit = 1e12;
|
meta->jerk_limit = 1e12;
|
||||||
|
meta->feed_override = 1.0;
|
||||||
meta->current_velocity = 0.0;
|
meta->current_velocity = 0.0;
|
||||||
meta->current_acceleration = 0.0;
|
meta->current_acceleration = 0.0;
|
||||||
meta->paused = 0;
|
meta->paused = 0;
|
||||||
@@ -147,6 +150,13 @@ int sim_motion_set_dynamics(sim_motion_queue *queue, double acceleration_limit,
|
|||||||
return SIM_MOTION_OK;
|
return SIM_MOTION_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int sim_motion_set_feed_override(sim_motion_queue *queue, double feed_override) {
|
||||||
|
motion_meta *meta = meta_for(queue, 1);
|
||||||
|
if (meta == NULL || !isfinite(feed_override) || feed_override < 0.0 || feed_override > 2.0) return SIM_MOTION_ERR_RANGE;
|
||||||
|
meta->feed_override = feed_override;
|
||||||
|
return SIM_MOTION_OK;
|
||||||
|
}
|
||||||
|
|
||||||
int sim_motion_set_paused(sim_motion_queue *queue, uint32_t paused) {
|
int sim_motion_set_paused(sim_motion_queue *queue, uint32_t paused) {
|
||||||
motion_meta *meta = meta_for(queue, 1);
|
motion_meta *meta = meta_for(queue, 1);
|
||||||
if (meta == NULL || paused > 1u) return SIM_MOTION_ERR_ARGUMENT;
|
if (meta == NULL || paused > 1u) return SIM_MOTION_ERR_ARGUMENT;
|
||||||
@@ -174,11 +184,12 @@ static double planned_velocity(const sim_motion_queue *queue, uint32_t offset) {
|
|||||||
uint32_t index;
|
uint32_t index;
|
||||||
if (queue == NULL || offset >= queue->count) return 0.0;
|
if (queue == NULL || offset >= queue->count) return 0.0;
|
||||||
index = (queue->head + offset) % SIM_MOTION_QUEUE_CAPACITY;
|
index = (queue->head + offset) % SIM_MOTION_QUEUE_CAPACITY;
|
||||||
velocity = queue->commands[index].segment.velocity;
|
velocity = queue->commands[index].segment.velocity * (meta == NULL || (queue->commands[index].flags & SIM_MOTION_COMMAND_FLAG_RAPID) != 0u ? 1.0 : meta->feed_override);
|
||||||
if (meta == NULL || meta->profile == SIM_MOTION_PROFILE_EXACT_STOP) return velocity;
|
if (meta == NULL || meta->profile == SIM_MOTION_PROFILE_EXACT_STOP) return velocity;
|
||||||
for (index = 1; index <= meta->lookahead && offset + index < queue->count; ++index) {
|
for (index = 1; index <= meta->lookahead && offset + index < queue->count; ++index) {
|
||||||
uint32_t next = (queue->head + offset + index) % SIM_MOTION_QUEUE_CAPACITY;
|
uint32_t next = (queue->head + offset + index) % SIM_MOTION_QUEUE_CAPACITY;
|
||||||
if (queue->commands[next].segment.velocity < velocity) velocity = queue->commands[next].segment.velocity;
|
const double next_velocity = queue->commands[next].segment.velocity * ((queue->commands[next].flags & SIM_MOTION_COMMAND_FLAG_RAPID) != 0u ? 1.0 : meta->feed_override);
|
||||||
|
if (next_velocity < velocity) velocity = next_velocity;
|
||||||
}
|
}
|
||||||
return velocity;
|
return velocity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ int32_t sim_task_command_gate(const sim_task *task, uint32_t command,
|
|||||||
/* AXIS allows jog commands while idle in all three task modes; the
|
/* AXIS allows jog commands while idle in all three task modes; the
|
||||||
manual tab changes mode before sending the command when required. */
|
manual tab changes mode before sending the command when required. */
|
||||||
return machine_on && idle ? SIM_TASK_GATE_ALLOWED : io_estop ? SIM_TASK_GATE_ESTOP_ACTIVE : machine_on ? SIM_TASK_GATE_INTERP_ACTIVE : SIM_TASK_GATE_MACHINE_OFF;
|
return machine_on && idle ? SIM_TASK_GATE_ALLOWED : io_estop ? SIM_TASK_GATE_ESTOP_ACTIVE : machine_on ? SIM_TASK_GATE_INTERP_ACTIVE : SIM_TASK_GATE_MACHINE_OFF;
|
||||||
|
case SIM_TASK_GATE_OVERRIDE:
|
||||||
|
/* Feed override is an online trajectory control. LinuxCNC accepts it
|
||||||
|
while AUTO is running or paused, provided the machine is powered. */
|
||||||
|
return io_estop ? SIM_TASK_GATE_ESTOP_ACTIVE : machine_on ? SIM_TASK_GATE_ALLOWED : SIM_TASK_GATE_MACHINE_OFF;
|
||||||
case SIM_TASK_GATE_MDI:
|
case SIM_TASK_GATE_MDI:
|
||||||
if (io_estop || !machine_on) return io_estop ? SIM_TASK_GATE_ESTOP_ACTIVE : SIM_TASK_GATE_MACHINE_OFF;
|
if (io_estop || !machine_on) return io_estop ? SIM_TASK_GATE_ESTOP_ACTIVE : SIM_TASK_GATE_MACHINE_OFF;
|
||||||
if (!idle) return SIM_TASK_GATE_INTERP_ACTIVE;
|
if (!idle) return SIM_TASK_GATE_INTERP_ACTIVE;
|
||||||
|
|||||||
@@ -44,6 +44,28 @@ int main(void) {
|
|||||||
assert(sim_motion_set_profile(&queue, SIM_MOTION_PROFILE_EXACT_STOP) == SIM_MOTION_OK);
|
assert(sim_motion_set_profile(&queue, SIM_MOTION_PROFILE_EXACT_STOP) == SIM_MOTION_OK);
|
||||||
assert(sim_motion_set_lookahead(&queue, 0) == SIM_MOTION_ERR_RANGE);
|
assert(sim_motion_set_lookahead(&queue, 0) == SIM_MOTION_ERR_RANGE);
|
||||||
assert(sim_motion_set_dynamics(&queue, 0.0, 1.0) == SIM_MOTION_ERR_RANGE);
|
assert(sim_motion_set_dynamics(&queue, 0.0, 1.0) == SIM_MOTION_ERR_RANGE);
|
||||||
|
{
|
||||||
|
sim_motion_queue override_queue = {0};
|
||||||
|
sim_motion_state override_state = {0};
|
||||||
|
sim_motion_command override_command = {0};
|
||||||
|
override_command.segment = line(0, 1);
|
||||||
|
assert(sim_motion_init(&override_queue, 0.1, 10.0) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_set_feed_override(&override_queue, 0.5) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_set_feed_override(&override_queue, 0.5) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_set_feed_override(&override_queue, -0.1) == SIM_MOTION_ERR_RANGE);
|
||||||
|
assert(sim_motion_set_feed_override(&override_queue, 2.1) == SIM_MOTION_ERR_RANGE);
|
||||||
|
assert(sim_motion_enqueue(&override_queue, &override_command, 0) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_step(&override_queue) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_read_state(&override_queue, &override_state) == SIM_MOTION_OK);
|
||||||
|
assert(override_state.position.tran.x > 0.049 && override_state.position.tran.x < 0.051);
|
||||||
|
assert(override_state.velocity > 0.49 && override_state.velocity < 0.51);
|
||||||
|
sim_motion_reset(&override_queue);
|
||||||
|
override_command.flags = SIM_MOTION_COMMAND_FLAG_RAPID;
|
||||||
|
assert(sim_motion_enqueue(&override_queue, &override_command, 0) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_step(&override_queue) == SIM_MOTION_OK);
|
||||||
|
assert(sim_motion_read_state(&override_queue, &override_state) == SIM_MOTION_OK);
|
||||||
|
assert(override_state.position.tran.x > 0.099 && override_state.position.tran.x < 0.101);
|
||||||
|
}
|
||||||
command.segment = line(9, 10);
|
command.segment = line(9, 10);
|
||||||
assert(sim_motion_enqueue(&queue, &command, 0) == SIM_MOTION_ERR_STATE);
|
assert(sim_motion_enqueue(&queue, &command, 0) == SIM_MOTION_ERR_STATE);
|
||||||
command.segment.motion_type = 99;
|
command.segment.motion_type = 99;
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ int main(void) {
|
|||||||
assert(sim_task_command_gate(&task, SIM_TASK_GATE_PAUSE, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_PAUSE_STATE);
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_PAUSE, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_PAUSE_STATE);
|
||||||
assert(sim_task_set_mode(&task, SIM_TASK_MODE_AUTO) == SIM_TASK_OK);
|
assert(sim_task_set_mode(&task, SIM_TASK_MODE_AUTO) == SIM_TASK_OK);
|
||||||
assert(sim_task_command_gate(&task, SIM_TASK_GATE_PAUSE, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_PAUSE, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
||||||
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_OVERRIDE, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
||||||
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_OVERRIDE, 0, 0, 0, 1, 0, 10) == SIM_TASK_GATE_MACHINE_OFF);
|
||||||
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_OVERRIDE, 0, 1, 1, 1, 0, 10) == SIM_TASK_GATE_ESTOP_ACTIVE);
|
||||||
assert(sim_task_command_gate(&task, SIM_TASK_GATE_POWER, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_POWER, 0, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
||||||
assert(sim_task_command_gate(&task, SIM_TASK_GATE_SET_MODE, SIM_TASK_MODE_MANUAL, 1, 0, 1, 0, 10) == SIM_TASK_GATE_MODE_AUTO_ACTIVE);
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_SET_MODE, SIM_TASK_MODE_MANUAL, 1, 0, 1, 0, 10) == SIM_TASK_GATE_MODE_AUTO_ACTIVE);
|
||||||
assert(sim_task_command_gate(&task, SIM_TASK_GATE_ESTOP, 1, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
assert(sim_task_command_gate(&task, SIM_TASK_GATE_ESTOP, 1, 1, 0, 1, 0, 10) == SIM_TASK_GATE_ALLOWED);
|
||||||
|
|||||||
@@ -189,9 +189,12 @@ export function App() {
|
|||||||
const programFileInput = useRef<HTMLInputElement>(null);
|
const programFileInput = useRef<HTMLInputElement>(null);
|
||||||
const [plotCleared, setPlotCleared] = useState(false);
|
const [plotCleared, setPlotCleared] = useState(false);
|
||||||
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
||||||
|
const [feedOverrideDraft, setFeedOverrideDraft] = useState(100);
|
||||||
|
const feedOverridePending = useRef<number | null>(null);
|
||||||
|
const feedOverrideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [previewProgress, setPreviewProgress] = useState<{ percent: number; phase: "reading" | "wasm" | "ready" | "error"; detail: string } | null>(null);
|
const [previewProgress, setPreviewProgress] = useState<{ percent: number; phase: "reading" | "wasm" | "ready" | "error"; detail: string } | null>(null);
|
||||||
const previewProgressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
const previewProgressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); client.close(); }; }, [client, operationController]);
|
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); client.close(); }; }, [client, operationController]);
|
||||||
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
||||||
const taskRecord = state.task ?? {};
|
const taskRecord = state.task ?? {};
|
||||||
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
||||||
@@ -207,7 +210,7 @@ export function App() {
|
|||||||
const programPaused = operationState === "PAUSED";
|
const programPaused = operationState === "PAUSED";
|
||||||
const idle = operationState === "IDLE";
|
const idle = operationState === "IDLE";
|
||||||
const wasmGate = (action: string) => state.gates[action] ?? { action, enabled: false, reason: "linuxcnc_wasm_snapshot_pending" };
|
const wasmGate = (action: string) => state.gates[action] ?? { action, enabled: false, reason: "linuxcnc_wasm_snapshot_pending" };
|
||||||
const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset") };
|
const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset"), override: wasmGate("override") };
|
||||||
const planner = state.snapshot?.authoritative?.planner;
|
const planner = state.snapshot?.authoritative?.planner;
|
||||||
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
|
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
|
||||||
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
|
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
|
||||||
@@ -421,6 +424,34 @@ export function App() {
|
|||||||
const coolant = { mist: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.mist_requested), flood: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.flood_requested) };
|
const coolant = { mist: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.mist_requested), flood: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.flood_requested) };
|
||||||
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
||||||
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
||||||
|
const wasmFeedOverride = Number(state.snapshot?.overrides?.feed ?? 1);
|
||||||
|
const feedOverridePercent = Number.isFinite(wasmFeedOverride) ? Math.max(0, Math.min(200, Math.round(wasmFeedOverride * 100))) : 100;
|
||||||
|
useEffect(() => {
|
||||||
|
if (feedOverridePending.current === null) setFeedOverrideDraft(feedOverridePercent);
|
||||||
|
else if (feedOverridePercent === feedOverridePending.current) {
|
||||||
|
feedOverridePending.current = null;
|
||||||
|
setFeedOverrideDraft(feedOverridePercent);
|
||||||
|
}
|
||||||
|
}, [feedOverridePercent]);
|
||||||
|
const requestFeedOverride = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const percent = Math.max(0, Math.min(200, Number(event.target.value)));
|
||||||
|
if (!Number.isFinite(percent)) return;
|
||||||
|
setFeedOverrideDraft(percent);
|
||||||
|
feedOverridePending.current = percent;
|
||||||
|
if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current);
|
||||||
|
feedOverrideTimer.current = setTimeout(() => {
|
||||||
|
feedOverrideTimer.current = null;
|
||||||
|
const target = feedOverridePending.current;
|
||||||
|
if (target === null) return;
|
||||||
|
void dispatch("SET_OVERRIDE", { feed: target / 100 }).then(() => {
|
||||||
|
if (feedOverridePending.current === target) feedOverridePending.current = null;
|
||||||
|
}).catch(() => {
|
||||||
|
if (feedOverridePending.current !== target) return;
|
||||||
|
feedOverridePending.current = null;
|
||||||
|
setFeedOverrideDraft(feedOverridePercent);
|
||||||
|
});
|
||||||
|
}, 80);
|
||||||
|
};
|
||||||
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9";
|
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9";
|
||||||
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
|
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
|
||||||
const currentLine = Number(state.program?.line ?? state.task?.current_line ?? 0) || null;
|
const currentLine = Number(state.program?.line ?? state.task?.current_line ?? 0) || null;
|
||||||
@@ -443,7 +474,7 @@ export function App() {
|
|||||||
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
||||||
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
|
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
|
||||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||||
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
<section className="axis-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverrideDraft}%</output><input type="range" min="0" max="200" step="1" value={feedOverrideDraft} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading} aria-label="Feed override" aria-valuetext={`${feedOverrideDraft}%`} data-axis-feed-override={feedOverrideDraft} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" aria-label="Spindle override" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
||||||
<XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
|
<XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
|
||||||
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
||||||
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
|
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
|||||||
state_hash: String(result.state_hash ?? previous.snapshot.state_hash),
|
state_hash: String(result.state_hash ?? previous.snapshot.state_hash),
|
||||||
task: (result.task as SimSnapshotV1["task"]) ?? previous.snapshot.task,
|
task: (result.task as SimSnapshotV1["task"]) ?? previous.snapshot.task,
|
||||||
program: (result.program as Record<string, unknown>) ?? previous.snapshot.program,
|
program: (result.program as Record<string, unknown>) ?? previous.snapshot.program,
|
||||||
|
overrides: (result.overrides as SimSnapshotV1["overrides"]) ?? previous.snapshot.overrides,
|
||||||
motion: {
|
motion: {
|
||||||
...previous.snapshot.motion,
|
...previous.snapshot.motion,
|
||||||
// Keep the XYZAC axis vector intact on every motion delta. Older
|
// Keep the XYZAC axis vector intact on every motion delta. Older
|
||||||
@@ -218,7 +219,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
|||||||
motion_trace: [],
|
motion_trace: [],
|
||||||
}
|
}
|
||||||
: previous.snapshot?.visual;
|
: previous.snapshot?.visual;
|
||||||
const snapshot = { simulation_tick: message.simulation_tick ?? previous.tick, state_hash: String(result.state_hash ?? previous.snapshot?.state_hash ?? ""), task: result.task as SimSnapshotV1["task"], motion: { axes, joints, velocity: Array.isArray(motionState.velocity) ? motionState.velocity as number[] : [], homed: typeof motionState.homed === "boolean" ? motionState.homed : undefined, homed_axes: Array.isArray(motionState.homed_axes) ? motionState.homed_axes as boolean[] : undefined }, authoritative, ...(visual ? { visual } : {}) } satisfies SimSnapshotV1;
|
const snapshot = { simulation_tick: message.simulation_tick ?? previous.tick, state_hash: String(result.state_hash ?? previous.snapshot?.state_hash ?? ""), task: result.task as SimSnapshotV1["task"], motion: { axes, joints, velocity: Array.isArray(motionState.velocity) ? motionState.velocity as number[] : [], homed: typeof motionState.homed === "boolean" ? motionState.homed : undefined, homed_axes: Array.isArray(motionState.homed_axes) ? motionState.homed_axes as boolean[] : undefined }, overrides: (result.overrides as SimSnapshotV1["overrides"]) ?? previous.snapshot?.overrides, authoritative, ...(visual ? { visual } : {}) } satisfies SimSnapshotV1;
|
||||||
return { snapshot, toolpath: nextToolpath, task: (result.task as Record<string, unknown>) ?? previous.task, program: programResult as Record<string, unknown> ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState, gates: (result.gates as FrontendState["gates"]) ?? previous.gates, executionTrace };
|
return { snapshot, toolpath: nextToolpath, task: (result.task as Record<string, unknown>) ?? previous.task, program: programResult as Record<string, unknown> ?? previous.program, devices: (result.devices as Record<string, unknown>) ?? previous.devices, deviceIo: (result.device_io as Record<string, unknown>) ?? previous.deviceIo, hal: (result.hal_runtime as Record<string, unknown>) ?? previous.hal, plc: (result.plc as Record<string, unknown>) ?? previous.plc, python: (result.python as Record<string, unknown>) ?? previous.python, ioAck: (result.io_ack as Record<string, unknown>) ?? previous.ioAck, axisState: (result.axis_state as AxisStateView) ?? previous.axisState, gates: (result.gates as FrontendState["gates"]) ?? previous.gates, executionTrace };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const PANEL_GATE_ACTIONS = Object.freeze({
|
|||||||
const WASM_GATE_COMMANDS = Object.freeze({
|
const WASM_GATE_COMMANDS = Object.freeze({
|
||||||
ESTOP: 1, RESET: 2, POWER: 3, HOME: 4, JOG: 5, MDI: 6, RUN: 7,
|
ESTOP: 1, RESET: 2, POWER: 3, HOME: 4, JOG: 5, MDI: 6, RUN: 7,
|
||||||
PAUSE: 8, RESUME: 9, ABORT: 10, RESTART: 11, KINEMATICS: 12,
|
PAUSE: 8, RESUME: 9, ABORT: 10, RESTART: 11, KINEMATICS: 12,
|
||||||
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15,
|
SET_MODE: 13, PROGRAM_LOAD: 14, STEP: 15, OVERRIDE: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
const WASM_GATE_REASONS = Object.freeze({
|
const WASM_GATE_REASONS = Object.freeze({
|
||||||
@@ -228,7 +228,7 @@ export class SimSession {
|
|||||||
axes: { x: position.x, y: position.y, z: position.z },
|
axes: { x: position.x, y: position.y, z: position.z },
|
||||||
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
|
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
|
||||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||||
feed_override: 1,
|
feed_override: this.wasmMotion.controls?.feed_override ?? 1,
|
||||||
spindle_override: 1,
|
spindle_override: 1,
|
||||||
homed: Boolean(home.state?.homed),
|
homed: Boolean(home.state?.homed),
|
||||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(home.state?.homed && index < (home.state?.joint_count ?? 0))),
|
||||||
@@ -319,6 +319,7 @@ export class SimSession {
|
|||||||
jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }),
|
jogStart: this.xyzacCommandGate({ kind: "JOG_START", payload: {} }),
|
||||||
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
jogStop: this.xyzacCommandGate({ kind: "JOG_STOP", payload: {} }),
|
||||||
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
home: this.xyzacCommandGate({ kind: "HOME", payload: {} }),
|
||||||
|
override: this.xyzacCommandGate({ kind: "SET_OVERRIDE", payload: { feed: 1 } }),
|
||||||
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
|
estop: this.xyzacCommandGate({ kind: "ESTOP", payload: { active: true } }),
|
||||||
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
|
recover: this.xyzacCommandGate({ kind: "IO_RECOVER", payload: {} }),
|
||||||
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
|
power: this.xyzacCommandGate({ kind: "POWER", payload: { active: ioState.estop ? false : !Boolean(ioState.ready) } }),
|
||||||
@@ -439,7 +440,8 @@ export class SimSession {
|
|||||||
const ready = Boolean(io.ready) && !estop;
|
const ready = Boolean(io.ready) && !estop;
|
||||||
const loaded = program.status !== 0;
|
const loaded = program.status !== 0;
|
||||||
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
|
const action = PANEL_GATE_ACTIONS[input.kind] ?? null;
|
||||||
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" || input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.JOG
|
const command = input.kind === "JOG_STOP" || input.kind === "JOG_START" ? WASM_GATE_COMMANDS.JOG
|
||||||
|
: input.kind === "SET_OVERRIDE" ? WASM_GATE_COMMANDS.OVERRIDE
|
||||||
: input.kind === "PROGRAM_LOAD" ? WASM_GATE_COMMANDS.PROGRAM_LOAD
|
: input.kind === "PROGRAM_LOAD" ? WASM_GATE_COMMANDS.PROGRAM_LOAD
|
||||||
: input.kind === "PROGRAM_RESTART" ? WASM_GATE_COMMANDS.RESTART
|
: input.kind === "PROGRAM_RESTART" ? WASM_GATE_COMMANDS.RESTART
|
||||||
: input.kind === "PROGRAM_RUN" ? WASM_GATE_COMMANDS.RUN
|
: input.kind === "PROGRAM_RUN" ? WASM_GATE_COMMANDS.RUN
|
||||||
@@ -1813,7 +1815,13 @@ export class SimSession {
|
|||||||
if (input.kind === "SET_OVERRIDE") {
|
if (input.kind === "SET_OVERRIDE") {
|
||||||
const feed = Number(input.payload?.feed ?? input.payload?.feed_override);
|
const feed = Number(input.payload?.feed ?? input.payload?.feed_override);
|
||||||
if (!Number.isFinite(feed) || feed < 0 || feed > 2) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
if (!Number.isFinite(feed) || feed < 0 || feed > 2) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
||||||
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, state: { ...this.motion.export(), feed_override: feed }, motion: this.motionProjection(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
try {
|
||||||
|
this.wasmMotion.setFeedOverride(feed);
|
||||||
|
} catch (error) {
|
||||||
|
return finish(this.error(input, "motion_override", String(error?.message ?? error)), "failed");
|
||||||
|
}
|
||||||
|
const motion = this.wasmMotionProjection();
|
||||||
|
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, feed_override: feed, overrides: { feed, spindle: [this.wasmMotionProjection().spindle_override] }, state: motion, motion, wasm_motion: this.wasmMotion.exportState(), motion_authority: "wasm", operation_id: input.operation_id ?? null }), "verified");
|
||||||
}
|
}
|
||||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||||
const current = this.wasmMotion.read();
|
const current = this.wasmMotion.read();
|
||||||
|
|||||||
65
web/worker/test_xyzac_feed_override.mjs
Normal file
65
web/worker/test_xyzac_feed_override.mjs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||||
|
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||||
|
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||||
|
|
||||||
|
const CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini";
|
||||||
|
const fetchImpl = async (url) => {
|
||||||
|
try {
|
||||||
|
const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url));
|
||||||
|
return { ok: true, text: async () => body.toString("utf8") };
|
||||||
|
} catch {
|
||||||
|
return { ok: false, text: async () => "" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bundle = await loadSimulationCase({ path: CASE, fetchImpl });
|
||||||
|
assert.equal(bundle.status, "ready");
|
||||||
|
const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) });
|
||||||
|
const session = new SimSession(core, { sessionId: "xyzac-feed-override" });
|
||||||
|
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
|
||||||
|
let seq = 0;
|
||||||
|
const send = (kind, payload = {}) => session.accept({ ...base, kind, seq: ++seq, operation_id: `override-${seq}`, payload });
|
||||||
|
|
||||||
|
send("HELLO");
|
||||||
|
send("SESSION_ATTACH");
|
||||||
|
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }).payload.status, "verified");
|
||||||
|
assert.equal(send("PROGRAM_LOAD", { path: "feed-override.ngc", source: "G21 G90\nG1 X10 F100\nM2\n" }).payload.status, "verified");
|
||||||
|
assert.equal(send("POWER", { active: true }).payload.status, "verified");
|
||||||
|
assert.equal(send("HOME", { all: true }).payload.status, "verified");
|
||||||
|
assert.equal(send("SET_OVERRIDE", { feed: 0.5 }).payload.status, "verified");
|
||||||
|
const half = send("PROGRAM_RUN");
|
||||||
|
assert.equal(half.payload.status, "verified");
|
||||||
|
const halfStep = send("SIM_STEP", { cycles: 3 });
|
||||||
|
assert.equal(halfStep.payload.status, "verified");
|
||||||
|
assert.equal(session.wasmMotion.controls.feed_override, 0.5);
|
||||||
|
assert.equal(halfStep.payload.panel_snapshot.overrides.feed, 0.5);
|
||||||
|
assert.ok(halfStep.payload.wasm_motion.state.velocity > 0.82 && halfStep.payload.wasm_motion.state.velocity < 0.85, JSON.stringify(halfStep.payload.wasm_motion.state));
|
||||||
|
|
||||||
|
const fullOverride = send("SET_OVERRIDE", { feed: 2 });
|
||||||
|
assert.equal(fullOverride.payload.status, "verified");
|
||||||
|
assert.equal(fullOverride.payload.panel_snapshot.overrides.feed, 2);
|
||||||
|
const fullStep = send("SIM_STEP", { cycles: 1 });
|
||||||
|
assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||||
|
assert.ok(fullStep.payload.wasm_motion.state.velocity > 3.32 && fullStep.payload.wasm_motion.state.velocity < 3.35, JSON.stringify(fullStep.payload.wasm_motion.state));
|
||||||
|
|
||||||
|
const invalid = send("SET_OVERRIDE", { feed: 2.01 });
|
||||||
|
assert.equal(invalid.kind, "ERROR");
|
||||||
|
assert.equal(invalid.payload.code, "motion_override");
|
||||||
|
|
||||||
|
const checkpoint = send("CHECKPOINT");
|
||||||
|
assert.equal(checkpoint.payload.status, "verified");
|
||||||
|
const recovered = send("RECOVER", { checkpoint: checkpoint.payload.checkpoint });
|
||||||
|
assert.ok(["verified", "recovered"].includes(recovered.payload.status));
|
||||||
|
assert.equal(session.wasmMotion.controls.feed_override, 2);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
schema: "xyzac-feed-override.v1",
|
||||||
|
status: "verified",
|
||||||
|
authority: "linuxcnc-wasm-motion-queue",
|
||||||
|
half: { requested: 50, base_feed_mm_min: 100, actual: halfStep.payload.wasm_motion.state.velocity, panel: halfStep.payload.panel_snapshot.overrides.feed },
|
||||||
|
full: { requested: 200, base_feed_mm_min: 100, actual: fullStep.payload.wasm_motion.state.velocity, panel: fullStep.payload.panel_snapshot.overrides.feed },
|
||||||
|
invalid: invalid.payload.message_key,
|
||||||
|
recovery_feed: session.wasmMotion.controls.feed_override,
|
||||||
|
}));
|
||||||
@@ -6,7 +6,7 @@ const SEGMENT_INT_OFFSET = SEGMENT_DOUBLES * 2;
|
|||||||
const QUEUE_HEADER_BYTES = 32;
|
const QUEUE_HEADER_BYTES = 32;
|
||||||
const STATE_POSITION_BYTES = 72;
|
const STATE_POSITION_BYTES = 72;
|
||||||
const MOTION_QUEUE_SCHEMA = "sim_motion_queue_state.v1";
|
const MOTION_QUEUE_SCHEMA = "sim_motion_queue_state.v1";
|
||||||
const DEFAULT_CONTROLS = Object.freeze({ profile: 0, lookahead: 1, acceleration_limit: 1e9, jerk_limit: 1e12, paused: false });
|
const DEFAULT_CONTROLS = Object.freeze({ profile: 0, lookahead: 1, acceleration_limit: 1e9, jerk_limit: 1e12, feed_override: 1, paused: false });
|
||||||
const MOTION_ERROR_REASONS = Object.freeze({
|
const MOTION_ERROR_REASONS = Object.freeze({
|
||||||
[-600]: "wasm_motion_invalid_argument", [-601]: "wasm_motion_nonfinite_value", [-602]: "wasm_motion_limit_range",
|
[-600]: "wasm_motion_invalid_argument", [-601]: "wasm_motion_nonfinite_value", [-602]: "wasm_motion_limit_range",
|
||||||
[-603]: "wasm_motion_invalid_state", [-604]: "wasm_motion_queue_full", [-605]: "wasm_motion_paused",
|
[-603]: "wasm_motion_invalid_state", [-604]: "wasm_motion_queue_full", [-605]: "wasm_motion_paused",
|
||||||
@@ -53,9 +53,10 @@ function controls(value = {}) {
|
|||||||
lookahead: value.lookahead ?? DEFAULT_CONTROLS.lookahead,
|
lookahead: value.lookahead ?? DEFAULT_CONTROLS.lookahead,
|
||||||
acceleration_limit: value.acceleration_limit ?? DEFAULT_CONTROLS.acceleration_limit,
|
acceleration_limit: value.acceleration_limit ?? DEFAULT_CONTROLS.acceleration_limit,
|
||||||
jerk_limit: value.jerk_limit ?? DEFAULT_CONTROLS.jerk_limit,
|
jerk_limit: value.jerk_limit ?? DEFAULT_CONTROLS.jerk_limit,
|
||||||
|
feed_override: value.feed_override ?? DEFAULT_CONTROLS.feed_override,
|
||||||
paused: value.paused ?? DEFAULT_CONTROLS.paused,
|
paused: value.paused ?? DEFAULT_CONTROLS.paused,
|
||||||
};
|
};
|
||||||
if (![0, 1].includes(result.profile) || !Number.isSafeInteger(result.lookahead) || result.lookahead < 1 || !Number.isFinite(result.acceleration_limit) || result.acceleration_limit <= 0 || !Number.isFinite(result.jerk_limit) || result.jerk_limit <= 0 || typeof result.paused !== "boolean") throw new Error("wasm_motion_controls_invalid");
|
if (![0, 1].includes(result.profile) || !Number.isSafeInteger(result.lookahead) || result.lookahead < 1 || !Number.isFinite(result.acceleration_limit) || result.acceleration_limit <= 0 || !Number.isFinite(result.jerk_limit) || result.jerk_limit <= 0 || !Number.isFinite(result.feed_override) || result.feed_override < 0 || result.feed_override > 2 || typeof result.paused !== "boolean") throw new Error("wasm_motion_controls_invalid");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,19 +102,22 @@ export class WasmMotionQueue {
|
|||||||
[...poseToArray(segment.start), ...poseToArray(segment.end), Number(segment.center?.x ?? 0), Number(segment.center?.y ?? 0), Number(segment.center?.z ?? 0), Number(segment.normal?.x ?? 0), Number(segment.normal?.y ?? 0), Number(segment.normal?.z ?? 0), Number(segment.velocity ?? 0), Number(segment.acceleration ?? 0)].forEach((value, index) => { doubles[base + index] = value; });
|
[...poseToArray(segment.start), ...poseToArray(segment.end), Number(segment.center?.x ?? 0), Number(segment.center?.y ?? 0), Number(segment.center?.z ?? 0), Number(segment.normal?.x ?? 0), Number(segment.normal?.y ?? 0), Number(segment.normal?.z ?? 0), Number(segment.velocity ?? 0), Number(segment.acceleration ?? 0)].forEach((value, index) => { doubles[base + index] = value; });
|
||||||
ints[this.command / 4 + SEGMENT_INT_OFFSET] = Number(segment.motion_type ?? 1);
|
ints[this.command / 4 + SEGMENT_INT_OFFSET] = Number(segment.motion_type ?? 1);
|
||||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 1] = Number(segment.turn ?? 0);
|
ints[this.command / 4 + SEGMENT_INT_OFFSET + 1] = Number(segment.turn ?? 0);
|
||||||
// sim_motion_command layout is segment (216 bytes), id (4), tag (4).
|
// sim_motion_command layout is segment (216 bytes), id (4), tag (4), flags (4).
|
||||||
// Keep the WASM switchkins mode in the tag field; the C queue assigns id.
|
// Keep the WASM switchkins mode in the tag field; the C queue assigns id.
|
||||||
ints[this.command / 4 + SEGMENT_INT_OFFSET + 3] = Number(segment.kinematics_mode ?? segment.tag ?? 0);
|
ints[this.command / 4 + SEGMENT_INT_OFFSET + 3] = Number(segment.kinematics_mode ?? segment.tag ?? 0);
|
||||||
|
ints[this.command / 4 + SEGMENT_INT_OFFSET + 4] = segment.kind === "rapid" ? 1 : 0;
|
||||||
const result = call(this.core, "sim_motion_enqueue", "number", ["number", "number", "number"], [this.queue, this.command, 0]);
|
const result = call(this.core, "sim_motion_enqueue", "number", ["number", "number", "number"], [this.queue, this.command, 0]);
|
||||||
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, source: "linuxcnc-wasm-motion" };
|
return result === 0 ? { status: "verified", source_line: segment.source_line ?? null } : { status: "error", code: `wasm_motion_enqueue_${result}`, reason: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, message_key: MOTION_ERROR_REASONS[result] ?? `wasm_motion_enqueue_${result}`, source: "linuxcnc-wasm-motion" };
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
if (!this.available) return this.read();
|
if (!this.available) return this.read();
|
||||||
|
const previousFeedOverride = this.controls.feed_override;
|
||||||
this.core.HEAPU8.fill(0, this.queue, this.queue + this.queueSize);
|
this.core.HEAPU8.fill(0, this.queue, this.queue + this.queueSize);
|
||||||
const result = call(this.core, "sim_motion_init", "number", ["number", "number", "number"], [this.queue, this.cycleTime, this.velocityLimit]);
|
const result = call(this.core, "sim_motion_init", "number", ["number", "number", "number"], [this.queue, this.cycleTime, this.velocityLimit]);
|
||||||
if (result !== 0) throw new Error(`wasm_motion_reset_${result}`);
|
if (result !== 0) throw new Error(`wasm_motion_reset_${result}`);
|
||||||
this.controls = controls();
|
this.controls = controls();
|
||||||
|
this.setFeedOverride(previousFeedOverride);
|
||||||
return this.read();
|
return this.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +148,15 @@ export class WasmMotionQueue {
|
|||||||
return this.read();
|
return this.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFeedOverride(feedOverride) {
|
||||||
|
const next = controls({ ...this.controls, feed_override: feedOverride });
|
||||||
|
if (!this.available) { this.controls = next; return this.read(); }
|
||||||
|
const result = optionalCall(this.core, "sim_motion_set_feed_override", "number", ["number", "number"], [this.queue, next.feed_override]);
|
||||||
|
if (result !== null && result !== 0) throw new Error(`wasm_motion_feed_override_${result}`);
|
||||||
|
this.controls = next;
|
||||||
|
return this.read();
|
||||||
|
}
|
||||||
|
|
||||||
setPaused(paused) {
|
setPaused(paused) {
|
||||||
const next = controls({ ...this.controls, paused: Boolean(paused) });
|
const next = controls({ ...this.controls, paused: Boolean(paused) });
|
||||||
if (!this.available) { this.controls = next; return this.read(); }
|
if (!this.available) { this.controls = next; return this.read(); }
|
||||||
@@ -158,11 +171,13 @@ export class WasmMotionQueue {
|
|||||||
const current = this.read();
|
const current = this.read();
|
||||||
if (current.status !== "verified") throw new Error("wasm_motion_abort_state");
|
if (current.status !== "verified") throw new Error("wasm_motion_abort_state");
|
||||||
const previous = this.core.HEAPU8.slice(this.queue, this.queue + this.queueSize);
|
const previous = this.core.HEAPU8.slice(this.queue, this.queue + this.queueSize);
|
||||||
|
const previousFeedOverride = this.controls.feed_override;
|
||||||
const headerOffset = this.queueCapacity * this.commandCapacity;
|
const headerOffset = this.queueCapacity * this.commandCapacity;
|
||||||
const stateOffset = headerOffset + QUEUE_HEADER_BYTES;
|
const stateOffset = headerOffset + QUEUE_HEADER_BYTES;
|
||||||
const previousView = new DataView(previous.buffer, previous.byteOffset, previous.byteLength);
|
const previousView = new DataView(previous.buffer, previous.byteOffset, previous.byteLength);
|
||||||
const nextId = previousView.getUint32(headerOffset + 8, true);
|
const nextId = previousView.getUint32(headerOffset + 8, true);
|
||||||
this.reset();
|
this.reset();
|
||||||
|
this.setFeedOverride(previousFeedOverride);
|
||||||
const bytes = new Uint8Array(this.queueSize);
|
const bytes = new Uint8Array(this.queueSize);
|
||||||
const view = new DataView(bytes.buffer);
|
const view = new DataView(bytes.buffer);
|
||||||
// sim_motion_queue stores commands first, followed by its 32-byte header.
|
// sim_motion_queue stores commands first, followed by its 32-byte header.
|
||||||
@@ -239,6 +254,7 @@ export class WasmMotionQueue {
|
|||||||
this.setProfile(nextControls.profile);
|
this.setProfile(nextControls.profile);
|
||||||
this.setLookahead(nextControls.lookahead);
|
this.setLookahead(nextControls.lookahead);
|
||||||
this.setDynamics(nextControls.acceleration_limit, nextControls.jerk_limit);
|
this.setDynamics(nextControls.acceleration_limit, nextControls.jerk_limit);
|
||||||
|
this.setFeedOverride(nextControls.feed_override);
|
||||||
this.setPaused(nextControls.paused);
|
this.setPaused(nextControls.paused);
|
||||||
this.core.HEAPU8.set(bytes, this.queue);
|
this.core.HEAPU8.set(bytes, this.queue);
|
||||||
const actual = this.read();
|
const actual = this.read();
|
||||||
|
|||||||
Reference in New Issue
Block a user