const DEFAULT_ALLOWED_USER_M_CODES = { M428: { code: "M428", label: "TCP kinematics", kinsType: "tcp", switchkinsType: 1, rtcpState: "on", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc", }, M429: { code: "M429", label: "Identity kinematics", kinsType: "identity", switchkinsType: 0, rtcpState: "off", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc", }, M430: { code: "M430", label: "User kinematics", kinsType: "userk", switchkinsType: 2, rtcpState: "on", sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc", }, M128: { code: "M128", label: "controlled millturn mill-mode user-M", kinsType: "mill", switchkinsType: 0, rtcpState: "off", sourceRel: "linuxcnc millturn user-M reference", }, M129: { code: "M129", label: "controlled millturn turn-mode user-M", kinsType: "turn", switchkinsType: 1, rtcpState: "off", sourceRel: "linuxcnc millturn user-M reference", }, }; export function createControlledUserMSimulation({ allowedCodes = DEFAULT_ALLOWED_USER_M_CODES } = {}) { return { apiName: "web-rtcp-5axis-controlled-user-m-simulation", ready: true, processReady: true, processScope: "web_simulation_only", hostProcessReady: false, hostProcessExecution: false, arbitraryUserMExecution: false, allowedCodes: Object.fromEntries( Object.entries(allowedCodes).map(([code, definition]) => [normalizeMCode(code), { ...definition, code: normalizeMCode(definition.code || code), }]), ), events: [], blockedEvents: [], semanticBoundary: "controlled_user_m_web_simulation_whitelist_not_host_process", linuxCncReferences: [ "linuxcnc/src/emc/rs274ngc", "linuxcnc/src/emc/task/emctask.cc", "linuxcnc/src/emc/task/emccanon.cc", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc", ], }; } export function runControlledUserM(simulation, code, context = {}) { const state = cloneSimulation(simulation); const normalizedCode = normalizeMCode(code); const definition = state.allowedCodes[normalizedCode] || null; if (!definition) { const event = createUserMEvent({ code: normalizedCode, allowed: false, reason: "not_in_controlled_user_m_whitelist", context, }); state.blockedEvents.push(event); return { simulation: state, event, statePatch: {}, halPatch: {}, }; } const profileKinsType = definition.kinsType === "tcp" ? tcpKinsTypeForProfile(context.profile) : definition.kinsType; const halPatch = { "motion.switchkins-type": definition.switchkinsType, "motion.analog-out-03": definition.switchkinsType, }; const statePatch = { kinsType: profileKinsType, rtcpState: definition.rtcpState, }; const event = createUserMEvent({ code: normalizedCode, allowed: true, definition, context, halPatch, statePatch, }); state.events.push(event); return { simulation: state, event, statePatch, halPatch, }; } export function createControlledUserMReadiness(simulation) { const ready = simulation?.processReady === true && simulation?.processScope === "web_simulation_only"; return { apiName: "web-rtcp-5axis-controlled-user-m-readiness", ready, externalUserMProcessReady: ready, externalUserMProcessScope: ready ? "web_simulation_only" : "not_ready", hostExternalUserMProcessReady: false, hostProcessExecution: false, arbitraryUserMExecution: false, allowedCodeCount: Object.keys(simulation?.allowedCodes || {}).length, blockedEventCount: simulation?.blockedEvents?.length || 0, semanticBoundary: "external_user_m_process_ready_for_web_simulation_only", }; } export function extractControlledUserMCodesFromProgram(programText = "") { const matches = []; const lines = String(programText).split(/\r?\n/); lines.forEach((line, index) => { const stripped = line.replace(/\([^)]*\)/g, " "); for (const match of stripped.matchAll(/\bM\s*(\d{2,3})\b/gi)) { const code = normalizeMCode(`M${match[1]}`); if (Number(match[1]) >= 100 || ["M428", "M429", "M430"].includes(code)) { matches.push({ code, line: index + 1, rawLine: line, }); } } }); return matches; } export function runControlledUserMProgramScan(simulation, programText = "", context = {}) { let state = simulation; const events = []; for (const occurrence of extractControlledUserMCodesFromProgram(programText)) { const result = runControlledUserM(state, occurrence.code, { ...context, line: occurrence.line, rawLine: occurrence.rawLine, }); state = result.simulation; events.push(result.event); } return { simulation: state, events }; } function createUserMEvent({ code, allowed, definition = null, context = {}, halPatch = {}, statePatch = {}, reason = null, }) { return { apiName: "web-rtcp-5axis-controlled-user-m-event", code, allowed, reason, label: definition?.label || null, sourceRel: definition?.sourceRel || context.sourceRel || null, line: context.line || null, halPatch, statePatch, createdAt: new Date().toISOString(), semanticBoundary: allowed ? "vendored_or_whitelisted_user_m_web_simulation_event" : "arbitrary_external_user_m_blocked", promotionScope: "web_simulation_only", hostProcessExecution: false, arbitraryUserMExecution: false, }; } function tcpKinsTypeForProfile(profile) { if (profile?.id === "xyzbc-trt") return "tcp-xyzbc"; return "tcp-xyzac"; } function normalizeMCode(code) { const normalized = String(code || "").trim().toUpperCase().replace(/\s+/g, ""); const match = normalized.match(/^M0*(\d+)$/); if (!match) return normalized; return `M${Number(match[1])}`; } function cloneSimulation(simulation) { return { ...simulation, allowedCodes: { ...(simulation.allowedCodes || {}) }, events: [...(simulation.events || [])], blockedEvents: [...(simulation.blockedEvents || [])], }; }