提交 xyzbc-trt 界面与验证更新

This commit is contained in:
mes123456
2026-07-02 20:25:37 -04:00
parent 68ecd05353
commit 370c344b96
868 changed files with 275426 additions and 39640 deletions

View File

@@ -14,6 +14,8 @@ DEFAULT_SOURCE_ROOT = "/home/mes123456/cnc_wams/linuxcnc"
DEFAULT_INI = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"
DEFAULT_PROGRAM = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"
DEFAULT_OUTPUT = "/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/native-xyzbc-trt-evidence.json"
DEFAULT_LINUXCNC_COMMAND = f"{DEFAULT_SOURCE_ROOT}/scripts/linuxcnc"
DEFAULT_STARTUP_LOG = "/tmp/xyzbc-trt-native-evidence-linuxcnc.log"
XYZBC_DEFAULT_TOOL = {
"id": 2,
"pocket": 2,
@@ -29,6 +31,8 @@ def main():
parser.add_argument("--output", default=DEFAULT_OUTPUT)
parser.add_argument("--run", action="store_true", help="Optionally execute the program through linuxcnc.command().")
parser.add_argument("--timeout", type=float, default=60.0)
parser.add_argument("--startup-timeout", type=float, default=35.0)
parser.add_argument("--no-autostart", action="store_true", help="Do not start LinuxCNC when no status buffer is available.")
args = parser.parse_args()
try:
@@ -47,18 +51,43 @@ def main():
})
return 2
stat = linuxcnc.stat()
command = linuxcnc.command()
error_channel = linuxcnc.error_channel()
runtime_process = None
startup = {
"autostartRequested": bool(args.run and not args.no_autostart),
"startedByCollector": False,
"logPath": DEFAULT_STARTUP_LOG,
}
try:
stat, command, error_channel, before = connect_linuxcnc_channels(linuxcnc)
except Exception as exc:
if args.run and not args.no_autostart:
runtime_process = start_linuxcnc_runtime(args.ini)
startup.update({
"startedByCollector": True,
"pid": runtime_process.pid,
"command": [DEFAULT_LINUXCNC_COMMAND, args.ini],
})
try:
stat, command, error_channel, before = wait_for_linuxcnc_channels(linuxcnc, args.startup_timeout)
except Exception as startup_exc:
cleanup_started_runtime(runtime_process)
write_connection_blocked_json(args, exc, startup_exc, startup)
return 2
else:
write_connection_blocked_json(args, exc, None, startup)
return 2
before = poll_stat(stat)
command_result = None
if args.run:
command_result = run_program(linuxcnc, stat, command, pathlib.Path(args.program), args.timeout)
try:
command_result = None
if args.run:
command_result = run_program(linuxcnc, stat, command, pathlib.Path(args.program), args.timeout)
after = poll_stat(stat)
hal = collect_hal_snapshot()
errors = drain_errors(error_channel)
after = poll_stat(stat)
hal = collect_hal_snapshot()
errors = drain_errors(error_channel)
finally:
if runtime_process is not None:
cleanup_started_runtime(runtime_process)
preview_path = collect_native_preview_path(pathlib.Path(args.program))
execution_path = execution_path_from_command(command_result)
@@ -77,6 +106,7 @@ def main():
"linuxcncRuntime": {
"pythonApi": True,
"processes": list_processes(),
"startup": startup,
},
"before": before,
"after": after,
@@ -169,6 +199,82 @@ def run_program(linuxcnc, stat, command, program_path, timeout):
return result
def connect_linuxcnc_channels(linuxcnc):
stat = linuxcnc.stat()
command = linuxcnc.command()
error_channel = linuxcnc.error_channel()
before = poll_stat(stat)
return stat, command, error_channel, before
def wait_for_linuxcnc_channels(linuxcnc, timeout):
deadline = time.time() + timeout
last_error = None
while time.time() < deadline:
try:
return connect_linuxcnc_channels(linuxcnc)
except Exception as exc:
last_error = exc
time.sleep(0.5)
if last_error is not None:
raise last_error
raise RuntimeError("linuxcnc status buffer was not available before timeout")
def start_linuxcnc_runtime(ini_path):
log_path = pathlib.Path(DEFAULT_STARTUP_LOG)
log_file = log_path.open("w", encoding="utf-8")
try:
return subprocess.Popen(
[DEFAULT_LINUXCNC_COMMAND, str(ini_path)],
cwd=DEFAULT_SOURCE_ROOT,
stdin=subprocess.DEVNULL,
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
)
finally:
log_file.close()
def cleanup_started_runtime(process):
if process.poll() is not None:
return
try:
process.terminate()
process.wait(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
except Exception:
pass
def write_connection_blocked_json(args, connect_error, startup_error, startup):
payload = {
"apiName": "xyzbc-trt-native-linuxcnc-evidence",
"status": "blocked",
"blocker": "linuxcnc_status_buffer_unavailable",
"error": f"{type(connect_error).__name__}: {connect_error}",
"startupError": None if startup_error is None else f"{type(startup_error).__name__}: {startup_error}",
"hint": "Start xyzbc-trt LinuxCNC or run with --run so the collector can autostart it.",
"sourceRoot": DEFAULT_SOURCE_ROOT,
"iniPath": args.ini,
"programPath": args.program,
"linuxcncRuntime": {
"pythonApi": True,
"processes": list_processes(),
"startup": startup,
},
"pathSampling": create_path_sampling(),
"previewPath": collect_native_preview_path(pathlib.Path(args.program)),
"executionPath": empty_path("linuxcnc-stat", "linuxcnc status buffer was unavailable"),
}
write_json(args.output, payload)
print(f"native_xyzbc_trt_evidence={args.output}")
print("native_xyzbc_trt_status=blocked blocker=linuxcnc_status_buffer_unavailable")
def poll_stat(stat):
stat.poll()
return {
@@ -464,7 +570,7 @@ def build_xyzbc_switchkins_segments(params):
"radius": radius,
"turns": turns,
})
pose = end
pose = dict(end)
quadrant_centers = [
(distance, distance, 18),

View File

@@ -18,6 +18,8 @@ import {
import { createSimulationStore } from "../app/src/state/store.js";
import { getFiveAxisProfile } from "../app/src/profiles/index.js";
import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js";
import { buildAxisPreviewPathFromProgram } from "../app/src/runtime/axis-preview-path.js";
import { AXIS_BUTTON_PARITY } from "../app/src/ui/axis-shell.js";
const SAMPLE_PERIOD_MS = 20;
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
@@ -171,6 +173,7 @@ const evidence = {
basicSimEquivalent: taskHalEquivalence.ready === true,
ngcguiSubroutinesExecutable: ngcguiExecution.ready === true,
nativeStateFlowRechecked: semanticFields.nativeStateFlowReview?.ready === true,
axisButtonParityCovered: axisMainUi.axisButtonParity?.ready === true,
toolTableToToolOffsetClosed: toolRuntime.ready
&& toolRuntime.toolTable.toolCount > 0
&& toolRuntime.activeOffsetApplied
@@ -274,7 +277,7 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
staged,
selectedPlan,
});
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool);
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool, selectedPlan, staged);
return {
previewPath,
executionPath: wasmArtifacts.ready
@@ -289,7 +292,10 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
}
}
function pathFromWebMotion(execution, profile, pathTool = null) {
function pathFromWebMotion(execution, profile, pathTool = null, selectedPlan = null, staged = null) {
const expandedPreview = pathFromAxisPreviewExpansion({ selectedPlan, staged, pathTool });
if (expandedPreview) return expandedPreview;
const plannerSamples = execution.plannerTiming?.samples || [];
const motion = execution.motion || [];
const motionByIndex = new Map(motion.map((event, index) => [index, event]));
@@ -319,6 +325,26 @@ function pathFromWebMotion(execution, profile, pathTool = null) {
};
}
function pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) {
const selectedProgramFilename = selectedPlan?.selectedProgramFilename || "";
if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null;
const programFile = staged?.save?.files?.find((file) => (
file.sourceRel === selectedPlan.selectedProgramSourceRel
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
));
return buildAxisPreviewPathFromProgram({
filename: selectedProgramFilename,
sourceRel: selectedPlan?.selectedProgramSourceRel,
content: programFile?.text || "",
tool: pathTool || {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
},
});
}
function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) {
if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return [];
const normalized = plannerSamples
@@ -782,6 +808,63 @@ function buildSemanticFields({
function buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticFields }) {
const taskPolicy = state.linuxCncTaskPolicy || {};
const requiredAxisButtonActions = [
"estop",
"power",
"open",
"reload",
"run-ready",
"run",
"pause",
"resume",
"step",
"stop",
"home-all",
"jog-minus",
"jog-plus",
"touch-off",
"tool-touch-off",
"spindle-forward",
"spindle-stop",
"spindle-reverse",
"feed-override-down",
"feed-override-up",
"rapid-override-down",
"rapid-override-up",
"spindle-override-down",
"spindle-override-up",
"ignore-limits",
"block-delete",
"optional-stop",
"toggle-flood",
"toggle-mist",
"mdi-form",
"mdi-history",
"kins-identity",
"kins-tcp",
"kins-userk",
"clear-preview",
"view-x",
"view-y",
"view-z",
"view-p",
];
const coveredActions = new Set(AXIS_BUTTON_PARITY.map((item) => item.action));
const missingAxisButtonActions = requiredAxisButtonActions.filter((action) => !coveredActions.has(action));
const axisButtonParity = {
sourceFile: "/home/mes123456/cnc_wams/linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
pyvcpSourceFiles: [
"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml",
"configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
],
requiredActions: requiredAxisButtonActions,
coveredActions: Array.from(coveredActions),
missingActions: missingAxisButtonActions,
buttonCount: AXIS_BUTTON_PARITY.length,
sourceReferencedCount: AXIS_BUTTON_PARITY.filter((item) => item.sourceSymbol && item.sourceLines).length,
ready: missingAxisButtonActions.length === 0
&& AXIS_BUTTON_PARITY.every((item) => item.sourceSymbol && item.sourceLines && item.expected),
};
const requiredSections = [
{ id: "preview", section: "preview-toolpath-and-machine-model" },
{ id: "dro", section: "coordinates-dro" },
@@ -844,12 +927,14 @@ function buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticF
ready: taskPolicy.canRunAuto !== undefined
&& taskPolicy.canExecuteMdi !== undefined
&& taskPolicy.canJog !== undefined
&& taskPolicy.canHome !== undefined,
&& taskPolicy.canHome !== undefined
&& axisButtonParity.ready,
evidence: {
canRunAuto: taskPolicy.canRunAuto,
canExecuteMdi: taskPolicy.canExecuteMdi,
canJog: taskPolicy.canJog,
canHome: taskPolicy.canHome,
axisButtonParity,
},
},
];
@@ -860,6 +945,8 @@ function buildAxisMainUiEvidence({ state, profile, paths, toolRuntime, semanticF
profileId: state.machineProfile,
coordinates: state.profile?.traj?.coordinates,
requiredSections,
axisButtonParity,
buttons: AXIS_BUTTON_PARITY,
capabilities,
missingCapabilities,
ready: missingCapabilities.length === 0,

View File

@@ -7,6 +7,9 @@ const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const nativePath = process.argv[2] || resolve(projectRoot, "working/evidence/native-xyzbc-trt-evidence.json");
const webPath = process.argv[3] || resolve(projectRoot, "working/evidence/web-xyzbc-trt-evidence.json");
const outputPath = process.argv[4] || resolve(projectRoot, "working/evidence/compare-xyzbc-trt-evidence.json");
const PREVIEW_TCP_MAX_ERROR_MM = 0.001;
const PREVIEW_JOINT_MAX_ERROR = 0.001;
const PREVIEW_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const nativeEvidence = JSON.parse(await readFile(nativePath, "utf8"));
const webEvidence = JSON.parse(await readFile(webPath, "utf8"));
@@ -88,6 +91,9 @@ const checks = [
check("ui", "web AXIS first screen exposes program, coordinates, status, MDI/switchkins, override, tool, preview, and execution", webEvidence.coverage?.axisMainUiEquivalent === true, {
axisMainUi: webEvidence.axisMainUi,
}),
check("ui", "web AXIS/PyVCP buttons are source-referenced and covered by a button-level parity matrix", webEvidence.coverage?.axisButtonParityCovered === true, {
axisButtonParity: webEvidence.axisMainUi?.axisButtonParity,
}),
check("ui", "web UI state flow, buttons, HAL pins, and paths are rechecked against native xyzbc-trt runtime", webEvidence.coverage?.nativeStateFlowRechecked === true && nativeEvidence.coverage?.taskStateFlowReadable === true, {
nativeTaskStateFlow: nativeEvidence.taskStateFlow,
webNativeStateFlowReview: webEvidence.nativeStateFlowReview,
@@ -101,6 +107,14 @@ const checks = [
webSampleCount: pathComparison.previewVsPreview.webSampleCount,
unavailable: pathComparison.previewVsPreview.unavailable,
}),
check("path-preview", "native and web preview tool paths are geometrically aligned", pathComparison.previewVsPreview.geometricAligned === true, {
thresholds: pathComparison.previewVsPreview.thresholds,
maxTcpErrorMm: pathComparison.previewVsPreview.maxTcpErrorMm,
rmsTcpErrorMm: pathComparison.previewVsPreview.rmsTcpErrorMm,
maxJointError: pathComparison.previewVsPreview.maxJointError,
maxToolAxisAngleDeg: pathComparison.previewVsPreview.maxToolAxisAngleDeg,
sampleCountDelta: pathComparison.previewVsPreview.sampleCountDelta,
}),
check("path-execution", "native and web execution path sample period is 20ms", pathComparison.executionVsExecution.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.executionVsExecution.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.executionVsExecution.webSamplePeriodMs,
@@ -219,9 +233,26 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
...(periodsMatch ? [] : [`sample period mismatch ${leftSamplePeriodMs}/${rightSamplePeriodMs}`]),
];
const stats = comparable ? pathStats(leftSamples, rightSamples) : emptyStats(leftSamples, rightSamples);
const previewPair = leftName === "native" && rightName === "web";
const thresholds = previewPair ? {
maxTcpErrorMm: PREVIEW_TCP_MAX_ERROR_MM,
maxJointError: PREVIEW_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: PREVIEW_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
} : null;
const geometricAligned = previewPair
? comparable
&& stats.maxTcpErrorMm <= thresholds.maxTcpErrorMm
&& stats.maxJointError <= thresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= thresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= thresholds.sampleCountDelta
&& stats.missingSamples.length === 0
: comparable;
return {
status: comparable ? "pass" : "fail",
status: comparable && (!previewPair || geometricAligned) ? "pass" : "fail",
comparable,
geometricAligned,
thresholds,
periodsMatch,
[`${leftName}SamplePeriodMs`]: leftSamplePeriodMs,
[`${rightName}SamplePeriodMs`]: rightSamplePeriodMs,