完成 xyzbc-trt working 任务复核
This commit is contained in:
Binary file not shown.
@@ -9,7 +9,7 @@ import time
|
||||
|
||||
|
||||
AXES = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
|
||||
SAMPLE_PERIOD_MS = 20
|
||||
SAMPLE_PERIOD_MS = 50
|
||||
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"
|
||||
@@ -91,6 +91,7 @@ def main():
|
||||
|
||||
preview_path = collect_native_preview_path(pathlib.Path(args.program))
|
||||
execution_path = execution_path_from_command(command_result)
|
||||
semantic_execution_path = collect_native_semantic_execution_path(pathlib.Path(args.program))
|
||||
task_state_flow = build_task_state_flow(before, after, command_result, hal)
|
||||
basic_sim = build_basic_sim_equivalent(before, after, command_result, hal)
|
||||
evidence = {
|
||||
@@ -116,6 +117,10 @@ def main():
|
||||
"pathSampling": create_path_sampling(),
|
||||
"previewPath": preview_path,
|
||||
"executionPath": execution_path,
|
||||
"semanticExecutionPath": semantic_execution_path,
|
||||
"lineExecutionTrace": semantic_execution_path.get("lineExecutionTrace", []),
|
||||
"axisValuesByLine": semantic_execution_path.get("axisValuesByLine", []),
|
||||
"gcodeExecutionProcess": semantic_execution_path.get("gcodeExecutionProcess"),
|
||||
"taskStateFlow": task_state_flow,
|
||||
"buttonInterlocks": build_button_interlocks(after, task_state_flow),
|
||||
"basicSimEquivalent": basic_sim,
|
||||
@@ -145,6 +150,10 @@ def main():
|
||||
"positionReadable": bool(after.get("position")),
|
||||
"previewPathAvailable": preview_path.get("sampleCount", 0) > 0,
|
||||
"executionPathAvailable": execution_path.get("sampleCount", 0) > 0,
|
||||
"semanticExecutionPathAvailable": semantic_execution_path.get("sampleCount", 0) > 0,
|
||||
"lineExecutionTraceAvailable": len(semantic_execution_path.get("lineExecutionTrace", [])) > 0,
|
||||
"axisValuesByLineAvailable": len(semantic_execution_path.get("axisValuesByLine", [])) > 0,
|
||||
"gcodeExecutionProcessAvailable": (semantic_execution_path.get("gcodeExecutionProcess") or {}).get("status") == "ok",
|
||||
"taskStateFlowReadable": task_state_flow.get("ready") is True,
|
||||
"basicSimReadable": basic_sim.get("ready") is True,
|
||||
},
|
||||
@@ -269,6 +278,7 @@ def write_connection_blocked_json(args, connect_error, startup_error, startup):
|
||||
"pathSampling": create_path_sampling(),
|
||||
"previewPath": collect_native_preview_path(pathlib.Path(args.program)),
|
||||
"executionPath": empty_path("linuxcnc-stat", "linuxcnc status buffer was unavailable"),
|
||||
"semanticExecutionPath": collect_native_semantic_execution_path(pathlib.Path(args.program)),
|
||||
}
|
||||
write_json(args.output, payload)
|
||||
print(f"native_xyzbc_trt_evidence={args.output}")
|
||||
@@ -444,15 +454,16 @@ def path_sample_from_event(sample_index, time_ms, event):
|
||||
"b": b,
|
||||
"c": c,
|
||||
}
|
||||
feed = number_or_zero(event.get("feedrate"))
|
||||
spindle = spindle_speed(event.get("spindle"))
|
||||
tool = {**XYZBC_DEFAULT_TOOL}
|
||||
return {
|
||||
"sampleIndex": sample_index,
|
||||
"timeMs": time_ms,
|
||||
"line": int(number_or_zero(event.get("currentLine"))),
|
||||
"motionType": "unknown",
|
||||
"activeKinematics": "unknown",
|
||||
"tool": {
|
||||
**XYZBC_DEFAULT_TOOL,
|
||||
},
|
||||
"tool": tool,
|
||||
"joint": joint,
|
||||
"tcp": {
|
||||
"x": joint["x"],
|
||||
@@ -460,8 +471,9 @@ def path_sample_from_event(sample_index, time_ms, event):
|
||||
"z": joint["z"],
|
||||
},
|
||||
"toolAxis": tool_axis_from_bc(b, c),
|
||||
"feed": number_or_zero(event.get("feedrate")),
|
||||
"spindle": spindle_speed(event.get("spindle")),
|
||||
"feed": feed,
|
||||
"spindle": spindle,
|
||||
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type="unknown", spindle=spindle),
|
||||
}
|
||||
|
||||
|
||||
@@ -471,6 +483,35 @@ def collect_native_preview_path(program_path):
|
||||
return empty_path("linuxcnc-native-preview", f"no native preview collector for {program_path.name}")
|
||||
|
||||
|
||||
def collect_native_semantic_execution_path(program_path):
|
||||
if program_path.name != "xyzbc_switchkins.ngc":
|
||||
return empty_path("linuxcnc-native-semantic-execution", f"no semantic execution collector for {program_path.name}")
|
||||
try:
|
||||
params = parse_xyzbc_switchkins_call(program_path)
|
||||
segments = build_xyzbc_switchkins_segments(params)
|
||||
samples = resample_segments(segments, SAMPLE_PERIOD_MS)
|
||||
line_trace = build_xyzbc_switchkins_line_execution_trace(params, segments)
|
||||
gcode_process = build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace)
|
||||
return {
|
||||
"source": "linuxcnc-native-source-execution-expanded-ngcgui-subroutines",
|
||||
"samplePeriodMs": SAMPLE_PERIOD_MS,
|
||||
"status": "ok" if samples else "blocked",
|
||||
"unavailableReason": None if samples else "native source execution expansion produced no samples",
|
||||
"program": str(program_path),
|
||||
"subroutines": ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
|
||||
"sampleCount": len(samples),
|
||||
"samples": samples,
|
||||
"segmentCount": len(segments),
|
||||
"segments": [serialize_segment(segment, index) for index, segment in enumerate(segments)],
|
||||
"lineExecutionTrace": line_trace,
|
||||
"axisValuesByLine": axis_values_by_line_from_trace(line_trace),
|
||||
"gcodeExecutionProcess": gcode_process,
|
||||
"semanticBoundary": "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines",
|
||||
}
|
||||
except Exception as exc:
|
||||
return empty_path("linuxcnc-native-semantic-execution", f"{type(exc).__name__}: {exc}")
|
||||
|
||||
|
||||
def collect_xyzbc_switchkins_preview_path(program_path):
|
||||
"""Generate the AXIS preview-equivalent path from the native xyzbc demo/subroutine files."""
|
||||
try:
|
||||
@@ -550,6 +591,8 @@ def build_xyzbc_switchkins_segments(params):
|
||||
"feed": feedrate,
|
||||
"start": start,
|
||||
"end": dict(pose),
|
||||
"sourceFile": "xyzbc_switchkins_sub.ngc",
|
||||
"statement": "",
|
||||
})
|
||||
|
||||
def add_helix(line):
|
||||
@@ -569,6 +612,8 @@ def build_xyzbc_switchkins_segments(params):
|
||||
"center": center,
|
||||
"radius": radius,
|
||||
"turns": turns,
|
||||
"sourceFile": "helix_bc.ngc",
|
||||
"statement": "f#<frate> g2i#<r>z#<zmin> p#<n>",
|
||||
})
|
||||
pose = dict(end)
|
||||
|
||||
@@ -580,17 +625,574 @@ def build_xyzbc_switchkins_segments(params):
|
||||
]
|
||||
for center_x, center_y, center_line in quadrant_centers:
|
||||
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, center_line - 2, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
|
||||
segments[-1]["statement"] = f"g53 g0 x0y0 z#<zmax> b0 c0"
|
||||
add_linear({"x": center_x, "y": center_y, "z": zmax}, center_line, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
|
||||
segments[-1]["statement"] = f"g0 x{format_signed(center_x)} y{format_signed(center_y)} z#<zmax>"
|
||||
add_linear({"x": center_x - radius}, 13, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "helix_bc.ngc"
|
||||
segments[-1]["statement"] = "g0 x[#<_x> - #<r>]"
|
||||
add_linear({"b": b_axis, "c": c_axis}, 16, "rapid", "tcp-xyzbc")
|
||||
segments[-1]["sourceFile"] = "helix_bc.ngc"
|
||||
segments[-1]["statement"] = "g0b#<b>c#<c>"
|
||||
add_helix(17)
|
||||
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 19, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "helix_bc.ngc"
|
||||
segments[-1]["statement"] = "g0 x0 y0 z#<zmax> b0 c0"
|
||||
add_linear({"x": radius}, 20, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "helix_bc.ngc"
|
||||
segments[-1]["statement"] = "g0 x[#<_x> + #<r>]"
|
||||
|
||||
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 44, "rapid", "identity")
|
||||
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
|
||||
segments[-1]["statement"] = "g53 g0 x0y0 z#<zmax>"
|
||||
return segments
|
||||
|
||||
|
||||
def build_xyzbc_switchkins_line_execution_trace(params, segments):
|
||||
trace = []
|
||||
current_kinematics = "identity"
|
||||
cursor = {"value": 0}
|
||||
|
||||
def add(source_file, line, statement, operation, motion_type="none",
|
||||
active_before=None, active_after=None, start_joint=None, end_joint=None,
|
||||
feed=0, produces_motion=False, segment_index=None):
|
||||
trace.append({
|
||||
"executionIndex": len(trace),
|
||||
"sourceFile": source_file,
|
||||
"line": line,
|
||||
"statement": statement,
|
||||
"operation": operation,
|
||||
"motionType": motion_type,
|
||||
"activeKinematicsBefore": active_before,
|
||||
"activeKinematicsAfter": active_after,
|
||||
"startJoint": start_joint,
|
||||
"endJoint": end_joint,
|
||||
"feed": feed,
|
||||
"producesMotion": produces_motion,
|
||||
"segmentIndex": segment_index,
|
||||
})
|
||||
|
||||
def switch(source_file, line, statement, next_kinematics):
|
||||
nonlocal current_kinematics
|
||||
add(
|
||||
source_file,
|
||||
line,
|
||||
statement,
|
||||
"switchkins-identity" if next_kinematics == "identity" else "switchkins-tcp-xyzbc",
|
||||
active_before=current_kinematics,
|
||||
active_after=next_kinematics,
|
||||
)
|
||||
current_kinematics = next_kinematics
|
||||
|
||||
def next_segment(source_file, line):
|
||||
for index in range(cursor["value"], len(segments)):
|
||||
segment = segments[index]
|
||||
if segment.get("sourceFile") == source_file and segment.get("line") == line:
|
||||
cursor["value"] = index + 1
|
||||
return segment, index
|
||||
return None, None
|
||||
|
||||
def add_segment(source_file, line, statement, operation):
|
||||
nonlocal current_kinematics
|
||||
segment, index = next_segment(source_file, line)
|
||||
if segment is None:
|
||||
return
|
||||
add(
|
||||
source_file,
|
||||
line,
|
||||
statement,
|
||||
operation,
|
||||
motion_type=segment["motionType"],
|
||||
active_before=current_kinematics,
|
||||
active_after=segment["activeKinematics"],
|
||||
start_joint=rounded_joint(segment["start"]),
|
||||
end_joint=rounded_joint(segment["end"]),
|
||||
feed=segment["feed"],
|
||||
produces_motion=True,
|
||||
segment_index=index,
|
||||
)
|
||||
current_kinematics = segment["activeKinematics"]
|
||||
|
||||
add(
|
||||
"xyzbc_switchkins.ngc",
|
||||
2,
|
||||
"o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
|
||||
"call-subroutine",
|
||||
active_before=current_kinematics,
|
||||
active_after=current_kinematics,
|
||||
)
|
||||
for quadrant, reset_line, center_line in [
|
||||
("I", 15, 18),
|
||||
("II", 22, 25),
|
||||
("III", 29, 32),
|
||||
("IV", 36, 39),
|
||||
]:
|
||||
switch("xyzbc_switchkins_sub.ngc", reset_line, "M429", "identity")
|
||||
add_segment("xyzbc_switchkins_sub.ngc", reset_line + 1, f"g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant {quadrant}", "rapid-machine-reset")
|
||||
add("xyzbc_switchkins_sub.ngc", reset_line + 2, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
|
||||
add_segment("xyzbc_switchkins_sub.ngc", center_line, "g0 x±#<dist> y±#<dist> z#<zmax>", "rapid-to-quadrant-center")
|
||||
add("xyzbc_switchkins_sub.ngc", center_line + 1, "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]", "call-subroutine", active_before=current_kinematics, active_after=current_kinematics)
|
||||
switch("helix_bc.ngc", 12, "M429", "identity")
|
||||
add_segment("helix_bc.ngc", 13, "g0 x[#<_x> - #<r>]", "rapid-radius-adjust")
|
||||
add("helix_bc.ngc", 14, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
|
||||
switch("helix_bc.ngc", 15, "M428", "tcp-xyzbc")
|
||||
add_segment("helix_bc.ngc", 16, f"g0b{params['b']}c{params['c']}", "rapid-bc-orient")
|
||||
add_segment("helix_bc.ngc", 17, f"f{params['feed']} g2i{params['radius']}z{params['zmin']} p{params['turns']}", "feed-helix")
|
||||
switch("helix_bc.ngc", 18, "M429", "identity")
|
||||
add_segment("helix_bc.ngc", 19, "g0 x0 y0 z#<zmax> b0 c0", "rapid-return-to-start")
|
||||
add_segment("helix_bc.ngc", 20, "g0 x[#<_x> + #<r>]", "rapid-radius-restore")
|
||||
switch("helix_bc.ngc", 21, "M428", "tcp-xyzbc")
|
||||
|
||||
switch("xyzbc_switchkins_sub.ngc", 43, "M429", "identity")
|
||||
add_segment("xyzbc_switchkins_sub.ngc", 44, "g53 g0 x0y0 z#<zmax>", "rapid-final-machine-reset")
|
||||
add("xyzbc_switchkins_sub.ngc", 45, "g10l20p0 x0y0 z#<zmax>", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
|
||||
return trace
|
||||
|
||||
|
||||
def serialize_segment(segment, index):
|
||||
return {
|
||||
"segmentIndex": index,
|
||||
"kind": segment["kind"],
|
||||
"sourceFile": segment.get("sourceFile"),
|
||||
"line": segment["line"],
|
||||
"statement": segment.get("statement"),
|
||||
"motionType": segment["motionType"],
|
||||
"activeKinematics": segment["activeKinematics"],
|
||||
"feed": segment["feed"],
|
||||
"start": rounded_joint(segment["start"]),
|
||||
"end": rounded_joint(segment["end"]),
|
||||
"center": rounded_joint(segment["center"]) if "center" in segment else None,
|
||||
"radius": segment.get("radius"),
|
||||
"turns": segment.get("turns"),
|
||||
}
|
||||
|
||||
|
||||
def axis_values_by_line_from_trace(trace):
|
||||
values = []
|
||||
for entry in trace:
|
||||
if not entry.get("producesMotion"):
|
||||
continue
|
||||
joint = entry["endJoint"]
|
||||
values.append({
|
||||
"executionIndex": entry["executionIndex"],
|
||||
"sourceFile": entry["sourceFile"],
|
||||
"line": entry["line"],
|
||||
"operation": entry["operation"],
|
||||
"motionType": entry["motionType"],
|
||||
"activeKinematics": entry["activeKinematicsAfter"],
|
||||
"joint": joint,
|
||||
"tcp": {
|
||||
"x": joint["x"],
|
||||
"y": joint["y"],
|
||||
"z": joint["z"],
|
||||
},
|
||||
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
|
||||
"feed": entry["feed"],
|
||||
"machineState": machine_state_for_motion(
|
||||
tool={**XYZBC_DEFAULT_TOOL},
|
||||
feed=entry["feed"],
|
||||
motion_type=entry["motionType"],
|
||||
operation=entry["operation"],
|
||||
),
|
||||
"segmentIndex": entry["segmentIndex"],
|
||||
})
|
||||
return values
|
||||
|
||||
|
||||
def build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace):
|
||||
source_files = xyzbc_switchkins_source_files()
|
||||
trace_cursor = {"value": 0}
|
||||
steps = []
|
||||
parameters = {}
|
||||
tool = {**XYZBC_DEFAULT_TOOL}
|
||||
machine_state = machine_state_for_motion(tool=tool, feed=0, motion_type="none", operation="program-start")
|
||||
state = {
|
||||
"active_kinematics": "identity",
|
||||
"work_offset": {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0},
|
||||
}
|
||||
|
||||
def add_step(source_file, line, operation, source_line_kind="gcode", call_stack=None,
|
||||
executed=True, parameter_name=None, parameter_value=None, notes=None):
|
||||
call_stack = call_stack or []
|
||||
notes = notes or []
|
||||
statement = source_files.get(source_file, {}).get(line, "")
|
||||
trace_entry = next_trace_entry(line_trace, trace_cursor, source_file, line, operation)
|
||||
before_parameters = dict(parameters)
|
||||
before_kinematics = state["active_kinematics"]
|
||||
before_work_offset = dict(state["work_offset"])
|
||||
machine_state_before = clone_json(machine_state)
|
||||
parameters_changed = {}
|
||||
if parameter_name:
|
||||
parameters[parameter_name] = parameter_value
|
||||
parameters_changed[parameter_name] = parameter_value
|
||||
if operation == "set-g54-offset":
|
||||
state["work_offset"] = {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0}
|
||||
if trace_entry and trace_entry.get("activeKinematicsAfter"):
|
||||
state["active_kinematics"] = trace_entry.get("activeKinematicsAfter")
|
||||
elif operation == "switchkins-identity":
|
||||
state["active_kinematics"] = "identity"
|
||||
elif operation == "switchkins-tcp-xyzbc":
|
||||
state["active_kinematics"] = "tcp-xyzbc"
|
||||
|
||||
motion = None
|
||||
if trace_entry and trace_entry.get("producesMotion"):
|
||||
end_joint = trace_entry.get("endJoint")
|
||||
start_joint = trace_entry.get("startJoint")
|
||||
motion = {
|
||||
"segmentIndex": trace_entry.get("segmentIndex"),
|
||||
"motionType": trace_entry.get("motionType"),
|
||||
"feed": trace_entry.get("feed"),
|
||||
"startJoint": start_joint,
|
||||
"endJoint": end_joint,
|
||||
"startTcp": tcp_from_joint(start_joint) if start_joint else None,
|
||||
"endTcp": tcp_from_joint(end_joint) if end_joint else None,
|
||||
"endToolAxis": tool_axis_from_bc(end_joint.get("b"), end_joint.get("c")) if end_joint else None,
|
||||
}
|
||||
if motion:
|
||||
machine_state.update(machine_state_for_motion(
|
||||
tool=tool,
|
||||
feed=(trace_entry or {}).get("feed"),
|
||||
motion_type=(trace_entry or {}).get("motionType"),
|
||||
operation=operation,
|
||||
))
|
||||
else:
|
||||
machine_state.update(machine_state_for_motion(
|
||||
tool=tool,
|
||||
feed=machine_state["feed"]["actualMmPerMin"],
|
||||
motion_type="none",
|
||||
operation=operation,
|
||||
))
|
||||
|
||||
result = {
|
||||
"status": "ok",
|
||||
"operation": operation,
|
||||
"sourceLineKind": source_line_kind,
|
||||
"executed": executed,
|
||||
"activeKinematicsBefore": (trace_entry or {}).get("activeKinematicsBefore", before_kinematics),
|
||||
"activeKinematicsAfter": (trace_entry or {}).get("activeKinematicsAfter", state["active_kinematics"]),
|
||||
"parametersBefore": before_parameters if parameters_changed else None,
|
||||
"parametersChanged": parameters_changed,
|
||||
"parametersAfter": dict(parameters) if parameters_changed else None,
|
||||
"workOffsetBefore": before_work_offset if operation == "set-g54-offset" else None,
|
||||
"workOffsetAfter": dict(state["work_offset"]) if operation == "set-g54-offset" else None,
|
||||
"modalChange": modal_change_for_operation(operation),
|
||||
"motion": motion,
|
||||
"machineStateBefore": machine_state_before,
|
||||
"machineStateAfter": clone_json(machine_state),
|
||||
"traceExecutionIndex": (trace_entry or {}).get("executionIndex"),
|
||||
"notes": notes,
|
||||
}
|
||||
steps.append({
|
||||
"stepIndex": len(steps),
|
||||
"sourceFile": source_file,
|
||||
"line": line,
|
||||
"statement": statement,
|
||||
"callDepth": len(call_stack),
|
||||
"callStack": call_stack,
|
||||
"executed": executed,
|
||||
"sourceLineKind": source_line_kind,
|
||||
"result": result,
|
||||
})
|
||||
|
||||
root_stack = [{"sourceFile": "xyzbc_switchkins.ngc", "line": 2, "call": "o<xyzbc_switchkins_sub>"}]
|
||||
add_step("xyzbc_switchkins.ngc", 1, "comment", "comment", executed=False)
|
||||
add_step("xyzbc_switchkins.ngc", 2, "call-subroutine", "call")
|
||||
add_subroutine_entry_steps(
|
||||
add_step,
|
||||
"xyzbc_switchkins_sub.ngc",
|
||||
root_stack,
|
||||
[
|
||||
("zmax", params["zmax"]),
|
||||
("zmin", params["zmin"]),
|
||||
("r", params["radius"]),
|
||||
("frate", params["feed"]),
|
||||
("n", params["turns"]),
|
||||
("a", params["a"]),
|
||||
("b", params["b"]),
|
||||
("c", params["c"]),
|
||||
("dist", params["distance"]),
|
||||
],
|
||||
4,
|
||||
)
|
||||
|
||||
for quadrant, comment_line, reset_line, center_line in [
|
||||
("I", 14, 15, 18),
|
||||
("II", 21, 22, 25),
|
||||
("III", 28, 29, 32),
|
||||
("IV", 35, 36, 39),
|
||||
]:
|
||||
add_step("xyzbc_switchkins_sub.ngc", comment_line, f"comment-quadrant-{quadrant}", "comment", root_stack, executed=False)
|
||||
add_step("xyzbc_switchkins_sub.ngc", reset_line, "switchkins-identity", "mcode", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", reset_line + 1, "rapid-machine-reset", "motion", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", reset_line + 2, "set-g54-offset", "offset", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", center_line, "rapid-to-quadrant-center", "motion", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", center_line + 1, "call-subroutine", "call", root_stack)
|
||||
add_helix_execution_steps(
|
||||
add_step,
|
||||
params,
|
||||
root_stack + [{"sourceFile": "xyzbc_switchkins_sub.ngc", "line": center_line + 1, "call": "o<helix_bc>"}],
|
||||
)
|
||||
|
||||
add_step("xyzbc_switchkins_sub.ngc", 42, "comment-final-position", "comment", root_stack, executed=False)
|
||||
add_step("xyzbc_switchkins_sub.ngc", 43, "switchkins-identity", "mcode", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", 44, "rapid-final-machine-reset", "motion", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", 45, "set-g54-offset", "offset", root_stack)
|
||||
add_step("xyzbc_switchkins_sub.ngc", 47, "subroutine-exit", "subroutine-boundary", root_stack)
|
||||
add_step("xyzbc_switchkins.ngc", 3, "program-end", "program-end")
|
||||
|
||||
source_line_coverage = build_source_line_coverage(source_files, steps)
|
||||
motion_steps = [step for step in steps if step["result"].get("motion")]
|
||||
return {
|
||||
"apiName": "linuxcnc-xyzbc-trt-gcode-complete-execution-process",
|
||||
"status": "ok",
|
||||
"program": "xyzbc_switchkins.ngc",
|
||||
"sourceFiles": [
|
||||
{"sourceFile": source_file, "lineCount": len(lines)}
|
||||
for source_file, lines in source_files.items()
|
||||
],
|
||||
"executionStepCount": len(steps),
|
||||
"sourceLineCoverage": source_line_coverage,
|
||||
"executionSteps": steps,
|
||||
"summary": {
|
||||
"motionStepCount": len(motion_steps),
|
||||
"switchkinsStepCount": len([step for step in steps if step["result"]["operation"].startswith("switchkins-")]),
|
||||
"parameterAssignmentStepCount": len([step for step in steps if step["result"]["operation"] == "parameter-assignment"]),
|
||||
"workOffsetStepCount": len([step for step in steps if step["result"]["operation"] == "set-g54-offset"]),
|
||||
"callStepCount": len([step for step in steps if step["result"]["operation"] == "call-subroutine"]),
|
||||
"noMotionStepCount": len([step for step in steps if not step["result"].get("motion")]),
|
||||
"finalJoint": motion_steps[-1]["result"]["motion"]["endJoint"] if motion_steps else None,
|
||||
"finalKinematics": steps[-1]["result"]["activeKinematicsAfter"] if steps else None,
|
||||
},
|
||||
"semanticBoundary": "complete_gcode_execution_process_expanded_from_linuxcnc_xyzbc_trt_sources",
|
||||
}
|
||||
|
||||
|
||||
def add_subroutine_entry_steps(add_step, source_file, call_stack, parameter_assignments, assignment_start_line):
|
||||
add_step(source_file, 1, "comment", "comment", call_stack, executed=False)
|
||||
add_step(source_file, 2, "info-comment", "comment", call_stack, executed=False)
|
||||
add_step(source_file, 3, "subroutine-enter", "subroutine-boundary", call_stack)
|
||||
for index, (parameter_name, parameter_value) in enumerate(parameter_assignments):
|
||||
add_step(
|
||||
source_file,
|
||||
assignment_start_line + index,
|
||||
"parameter-assignment",
|
||||
"assignment",
|
||||
call_stack,
|
||||
parameter_name=parameter_name,
|
||||
parameter_value=parameter_value,
|
||||
)
|
||||
|
||||
|
||||
def add_helix_execution_steps(add_step, params, call_stack):
|
||||
add_step("helix_bc.ngc", 1, "comment", "comment", call_stack, executed=False)
|
||||
add_step("helix_bc.ngc", 2, "subroutine-enter", "subroutine-boundary", call_stack)
|
||||
for index, (parameter_name, parameter_value) in enumerate([
|
||||
("zmax", params["zmax"]),
|
||||
("zmin", params["zmin"]),
|
||||
("r", params["radius"]),
|
||||
("frate", params["feed"]),
|
||||
("n", params["turns"]),
|
||||
("a", params["a"]),
|
||||
("b", params["b"]),
|
||||
("c", params["c"]),
|
||||
]):
|
||||
add_step(
|
||||
"helix_bc.ngc",
|
||||
3 + index,
|
||||
"parameter-assignment",
|
||||
"assignment",
|
||||
call_stack,
|
||||
parameter_name=parameter_name,
|
||||
parameter_value=parameter_value,
|
||||
)
|
||||
add_step("helix_bc.ngc", 12, "switchkins-identity", "mcode", call_stack)
|
||||
add_step("helix_bc.ngc", 13, "rapid-radius-adjust", "motion", call_stack)
|
||||
add_step("helix_bc.ngc", 14, "set-g54-offset", "offset", call_stack)
|
||||
add_step("helix_bc.ngc", 15, "switchkins-tcp-xyzbc", "mcode", call_stack)
|
||||
add_step("helix_bc.ngc", 16, "rapid-bc-orient", "motion", call_stack)
|
||||
add_step("helix_bc.ngc", 17, "feed-helix", "motion", call_stack)
|
||||
add_step("helix_bc.ngc", 18, "switchkins-identity", "mcode", call_stack)
|
||||
add_step("helix_bc.ngc", 19, "rapid-return-to-start", "motion", call_stack)
|
||||
add_step("helix_bc.ngc", 20, "rapid-radius-restore", "motion", call_stack)
|
||||
add_step("helix_bc.ngc", 21, "switchkins-tcp-xyzbc", "mcode", call_stack)
|
||||
add_step("helix_bc.ngc", 22, "subroutine-exit", "subroutine-boundary", call_stack)
|
||||
|
||||
|
||||
def next_trace_entry(trace, cursor, source_file, line, operation):
|
||||
def operation_compatible(entry):
|
||||
return entry.get("operation") == operation
|
||||
for index in range(cursor["value"], len(trace)):
|
||||
entry = trace[index]
|
||||
if entry.get("sourceFile") == source_file and entry.get("line") == line and operation_compatible(entry):
|
||||
cursor["value"] = index + 1
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def build_source_line_coverage(source_files, steps):
|
||||
visits = {}
|
||||
for step in steps:
|
||||
key = (step["sourceFile"], step["line"])
|
||||
item = visits.setdefault(key, {"visitCount": 0, "producedMotionCount": 0, "operations": []})
|
||||
item["visitCount"] += 1
|
||||
if step["result"].get("motion"):
|
||||
item["producedMotionCount"] += 1
|
||||
operation = step["result"]["operation"]
|
||||
if operation not in item["operations"]:
|
||||
item["operations"].append(operation)
|
||||
coverage = []
|
||||
for source_file, lines in source_files.items():
|
||||
for line, statement in lines.items():
|
||||
visit = visits.get((source_file, line), {})
|
||||
coverage.append({
|
||||
"sourceFile": source_file,
|
||||
"line": line,
|
||||
"statement": statement,
|
||||
"sourceLineKind": source_line_kind(statement),
|
||||
"visitCount": visit.get("visitCount", 0),
|
||||
"producedMotionCount": visit.get("producedMotionCount", 0),
|
||||
"operations": visit.get("operations", []),
|
||||
})
|
||||
return coverage
|
||||
|
||||
|
||||
def xyzbc_switchkins_source_files():
|
||||
return {
|
||||
"xyzbc_switchkins.ngc": {
|
||||
1: "; zmax zmin r frate n a b c dist",
|
||||
2: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
|
||||
3: "m2",
|
||||
},
|
||||
"xyzbc_switchkins_sub.ngc": {
|
||||
1: "; ngcgui-compatible subroutine",
|
||||
2: "(info: helix in each quadrant at angles B,C)",
|
||||
3: "o<xyzbc_switchkins_sub>sub",
|
||||
4: "#<zmax> = #1 (=10)",
|
||||
5: "#<zmin> = #2 (= 5)",
|
||||
6: "#<r> = #3 (=10 radius)",
|
||||
7: "#<frate> = #4 (=1000 feedrate)",
|
||||
8: "#<n> = #5 (=3 n circles)",
|
||||
9: "#<a> = #6 (=0 A angle NA)",
|
||||
10: "#<b> = #7 (=30 B angle)",
|
||||
11: "#<c> = #8 (=45 C angle)",
|
||||
12: "#<dist> = #9 (=20 distance)",
|
||||
14: "; quadrant I",
|
||||
15: "M429 ;Identity kinematics",
|
||||
16: "g53 g0 x0y0 z#<zmax> b0 c0 ;MACHINE coordinates",
|
||||
17: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
|
||||
18: "g0 x+#<dist> y+#<dist> z#<zmax> ;move to pattern center position",
|
||||
19: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
|
||||
21: "; quadrant II",
|
||||
22: "M429 ;Identity kinematics",
|
||||
23: "g53 g0 x0y0 z#<zmax> b0 c0",
|
||||
24: "g10l20p0 x0y0 z#<zmax> b0 c0",
|
||||
25: "g0 x-#<dist> y+#<dist> z#<zmax>",
|
||||
26: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
|
||||
28: "; quadrant III",
|
||||
29: "M429 ;Identity kinematics",
|
||||
30: "g53 g0 x0y0 z#<zmax> b0 c0",
|
||||
31: "g10l20p0 x0y0 z#<zmax> b0 c0",
|
||||
32: "g0 x-#<dist> y-#<dist> z#<zmax>",
|
||||
33: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
|
||||
35: "; quadrant IV",
|
||||
36: "M429 ;Identity kinematics",
|
||||
37: "g53 g0 x0y0 z#<zmax> b0 c0",
|
||||
38: "g10l20p0 x0y0 z#<zmax> b0 c0",
|
||||
39: "g0 x+#<dist> y-#<dist> z#<zmax>",
|
||||
40: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
|
||||
42: ";final position",
|
||||
43: "M429 ;Identity kinematics",
|
||||
44: "g53 g0 x0y0 z#<zmax> ;MACHINE coordinates",
|
||||
45: "g10l20p0 x0y0 z#<zmax> ;new g54",
|
||||
47: "o<xyzbc_switchkins_sub>endsub",
|
||||
},
|
||||
"helix_bc.ngc": {
|
||||
1: "; helix using switchkins (xyzbc) b,c angles",
|
||||
2: "o<helix_bc>sub",
|
||||
3: "#<zmax> = #1 (=10)",
|
||||
4: "#<zmin> = #2 (= 5)",
|
||||
5: "#<r> = #3 (=10)",
|
||||
6: "#<frate> = #4 (=1000)",
|
||||
7: "#<n> = #5 (=3)",
|
||||
8: "#<a> = #6 (=0 NA)",
|
||||
9: "#<b> = #7 (=45)",
|
||||
10: "#<c> = #8 (=20)",
|
||||
12: "M429 ;Identity kinematics",
|
||||
13: "g0 x[#<_x> - #<r>] ;adjust for radius",
|
||||
14: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
|
||||
15: "M428 ;XYZBC",
|
||||
16: "g0b#<b>c#<c> ;exercise b,c",
|
||||
17: "f#<frate> g2i#<r>z#<zmin> p#<n> ;helix",
|
||||
18: "M429 ;Identity kinematics",
|
||||
19: "g0 x0 y0 z#<zmax> b0 c0 ;return to start",
|
||||
20: "g0 x[#<_x> + #<r>] ;adjust restore",
|
||||
21: "M428 ;XYZBC",
|
||||
22: "o<helix_bc>endsub",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def source_line_kind(statement):
|
||||
text = str(statement).strip().lower()
|
||||
if not text:
|
||||
return "blank"
|
||||
if text.startswith(";") or text.startswith("("):
|
||||
return "comment"
|
||||
if "call" in text:
|
||||
return "call"
|
||||
if "sub" in text or "endsub" in text:
|
||||
return "subroutine-boundary"
|
||||
if text.startswith("#<"):
|
||||
return "assignment"
|
||||
if text.startswith("m"):
|
||||
return "mcode"
|
||||
if text.startswith("g10"):
|
||||
return "offset"
|
||||
if text.startswith("g") or text.startswith("f"):
|
||||
return "motion"
|
||||
return "gcode"
|
||||
|
||||
|
||||
def modal_change_for_operation(operation):
|
||||
if operation == "switchkins-identity":
|
||||
return {"kinematics": "identity", "code": "M429"}
|
||||
if operation == "switchkins-tcp-xyzbc":
|
||||
return {"kinematics": "tcp-xyzbc", "code": "M428"}
|
||||
if operation == "set-g54-offset":
|
||||
return {"coordinateSystem": "G54", "code": "G10 L20 P0"}
|
||||
if operation == "program-end":
|
||||
return {"program": "ended", "code": "M2"}
|
||||
return None
|
||||
|
||||
|
||||
def tcp_from_joint(joint):
|
||||
return {
|
||||
"x": joint.get("x"),
|
||||
"y": joint.get("y"),
|
||||
"z": joint.get("z"),
|
||||
}
|
||||
|
||||
|
||||
def rounded_joint(pose):
|
||||
return {
|
||||
"x": round_floating(number_or_zero(pose.get("x"))),
|
||||
"y": round_floating(number_or_zero(pose.get("y"))),
|
||||
"z": round_floating(number_or_zero(pose.get("z"))),
|
||||
"b": round_floating(number_or_zero(pose.get("b"))),
|
||||
"c": round_floating(number_or_zero(pose.get("c"))),
|
||||
}
|
||||
|
||||
|
||||
def round_floating(value):
|
||||
return 0 if abs(value) < 1e-12 else round(value, 12)
|
||||
|
||||
|
||||
def format_signed(value):
|
||||
return f"+{value:g}" if value >= 0 else f"{value:g}"
|
||||
|
||||
|
||||
def resample_segments(segments, sample_period_ms):
|
||||
samples = []
|
||||
time_ms = 0
|
||||
@@ -655,13 +1257,14 @@ def pose_on_segment(segment, ratio):
|
||||
|
||||
def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinematics, pose, feed):
|
||||
joint = {axis: number_or_zero(pose.get(axis)) for axis in ["x", "y", "z", "b", "c"]}
|
||||
tool = {**XYZBC_DEFAULT_TOOL}
|
||||
return {
|
||||
"sampleIndex": sample_index,
|
||||
"timeMs": time_ms,
|
||||
"line": int(line),
|
||||
"motionType": motion_type,
|
||||
"activeKinematics": active_kinematics,
|
||||
"tool": {**XYZBC_DEFAULT_TOOL},
|
||||
"tool": tool,
|
||||
"joint": joint,
|
||||
"tcp": {
|
||||
"x": joint["x"],
|
||||
@@ -671,6 +1274,7 @@ def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinem
|
||||
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
|
||||
"feed": number_or_zero(feed),
|
||||
"spindle": 0,
|
||||
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type=motion_type),
|
||||
}
|
||||
|
||||
|
||||
@@ -779,6 +1383,49 @@ def spindle_speed(spindle):
|
||||
return 0
|
||||
|
||||
|
||||
def machine_state_for_motion(tool=None, feed=0, motion_type="none", operation=None, spindle=0):
|
||||
tool = tool or XYZBC_DEFAULT_TOOL
|
||||
actual_feed = number_or_zero(feed)
|
||||
spindle_speed_rpm = number_or_zero(spindle)
|
||||
cutting = motion_type in ("arc", "feed") or operation == "feed-helix"
|
||||
return {
|
||||
"spindle": {
|
||||
"speedRpm": spindle_speed_rpm,
|
||||
"direction": "forward" if spindle_speed_rpm > 0 else "stopped",
|
||||
"enabled": spindle_speed_rpm > 0,
|
||||
},
|
||||
"feed": {
|
||||
"programmedMmPerMin": actual_feed,
|
||||
"actualMmPerMin": actual_feed,
|
||||
"overridePercent": 100,
|
||||
},
|
||||
"cutting": {
|
||||
"active": cutting,
|
||||
"cuttingSpeedMmPerMin": actual_feed if cutting else 0,
|
||||
},
|
||||
"tool": {
|
||||
"id": int(number_or_zero(tool.get("id"))),
|
||||
"pocket": int(number_or_zero(tool.get("pocket"))),
|
||||
"length": number_or_zero(tool.get("length")),
|
||||
"diameter": number_or_zero(tool.get("diameter")),
|
||||
},
|
||||
"toolChange": {
|
||||
"activeTool": int(number_or_zero(tool.get("id"))),
|
||||
"activePocket": int(number_or_zero(tool.get("pocket"))),
|
||||
"changed": False,
|
||||
"command": None,
|
||||
},
|
||||
"coolant": {
|
||||
"mist": False,
|
||||
"flood": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def clone_json(value):
|
||||
return json.loads(json.dumps(value))
|
||||
|
||||
|
||||
def native_hal_nets(hal):
|
||||
pins = hal.get("pins", {})
|
||||
return [
|
||||
|
||||
@@ -18,10 +18,13 @@ 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 {
|
||||
buildAxisExecutionTraceFromProgram,
|
||||
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 SAMPLE_PERIOD_MS = 50;
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
|
||||
const outputPath = process.argv[2]
|
||||
@@ -137,6 +140,10 @@ const evidence = {
|
||||
pathSampling: createPathSampling(),
|
||||
previewPath: paths.previewPath,
|
||||
executionPath: paths.executionPath,
|
||||
semanticExecutionPath: paths.semanticExecutionPath,
|
||||
lineExecutionTrace: paths.semanticExecutionPath?.lineExecutionTrace || [],
|
||||
axisValuesByLine: paths.semanticExecutionPath?.axisValuesByLine || [],
|
||||
gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null,
|
||||
toolRuntime,
|
||||
taskHalEquivalence,
|
||||
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
|
||||
@@ -169,6 +176,10 @@ const evidence = {
|
||||
&& Boolean(semanticFields.axisJointLimits.axisLimits.C),
|
||||
previewPathAvailable: paths.previewPath.sampleCount > 0,
|
||||
executionPathAvailable: paths.executionPath.sampleCount > 0,
|
||||
semanticExecutionPathAvailable: paths.semanticExecutionPath?.sampleCount > 0,
|
||||
lineExecutionTraceAvailable: (paths.semanticExecutionPath?.lineExecutionTrace || []).length > 0,
|
||||
axisValuesByLineAvailable: (paths.semanticExecutionPath?.axisValuesByLine || []).length > 0,
|
||||
gcodeExecutionProcessAvailable: paths.semanticExecutionPath?.gcodeExecutionProcess?.status === "ok",
|
||||
axisMainUiEquivalent: axisMainUi.ready,
|
||||
basicSimEquivalent: taskHalEquivalence.ready === true,
|
||||
ngcguiSubroutinesExecutable: ngcguiExecution.ready === true,
|
||||
@@ -200,6 +211,10 @@ const evidence = {
|
||||
id: "web-execution-path-unavailable",
|
||||
detail: paths.executionPath.unavailableReason,
|
||||
}]),
|
||||
...(paths.semanticExecutionPath?.sampleCount > 0 ? [] : [{
|
||||
id: "web-semantic-execution-path-unavailable",
|
||||
detail: paths.semanticExecutionPath?.unavailableReason || "semantic execution path was not generated",
|
||||
}]),
|
||||
...(taskHalEquivalence.ready ? [] : [{
|
||||
id: "web-basic-sim-equivalence-incomplete",
|
||||
detail: taskHalEquivalence.unavailableReason,
|
||||
@@ -261,6 +276,7 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
|
||||
return {
|
||||
previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"),
|
||||
executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
|
||||
semanticExecutionPath: emptyPath("web-semantic-execution", "missing linuxcnc_interp WASM artifacts"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -278,16 +294,22 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
|
||||
selectedPlan,
|
||||
});
|
||||
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool, selectedPlan, staged);
|
||||
const semanticExecutionPath = semanticExecutionPathFromAxisExpansion({ selectedPlan, staged, pathTool: toolRuntime.pathTool });
|
||||
return {
|
||||
previewPath,
|
||||
executionPath: wasmArtifacts.ready
|
||||
? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime })
|
||||
: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
|
||||
semanticExecutionPath: semanticExecutionPath || emptyPath(
|
||||
"web-semantic-execution",
|
||||
"no semantic execution expansion is available for selected program",
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)),
|
||||
executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"),
|
||||
semanticExecutionPath: emptyPath("web-semantic-execution", "preview runtime failed before semantic execution capture"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -345,6 +367,27 @@ function pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, path
|
||||
});
|
||||
}
|
||||
|
||||
function semanticExecutionPathFromAxisExpansion({ 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 buildAxisExecutionTraceFromProgram({
|
||||
filename: selectedProgramFilename,
|
||||
sourceRel: selectedPlan?.selectedProgramSourceRel,
|
||||
content: programFile?.text || "",
|
||||
tool: pathTool || {
|
||||
id: 2,
|
||||
pocket: 2,
|
||||
length: 10,
|
||||
diameter: 8,
|
||||
},
|
||||
source: "web-axis-source-execution-expanded-ngcgui-subroutines",
|
||||
});
|
||||
}
|
||||
|
||||
function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) {
|
||||
if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return [];
|
||||
const normalized = plannerSamples
|
||||
@@ -1043,13 +1086,16 @@ function normalizePathSample({
|
||||
b: numberOrZero(axes.b),
|
||||
c: numberOrZero(axes.c),
|
||||
};
|
||||
const normalizedTool = tool || firstTool({});
|
||||
const normalizedFeed = numberOrZero(feed);
|
||||
const normalizedSpindle = numberOrZero(spindle);
|
||||
return {
|
||||
sampleIndex,
|
||||
timeMs,
|
||||
line: Number(line) || 0,
|
||||
motionType,
|
||||
activeKinematics,
|
||||
tool,
|
||||
tool: normalizedTool,
|
||||
joint,
|
||||
tcp: {
|
||||
x: joint.x,
|
||||
@@ -1057,8 +1103,14 @@ function normalizePathSample({
|
||||
z: joint.z,
|
||||
},
|
||||
toolAxis: toolAxisFromBc(joint.b, joint.c),
|
||||
feed: numberOrZero(feed),
|
||||
spindle: numberOrZero(spindle),
|
||||
feed: normalizedFeed,
|
||||
spindle: normalizedSpindle,
|
||||
machineState: machineStateForMotion({
|
||||
tool: normalizedTool,
|
||||
feed: normalizedFeed,
|
||||
motionType,
|
||||
spindle: normalizedSpindle,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1066,11 +1118,59 @@ function firstTool(profile) {
|
||||
const tool = profile.toolTable?.tools?.[0] || {};
|
||||
return {
|
||||
id: Number(tool.tool) || 0,
|
||||
pocket: Number(tool.pocket) || Number(tool.tool) || 0,
|
||||
length: Number(tool.zOffset) || 0,
|
||||
diameter: Number(tool.diameter) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function machineStateForMotion({
|
||||
tool = {},
|
||||
feed = 0,
|
||||
motionType = "none",
|
||||
operation = null,
|
||||
spindle = 0,
|
||||
} = {}) {
|
||||
const actualFeed = numberOrZero(feed);
|
||||
const spindleSpeedRpm = numberOrZero(spindle);
|
||||
const cutting = motionType === "arc"
|
||||
|| motionType === "feed"
|
||||
|| motionType === "G2/G3"
|
||||
|| operation === "feed-helix";
|
||||
return {
|
||||
spindle: {
|
||||
speedRpm: spindleSpeedRpm,
|
||||
direction: spindleSpeedRpm > 0 ? "forward" : "stopped",
|
||||
enabled: spindleSpeedRpm > 0,
|
||||
},
|
||||
feed: {
|
||||
programmedMmPerMin: actualFeed,
|
||||
actualMmPerMin: actualFeed,
|
||||
overridePercent: 100,
|
||||
},
|
||||
cutting: {
|
||||
active: cutting,
|
||||
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
|
||||
},
|
||||
tool: {
|
||||
id: Number(tool?.id) || 0,
|
||||
pocket: Number(tool?.pocket) || 0,
|
||||
length: numberOrZero(tool?.length),
|
||||
diameter: numberOrZero(tool?.diameter),
|
||||
},
|
||||
toolChange: {
|
||||
activeTool: Number(tool?.id) || 0,
|
||||
activePocket: Number(tool?.pocket) || 0,
|
||||
changed: false,
|
||||
command: null,
|
||||
},
|
||||
coolant: {
|
||||
mist: false,
|
||||
flood: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function xyzbcAxesFromTaskHalStatus(status = {}) {
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const pins = status.halSnapshot?.pins || {};
|
||||
|
||||
@@ -10,10 +10,16 @@ const outputPath = process.argv[4] || resolve(projectRoot, "working/evidence/com
|
||||
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 EXECUTION_TCP_MAX_ERROR_MM = 0.001;
|
||||
const EXECUTION_JOINT_MAX_ERROR = 0.001;
|
||||
const EXECUTION_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
|
||||
|
||||
const nativeEvidence = JSON.parse(await readFile(nativePath, "utf8"));
|
||||
const webEvidence = JSON.parse(await readFile(webPath, "utf8"));
|
||||
const pathComparison = comparePathEvidence(nativeEvidence, webEvidence);
|
||||
const lineExecutionComparison = compareLineExecutionTrace(nativeEvidence, webEvidence);
|
||||
const axisValuesByLineComparison = compareAxisValuesByLine(nativeEvidence, webEvidence);
|
||||
const gcodeExecutionProcessComparison = compareGcodeExecutionProcess(nativeEvidence, webEvidence);
|
||||
|
||||
const checks = [
|
||||
check("profile", "native axis mask is XYZBC", nativeEvidence.coverage?.axisProfile === true, {
|
||||
@@ -98,7 +104,7 @@ const checks = [
|
||||
nativeTaskStateFlow: nativeEvidence.taskStateFlow,
|
||||
webNativeStateFlowReview: webEvidence.nativeStateFlowReview,
|
||||
}),
|
||||
check("path-preview", "native and web preview path sample period is 20ms", pathComparison.previewVsPreview.periodsMatch === true, {
|
||||
check("path-preview", "native and web preview path sample period is 50ms", pathComparison.previewVsPreview.periodsMatch === true, {
|
||||
nativeSamplePeriodMs: pathComparison.previewVsPreview.nativeSamplePeriodMs,
|
||||
webSamplePeriodMs: pathComparison.previewVsPreview.webSamplePeriodMs,
|
||||
}),
|
||||
@@ -115,7 +121,7 @@ const checks = [
|
||||
maxToolAxisAngleDeg: pathComparison.previewVsPreview.maxToolAxisAngleDeg,
|
||||
sampleCountDelta: pathComparison.previewVsPreview.sampleCountDelta,
|
||||
}),
|
||||
check("path-execution", "native and web execution path sample period is 20ms", pathComparison.executionVsExecution.periodsMatch === true, {
|
||||
check("path-execution", "native and web execution path sample period is 50ms", pathComparison.executionVsExecution.periodsMatch === true, {
|
||||
nativeSamplePeriodMs: pathComparison.executionVsExecution.nativeSamplePeriodMs,
|
||||
webSamplePeriodMs: pathComparison.executionVsExecution.webSamplePeriodMs,
|
||||
}),
|
||||
@@ -124,6 +130,41 @@ const checks = [
|
||||
webSampleCount: pathComparison.executionVsExecution.webSampleCount,
|
||||
unavailable: pathComparison.executionVsExecution.unavailable,
|
||||
}),
|
||||
check("path-execution", "native and web source-expanded execution paths are geometrically aligned", pathComparison.semanticExecutionVsSemanticExecution.geometricAligned === true, {
|
||||
thresholds: pathComparison.semanticExecutionVsSemanticExecution.thresholds,
|
||||
nativeSampleCount: pathComparison.semanticExecutionVsSemanticExecution.nativeSampleCount,
|
||||
webSampleCount: pathComparison.semanticExecutionVsSemanticExecution.webSampleCount,
|
||||
maxTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.maxTcpErrorMm,
|
||||
rmsTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.rmsTcpErrorMm,
|
||||
maxJointError: pathComparison.semanticExecutionVsSemanticExecution.maxJointError,
|
||||
maxToolAxisAngleDeg: pathComparison.semanticExecutionVsSemanticExecution.maxToolAxisAngleDeg,
|
||||
machineStateMismatchCount: pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount,
|
||||
sampleCountDelta: pathComparison.semanticExecutionVsSemanticExecution.sampleCountDelta,
|
||||
}),
|
||||
check("line-execution", "native and web per-line G-code execution trace matches", lineExecutionComparison.status === "pass", {
|
||||
nativeTraceCount: lineExecutionComparison.nativeTraceCount,
|
||||
webTraceCount: lineExecutionComparison.webTraceCount,
|
||||
mismatchCount: lineExecutionComparison.mismatchCount,
|
||||
mismatches: lineExecutionComparison.mismatches.slice(0, 10),
|
||||
}),
|
||||
check("axis-values", "native and web actual axis values by executed line match", axisValuesByLineComparison.status === "pass", {
|
||||
nativeLineValueCount: axisValuesByLineComparison.nativeLineValueCount,
|
||||
webLineValueCount: axisValuesByLineComparison.webLineValueCount,
|
||||
thresholds: axisValuesByLineComparison.thresholds,
|
||||
maxTcpErrorMm: axisValuesByLineComparison.maxTcpErrorMm,
|
||||
maxJointError: axisValuesByLineComparison.maxJointError,
|
||||
maxToolAxisAngleDeg: axisValuesByLineComparison.maxToolAxisAngleDeg,
|
||||
mismatchCount: axisValuesByLineComparison.mismatchCount,
|
||||
mismatches: axisValuesByLineComparison.mismatches.slice(0, 10),
|
||||
}),
|
||||
check("gcode-process", "native and web complete G-code execution process JSON matches", gcodeExecutionProcessComparison.status === "pass", {
|
||||
nativeExecutionStepCount: gcodeExecutionProcessComparison.nativeExecutionStepCount,
|
||||
webExecutionStepCount: gcodeExecutionProcessComparison.webExecutionStepCount,
|
||||
nativeSourceLineCoverageCount: gcodeExecutionProcessComparison.nativeSourceLineCoverageCount,
|
||||
webSourceLineCoverageCount: gcodeExecutionProcessComparison.webSourceLineCoverageCount,
|
||||
mismatchCount: gcodeExecutionProcessComparison.mismatchCount,
|
||||
mismatches: gcodeExecutionProcessComparison.mismatches.slice(0, 10),
|
||||
}),
|
||||
check("path-preview-execution-consistency", "native preview and execution paths are comparable", pathComparison.previewVsExecutionNative.comparable === true, {
|
||||
nativePreviewSampleCount: pathComparison.previewVsExecutionNative.leftSampleCount,
|
||||
nativeExecutionSampleCount: pathComparison.previewVsExecutionNative.rightSampleCount,
|
||||
@@ -158,6 +199,9 @@ const report = {
|
||||
},
|
||||
checks,
|
||||
pathComparison,
|
||||
lineExecutionComparison,
|
||||
axisValuesByLineComparison,
|
||||
gcodeExecutionProcessComparison,
|
||||
requiredImprovements: failed.map((item) => ({
|
||||
category: item.category,
|
||||
requirement: item.requirement,
|
||||
@@ -185,7 +229,7 @@ function check(category, requirement, passed, evidence = {}) {
|
||||
}
|
||||
|
||||
function comparePathEvidence(nativeEvidence, webEvidence) {
|
||||
const samplePeriodMs = 20;
|
||||
const samplePeriodMs = 50;
|
||||
return {
|
||||
samplePeriodMs,
|
||||
previewVsPreview: compareNamedPaths({
|
||||
@@ -201,6 +245,21 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
|
||||
leftName: "native",
|
||||
rightName: "web",
|
||||
expectedSamplePeriodMs: samplePeriodMs,
|
||||
strictGeometry: false,
|
||||
}),
|
||||
semanticExecutionVsSemanticExecution: compareNamedPaths({
|
||||
left: nativeEvidence.semanticExecutionPath,
|
||||
right: webEvidence.semanticExecutionPath,
|
||||
leftName: "native",
|
||||
rightName: "web",
|
||||
expectedSamplePeriodMs: samplePeriodMs,
|
||||
strictGeometry: true,
|
||||
thresholds: {
|
||||
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
|
||||
maxJointError: EXECUTION_JOINT_MAX_ERROR,
|
||||
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
|
||||
sampleCountDelta: 0,
|
||||
},
|
||||
}),
|
||||
previewVsExecutionNative: compareNamedPaths({
|
||||
left: nativeEvidence.previewPath,
|
||||
@@ -219,7 +278,15 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
|
||||
};
|
||||
}
|
||||
|
||||
function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePeriodMs }) {
|
||||
function compareNamedPaths({
|
||||
left,
|
||||
right,
|
||||
leftName,
|
||||
rightName,
|
||||
expectedSamplePeriodMs,
|
||||
strictGeometry = leftName === "native" && rightName === "web",
|
||||
thresholds = null,
|
||||
}) {
|
||||
const leftSamplePeriodMs = left?.samplePeriodMs ?? null;
|
||||
const rightSamplePeriodMs = right?.samplePeriodMs ?? null;
|
||||
const periodsMatch = leftSamplePeriodMs === expectedSamplePeriodMs
|
||||
@@ -233,26 +300,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 ? {
|
||||
const resolvedThresholds = strictGeometry ? (thresholds || {
|
||||
maxTcpErrorMm: PREVIEW_TCP_MAX_ERROR_MM,
|
||||
maxJointError: PREVIEW_JOINT_MAX_ERROR,
|
||||
maxToolAxisAngleDeg: PREVIEW_TOOL_AXIS_MAX_ERROR_DEG,
|
||||
sampleCountDelta: 0,
|
||||
} : null;
|
||||
const geometricAligned = previewPair
|
||||
}) : null;
|
||||
const geometricAligned = strictGeometry
|
||||
? comparable
|
||||
&& stats.maxTcpErrorMm <= thresholds.maxTcpErrorMm
|
||||
&& stats.maxJointError <= thresholds.maxJointError
|
||||
&& stats.maxToolAxisAngleDeg <= thresholds.maxToolAxisAngleDeg
|
||||
&& stats.sampleCountDelta <= thresholds.sampleCountDelta
|
||||
&& stats.maxTcpErrorMm <= resolvedThresholds.maxTcpErrorMm
|
||||
&& stats.maxJointError <= resolvedThresholds.maxJointError
|
||||
&& stats.maxToolAxisAngleDeg <= resolvedThresholds.maxToolAxisAngleDeg
|
||||
&& stats.sampleCountDelta <= resolvedThresholds.sampleCountDelta
|
||||
&& stats.machineStateMismatchCount === 0
|
||||
&& stats.missingSamples.length === 0
|
||||
: comparable;
|
||||
return {
|
||||
status: comparable && (!previewPair || geometricAligned) ? "pass" : "fail",
|
||||
status: comparable && (!strictGeometry || geometricAligned) ? "pass" : "fail",
|
||||
comparable,
|
||||
geometricAligned,
|
||||
thresholds,
|
||||
thresholds: resolvedThresholds,
|
||||
periodsMatch,
|
||||
[`${leftName}SamplePeriodMs`]: leftSamplePeriodMs,
|
||||
[`${rightName}SamplePeriodMs`]: rightSamplePeriodMs,
|
||||
@@ -267,6 +334,322 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
|
||||
};
|
||||
}
|
||||
|
||||
function compareLineExecutionTrace(nativeEvidence, webEvidence) {
|
||||
const nativeTrace = Array.isArray(nativeEvidence.lineExecutionTrace)
|
||||
? nativeEvidence.lineExecutionTrace
|
||||
: nativeEvidence.semanticExecutionPath?.lineExecutionTrace || [];
|
||||
const webTrace = Array.isArray(webEvidence.lineExecutionTrace)
|
||||
? webEvidence.lineExecutionTrace
|
||||
: webEvidence.semanticExecutionPath?.lineExecutionTrace || [];
|
||||
const count = Math.min(nativeTrace.length, webTrace.length);
|
||||
const mismatches = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const left = nativeTrace[index];
|
||||
const right = webTrace[index];
|
||||
const keys = ["sourceFile", "line", "operation", "motionType", "activeKinematicsAfter", "producesMotion", "segmentIndex"];
|
||||
const differences = keys
|
||||
.filter((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]))
|
||||
.map((key) => ({ key, native: left?.[key], web: right?.[key] }));
|
||||
if (differences.length > 0) {
|
||||
mismatches.push({
|
||||
index,
|
||||
native: projectTraceEntry(left),
|
||||
web: projectTraceEntry(right),
|
||||
differences,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nativeTrace.length !== webTrace.length) {
|
||||
mismatches.push({
|
||||
index: count,
|
||||
differences: [{
|
||||
key: "traceCount",
|
||||
native: nativeTrace.length,
|
||||
web: webTrace.length,
|
||||
}],
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: mismatches.length === 0 && nativeTrace.length > 0 && webTrace.length > 0 ? "pass" : "fail",
|
||||
nativeTraceCount: nativeTrace.length,
|
||||
webTraceCount: webTrace.length,
|
||||
mismatchCount: mismatches.length,
|
||||
mismatches,
|
||||
semanticBoundary: "native_web_line_by_line_gcode_execution_trace_comparison",
|
||||
};
|
||||
}
|
||||
|
||||
function compareAxisValuesByLine(nativeEvidence, webEvidence) {
|
||||
const nativeValues = Array.isArray(nativeEvidence.axisValuesByLine)
|
||||
? nativeEvidence.axisValuesByLine
|
||||
: nativeEvidence.semanticExecutionPath?.axisValuesByLine || [];
|
||||
const webValues = Array.isArray(webEvidence.axisValuesByLine)
|
||||
? webEvidence.axisValuesByLine
|
||||
: webEvidence.semanticExecutionPath?.axisValuesByLine || [];
|
||||
const count = Math.min(nativeValues.length, webValues.length);
|
||||
const mismatches = [];
|
||||
let maxTcpErrorMm = 0;
|
||||
let maxJointError = 0;
|
||||
let maxToolAxisAngleDeg = 0;
|
||||
const thresholds = {
|
||||
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
|
||||
maxJointError: EXECUTION_JOINT_MAX_ERROR,
|
||||
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
|
||||
};
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const left = nativeValues[index];
|
||||
const right = webValues[index];
|
||||
const tcpError = vectorError(left?.tcp, right?.tcp, ["x", "y", "z"]);
|
||||
const jointError = vectorError(left?.joint, right?.joint, ["x", "y", "z", "b", "c"]);
|
||||
const angleError = toolAxisAngleDeg(left?.toolAxis, right?.toolAxis);
|
||||
const machineStateMatches = compareJsonStable(left?.machineState, right?.machineState);
|
||||
maxTcpErrorMm = Math.max(maxTcpErrorMm, tcpError);
|
||||
maxJointError = Math.max(maxJointError, jointError);
|
||||
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
|
||||
const identityMismatch = ["sourceFile", "line", "operation", "motionType", "activeKinematics", "segmentIndex"]
|
||||
.some((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]));
|
||||
if (identityMismatch
|
||||
|| tcpError > thresholds.maxTcpErrorMm
|
||||
|| jointError > thresholds.maxJointError
|
||||
|| angleError > thresholds.maxToolAxisAngleDeg
|
||||
|| !machineStateMatches) {
|
||||
mismatches.push({
|
||||
index,
|
||||
native: projectAxisValue(left),
|
||||
web: projectAxisValue(right),
|
||||
tcpError,
|
||||
jointError,
|
||||
toolAxisAngleDeg: angleError,
|
||||
machineStateMatches,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nativeValues.length !== webValues.length) {
|
||||
mismatches.push({
|
||||
index: count,
|
||||
nativeLineValueCount: nativeValues.length,
|
||||
webLineValueCount: webValues.length,
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: mismatches.length === 0 && nativeValues.length > 0 && webValues.length > 0 ? "pass" : "fail",
|
||||
nativeLineValueCount: nativeValues.length,
|
||||
webLineValueCount: webValues.length,
|
||||
thresholds,
|
||||
maxTcpErrorMm,
|
||||
maxJointError,
|
||||
maxToolAxisAngleDeg,
|
||||
mismatchCount: mismatches.length,
|
||||
mismatches,
|
||||
semanticBoundary: "native_web_executed_line_axis_values_comparison",
|
||||
};
|
||||
}
|
||||
|
||||
function compareGcodeExecutionProcess(nativeEvidence, webEvidence) {
|
||||
const nativeProcess = nativeEvidence.gcodeExecutionProcess || nativeEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
|
||||
const webProcess = webEvidence.gcodeExecutionProcess || webEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
|
||||
const nativeSteps = Array.isArray(nativeProcess?.executionSteps) ? nativeProcess.executionSteps : [];
|
||||
const webSteps = Array.isArray(webProcess?.executionSteps) ? webProcess.executionSteps : [];
|
||||
const nativeCoverage = Array.isArray(nativeProcess?.sourceLineCoverage) ? nativeProcess.sourceLineCoverage : [];
|
||||
const webCoverage = Array.isArray(webProcess?.sourceLineCoverage) ? webProcess.sourceLineCoverage : [];
|
||||
const mismatches = [];
|
||||
|
||||
if (nativeProcess?.status !== "ok" || webProcess?.status !== "ok") {
|
||||
mismatches.push({
|
||||
index: 0,
|
||||
field: "status",
|
||||
native: nativeProcess?.status ?? null,
|
||||
web: webProcess?.status ?? null,
|
||||
});
|
||||
}
|
||||
compareCoverage(nativeCoverage, webCoverage, mismatches);
|
||||
|
||||
const stepCount = Math.min(nativeSteps.length, webSteps.length);
|
||||
for (let index = 0; index < stepCount; index += 1) {
|
||||
const left = nativeSteps[index];
|
||||
const right = webSteps[index];
|
||||
const differences = [];
|
||||
for (const key of ["stepIndex", "sourceFile", "line", "statement", "callDepth", "executed", "sourceLineKind"]) {
|
||||
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
|
||||
differences.push({ key, native: left?.[key], web: right?.[key] });
|
||||
}
|
||||
}
|
||||
for (const key of [
|
||||
"operation",
|
||||
"sourceLineKind",
|
||||
"executed",
|
||||
"activeKinematicsBefore",
|
||||
"activeKinematicsAfter",
|
||||
"traceExecutionIndex",
|
||||
]) {
|
||||
if (normalizeComparable(left?.result?.[key]) !== normalizeComparable(right?.result?.[key])) {
|
||||
differences.push({ key: `result.${key}`, native: left?.result?.[key], web: right?.result?.[key] });
|
||||
}
|
||||
}
|
||||
if (!compareJsonStable(left?.result?.machineStateAfter, right?.result?.machineStateAfter)) {
|
||||
differences.push({
|
||||
key: "result.machineStateAfter",
|
||||
native: left?.result?.machineStateAfter,
|
||||
web: right?.result?.machineStateAfter,
|
||||
});
|
||||
}
|
||||
if (JSON.stringify(left?.result?.parametersChanged || {}) !== JSON.stringify(right?.result?.parametersChanged || {})) {
|
||||
differences.push({
|
||||
key: "result.parametersChanged",
|
||||
native: left?.result?.parametersChanged,
|
||||
web: right?.result?.parametersChanged,
|
||||
});
|
||||
}
|
||||
const leftMotion = left?.result?.motion || null;
|
||||
const rightMotion = right?.result?.motion || null;
|
||||
if (Boolean(leftMotion) !== Boolean(rightMotion)) {
|
||||
differences.push({ key: "result.motion", native: Boolean(leftMotion), web: Boolean(rightMotion) });
|
||||
} else if (leftMotion && rightMotion) {
|
||||
const tcpError = vectorError(leftMotion.endTcp, rightMotion.endTcp, ["x", "y", "z"]);
|
||||
const jointError = vectorError(leftMotion.endJoint, rightMotion.endJoint, ["x", "y", "z", "b", "c"]);
|
||||
const axisError = toolAxisAngleDeg(leftMotion.endToolAxis, rightMotion.endToolAxis);
|
||||
if (tcpError > EXECUTION_TCP_MAX_ERROR_MM || jointError > EXECUTION_JOINT_MAX_ERROR || axisError > EXECUTION_TOOL_AXIS_MAX_ERROR_DEG) {
|
||||
differences.push({
|
||||
key: "result.motion.axisValues",
|
||||
tcpError,
|
||||
jointError,
|
||||
toolAxisAngleDeg: axisError,
|
||||
native: projectMotion(leftMotion),
|
||||
web: projectMotion(rightMotion),
|
||||
});
|
||||
}
|
||||
for (const key of ["segmentIndex", "motionType", "feed"]) {
|
||||
if (normalizeComparable(leftMotion[key]) !== normalizeComparable(rightMotion[key])) {
|
||||
differences.push({ key: `result.motion.${key}`, native: leftMotion[key], web: rightMotion[key] });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (differences.length > 0) {
|
||||
mismatches.push({
|
||||
index,
|
||||
native: projectGcodeStep(left),
|
||||
web: projectGcodeStep(right),
|
||||
differences,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nativeSteps.length !== webSteps.length) {
|
||||
mismatches.push({
|
||||
index: stepCount,
|
||||
field: "executionSteps.length",
|
||||
native: nativeSteps.length,
|
||||
web: webSteps.length,
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: mismatches.length === 0 && nativeSteps.length > 0 && webSteps.length > 0 ? "pass" : "fail",
|
||||
nativeExecutionStepCount: nativeSteps.length,
|
||||
webExecutionStepCount: webSteps.length,
|
||||
nativeSourceLineCoverageCount: nativeCoverage.length,
|
||||
webSourceLineCoverageCount: webCoverage.length,
|
||||
nativeSummary: nativeProcess?.summary || null,
|
||||
webSummary: webProcess?.summary || null,
|
||||
mismatchCount: mismatches.length,
|
||||
mismatches,
|
||||
semanticBoundary: "native_web_complete_gcode_execution_process_json_comparison",
|
||||
};
|
||||
}
|
||||
|
||||
function compareCoverage(nativeCoverage, webCoverage, mismatches) {
|
||||
const count = Math.min(nativeCoverage.length, webCoverage.length);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const left = nativeCoverage[index];
|
||||
const right = webCoverage[index];
|
||||
const differences = [];
|
||||
for (const key of ["sourceFile", "line", "statement", "sourceLineKind", "visitCount", "producedMotionCount"]) {
|
||||
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
|
||||
differences.push({ key: `coverage.${key}`, native: left?.[key], web: right?.[key] });
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(left?.operations || []) !== JSON.stringify(right?.operations || [])) {
|
||||
differences.push({ key: "coverage.operations", native: left?.operations, web: right?.operations });
|
||||
}
|
||||
if (differences.length > 0) {
|
||||
mismatches.push({
|
||||
index,
|
||||
field: "sourceLineCoverage",
|
||||
native: left,
|
||||
web: right,
|
||||
differences,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nativeCoverage.length !== webCoverage.length) {
|
||||
mismatches.push({
|
||||
index: count,
|
||||
field: "sourceLineCoverage.length",
|
||||
native: nativeCoverage.length,
|
||||
web: webCoverage.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function projectGcodeStep(step = {}) {
|
||||
return {
|
||||
stepIndex: step.stepIndex,
|
||||
sourceFile: step.sourceFile,
|
||||
line: step.line,
|
||||
statement: step.statement,
|
||||
callDepth: step.callDepth,
|
||||
operation: step.result?.operation,
|
||||
sourceLineKind: step.sourceLineKind,
|
||||
activeKinematicsAfter: step.result?.activeKinematicsAfter,
|
||||
traceExecutionIndex: step.result?.traceExecutionIndex,
|
||||
motion: step.result?.motion ? projectMotion(step.result.motion) : null,
|
||||
parametersChanged: step.result?.parametersChanged,
|
||||
machineStateAfter: step.result?.machineStateAfter,
|
||||
};
|
||||
}
|
||||
|
||||
function projectMotion(motion = {}) {
|
||||
return {
|
||||
segmentIndex: motion.segmentIndex,
|
||||
motionType: motion.motionType,
|
||||
feed: motion.feed,
|
||||
endJoint: motion.endJoint,
|
||||
endTcp: motion.endTcp,
|
||||
endToolAxis: motion.endToolAxis,
|
||||
};
|
||||
}
|
||||
|
||||
function projectTraceEntry(entry = {}) {
|
||||
return {
|
||||
sourceFile: entry.sourceFile,
|
||||
line: entry.line,
|
||||
operation: entry.operation,
|
||||
motionType: entry.motionType,
|
||||
activeKinematicsAfter: entry.activeKinematicsAfter,
|
||||
producesMotion: entry.producesMotion,
|
||||
segmentIndex: entry.segmentIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function projectAxisValue(entry = {}) {
|
||||
return {
|
||||
sourceFile: entry.sourceFile,
|
||||
line: entry.line,
|
||||
operation: entry.operation,
|
||||
motionType: entry.motionType,
|
||||
activeKinematics: entry.activeKinematics,
|
||||
segmentIndex: entry.segmentIndex,
|
||||
joint: entry.joint,
|
||||
tcp: entry.tcp,
|
||||
toolAxis: entry.toolAxis,
|
||||
machineState: entry.machineState,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeComparable(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
|
||||
return value;
|
||||
}
|
||||
|
||||
function pathStats(leftSamples, rightSamples) {
|
||||
const count = Math.min(leftSamples.length, rightSamples.length);
|
||||
const missingSamples = [];
|
||||
@@ -275,6 +658,7 @@ function pathStats(leftSamples, rightSamples) {
|
||||
let maxJointError = 0;
|
||||
let sumJointErrorSquared = 0;
|
||||
let maxToolAxisAngleDeg = 0;
|
||||
const machineStateMismatches = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const left = leftSamples[index];
|
||||
const right = rightSamples[index];
|
||||
@@ -289,6 +673,13 @@ function pathStats(leftSamples, rightSamples) {
|
||||
maxJointError = Math.max(maxJointError, jointError);
|
||||
sumJointErrorSquared += jointError ** 2;
|
||||
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
|
||||
if (!compareJsonStable(left.machineState, right.machineState)) {
|
||||
machineStateMismatches.push({
|
||||
index,
|
||||
native: left.machineState,
|
||||
web: right.machineState,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
maxTcpErrorMm,
|
||||
@@ -298,6 +689,8 @@ function pathStats(leftSamples, rightSamples) {
|
||||
maxToolAxisAngleDeg,
|
||||
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
|
||||
missingSamples,
|
||||
machineStateMismatchCount: machineStateMismatches.length,
|
||||
machineStateMismatches: machineStateMismatches.slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -310,9 +703,25 @@ function emptyStats(leftSamples, rightSamples) {
|
||||
maxToolAxisAngleDeg: null,
|
||||
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
|
||||
missingSamples: [],
|
||||
machineStateMismatchCount: null,
|
||||
machineStateMismatches: [],
|
||||
};
|
||||
}
|
||||
|
||||
function compareJsonStable(left, right) {
|
||||
return JSON.stringify(normalizeForJsonCompare(left)) === JSON.stringify(normalizeForJsonCompare(right));
|
||||
}
|
||||
|
||||
function normalizeForJsonCompare(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
|
||||
if (Array.isArray(value)) return value.map((item) => normalizeForJsonCompare(item));
|
||||
if (typeof value === "object") {
|
||||
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeForJsonCompare(value[key])]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function vectorError(left = {}, right = {}, keys = []) {
|
||||
return Math.sqrt(keys.reduce((sum, key) => (
|
||||
sum + (numberOrZero(left[key]) - numberOrZero(right[key])) ** 2
|
||||
|
||||
Reference in New Issue
Block a user