#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" UPSTREAM_RS274="$ROOT_DIR/../linuxcnc/bin/rs274" GCODE_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/gcode" CANON_FIXTURE_DIR="$ROOT_DIR/tests/fixtures/canon" FIXTURES=( minimal_linear arc_semantics canned_cycles canon_runtime_edges comment_logging coordinate_offsets cutter_comp_motion feed_control_modes file_open_reset g53_machine_coordinates length_units plane_selection modal_incremental numbered_params nurbs_g5_semantics nurbs_g6_semantics oword_subroutine percent_file_finish position_params probe_semantics program_end_modal_reset spindle_orient state_tag_motion threading_sync tool_semantics tool_table_setup tool_reload ) if [[ ! -x "$UPSTREAM_RS274" ]]; then echo "missing upstream rs274 executable: $UPSTREAM_RS274" >&2 exit 1 fi TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT TOOL_TABLE_BASELINE="$TMP_DIR/tool_table_setup.tbl" cat > "$TOOL_TABLE_BASELINE" <<'TOOL_TABLE' T0 P0 ; no tool T2 P2 Z1.25 D0.25 ; standalone fixture tool 2 TOOL_TABLE TOOL_TABLE_INI="$TMP_DIR/tool_table_setup.ini" cat > "$TOOL_TABLE_INI" <<'TOOL_INI' [TRAJ] LINEAR_UNITS = mm TOOL_INI normalize_standalone_events() { awk ' function norm(value) { return sprintf("%.12g", value + 0) } function norm_arc(value, numeric) { numeric = value + 0 if (numeric > -0.00005 && numeric < 0.00005) { numeric = 0 } return sprintf("%.4f", numeric) } /^canon_event=/ || /^(STRAIGHT_(TRAVERSE|FEED)|SET_FEED_RATE) / { line = $0 sub(/^canon_event=/, "", line) if (line == "ON_RESET") { print line next } if (line ~ /^COMMENT: /) { sub(/^COMMENT: /, "", line) print "COMMENT|" line next } if (line ~ /^MESSAGE: /) { sub(/^MESSAGE: /, "", line) print "MESSAGE|" line next } if (line ~ /^LOG(OPEN|APPEND)?: /) { op = line sub(/:.*/, "", op) sub(/^[^:]+: /, "", line) print op "|" line next } if (line == "LOGCLOSE" || line == "FINISH" || line == "RELOAD_TOOLDATA") { print line next } if (line == "CHANGE_TOOL") { print line next } if (line ~ /^NURBS_G5_FEED /) { print "NURBS_G5_FEED" next } if (line ~ /^NURBS_G6_FEED /) { print "NURBS_G6_FEED" next } if (line == "MIST_ON" || line == "MIST_OFF" || line == "FLOOD_ON" || line == "FLOOD_OFF" || line == "ENABLE_FEED_OVERRIDE" || line == "DISABLE_FEED_OVERRIDE" || line == "DISABLE_ADAPTIVE_FEED" || line == "ENABLE_ADAPTIVE_FEED" || line == "DISABLE_FEED_HOLD" || line == "ENABLE_FEED_HOLD" || line == "PROGRAM_STOP" || line == "PROGRAM_END") { print line next } if (line ~ /^SELECT_PLANE plane=/) { value = line sub(/^SELECT_PLANE plane=/, "", value) if (value == "1") { print "SELECT_PLANE|XY" } else if (value == "2") { print "SELECT_PLANE|YZ" } else if (value == "3") { print "SELECT_PLANE|XZ" } next } if (line ~ /^USE_LENGTH_UNITS units=/) { value = line sub(/^USE_LENGTH_UNITS units=/, "", value) if (value == "1") { print "USE_LENGTH_UNITS|INCHES" } else if (value == "2") { print "USE_LENGTH_UNITS|MM" } next } if (line ~ /^SET_G5X_OFFSET index=/) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_G5X_OFFSET|" values["index"] "|" norm(values["x"]) "|" \ norm(values["y"]) "|" norm(values["z"]) "|" norm(values["a"]) "|" \ norm(values["b"]) "|" norm(values["c"]) delete values next } if (line ~ /^SET_G92_OFFSET /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_G92_OFFSET|" norm(values["x"]) "|" norm(values["y"]) "|" \ norm(values["z"]) "|" norm(values["a"]) "|" norm(values["b"]) "|" \ norm(values["c"]) delete values next } if (line ~ /^SET_XY_ROTATION rotation=/) { value = line sub(/^SET_XY_ROTATION rotation=/, "", value) print "SET_XY_ROTATION|" norm(value) next } if (line ~ /^SET_FEED_RATE rate=/) { value = line sub(/^SET_FEED_RATE rate=/, "", value) print "SET_FEED_RATE|" norm(value) next } if (line ~ /^SET_SPINDLE_SPEED /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_SPINDLE_SPEED|" values["spindle"] "|" norm(values["speed"]) delete values next } if (line ~ /^START_SPINDLE_(CLOCKWISE|COUNTERCLOCKWISE) /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print fields[1] "|" values["spindle"] delete values next } if (line ~ /^STOP_SPINDLE_TURNING /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "STOP_SPINDLE_TURNING|" values["spindle"] delete values next } if (line ~ /^SELECT_TOOL /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SELECT_TOOL|" values["tool"] delete values next } if (line ~ /^CHANGE_TOOL_NUMBER /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "CHANGE_TOOL_NUMBER|" values["pocket"] delete values next } if (line ~ /^USE_TOOL_LENGTH_OFFSET /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "USE_TOOL_LENGTH_OFFSET|" norm(values["x"]) "|" norm(values["y"]) "|" norm(values["z"]) delete values next } if (line ~ /^(ENABLE|DISABLE)_SPEED_OVERRIDE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print fields[1] "|" values["spindle"] delete values next } if (line ~ /^(SET|CLEAR)_(MOTION|AUX)_OUTPUT_BIT /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print fields[1] "|" values["index"] delete values next } if (line ~ /^SET_(MOTION|AUX)_OUTPUT_VALUE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print fields[1] "|" values["index"] "|" norm(values["value"]) delete values next } if (line ~ /^SET_SPINDLE_MODE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_SPINDLE_MODE|" values["spindle"] "|" norm(values["css_max"]) delete values next } if (line ~ /^SET_TOOL_TABLE_ENTRY /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_TOOL_TABLE_ENTRY|" values["toolno"] "|" norm(values["x"]) "|" \ norm(values["y"]) "|" norm(values["z"]) "|" \ norm(values["frontangle"]) "|" norm(values["backangle"]) "|" values["orientation"] delete values next } if (line ~ /^START_SPEED_FEED_SYNCH /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "START_SPEED_FEED_SYNCH|" norm(values["feed_per_revolution"]) "|" values["velocity_mode"] delete values next } if (line == "STOP_SPEED_FEED_SYNCH") { print line next } if (line ~ /^RIGID_TAP /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "RIGID_TAP|" norm(values["x"]) "|" norm(values["y"]) "|" norm(values["z"]) delete values next } if (line ~ /^ARC_FEED /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "ARC_FEED|" norm_arc(values["first_end"]) "|" norm_arc(values["second_end"]) "|" \ norm_arc(values["first_axis"]) "|" norm_arc(values["second_axis"]) "|" values["rotation"] "|" \ norm_arc(values["axis_end_point"]) "|" norm_arc(values["a"]) "|" norm_arc(values["b"]) "|" \ norm_arc(values["c"]) delete values next } # Upstream rs274 does not emit saicanon WAIT for the M66 case. if (line ~ /^WAIT /) { next } if (line ~ /^SET_FEED_MODE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_FEED_MODE|" values["spindle"] "|" values["mode"] delete values next } if (line ~ /^SET_MOTION_CONTROL_MODE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "SET_MOTION_CONTROL_MODE|" values["mode"] "|" norm(values["tolerance"]) delete values next } if (line ~ /^SET_NAIVECAM_TOLERANCE tolerance=/) { value = line sub(/^SET_NAIVECAM_TOLERANCE tolerance=/, "", value) print "SET_NAIVECAM_TOLERANCE|" norm(value) next } if (line ~ /^DWELL seconds=/) { value = line sub(/^DWELL seconds=/, "", value) print "DWELL|" norm(value) next } if (line ~ /^ORIENT_SPINDLE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "ORIENT_SPINDLE|" values["spindle"] "|" norm(values["orientation"]) "|" values["mode"] delete values next } if (line ~ /^WAIT_SPINDLE_ORIENT_COMPLETE /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } print "WAIT_SPINDLE_ORIENT_COMPLETE|" values["spindle"] "|" norm(values["timeout"]) delete values next } if (line == "TURN_PROBE_ON" || line == "TURN_PROBE_OFF") { print line next } if (line ~ /^STRAIGHT_PROBE /) { print "STRAIGHT_PROBE" next } if (line ~ /^STRAIGHT_(TRAVERSE|FEED) /) { split(line, fields, /[[:space:]]+/) for (i in fields) { split(fields[i], pair, "=") values[pair[1]] = pair[2] } if (!("x" in values)) { print fields[1] delete values next } print fields[1] "|" norm(values["x"]) "|" norm(values["y"]) "|" \ norm(values["z"]) "|" norm(values["a"]) "|" norm(values["b"]) "|" \ norm(values["c"]) delete values next } } ' } normalize_upstream_rs274_output() { awk ' function norm(value) { return sprintf("%.12g", value + 0) } function norm_arc(value, numeric) { numeric = value + 0 if (numeric > -0.00005 && numeric < 0.00005) { numeric = 0 } return sprintf("%.4f", numeric) } function trim(value) { sub(/^[[:space:]]+/, "", value) sub(/[[:space:]]+$/, "", value) return value } function args_of(call, args) { args = call sub(/^[^(]+\(/, "", args) sub(/\).*$/, "", args) return args } /^[[:space:]]*[0-9]+[[:space:]]+N\.\.\.\.\./ { line = $0 sub(/^[[:space:]]*[0-9]+[[:space:]]+N\.\.\.\.\.[[:space:]]+/, "", line) if (line == "ON_RESET()") { if (!seen_reset) { print "ON_RESET" } seen_reset = 1 next } if (!seen_reset) { next } if (line ~ /^NURBS_G5_FEED\(/) { print "NURBS_G5_FEED" next } if (line ~ /^COMMENT\("/) { value = args_of(line) sub(/^"/, "", value) sub(/"$/, "", value) print "COMMENT|" value next } if (line ~ /^MESSAGE\("/) { value = args_of(line) sub(/^"/, "", value) sub(/"$/, "", value) print "MESSAGE|" value next } if (line ~ /^LOG(OPEN|APPEND)?\("/) { op = line sub(/\(.*/, "", op) value = args_of(line) sub(/^"/, "", value) sub(/"$/, "", value) print op "|" value next } if (line == "LOGCLOSE()" || line == "FINISH()" || line == "RELOAD_TOOLDATA()") { value = line sub(/\(\)$/, "", value) print value next } if (line == "CHANGE_TOOL()") { print "CHANGE_TOOL" next } if (line ~ /^(MIST_ON|MIST_OFF|FLOOD_ON|FLOOD_OFF|ENABLE_FEED_OVERRIDE|DISABLE_FEED_OVERRIDE|DISABLE_ADAPTIVE_FEED|ENABLE_ADAPTIVE_FEED|DISABLE_FEED_HOLD|ENABLE_FEED_HOLD|PROGRAM_STOP|PROGRAM_END)\(\)$/) { value = line sub(/\(\)$/, "", value) print value next } if (line ~ /^SELECT_PLANE\(CANON_PLANE_/) { value = line sub(/^SELECT_PLANE\(CANON_PLANE_/, "", value) sub(/\)$/, "", value) print "SELECT_PLANE|" value next } if (line ~ /^USE_LENGTH_UNITS\(CANON_UNITS_/) { value = line sub(/^USE_LENGTH_UNITS\(CANON_UNITS_/, "", value) sub(/\)$/, "", value) print "USE_LENGTH_UNITS|" value next } if (line ~ /^SET_G5X_OFFSET\(/) { args = args_of(line) split(args, values, ",") print "SET_G5X_OFFSET|" trim(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \ norm(values[4]) "|" norm(values[5]) "|" norm(values[6]) "|" norm(values[7]) next } if (line ~ /^SET_G92_OFFSET\(/) { args = args_of(line) split(args, values, ",") print "SET_G92_OFFSET|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \ norm(values[4]) "|" norm(values[5]) "|" norm(values[6]) next } if (line ~ /^SET_XY_ROTATION\(/) { args = args_of(line) print "SET_XY_ROTATION|" norm(args) next } if (line ~ /^SET_FEED_RATE\(/) { args = args_of(line) print "SET_FEED_RATE|" norm(args) next } if (line ~ /^SET_SPINDLE_SPEED\(/) { args = args_of(line) split(args, values, ",") print "SET_SPINDLE_SPEED|" trim(values[1]) "|" norm(values[2]) next } if (line ~ /^START_SPINDLE_(CLOCKWISE|COUNTERCLOCKWISE)\(/) { op = line sub(/\(.*/, "", op) args = args_of(line) print op "|" trim(args) next } if (line ~ /^STOP_SPINDLE_TURNING\(/) { args = args_of(line) print "STOP_SPINDLE_TURNING|" trim(args) next } if (line ~ /^SELECT_TOOL\(/) { args = args_of(line) print "SELECT_TOOL|" trim(args) next } if (line ~ /^CHANGE_TOOL_NUMBER\(/) { args = args_of(line) print "CHANGE_TOOL_NUMBER|" trim(args) next } if (line ~ /^USE_TOOL_LENGTH_OFFSET\(/) { args = args_of(line) split(args, groups, ",") gsub(/[[:space:]]+/, " ", groups[1]) split(trim(groups[1]), offset, " ") print "USE_TOOL_LENGTH_OFFSET|" norm(offset[1]) "|" norm(offset[2]) "|" norm(offset[3]) next } if (line ~ /^(ENABLE|DISABLE)_SPEED_OVERRIDE\(/) { op = line sub(/\(.*/, "", op) args = args_of(line) print op "|" trim(args) next } if (line ~ /^(SET|CLEAR)_(MOTION|AUX)_OUTPUT_BIT\(/) { op = line sub(/\(.*/, "", op) args = args_of(line) print op "|" trim(args) next } if (line ~ /^SET_(MOTION|AUX)_OUTPUT_VALUE\(/) { op = line sub(/\(.*/, "", op) args = args_of(line) split(args, values, ",") print op "|" trim(values[1]) "|" norm(values[2]) next } if (line ~ /^SET_SPINDLE_MODE\(/) { args = args_of(line) gsub(/[[:space:]]+/, " ", args) split(trim(args), values, " ") print "SET_SPINDLE_MODE|" values[1] "|" norm(values[2]) next } if (line ~ /^SET_TOOL_TABLE_ENTRY\(/) { args = args_of(line) split(args, values, ",") gsub(/[[:space:]]+/, " ", values[3]) split(trim(values[3]), offset, " ") print "SET_TOOL_TABLE_ENTRY|" trim(values[2]) "|" norm(offset[1]) "|" \ norm(offset[2]) "|" norm(offset[3]) "|" \ norm(values[4]) "|" norm(values[5]) "|" trim(values[6]) next } if (line ~ /^START_SPEED_FEED_SYNC\(/) { args = args_of(line) split(args, values, ",") print "START_SPEED_FEED_SYNCH|" norm(values[1]) "|" trim(values[2]) next } if (line == "STOP_SPEED_FEED_SYNCH()") { print "STOP_SPEED_FEED_SYNCH" next } if (line ~ /^RIGID_TAP\(/) { args = args_of(line) split(args, values, ",") print "RIGID_TAP|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3]) next } if (line ~ /^ARC_FEED\(/) { args = args_of(line) split(args, values, ",") print "ARC_FEED|" norm_arc(values[1]) "|" norm_arc(values[2]) "|" norm_arc(values[3]) "|" \ norm_arc(values[4]) "|" trim(values[5]) "|" norm_arc(values[6]) "|" norm_arc(values[7]) "|" \ norm_arc(values[8]) "|" norm_arc(values[9]) next } if (line ~ /^SET_FEED_MODE\(/) { args = args_of(line) split(args, values, ",") print "SET_FEED_MODE|" trim(values[1]) "|" trim(values[2]) next } if (line ~ /^SET_MOTION_CONTROL_MODE\(/) { args = args_of(line) split(args, values, ",") mode_name = trim(values[1]) if (mode_name == "CANON_EXACT_STOP") { mode = 1 } else if (mode_name == "CANON_EXACT_PATH") { mode = 2 } else if (mode_name == "CANON_CONTINUOUS") { mode = 3 } else { next } print "SET_MOTION_CONTROL_MODE|" mode "|" norm(values[2]) next } if (line ~ /^SET_NAIVECAM_TOLERANCE\(/) { args = args_of(line) print "SET_NAIVECAM_TOLERANCE|" norm(args) next } if (line ~ /^DWELL\(/) { args = args_of(line) print "DWELL|" norm(args) next } if (line ~ /^ORIENT_SPINDLE\(/) { args = args_of(line) split(args, values, ",") print "ORIENT_SPINDLE|" trim(values[1]) "|" norm(values[2]) "|" trim(values[3]) next } if (line ~ /^SPINDLE\.[0-9]+\.WAIT_ORIENT_COMPLETE\(/) { spindle = line sub(/^SPINDLE\./, "", spindle) sub(/\.WAIT_ORIENT_COMPLETE.*/, "", spindle) args = args_of(line) print "WAIT_SPINDLE_ORIENT_COMPLETE|" spindle "|" norm(args) next } if (line == "TURN_PROBE_ON()" || line == "TURN_PROBE_OFF()") { value = line sub(/\(\)$/, "", value) print value next } if (line ~ /^STRAIGHT_PROBE\(/) { print "STRAIGHT_PROBE" next } if (line ~ /^STRAIGHT_(TRAVERSE|FEED)\(/) { op = line sub(/\(.*/, "", op) args = args_of(line) split(args, values, ",") if (op in sparse_ops) { print op next } print op "|" norm(values[1]) "|" norm(values[2]) "|" norm(values[3]) "|" \ norm(values[4]) "|" norm(values[5]) "|" norm(values[6]) next } } /^N\.\.\.\.\. saicanon NURBS_G6_FEED_/ && seen_reset { print "NURBS_G6_FEED" next } ' } filter_to_expected_event_keys() { local expected_norm="$1" awk ' NR == FNR { key = $0 sub(/\|.*/, "", key) keys[key] = 1 next } { key = $0 sub(/\|.*/, "", key) if (key in keys) { print } } ' "$expected_norm" - } for name in "${FIXTURES[@]}"; do gcode_file="$GCODE_FIXTURE_DIR/$name.ngc" expected_file="$CANON_FIXTURE_DIR/$name.events" native_raw="$TMP_DIR/$name.rs274.raw" native_norm="$TMP_DIR/$name.rs274.normalized" native_filtered="$TMP_DIR/$name.rs274.filtered" expected_norm="$TMP_DIR/$name.expected.normalized" if [[ ! -f "$gcode_file" ]]; then echo "missing G-code fixture: $gcode_file" >&2 exit 1 fi if [[ ! -f "$expected_file" ]]; then echo "missing canonical fixture: $expected_file" >&2 exit 1 fi set +e if [[ "$name" == "tool_table_setup" || "$name" == "tool_semantics" ]]; then "$UPSTREAM_RS274" -g -i "$TOOL_TABLE_INI" -t "$TOOL_TABLE_BASELINE" "$gcode_file" > "$native_raw" 2>&1 else "$UPSTREAM_RS274" -g "$gcode_file" > "$native_raw" 2>&1 fi rs274_rc=$? set -e if [[ "$rs274_rc" != "0" ]] && ! grep -Fq "File ended with no percent sign (%) or program end (M2)" "$native_raw"; then echo "upstream rs274 failed for fixture: $name" >&2 sed -n '1,160p' "$native_raw" >&2 exit 1 fi normalize_upstream_rs274_output < "$native_raw" > "$native_norm" normalize_standalone_events < "$expected_file" > "$expected_norm" if [[ "$name" == "arc_semantics" ]]; then grep -v '^COMMENT|' "$native_norm" > "$TMP_DIR/$name.rs274.no-comments" grep -v '^COMMENT|' "$expected_norm" > "$TMP_DIR/$name.expected.no-comments" mv "$TMP_DIR/$name.rs274.no-comments" "$native_norm" mv "$TMP_DIR/$name.expected.no-comments" "$expected_norm" fi if [[ "$name" == "tool_semantics" ]]; then grep -Ev '^(CHANGE_TOOL_NUMBER|MESSAGE\| tool after_m61)' "$native_norm" > "$TMP_DIR/$name.rs274.tool-subset" grep -Ev '^(CHANGE_TOOL_NUMBER|MESSAGE\| tool after_m61)' "$expected_norm" > "$TMP_DIR/$name.expected.tool-subset" mv "$TMP_DIR/$name.rs274.tool-subset" "$native_norm" mv "$TMP_DIR/$name.expected.tool-subset" "$expected_norm" fi if grep -Eq '^STRAIGHT_(TRAVERSE|FEED)$' "$expected_norm"; then sparse_ops_file="$TMP_DIR/$name.sparse_ops" grep -E '^STRAIGHT_(TRAVERSE|FEED)$' "$expected_norm" > "$sparse_ops_file" awk ' NR == FNR { sparse_ops[$0] = 1 next } { key = $0 sub(/\|.*/, "", key) if (key in sparse_ops) { print key } else { print } } ' "$sparse_ops_file" "$native_norm" > "$TMP_DIR/$name.rs274.sparse" mv "$TMP_DIR/$name.rs274.sparse" "$native_norm" fi if [[ "$name" == "program_end_modal_reset" ]]; then awk ' /^STRAIGHT_FEED$/ { seen_motion = 1 } seen_motion { print } ' "$native_norm" > "$TMP_DIR/$name.rs274.program-end" mv "$TMP_DIR/$name.rs274.program-end" "$native_norm" fi filter_to_expected_event_keys "$expected_norm" < "$native_norm" > "$native_filtered" if [[ ! -s "$native_filtered" ]]; then echo "no normalized upstream rs274 events for fixture: $name" >&2 sed -n '1,160p' "$native_raw" >&2 exit 1 fi if ! diff -u "$expected_norm" "$native_filtered"; then echo "native LinuxCNC rs274 baseline mismatch for fixture: $name" >&2 exit 1 fi done echo "native LinuxCNC fixture baseline validation complete"