feat: sync latest run execution updates

This commit is contained in:
2026-06-22 21:47:16 -04:00
parent 0b1aad39e1
commit 8d3177cb73
92 changed files with 22837 additions and 246 deletions

View File

@@ -93,6 +93,17 @@ export function wrapTaskHalSdk(sdk, {
return rc;
},
loadProgramMotionPlan(plan = {}) {
if (typeof sdk.loadProgramMotionPlan !== "function") {
throw new Error("task/HAL SDK missing loadProgramMotionPlan; rebuild wasm-port/tools/build_task_hal_wasm.sh");
}
const rc = sdk.loadProgramMotionPlan(plan);
if (rc !== 0) {
throw new Error(`lctask_load_program_motion_plan_json failed rc=${rc}`);
}
return rc;
},
sendCommand(command) {
const rc = sdk.sendCommand(command);
if (rc !== 0) {
@@ -152,10 +163,65 @@ export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selec
};
}
export function buildTaskHalProgramMotionPlan({
programPath = null,
motion = [],
timing = null,
linearUnits = "mm",
programLines = [],
} = {}) {
const segments = Array.isArray(timing?.segments) ? timing.segments : [];
let planSegments = segments.map((segment, index) => {
const event = motion[index] || {};
const startAxes = normalizePlanAxes(segment.startAxes || motion[index - 1]?.axes || {});
const endAxes = normalizePlanAxes(segment.endAxes || segment.axes || event.axes || startAxes);
return {
line: Number(segment.line ?? event.line ?? index + 1),
type: segment.type || event.type || "STRAIGHT_FEED",
motionClass: segment.motionClass || (event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed"),
feedMode: segment.feedMode || event.feedMode || "units-per-minute",
startSeconds: Number(segment.startSeconds || 0),
durationSeconds: Math.max(Number(segment.durationSeconds || 0), 0),
elapsedSeconds: Number(segment.elapsedSeconds || 0),
feedRate: Number(segment.feedRate || event.feedRate || 0),
linearUnits: segment.linearUnits || event.linearUnits || linearUnits,
velocityMmPerMin: Math.max(Number(segment.velocityMmPerMin || 0), 0),
requestedVelocityMmPerMin: Math.max(Number(segment.requestedVelocityMmPerMin || segment.velocityMmPerMin || 0), 0),
startAxes,
endAxes,
};
}).filter((segment) => segment.durationSeconds > 0 || segment.line > 0);
const lineSegments = buildSourceLineMotionSegments({
programLines,
seedSegments: planSegments,
linearUnits,
});
if (shouldUseSourceLineSegments(planSegments, lineSegments)) {
planSegments = lineSegments;
}
return {
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
semanticBoundary: "linuxcnc_canonical_motion_feed_timed_task_hal_plan",
programPath,
linearUnits,
totalSeconds: Number(timing?.totalSeconds || 0),
segmentCount: planSegments.length,
segments: planSegments,
};
}
export function normalizeTaskHalStatus(status = {}) {
const motion = status.motionStatus?.motion || {};
const axis = status.motionStatus?.axis || {};
const halPins = status.halSnapshot?.pins || {};
const motionProgramLine = Number(motion.programLine || 0);
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
const activeLine = motionProgramLine > 0
? motionProgramLine
: halProgramLine > 0
? halProgramLine
: 1;
return {
...status,
semanticBoundary: SEMANTIC_BOUNDARY,
@@ -183,7 +249,13 @@ export function normalizeTaskHalStatus(status = {}) {
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
? status.halSnapshot.changedPins.length
: Number(status.halSnapshot?.changedPinCount || 0),
activeLine: Number(motion.programLine || halPins["motion.program-line"]?.value || 1),
activeLine,
motionProgramLine,
halProgramLine,
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
? motionProgramLine === halProgramLine
: false,
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
axisPose: {
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
@@ -199,6 +271,133 @@ export function normalizeTaskHalStatus(status = {}) {
};
}
function shouldUseSourceLineSegments(planSegments, lineSegments) {
if (lineSegments.length < 3) return false;
const plannedLines = new Set(planSegments.map((segment) => Number(segment.line)).filter(Number.isFinite));
const lineCount = lineSegments.length;
return plannedLines.size <= 2 && lineCount > plannedLines.size;
}
function buildSourceLineMotionSegments({
programLines = [],
seedSegments = [],
linearUnits = "mm",
} = {}) {
if (!Array.isArray(programLines) || programLines.length === 0) return [];
const seedByLine = new Map(seedSegments.map((segment) => [Number(segment.line), segment]));
const axes = normalizePlanAxes(seedSegments[0]?.startAxes || {});
const segments = [];
let elapsedSeconds = 0;
let feedRate = firstPositive(seedSegments.map((segment) => segment.feedRate), 100);
let rapidVelocity = firstPositive(
seedSegments.filter((segment) => segment.motionClass === "rapid").map((segment) => segment.velocityMmPerMin),
2100,
);
for (let index = 0; index < programLines.length; index += 1) {
const line = index + 1;
const code = stripSourceLineComments(programLines[index]);
if (!isExecutableGcodeLine(code)) continue;
const seed = seedByLine.get(line) || null;
const startAxes = normalizePlanAxes(seed?.startAxes || axes);
const parsed = parseGcodeLineMotion(code, startAxes, feedRate);
if (parsed.feedRate > 0) feedRate = parsed.feedRate;
const motionClass = seed?.motionClass || parsed.motionClass;
const endAxes = normalizePlanAxes(seed?.endAxes || parsed.endAxes);
const velocityMmPerMin = Number(seed?.velocityMmPerMin) > 0
? Number(seed.velocityMmPerMin)
: motionClass === "rapid"
? rapidVelocity
: Math.max(feedRate, 1);
if (motionClass === "rapid" && velocityMmPerMin > 0) {
rapidVelocity = velocityMmPerMin;
}
const durationSeconds = Math.max(
Number(seed?.durationSeconds || 0),
estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin),
0.05,
);
const segment = {
line,
type: seed?.type || parsed.type,
motionClass,
feedMode: seed?.feedMode || parsed.feedMode,
startSeconds: elapsedSeconds,
durationSeconds,
elapsedSeconds: elapsedSeconds + durationSeconds,
feedRate,
linearUnits: seed?.linearUnits || linearUnits,
velocityMmPerMin,
requestedVelocityMmPerMin: Number(seed?.requestedVelocityMmPerMin || velocityMmPerMin),
startAxes,
endAxes,
};
segments.push(segment);
Object.assign(axes, endAxes);
elapsedSeconds += durationSeconds;
}
return segments;
}
function parseGcodeLineMotion(code, startAxes, currentFeedRate) {
const numberPattern = "[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)";
const gCodes = [...code.matchAll(new RegExp(`\\bG\\s*(${numberPattern})\\b`, "gi"))].map((match) => Number(match[1]));
const feedMode = gCodes.includes(93) ? "inverse-time" : "units-per-minute";
const rapid = gCodes.includes(0);
const feed = gCodes.some((value) => value === 1 || value === 2 || value === 3);
const endAxes = { ...startAxes };
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
const match = code.match(new RegExp(`\\b${axis}\\s*(${numberPattern})`, "i"));
if (match) endAxes[axis] = Number(match[1]);
}
const feedMatch = code.match(new RegExp(`\\bF\\s*(${numberPattern})`, "i"));
const feedRate = feedMatch && Number(feedMatch[1]) > 0 ? Number(feedMatch[1]) : Number(currentFeedRate || 0);
return {
type: rapid ? "STRAIGHT_TRAVERSE" : "STRAIGHT_FEED",
motionClass: rapid && !feed ? "rapid" : "feed",
feedMode,
feedRate,
endAxes,
};
}
function stripSourceLineComments(line) {
return String(line || "")
.replace(/\([^)]*\)/g, " ")
.replace(/;.*$/g, " ")
.trim();
}
function isExecutableGcodeLine(code) {
if (!code || code === "%") return false;
return /\b[GMTXYZABCUVWF]\s*[-+]?\d/i.test(code);
}
function estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin) {
const distance = Math.sqrt(["x", "y", "z"].reduce((total, axis) => {
const delta = Number(endAxes[axis] || 0) - Number(startAxes[axis] || 0);
return total + delta * delta;
}, 0));
if (distance <= 0 || velocityMmPerMin <= 0) return 0;
return distance / Math.max(velocityMmPerMin / 60, 0.000001);
}
function firstPositive(values, fallback) {
for (const value of values) {
const number = Number(value);
if (Number.isFinite(number) && number > 0) return number;
}
return fallback;
}
function normalizePlanAxes(axes = {}) {
return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => [
axis,
Number.isFinite(Number(axes[axis])) ? Number(axes[axis]) : 0,
]));
}
function isJogMotion(motion = {}) {
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
}