提交 xyzbc-trt 界面与验证更新
This commit is contained in:
Binary file not shown.
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user