第 6 组:源码链接、构建和文档约束闭环完成

This commit is contained in:
cnc
2026-06-01 06:11:08 +08:00
parent 1498d9830d
commit 4c55ab3435
43 changed files with 8414 additions and 636 deletions

View File

@@ -5,6 +5,11 @@ cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
manifest=${1:-linuxcnc-rs274-wasm-source-files.txt}
build_jobs=${CNC_SIM_BUILD_JOBS:-8}
if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then
echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2
exit 1
fi
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest"
manifest=$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")
@@ -113,8 +118,8 @@ emcmake cmake -S core -B build/wasm \
-DCNC_SIM_LINUXCNC_ROOT="$linuxcnc_root" \
-DCNC_SIM_LINUXCNC_WASM_SOURCE_MANIFEST="$manifest" \
-DCNC_SIM_ENABLE_LINUXCNC_WASM_SAFE_PROBE=ON
cmake --build build/wasm --target cnc_sim_wasm_runtime_probe
cmake --build build/wasm --target cnc_sim_wasm
cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"
cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs"
mkdir -p web/public
cp build/wasm/cnc_sim.js web/public/cnc_sim.js
cp build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm

View File

@@ -6,6 +6,29 @@ cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
manifest=${1:-}
require_complete=${2:-}
allowed_manifest_groups=(
asset
binding
blocked
blocked-header
component
config
core
generated
header
metadata
remap
tooldata
)
contains_manifest_group() {
local candidate=$1
local group
for group in "${allowed_manifest_groups[@]}"; do
[[ "$group" == "$candidate" ]] && return 0
done
return 1
}
if [[ "$manifest" == "--root-only" ]]; then
manifest=
@@ -32,6 +55,43 @@ if [[ ! -d "$linuxcnc_root" ]]; then
echo "missing LinuxCNC root: $linuxcnc_root" >&2
exit 1
fi
linuxcnc_root=$(cd "$linuxcnc_root" && pwd)
if [[ -n "$manifest" ]]; then
seen_manifest_sources=()
manifest_line_number=0
while IFS= read -r manifest_line || [[ -n "$manifest_line" ]]; do
manifest_line_number=$((manifest_line_number + 1))
case "$manifest_line" in
""|\#*)
continue
;;
esac
IFS=: read -r group path note extra <<<"$manifest_line"
if [[ -z "$group" || -z "$path" || -z "$note" || -n "${extra:-}" ]]; then
echo "bad manifest line $manifest_line_number: $manifest_line" >&2
exit 1
fi
if ! contains_manifest_group "$group"; then
echo "unknown manifest group: $group" >&2
exit 1
fi
if [[ "$path" = /* || "$path" == .. || "$path" == ../* || "$path" == */.. || "$path" == */../* ]]; then
echo "manifest source must be LinuxCNC-root relative: $path" >&2
exit 1
fi
if printf '%s\n' "${seen_manifest_sources[@]}" | grep -Fx -- "$path" >/dev/null; then
echo "duplicate manifest source: $path" >&2
exit 1
fi
if [[ ! -f "$linuxcnc_root/$path" ]]; then
echo "missing manifest source: $path" >&2
exit 1
fi
seen_manifest_sources+=("$path")
done < "$manifest"
fi
if [[ -n "$manifest" && "$require_complete" == "--require-rs274-complete" ]]; then
rs274_dir="$linuxcnc_root/src/emc/rs274ngc"
@@ -96,6 +156,148 @@ if [[ -n "$manifest" && "$require_complete" == "--require-kinematics-complete" ]
exit 1
fi
mapfile -t kinematics_header_sources < <(
find "$kinematics_dir" -maxdepth 1 -type f -name '*.h' \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_kinematics_header_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "header" && $2 ~ /^src\/emc\/kinematics\/.*\.h$/ { print $2 }
' "$manifest" | sort -u
)
mapfile -t kinematics_metadata_sources < <(
find "$kinematics_dir" -maxdepth 1 -type f \
\( -name 'Submakefile' -o -name 'meson.build' \) \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_kinematics_metadata_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "metadata" && $2 ~ /^src\/emc\/kinematics\/(Submakefile|meson\.build)$/ { print $2 }
' "$manifest" | sort -u
)
mapfile -t kinematics_asset_sources < <(
find "$kinematics_dir" -maxdepth 1 -type f -name '*.fig' \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_kinematics_asset_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "asset" && $2 ~ /^src\/emc\/kinematics\/.*\.fig$/ { print $2 }
' "$manifest" | sort -u
)
missing_kinematics_header_sources=()
for source in "${kinematics_header_sources[@]}"; do
if ! printf '%s\n' "${manifest_kinematics_header_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_kinematics_header_sources+=("$source")
fi
done
if ((${#missing_kinematics_header_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC kinematics headers as header sources: $manifest" >&2
printf 'missing kinematics header source: %s\n' "${missing_kinematics_header_sources[@]}" >&2
exit 1
fi
missing_kinematics_metadata_sources=()
for source in "${kinematics_metadata_sources[@]}"; do
if ! printf '%s\n' "${manifest_kinematics_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_kinematics_metadata_sources+=("$source")
fi
done
if ((${#missing_kinematics_metadata_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC kinematics build metadata as metadata sources: $manifest" >&2
printf 'missing kinematics metadata source: %s\n' "${missing_kinematics_metadata_sources[@]}" >&2
exit 1
fi
missing_kinematics_asset_sources=()
for source in "${kinematics_asset_sources[@]}"; do
if ! printf '%s\n' "${manifest_kinematics_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_kinematics_asset_sources+=("$source")
fi
done
if ((${#missing_kinematics_asset_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC kinematics reference assets as asset sources: $manifest" >&2
printf 'missing kinematics asset source: %s\n' "${missing_kinematics_asset_sources[@]}" >&2
exit 1
fi
posemath_dir="$linuxcnc_root/src/libnml/posemath"
if [[ ! -d "$posemath_dir" ]]; then
echo "missing LinuxCNC posemath source directory: $posemath_dir" >&2
exit 1
fi
mapfile -t posemath_core_sources < <(
find "$posemath_dir" -maxdepth 1 -type f \
\( -name '*.c' -o -name '*.cc' \) \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_posemath_core_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "core" && $2 ~ /^src\/libnml\/posemath\/.*\.(c|cc)$/ { print $2 }
' "$manifest" | sort -u
)
mapfile -t posemath_header_sources < <(
find "$posemath_dir" -maxdepth 1 -type f -name '*.h' \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_posemath_header_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "header" && $2 ~ /^src\/libnml\/posemath\/.*\.h$/ { print $2 }
' "$manifest" | sort -u
)
mapfile -t posemath_metadata_sources < <(
find "$posemath_dir" -maxdepth 1 -type f \
\( -name 'Submakefile' -o -name 'meson.build' \) \
| sed "s#^$linuxcnc_root/##" \
| sort
)
mapfile -t manifest_posemath_metadata_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "metadata" && $2 ~ /^src\/libnml\/posemath\/(Submakefile|meson\.build)$/ { print $2 }
' "$manifest" | sort -u
)
missing_posemath_core_sources=()
for source in "${posemath_core_sources[@]}"; do
if ! printf '%s\n' "${manifest_posemath_core_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_posemath_core_sources+=("$source")
fi
done
if ((${#missing_posemath_core_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC posemath C/C++ sources as core sources: $manifest" >&2
printf 'missing posemath core source: %s\n' "${missing_posemath_core_sources[@]}" >&2
exit 1
fi
missing_posemath_header_sources=()
for source in "${posemath_header_sources[@]}"; do
if ! printf '%s\n' "${manifest_posemath_header_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_posemath_header_sources+=("$source")
fi
done
if ((${#missing_posemath_header_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC posemath headers as header sources: $manifest" >&2
printf 'missing posemath header source: %s\n' "${missing_posemath_header_sources[@]}" >&2
exit 1
fi
missing_posemath_metadata_sources=()
for source in "${posemath_metadata_sources[@]}"; do
if ! printf '%s\n' "${manifest_posemath_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_posemath_metadata_sources+=("$source")
fi
done
if ((${#missing_posemath_metadata_sources[@]} > 0)); then
echo "manifest does not group all LinuxCNC posemath build metadata as metadata sources: $manifest" >&2
printf 'missing posemath metadata source: %s\n' "${missing_posemath_metadata_sources[@]}" >&2
exit 1
fi
mapfile -t m428_m430_remap_sources < <(
find "$linuxcnc_root/configs" -path '*/remap_subs/*' -type f \
\( -name '428remap.ngc' -o -name '429remap.ngc' -o -name '430remap.ngc' \) \
@@ -165,4 +367,724 @@ if [[ -n "$manifest" && "$require_complete" == "--require-kinematics-complete" ]
printf 'missing M428/M429/M430 INI source: %s\n' "${missing_m428_m430_ini_sources[@]}" >&2
exit 1
fi
mapfile -t manifest_component_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "component" { print $2 }
' "$manifest" | sort -u
)
mapfile -t manifest_generated_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "generated" { print $2 }
' "$manifest" | sort -u
)
mapfile -t manifest_metadata_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "metadata" { print $2 }
' "$manifest" | sort -u
)
m428_m430_kinematics_component_sources=()
m428_m430_kinematics_generated_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
while IFS= read -r kinematics_name; do
[[ -n "$kinematics_name" ]] || continue
component_source="src/hal/components/$kinematics_name.comp"
generated_source="src/objects/hal/components/$kinematics_name.c"
if [[ -f "$linuxcnc_root/$component_source" ]]; then
m428_m430_kinematics_component_sources+=("$component_source")
fi
if [[ -f "$linuxcnc_root/$generated_source" ]]; then
m428_m430_kinematics_generated_sources+=("$generated_source")
fi
done < <(
sed -n 's/^[[:space:]]*KINEMATICS[[:space:]]*=[[:space:]]*\([^[:space:]#;]*\).*/\1/Ip' "$linuxcnc_root/$source"
)
done
mapfile -t m428_m430_kinematics_component_sources < <(
printf '%s\n' "${m428_m430_kinematics_component_sources[@]}" | sed '/^$/d' | sort -u
)
mapfile -t m428_m430_kinematics_generated_sources < <(
printf '%s\n' "${m428_m430_kinematics_generated_sources[@]}" | sed '/^$/d' | sort -u
)
missing_m428_m430_kinematics_component_sources=()
for source in "${m428_m430_kinematics_component_sources[@]}"; do
if ! printf '%s\n' "${manifest_component_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_kinematics_component_sources+=("$source")
fi
done
if ((${#missing_m428_m430_kinematics_component_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC component sources referenced by M428/M429/M430 INI KINEMATICS entries: $manifest" >&2
printf 'missing M428/M429/M430 KINEMATICS component source: %s\n' "${missing_m428_m430_kinematics_component_sources[@]}" >&2
exit 1
fi
missing_m428_m430_kinematics_generated_sources=()
for source in "${m428_m430_kinematics_generated_sources[@]}"; do
if ! printf '%s\n' "${manifest_generated_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_kinematics_generated_sources+=("$source")
fi
done
if ((${#missing_m428_m430_kinematics_generated_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC generated component sources referenced by M428/M429/M430 INI KINEMATICS entries: $manifest" >&2
printf 'missing M428/M429/M430 KINEMATICS generated source: %s\n' "${missing_m428_m430_kinematics_generated_sources[@]}" >&2
exit 1
fi
generated_component_metadata_sources=()
for source in "${manifest_generated_sources[@]}"; do
case "$source" in
src/objects/hal/components/*.c)
metadata_source=${source%.c}.mak
if [[ -f "$linuxcnc_root/$metadata_source" ]]; then
generated_component_metadata_sources+=("$metadata_source")
fi
;;
esac
done
mapfile -t generated_component_metadata_sources < <(
printf '%s\n' "${generated_component_metadata_sources[@]}" | sed '/^$/d' | sort -u
)
missing_generated_component_metadata_sources=()
for source in "${generated_component_metadata_sources[@]}"; do
if ! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_generated_component_metadata_sources+=("$source")
fi
done
if ((${#missing_generated_component_metadata_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC generated component make metadata adjacent to generated kinematics sources: $manifest" >&2
printf 'missing generated component make metadata source: %s\n' "${missing_generated_component_metadata_sources[@]}" >&2
exit 1
fi
missing_m428_m430_ini_remap_sources=()
unresolved_m428_m430_ini_remaps=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config="$linuxcnc_root/$source"
config_dir=$(cd "$(dirname "$config")" && pwd)
mapfile -t subroutine_paths < <(
sed -n 's/^[[:space:]]*SUBROUTINE_PATH[[:space:]]*=[[:space:]]*//Ip' "$config" |
sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//'
)
if ((${#subroutine_paths[@]} == 0)); then
subroutine_paths=(".")
fi
for mcode in 428 429 430; do
ngc_name=$(sed -n "s/.*REMAP[[:space:]]*=[[:space:]]*M$mcode[[:space:]][^#;]*ngc[[:space:]]*=[[:space:]]*\\([^[:space:]#;]*\\).*/\\1/Ip" "$config" |
head -n 1)
[[ -n "$ngc_name" ]] || continue
[[ "$ngc_name" == *.ngc ]] || ngc_name="$ngc_name.ngc"
resolved_source=
for subroutine_path in "${subroutine_paths[@]}"; do
[[ -n "$subroutine_path" ]] || continue
IFS=: read -ra path_parts <<<"$subroutine_path"
for path_part in "${path_parts[@]}"; do
[[ -n "$path_part" ]] || continue
if [[ "$path_part" = /* ]]; then
candidate_dir=$path_part
else
candidate_dir="$config_dir/$path_part"
fi
if [[ -f "$candidate_dir/$ngc_name" ]]; then
resolved_abs=$(cd "$(dirname "$candidate_dir/$ngc_name")" && pwd)/$(basename "$candidate_dir/$ngc_name")
resolved_source=${resolved_abs#"$linuxcnc_root/"}
break 2
fi
done
done
if [[ -z "$resolved_source" ]]; then
unresolved_m428_m430_ini_remaps+=("$source M$mcode ngc=$ngc_name")
elif ! printf '%s\n' "${manifest_m428_m430_remap_sources[@]}" | grep -Fx -- "$resolved_source" >/dev/null; then
missing_m428_m430_ini_remap_sources+=("$resolved_source")
fi
done
done
if ((${#unresolved_m428_m430_ini_remaps[@]} > 0)); then
echo "LinuxCNC M428/M429/M430 INI remap sources could not be resolved through SUBROUTINE_PATH: $manifest" >&2
printf 'unresolved M428/M429/M430 INI remap: %s\n' "${unresolved_m428_m430_ini_remaps[@]}" >&2
exit 1
fi
if ((${#missing_m428_m430_ini_remap_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC M428/M429/M430 remap sources referenced by INI REMAP entries: $manifest" >&2
printf 'missing INI-referenced M428/M429/M430 remap source: %s\n' "${missing_m428_m430_ini_remap_sources[@]}" >&2
exit 1
fi
mapfile -t manifest_config_hal_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "config" && $2 ~ /\.hal$/ { print $2 }
' "$manifest" | sort -u
)
unresolved_m428_m430_config_hal_sources=()
mapfile -t m428_m430_config_hal_sources < <(
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd)
sed -n 's/^[[:space:]]*\(HALFILE\|POSTGUI_HALFILE\)[[:space:]]*=[[:space:]]*//Ip' "$linuxcnc_root/$source" |
sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' |
while IFS= read -r hal_source; do
[[ -n "$hal_source" && "$hal_source" != LIB:* ]] || continue
if [[ "$hal_source" = /* ]]; then
candidate=$hal_source
else
candidate="$config_dir/$hal_source"
fi
if [[ -f "$candidate" ]]; then
candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")
printf '%s\n' "${candidate_abs#"$linuxcnc_root/"}"
else
printf '%s HALFILE=%s\n' "$source" "$hal_source" >&2
fi
done
done | sort -u
)
mapfile -t unresolved_m428_m430_config_hal_sources < <(
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd)
sed -n 's/^[[:space:]]*\(HALFILE\|POSTGUI_HALFILE\)[[:space:]]*=[[:space:]]*//Ip' "$linuxcnc_root/$source" |
sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//' |
while IFS= read -r hal_source; do
[[ -n "$hal_source" && "$hal_source" != LIB:* ]] || continue
if [[ "$hal_source" = /* ]]; then
candidate=$hal_source
else
candidate="$config_dir/$hal_source"
fi
[[ -f "$candidate" ]] || printf '%s HALFILE=%s\n' "$source" "$hal_source"
done
done | sort -u
)
if ((${#unresolved_m428_m430_config_hal_sources[@]} > 0)); then
echo "LinuxCNC M428/M429/M430 INI HALFILE/POSTGUI_HALFILE sources could not be resolved: $manifest" >&2
printf 'unresolved M428/M429/M430 HAL source: %s\n' "${unresolved_m428_m430_config_hal_sources[@]}" >&2
exit 1
fi
missing_m428_m430_config_hal_sources=()
for source in "${m428_m430_config_hal_sources[@]}"; do
if ! printf '%s\n' "${manifest_config_hal_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_config_hal_sources+=("$source")
fi
done
if ((${#missing_m428_m430_config_hal_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC HALFILE/POSTGUI_HALFILE sources referenced by M428/M429/M430 INI files: $manifest" >&2
printf 'missing M428/M429/M430 HAL source: %s\n' "${missing_m428_m430_config_hal_sources[@]}" >&2
exit 1
fi
mapfile -t manifest_asset_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "asset" { print $2 }
' "$manifest" | sort -u
)
mapfile -t manifest_tooldata_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "tooldata" { print $2 }
' "$manifest" | sort -u
)
resolve_vismach_loadusr_source() {
local command_name=$1
case "$command_name" in
*/*|*\\*|*.py)
return 0
;;
esac
local vismach_source="src/hal/user_comps/vismach/$command_name.py"
if [[ -f "$linuxcnc_root/$vismach_source" ]]; then
printf '%s\n' "$vismach_source"
fi
}
m428_m430_vismach_loadusr_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
while IFS= read -r command_name; do
[[ -n "$command_name" ]] || continue
while IFS= read -r vismach_source; do
[[ -n "$vismach_source" ]] && m428_m430_vismach_loadusr_sources+=("$vismach_source")
done < <(resolve_vismach_loadusr_source "$command_name")
done < <(
awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr[[:space:]]/ {
for (i = 1; i <= NF; i++) {
if ($i == "-W" && (i + 1) <= NF) {
value = $(i + 1)
sub(/[[:space:]#;].*/, "", value)
if (value != "") print value
}
}
}
' "$linuxcnc_root/$source"
)
done
for source in "${m428_m430_config_hal_sources[@]}"; do
while IFS= read -r command_name; do
[[ -n "$command_name" ]] || continue
while IFS= read -r vismach_source; do
[[ -n "$vismach_source" ]] && m428_m430_vismach_loadusr_sources+=("$vismach_source")
done < <(resolve_vismach_loadusr_source "$command_name")
done < <(
awk '
/^[[:space:]]*loadusr[[:space:]]/ {
for (i = 1; i <= NF; i++) {
if ($i == "-W" && (i + 1) <= NF) {
value = $(i + 1)
sub(/[[:space:]#;].*/, "", value)
if (value != "") print value
}
}
}
' "$linuxcnc_root/$source"
)
done
mapfile -t m428_m430_vismach_loadusr_sources < <(
printf '%s\n' "${m428_m430_vismach_loadusr_sources[@]}" | sed '/^$/d' | sort -u
)
missing_m428_m430_vismach_loadusr_sources=()
for source in "${m428_m430_vismach_loadusr_sources[@]}"; do
if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_vismach_loadusr_sources+=("$source")
fi
done
if ((${#missing_m428_m430_vismach_loadusr_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC vismach user component sources referenced by M428/M429/M430 INI/HAL loadusr commands as asset sources: $manifest" >&2
printf 'missing M428/M429/M430 vismach loadusr source: %s\n' "${missing_m428_m430_vismach_loadusr_sources[@]}" >&2
exit 1
fi
if ((${#m428_m430_vismach_loadusr_sources[@]} > 0)); then
vismach_submakefile_source=src/hal/user_comps/vismach/Submakefile
if [[ -f "$linuxcnc_root/$vismach_submakefile_source" ]] &&
! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$vismach_submakefile_source" >/dev/null; then
echo "manifest does not cover LinuxCNC vismach user component build metadata as metadata sources: $manifest" >&2
printf 'missing vismach user component metadata source: %s\n' "$vismach_submakefile_source" >&2
exit 1
fi
fi
m428_m430_vismach_man_sources=()
for source in "${m428_m430_vismach_loadusr_sources[@]}"; do
command_name=$(basename "$source" .py)
man_source="docs/src/man/man1/$command_name.1.adoc"
if [[ -f "$linuxcnc_root/$man_source" ]]; then
m428_m430_vismach_man_sources+=("$man_source")
fi
done
mapfile -t m428_m430_vismach_man_sources < <(
printf '%s\n' "${m428_m430_vismach_man_sources[@]}" | sed '/^$/d' | sort -u
)
missing_m428_m430_vismach_man_sources=()
for source in "${m428_m430_vismach_man_sources[@]}"; do
if ! printf '%s\n' "${manifest_metadata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_vismach_man_sources+=("$source")
fi
done
if ((${#missing_m428_m430_vismach_man_sources[@]} > 0)); then
echo "manifest does not cover LinuxCNC vismach user component manpage sources as metadata sources: $manifest" >&2
printf 'missing vismach user component manpage source: %s\n' "${missing_m428_m430_vismach_man_sources[@]}" >&2
exit 1
fi
qtvcp_panel_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
if grep -Eiq '^[[:space:]]*EMBED_TAB_COMMAND[[:space:]]*=[[:space:]]*qtvcp[[:space:]]+vismach_scara([[:space:]#;]|$)' "$linuxcnc_root/$source"; then
qtvcp_panel_sources+=(
share/qtvcp/panels/vismach_scara/vismach_scara.ui
share/qtvcp/panels/vismach_scara/vismach_scara_handler.py
lib/python/qtvcp/lib/qt_vismach/README.txt
lib/python/qtvcp/lib/qt_vismach/__init__.py
lib/python/qtvcp/lib/qt_vismach/primitives.py
lib/python/qtvcp/lib/qt_vismach/qt_vismach.py
lib/python/qtvcp/lib/qt_vismach/scara.py
)
fi
done
mapfile -t qtvcp_panel_sources < <(
printf '%s\n' "${qtvcp_panel_sources[@]}" | sed '/^$/d' | sort -u
)
missing_qtvcp_panel_sources=()
for source in "${qtvcp_panel_sources[@]}"; do
if [[ ! -f "$linuxcnc_root/$source" ]]; then
missing_qtvcp_panel_sources+=("$source")
elif ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_qtvcp_panel_sources+=("$source")
fi
done
if ((${#missing_qtvcp_panel_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC QtVCP vismach_scara sources referenced by M428/M429/M430 INI EMBED_TAB_COMMAND entries as asset sources: $manifest" >&2
printf 'missing M428/M429/M430 QtVCP panel source: %s\n' "${missing_qtvcp_panel_sources[@]}" >&2
exit 1
fi
m428_m430_config_asset_sources=()
unresolved_m428_m430_config_asset_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd)
while IFS= read -r config_asset; do
key=${config_asset%%=*}
asset_source=${config_asset#*=}
[[ -n "$asset_source" ]] || continue
if [[ "$asset_source" = /* ]]; then
candidate=$asset_source
else
candidate="$config_dir/$asset_source"
fi
if [[ -f "$candidate" ]]; then
candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")
case "$candidate_abs" in
"$linuxcnc_root"/*)
m428_m430_config_asset_sources+=("${candidate_abs#"$linuxcnc_root/"}")
;;
*)
unresolved_m428_m430_config_asset_sources+=("$source $key=$asset_source")
;;
esac
else
unresolved_m428_m430_config_asset_sources+=("$source $key=$asset_source")
fi
done < <(
awk -F= '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*(PYVCP|OPEN_FILE)[[:space:]]*=/ {
key = $1
value = $0
sub(/^[^=]*=/, "", value)
sub(/[[:space:]]*[#;].*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
gsub(/^"|"$/, "", value)
if (value != "") {
print toupper(key) "=" value
}
}
' "$linuxcnc_root/$source"
)
done
mapfile -t m428_m430_config_asset_sources < <(
printf '%s\n' "${m428_m430_config_asset_sources[@]}" | sed '/^$/d' | sort -u
)
mapfile -t unresolved_m428_m430_config_asset_sources < <(
printf '%s\n' "${unresolved_m428_m430_config_asset_sources[@]}" | sed '/^$/d' | sort -u
)
if ((${#unresolved_m428_m430_config_asset_sources[@]} > 0)); then
echo "LinuxCNC M428/M429/M430 INI PYVCP/OPEN_FILE assets could not be resolved: $manifest" >&2
printf 'unresolved M428/M429/M430 config asset: %s\n' "${unresolved_m428_m430_config_asset_sources[@]}" >&2
exit 1
fi
missing_m428_m430_config_asset_sources=()
for source in "${m428_m430_config_asset_sources[@]}"; do
if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_config_asset_sources+=("$source")
fi
done
if ((${#missing_m428_m430_config_asset_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC PYVCP/OPEN_FILE assets referenced by M428/M429/M430 INI files as asset sources: $manifest" >&2
printf 'missing M428/M429/M430 config asset source: %s\n' "${missing_m428_m430_config_asset_sources[@]}" >&2
exit 1
fi
m428_m430_config_readme_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd)
readme_source="$config_dir/README"
if [[ -f "$readme_source" ]]; then
readme_abs=$(cd "$(dirname "$readme_source")" && pwd)/$(basename "$readme_source")
m428_m430_config_readme_sources+=("${readme_abs#"$linuxcnc_root/"}")
fi
done
mapfile -t m428_m430_config_readme_sources < <(
printf '%s\n' "${m428_m430_config_readme_sources[@]}" | sed '/^$/d' | sort -u
)
missing_m428_m430_config_readme_sources=()
for source in "${m428_m430_config_readme_sources[@]}"; do
if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_config_readme_sources+=("$source")
fi
done
if ((${#missing_m428_m430_config_readme_sources[@]} > 0)); then
echo "manifest does not cover LinuxCNC README sources adjacent to M428/M429/M430 INI files as asset sources: $manifest" >&2
printf 'missing M428/M429/M430 README source: %s\n' "${missing_m428_m430_config_readme_sources[@]}" >&2
exit 1
fi
m428_m430_config_tooldata_sources=()
unresolved_m428_m430_config_tooldata_sources=()
for source in "${manifest_m428_m430_ini_sources[@]}"; do
config_dir=$(cd "$(dirname "$linuxcnc_root/$source")" && pwd)
while IFS= read -r tool_table; do
[[ -n "$tool_table" ]] || continue
if [[ "$tool_table" = /* ]]; then
candidate=$tool_table
else
candidate="$config_dir/$tool_table"
fi
if [[ -f "$candidate" ]]; then
candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")
case "$candidate_abs" in
"$linuxcnc_root"/*)
m428_m430_config_tooldata_sources+=("${candidate_abs#"$linuxcnc_root/"}")
;;
*)
unresolved_m428_m430_config_tooldata_sources+=("$source TOOL_TABLE=$tool_table")
;;
esac
else
unresolved_m428_m430_config_tooldata_sources+=("$source TOOL_TABLE=$tool_table")
fi
done < <(
awk -F= '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*TOOL_TABLE[[:space:]]*=/ {
value = $0
sub(/^[^=]*=/, "", value)
sub(/[[:space:]]*[#;].*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
gsub(/^"|"$/, "", value)
if (value != "") print value
}
' "$linuxcnc_root/$source"
)
done
mapfile -t m428_m430_config_tooldata_sources < <(
printf '%s\n' "${m428_m430_config_tooldata_sources[@]}" | sed '/^$/d' | sort -u
)
mapfile -t unresolved_m428_m430_config_tooldata_sources < <(
printf '%s\n' "${unresolved_m428_m430_config_tooldata_sources[@]}" | sed '/^$/d' | sort -u
)
missing_m428_m430_config_tooldata_sources=()
for source in "${m428_m430_config_tooldata_sources[@]}"; do
if ! printf '%s\n' "${manifest_tooldata_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_m428_m430_config_tooldata_sources+=("$source")
fi
done
if ((${#missing_m428_m430_config_tooldata_sources[@]} > 0)); then
echo "manifest does not cover all resolvable LinuxCNC TOOL_TABLE files referenced by M428/M429/M430 INI files as tooldata sources: $manifest" >&2
printf 'missing M428/M429/M430 tool table source: %s\n' "${missing_m428_m430_config_tooldata_sources[@]}" >&2
exit 1
fi
mapfile -t manifest_twp_ini_sources < <(
awk -F: '
$1 !~ /^($|#)/ && $1 == "config" && $2 ~ /table-rotary_spindle-rotary-nutating\/.*_twp\/.*\.ini$/ { print $2 }
' "$manifest" | sort -u
)
twp_config_asset_sources=()
unresolved_twp_config_asset_sources=()
resolve_twp_config_asset() {
local config_dir=$1
local source=$2
local key=$3
local asset_source=$4
local candidate
if [[ "$asset_source" = /* ]]; then
candidate=$asset_source
else
candidate="$config_dir/$asset_source"
fi
if [[ -f "$candidate" ]]; then
local candidate_abs
candidate_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")
case "$candidate_abs" in
"$linuxcnc_root"/*)
twp_config_asset_sources+=("${candidate_abs#"$linuxcnc_root/"}")
;;
*)
unresolved_twp_config_asset_sources+=("$source $key=$asset_source")
;;
esac
else
unresolved_twp_config_asset_sources+=("$source $key=$asset_source")
fi
}
resolve_twp_subroutine_asset() {
local config_dir=$1
local source=$2
local key=$3
local ngc_name=$4
shift 4
local subroutine_paths=("$@")
[[ "$ngc_name" == *.ngc ]] || ngc_name="$ngc_name.ngc"
local resolved_source=
local subroutine_path path_part candidate_dir candidate resolved_abs
for subroutine_path in "${subroutine_paths[@]}"; do
[[ -n "$subroutine_path" ]] || continue
IFS=: read -ra path_parts <<<"$subroutine_path"
for path_part in "${path_parts[@]}"; do
[[ -n "$path_part" ]] || continue
if [[ "$path_part" = /* ]]; then
candidate_dir=$path_part
else
candidate_dir="$config_dir/$path_part"
fi
candidate="$candidate_dir/$ngc_name"
if [[ -f "$candidate" ]]; then
resolved_abs=$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")
resolved_source=${resolved_abs#"$linuxcnc_root/"}
break 2
fi
done
done
if [[ -n "$resolved_source" ]]; then
twp_config_asset_sources+=("$resolved_source")
else
unresolved_twp_config_asset_sources+=("$source $key=$ngc_name")
fi
}
for source in "${manifest_twp_ini_sources[@]}"; do
config="$linuxcnc_root/$source"
config_dir=$(cd "$(dirname "$config")" && pwd)
mapfile -t subroutine_paths < <(
sed -n 's/^[[:space:]]*SUBROUTINE_PATH[[:space:]]*=[[:space:]]*//Ip' "$config" |
sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//'
)
if ((${#subroutine_paths[@]} == 0)); then
subroutine_paths=(".")
fi
while IFS='|' read -r remap_code ngc_name; do
[[ -n "$remap_code" && -n "$ngc_name" ]] || continue
case "$remap_code" in
M428|m428|M429|m429|M430|m430)
continue
;;
esac
resolve_twp_subroutine_asset "$config_dir" "$source" "REMAP $remap_code ngc" "$ngc_name" "${subroutine_paths[@]}"
done < <(
awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*REMAP[[:space:]]*=/ && /[[:space:]]ngc[[:space:]]*=/ {
code = $0
ngc = $0
sub(/.*REMAP[[:space:]]*=[[:space:]]*/, "", code)
sub(/[[:space:]].*/, "", code)
sub(/.*[[:space:]]ngc[[:space:]]*=[[:space:]]*/, "", ngc)
sub(/[[:space:]#;].*/, "", ngc)
print code "|" ngc
}
' "$config"
)
while IFS= read -r abort_name; do
[[ -n "$abort_name" ]] || continue
resolve_twp_subroutine_asset "$config_dir" "$source" "ON_ABORT_COMMAND" "$abort_name" "${subroutine_paths[@]}"
done < <(
sed -n 's/^[[:space:]]*ON_ABORT_COMMAND[[:space:]]*=[[:space:]]*o[[:space:]]*<\([^>]*\)>.*/\1/Ip' "$config"
)
while IFS= read -r twp_asset; do
[[ -n "$twp_asset" ]] || continue
resolve_twp_config_asset "$config_dir" "$source" "TWP asset" "$twp_asset"
done < <(
awk -F= '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*TOPLEVEL[[:space:]]*=/ {
value = $0
sub(/^[^=]*=/, "", value)
sub(/[[:space:]]*[#;].*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value != "") print value
}
/^[[:space:]]*EMBED_TAB_COMMAND[[:space:]]*=/ && /pyvcp[[:space:]]/ {
for (i = 1; i <= NF; i++) {
if ($i == "pyvcp" && (i + 1) <= NF) {
value = $(i + 1)
sub(/[[:space:]#;].*$/, "", value)
if (value != "") print value
}
}
}
/^[[:space:]]*TOOL_TABLE[[:space:]]*=/ {
next
}
/^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr/ {
for (i = 1; i <= NF; i++) {
if ($i ~ /^\.\.\/.*\.py$/) print $i
}
}
' "$config"
)
mapfile -t twp_loaded_python_sources < <(
awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*TOPLEVEL[[:space:]]*=/ {
value = $0
sub(/^[^=]*=/, "", value)
sub(/[[:space:]]*[#;].*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if (value != "") print value
}
/^[[:space:]]*HALCMD[[:space:]]*=/ && /loadusr/ {
for (i = 1; i <= NF; i++) {
if ($i ~ /^\.\.\/.*\.py$/) print $i
}
}
' "$config"
)
for twp_python_source in "${twp_loaded_python_sources[@]}"; do
python_candidate="$config_dir/$twp_python_source"
if [[ ! -f "$python_candidate" ]]; then
continue
fi
python_abs=$(cd "$(dirname "$python_candidate")" && pwd)/$(basename "$python_candidate")
python_dir=$(dirname "$python_abs")
if grep -Eq '^[[:space:]]*import[[:space:]]+remap([[:space:]]|$)' "$python_abs"; then
resolve_twp_config_asset "$python_dir" "$source" "python import" "remap.py"
fi
if grep -Eq '^[[:space:]]*from[[:space:]]+util[[:space:]]+import([[:space:]]|$)' "$python_abs"; then
resolve_twp_config_asset "$python_dir" "$source" "python import" "util.py"
fi
if grep -Eq '^[[:space:]]*from[[:space:]]+twp_vismach[[:space:]]+import([[:space:]]|$)' "$python_abs"; then
resolve_twp_config_asset "$python_dir" "$source" "python import" "twp_vismach.py"
fi
if grep -F 'work_piece_1.stl' "$python_abs" >/dev/null; then
resolve_twp_config_asset "$config_dir" "$source" "vismach STL" "work_piece_1.stl"
fi
done
done
mapfile -t twp_config_asset_sources < <(
printf '%s\n' "${twp_config_asset_sources[@]}" | sed '/^$/d' | sort -u
)
mapfile -t unresolved_twp_config_asset_sources < <(
printf '%s\n' "${unresolved_twp_config_asset_sources[@]}" | sed '/^$/d' | sort -u
)
if ((${#unresolved_twp_config_asset_sources[@]} > 0)); then
echo "LinuxCNC TWP INI source assets could not be resolved: $manifest" >&2
printf 'unresolved TWP config asset: %s\n' "${unresolved_twp_config_asset_sources[@]}" >&2
exit 1
fi
missing_twp_config_asset_sources=()
for source in "${twp_config_asset_sources[@]}"; do
if ! printf '%s\n' "${manifest_asset_sources[@]}" | grep -Fx -- "$source" >/dev/null; then
missing_twp_config_asset_sources+=("$source")
fi
done
if ((${#missing_twp_config_asset_sources[@]} > 0)); then
echo "manifest does not cover all LinuxCNC TWP INI source assets as asset sources: $manifest" >&2
printf 'missing TWP config asset source: %s\n' "${missing_twp_config_asset_sources[@]}" >&2
exit 1
fi
fi

File diff suppressed because it is too large Load Diff

View File

@@ -544,7 +544,8 @@ if(EMSCRIPTEN)
"-sEXPORT_NAME=createCncSimModule"
"-sALLOW_MEMORY_GROWTH=1"
"-sALLOW_TABLE_GROWTH=1"
"-sFORCE_FILESYSTEM=1"
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_cnc_sim_create','_cnc_sim_destroy','_cnc_sim_reset','_cnc_sim_set_dialect','_cnc_sim_set_event_callback','_cnc_sim_load_config_json','_cnc_sim_parse_program','_cnc_sim_last_error']"
"-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','addFunction','removeFunction']"
"-sEXPORTED_RUNTIME_METHODS=['FS','ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','addFunction','removeFunction']"
)
endif()

View File

@@ -103,9 +103,17 @@ bool contains_switchkins_config_alias(const std::string &compact, const char *al
contains_string_value(compact, "kinematics", alias) ||
contains_string_value(compact, "halfile", alias) ||
contains_string_value(compact, "hal_file", alias) ||
contains_string_value(compact, "postguihalfile", alias) ||
contains_string_value(compact, "postgui_halfile", alias) ||
contains_string_value(compact, "postgui_hal_file", alias) ||
contains_string_value(compact, "config", alias) ||
contains_string_value(compact, "configpath", alias) ||
contains_string_value(compact, "config_path", alias);
contains_string_value(compact, "config_path", alias) ||
contains_string_value(compact, "ini", alias) ||
contains_string_value(compact, "inifile", alias) ||
contains_string_value(compact, "inifilename", alias) ||
contains_string_value(compact, "ini_file", alias) ||
contains_string_value(compact, "ini_file_name", alias);
}
bool find_number_value(const std::string &compact, const char *key, double *value) {

View File

@@ -28,6 +28,19 @@ double motion_control_tolerance = 0.0;
double naivecam_tolerance = 0.0;
std::string parameter_file_name = "rs274ngc.var";
void load_nonrandom_tool_to_spindle(int pocket) {
// LinuxCNC Task::load_tool() uses pocket 0 as the non-random
// toolchanger unload handshake. Do not eagerly mirror nonzero
// loads here; the source-linked interpreter observes its own
// post-canonical state before external task-side load completion.
if (pocket == 0) {
CANON_TOOL_TABLE unloaded = tooldata_entry_init();
unloaded.toolno = 0;
unloaded.pocketno = 0;
tooldata_put(unloaded, 0);
}
}
CncSimPose make_pose(double x, double y, double z,
double a, double b, double c,
double u, double v, double w) {
@@ -215,6 +228,10 @@ void SELECT_TOOL(int tool) {
void CHANGE_TOOL() {
trace_call("CHANGE_TOOL");
if (active_sink) {
const int selected_tool = active_sink->selected_tool();
if (selected_tool == 0) {
load_nonrandom_tool_to_spindle(0);
}
active_sink->change_tool(event_line());
}
}
@@ -524,6 +541,7 @@ void USE_TOOL_LENGTH_OFFSET(const EmcPose &offset) {
void CHANGE_TOOL_NUMBER(int number) {
if (active_sink) {
load_nonrandom_tool_to_spindle(number);
active_sink->select_tool(number);
active_sink->change_tool(event_line());
}

View File

@@ -1,5 +1,5 @@
// Generated by ./generate-linuxcnc-switchkins-remap-table.sh.
// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/SUBROUTINE_PATH/REMAP entries and
// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and
// adjacent remap_subs/{428,429,430}remap.ngc #<kinstype> assignments.
{"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"configs/sim/axis/vismach/5axis/bridgemill", 0, 1, 2},
@@ -11,6 +11,8 @@
{"5axiskins", 0, 1, 2},
{"5axisgui.hal", 0, 1, 2},
{"5axisgui", 0, 1, 2},
{"5axis_postgui.hal", 0, 1, 2},
{"5axis_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/5axis/bridgemill/remap_subs", 0, 1, 2},
{"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"configs/sim/axis/vismach/5axis/table-dual-rotary", 1, 0, -1},
@@ -21,6 +23,8 @@
{"xyzab_tdr_kins", 1, 0, -1},
{"xyzab_tdr", 1, 0, -1},
{"xyzab-tdr-kins", 1, 0, -1},
{"xyzab-tdr-postgui.hal", 1, 0, -1},
{"xyzab-tdr-postgui", 1, 0, -1},
{"configs/sim/axis/vismach/5axis/table-dual-rotary/remap_subs", 1, 0, -1},
{"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"configs/sim/axis/vismach/5axis/table-rotary-tilting", 1, 0, 2},
@@ -30,6 +34,8 @@
{"sim-xyzac-trt-kins", 1, 0, 2},
{"xyzac-trt-kinssparm=identityfirst", 1, 0, 2},
{"xyzac-trt-kins", 1, 0, 2},
{"switchkins_postgui.hal", 1, 0, 2},
{"switchkins_postgui", 1, 0, 2},
{"configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs", 1, 0, 2},
{"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"xyzbc-trt", 1, 0, 2},
@@ -43,6 +49,8 @@
{"xyzacb-trsrn", 0, 1, 2},
{"xyzacb-trsrn(switchkins)", 0, 1, 2},
{"xyzacb_trsrn", 0, 1, 2},
{"xyzacb-trsrn_postgui.hal", 0, 1, 2},
{"xyzacb-trsrn_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs", 0, 1, 2},
{"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating", 0, 1, 2},
{"table-rotary_spindle-rotary-nutating", 0, 1, 2},
@@ -52,6 +60,8 @@
{"xyzbca-trsrn", 0, 1, 2},
{"xyzbca-trsrn(switchkins)", 0, 1, 2},
{"xyzbca_trsrn", 0, 1, 2},
{"xyzbca-trsrn_postgui.hal", 0, 1, 2},
{"xyzbca-trsrn_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"configs/sim/axis/vismach/hexapod-sim", 0, 1, 2},
{"hexapod-sim", 0, 1, 2},
@@ -60,6 +70,8 @@
{"genhexkins", 0, 1, 2},
{"kinematics.hal", 0, 1, 2},
{"kinematics", 0, 1, 2},
{"hexapod_postgui.hal", 0, 1, 2},
{"hexapod_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/hexapod-sim/remap_subs", 0, 1, 2},
{"configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"configs/sim/axis/vismach/melfa-sim", 0, 1, 2},
@@ -69,12 +81,16 @@
{"genserkins", 0, 1, 2},
{"melfa_dh.hal", 0, 1, 2},
{"melfa_dh", 0, 1, 2},
{"melfa-postgui.hal", 0, 1, 2},
{"melfa-postgui", 0, 1, 2},
{"configs/sim/axis/vismach/melfa-sim/remap_subs", 0, 1, 2},
{"configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"configs/sim/axis/vismach/millturn", 0, 1, -1},
{"millturn", 0, 1, -1},
{"millturn(mm)", 0, 1, -1},
{"millturn.hal", 0, 1, -1},
{"millturn-postgui.hal", 0, 1, -1},
{"millturn-postgui", 0, 1, -1},
{"configs/sim/axis/vismach/millturn/remap_subs", 0, 1, -1},
{"configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"configs/sim/axis/vismach/puma", 0, 1, 2},
@@ -83,6 +99,8 @@
{"pumakins", 0, 1, 2},
{"puma_dh.hal", 0, 1, 2},
{"puma_dh", 0, 1, 2},
{"puma_postgui.hal", 0, 1, 2},
{"puma_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/puma/remap_subs", 0, 1, 2},
{"configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"puma_cube", 0, 1, 2},
@@ -93,6 +111,8 @@
{"puma560(switchkins)(inch)", 0, 1, 2},
{"puma560_dh.hal", 0, 1, 2},
{"puma560_dh", 0, 1, 2},
{"puma560_postgui.hal", 0, 1, 2},
{"puma560_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"puma560_uvw", 0, 1, 2},
{"configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
@@ -101,6 +121,8 @@
{"scara(genserkins,switchkins)", 0, 1, 2},
{"scarakinscoordinates=xyzcab", 0, 1, 2},
{"scarakins", 0, 1, 2},
{"scara_postgui.hal", 0, 1, 2},
{"scara_postgui", 0, 1, 2},
{"configs/sim/axis/vismach/scara/remap_subs", 0, 1, 2},
{"configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"configs/sim/qtaxis/non-trivial/scara", 0, 1, 2},

View File

@@ -1,5 +1,6 @@
#include "cnc_sim_api.h"
#include <cstdlib>
#include <cmath>
#include <iostream>
#include <string>
@@ -35,6 +36,31 @@ bool near(double lhs, double rhs) {
return std::fabs(lhs - rhs) < 0.000001;
}
class ScopedEnv {
public:
ScopedEnv(const char *name, const char *value) : name_(name) {
const char *old = std::getenv(name);
if (old) {
had_old_ = true;
old_ = old;
}
setenv(name, value, 1);
}
~ScopedEnv() {
if (had_old_) {
setenv(name_, old_.c_str(), 1);
} else {
unsetenv(name_);
}
}
private:
const char *name_;
bool had_old_ = false;
std::string old_;
};
bool saw_comment_state(const std::vector<CncSimEvent> &events,
int line,
int reserved,
@@ -66,6 +92,79 @@ bool saw_any_kinematics_switch(const std::vector<CncSimEvent> &events, int line)
return false;
}
bool saw_kinematics_switch(const std::vector<CncSimEvent> &events, int line, int kinstype) {
for (const auto &event : events) {
if (event.type == CNC_SIM_EVENT_KINEMATICS_SWITCH &&
event.line == line &&
event.reserved == kinstype) {
return true;
}
}
return false;
}
bool expect_switchkins_remap_config(const std::string &json,
int m428_type,
int m429_type,
int m430_type,
const char *message) {
std::vector<CncSimEvent> events;
CncSimHandle *sim = cnc_sim_create();
cnc_sim_set_event_callback(sim, collect_event, &events);
const char supported_program[] =
"M428\n"
"M429\n";
const char full_program[] =
"M428\n"
"M429\n"
"M430\n";
const char *program = m430_type >= 0 ? full_program : supported_program;
const size_t program_len = m430_type >= 0 ? sizeof(full_program) - 1 : sizeof(supported_program) - 1;
bool ok = true;
const std::string load_message = std::string(message) + " load";
const std::string parse_message = std::string(message) + " parse M428/M429";
const std::string m428_message = std::string(message) + " M428 kinstype";
const std::string m429_message = std::string(message) + " M429 kinstype";
ok &= expect(cnc_sim_load_config_json(sim, json.c_str(), json.size()) == 0, load_message.c_str());
ok &= expect(cnc_sim_parse_program(sim, program, program_len) == 0, parse_message.c_str());
ok &= expect(saw_kinematics_switch(events, 1, m428_type), m428_message.c_str());
ok &= expect(saw_kinematics_switch(events, 2, m429_type), m429_message.c_str());
if (m430_type >= 0) {
const std::string m430_message = std::string(message) + " M430 kinstype";
ok &= expect(saw_kinematics_switch(events, 3, m430_type), m430_message.c_str());
} else {
const std::string m430_reject_message = std::string(message) + " reject M430";
const std::string m430_error_message = std::string(message) + " M430 error";
const std::string m430_no_switch_message = std::string(message) + " M430 no switch";
const std::string mixed_load_message = std::string(message) + " mixed load";
const std::string mixed_reject_message = std::string(message) + " reject mixed M428/M430";
const std::string mixed_error_message = std::string(message) + " mixed M430 error";
const std::string mixed_no_switch_message = std::string(message) + " mixed no partial switch";
events.clear();
ok &= expect(cnc_sim_parse_program(sim, "M430\n", 5) != 0, m430_reject_message.c_str());
// Source: LinuxCNC interp_read.cc reports NCE_M_CODE_GREATER_THAN_199;
// linuxcnc_rs274_backend.cpp preserves the read-stage context.
ok &= expect(std::string(cnc_sim_last_error(sim)) == "read failed: M-code greater than 199: M430",
m430_error_message.c_str());
ok &= expect(!saw_kinematics_switch(events, 1, 2), m430_no_switch_message.c_str());
std::vector<CncSimEvent> mixed_events;
CncSimHandle *mixed_sim = cnc_sim_create();
cnc_sim_set_event_callback(mixed_sim, collect_event, &mixed_events);
ok &= expect(cnc_sim_load_config_json(mixed_sim, json.c_str(), json.size()) == 0,
mixed_load_message.c_str());
ok &= expect(cnc_sim_parse_program(mixed_sim, "M428 M430\n", 9) != 0,
mixed_reject_message.c_str());
ok &= expect(std::string(cnc_sim_last_error(mixed_sim)) == "M-code greater than 199: M430",
mixed_error_message.c_str());
ok &= expect(!saw_any_kinematics_switch(mixed_events, 1), mixed_no_switch_message.c_str());
cnc_sim_destroy(mixed_sim);
}
cnc_sim_destroy(sim);
return ok;
}
} // namespace
int main() {
@@ -1577,6 +1676,35 @@ int main() {
ok &= expect(cnc_sim_load_config_json(sim, config, sizeof(config) - 1) == 0,
cnc_sim_last_error(sim));
{
struct SourceBackedConfigCase {
const char *field;
const char *value;
int m428_type;
int m429_type;
int m430_type;
};
const SourceBackedConfigCase source_cases[] = {
#include "linuxcnc_switchkins_remap_config_cases.inc"
};
for (const SourceBackedConfigCase &source_case : source_cases) {
const std::string json = std::string("{\"backend\":\"linuxcnc-rs274\",\"") +
source_case.field +
"\":\"" +
source_case.value +
"\"}";
const std::string message = std::string("expected LinuxCNC source-backed switchkins remap config for ") +
source_case.field +
"=" +
source_case.value;
ok &= expect_switchkins_remap_config(json,
source_case.m428_type,
source_case.m429_type,
source_case.m430_type,
message.c_str());
}
}
const char exponent_m428_control_program[] =
"G21 G90\n"
"M4.28e2\n";

View File

@@ -1,67 +1,384 @@
// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases.
// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, and non-LIB HALFILE entries
// for M428/M429/M430 remap #<kinstype> assignments.
// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE,
// and adjacent remap_subs entries for M428/M429/M430 #<kinstype> assignments.
{"config", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/bridgemill/5axis.ini", 0, 1, 2},
{"machine", "Sim-5Axis Bridge Mill (xyzbcw)", 0, 1, 2},
{"kinematics", "5axiskins coordinates=xyzbcwy", 0, 1, 2},
{"switchkins", "bridgemill", 0, 1, 2},
{"switchkins", "5axis", 0, 1, 2},
{"switchkins", "Sim-5Axis Bridge Mill (xyzbcw)", 0, 1, 2},
{"switchkins", "Sim-5Axis Bridge Mill", 0, 1, 2},
{"switchkins", "5axiskins coordinates=xyzbcwy", 0, 1, 2},
{"switchkins", "5axiskins", 0, 1, 2},
{"halFile", "5axisgui.hal", 0, 1, 2},
{"halFile", "5axisgui", 0, 1, 2},
{"halfile", "5axisgui.hal", 0, 1, 2},
{"halfile", "5axisgui", 0, 1, 2},
{"hal_file", "5axisgui.hal", 0, 1, 2},
{"hal_file", "5axisgui", 0, 1, 2},
{"HALFILE", "5axisgui.hal", 0, 1, 2},
{"HALFILE", "5axisgui", 0, 1, 2},
{"postguiHalFile", "5axis_postgui.hal", 0, 1, 2},
{"postguiHalFile", "5axis_postgui", 0, 1, 2},
{"postgui_halfile", "5axis_postgui.hal", 0, 1, 2},
{"postgui_halfile", "5axis_postgui", 0, 1, 2},
{"postgui_hal_file", "5axis_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "5axis_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "5axis_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "5axis_postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/5axis/bridgemill", 0, 1, 2},
{"remap", "bridgemill", 0, 1, 2},
{"config", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"configPath", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"config_path", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"ini", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"iniFile", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"iniFileName", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"ini_file", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"ini_file_name", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini", 1, 0, -1},
{"machine", "sim-xyzab-tdr-kins (switchkins)", 1, 0, -1},
{"kinematics", "xyzab_tdr_kins", 1, 0, -1},
{"switchkins", "table-dual-rotary", 1, 0, -1},
{"switchkins", "xyzab-tdr", 1, 0, -1},
{"switchkins", "sim-xyzab-tdr-kins (switchkins)", 1, 0, -1},
{"switchkins", "sim-xyzab-tdr-kins", 1, 0, -1},
{"switchkins", "xyzab_tdr_kins", 1, 0, -1},
{"switchkins", "xyzab_tdr", 1, 0, -1},
{"switchkins", "xyzab-tdr-kins", 1, 0, -1},
{"postguiHalFile", "xyzab-tdr-postgui.hal", 1, 0, -1},
{"postguiHalFile", "xyzab-tdr-postgui", 1, 0, -1},
{"postgui_halfile", "xyzab-tdr-postgui.hal", 1, 0, -1},
{"postgui_halfile", "xyzab-tdr-postgui", 1, 0, -1},
{"postgui_hal_file", "xyzab-tdr-postgui.hal", 1, 0, -1},
{"postgui_hal_file", "xyzab-tdr-postgui", 1, 0, -1},
{"POSTGUI_HALFILE", "xyzab-tdr-postgui.hal", 1, 0, -1},
{"POSTGUI_HALFILE", "xyzab-tdr-postgui", 1, 0, -1},
{"remap", "configs/sim/axis/vismach/5axis/table-dual-rotary", 1, 0, -1},
{"remap", "table-dual-rotary", 1, 0, -1},
{"config", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"configPath", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"config_path", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"ini", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"iniFile", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"ini_file", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", 1, 0, 2},
{"machine", "sim-xyzac-trt-kins (switchkins)", 1, 0, 2},
{"kinematics", "xyzac-trt-kins sparm=identityfirst", 1, 0, 2},
{"switchkins", "table-rotary-tilting", 1, 0, 2},
{"switchkins", "xyzac-trt", 1, 0, 2},
{"switchkins", "sim-xyzac-trt-kins (switchkins)", 1, 0, 2},
{"switchkins", "sim-xyzac-trt-kins", 1, 0, 2},
{"switchkins", "xyzac-trt-kins sparm=identityfirst", 1, 0, 2},
{"switchkins", "xyzac-trt-kins", 1, 0, 2},
{"postguiHalFile", "switchkins_postgui.hal", 1, 0, 2},
{"postguiHalFile", "switchkins_postgui", 1, 0, 2},
{"postgui_halfile", "switchkins_postgui.hal", 1, 0, 2},
{"postgui_halfile", "switchkins_postgui", 1, 0, 2},
{"postgui_hal_file", "switchkins_postgui.hal", 1, 0, 2},
{"postgui_hal_file", "switchkins_postgui", 1, 0, 2},
{"POSTGUI_HALFILE", "switchkins_postgui.hal", 1, 0, 2},
{"POSTGUI_HALFILE", "switchkins_postgui", 1, 0, 2},
{"remap", "configs/sim/axis/vismach/5axis/table-rotary-tilting", 1, 0, 2},
{"remap", "table-rotary-tilting", 1, 0, 2},
{"config", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"configPath", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"config_path", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"ini", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"iniFile", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"ini_file", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", 1, 0, 2},
{"machine", "sim-xyzbc-trt-kins (switchkins)", 1, 0, 2},
{"kinematics", "xyzbc-trt-kins sparm=identityfirst", 1, 0, 2},
{"switchkins", "xyzbc-trt", 1, 0, 2},
{"switchkins", "sim-xyzbc-trt-kins (switchkins)", 1, 0, 2},
{"switchkins", "sim-xyzbc-trt-kins", 1, 0, 2},
{"switchkins", "xyzbc-trt-kins sparm=identityfirst", 1, 0, 2},
{"switchkins", "xyzbc-trt-kins", 1, 0, 2},
{"config", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini", 0, 1, 2},
{"machine", "xyzacb-trsrn (switchkins)", 0, 1, 2},
{"kinematics", "xyzacb_trsrn", 0, 1, 2},
{"switchkins", "xyzacb-trsrn_twp", 0, 1, 2},
{"switchkins", "xyzacb-trsrn", 0, 1, 2},
{"switchkins", "xyzacb-trsrn (switchkins)", 0, 1, 2},
{"switchkins", "xyzacb_trsrn", 0, 1, 2},
{"postguiHalFile", "xyzacb-trsrn_postgui.hal", 0, 1, 2},
{"postguiHalFile", "xyzacb-trsrn_postgui", 0, 1, 2},
{"postgui_halfile", "xyzacb-trsrn_postgui.hal", 0, 1, 2},
{"postgui_halfile", "xyzacb-trsrn_postgui", 0, 1, 2},
{"postgui_hal_file", "xyzacb-trsrn_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "xyzacb-trsrn_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "xyzacb-trsrn_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "xyzacb-trsrn_postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating", 0, 1, 2},
{"remap", "table-rotary_spindle-rotary-nutating", 0, 1, 2},
{"config", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini", 0, 1, 2},
{"machine", "xyzbca-trsrn (switchkins)", 0, 1, 2},
{"kinematics", "xyzbca_trsrn", 0, 1, 2},
{"switchkins", "xyzbca-trsrn_twp", 0, 1, 2},
{"switchkins", "xyzbca-trsrn", 0, 1, 2},
{"switchkins", "xyzbca-trsrn (switchkins)", 0, 1, 2},
{"switchkins", "xyzbca_trsrn", 0, 1, 2},
{"postguiHalFile", "xyzbca-trsrn_postgui.hal", 0, 1, 2},
{"postguiHalFile", "xyzbca-trsrn_postgui", 0, 1, 2},
{"postgui_halfile", "xyzbca-trsrn_postgui.hal", 0, 1, 2},
{"postgui_halfile", "xyzbca-trsrn_postgui", 0, 1, 2},
{"postgui_hal_file", "xyzbca-trsrn_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "xyzbca-trsrn_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "xyzbca-trsrn_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "xyzbca-trsrn_postgui", 0, 1, 2},
{"config", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/hexapod-sim/hexapod.ini", 0, 1, 2},
{"machine", "hexapod (switchkins)", 0, 1, 2},
{"kinematics", "genhexkins", 0, 1, 2},
{"switchkins", "hexapod-sim", 0, 1, 2},
{"switchkins", "hexapod", 0, 1, 2},
{"switchkins", "hexapod (switchkins)", 0, 1, 2},
{"switchkins", "genhexkins", 0, 1, 2},
{"halFile", "kinematics.hal", 0, 1, 2},
{"halFile", "kinematics", 0, 1, 2},
{"halfile", "kinematics.hal", 0, 1, 2},
{"halfile", "kinematics", 0, 1, 2},
{"hal_file", "kinematics.hal", 0, 1, 2},
{"hal_file", "kinematics", 0, 1, 2},
{"HALFILE", "kinematics.hal", 0, 1, 2},
{"HALFILE", "kinematics", 0, 1, 2},
{"postguiHalFile", "hexapod_postgui.hal", 0, 1, 2},
{"postguiHalFile", "hexapod_postgui", 0, 1, 2},
{"postgui_halfile", "hexapod_postgui.hal", 0, 1, 2},
{"postgui_halfile", "hexapod_postgui", 0, 1, 2},
{"postgui_hal_file", "hexapod_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "hexapod_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "hexapod_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "hexapod_postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/hexapod-sim", 0, 1, 2},
{"remap", "hexapod-sim", 0, 1, 2},
{"config", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/melfa-sim/melfa.ini", 0, 1, 2},
{"machine", "melfa (mm)", 0, 1, 2},
{"kinematics", "genserkins", 0, 1, 2},
{"switchkins", "melfa-sim", 0, 1, 2},
{"switchkins", "melfa", 0, 1, 2},
{"switchkins", "melfa (mm)", 0, 1, 2},
{"switchkins", "genserkins", 0, 1, 2},
{"halFile", "melfa_dh.hal", 0, 1, 2},
{"halFile", "melfa_dh", 0, 1, 2},
{"halfile", "melfa_dh.hal", 0, 1, 2},
{"halfile", "melfa_dh", 0, 1, 2},
{"hal_file", "melfa_dh.hal", 0, 1, 2},
{"hal_file", "melfa_dh", 0, 1, 2},
{"HALFILE", "melfa_dh.hal", 0, 1, 2},
{"HALFILE", "melfa_dh", 0, 1, 2},
{"postguiHalFile", "melfa-postgui.hal", 0, 1, 2},
{"postguiHalFile", "melfa-postgui", 0, 1, 2},
{"postgui_halfile", "melfa-postgui.hal", 0, 1, 2},
{"postgui_halfile", "melfa-postgui", 0, 1, 2},
{"postgui_hal_file", "melfa-postgui.hal", 0, 1, 2},
{"postgui_hal_file", "melfa-postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "melfa-postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "melfa-postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/melfa-sim", 0, 1, 2},
{"remap", "melfa-sim", 0, 1, 2},
{"config", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"configPath", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"config_path", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"ini", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"iniFile", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"iniFileName", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"ini_file", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"ini_file_name", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"INI_FILE_NAME", "configs/sim/axis/vismach/millturn/millturn.ini", 0, 1, -1},
{"machine", "millturn (mm)", 0, 1, -1},
{"kinematics", "millturn", 0, 1, -1},
{"switchkins", "millturn", 0, 1, -1},
{"switchkins", "millturn (mm)", 0, 1, -1},
{"halFile", "millturn.hal", 0, 1, -1},
{"halFile", "millturn", 0, 1, -1},
{"halfile", "millturn.hal", 0, 1, -1},
{"halfile", "millturn", 0, 1, -1},
{"hal_file", "millturn.hal", 0, 1, -1},
{"hal_file", "millturn", 0, 1, -1},
{"HALFILE", "millturn.hal", 0, 1, -1},
{"HALFILE", "millturn", 0, 1, -1},
{"postguiHalFile", "millturn-postgui.hal", 0, 1, -1},
{"postguiHalFile", "millturn-postgui", 0, 1, -1},
{"postgui_halfile", "millturn-postgui.hal", 0, 1, -1},
{"postgui_halfile", "millturn-postgui", 0, 1, -1},
{"postgui_hal_file", "millturn-postgui.hal", 0, 1, -1},
{"postgui_hal_file", "millturn-postgui", 0, 1, -1},
{"POSTGUI_HALFILE", "millturn-postgui.hal", 0, 1, -1},
{"POSTGUI_HALFILE", "millturn-postgui", 0, 1, -1},
{"remap", "configs/sim/axis/vismach/millturn", 0, 1, -1},
{"remap", "millturn", 0, 1, -1},
{"config", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma.ini", 0, 1, 2},
{"machine", "PUMA (pumakins,switchkins)", 0, 1, 2},
{"kinematics", "pumakins", 0, 1, 2},
{"switchkins", "puma", 0, 1, 2},
{"switchkins", "PUMA (pumakins,switchkins)", 0, 1, 2},
{"switchkins", "PUMA", 0, 1, 2},
{"switchkins", "pumakins", 0, 1, 2},
{"halFile", "puma_dh.hal", 0, 1, 2},
{"halFile", "puma_dh", 0, 1, 2},
{"halfile", "puma_dh.hal", 0, 1, 2},
{"halfile", "puma_dh", 0, 1, 2},
{"hal_file", "puma_dh.hal", 0, 1, 2},
{"hal_file", "puma_dh", 0, 1, 2},
{"HALFILE", "puma_dh.hal", 0, 1, 2},
{"HALFILE", "puma_dh", 0, 1, 2},
{"postguiHalFile", "puma_postgui.hal", 0, 1, 2},
{"postguiHalFile", "puma_postgui", 0, 1, 2},
{"postgui_halfile", "puma_postgui.hal", 0, 1, 2},
{"postgui_halfile", "puma_postgui", 0, 1, 2},
{"postgui_hal_file", "puma_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "puma_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "puma_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "puma_postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/puma", 0, 1, 2},
{"remap", "puma", 0, 1, 2},
{"config", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma_cube.ini", 0, 1, 2},
{"machine", "puma_cube.ini (pumakins)", 0, 1, 2},
{"switchkins", "puma_cube", 0, 1, 2},
{"switchkins", "puma_cube.ini (pumakins)", 0, 1, 2},
{"switchkins", "puma_cube.ini", 0, 1, 2},
{"config", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma560.ini", 0, 1, 2},
{"machine", "puma560 (switchkins) (inch)", 0, 1, 2},
{"switchkins", "puma560", 0, 1, 2},
{"switchkins", "puma560 (switchkins) (inch)", 0, 1, 2},
{"halFile", "puma560_dh.hal", 0, 1, 2},
{"halFile", "puma560_dh", 0, 1, 2},
{"halfile", "puma560_dh.hal", 0, 1, 2},
{"halfile", "puma560_dh", 0, 1, 2},
{"hal_file", "puma560_dh.hal", 0, 1, 2},
{"hal_file", "puma560_dh", 0, 1, 2},
{"HALFILE", "puma560_dh.hal", 0, 1, 2},
{"HALFILE", "puma560_dh", 0, 1, 2},
{"postguiHalFile", "puma560_postgui.hal", 0, 1, 2},
{"postguiHalFile", "puma560_postgui", 0, 1, 2},
{"postgui_halfile", "puma560_postgui.hal", 0, 1, 2},
{"postgui_halfile", "puma560_postgui", 0, 1, 2},
{"postgui_hal_file", "puma560_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "puma560_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "puma560_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "puma560_postgui", 0, 1, 2},
{"config", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/puma/puma560_uvw.ini", 0, 1, 2},
{"switchkins", "puma560_uvw", 0, 1, 2},
{"config", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"configPath", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"config_path", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"ini", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"iniFile", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"iniFileName", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"ini_file", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/axis/vismach/scara/scara.ini", 0, 1, 2},
{"machine", "SCARA (genserkins,switchkins)", 0, 1, 2},
{"kinematics", "scarakins coordinates=xyzcab", 0, 1, 2},
{"switchkins", "scara", 0, 1, 2},
{"switchkins", "SCARA (genserkins,switchkins)", 0, 1, 2},
{"switchkins", "SCARA", 0, 1, 2},
{"switchkins", "scarakins coordinates=xyzcab", 0, 1, 2},
{"switchkins", "scarakins", 0, 1, 2},
{"postguiHalFile", "scara_postgui.hal", 0, 1, 2},
{"postguiHalFile", "scara_postgui", 0, 1, 2},
{"postgui_halfile", "scara_postgui.hal", 0, 1, 2},
{"postgui_halfile", "scara_postgui", 0, 1, 2},
{"postgui_hal_file", "scara_postgui.hal", 0, 1, 2},
{"postgui_hal_file", "scara_postgui", 0, 1, 2},
{"POSTGUI_HALFILE", "scara_postgui.hal", 0, 1, 2},
{"POSTGUI_HALFILE", "scara_postgui", 0, 1, 2},
{"remap", "configs/sim/axis/vismach/scara", 0, 1, 2},
{"remap", "scara", 0, 1, 2},
{"config", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"configPath", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"config_path", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"iniFile", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"iniFileName", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini_file", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/qtaxis/non-trivial/scara/scara.ini", 0, 1, 2},
{"remap", "configs/sim/qtaxis/non-trivial/scara", 0, 1, 2},
{"config", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"configPath", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"config_path", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"iniFile", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"iniFileName", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini_file", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"ini_file_name", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"INI_FILE_NAME", "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini", 0, 1, 2},
{"remap", "configs/sim/qtvcp_screens/non-trivial/scara", 0, 1, 2},

View File

@@ -27,6 +27,22 @@ designed project code. See `docs/linuxcnc-source-policy.md`.
4. Compile with Emscripten after replacing `dlopen`, Python, HAL and filesystem-only features.
5. Compare event output with native LinuxCNC using the same G-code corpus.
## Current Workstreams
The current porting work is being split into six small contexts and should be
advanced in this order:
1. `switchkins/remap` generator and tables
2. `linuxcnc-kinematics` source manifest
3. WASM filesystem and OPFS persistence
4. browser app integration
5. Node and browser smoke coverage
6. source-link, build, and documentation constraints
Each step should stay source-backed and close one small gap at a time. A single
pass should advance one workstream context only; crossing into another context
requires naming the dependency and keeping the edit set minimal.
## Dialect expansion
LinuxCNC support should be the baseline. Fanuc and Siemens support should be implemented as dialect adapters, not by forking the simulator core.

View File

@@ -159,8 +159,13 @@ LinuxCNC interpreter sources currently involve Python/Boost.Python remap paths.
- disable Python remap
- disable dynamic module loading
- replace file-backed parameter persistence with in-memory buffers
- provide browser-safe tool table and INI/config loading through JSON
- route LinuxCNC parameter-file reads and writes through an OPFS-backed wasm
workspace, preserving `rs274ngc.var` and `.bak` persistence in browser runs
as defined by `src/emc/rs274ngc/interp_internal.hh` and exercised by
`Interp::restore_parameters`/`Interp::save_parameters` in
`src/emc/rs274ngc/rs274ngc_pre.cc`
- provide browser-safe tool table and INI/config loading through JSON or
OPFS-backed files
## Step 5: Emscripten build

View File

@@ -3,8 +3,23 @@
The simulator must not grow independently designed functional behavior.
Functional behavior must come from LinuxCNC source code.
Hard rules:
- Port-first: prefer direct LinuxCNC source porting, trimming, wrapping, or
platform adaptation over project-authored replacements.
- Do not write project-owned functional CNC behavior.
- Functional CNC behavior must be ported, trimmed, wrapped, or routed from
LinuxCNC source code.
- Any G/M-code interpretation, motion behavior, kinematics, coordinate
handling, cutter compensation, canned cycle, parameter expression, modal
state, tool data, remap behavior, or RTCP behavior must first be traced to
LinuxCNC source before implementation.
- Browser-side wasm filesystem behavior must use OPFS-backed storage. Do not
add browser filesystem paths that bypass OPFS.
Allowed project code:
- LinuxCNC source porting, trimming, wrappers, and platform adaptation.
- Thin adapters between LinuxCNC source code and the simulator C API.
- Platform shims needed to compile LinuxCNC code for native tests or wasm.
- Event serialization, test fixtures, build scripts, and documentation.
@@ -22,7 +37,81 @@ Required workflow for every functional change:
2. Add or update a test that demonstrates the LinuxCNC behavior.
3. Port, wrap, or route to that LinuxCNC implementation.
4. Record the source file/function in code comments or nearby documentation when the mapping is not obvious.
5. Run `./test-native.sh` and `./test-linuxcnc-source-link.sh`.
5. Keep the change inside one active workstream context unless a dependency is
unavoidable and explicitly named in the final report.
6. Run `./test-native.sh` and `./test-linuxcnc-source-link.sh`.
7. For source-manifest, build, or build-policy changes, also run the aggregate
and guardrail checks: `./test-all-native.sh`,
`./test-linuxcnc-source-syntax.sh`, and
`./test-linuxcnc-wasm-cmake-safe-probe.sh`.
Efficiency rules:
- Do not add a smoke-only behavior path when the LinuxCNC-backed route already
exists; add coverage to the source-backed route instead.
- Do not mix generator/table work, manifests, OPFS/browser work, and build
policy cleanup in the same pass unless the files directly depend on each
other.
- Prefer tightening an existing probe or manifest over adding another broad
end-to-end smoke case.
- Default native, source-link, build-wasm, and wasm-safe CMake probe
parallelism to `CNC_SIM_BUILD_JOBS:-8`; raise it only by explicit
positive-integer environment override.
- Persistent native, source-link, and wasm-safe CMake probe build directories
may be overridden for local workflows, but must not point at the filesystem
root.
- Build policy knobs are intentionally narrow:
`CNC_SIM_BUILD_JOBS`, `CNC_SIM_NATIVE_BUILD_DIR`,
`CNC_SIM_SOURCE_LINK_BUILD_DIR`, `CNC_SIM_SOURCE_SYNTAX_BUILD_DIR`, and
`CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR`. Build directory overrides must
stay non-empty, whitespace-free, and outside the filesystem root.
- `build-wasm.sh` keeps fixed artifact paths: CMake writes `build/wasm`, then
the script copies and byte-compares `cnc_sim.js` and `cnc_sim.wasm` under
`web/public` before running Node and browser smoke checks.
Build and source-link guardrails:
- `CNC_SIM_BUILD_JOBS` must be a positive integer and defaults to `8`.
- `test-native.sh` uses persistent `build/native-test` objects, dependency
files, and `native-build.signature`.
- `test-linuxcnc-source-link.sh` uses persistent `build/source-link-test`
objects, dependency files, and `source-link-build.signature`.
- `test-linuxcnc-source-syntax.sh` uses persistent
`build/source-syntax-test` stamps, dependency files, and
`source-syntax.signature`.
- `test-linuxcnc-wasm-cmake-safe-probe.sh` uses persistent
`build/wasm-cmake-safe-probe` and `wasm-cmake-safe-probe.signature`.
- Native, source-link, source-syntax, wasm-safe CMake probe, and build-wasm
paths must reject empty, whitespace-containing, or filesystem-root build
directory overrides before doing destructive cleanup.
- Native, source-link, source-syntax, and build-wasm entry points must serialize
shared non-concurrency-safe paths with lock files before writing fixed
outputs.
- Source-link and syntax probes must build from generated makefiles with
`-MMD -MP` dependency tracking and `make --output-sync=target`.
- `build-wasm.sh` must run blocker, syntax, object, CMake, tooldata,
`interp_*`, Python, `rs274ngc_pre`, and runtime link probes before copying
browser artifacts.
- `build-wasm.sh` must check `cmake`, `emcmake`, `emcc`, and `node` before the
Emscripten configure step.
- `build-wasm.sh` must build `cnc_sim_wasm_runtime_probe` before
`cnc_sim_wasm`.
- `build-wasm.sh` must copy `build/wasm/cnc_sim.js` and
`build/wasm/cnc_sim.wasm` exactly once each.
- `build-wasm.sh` must compare copied artifacts with `cmp -s` before running
Node or browser smoke tests.
- `build-wasm.sh` must run the Node smoke before the browser smoke.
- The default wasm manifest partition must remain explicitly checked for 25
wasm-safe core sources and 9 blocked sources.
- Manifest source listers must reject malformed lines, absolute source paths,
unknown groups, unknown filters, and duplicate source entries.
- Native link flags must include the built LinuxCNC `lib` path and rpath.
- Common CXX flag helpers must surface helper failures instead of hiding them
behind process substitution or `read`.
- Temporary probe reports must be cleaned on failure and preserved only after a
successful report path explicitly opts in.
- Documentation-only build policy edits should still update the guardrail
checks that enforce the relevant script and artifact ordering.
Current temporary exceptions:

View File

@@ -5,15 +5,49 @@ cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
mode=table
if [[ "${1:-}" == "--config-cases" ]]; then
all_output_dir=
usage() {
echo "usage: $0 [--config-cases|--json-cases|--all-output-dir DIR] [manifest]" >&2
}
case "${1:-}" in
--config-cases)
mode=config-cases
shift
elif [[ "${1:-}" == "--json-cases" ]]; then
;;
--json-cases)
mode=json-cases
shift
fi
;;
--all-output-dir)
mode=all
if [[ -z "${2:-}" || "${2:-}" == --* ]]; then
usage
echo "missing output directory for --all-output-dir" >&2
exit 1
fi
all_output_dir=$2
shift 2
;;
--help|-h)
usage
exit 0
;;
--*)
usage
echo "unknown switchkins remap table generator option: $1" >&2
exit 1
;;
esac
manifest=${1:-linuxcnc-kinematics-source-files.txt}
if [[ -n "${2:-}" ]]; then
usage
echo "too many switchkins remap table generator arguments" >&2
exit 1
fi
if [[ ! -d "$linuxcnc_root" ]]; then
echo "missing LinuxCNC root: $linuxcnc_root" >&2
exit 1
@@ -22,34 +56,58 @@ if [[ ! -f "$manifest" ]]; then
echo "missing kinematics manifest: $manifest" >&2
exit 1
fi
if [[ "$mode" == all && -z "$all_output_dir" ]]; then
echo "missing output directory for --all-output-dir" >&2
exit 1
fi
linuxcnc_root=$(cd "$linuxcnc_root" && pwd)
normalize_alias() {
tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'
}
trim_value() {
sed 's/[[:space:]]*[#;].*$//; s/^[[:space:]]*//; s/[[:space:]]*$//'
}
ini_value() {
read_ini_switchkins_config() {
local file=$1
local key=$2
sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//Ip" "$file" | head -n 1 | trim_value
}
awk '
function trim(value) {
sub(/^[[:space:]]*/, "", value)
sub(/[[:space:]]*$/, "", value)
return value
}
{
line = $0
sub(/[[:space:]]*[#;].*$/, "", line)
line = trim(line)
if (line !~ /=/) {
next
}
key = line
sub(/[[:space:]]*=.*/, "", key)
key = tolower(trim(key))
value = line
sub(/^[^=]*=[[:space:]]*/, "", value)
value = trim(value)
ini_values() {
local file=$1
local key=$2
sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//Ip" "$file" | trim_value
}
remap_ngc_name() {
local file=$1
local mcode=$2
sed -n "s/.*REMAP[[:space:]]*=[[:space:]]*M$mcode[[:space:]][^#;]*ngc[[:space:]]*=[[:space:]]*\\([^[:space:]#;]*\\).*/\\1/Ip" "$file" |
head -n 1
if (key == "machine") {
print "machine\t" value
} else if (key == "kinematics") {
print "kinematics\t" value
} else if (key == "halfile") {
print "halfile\t" value
} else if (key == "postgui_halfile") {
print "postgui_halfile\t" value
} else if (key == "subroutine_path") {
print "subroutine_path\t" value
} else if (key == "remap") {
lower = tolower(value)
for (mcode = 428; mcode <= 430; mcode++) {
if (lower ~ "m" mcode "([^0-9]|$)" &&
match(lower, /ngc[[:space:]]*=[[:space:]]*[^[:space:]]+/)) {
ngc = substr(value, RSTART, RLENGTH)
sub(/^[Nn][Gg][Cc][[:space:]]*=[[:space:]]*/, "", ngc)
print "remap_" mcode "\t" ngc
}
}
}
}
' "$file"
}
resolve_config_path() {
@@ -64,26 +122,92 @@ resolve_config_path() {
relative_linuxcnc_path() {
local path=$1
path=$(cd "$(dirname "$path")" && pwd)/$(basename "$path")
path=$(cd "${path%/*}" && pwd)/${path##*/}
if [[ "$path" != "$linuxcnc_root/"* ]]; then
echo "resolved LinuxCNC source path escapes LinuxCNC root: $path" >&2
exit 1
fi
printf '%s\n' "${path#"$linuxcnc_root/"}"
}
remap_source_for_mcode() {
local config=$1
local mcode=$2
local ngc_name
ngc_name=$(remap_ngc_name "$config" "$mcode")
if [[ -z "$ngc_name" ]]; then
return 1
declare -A config_dir_cache=()
declare -A manifest_config_sources=()
declare -A used_config_sources=()
declare -A manifest_remap_sources=()
declare -A used_remap_sources=()
manifest_config_count=0
manifest_remap_count=0
validate_manifest_source_path() {
local group=$1
local path=$2
if [[ -z "$path" ]]; then
echo "empty LinuxCNC $group source path in $manifest" >&2
exit 1
fi
if [[ "$path" = /* || "$path" == *..* ]]; then
echo "LinuxCNC $group source path must stay under LinuxCNC root: $path" >&2
exit 1
fi
if [[ ! -f "$linuxcnc_root/$path" ]]; then
echo "missing LinuxCNC $group source listed in $manifest: $path" >&2
exit 1
fi
}
while IFS=: read -r manifest_group manifest_path _manifest_note; do
case "$manifest_group" in
config)
validate_manifest_source_path "$manifest_group" "$manifest_path"
if [[ -n "${manifest_config_sources[$manifest_path]:-}" ]]; then
echo "duplicate LinuxCNC config source in $manifest: $manifest_path" >&2
exit 1
fi
manifest_config_sources[$manifest_path]=1
manifest_config_count=$((manifest_config_count + 1))
;;
remap)
validate_manifest_source_path "$manifest_group" "$manifest_path"
if [[ "$manifest_path" != *.ngc ]]; then
echo "LinuxCNC remap source must be an ngc file in $manifest: $manifest_path" >&2
exit 1
fi
case "${manifest_path##*/}" in
428remap.ngc|429remap.ngc|430remap.ngc)
;;
*)
echo "LinuxCNC switchkins remap source must be 428/429/430remap.ngc in $manifest: $manifest_path" >&2
exit 1
;;
esac
if [[ -n "${manifest_remap_sources[$manifest_path]:-}" ]]; then
echo "duplicate LinuxCNC remap source in $manifest: $manifest_path" >&2
exit 1
fi
manifest_remap_sources[$manifest_path]=1
manifest_remap_count=$((manifest_remap_count + 1))
;;
esac
done <"$manifest"
config_dir_for() {
local config=$1
if [[ -z "${config_dir_cache[$config]:-}" ]]; then
config_dir_cache[$config]=$(cd "${config%/*}" && pwd)
fi
printf '%s\n' "${config_dir_cache[$config]}"
}
remap_source_for_ngc() {
local config=$1
local ngc_name=$2
local subroutine_paths=${3:-}
if [[ "$ngc_name" != *.ngc ]]; then
ngc_name="$ngc_name.ngc"
fi
local config_dir
config_dir=$(cd "$(dirname "$config")" && pwd)
local subroutine_paths
subroutine_paths=$(ini_values "$config" "SUBROUTINE_PATH")
config_dir=$(config_dir_for "$config")
if [[ -z "$subroutine_paths" ]]; then
subroutine_paths="."
fi
@@ -107,41 +231,114 @@ remap_source_for_mcode() {
return 1
}
kinstype_for_mcode() {
config_source_for_halfile() {
local config=$1
local mcode=$2
local halfile=$2
[[ -n "$halfile" ]] || return 1
[[ "$halfile" == LIB:* ]] && return 1
local hal_path=${halfile%%[[:space:]]*}
[[ -n "$hal_path" ]] || return 1
local config_dir
config_dir=$(config_dir_for "$config")
local resolved
resolved=$(resolve_config_path "$config_dir" "$hal_path")
if [[ ! -f "$resolved" ]]; then
echo "missing LinuxCNC HAL source referenced by ${config#"$linuxcnc_root/"}: $halfile" >&2
exit 1
fi
relative_linuxcnc_path "$resolved"
}
kinstype_for_remap_source() {
local source
if ! source=$(remap_source_for_mcode "$config" "$mcode"); then
printf '%s\n' "-1"
source=$1
if [[ -z "$source" ]]; then
printf '%s\n' -1
return 0
fi
sed -n 's/^[[:space:]]*#<kinstype>[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' \
"$linuxcnc_root/$source" |
head -n 1
awk '
/^[[:space:]]*#<kinstype>[[:space:]]*=/ {
value = $0
sub(/^[[:space:]]*#<kinstype>[[:space:]]*=[[:space:]]*/, "", value)
sub(/[^0-9].*$/, "", value)
print value
exit
}
' "$linuxcnc_root/$source"
}
json_compact_string() {
local value=$1
printf '%s' "$value" | normalize_alias | sed 's/\\/\\\\/g; s/"/\\"/g'
require_remap_source() {
local config_path=$1
local mcode=$2
local ngc_name=$3
local source=$4
if [[ -z "$source" ]]; then
echo "missing LinuxCNC M$mcode remap source for $config_path: ngc=$ngc_name" >&2
exit 1
fi
}
cpp_string() {
local value=$1
printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g'
require_kinstype() {
local config_path=$1
local mcode=$2
local source=$3
local kinstype=$4
if [[ -z "$kinstype" || "$kinstype" == -1 ]]; then
echo "missing #<kinstype> assignment for LinuxCNC M$mcode remap source: $source ($config_path)" >&2
exit 1
fi
}
record_remap_source() {
local config_path=$1
local mcode=$2
local source=$3
if [[ -z "${manifest_remap_sources[$source]:-}" ]]; then
echo "LinuxCNC M$mcode remap source is not listed in $manifest: $source ($config_path)" >&2
exit 1
fi
used_remap_sources[$source]=1
}
record_config_source() {
local config_path=$1
local source=$2
local label=$3
if [[ -z "${manifest_config_sources[$source]:-}" ]]; then
echo "LinuxCNC $label source is not listed in $manifest: $source ($config_path)" >&2
exit 1
fi
used_config_sources[$source]=1
}
declare -A seen_aliases=()
declare -A seen_cases=()
declare -A seen_case_values=()
entries=()
case_entries=()
json_case_entries=()
processed_config_count=0
resolved_remap_count=0
unique_config_source_count=0
unique_resolved_remap_count=0
escape_generated_string() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
printf '%s\n' "$value"
}
add_alias() {
local alias=$1
local m428=$2
local m429=$3
local m430=$4
alias=$(json_compact_string "$alias")
alias=${alias,,}
alias=${alias//[[:space:]]/}
alias=$(escape_generated_string "$alias")
[[ -n "$alias" ]] || return 0
local types="$m428,$m429,$m430"
@@ -166,18 +363,80 @@ add_case() {
[[ -n "$value" ]] || return 0
local types="$m428,$m429,$m430"
local value_key="$field|$value"
if [[ -n "${seen_case_values[$value_key]:-}" && "${seen_case_values[$value_key]}" != "$types" ]]; then
echo "ambiguous switchkins config case '$field=$value': ${seen_case_values[$value_key]} and $types" >&2
exit 1
fi
seen_case_values[$value_key]=$types
local key="$field|$value|$types"
[[ -z "${seen_cases[$key]:-}" ]] || return 0
seen_cases[$key]=1
local cpp_field
local cpp_value
cpp_field=$(cpp_string "$field")
cpp_value=$(cpp_string "$value")
local cpp_field=$field
local cpp_value=$value
cpp_field=$(escape_generated_string "$cpp_field")
cpp_value=$(escape_generated_string "$cpp_value")
case_entries+=(" {\"$cpp_field\", \"$cpp_value\", $m428, $m429, $m430},")
json_case_entries+=("{\"field\":\"$cpp_field\",\"value\":\"$cpp_value\",\"m428\":$m428,\"m429\":$m429,\"m430\":$m430}")
}
trim_machine_label() {
local machine=$1
machine=${machine%%(*}
machine="${machine#"${machine%%[![:space:]]*}"}"
machine="${machine%"${machine##*[![:space:]]}"}"
printf '%s\n' "$machine"
}
add_kinematics_variants() {
local callback=$1
local field=$2
local kinematics=$3
local m428=$4
local m429=$5
local m430=$6
[[ -n "$kinematics" ]] || return 0
"$callback" "$field" "$kinematics" "$m428" "$m429" "$m430"
local first_word=${kinematics%%[[:space:]]*}
"$callback" "$field" "$first_word" "$m428" "$m429" "$m430"
"$callback" "$field" "${first_word%_kins}" "$m428" "$m429" "$m430"
"$callback" "$field" "${first_word%-kins}" "$m428" "$m429" "$m430"
local first_word_hyphen=${first_word//_/-}
local first_word_without_underscore_kins=${first_word%_kins}
local first_word_without_dash_kins=${first_word%-kins}
"$callback" "$field" "$first_word_hyphen" "$m428" "$m429" "$m430"
"$callback" "$field" "${first_word_without_underscore_kins//_/-}" "$m428" "$m429" "$m430"
"$callback" "$field" "${first_word_without_dash_kins//_/-}" "$m428" "$m429" "$m430"
}
add_alias_variant() {
local _field=$1
shift
add_alias "$@"
}
add_case_variant() {
add_case "$@"
}
add_halfile_case_fields() {
local field=$1
local halfile=$2
local m428=$3
local m429=$4
local m430=$5
local hal_base=${halfile##*/}
local hal_stem=${hal_base%.hal}
add_case "$field" "$halfile" "$m428" "$m429" "$m430"
add_case "$field" "$hal_base" "$m428" "$m429" "$m430"
add_case "$field" "$hal_stem" "$m428" "$m429" "$m430"
}
add_machine_aliases() {
local machine=$1
local m428=$2
@@ -185,7 +444,9 @@ add_machine_aliases() {
local m430=$4
[[ -n "$machine" ]] || return 0
add_alias "$machine" "$m428" "$m429" "$m430"
add_alias "${machine%%(*}" "$m428" "$m429" "$m430"
local trimmed_machine
trimmed_machine=$(trim_machine_label "$machine")
add_alias "$trimmed_machine" "$m428" "$m429" "$m430"
}
add_kinematics_aliases() {
@@ -194,15 +455,31 @@ add_kinematics_aliases() {
local m429=$3
local m430=$4
[[ -n "$kinematics" ]] || return 0
add_alias "$kinematics" "$m428" "$m429" "$m430"
add_kinematics_variants add_alias_variant "" "$kinematics" "$m428" "$m429" "$m430"
}
local first_word=${kinematics%%[[:space:]]*}
add_alias "$first_word" "$m428" "$m429" "$m430"
add_alias "${first_word%_kins}" "$m428" "$m429" "$m430"
add_alias "${first_word%-kins}" "$m428" "$m429" "$m430"
add_alias "$(printf '%s' "$first_word" | tr '_' '-')" "$m428" "$m429" "$m430"
add_alias "$(printf '%s' "${first_word%_kins}" | tr '_' '-')" "$m428" "$m429" "$m430"
add_alias "$(printf '%s' "${first_word%-kins}" | tr '_' '-')" "$m428" "$m429" "$m430"
add_switchkins_cases() {
local config_dir=$1
local config_stem=$2
local machine=$3
local kinematics=$4
local m428=$5
local m429=$6
local m430=$7
add_case "switchkins" "${config_dir##*/}" "$m428" "$m429" "$m430"
add_case "switchkins" "$config_stem" "$m428" "$m429" "$m430"
if [[ -n "$machine" ]]; then
add_case "switchkins" "$machine" "$m428" "$m429" "$m430"
local trimmed_machine
trimmed_machine=$(trim_machine_label "$machine")
add_case "switchkins" "$trimmed_machine" "$m428" "$m429" "$m430"
fi
if [[ -n "$kinematics" ]]; then
add_kinematics_variants add_case_variant "switchkins" "$kinematics" "$m428" "$m429" "$m430"
fi
}
add_halfile_aliases() {
@@ -226,14 +503,24 @@ add_halfile_cases() {
[[ -n "$halfile" ]] || return 0
[[ "$halfile" == LIB:* ]] && return 0
local hal_base=${halfile##*/}
local hal_stem=${hal_base%.hal}
add_case "halfile" "$halfile" "$m428" "$m429" "$m430"
add_case "halfile" "$hal_base" "$m428" "$m429" "$m430"
add_case "halfile" "$hal_stem" "$m428" "$m429" "$m430"
add_case "hal_file" "$halfile" "$m428" "$m429" "$m430"
add_case "hal_file" "$hal_base" "$m428" "$m429" "$m430"
add_case "hal_file" "$hal_stem" "$m428" "$m429" "$m430"
add_halfile_case_fields "halFile" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "halfile" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "hal_file" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "HALFILE" "$halfile" "$m428" "$m429" "$m430"
}
add_postgui_halfile_cases() {
local halfile=$1
local m428=$2
local m429=$3
local m430=$4
[[ -n "$halfile" ]] || return 0
[[ "$halfile" == LIB:* ]] && return 0
add_halfile_case_fields "postguiHalFile" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "postgui_halfile" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "postgui_hal_file" "$halfile" "$m428" "$m429" "$m430"
add_halfile_case_fields "POSTGUI_HALFILE" "$halfile" "$m428" "$m429" "$m430"
}
while IFS=: read -r group path _note; do
@@ -248,23 +535,96 @@ while IFS=: read -r group path _note; do
continue
fi
m428=$(kinstype_for_mcode "$config" 428)
m429=$(kinstype_for_mcode "$config" 429)
m430=$(kinstype_for_mcode "$config" 430)
machine=
kinematics=
halfiles=
postgui_halfiles=
subroutine_paths=
remap_ngc_428=
remap_ngc_429=
remap_ngc_430=
while IFS=$'\t' read -r ini_key ini_value; do
case "$ini_key" in
machine)
[[ -n "$machine" ]] || machine=$ini_value
;;
kinematics)
[[ -n "$kinematics" ]] || kinematics=$ini_value
;;
halfile)
halfiles+="${halfiles:+$'\n'}$ini_value"
;;
postgui_halfile)
postgui_halfiles+="${postgui_halfiles:+$'\n'}$ini_value"
;;
subroutine_path)
subroutine_paths+="${subroutine_paths:+$'\n'}$ini_value"
;;
remap_428)
[[ -n "$remap_ngc_428" ]] || remap_ngc_428=$ini_value
;;
remap_429)
[[ -n "$remap_ngc_429" ]] || remap_ngc_429=$ini_value
;;
remap_430)
[[ -n "$remap_ngc_430" ]] || remap_ngc_430=$ini_value
;;
esac
done < <(read_ini_switchkins_config "$config")
if [[ -z "$subroutine_paths" ]]; then
subroutine_paths="."
fi
remap_source_428=
remap_source_429=
remap_source_430=
if [[ -n "$remap_ngc_428" ]]; then
remap_source_428=$(remap_source_for_ngc "$config" "$remap_ngc_428" "$subroutine_paths" || true)
require_remap_source "$path" 428 "$remap_ngc_428" "$remap_source_428"
record_remap_source "$path" 428 "$remap_source_428"
resolved_remap_count=$((resolved_remap_count + 1))
fi
if [[ -n "$remap_ngc_429" ]]; then
remap_source_429=$(remap_source_for_ngc "$config" "$remap_ngc_429" "$subroutine_paths" || true)
require_remap_source "$path" 429 "$remap_ngc_429" "$remap_source_429"
record_remap_source "$path" 429 "$remap_source_429"
resolved_remap_count=$((resolved_remap_count + 1))
fi
if [[ -n "$remap_ngc_430" ]]; then
remap_source_430=$(remap_source_for_ngc "$config" "$remap_ngc_430" "$subroutine_paths" || true)
require_remap_source "$path" 430 "$remap_ngc_430" "$remap_source_430"
record_remap_source "$path" 430 "$remap_source_430"
resolved_remap_count=$((resolved_remap_count + 1))
fi
m428=$(kinstype_for_remap_source "$remap_source_428")
m429=$(kinstype_for_remap_source "$remap_source_429")
m430=$(kinstype_for_remap_source "$remap_source_430")
[[ -z "$remap_ngc_428" ]] || require_kinstype "$path" 428 "$remap_source_428" "$m428"
[[ -z "$remap_ngc_429" ]] || require_kinstype "$path" 429 "$remap_source_429" "$m429"
[[ -z "$remap_ngc_430" ]] || require_kinstype "$path" 430 "$remap_source_430" "$m430"
[[ -n "$m428" ]] || m428=-1
[[ -n "$m429" ]] || m429=-1
[[ -n "$m430" ]] || m430=-1
processed_config_count=$((processed_config_count + 1))
record_config_source "$path" "$path" "INI config"
machine=$(ini_value "$config" "MACHINE")
kinematics=$(ini_value "$config" "KINEMATICS")
halfiles=$(ini_values "$config" "HALFILE")
config_dir=${path%/*}
config_base=${path##*/}
config_stem=${config_base%.ini}
add_case "config" "$path" "$m428" "$m429" "$m430"
add_case "configPath" "$path" "$m428" "$m429" "$m430"
add_case "config_path" "$path" "$m428" "$m429" "$m430"
add_case "ini" "$path" "$m428" "$m429" "$m430"
add_case "iniFile" "$path" "$m428" "$m429" "$m430"
add_case "iniFileName" "$path" "$m428" "$m429" "$m430"
add_case "ini_file" "$path" "$m428" "$m429" "$m430"
add_case "ini_file_name" "$path" "$m428" "$m429" "$m430"
add_case "INI_FILE_NAME" "$path" "$m428" "$m429" "$m430"
add_case "machine" "$machine" "$m428" "$m429" "$m430"
add_case "kinematics" "$kinematics" "$m428" "$m429" "$m430"
add_switchkins_cases "$config_dir" "$config_stem" "$machine" "$kinematics" "$m428" "$m429" "$m430"
add_alias "$path" "$m428" "$m429" "$m430"
add_alias "$config_dir" "$m428" "$m429" "$m430"
@@ -273,39 +633,119 @@ while IFS=: read -r group path _note; do
add_machine_aliases "$machine" "$m428" "$m429" "$m430"
add_kinematics_aliases "$kinematics" "$m428" "$m429" "$m430"
while IFS= read -r halfile; do
if hal_source=$(config_source_for_halfile "$config" "$halfile"); then
record_config_source "$path" "$hal_source" "HALFILE"
fi
add_halfile_cases "$halfile" "$m428" "$m429" "$m430"
add_halfile_aliases "$halfile" "$m428" "$m429" "$m430"
done <<<"$halfiles"
while IFS= read -r halfile; do
if hal_source=$(config_source_for_halfile "$config" "$halfile"); then
record_config_source "$path" "$hal_source" "POSTGUI_HALFILE"
fi
add_postgui_halfile_cases "$halfile" "$m428" "$m429" "$m430"
add_halfile_aliases "$halfile" "$m428" "$m429" "$m430"
done <<<"$postgui_halfiles"
for mcode in 428 429 430; do
if remap_source=$(remap_source_for_mcode "$config" "$mcode"); then
for remap_source in "$remap_source_428" "$remap_source_429" "$remap_source_430"; do
if [[ -n "$remap_source" ]]; then
remap_dir=${remap_source%/*}
add_alias "$remap_dir" "$m428" "$m429" "$m430"
add_alias "${remap_dir%/*}" "$m428" "$m429" "$m430"
add_alias "${remap_dir%/remap_subs}" "$m428" "$m429" "$m430"
remap_parent=${remap_dir%/remap_subs}
add_alias "${remap_parent##*/}" "$m428" "$m429" "$m430"
add_case "remap" "$remap_parent" "$m428" "$m429" "$m430"
add_case "remap" "${remap_parent##*/}" "$m428" "$m429" "$m430"
fi
done
done <"$manifest"
if [[ "$mode" == table ]]; then
unique_config_source_count=${#used_config_sources[@]}
unique_resolved_remap_count=${#used_remap_sources[@]}
validate_generated_content() {
if ((manifest_config_count == 0)); then
echo "no LinuxCNC config sources listed in manifest: $manifest" >&2
exit 1
fi
if ((manifest_remap_count == 0)); then
echo "no LinuxCNC remap sources listed in manifest: $manifest" >&2
exit 1
fi
if ((processed_config_count == 0)); then
echo "no LinuxCNC switchkins remap configs found in manifest: $manifest" >&2
exit 1
fi
if ((resolved_remap_count == 0)); then
echo "no LinuxCNC M428/M429/M430 remap sources resolved from manifest: $manifest" >&2
exit 1
fi
if ((unique_resolved_remap_count == 0)); then
echo "no unique LinuxCNC M428/M429/M430 remap sources resolved from manifest: $manifest" >&2
exit 1
fi
if ((unique_config_source_count == 0)); then
echo "no unique LinuxCNC config sources used from manifest: $manifest" >&2
exit 1
fi
if ((unique_config_source_count != manifest_config_count)); then
echo "LinuxCNC config manifest coverage mismatch: used $unique_config_source_count of $manifest_config_count config sources" >&2
local config_source
for config_source in "${!manifest_config_sources[@]}"; do
if [[ -z "${used_config_sources[$config_source]:-}" ]]; then
echo "unused LinuxCNC config manifest source: $config_source" >&2
fi
done
exit 1
fi
if ((unique_resolved_remap_count != manifest_remap_count)); then
echo "LinuxCNC remap manifest coverage mismatch: resolved $unique_resolved_remap_count of $manifest_remap_count remap sources" >&2
local remap_source
for remap_source in "${!manifest_remap_sources[@]}"; do
if [[ -z "${used_remap_sources[$remap_source]:-}" ]]; then
echo "unused LinuxCNC remap manifest source: $remap_source" >&2
fi
done
exit 1
fi
if ((${#entries[@]} == 0)); then
echo "empty generated switchkins remap alias table" >&2
exit 1
fi
if ((${#case_entries[@]} == 0)); then
echo "empty generated switchkins remap config cases" >&2
exit 1
fi
if ((${#case_entries[@]} != ${#json_case_entries[@]})); then
echo "generated C++/JSON switchkins config case count mismatch: ${#case_entries[@]} != ${#json_case_entries[@]}" >&2
exit 1
fi
}
validate_generated_content
write_table() {
cat <<'EOF'
// Generated by ./generate-linuxcnc-switchkins-remap-table.sh.
// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/SUBROUTINE_PATH/REMAP entries and
// Source: LinuxCNC INI MACHINE/KINEMATICS/HALFILE/POSTGUI_HALFILE/SUBROUTINE_PATH/REMAP entries and
// adjacent remap_subs/{428,429,430}remap.ngc #<kinstype> assignments.
EOF
printf '%s\n' "${entries[@]}"
elif [[ "$mode" == config-cases ]]; then
}
write_config_cases() {
cat <<'EOF'
// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases.
// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, and non-LIB HALFILE entries
// for M428/M429/M430 remap #<kinstype> assignments.
// Source: LinuxCNC INI config path, MACHINE, KINEMATICS, non-LIB HALFILE/POSTGUI_HALFILE,
// and adjacent remap_subs entries for M428/M429/M430 #<kinstype> assignments.
EOF
printf '%s\n' "${case_entries[@]}"
elif [[ "$mode" == json-cases ]]; then
}
write_json_cases() {
printf '[\n'
for i in "${!json_case_entries[@]}"; do
suffix=,
@@ -315,6 +755,34 @@ elif [[ "$mode" == json-cases ]]; then
printf ' %s%s\n' "${json_case_entries[$i]}" "$suffix"
done
printf ']\n'
}
write_generated_file() {
local path=$1
local label=$2
local writer=$3
if [[ -e "$path" && ! -f "$path" ]]; then
echo "cannot write generated $label: $path is not a file" >&2
exit 1
fi
"$writer" >"$path"
}
if [[ "$mode" == table ]]; then
write_table
elif [[ "$mode" == config-cases ]]; then
write_config_cases
elif [[ "$mode" == json-cases ]]; then
write_json_cases
elif [[ "$mode" == all ]]; then
if [[ -e "$all_output_dir" && ! -d "$all_output_dir" ]]; then
echo "switchkins remap output path is not a directory: $all_output_dir" >&2
exit 1
fi
mkdir -p "$all_output_dir"
write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_table.inc" "table" write_table
write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_config_cases.inc" "config cases" write_config_cases
write_generated_file "$all_output_dir/linuxcnc_switchkins_remap_config_cases.json" "JSON config cases" write_json_cases
else
echo "unknown switchkins remap table generator mode: $mode" >&2
exit 1

View File

@@ -26,23 +26,121 @@ core:src/emc/kinematics/ugenserkins.c:user generic serial kinematics module
core:src/emc/kinematics/userkfuncs.c:user kinematics helper functions used by M428/M429/M430 coverage
core:src/emc/kinematics/xyzac-trt-kins.c:XYZAC table rotary tilting switchkins module
core:src/emc/kinematics/xyzbc-trt-kins.c:XYZBC table rotary tilting switchkins module
core:src/libnml/posemath/_posemath.c:pose math support included by PUMA kinematics adapter
core:src/libnml/posemath/gomath.c:generic serial pose math support included by genser kinematics adapter
core:src/libnml/posemath/posemath.cc:LinuxCNC pose math C++ source tracked with kinematics posemath coverage
core:src/libnml/posemath/sincos.c:shared pose math sincos support included by PUMA/genser kinematics adapters
header:src/libnml/posemath/gomath.h:generic serial pose math declarations included by gomath source
header:src/libnml/posemath/gotypes.h:generic serial pose math type declarations included by gomath header
header:src/libnml/posemath/posemath.h:pose math declarations included by LinuxCNC kinematics adapters
header:src/libnml/posemath/sincos.h:pose math sincos declaration included by posemath sources
config:configs/sim/axis/vismach/5axis/bridgemill/5axis.ini:bridgemill MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal:bridgemill HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal:bridgemill POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini:XYZAB table dual rotary MACHINE/KINEMATICS and M428/M429 remap configuration source
config:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr-postgui.hal:XYZAB table dual rotary POSTGUI_HALFILE source referenced by M428/M429 switchkins config
config:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini:XYZAC TRT switchkins HAL parameter source used by M428 coverage
config:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini:XYZBC TRT switchkins HAL parameter source used by M428 coverage
config:configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal:XYZAC/XYZBC TRT POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini:XYZACB TRSRN MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn_postgui.hal:XYZACB TRSRN POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini:XYZBCA TRSRN MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn_postgui.hal:XYZBCA TRSRN POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/hexapod-sim/hexapod.ini:hexapod MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/hexapod-sim/kinematics.hal:hexapod HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/hexapod-sim/hexapod_postgui.hal:hexapod POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/melfa-sim/melfa.ini:melfa MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/melfa-sim/melfa_dh.hal:melfa DH HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal:melfa POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/millturn/millturn.ini:millturn MACHINE/KINEMATICS and M428/M429 remap configuration source
config:configs/sim/axis/vismach/millturn/millturn.hal:millturn HALFILE source referenced by M428/M429 switchkins config
config:configs/sim/axis/vismach/millturn/millturn-postgui.hal:millturn POSTGUI_HALFILE source referenced by M428/M429 switchkins config
config:configs/sim/axis/vismach/puma/puma.ini:PUMA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/puma/puma_dh.hal:PUMA DH HAL parameters referenced by PUMA switchkins configs
config:configs/sim/axis/vismach/puma/puma_postgui.hal:PUMA POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/puma/puma_cube.ini:PUMA cube MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/puma/puma560.ini:PUMA 560 MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/puma/puma560_dh.hal:PUMA 560 genserkins DH HAL parameters used by M428 coverage
config:configs/sim/axis/vismach/puma/puma560_postgui.hal:PUMA 560 POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/axis/vismach/puma/puma560_uvw.ini:PUMA 560 UVW MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/scara/scara.ini:AXIS SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/axis/vismach/scara/scara_postgui.hal:AXIS SCARA POSTGUI_HALFILE source referenced by M428/M429/M430 switchkins config
config:configs/sim/qtaxis/non-trivial/scara/scara.ini:QtAxis SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
config:configs/sim/qtvcp_screens/non-trivial/scara/scara.ini:QtVCP SCARA MACHINE/KINEMATICS and M428/M429/M430 remap configuration source
asset:configs/sim/axis/vismach/5axis/bridgemill/5axis.xml:bridgemill PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/bridgemill/5axisgui.ngc:bridgemill OPEN_FILE asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/bridgemill/README:bridgemill configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/5axis/table-dual-rotary/demos/xyzab-tdr-demo.ngc:XYZAB table dual rotary OPEN_FILE demo asset referenced by M428/M429 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-dual-rotary/README:XYZAB table dual rotary configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml:XYZAB table dual rotary PYVCP asset referenced by M428/M429 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc:XYZAC TRT OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc:XYZBC TRT OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/README:XYZAC/XYZBC TRT configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml:XYZAC TRT PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml:XYZBC TRT PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc:TRSRN OPEN_FILE demo asset referenced by XYZACB/XYZBCA M428/M429/M430 switchkins INI files
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/remap.py:TRSRN TWP Python remap source referenced by XYZACB/XYZBCA REMAP python entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/toplevel.py:TRSRN TWP Python TOPLEVEL source referenced by XYZACB/XYZBCA INI files
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/twp-helper-comp.py:TRSRN TWP HAL helper source referenced by XYZACB/XYZBCA HALCMD loadusr entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/python/util.py:TRSRN TWP Python utility source imported by remap.py
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g531remap.ngc:TRSRN G53.1 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g533remap.ngc:TRSRN G53.3 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g536remap.ngc:TRSRN G53.6 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/g69remap.ngc:TRSRN G69 remap wrapper referenced by XYZACB/XYZBCA INI REMAP entries
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_no_twp_reset.ngc:TRSRN ON_ABORT source referenced by XYZACB INI
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/remap_subs/on_abort_with_twp_reset.ngc:TRSRN ON_ABORT source referenced by XYZBCA INI
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/twp_vismach.py:TRSRN shared vismach source imported by XYZACB/XYZBCA GUI files
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzacb-trsrn-gui.py:XYZACB TRSRN vismach GUI source referenced by HALCMD loadusr
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/xyzbca-trsrn-gui.py:XYZBCA TRSRN vismach GUI source referenced by HALCMD loadusr
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/README:XYZACB TRSRN TWP configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/pyvcp_embed_tab_tcp.xml:XYZACB TRSRN embedded PyVCP TCP asset referenced by EMBED_TAB_COMMAND
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/work_piece_1.stl:XYZACB TRSRN workpiece STL asset referenced by vismach GUI source
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.xml:XYZACB TRSRN PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/README:XYZBCA TRSRN TWP configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/pyvcp_embed_tab_tcp.xml:XYZBCA TRSRN embedded PyVCP TCP asset referenced by EMBED_TAB_COMMAND
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/work_piece_1.stl:XYZBCA TRSRN workpiece STL asset referenced by vismach GUI source
asset:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.xml:XYZBCA TRSRN PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/hexapod-sim/README:hexapod configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/hexapod-sim/hexapod.xml:hexapod PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/melfa-sim/example.ngc:melfa OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/melfa-sim/README:melfa configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/melfa-sim/melfa.xml:melfa PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/millturn/example.ngc:millturn OPEN_FILE demo asset referenced by M428/M429 switchkins INI
asset:configs/sim/axis/vismach/millturn/README:millturn configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/millturn/millturn.xml:millturn PYVCP asset referenced by M428/M429 switchkins INI
asset:configs/sim/axis/vismach/puma/README:PUMA configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/puma/puma.xml:PUMA PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/puma/puma560.xml:PUMA 560 PYVCP asset referenced by M428/M429/M430 switchkins INI files
asset:configs/sim/axis/vismach/puma/puma_cube.ngc:PUMA cube OPEN_FILE demo asset referenced by M428/M429/M430 switchkins INI
asset:configs/sim/axis/vismach/scara/README:AXIS SCARA configuration source notes adjacent to INI coverage
asset:configs/sim/axis/vismach/scara/scara.xml:AXIS SCARA PYVCP asset referenced by M428/M429/M430 switchkins INI
asset:src/hal/user_comps/vismach/5axisgui.py:bridgemill vismach GUI source referenced by HALFILE loadusr
asset:src/hal/user_comps/vismach/hexagui.py:hexapod vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/melfagui.py:melfa vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/millturngui.py:millturn vismach GUI source referenced by HALFILE loadusr
asset:src/hal/user_comps/vismach/puma560gui.py:PUMA 560 vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/pumagui.py:PUMA vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/scaragui.py:SCARA vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/xyzab-tdr-gui.py:XYZAB table dual rotary vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/xyzac-trt-gui.py:XYZAC TRT vismach GUI source referenced by INI HALCMD loadusr
asset:src/hal/user_comps/vismach/xyzbc-trt-gui.py:XYZBC TRT vismach GUI source referenced by INI HALCMD loadusr
asset:share/qtvcp/panels/vismach_scara/vismach_scara.ui:QtVCP SCARA panel UI source referenced by EMBED_TAB_COMMAND
asset:share/qtvcp/panels/vismach_scara/vismach_scara_handler.py:QtVCP SCARA panel handler source referenced by EMBED_TAB_COMMAND
asset:lib/python/qtvcp/lib/qt_vismach/README.txt:QtVCP vismach library source notes adjacent to SCARA panel coverage
asset:lib/python/qtvcp/lib/qt_vismach/__init__.py:QtVCP vismach package source used by SCARA panel
asset:lib/python/qtvcp/lib/qt_vismach/primitives.py:QtVCP vismach primitives source used by SCARA panel
asset:lib/python/qtvcp/lib/qt_vismach/qt_vismach.py:QtVCP vismach base window source used by SCARA panel
asset:lib/python/qtvcp/lib/qt_vismach/scara.py:QtVCP SCARA vismach source imported by vismach_scara_handler.py
tooldata:configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl:bridgemill tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.tbl:XYZAB table dual rotary tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl:XYZAC TRT tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl:XYZBC TRT tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.tbl:XYZACB TRSRN tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.tbl:XYZBCA TRSRN tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/melfa-sim/melfa.tbl:melfa tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/millturn/millturn.tbl:millturn tool table source referenced by INI TOOL_TABLE
tooldata:configs/sim/axis/vismach/puma/puma.tbl:PUMA tool table source referenced by PUMA switchkins INI TOOL_TABLE entries
tooldata:configs/sim/qtaxis/non-trivial/scara/scara.tbl:QtAxis SCARA tool table source referenced by INI TOOL_TABLE
remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc:LinuxCNC M428 remap source selecting bridgemill kinstype 0
remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/429remap.ngc:LinuxCNC M429 remap source selecting bridgemill kinstype 1
remap:configs/sim/axis/vismach/5axis/bridgemill/remap_subs/430remap.ngc:LinuxCNC M430 remap source selecting bridgemill kinstype 2
@@ -74,11 +172,13 @@ remap:configs/sim/qtaxis/non-trivial/scara/remap_subs/430remap.ngc:LinuxCNC M430
remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/428remap.ngc:LinuxCNC M428 remap source selecting qtvcp scara kinstype 0
remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/429remap.ngc:LinuxCNC M429 remap source selecting qtvcp scara kinstype 1
remap:configs/sim/qtvcp_screens/non-trivial/scara/remap_subs/430remap.ngc:LinuxCNC M430 remap source selecting qtvcp scara kinstype 2
component:src/hal/components/millturn.comp:millturn switchable kinematics component source referenced by millturn INI KINEMATICS
component:src/hal/components/xyzab_tdr_kins.comp:XYZAB table dual rotary switchkins component source used by M428/M429/M430 coverage
component:src/hal/components/xyzacb_trsrn.comp:XYZACB transformation component source adjacent to LinuxCNC RTCP kinematics coverage
component:src/hal/components/xyzbca_trsrn.comp:XYZBCA transformation component source adjacent to LinuxCNC RTCP kinematics coverage
component:src/hal/components/matrixkins.comp:matrix kinematics component source for LinuxCNC kinematics completeness
component:src/hal/components/userkins.comp:user kinematics component template source for LinuxCNC switchkins/user kinematics coverage
generated:src/objects/hal/components/millturn.c:halcompile-generated millturn switchable kinematics C source used by millturn INI KINEMATICS
generated:src/objects/hal/components/xyzab_tdr_kins.c:halcompile-generated XYZAB table dual rotary switchkins C source used by the adapter
generated:src/objects/hal/components/xyzacb_trsrn.c:halcompile-generated XYZACB transformation C source
generated:src/objects/hal/components/xyzbca_trsrn.c:halcompile-generated XYZBCA transformation C source
@@ -95,4 +195,22 @@ header:src/emc/kinematics/rotarydeltakins-common.h:rotary delta shared header
header:src/emc/kinematics/switchkins.h:switchable kinematics header
metadata:src/emc/kinematics/Submakefile:LinuxCNC kinematics make build metadata
metadata:src/emc/kinematics/meson.build:LinuxCNC kinematics meson build metadata
metadata:src/libnml/posemath/Submakefile:LinuxCNC posemath make build metadata used by kinematics pose math coverage
metadata:src/libnml/posemath/meson.build:LinuxCNC posemath meson build metadata used by kinematics pose math coverage
metadata:src/hal/user_comps/vismach/Submakefile:LinuxCNC vismach user component build metadata for loadusr GUI sources
metadata:docs/src/man/man1/5axisgui.1.adoc:LinuxCNC bridgemill vismach GUI command documentation source
metadata:docs/src/man/man1/hexagui.1.adoc:LinuxCNC hexapod vismach GUI command documentation source
metadata:docs/src/man/man1/melfagui.1.adoc:LinuxCNC melfa vismach GUI command documentation source
metadata:docs/src/man/man1/puma560gui.1.adoc:LinuxCNC PUMA 560 vismach GUI command documentation source
metadata:docs/src/man/man1/pumagui.1.adoc:LinuxCNC PUMA vismach GUI command documentation source
metadata:docs/src/man/man1/scaragui.1.adoc:LinuxCNC SCARA vismach GUI command documentation source
metadata:docs/src/man/man1/xyzab-tdr-gui.1.adoc:LinuxCNC XYZAB table dual rotary vismach GUI command documentation source
metadata:docs/src/man/man1/xyzac-trt-gui.1.adoc:LinuxCNC XYZAC TRT vismach GUI command documentation source
metadata:docs/src/man/man1/xyzbc-trt-gui.1.adoc:LinuxCNC XYZBC TRT vismach GUI command documentation source
metadata:src/objects/hal/components/matrixkins.mak:halcompile-generated matrixkins make metadata adjacent to generated kinematics source
metadata:src/objects/hal/components/millturn.mak:halcompile-generated millturn make metadata adjacent to generated kinematics source
metadata:src/objects/hal/components/userkins.mak:halcompile-generated userkins make metadata adjacent to generated kinematics source
metadata:src/objects/hal/components/xyzab_tdr_kins.mak:halcompile-generated XYZAB table dual rotary make metadata adjacent to generated kinematics source
metadata:src/objects/hal/components/xyzacb_trsrn.mak:halcompile-generated XYZACB TRSRN make metadata adjacent to generated kinematics source
metadata:src/objects/hal/components/xyzbca_trsrn.mak:halcompile-generated XYZBCA TRSRN make metadata adjacent to generated kinematics source
asset:src/emc/kinematics/blend.fig:LinuxCNC kinematics reference asset tracked for full directory coverage

View File

@@ -11,6 +11,6 @@ output_mode=${3:-relative}
"$manifest" \
"$filter_group" \
"$output_mode" \
core,config,remap,component,generated,header,metadata,asset \
core,config,remap,component,generated,header,metadata,asset,tooldata \
all \
"missing kinematics manifest source"

View File

@@ -23,6 +23,14 @@ missing_bridge_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_bridge_smoke
missing_support_object_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_linuxcnc_support_object.XXXXXX.log")
missing_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_native_syntax_missing_source_manifest.XXXXXX.txt")
missing_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_native_syntax_missing_source.XXXXXX.log")
bad_line_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_bad_manifest_line.XXXXXX.txt")
bad_line_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_bad_manifest_line.XXXXXX.log")
absolute_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_absolute_manifest_source.XXXXXX.txt")
absolute_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_absolute_manifest_source.XXXXXX.log")
invalid_build_jobs_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_invalid_build_jobs.XXXXXX.log")
invalid_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_invalid_build_dir.XXXXXX.log")
empty_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_empty_build_dir.XXXXXX.log")
whitespace_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_whitespace_build_dir.XXXXXX.log")
cleanup_native_test_temps() {
rm -f \
@@ -41,7 +49,15 @@ cleanup_native_test_temps() {
"$missing_bridge_source_log" \
"$missing_support_object_log" \
"$missing_source_manifest" \
"$missing_source_log"
"$missing_source_log" \
"$bad_line_manifest" \
"$bad_line_log" \
"$absolute_source_manifest" \
"$absolute_source_log" \
"$invalid_build_jobs_log" \
"$invalid_build_dir_log" \
"$empty_build_dir_log" \
"$whitespace_build_dir_log"
}
trap cleanup_native_test_temps EXIT
@@ -67,12 +83,40 @@ for forbidden_manual_cleanup in \
'rm -f "$missing_core_source_log"' \
'rm -f "$missing_bridge_source_log"' \
'rm -f "$missing_support_object_log"' \
'rm -f "$missing_source_manifest" "$missing_source_log"'; do
'rm -f "$missing_source_manifest" "$missing_source_log"' \
'rm -f "$bad_line_manifest" "$bad_line_log"' \
'rm -f "$absolute_source_manifest" "$absolute_source_log"' \
'rm -f "$invalid_build_jobs_log"' \
'rm -f "$invalid_build_dir_log"' \
'rm -f "$empty_build_dir_log"' \
'rm -f "$whitespace_build_dir_log"'; do
if grep -Fx "$forbidden_manual_cleanup" test-all-native.sh >/dev/null; then
echo "native aggregate test still relies on manual cleanup: $forbidden_manual_cleanup" >&2
exit 1
fi
done
printf 'core:src/emc/rs274ngc/interp_arc.cc\n' >"$bad_line_manifest"
if ./check-linuxcnc-inputs.sh "$bad_line_manifest" >"$bad_line_log" 2>&1; then
echo "check-linuxcnc-inputs accepted a manifest line without a note" >&2
exit 1
fi
if ! grep -F "bad manifest line 1: core:src/emc/rs274ngc/interp_arc.cc" "$bad_line_log" >/dev/null; then
echo "check-linuxcnc-inputs did not report malformed manifest lines clearly" >&2
sed -n '1,20p' "$bad_line_log" >&2
exit 1
fi
printf 'core:/src/emc/rs274ngc/interp_arc.cc:absolute source path\n' >"$absolute_source_manifest"
if ./check-linuxcnc-inputs.sh "$absolute_source_manifest" >"$absolute_source_log" 2>&1; then
echo "check-linuxcnc-inputs accepted an absolute manifest source path" >&2
exit 1
fi
if ! grep -F "manifest source must be LinuxCNC-root relative: /src/emc/rs274ngc/interp_arc.cc" "$absolute_source_log" >/dev/null; then
echo "check-linuxcnc-inputs did not report absolute manifest paths clearly" >&2
sed -n '1,20p' "$absolute_source_log" >&2
exit 1
fi
grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.lock"' test-linuxcnc-source-link.sh >/dev/null
grep -F "flock 9" test-linuxcnc-source-link.sh >/dev/null
grep -F "missing LinuxCNC support object" list-linuxcnc-source-support-objects.sh >/dev/null
@@ -86,7 +130,6 @@ grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_linuxcnc_rs274_native.lock"' test-linux
grep -F "flock 9" test-linuxcnc-rs274-native.sh >/dev/null
for build_dir_probe_script in \
test-linuxcnc-source-objects.sh \
test-linuxcnc-source-link.sh \
test-linuxcnc-bridge-native.sh \
test-linuxcnc-rs274-native.sh \
test-linuxcnc-api-native.sh; do
@@ -99,11 +142,261 @@ for build_dir_probe_script in \
exit 1
fi
done
if ! awk '
/cleanup_wasm_cmake_probe_temps\(\)/ { in_cleanup = 1 }
in_cleanup && index($0, "\"$build_dir\"") { found_build_dir = 1 }
in_cleanup && /^}/ { in_cleanup = 0 }
END { exit found_build_dir }
' test-linuxcnc-wasm-cmake-safe-probe.sh; then
echo "wasm CMake safe probe cleanup no longer removes its persistent build dir" >&2
exit 1
fi
grep -F 'build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test}' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-link.sh >/dev/null
grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.lock"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'source_link_signature="$build_dir/source-link-build.signature"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''%s: %s\n'\'' "$build_dir/linuxcnc_${linuxcnc_index}.o" "$source"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''%s: %s\n'\'' "$build_dir/project_${project_index}.o" "$source"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-linuxcnc-source-link.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test}' test-native.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be empty' test-native.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir' test-native.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root' test-native.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-native.sh >/dev/null
grep -F 'exec 9>"$build_dir/native-test.lock"' test-native.sh >/dev/null
grep -F 'native_signature="$build_dir/native-build.signature"' test-native.sh >/dev/null
grep -F 'if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then' test-native.sh >/dev/null
grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-native.sh >/dev/null
grep -F 'printf '\''%s: %s\n'\'' "${core_objects[$core_index]}" "$source"' test-native.sh >/dev/null
grep -F 'printf '\''\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-native.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_SOURCE_SYNTAX_BUILD_DIR-build/source-syntax-test}' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'exec 9>"$build_dir/source-syntax.lock"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'source_list="$build_dir/linuxcnc_source_syntax_sources.txt"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'source_syntax_signature="$build_dir/source-syntax.signature"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'if [[ ! -f "$source_syntax_signature" ]] || ! cmp -s "$source_syntax_signature" "$source_syntax_next_signature"; then' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'printf '\''MANIFEST=%s\n'\'' "$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'printf '\''%s/source_%s.stamp: %s\n'\'' "$build_dir" "$source_index" "$source"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s/source_%s.d %s\n'\''' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'make --output-sync=target -j"$build_jobs" -f "$source_syntax_makefile"' test-linuxcnc-source-syntax.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' build-wasm.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' build-wasm.sh >/dev/null
grep -F 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' build-wasm.sh >/dev/null
grep -F 'cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs"' build-wasm.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_signature="$build_dir/wasm-cmake-safe-probe.signature"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX")' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$linuxcnc_root"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''MANIFEST=%s\n'\'' "$manifest"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'CNC_SIM_SOURCE_SYNTAX_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'Build directory overrides must' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'copies and byte-compares `cnc_sim.js` and `cnc_sim.wasm` under' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`CNC_SIM_BUILD_JOBS` must be a positive integer and defaults to `8`.' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`test-native.sh` uses persistent `build/native-test` objects' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`test-linuxcnc-source-link.sh` uses persistent `build/source-link-test`' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`test-linuxcnc-source-syntax.sh` uses persistent' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`test-linuxcnc-wasm-cmake-safe-probe.sh` uses persistent' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must reject empty, whitespace-containing, or filesystem-root build' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'entry points must serialize' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'shared non-concurrency-safe paths with lock files' docs/linuxcnc-source-policy.md >/dev/null
grep -F '`-MMD -MP` dependency tracking and `make --output-sync=target`' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must run blocker, syntax, object, CMake, tooldata,' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must check `cmake`, `emcmake`, `emcc`, and `node`' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must build `cnc_sim_wasm_runtime_probe` before' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must copy `build/wasm/cnc_sim.js` and' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must compare copied artifacts with `cmp -s` before running' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must run the Node smoke before the browser smoke' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'explicitly checked for 25' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'wasm-safe core sources and 9 blocked sources' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must reject malformed lines, absolute source paths,' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must include the built LinuxCNC `lib` path and rpath' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'must surface helper failures instead of hiding them' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'Temporary probe reports must be cleaned on failure' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'Documentation-only build policy edits should still update the guardrail' docs/linuxcnc-source-policy.md >/dev/null
grep -F 'python_include_output=$(python3.13-config --includes 2>/dev/null || python3-config --includes)' list-linuxcnc-source-common-cxxflags.sh >/dev/null
if grep -F 'read -r -a python_includes <<<"$(' list-linuxcnc-source-common-cxxflags.sh >/dev/null; then
echo "source common cxxflags still hides python-config failures behind read" >&2
exit 1
fi
for script in test-native.sh test-linuxcnc-source-syntax.sh test-linuxcnc-source-link.sh build-wasm.sh; do
if CNC_SIM_BUILD_JOBS=0 "./$script" >"$invalid_build_jobs_log" 2>&1; then
echo "$script accepted invalid CNC_SIM_BUILD_JOBS" >&2
exit 1
fi
if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then
echo "$script did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2
sed -n '1,20p' "$invalid_build_jobs_log" >&2
exit 1
fi
done
if CNC_SIM_BUILD_JOBS=0 ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_jobs_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted invalid CNC_SIM_BUILD_JOBS" >&2
exit 1
fi
if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2
sed -n '1,20p' "$invalid_build_jobs_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR=/ ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR= ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$empty_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted empty build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report empty build dir clearly" >&2
sed -n '1,20p' "$empty_build_dir_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR="build/wasm safe probe" ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$whitespace_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted whitespace in build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: build/wasm safe probe" "$whitespace_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report whitespace build dir clearly" >&2
sed -n '1,20p' "$whitespace_build_dir_log" >&2
exit 1
fi
if CNC_SIM_NATIVE_BUILD_DIR=/ ./test-native.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-native.sh accepted filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-native.sh did not report filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_NATIVE_BUILD_DIR=/tmp/.. ./test-native.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-native.sh accepted canonical filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-native.sh did not report canonical filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_NATIVE_BUILD_DIR= ./test-native.sh >"$empty_build_dir_log" 2>&1; then
echo "test-native.sh accepted empty build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then
echo "test-native.sh did not report empty build dir clearly" >&2
sed -n '1,20p' "$empty_build_dir_log" >&2
exit 1
fi
if CNC_SIM_NATIVE_BUILD_DIR="build/native test" ./test-native.sh >"$whitespace_build_dir_log" 2>&1; then
echo "test-native.sh accepted whitespace in build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: build/native test" "$whitespace_build_dir_log" >/dev/null; then
echo "test-native.sh did not report whitespace build dir clearly" >&2
sed -n '1,20p' "$whitespace_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR=/ ./test-linuxcnc-source-syntax.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-syntax.sh accepted filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR=/tmp/.. ./test-linuxcnc-source-syntax.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-syntax.sh accepted canonical filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report canonical filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR= ./test-linuxcnc-source-syntax.sh >"$empty_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-syntax.sh accepted empty build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report empty build dir clearly" >&2
sed -n '1,20p' "$empty_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_SYNTAX_BUILD_DIR="build/source syntax test" ./test-linuxcnc-source-syntax.sh >"$whitespace_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-syntax.sh accepted whitespace in build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: build/source syntax test" "$whitespace_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report whitespace build dir clearly" >&2
sed -n '1,20p' "$whitespace_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_LINK_BUILD_DIR=/ ./test-linuxcnc-source-link.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-link.sh accepted filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-link.sh did not report filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_LINK_BUILD_DIR=/tmp/.. ./test-linuxcnc-source-link.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-link.sh accepted canonical filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-link.sh did not report canonical filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_LINK_BUILD_DIR= ./test-linuxcnc-source-link.sh >"$empty_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-link.sh accepted empty build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-link.sh did not report empty build dir clearly" >&2
sed -n '1,20p' "$empty_build_dir_log" >&2
exit 1
fi
if CNC_SIM_SOURCE_LINK_BUILD_DIR="build/source link test" ./test-linuxcnc-source-link.sh >"$whitespace_build_dir_log" 2>&1; then
echo "test-linuxcnc-source-link.sh accepted whitespace in build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: build/source link test" "$whitespace_build_dir_log" >/dev/null; then
echo "test-linuxcnc-source-link.sh did not report whitespace build dir clearly" >&2
sed -n '1,20p' "$whitespace_build_dir_log" >&2
exit 1
fi
forbidden_common_flags_process_substitution='readarray -t common_flags < <('
forbidden_link_flags_process_substitution='readarray -t link_flags < <('
forbidden_source_common_flags_helper='./list-linuxcnc-source-common-cxxflags.sh'
@@ -155,8 +448,8 @@ grep -F 'CNC_SIM_ENABLE_SMOKE_BACKEND cannot be combined with CNC_SIM_ENABLE_LIN
grep -F 'smoke backend is not compiled into this build' core/src/gcode_backend.cpp >/dev/null
grep -F './list-linuxcnc-bridge-smoke-project-sources.sh > "$project_source_list"' test-linuxcnc-bridge-native.sh >/dev/null
grep -F 'LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-support-objects.sh > "$support_object_list"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'trap '\''rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"'\'' EXIT' test-linuxcnc-source-syntax.sh >/dev/null
if grep -F 'rm -f "$missing_source_manifest" "$missing_source_log"' test-linuxcnc-source-syntax.sh >/dev/null; then
grep -Fx 'trap cleanup_source_syntax_temps EXIT' test-linuxcnc-source-syntax.sh >/dev/null
if grep -Fx "trap 'rm -f \"\$source_list\" \"\$missing_source_manifest\" \"\$missing_source_log\"' EXIT" test-linuxcnc-source-syntax.sh >/dev/null; then
echo "linuxcnc source syntax probe still relies on manual cleanup for missing-source temporaries" >&2
exit 1
fi
@@ -421,11 +714,11 @@ fi
printf 'core:src/emc/rs274ngc/does_not_exist.cc:test missing source\n' >"$missing_source_manifest"
if ./test-linuxcnc-source-syntax.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then
echo "test-linuxcnc-source-syntax.sh accepted missing manifest file" >&2
echo "test-linuxcnc-source-syntax.sh accepted missing manifest source" >&2
exit 1
fi
if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report missing manifest file clearly" >&2
if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "test-linuxcnc-source-syntax.sh did not report missing manifest source clearly" >&2
sed -n '1,20p' "$missing_source_log" >&2
exit 1
fi

View File

@@ -5,10 +5,29 @@ cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
cxx=${CXX:-g++}
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_rs274_source_link.XXXXXX")
build_jobs=${CNC_SIM_BUILD_JOBS:-8}
if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then
echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2
exit 1
fi
build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test}
if [[ -z "$build_dir" ]]; then
echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty" >&2
exit 1
fi
if [[ "$build_dir" =~ [[:space:]] ]]; then
echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir" >&2
exit 1
fi
manifest=${1:-linuxcnc-rs274-source-files.txt}
trap 'rm -rf "$build_dir"' EXIT
mkdir -p "$build_dir"
build_dir=$(cd "$build_dir" && pwd)
if [[ "$build_dir" == "/" ]]; then
echo "CNC_SIM_SOURCE_LINK_BUILD_DIR must not be the filesystem root" >&2
exit 1
fi
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest"
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-switchkins-remap-table.sh linuxcnc-kinematics-source-files.txt
# The LinuxCNC source-linked dump path is not concurrency-safe under parallel execution.
@@ -33,7 +52,6 @@ objects=()
linuxcnc_index=0
for source in "${linuxcnc_sources[@]}"; do
obj="$build_dir/linuxcnc_${linuxcnc_index}.o"
"$cxx" "${common_flags[@]}" -c "$source" -o "$obj"
objects+=("$obj")
linuxcnc_index=$((linuxcnc_index + 1))
done
@@ -44,33 +62,94 @@ project_source_list="$build_dir/linuxcnc_rs274_dump_project_sources.txt"
mapfile -t project_sources < "$project_source_list"
for source in "${project_sources[@]}"; do
obj="$build_dir/project_${project_index}.o"
"$cxx" "${common_flags[@]}" -c "$source" -o "$obj"
objects+=("$obj")
project_index=$((project_index + 1))
done
"$cxx" -std=c++17 -I core/include -I core/src \
core/tests/linuxcnc_gees_table_smoke.cpp \
-o "$build_dir/linuxcnc_gees_table_smoke"
support_object_list="$build_dir/linuxcnc_support_objects.txt"
LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-support-objects.sh > "$support_object_list"
mapfile -t support_objects < "$support_object_list"
"$cxx" "${objects[@]}" "${support_objects[@]}" \
"${link_flags[@]}" \
-llinuxcnc-uspace-posix \
-llinuxcncini \
-llinuxcnchal \
-lpyplugin \
-ltooldata \
-lnml \
-lposemath \
-lboost_python313 \
-lpython3.13 \
-lfmt \
-ldl \
-o "$build_dir/linuxcnc_rs274_source_dump"
source_link_signature="$build_dir/source-link-build.signature"
source_link_next_signature="$build_dir/source-link-build.signature.next"
{
printf 'CXX=%s\n' "$cxx"
printf 'PWD=%s\n' "$PWD"
printf 'LINUXCNC_ROOT=%s\n' "$(cd "$linuxcnc_root" && pwd)"
printf 'COMMON_FLAGS='
printf ' %s' "${common_flags[@]}"
printf '\n'
printf 'LINK_FLAGS='
printf ' %s' "${link_flags[@]}"
printf '\n'
printf 'LINUXCNC_SOURCES:\n'
printf '%s\n' "${linuxcnc_sources[@]}"
printf 'PROJECT_SOURCES:\n'
printf '%s\n' "${project_sources[@]}"
printf 'SUPPORT_OBJECTS:\n'
printf '%s\n' "${support_objects[@]}"
} > "$source_link_next_signature"
if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then
rm -f "$build_dir"/linuxcnc_*.o \
"$build_dir"/*.d \
"$build_dir"/project_*.o \
"$build_dir"/linuxcnc_gees_table_smoke.o \
"$build_dir"/linuxcnc_rs274_source_dump \
"$build_dir"/linuxcnc_gees_table_smoke
fi
mv "$source_link_next_signature" "$source_link_signature"
source_link_makefile="$build_dir/source-link.mk"
{
printf 'CXX := %s\n' "$cxx"
printf 'COMMON_FLAGS :='
printf ' %s' "${common_flags[@]}"
printf '\n'
printf 'LINK_FLAGS :='
printf ' %s' "${link_flags[@]}"
printf '\n'
printf 'OBJECTS :='
printf ' %s' "${objects[@]}"
printf '\n'
printf 'SUPPORT_OBJECTS :='
printf ' %s' "${support_objects[@]}"
printf '\n\n'
printf 'DEP_FILES :=\n'
for object in "${objects[@]}"; do
printf 'DEP_FILES += %s.d\n' "$object"
done
printf 'DEP_FILES += %s.d\n\n' "$build_dir/linuxcnc_gees_table_smoke.o"
printf '.PHONY: all\n'
printf 'all: %s %s\n\n' \
"$build_dir/linuxcnc_rs274_source_dump" \
"$build_dir/linuxcnc_gees_table_smoke"
linuxcnc_index=0
for source in "${linuxcnc_sources[@]}"; do
printf '%s: %s\n' "$build_dir/linuxcnc_${linuxcnc_index}.o" "$source"
printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \
"$build_dir/linuxcnc_${linuxcnc_index}.o" "$source" "$build_dir/linuxcnc_${linuxcnc_index}.o"
linuxcnc_index=$((linuxcnc_index + 1))
done
project_index=0
for source in "${project_sources[@]}"; do
printf '%s: %s\n' "$build_dir/project_${project_index}.o" "$source"
printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \
"$build_dir/project_${project_index}.o" "$source" "$build_dir/project_${project_index}.o"
project_index=$((project_index + 1))
done
printf '%s: core/tests/linuxcnc_gees_table_smoke.cpp\n' "$build_dir/linuxcnc_gees_table_smoke.o"
printf '\t@$(CXX) -std=c++17 -I core/include -I core/src -MMD -MP -MF %s.d -c core/tests/linuxcnc_gees_table_smoke.cpp -o %s\n\n' \
"$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke.o"
printf '%s: %s\n' "$build_dir/linuxcnc_gees_table_smoke" "$build_dir/linuxcnc_gees_table_smoke.o"
printf '\t@$(CXX) -std=c++17 -I core/include -I core/src %s -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke"
printf '%s: $(OBJECTS) $(SUPPORT_OBJECTS)\n' "$build_dir/linuxcnc_rs274_source_dump"
printf '\t@$(CXX) $(OBJECTS) $(SUPPORT_OBJECTS) $(LINK_FLAGS) -llinuxcnc-uspace-posix -llinuxcncini -llinuxcnchal -lpyplugin -ltooldata -lnml -lposemath -lboost_python313 -lpython3.13 -lfmt -ldl -o %s\n' "$build_dir/linuxcnc_rs274_source_dump"
printf '\n-include $(DEP_FILES)\n'
} > "$source_link_makefile"
make --output-sync=target -j"$build_jobs" -f "$source_link_makefile"
var_file="$build_dir/rs274ngc-source.var"
base_var_file="$build_dir/rs274ngc-source-base.var"

View File

@@ -5,15 +5,40 @@ cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
cxx=${CXX:-g++}
build_jobs=${CNC_SIM_BUILD_JOBS:-8}
if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then
echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2
exit 1
fi
build_dir=${CNC_SIM_SOURCE_SYNTAX_BUILD_DIR-build/source-syntax-test}
if [[ -z "$build_dir" ]]; then
echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be empty" >&2
exit 1
fi
if [[ "$build_dir" =~ [[:space:]] ]]; then
echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not contain whitespace: $build_dir" >&2
exit 1
fi
mkdir -p "$build_dir"
build_dir=$(cd "$build_dir" && pwd)
if [[ "$build_dir" == "/" ]]; then
echo "CNC_SIM_SOURCE_SYNTAX_BUILD_DIR must not be the filesystem root" >&2
exit 1
fi
exec 9>"$build_dir/source-syntax.lock"
flock 9
manifest=${1:-linuxcnc-rs274-source-files.txt}
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest"
missing_source_manifest=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_rs274_manifest_source.XXXXXX.txt")
missing_source_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_rs274_manifest_source.XXXXXX.log")
source_list=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_linuxcnc_source_syntax_sources.XXXXXX.txt")
trap 'rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"' EXIT
grep -Fx 'trap '\''rm -f "$source_list" "$missing_source_manifest" "$missing_source_log"'\'' EXIT' test-linuxcnc-source-syntax.sh >/dev/null
source_list="$build_dir/linuxcnc_source_syntax_sources.txt"
cleanup_source_syntax_temps() {
rm -f "$missing_source_manifest" "$missing_source_log"
}
trap cleanup_source_syntax_temps EXIT
grep -Fx 'trap cleanup_source_syntax_temps EXIT' test-linuxcnc-source-syntax.sh >/dev/null
if awk '/^rm -f "\$missing_source_manifest" "\$missing_source_log"$/{ found = 1 } END { exit !found }' test-linuxcnc-source-syntax.sh >/dev/null; then
echo "linuxcnc source syntax probe still relies on manual cleanup for missing-source temporaries" >&2
exit 1
@@ -24,20 +49,20 @@ LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-manifest-sources.sh "$mani
printf 'core:src/emc/rs274ngc/does_not_exist.cc:test missing source\n' >"$missing_source_manifest"
if ./test-linuxcnc-source-objects.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then
echo "linuxcnc source object probe accepted missing manifest file" >&2
echo "linuxcnc source object probe accepted missing manifest source" >&2
exit 1
fi
if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "linuxcnc source object probe did not report missing manifest file clearly" >&2
if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "linuxcnc source object probe did not report missing manifest source clearly" >&2
sed -n '1,20p' "$missing_source_log" >&2
exit 1
fi
if ./test-linuxcnc-source-link.sh "$missing_source_manifest" >"$missing_source_log" 2>&1; then
echo "linuxcnc source link probe accepted missing manifest file" >&2
echo "linuxcnc source link probe accepted missing manifest source" >&2
exit 1
fi
if ! grep -F "missing manifest file: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "linuxcnc source link probe did not report missing manifest file clearly" >&2
if ! grep -F "missing manifest source: src/emc/rs274ngc/does_not_exist.cc" "$missing_source_log" >/dev/null; then
echo "linuxcnc source link probe did not report missing manifest source clearly" >&2
sed -n '1,20p' "$missing_source_log" >&2
exit 1
fi
@@ -45,9 +70,59 @@ fi
common_flag_output=$(LINUXCNC_ROOT="$linuxcnc_root" ./list-linuxcnc-source-common-cxxflags.sh --python --syntax-only)
mapfile -t common_flags <<<"$common_flag_output"
while IFS= read -r source; do
source_syntax_signature="$build_dir/source-syntax.signature"
source_syntax_next_signature="$build_dir/source-syntax.signature.next"
{
printf 'CXX=%s\n' "$cxx"
printf 'PWD=%s\n' "$PWD"
printf 'LINUXCNC_ROOT=%s\n' "$(cd "$linuxcnc_root" && pwd)"
printf 'MANIFEST=%s\n' "$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")"
printf 'BUILD_JOBS=%s\n' "$build_jobs"
printf 'COMMON_FLAGS='
printf ' %s' "${common_flags[@]}"
printf '\n'
printf 'SOURCES:\n'
cat "$source_list"
} > "$source_syntax_next_signature"
if [[ ! -f "$source_syntax_signature" ]] || ! cmp -s "$source_syntax_signature" "$source_syntax_next_signature"; then
rm -f "$build_dir"/source_*.stamp "$build_dir"/source_*.d
fi
mv "$source_syntax_next_signature" "$source_syntax_signature"
source_syntax_makefile="$build_dir/source-syntax.mk"
{
printf 'CXX := %s\n' "$cxx"
printf 'COMMON_FLAGS :='
printf ' %s' "${common_flags[@]}"
printf '\n\n'
printf 'STAMPS :=\n'
source_index=0
while IFS= read -r source; do
[[ -z "$source" ]] && continue
"$cxx" "${common_flags[@]}" "$source"
done < "$source_list"
printf 'STAMPS += %s/source_%s.stamp\n' "$build_dir" "$source_index"
source_index=$((source_index + 1))
done < "$source_list"
printf '\n.PHONY: all\n'
printf 'all: $(STAMPS)\n\n'
source_index=0
while IFS= read -r source; do
[[ -z "$source" ]] && continue
printf '%s/source_%s.stamp: %s\n' "$build_dir" "$source_index" "$source"
printf '\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s/source_%s.d %s\n' "$build_dir" "$source_index" "$source"
printf '\t@touch %s/source_%s.stamp\n\n' "$build_dir" "$source_index"
source_index=$((source_index + 1))
done < "$source_list"
printf '%s' '-include'
source_index=0
while IFS= read -r source; do
[[ -z "$source" ]] && continue
printf ' %s/source_%s.d' "$build_dir" "$source_index"
source_index=$((source_index + 1))
done < "$source_list"
printf '\n'
} > "$source_syntax_makefile"
make --output-sync=target -j"$build_jobs" -f "$source_syntax_makefile"
echo "linuxcnc rs274 source syntax probe passed"

View File

@@ -4,20 +4,47 @@ set -euo pipefail
cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_linuxcnc_wasm_cmake_safe_probe.XXXXXX")
build_jobs=${CNC_SIM_BUILD_JOBS:-8}
if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then
echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2
exit 1
fi
build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe}
if [[ -z "$build_dir" ]]; then
echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" >&2
exit 1
fi
if [[ "$build_dir" =~ [[:space:]] ]]; then
echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir" >&2
exit 1
fi
mkdir -p "$build_dir"
build_dir=$(cd "$build_dir" && pwd)
if [[ "$build_dir" == "/" ]]; then
echo "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" >&2
exit 1
fi
manifest=${1:-linuxcnc-rs274-wasm-source-files.txt}
missing_manifest=${TMPDIR:-/tmp}/cnc_sim_missing_build_wasm_manifest.txt
missing_manifest_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_missing_build_wasm_manifest.XXXXXX.log")
missing_root=/tmp/does-not-exist-linuxcnc
missing_root_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_missing_root.XXXXXX.log")
invalid_build_jobs_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_invalid_jobs.XXXXXX.log")
invalid_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_invalid_dir.XXXXXX.log")
empty_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_empty_dir.XXXXXX.log")
whitespace_build_dir_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_build_wasm_whitespace_dir.XXXXXX.log")
cleanup_wasm_cmake_probe_temps() {
local temp_path
for temp_path in \
"$build_dir" \
"$missing_manifest" \
"$missing_manifest_log" \
"$missing_root_log" \
"$invalid_build_jobs_log" \
"$invalid_build_dir_log" \
"$empty_build_dir_log" \
"$whitespace_build_dir_log" \
"${build_next_signature:-}" \
"${unknown_manifest_filter_log:-}" \
"${unknown_manifest_output_log:-}" \
"${missing_shim_log:-}" \
@@ -62,6 +89,22 @@ cleanup_wasm_cmake_probe_temps() {
trap cleanup_wasm_cmake_probe_temps EXIT
LINUXCNC_ROOT="$linuxcnc_root" ./check-linuxcnc-inputs.sh "$manifest"
manifest=$(cd "$(dirname "$manifest")" && pwd)/$(basename "$manifest")
linuxcnc_root=$(cd "$linuxcnc_root" && pwd)
build_signature="$build_dir/wasm-cmake-safe-probe.signature"
build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX")
{
printf 'CXX=%s\n' "${CXX:-g++}"
printf 'PWD=%s\n' "$PWD"
printf 'LINUXCNC_ROOT=%s\n' "$linuxcnc_root"
printf 'MANIFEST=%s\n' "$manifest"
printf 'BUILD_JOBS=%s\n' "$build_jobs"
} > "$build_next_signature"
if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then
rm -rf "$build_dir"
mkdir -p "$build_dir"
fi
mv "$build_next_signature" "$build_signature"
rm -f "$missing_manifest"
if CNC_SIM_BUILD_WASM_SKIP_LOCK=1 ./build-wasm.sh "$missing_manifest" >"$missing_manifest_log" 2>&1; then
@@ -83,6 +126,43 @@ if ! grep -F "missing LinuxCNC root: $missing_root" "$missing_root_log" >/dev/nu
sed -n '1,20p' "$missing_root_log" >&2
exit 1
fi
if CNC_SIM_BUILD_WASM_SKIP_LOCK=1 CNC_SIM_BUILD_JOBS=0 ./build-wasm.sh >"$invalid_build_jobs_log" 2>&1; then
echo "build-wasm accepted invalid CNC_SIM_BUILD_JOBS" >&2
exit 1
fi
if ! grep -F "CNC_SIM_BUILD_JOBS must be a positive integer: 0" "$invalid_build_jobs_log" >/dev/null; then
echo "build-wasm did not report invalid CNC_SIM_BUILD_JOBS clearly" >&2
sed -n '1,20p' "$invalid_build_jobs_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR=/ ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$invalid_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted filesystem root as build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root" "$invalid_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report filesystem-root build dir clearly" >&2
sed -n '1,20p' "$invalid_build_dir_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR= ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$empty_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted empty build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty" "$empty_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report empty build dir clearly" >&2
sed -n '1,20p' "$empty_build_dir_log" >&2
exit 1
fi
if CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR="build/wasm safe probe" ./test-linuxcnc-wasm-cmake-safe-probe.sh >"$whitespace_build_dir_log" 2>&1; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh accepted whitespace in build dir" >&2
exit 1
fi
if ! grep -F "CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: build/wasm safe probe" "$whitespace_build_dir_log" >/dev/null; then
echo "test-linuxcnc-wasm-cmake-safe-probe.sh did not report whitespace build dir clearly" >&2
sed -n '1,20p' "$whitespace_build_dir_log" >&2
exit 1
fi
grep -F 'exec 9>"${TMPDIR:-/tmp}/cnc_sim_build_wasm.lock"' build-wasm.sh >/dev/null
grep -F "flock 9" build-wasm.sh >/dev/null
grep -F -- './test-linuxcnc-wasm-blockers.sh "$manifest"' build-wasm.sh >/dev/null
@@ -207,9 +287,50 @@ grep -F -- 'for required_tool in cmake emcmake emcc node; do' build-wasm.sh >/de
grep -F -- 'missing_tools+=("$required_tool")' build-wasm.sh >/dev/null
grep -F "WASM build tools are required. Missing:" build-wasm.sh >/dev/null
grep -F "Install/activate cmake, emsdk, and Node.js so cmake, emcmake, emcc, and node are in PATH." build-wasm.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' build-wasm.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' build-wasm.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR-build/wasm-cmake-safe-probe}' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be empty' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'CNC_SIM_WASM_CMAKE_SAFE_PROBE_BUILD_DIR must not be the filesystem root' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_signature="$build_dir/wasm-cmake-safe-probe.signature"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_next_signature=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_wasm_cmake_safe_probe.signature.XXXXXX")' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'if [[ ! -f "$build_signature" ]] || ! cmp -s "$build_signature" "$build_next_signature"; then' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$linuxcnc_root"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''MANIFEST=%s\n'\'' "$manifest"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'printf '\''BUILD_JOBS=%s\n'\'' "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-native.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-native.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test}' test-native.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not be empty' test-native.sh >/dev/null
grep -F 'CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir' test-native.sh >/dev/null
grep -F 'exec 9>"$build_dir/native-test.lock"' test-native.sh >/dev/null
grep -F 'native_signature="$build_dir/native-build.signature"' test-native.sh >/dev/null
grep -F 'if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then' test-native.sh >/dev/null
grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-native.sh >/dev/null
grep -F 'printf '\''\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-native.sh >/dev/null
grep -F 'printf '\''\n-include $(DEP_FILES)\n'\''' test-native.sh >/dev/null
grep -F 'make --output-sync=target -j"$build_jobs" -f "$native_makefile"' test-native.sh >/dev/null
grep -F 'build_jobs=${CNC_SIM_BUILD_JOBS:-8}' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs' test-linuxcnc-source-link.sh >/dev/null
grep -F 'build_dir=${CNC_SIM_SOURCE_LINK_BUILD_DIR-build/source-link-test}' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not be empty' test-linuxcnc-source-link.sh >/dev/null
grep -F 'CNC_SIM_SOURCE_LINK_BUILD_DIR must not contain whitespace: $build_dir' test-linuxcnc-source-link.sh >/dev/null
grep -F 'source_link_signature="$build_dir/source-link-build.signature"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'if [[ ! -f "$source_link_signature" ]] || ! cmp -s "$source_link_signature" "$source_link_next_signature"; then' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''PWD=%s\n'\'' "$PWD"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''LINUXCNC_ROOT=%s\n'\'' "$(cd "$linuxcnc_root" && pwd)"' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''\t@$(CXX) $(COMMON_FLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n'\''' test-linuxcnc-source-link.sh >/dev/null
grep -F 'printf '\''\n-include $(DEP_FILES)\n'\''' test-linuxcnc-source-link.sh >/dev/null
grep -F 'make --output-sync=target -j"$build_jobs" -f "$source_link_makefile"' test-linuxcnc-source-link.sh >/dev/null
grep -F -- 'emcmake cmake -S core -B build/wasm \' build-wasm.sh >/dev/null
grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe' build-wasm.sh >/dev/null
grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm' build-wasm.sh >/dev/null
grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' build-wasm.sh >/dev/null
grep -F -- 'cmake --build build/wasm --target cnc_sim_wasm --parallel "$build_jobs"' build-wasm.sh >/dev/null
grep -F -- 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F -- 'cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F -- 'cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"' test-linuxcnc-wasm-cmake-safe-probe.sh >/dev/null
grep -F -- 'mkdir -p web/public' build-wasm.sh >/dev/null
grep -F -- 'cp build/wasm/cnc_sim.js web/public/cnc_sim.js' build-wasm.sh >/dev/null
grep -F -- 'cp build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm' build-wasm.sh >/dev/null
@@ -217,6 +338,41 @@ grep -F -- 'cmp -s build/wasm/cnc_sim.js web/public/cnc_sim.js' build-wasm.sh >/
grep -F -- 'cmp -s build/wasm/cnc_sim.wasm web/public/cnc_sim.wasm' build-wasm.sh >/dev/null
grep -F -- 'node test-web-wasm-node-smoke.cjs' build-wasm.sh >/dev/null
grep -F -- './test-web-wasm-browser-smoke.sh' build-wasm.sh >/dev/null
grep -F 'const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false)' web/src/wasm-core.js >/dev/null
grep -F 'OPFS cannot be disabled in browser contexts with OPFS support' web/src/wasm-core.js >/dev/null
grep -F 'async function opfsEntryExists(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null
grep -F 'async function statOpfsEntry(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null
grep -F 'async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "")' web/src/wasm-core.js >/dev/null
grep -F 'function readWasmDirectoryTree(module, rootPath, relativeRoot = "")' web/src/wasm-core.js >/dev/null
grep -F 'function writeWasmFileReplacingPath(module, path, data)' web/src/wasm-core.js >/dev/null
grep -F 'copyFile(fromPath: string, toPath: string): Promise<Uint8Array>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'moveFile(fromPath: string, toPath: string): Promise<void>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'exists(path: string): Promise<boolean>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'stat(path: string): Promise<WasmOpfsEntryStat>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the' web/src/wasm-core.js >/dev/null
grep -F 'before replacing the main file and managing filename + ".bak".' web/src/wasm-core.js >/dev/null
grep -F 'await removeOpfsEntry(workspacePath, `${path}.new`, false);' web/src/wasm-core.js >/dev/null
grep -F 'OPFS workspace is required for browser program parsing' web/src/app.js >/dev/null
grep -F 'const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);' web/src/app.js >/dev/null
grep -F 'default browser simulator did not create an OPFS workspace' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null
grep -F 'browser simulator allowed OPFS to be disabled' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null
grep -F 'app frame did not restore current program from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not report OPFS program restore after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not persist LinuxCNC parameter state into OPFS' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not reload LinuxCNC parameter state from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F '"parameters/missing-main.var.new", "stale temporary without main file' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'OPFS copyFile did not create the backup file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS copyFile did not update the WASM mirror destination' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS moveFile did not replace the destination file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS moveFile left the WASM mirror source behind' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS metadata queries polluted the WASM filesystem mirror' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS persistDirectory did not preserve an empty directory' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS readDirectory did not mirror an empty directory into WASM FS' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS readFile did not replace the WASM mirror directory with a file' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'stale LinuxCNC missing-parameter temporary file remained in OPFS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'stale LinuxCNC missing-parameter temporary file remained in WASM FS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'LinuxCNC out-of-order parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'LinuxCNC out-of-range parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
if [[ "$(grep -Fc 'cp build/wasm/cnc_sim.js' build-wasm.sh)" -ne 1 ]]; then
echo "build-wasm should copy cnc_sim.js exactly once" >&2
exit 1
@@ -251,8 +407,8 @@ if ! awk '
exit 1
fi
if ! awk '
index($0, "cmake --build build/wasm --target cnc_sim_wasm_runtime_probe") { runtime_probe_line = NR }
index($0, "cmake --build build/wasm --target cnc_sim_wasm") &&
index($0, "cmake --build build/wasm --target cnc_sim_wasm_runtime_probe --parallel \"$build_jobs\"") { runtime_probe_line = NR }
index($0, "cmake --build build/wasm --target cnc_sim_wasm --parallel \"$build_jobs\"") &&
!index($0, "cnc_sim_wasm_runtime_probe") { wasm_line = NR }
END { exit !(runtime_probe_line && wasm_line && runtime_probe_line < wasm_line) }
' build-wasm.sh; then
@@ -838,10 +994,10 @@ cmake -S core -B "$build_dir" \
-DCNC_SIM_LINUXCNC_WASM_SOURCE_MANIFEST="$manifest" \
-DCNC_SIM_ENABLE_LINUXCNC_WASM_SAFE_PROBE=ON
cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects
cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe
cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe_objects --parallel "$build_jobs"
cmake --build "$build_dir" --target linuxcnc_rs274_wasm_safe_probe --parallel "$build_jobs"
"$build_dir/linuxcnc_rs274_wasm_safe_probe"
cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe
cmake --build "$build_dir" --target cnc_sim_wasm_runtime_probe --parallel "$build_jobs"
"$build_dir/cnc_sim_wasm_runtime_probe"
echo "linuxcnc wasm-safe CMake probe target passed"

View File

@@ -100,7 +100,7 @@ nm --defined-only "$build_dir"/*.o \
| awk '
$0 ~ /^(_GLOBAL_OFFSET_TABLE_|_Unwind_Resume)$/ { next }
$0 ~ /^(__assert_fail|__cxa_|__divdc3|__dso_handle|__errno_location|__gxx_personality_v0|__isoc23_|__muldc3|__stack_chk_fail)/ { next }
$0 ~ /^(abort|access|acos|asin|atan2|atof|atoi|basename|cabs|calloc|ceil|close|closedir|copysign|cos|cosl|exit|exp|fclose|fdatasync|fdopen|feof|fflush|fgetc|fgets|fileno|floor|fmax|fmod|fopen|fprintf|fputs|free|freelocale|fseek|fstat|ftell|fwrite|getcwd|getenv|getpid|gettimeofday|hypot|islower|isprint|isspace|isupper|isxdigit|link|localtime|log|lstat|malloc|memchr|memcmp|memcpy|memmove|memset|mkstemp|nearbyint|newlocale|open|opendir|perror|pow|printf|pthread_mutex_lock|pthread_mutex_unlock|putc|puts|read|readdir|realpath|realloc|remove|rename|setlocale|sin|sinl|snprintf|sprintf|sqrt|sscanf|stat|stderr|stdout|strcasecmp|strcat|strchr|strcmp|strcpy|strdup|strerror|strlen|strncasecmp|strncat|strncmp|strncpy|strrchr|strspn|strstr|strtod|strtok_r|strtol|strtoul|tan|tanl|tolower|toupper|towlower|unlink|uselocale|vfprintf|vsnprintf|wordexp|wordfree)$/ { next }
$0 ~ /^(abort|access|acos|asin|atan2|atof|atoi|basename|cabs|calloc|ceil|close|closedir|copysign|cos|cosl|exit|exp|fclose|fdatasync|fdopen|feof|fflush|fgetc|fgets|fileno|floor|fmax|fmod|fopen|fprintf|fputs|free|freelocale|fseek|fstat|ftell|fwrite|getcwd|getenv|getpid|gettimeofday|hypot|isalnum|isalpha|islower|isprint|isspace|isupper|isxdigit|link|localtime|log|lstat|malloc|memchr|memcmp|memcpy|memmove|memset|mkstemp|nearbyint|newlocale|open|opendir|perror|pow|printf|pthread_mutex_lock|pthread_mutex_unlock|putc|puts|read|readdir|realpath|realloc|remove|rename|setlocale|sin|sinl|snprintf|sprintf|sqrt|sscanf|stat|stderr|stdout|strcasecmp|strcat|strchr|strcmp|strcpy|strdup|strerror|strlen|strncasecmp|strncat|strncmp|strncpy|strrchr|strspn|strstr|strtod|strtok_r|strtol|strtoul|tan|tanl|tolower|toupper|towlower|unlink|uselocale|vfprintf|vsnprintf|wordexp|wordfree)$/ { next }
$0 ~ /^(_ZN3fmt|_ZSt|_ZNSt|_ZNKSt|_ZNKRSt|_ZNS|_ZTI|_ZTS|_ZTV|_Zdl|_Znwm|_Znam|_Zda|_ZdaPv|_ZdaPvm|_ZdlPvm|_ZTv|_ZTh)/ { next }
{ print }
'

View File

@@ -4,24 +4,86 @@ set -euo pipefail
cd "$(dirname "$0")"
cxx=${CXX:-g++}
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_native.XXXXXX")
trap 'rm -rf "$build_dir"' EXIT
build_jobs=${CNC_SIM_BUILD_JOBS:-8}
if ! [[ "$build_jobs" =~ ^[1-9][0-9]*$ ]]; then
echo "CNC_SIM_BUILD_JOBS must be a positive integer: $build_jobs" >&2
exit 1
fi
build_dir=${CNC_SIM_NATIVE_BUILD_DIR-build/native-test}
if [[ -z "$build_dir" ]]; then
echo "CNC_SIM_NATIVE_BUILD_DIR must not be empty" >&2
exit 1
fi
if [[ "$build_dir" =~ [[:space:]] ]]; then
echo "CNC_SIM_NATIVE_BUILD_DIR must not contain whitespace: $build_dir" >&2
exit 1
fi
mkdir -p "$build_dir"
build_dir=$(cd "$build_dir" && pwd)
if [[ "$build_dir" == "/" ]]; then
echo "CNC_SIM_NATIVE_BUILD_DIR must not be the filesystem root" >&2
exit 1
fi
exec 9>"$build_dir/native-test.lock"
flock 9
./check-linuxcnc-inputs.sh linuxcnc-rs274-source-files.txt --require-rs274-complete
./check-linuxcnc-inputs.sh linuxcnc-rs274-wasm-source-files.txt --require-rs274-complete
./check-linuxcnc-inputs.sh linuxcnc-kinematics-source-files.txt --require-kinematics-complete
./check-linuxcnc-switchkins-remap-table.sh linuxcnc-kinematics-source-files.txt
grep -F '#include "linuxcnc_switchkins_remap_table.inc"' core/src/cnc_sim_api.cpp >/dev/null
grep -F 'switchkins: options.switchkins' web/src/wasm-core.js >/dev/null
grep -F 'remap: options.remap' web/src/wasm-core.js >/dev/null
grep -F 'iniFile: options.iniFile' web/src/wasm-core.js >/dev/null
grep -F 'iniFileName: options.iniFileName' web/src/wasm-core.js >/dev/null
grep -F 'INI_FILE_NAME: options.INI_FILE_NAME' web/src/wasm-core.js >/dev/null
grep -F 'halFile: options.halFile' web/src/wasm-core.js >/dev/null
grep -F 'postguiHalFile: options.postguiHalFile' web/src/wasm-core.js >/dev/null
grep -F 'POSTGUI_HALFILE: options.POSTGUI_HALFILE' web/src/wasm-core.js >/dev/null
grep -F 'kinematics: options.kinematics' web/src/wasm-core.js >/dev/null
grep -F 'pivotLength: options.pivotLength' web/src/wasm-core.js >/dev/null
grep -F 'xyzbcTrt: options.xyzbcTrt' web/src/wasm-core.js >/dev/null
grep -F 'globalThis.createCncSimModule' web/src/wasm-core.js >/dev/null
grep -F 'createModule(moduleOptions)' web/src/wasm-core.js >/dev/null
grep -F 'async parseFile(path, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null
grep -F 'async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null
grep -F 'async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {})' web/src/wasm-core.js >/dev/null
grep -F 'LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var"' web/src/wasm-core.js >/dev/null
grep -F 'export type CncParseOptions' web/src/index.ts >/dev/null
grep -F 'switchkins?: string;' web/src/index.ts >/dev/null
grep -F 'remap?: string;' web/src/index.ts >/dev/null
grep -F 'iniFile?: string;' web/src/index.ts >/dev/null
grep -F 'iniFileName?: string;' web/src/index.ts >/dev/null
grep -F 'INI_FILE_NAME?: string;' web/src/index.ts >/dev/null
grep -F 'halFile?: string;' web/src/index.ts >/dev/null
grep -F 'postguiHalFile?: string;' web/src/index.ts >/dev/null
grep -F 'POSTGUI_HALFILE?: string;' web/src/index.ts >/dev/null
grep -F '): CncEvent[];' web/src/index.ts >/dev/null
grep -F 'options?: CncParseOptions' web/src/wasm-core.d.ts >/dev/null
grep -F 'parseFile(path: string, dialect?: CncDialect, options?: CncParseOptions): Promise<CncEvent[]>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'parseWithParameterFile(' web/src/wasm-core.d.ts >/dev/null
grep -F 'parseFileWithParameterFile(' web/src/wasm-core.d.ts >/dev/null
grep -F 'copyFile(fromPath: string, toPath: string): Promise<Uint8Array>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'moveFile(fromPath: string, toPath: string): Promise<void>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'exists(path: string): Promise<boolean>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'stat(path: string): Promise<WasmOpfsEntryStat>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'loadParameterFile(path: string): Promise<Uint8Array | null>;' web/src/wasm-core.d.ts >/dev/null
grep -F 'async function opfsEntryExists(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null
grep -F 'async function statOpfsEntry(workspacePath, relativePath)' web/src/wasm-core.js >/dev/null
grep -F 'async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "")' web/src/wasm-core.js >/dev/null
grep -F 'function readWasmDirectoryTree(module, rootPath, relativeRoot = "")' web/src/wasm-core.js >/dev/null
grep -F 'function writeWasmFileReplacingPath(module, path, data)' web/src/wasm-core.js >/dev/null
grep -F '`${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`' web/src/wasm-core.js >/dev/null
grep -F 'export type WasmModuleOptions' web/src/wasm-core.d.ts >/dev/null
grep -F 'printErr?: (text: string) => void;' web/src/wasm-core.d.ts >/dev/null
grep -F '"field":"switchkins"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null
grep -F '"field":"remap"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null
grep -F '"field":"postguiHalFile"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null
grep -F '"field":"POSTGUI_HALFILE"' web/public/linuxcnc_switchkins_remap_config_cases.json >/dev/null
grep -F '{"switchkins", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null
grep -F '{"remap", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null
grep -F '{"postguiHalFile", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null
grep -F '{"POSTGUI_HALFILE", ' core/tests/linuxcnc_switchkins_remap_config_cases.inc >/dev/null
grep -F '"moduleResolution": "Bundler"' web/tsconfig.json >/dev/null
grep -F '"include": ["src/index.ts", "src/**/*.d.ts"]' web/tsconfig.json >/dev/null
grep -F 'createWasmSimulator' test-web-wasm-node-smoke.cjs >/dev/null
@@ -29,10 +91,90 @@ grep -F 'locateFile: (file) => path.join(wasmDir, file)' test-web-wasm-node-smok
grep -F 'printErr: () => {}' test-web-wasm-node-smoke.cjs >/dev/null
grep -F 'expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM' test-web-wasm-node-smoke.cjs >/dev/null
grep -F 'node test-web-wasm-node-smoke.cjs' test-web-wasm-node-smoke.sh >/dev/null
grep -F 'WASM_NODE_SMOKE_MIN_STEPS:-50' test-web-wasm-node-smoke.sh >/dev/null
grep -F 'web wasm node smoke passed ([0-9][0-9]* steps)' test-web-wasm-node-smoke.sh >/dev/null
grep -F 'createWasmSimulator' web/test-browser-wasm-smoke.html >/dev/null
grep -F 'runBrowserWasmSmoke' web/test-browser-wasm-smoke.html >/dev/null
grep -F 'runBrowserOpfsWorkspaceSections' web/test-browser-wasm-smoke-sections.js >/dev/null
grep -F 'runBrowserOpfsPolicySections' web/test-browser-wasm-smoke-sections.js >/dev/null
grep -F 'runBrowserAppOpfsSections' web/test-browser-wasm-smoke-sections.js >/dev/null
grep -F 'runLinuxCncBrowserSections' web/test-browser-wasm-smoke-sections.js >/dev/null
grep -F 'runBrowserOpfsBasicSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null
grep -F 'runBrowserOpfsParameterSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null
grep -F 'runBrowserOpfsMirrorSections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null
grep -F 'runBrowserOpfsDirectorySections' web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null
grep -F 'await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc"' web/test-browser-wasm-smoke-opfs-basic-sections.js >/dev/null
grep -F 'await simulator.parseWithParameterFile(' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'await simulator.parseFileWithParameterFile(' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'OPFS copyFile did not create the backup file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS copyFile did not update the WASM mirror destination' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS moveFile did not replace the destination file' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS moveFile left the WASM mirror source behind' web/test-browser-wasm-smoke-opfs-mirror-sections.js >/dev/null
grep -F 'OPFS metadata queries polluted the WASM filesystem mirror' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS persistDirectory did not preserve an empty directory' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS readDirectory did not mirror an empty directory into WASM FS' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F 'OPFS readFile did not replace the WASM mirror directory with a file' web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null
grep -F '"parameters/rs274ngc.var"' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F '"parameters/rs274ngc.var.bak"' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false)' web/src/wasm-core.js >/dev/null
grep -F 'OPFS cannot be disabled in browser contexts with OPFS support' web/src/wasm-core.js >/dev/null
grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the' web/src/wasm-core.js >/dev/null
grep -F 'before replacing the main file and managing filename + ".bak".' web/src/wasm-core.js >/dev/null
grep -F 'await removeOpfsEntry(workspacePath, `${path}.new`, false);' web/src/wasm-core.js >/dev/null
grep -F 'OPFS workspace is required for browser program parsing' web/src/app.js >/dev/null
grep -F 'const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);' web/src/app.js >/dev/null
grep -F 'default browser simulator did not create an OPFS workspace' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null
grep -F 'browser simulator allowed OPFS to be disabled' web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null
grep -F 'app frame did not restore current program from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not report OPFS program restore after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not persist LinuxCNC parameter state into OPFS' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F 'app frame did not reload LinuxCNC parameter state from OPFS after reload' web/test-browser-wasm-smoke-app-sections.js >/dev/null
grep -F '"parameters/missing-main.var.new", "stale temporary without main file' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'stale LinuxCNC missing-parameter temporary file remained in OPFS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'stale LinuxCNC missing-parameter temporary file remained in WASM FS' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'LinuxCNC out-of-order parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'LinuxCNC out-of-range parameter file remained in WASM FS after restore failure' web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null
grep -F 'browser wasm smoke passed' web/test-browser-wasm-smoke-helpers.js >/dev/null
grep -F 'browser wasm smoke passed' web/test-browser-wasm-smoke.html >/dev/null
grep -F 'chromium chromium-browser google-chrome' test-web-wasm-browser-smoke.sh >/dev/null
grep -F 'browser wasm smoke passed' test-web-wasm-browser-smoke.sh >/dev/null
grep -F 'BROWSER_SMOKE_MIN_SECTIONS:-50' test-web-wasm-browser-smoke.sh >/dev/null
grep -F 'browser wasm smoke passed ([0-9][0-9]* sections)' test-web-wasm-browser-smoke.sh >/dev/null
python3 - <<'PY'
from pathlib import Path
import re
import sys
node_steps = len(re.findall(r'await\s+runStep\(', Path("test-web-wasm-node-smoke.cjs").read_text(encoding="utf-8")))
browser_sections = sum(
len(re.findall(r'await\s+runSection\(', path.read_text(encoding="utf-8")))
for path in Path("web").glob("test-browser-wasm-smoke-*-sections.js")
)
total = node_steps + browser_sections
if total < 50:
print(
f"expected Node/browser smoke to keep at least 50 explicit closed-loop steps/sections, got {total}",
file=sys.stderr,
)
sys.exit(1)
html_lines = Path("web/test-browser-wasm-smoke.html").read_text(encoding="utf-8").count("\n") + 1
if html_lines > 80:
print(
f"expected browser smoke HTML to stay as a small loader, got {html_lines} lines",
file=sys.stderr,
)
sys.exit(1)
for path in sorted(Path("web").glob("test-browser-wasm-smoke-*-sections.js")):
line_count = path.read_text(encoding="utf-8").count("\n") + 1
if line_count > 300:
print(
f"expected browser smoke section module {path} to stay below 300 lines, got {line_count}",
file=sys.stderr,
)
sys.exit(1)
PY
kinematics_manifest_sources="$build_dir/linuxcnc_kinematics_manifest_sources.txt"
./list-linuxcnc-kinematics-manifest-sources.sh linuxcnc-kinematics-source-files.txt all > "$kinematics_manifest_sources"
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" "$kinematics_manifest_sources" >/dev/null
@@ -51,7 +193,21 @@ grep -Fx "configs/sim/axis/vismach/puma/puma560_uvw.ini" "$kinematics_manifest_s
grep -Fx "configs/sim/axis/vismach/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/qtaxis/non-trivial/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/qtvcp_screens/non-trivial/scara/scara.ini" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/hexapod-sim/kinematics.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa_dh.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/millturn/millturn.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/puma/puma_dh.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/puma/puma560_dh.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn_postgui.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa-postgui.hal" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/puma/puma560_postgui.hal" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '_postgui\.hal$|-postgui\.hal$' "$kinematics_manifest_sources")" -ne 11 ]]; then
echo "expected kinematics manifest to cover all LinuxCNC POSTGUI_HALFILE switchkins sources" >&2
exit 1
fi
grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/remap_subs/428remap.ngc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/remap_subs/428remap.ngc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc" "$kinematics_manifest_sources" >/dev/null
@@ -73,43 +229,216 @@ grep -Fx "src/hal/components/xyzacb_trsrn.comp" "$kinematics_manifest_sources" >
grep -Fx "src/objects/hal/components/xyzacb_trsrn.c" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/components/xyzbca_trsrn.comp" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/xyzbca_trsrn.c" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/components/millturn.comp" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/millturn.c" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/components/matrixkins.comp" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/matrixkins.c" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/components/userkins.comp" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/userkins.c" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/millturn.mak" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/xyzab_tdr_kins.mak" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/xyzacb_trsrn.mak" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/xyzbca_trsrn.mak" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/matrixkins.mak" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/objects/hal/components/userkins.mak" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '^src/objects/hal/components/.*\.mak$' "$kinematics_manifest_sources")" -ne 6 ]]; then
echo "expected kinematics manifest to cover all generated halcompile make metadata sources" >&2
exit 1
fi
grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/5axis.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/melfa-sim/melfa.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/millturn/millturn.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/puma/puma.tbl" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/qtaxis/non-trivial/scara/scara.tbl" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '\.tbl$' "$kinematics_manifest_sources")" -ne 10 ]]; then
echo "expected kinematics manifest to cover all resolvable LinuxCNC TOOL_TABLE switchkins sources" >&2
exit 1
fi
grep -Fx "configs/sim/axis/vismach/5axis/bridgemill/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-dual-rotary/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/5axis/table-rotary-tilting/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/hexapod-sim/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/melfa-sim/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/millturn/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/puma/README" "$kinematics_manifest_sources" >/dev/null
grep -Fx "configs/sim/axis/vismach/scara/README" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '/README$' "$kinematics_manifest_sources")" -ne 10 ]]; then
echo "expected kinematics manifest to cover all LinuxCNC README sources adjacent to switchkins INI files" >&2
exit 1
fi
grep -Fx "src/hal/user_comps/vismach/5axisgui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/hexagui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/melfagui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/millturngui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/puma560gui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/pumagui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/scaragui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/xyzab-tdr-gui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/xyzac-trt-gui.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/hal/user_comps/vismach/xyzbc-trt-gui.py" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '^src/hal/user_comps/vismach/.*gui\.py$|^src/hal/user_comps/vismach/xyz[abc-]+trt-gui\.py$|^src/hal/user_comps/vismach/xyzab-tdr-gui\.py$' "$kinematics_manifest_sources")" -ne 10 ]]; then
echo "expected kinematics manifest to cover all LinuxCNC vismach loadusr GUI sources" >&2
exit 1
fi
grep -Fx "share/qtvcp/panels/vismach_scara/vismach_scara.ui" "$kinematics_manifest_sources" >/dev/null
grep -Fx "share/qtvcp/panels/vismach_scara/vismach_scara_handler.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "lib/python/qtvcp/lib/qt_vismach/README.txt" "$kinematics_manifest_sources" >/dev/null
grep -Fx "lib/python/qtvcp/lib/qt_vismach/__init__.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "lib/python/qtvcp/lib/qt_vismach/primitives.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "lib/python/qtvcp/lib/qt_vismach/qt_vismach.py" "$kinematics_manifest_sources" >/dev/null
grep -Fx "lib/python/qtvcp/lib/qt_vismach/scara.py" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '^(share/qtvcp/panels/vismach_scara/|lib/python/qtvcp/lib/qt_vismach/)' "$kinematics_manifest_sources")" -ne 7 ]]; then
echo "expected kinematics manifest to cover all LinuxCNC QtVCP vismach_scara panel sources" >&2
exit 1
fi
grep -Fx "src/hal/user_comps/vismach/Submakefile" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/5axisgui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/hexagui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/melfagui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/puma560gui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/pumagui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/scaragui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/xyzab-tdr-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/xyzac-trt-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null
grep -Fx "docs/src/man/man1/xyzbc-trt-gui.1.adoc" "$kinematics_manifest_sources" >/dev/null
if [[ "$(grep -Ec '^docs/src/man/man1/(5axisgui|hexagui|melfagui|puma560gui|pumagui|scaragui|xyzab-tdr-gui|xyzac-trt-gui|xyzbc-trt-gui)\.1\.adoc$' "$kinematics_manifest_sources")" -ne 9 ]]; then
echo "expected kinematics manifest to cover all LinuxCNC vismach GUI manpage sources" >&2
exit 1
fi
grep -Fx "src/emc/kinematics/switchkins.h" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/emc/kinematics/Submakefile" "$kinematics_manifest_sources" >/dev/null
grep -Fx "src/emc/kinematics/blend.fig" "$kinematics_manifest_sources" >/dev/null
grep -F 'manifest does not group all LinuxCNC kinematics headers as header sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not group all LinuxCNC kinematics build metadata as metadata sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not group all LinuxCNC kinematics reference assets as asset sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all LinuxCNC component sources referenced by M428/M429/M430 INI KINEMATICS entries' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all LinuxCNC generated component sources referenced by M428/M429/M430 INI KINEMATICS entries' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all LinuxCNC generated component make metadata adjacent to generated kinematics sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all resolvable LinuxCNC TOOL_TABLE files referenced by M428/M429/M430 INI files as tooldata sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover LinuxCNC README sources adjacent to M428/M429/M430 INI files as asset sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all LinuxCNC vismach user component sources referenced by M428/M429/M430 INI/HAL loadusr commands as asset sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover LinuxCNC vismach user component build metadata as metadata sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover LinuxCNC vismach user component manpage sources as metadata sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest does not cover all LinuxCNC QtVCP vismach_scara sources referenced by M428/M429/M430 INI EMBED_TAB_COMMAND entries as asset sources' check-linuxcnc-inputs.sh >/dev/null
grep -F 'bad manifest line $manifest_line_number' check-linuxcnc-inputs.sh >/dev/null
grep -F 'manifest source must be LinuxCNC-root relative' check-linuxcnc-inputs.sh >/dev/null
grep -F 'missing manifest source: $path' check-linuxcnc-inputs.sh >/dev/null
core_source_list="$build_dir/cnc_sim_core_sources.txt"
./list-cnc-sim-core-sources.sh --with-smoke > "$core_source_list"
mapfile -t core_sources < "$core_source_list"
smoke_cxxflags=(-std=c++17 -fpermissive -DCNC_SIM_ENABLE_SMOKE_BACKEND -I core/include -I core/src -I ../linuxcnc/include)
"$cxx" "${smoke_cxxflags[@]}" \
"${core_sources[@]}" \
core/tests/canon_event_sink_smoke.cpp \
-o "$build_dir/canon_event_sink_smoke"
native_signature="$build_dir/native-build.signature"
native_next_signature="$build_dir/native-build.signature.next"
{
printf 'CXX=%s\n' "$cxx"
printf 'PWD=%s\n' "$PWD"
printf 'SMOKE_CXXFLAGS='
printf ' %s' "${smoke_cxxflags[@]}"
printf '\n'
printf 'CORE_SOURCES:\n'
printf '%s\n' "${core_sources[@]}"
} > "$native_next_signature"
if [[ ! -f "$native_signature" ]] || ! cmp -s "$native_signature" "$native_next_signature"; then
rm -f "$build_dir"/core_*.o \
"$build_dir"/*.d \
"$build_dir"/canon_event_sink_smoke.o \
"$build_dir"/rtcp_kinematics_smoke.o \
"$build_dir"/simulator_gcode_controls_smoke.o \
"$build_dir"/cnc_sim_api_smoke.o \
"$build_dir"/cnc_sim_dump.o \
"$build_dir"/linuxcnc_gees_table_smoke.o \
"$build_dir"/canon_event_sink_smoke \
"$build_dir"/rtcp_kinematics_smoke \
"$build_dir"/linuxcnc_gees_table_smoke \
"$build_dir"/simulator_gcode_controls_smoke \
"$build_dir"/cnc_sim_api_smoke \
"$build_dir"/cnc_sim_dump
fi
mv "$native_next_signature" "$native_signature"
"$cxx" "${smoke_cxxflags[@]}" \
"${core_sources[@]}" \
core/tests/rtcp_kinematics_smoke.cpp \
-o "$build_dir/rtcp_kinematics_smoke"
core_objects=()
dep_files=()
core_index=0
for source in "${core_sources[@]}"; do
obj="$build_dir/core_${core_index}.o"
core_objects+=("$obj")
dep_files+=("$obj.d")
core_index=$((core_index + 1))
done
for obj in \
"$build_dir/canon_event_sink_smoke.o" \
"$build_dir/rtcp_kinematics_smoke.o" \
"$build_dir/simulator_gcode_controls_smoke.o" \
"$build_dir/cnc_sim_api_smoke.o" \
"$build_dir/cnc_sim_dump.o" \
"$build_dir/linuxcnc_gees_table_smoke.o"
do
dep_files+=("$obj.d")
done
"$cxx" "${smoke_cxxflags[@]}" \
core/tests/linuxcnc_gees_table_smoke.cpp \
-o "$build_dir/linuxcnc_gees_table_smoke"
native_makefile="$build_dir/native.mk"
{
printf 'CXX := %s\n' "$cxx"
printf 'SMOKE_CXXFLAGS :='
printf ' %s' "${smoke_cxxflags[@]}"
printf '\n\n'
printf 'CORE_OBJECTS :='
printf ' %s' "${core_objects[@]}"
printf '\n\n'
printf 'DEP_FILES :='
printf ' %s' "${dep_files[@]}"
printf '\n\n'
printf '.PHONY: all\n'
printf 'all: %s %s %s %s %s %s\n\n' \
"$build_dir/canon_event_sink_smoke" \
"$build_dir/rtcp_kinematics_smoke" \
"$build_dir/linuxcnc_gees_table_smoke" \
"$build_dir/simulator_gcode_controls_smoke" \
"$build_dir/cnc_sim_api_smoke" \
"$build_dir/cnc_sim_dump"
"$cxx" "${smoke_cxxflags[@]}" \
"${core_sources[@]}" \
core/tests/simulator_gcode_controls_smoke.cpp \
-o "$build_dir/simulator_gcode_controls_smoke"
core_index=0
for source in "${core_sources[@]}"; do
printf '%s: %s\n' "${core_objects[$core_index]}" "$source"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c %s -o %s\n\n' \
"${core_objects[$core_index]}" "$source" "${core_objects[$core_index]}"
core_index=$((core_index + 1))
done
"$cxx" "${smoke_cxxflags[@]}" \
"${core_sources[@]}" \
core/tests/cnc_sim_api_smoke.cpp \
-o "$build_dir/cnc_sim_api_smoke"
printf '%s: core/tests/canon_event_sink_smoke.cpp\n' "$build_dir/canon_event_sink_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/canon_event_sink_smoke.cpp -o %s\n\n' "$build_dir/canon_event_sink_smoke.o" "$build_dir/canon_event_sink_smoke.o"
printf '%s: core/tests/rtcp_kinematics_smoke.cpp\n' "$build_dir/rtcp_kinematics_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/rtcp_kinematics_smoke.cpp -o %s\n\n' "$build_dir/rtcp_kinematics_smoke.o" "$build_dir/rtcp_kinematics_smoke.o"
printf '%s: core/tests/simulator_gcode_controls_smoke.cpp\n' "$build_dir/simulator_gcode_controls_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/simulator_gcode_controls_smoke.cpp -o %s\n\n' "$build_dir/simulator_gcode_controls_smoke.o" "$build_dir/simulator_gcode_controls_smoke.o"
printf '%s: core/tests/cnc_sim_api_smoke.cpp\n' "$build_dir/cnc_sim_api_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/cnc_sim_api_smoke.cpp -o %s\n\n' "$build_dir/cnc_sim_api_smoke.o" "$build_dir/cnc_sim_api_smoke.o"
printf '%s: core/tools/cnc_sim_dump.cpp\n' "$build_dir/cnc_sim_dump.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tools/cnc_sim_dump.cpp -o %s\n\n' "$build_dir/cnc_sim_dump.o" "$build_dir/cnc_sim_dump.o"
printf '%s: core/tests/linuxcnc_gees_table_smoke.cpp\n' "$build_dir/linuxcnc_gees_table_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) -MMD -MP -MF %s.d -c core/tests/linuxcnc_gees_table_smoke.cpp -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke.o"
"$cxx" "${smoke_cxxflags[@]}" \
"${core_sources[@]}" \
core/tools/cnc_sim_dump.cpp \
-o "$build_dir/cnc_sim_dump"
printf '%s: %s\n' "$build_dir/linuxcnc_gees_table_smoke" "$build_dir/linuxcnc_gees_table_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) %s -o %s\n\n' "$build_dir/linuxcnc_gees_table_smoke.o" "$build_dir/linuxcnc_gees_table_smoke"
printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/canon_event_sink_smoke" "$build_dir/canon_event_sink_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/canon_event_sink_smoke.o" "$build_dir/canon_event_sink_smoke"
printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/rtcp_kinematics_smoke" "$build_dir/rtcp_kinematics_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/rtcp_kinematics_smoke.o" "$build_dir/rtcp_kinematics_smoke"
printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/simulator_gcode_controls_smoke" "$build_dir/simulator_gcode_controls_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/simulator_gcode_controls_smoke.o" "$build_dir/simulator_gcode_controls_smoke"
printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/cnc_sim_api_smoke" "$build_dir/cnc_sim_api_smoke.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n\n' "$build_dir/cnc_sim_api_smoke.o" "$build_dir/cnc_sim_api_smoke"
printf '%s: $(CORE_OBJECTS) %s\n' "$build_dir/cnc_sim_dump" "$build_dir/cnc_sim_dump.o"
printf '\t@$(CXX) $(SMOKE_CXXFLAGS) $(CORE_OBJECTS) %s -o %s\n' "$build_dir/cnc_sim_dump.o" "$build_dir/cnc_sim_dump"
printf '\n-include $(DEP_FILES)\n'
} > "$native_makefile"
make --output-sync=target -j"$build_jobs" -f "$native_makefile"
"$build_dir/canon_event_sink_smoke"
"$build_dir/rtcp_kinematics_smoke"

View File

@@ -11,32 +11,197 @@ if [[ -z "$chromium_bin" ]]; then
break
fi
done
elif [[ ! -x "$chromium_bin" ]] && command -v "$chromium_bin" >/dev/null 2>&1; then
chromium_bin=$(command -v "$chromium_bin")
fi
missing=()
minimum_sections=${BROWSER_SMOKE_MIN_SECTIONS:-50}
if ! [[ "$minimum_sections" =~ ^[1-9][0-9]*$ ]]; then
echo "BROWSER_SMOKE_MIN_SECTIONS must be a positive integer: $minimum_sections" >&2
exit 1
fi
require_command() {
local command_name=$1
if ! command -v "$command_name" >/dev/null 2>&1; then
missing+=("$command_name")
fi
}
require_file() {
local file_path=$1
if [[ ! -f "$file_path" ]]; then
missing+=("$file_path")
elif [[ ! -r "$file_path" || ! -s "$file_path" ]]; then
missing+=("readable non-empty $file_path")
fi
}
report_missing_prerequisites() {
echo "WASM browser smoke prerequisites are missing: ${missing[*]}." >&2
echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2
}
if [[ -z "$chromium_bin" ]]; then
missing+=("chromium")
elif [[ ! -x "$chromium_bin" ]]; then
missing+=("executable chromium at $chromium_bin")
fi
for required in python3 web/public/cnc_sim.js web/public/cnc_sim.wasm web/test-browser-wasm-smoke.html; do
if [[ "$required" == python3 ]]; then
if ! command -v python3 >/dev/null 2>&1; then
missing+=("python3")
fi
elif [[ ! -f "$required" ]]; then
missing+=("$required")
required_files=(
web/public/cnc_sim.js
web/public/cnc_sim.wasm
web/public/linuxcnc_switchkins_remap_config_cases.json
web/src/wasm-core.js
web/src/app.js
web/index.html
web/styles.css
web/test-browser-wasm-smoke.html
web/test-browser-wasm-smoke-helpers.js
web/test-browser-wasm-smoke-sections.js
web/test-browser-wasm-smoke-linuxcnc-sections.js
web/test-browser-wasm-smoke-opfs-workspace-sections.js
web/test-browser-wasm-smoke-opfs-basic-sections.js
web/test-browser-wasm-smoke-opfs-parameter-sections.js
web/test-browser-wasm-smoke-opfs-mirror-sections.js
web/test-browser-wasm-smoke-opfs-directory-sections.js
web/test-browser-wasm-smoke-opfs-policy-sections.js
web/test-browser-wasm-smoke-app-sections.js
)
for required in \
node \
python3 \
"${required_files[@]}"
do
if [[ "$required" == node || "$required" == python3 ]]; then
require_command "$required"
else
require_file "$required"
fi
done
if ((${#missing[@]} > 0)); then
echo "WASM browser smoke prerequisites are missing: ${missing[*]}." >&2
echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2
report_missing_prerequisites
exit 1
fi
if ! python3 - <<'PY'
from pathlib import Path
import json
import sys
cases = json.loads(Path("web/public/linuxcnc_switchkins_remap_config_cases.json").read_text(encoding="utf-8"))
if not isinstance(cases, list) or not cases:
print("expected non-empty LinuxCNC switchkins config case JSON", file=sys.stderr)
sys.exit(1)
for index, config_case in enumerate(cases):
if (
not isinstance(config_case, dict)
or not isinstance(config_case.get("field"), str)
or not isinstance(config_case.get("value"), str)
or not isinstance(config_case.get("m428"), int)
or not isinstance(config_case.get("m429"), int)
or not isinstance(config_case.get("m430"), int)
):
print(f"invalid LinuxCNC switchkins config case at index {index}", file=sys.stderr)
sys.exit(1)
PY
then
echo "WASM browser smoke switchkins config case JSON check failed" >&2
exit 1
fi
if ! python3 - <<'PY'
from pathlib import Path
import re
import sys
html = Path("web/test-browser-wasm-smoke.html").read_text(encoding="utf-8")
if '<script src="/public/cnc_sim.js"></script>' not in html:
print("browser smoke HTML must load /public/cnc_sim.js", file=sys.stderr)
sys.exit(1)
module_scripts = re.findall(r'<script\s+type="module">(.*?)</script>', html, flags=re.S)
if len(module_scripts) != 1 or not module_scripts[0].strip():
print("browser smoke HTML must contain exactly one non-empty module script", file=sys.stderr)
sys.exit(1)
if 'from "/src/wasm-core.js"' not in module_scripts[0]:
print("browser smoke module script must import /src/wasm-core.js", file=sys.stderr)
sys.exit(1)
if 'from "/test-browser-wasm-smoke-sections.js"' not in module_scripts[0]:
print("browser smoke module script must import /test-browser-wasm-smoke-sections.js", file=sys.stderr)
sys.exit(1)
section_entry = Path("web/test-browser-wasm-smoke-sections.js").read_text(encoding="utf-8")
for imported_module in (
"/test-browser-wasm-smoke-helpers.js",
"/test-browser-wasm-smoke-linuxcnc-sections.js",
"/test-browser-wasm-smoke-opfs-workspace-sections.js",
"/test-browser-wasm-smoke-opfs-policy-sections.js",
"/test-browser-wasm-smoke-app-sections.js",
):
if f'from "{imported_module}"' not in section_entry:
print(f"browser smoke sections module must import {imported_module}", file=sys.stderr)
sys.exit(1)
opfs_workspace = Path("web/test-browser-wasm-smoke-opfs-workspace-sections.js").read_text(encoding="utf-8")
for imported_module in (
"/test-browser-wasm-smoke-opfs-basic-sections.js",
"/test-browser-wasm-smoke-opfs-parameter-sections.js",
"/test-browser-wasm-smoke-opfs-mirror-sections.js",
"/test-browser-wasm-smoke-opfs-directory-sections.js",
):
if f'from "{imported_module}"' not in opfs_workspace:
print(f"browser smoke OPFS workspace module must import {imported_module}", file=sys.stderr)
sys.exit(1)
app_html = Path("web/index.html").read_text(encoding="utf-8")
if '<link rel="stylesheet" href="/styles.css" />' not in app_html:
print("browser app HTML must load /styles.css", file=sys.stderr)
sys.exit(1)
if '<script type="module" src="/src/app.js"></script>' not in app_html:
print("browser app HTML must load /src/app.js", file=sys.stderr)
sys.exit(1)
for required_id in ("parseBtn", "programInput", "alarmList", "axisX", "wasmState", "storageState"):
if f'id="{required_id}"' not in app_html:
print(f"browser app HTML missing #{required_id}", file=sys.stderr)
sys.exit(1)
PY
then
exit 1
fi
for web_module in \
web/src/wasm-core.js \
web/src/app.js \
web/test-browser-wasm-smoke-helpers.js \
web/test-browser-wasm-smoke-sections.js \
web/test-browser-wasm-smoke-linuxcnc-sections.js \
web/test-browser-wasm-smoke-opfs-workspace-sections.js \
web/test-browser-wasm-smoke-opfs-basic-sections.js \
web/test-browser-wasm-smoke-opfs-parameter-sections.js \
web/test-browser-wasm-smoke-opfs-mirror-sections.js \
web/test-browser-wasm-smoke-opfs-directory-sections.js \
web/test-browser-wasm-smoke-opfs-policy-sections.js \
web/test-browser-wasm-smoke-app-sections.js
do
if ! node --check "$web_module"; then
echo "WASM browser smoke web module syntax check failed for $web_module" >&2
exit 1
fi
done
chromium_version=$("$chromium_bin" --version 2>/dev/null || true)
if [[ -n "$chromium_version" ]]; then
echo "browser smoke chromium version: $chromium_version"
fi
virtual_time_budget=${BROWSER_SMOKE_VIRTUAL_TIME_BUDGET:-20000}
server_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_server.XXXXXX.log")
dom_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_dom.XXXXXX.html")
profile_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_profile.XXXXXX")
cleanup() {
if [[ -n "${chromium_pid:-}" ]]; then
kill "$chromium_pid" >/dev/null 2>&1 || true
wait "$chromium_pid" >/dev/null 2>&1 || true
fi
if [[ -n "${server_pid:-}" ]]; then
kill "$server_pid" >/dev/null 2>&1 || true
wait "$server_pid" >/dev/null 2>&1 || true
@@ -46,15 +211,75 @@ cleanup() {
}
trap cleanup EXIT
port=$(python3 - <<'PY'
pick_free_port() {
python3 - <<'PY'
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
PY
)
}
print_browser_failure_context() {
if grep -F "browser wasm smoke failed:" "$dom_log" >/dev/null; then
grep -o "browser wasm smoke failed:[^<]*" "$dom_log" | sed -n '1,20p' >&2
elif grep -F "running:" "$dom_log" >/dev/null; then
grep -o "running:[^<]*" "$dom_log" | tail -n 1 >&2
elif [[ ! -s "$dom_log" ]]; then
echo "browser smoke did not produce DOM output" >&2
fi
echo "browser smoke HTTP server log:" >&2
sed -n '1,40p' "$server_log" >&2
echo "browser smoke DOM excerpt:" >&2
sed -n '1,120p' "$dom_log" >&2
}
probe_http_resource() {
local resource_path=$1
local expected_prefix=$2
local expected_type=${3:-}
if ! python3 - "$port" "$resource_path" "$expected_prefix" "$expected_type" <<'PY'
import http.client
import sys
port = int(sys.argv[1])
path = sys.argv[2]
expected_prefix = sys.argv[3].encode("utf-8")
expected_type = sys.argv[4]
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=2)
try:
conn.request("GET", path)
response = conn.getresponse()
body = response.read(max(len(expected_prefix), 1))
finally:
conn.close()
if response.status != 200:
print(f"{path} returned HTTP {response.status}", file=sys.stderr)
sys.exit(1)
content_type = response.getheader("Content-Type", "").split(";", 1)[0]
if expected_type and content_type != expected_type:
print(f"{path} returned Content-Type {content_type!r}, expected {expected_type!r}", file=sys.stderr)
sys.exit(1)
if not body:
print(f"{path} returned an empty body", file=sys.stderr)
sys.exit(1)
if expected_prefix and not body.startswith(expected_prefix):
print(f"{path} did not start with expected content", file=sys.stderr)
sys.exit(1)
PY
then
echo "browser smoke HTTP resource probe failed for $resource_path" >&2
sed -n '1,40p' "$server_log" >&2
exit 1
fi
}
port=$(pick_free_port)
echo "running WASM browser smoke with $chromium_bin on http://127.0.0.1:$port/test-browser-wasm-smoke.html"
python3 -m http.server "$port" --bind 127.0.0.1 --directory web >"$server_log" 2>&1 &
server_pid=$!
@@ -92,19 +317,211 @@ then
exit 1
fi
probe_http_resource "/test-browser-wasm-smoke.html" "<!doctype html>" "text/html"
probe_http_resource "/" "<!doctype html>" "text/html"
probe_http_resource "/styles.css" "" "text/css"
probe_http_resource "/public/cnc_sim.js" "" "text/javascript"
probe_http_resource "/public/cnc_sim.wasm" "" "application/wasm"
probe_http_resource "/public/linuxcnc_switchkins_remap_config_cases.json" "[" "application/json"
probe_http_resource "/src/app.js" "" "text/javascript"
probe_http_resource "/src/wasm-core.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-helpers.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-linuxcnc-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-workspace-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-basic-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-parameter-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-mirror-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-directory-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-opfs-policy-sections.js" "" "text/javascript"
probe_http_resource "/test-browser-wasm-smoke-app-sections.js" "" "text/javascript"
debug_port=$(pick_free_port)
"$chromium_bin" \
--headless=new \
--disable-gpu \
--no-sandbox \
--user-data-dir="$profile_dir" \
--virtual-time-budget=5000 \
--dump-dom \
"http://127.0.0.1:$port/test-browser-wasm-smoke.html" >"$dom_log" 2>&1
--remote-debugging-port="$debug_port" \
"http://127.0.0.1:$port/test-browser-wasm-smoke.html" >"$dom_log" 2>&1 &
chromium_pid=$!
if ! grep -F "browser wasm smoke passed" "$dom_log" >/dev/null; then
echo "browser WASM smoke did not pass" >&2
sed -n '1,120p' "$dom_log" >&2
if ! python3 - "$debug_port" "$dom_log" "$virtual_time_budget" <<'PY'
import base64
import http.client
import json
import os
import socket
import struct
import sys
import time
from urllib.parse import urlparse
debug_port = int(sys.argv[1])
dom_log = sys.argv[2]
timeout_ms = int(sys.argv[3])
deadline = time.monotonic() + (timeout_ms / 1000.0)
def http_json(path):
conn = http.client.HTTPConnection("127.0.0.1", debug_port, timeout=1)
try:
conn.request("GET", path)
response = conn.getresponse()
body = response.read()
finally:
conn.close()
if response.status != 200:
raise RuntimeError(f"{path} returned HTTP {response.status}")
return json.loads(body.decode("utf-8"))
def websocket_connect(url):
parsed = urlparse(url)
key = base64.b64encode(os.urandom(16)).decode("ascii")
sock = socket.create_connection((parsed.hostname, parsed.port), timeout=2)
request = (
f"GET {parsed.path} HTTP/1.1\r\n"
f"Host: {parsed.hostname}:{parsed.port}\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n"
)
sock.sendall(request.encode("ascii"))
response = sock.recv(4096)
if b" 101 " not in response.split(b"\r\n", 1)[0]:
raise RuntimeError("DevTools websocket handshake failed")
sock.settimeout(1)
return sock
def websocket_send(sock, payload):
data = payload.encode("utf-8")
header = bytearray([0x81])
if len(data) < 126:
header.append(0x80 | len(data))
elif len(data) < 65536:
header.append(0x80 | 126)
header.extend(struct.pack("!H", len(data)))
else:
header.append(0x80 | 127)
header.extend(struct.pack("!Q", len(data)))
mask = os.urandom(4)
header.extend(mask)
masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(data))
sock.sendall(header + masked)
def recv_exact(sock, size):
chunks = []
remaining = size
while remaining:
chunk = sock.recv(remaining)
if not chunk:
raise RuntimeError("DevTools websocket closed")
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def websocket_recv(sock):
first, second = recv_exact(sock, 2)
opcode = first & 0x0F
length = second & 0x7F
if length == 126:
length = struct.unpack("!H", recv_exact(sock, 2))[0]
elif length == 127:
length = struct.unpack("!Q", recv_exact(sock, 8))[0]
masked = bool(second & 0x80)
mask = recv_exact(sock, 4) if masked else b""
payload = recv_exact(sock, length)
if masked:
payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
if opcode == 8:
raise RuntimeError("DevTools websocket closed")
if opcode != 1:
return None
return json.loads(payload.decode("utf-8"))
def find_page_ws_url():
while time.monotonic() < deadline:
try:
for target in http_json("/json/list"):
if target.get("type") == "page" and target.get("url", "").endswith("/test-browser-wasm-smoke.html"):
return target["webSocketDebuggerUrl"]
except Exception:
pass
time.sleep(0.1)
raise RuntimeError("timed out waiting for browser DevTools page target")
sock = websocket_connect(find_page_ws_url())
next_id = 0
def cdp(method, params=None):
global next_id
next_id += 1
message_id = next_id
websocket_send(sock, json.dumps({"id": message_id, "method": method, "params": params or {}}))
while time.monotonic() < deadline:
try:
message = websocket_recv(sock)
except TimeoutError:
continue
except socket.timeout:
continue
if message and message.get("id") == message_id:
if "error" in message:
raise RuntimeError(message["error"])
return message["result"]
raise RuntimeError(f"timed out waiting for DevTools response to {method}")
def evaluate(expression):
result = cdp("Runtime.evaluate", {"expression": expression, "returnByValue": True})
return result.get("result", {}).get("value", "")
try:
cdp("Runtime.enable")
text = ""
while time.monotonic() < deadline:
text = evaluate('document.querySelector("#result")?.textContent || ""')
if "browser wasm smoke passed" in text:
html = evaluate("document.documentElement.outerHTML")
with open(dom_log, "w", encoding="utf-8") as handle:
handle.write(html)
sys.exit(0)
if "browser wasm smoke failed:" in text:
print(text, file=sys.stderr)
html = evaluate("document.documentElement.outerHTML")
with open(dom_log, "w", encoding="utf-8") as handle:
handle.write(html)
sys.exit(1)
time.sleep(0.2)
print(f"timed out waiting for browser smoke result; last status: {text}", file=sys.stderr)
html = evaluate("document.documentElement.outerHTML")
with open(dom_log, "w", encoding="utf-8") as handle:
handle.write(html)
sys.exit(1)
finally:
sock.close()
PY
then
echo "browser WASM smoke failed or timed out" >&2
print_browser_failure_context
exit 1
fi
echo "browser wasm smoke passed"
if ! grep -F "browser wasm smoke passed" "$dom_log" >/dev/null; then
echo "browser WASM smoke did not pass" >&2
print_browser_failure_context
exit 1
fi
passed_text=$(grep -o "browser wasm smoke passed ([0-9][0-9]* sections)" "$dom_log" | tail -n 1 || true)
passed_sections=$(printf '%s\n' "$passed_text" | grep -o '[0-9][0-9]*' || true)
if [[ -z "$passed_sections" ]]; then
echo "browser WASM smoke did not report a completed section count" >&2
print_browser_failure_context
exit 1
fi
if ((passed_sections < minimum_sections)); then
echo "browser WASM smoke covered $passed_sections sections, expected at least $minimum_sections" >&2
print_browser_failure_context
exit 1
fi
echo "${passed_text:-browser wasm smoke passed}"

View File

@@ -7,7 +7,13 @@ const { pathToFileURL } = require("url");
const wasmDir = path.join(__dirname, "web", "public");
const cjsLoader = path.join(os.tmpdir(), `cnc_sim_node_loader_${process.pid}.cjs`);
fs.copyFileSync(path.join(wasmDir, "cnc_sim.js"), cjsLoader);
process.on("exit", () => {
fs.rmSync(cjsLoader, { force: true });
});
const createCncSimModule = require(cjsLoader);
if (typeof createCncSimModule !== "function") {
throw new Error("cnc_sim.js did not export a Node-loadable createCncSimModule function");
}
const EVENT_TYPE = {
2: "comment",
@@ -61,10 +67,87 @@ function expectKinematicsSwitch(events, line, kinstype, label) {
}
}
function expectEvent(events, predicate, message) {
if (!events.some(predicate)) {
throw new Error(message);
}
}
async function expectErrorContaining(action, expectedText, message) {
try {
await action();
throw new Error(message);
} catch (error) {
if (!String(error?.message ?? error).includes(expectedText)) {
throw error;
}
}
}
function expectRtcpPivot(events, expected, message) {
expectEvent(
events,
(event) => event.type === "rtcp-pivot" &&
event.line === expected.line &&
event.reserved === expected.reserved &&
near(event.dwellSeconds, expected.dwellSeconds) &&
(expected.endX === undefined || near(event.endX, expected.endX)) &&
(expected.endY === undefined || near(event.endY, expected.endY)) &&
(expected.endZ === undefined || near(event.endZ, expected.endZ)) &&
(expected.endA === undefined || near(event.endA, expected.endA)) &&
(expected.endB === undefined || near(event.endB, expected.endB)) &&
(expected.endC === undefined || near(event.endC, expected.endC)),
message,
);
}
function near(actual, expected) {
return Math.abs(actual - expected) < 1e-6;
}
let completedSteps = 0;
function loadSwitchkinsCases() {
const configCases = JSON.parse(
fs.readFileSync(path.join(wasmDir, "linuxcnc_switchkins_remap_config_cases.json"), "utf8"),
);
if (!Array.isArray(configCases) || configCases.length === 0) {
throw new Error("expected non-empty generated LinuxCNC switchkins config cases");
}
for (const [index, configCase] of configCases.entries()) {
if (
!configCase ||
typeof configCase.field !== "string" ||
typeof configCase.value !== "string" ||
!Number.isInteger(configCase.m428) ||
!Number.isInteger(configCase.m429) ||
!Number.isInteger(configCase.m430)
) {
throw new Error(`invalid generated LinuxCNC switchkins config case at index ${index}`);
}
}
return configCases.map((configCase) => ({
label: `${configCase.field}=${configCase.value}`,
config: { backend: "linuxcnc-rs274", [configCase.field]: configCase.value },
program: configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n",
expected: [
[1, configCase.m428],
[2, configCase.m429],
...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []),
],
}));
}
async function runStep(name, action) {
try {
await action();
completedSteps += 1;
} catch (error) {
const detail = error && error.stack ? error.stack : error;
throw new Error(`Node WASM smoke step "${name}" failed: ${detail}`);
}
}
(async () => {
const module = await createCncSimModule({
locateFile: (file) => path.join(wasmDir, file),
@@ -80,7 +163,40 @@ function near(actual, expected) {
const loadConfig = module.cwrap("cnc_sim_load_config_json", "number", ["number", "number", "number"]);
const parseProgram = module.cwrap("cnc_sim_parse_program", "number", ["number", "number", "number"]);
const lastError = module.cwrap("cnc_sim_last_error", "number", ["number"]);
const throwLastError = (targetHandle) => {
throw new Error(module.UTF8ToString(lastError(targetHandle)));
};
const initializeHandle = (targetHandle) => {
reset(targetHandle);
if (setDialect(targetHandle, 0) !== 0) {
throwLastError(targetHandle);
}
};
const loadJsonConfig = (targetHandle, value) => {
const config = writeString(module, JSON.stringify(value));
try {
if (loadConfig(targetHandle, config.ptr, config.len) !== 0) {
throwLastError(targetHandle);
}
} finally {
module._free(config.ptr);
}
};
const parseText = (targetHandle, value) => {
const program = writeString(module, value);
try {
if (parseProgram(targetHandle, program.ptr, program.len) !== 0) {
throwLastError(targetHandle);
}
} finally {
module._free(program.ptr);
}
};
const attachCallback = (targetHandle, targetCallbackPtr) => {
if (setCallback(targetHandle, targetCallbackPtr, 0) !== 0) {
throwLastError(targetHandle);
}
};
const handle = create();
if (!handle) {
throw new Error("cnc_sim_create returned null");
@@ -88,258 +204,146 @@ function near(actual, expected) {
let callbackPtr = 0;
try {
reset(handle);
if (setDialect(handle, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
const config = writeString(module, JSON.stringify({ backend: "linuxcnc-rs274" }));
try {
if (loadConfig(handle, config.ptr, config.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(config.ptr);
}
initializeHandle(handle);
loadJsonConfig(handle, { backend: "linuxcnc-rs274" });
const events = [];
callbackPtr = module.addFunction((eventPtr) => {
events.push(readEvent(module, eventPtr));
return 0;
}, "iii");
if (setCallback(handle, callbackPtr, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
const program = writeString(module, "G21 G90\nG0 X0\nG1 X5 F100\nM30\n");
try {
if (parseProgram(handle, program.ptr, program.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(program.ptr);
}
if (!events.some((event) => event.type === "linear-feed" && event.endX === 5)) {
throw new Error("expected LinuxCNC WASM linear-feed event ending at X5");
}
if (!events.some((event) => event.type === "program-end")) {
throw new Error("expected LinuxCNC WASM program-end event");
}
events.length = 0;
const switchProgram = writeString(module, "M428\nM429\nM430\n");
try {
if (parseProgram(handle, switchProgram.ptr, switchProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(switchProgram.ptr);
}
for (const kinstype of [1, 0, 2]) {
if (!events.some((event) => event.type === "kinematics-switch" && event.reserved === kinstype)) {
throw new Error(`expected LinuxCNC WASM kinematics switch ${kinstype}`);
}
if (!events.some((event) => event.type === "comment" &&
event.reserved === 68 &&
event.tool === 3 &&
event.feed === kinstype)) {
throw new Error(`expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect`);
}
}
if (!events.some((event) => event.type === "comment" &&
event.reserved === 66 &&
event.tool === 0 &&
event.feed === 0)) {
throw new Error("expected LinuxCNC WASM M66 E0 L0 remap side effect");
}
const switchkinsCases = [
{
label: "scara",
config: { backend: "linuxcnc-rs274", switchkins: "scara" },
program: "M428\nM429\nM430\n",
expected: [
[1, 0],
[2, 1],
[3, 2],
],
},
{
label: "table-dual-rotary",
config: { backend: "linuxcnc-rs274", switchkins: "table-dual-rotary" },
program: "M428\nM429\n",
expected: [
[1, 1],
[2, 0],
],
},
{
label: "sim-xyzab-tdr-kins",
config: { backend: "linuxcnc-rs274", switchkins: "sim-xyzab-tdr-kins" },
program: "M428\nM429\n",
expected: [
[1, 1],
[2, 0],
],
},
{
label: "puma",
config: { backend: "linuxcnc-rs274", switchkins: "puma" },
program: "M428\nM429\nM430\n",
expected: [
[1, 0],
[2, 1],
[3, 2],
],
},
{
label: "melfa-sim",
config: { backend: "linuxcnc-rs274", switchkins: "melfa-sim" },
program: "M428\nM429\nM430\n",
expected: [
[1, 0],
[2, 1],
[3, 2],
],
},
{
label: "hexapod-sim",
config: { backend: "linuxcnc-rs274", remap: "hexapod-sim" },
program: "M428\nM429\nM430\n",
expected: [
[1, 0],
[2, 1],
[3, 2],
],
},
{
label: "xyzbca-trsrn",
config: { backend: "linuxcnc-rs274", switchkins: "xyzbca-trsrn" },
program: "M428\nM429\nM430\n",
expected: [
[1, 0],
[2, 1],
[3, 2],
],
},
];
for (const testCase of switchkinsCases) {
attachCallback(handle, callbackPtr);
const parseCaseWithHandle = (testCase) => {
reset(handle);
events.length = 0;
const switchkinsConfig = writeString(module, JSON.stringify(testCase.config));
loadJsonConfig(handle, testCase.config);
parseText(handle, testCase.program);
};
const withConfiguredHandle = async (label, config, program, action) => {
events.length = 0;
const targetHandle = create();
if (!targetHandle) {
throw new Error(`cnc_sim_create returned null for ${label} case`);
}
try {
if (loadConfig(handle, switchkinsConfig.ptr, switchkinsConfig.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
initializeHandle(targetHandle);
attachCallback(targetHandle, callbackPtr);
loadJsonConfig(targetHandle, config);
parseText(targetHandle, program);
await action();
} finally {
module._free(switchkinsConfig.ptr);
destroy(targetHandle);
}
};
const switchkinsProgram = writeString(module, testCase.program);
try {
if (parseProgram(handle, switchkinsProgram.ptr, switchkinsProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(switchkinsProgram.ptr);
}
await runStep("basic LinuxCNC RS274 parse", async () => {
parseText(handle, "G21 G90\nG0 X0\nG1 X5 F100\nM30\n");
});
await runStep("basic LinuxCNC RS274 linear-feed event", async () => {
expectEvent(events, (event) => event.type === "linear-feed" && event.endX === 5,
"expected LinuxCNC WASM linear-feed event ending at X5");
});
await runStep("basic LinuxCNC RS274 program-end event", async () => {
expectEvent(events, (event) => event.type === "program-end",
"expected LinuxCNC WASM program-end event");
});
await runStep("default switchkins remap parse", async () => {
events.length = 0;
parseText(handle, "M428\nM429\nM430\n");
});
for (const kinstype of [1, 0, 2]) {
await runStep(`default switchkins kinematics switch ${kinstype}`, async () => {
expectEvent(events, (event) => event.type === "kinematics-switch" && event.reserved === kinstype,
`expected LinuxCNC WASM kinematics switch ${kinstype}`);
});
await runStep(`default switchkins M68 E3 Q${kinstype} side effect`, async () => {
expectEvent(events, (event) => event.type === "comment" &&
event.reserved === 68 &&
event.tool === 3 &&
event.feed === kinstype,
`expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect`);
});
}
await runStep("default switchkins M66 E0 L0 side effect", async () => {
expectEvent(events, (event) => event.type === "comment" &&
event.reserved === 66 &&
event.tool === 0 &&
event.feed === 0,
"expected LinuxCNC WASM M66 E0 L0 remap side effect");
});
const switchkinsCases = loadSwitchkinsCases();
await runStep("generated switchkins config cases load", async () => {
if (switchkinsCases.length === 0) {
throw new Error("expected generated LinuxCNC WASM switchkins config cases");
}
});
for (const testCase of switchkinsCases) {
await runStep(`generated switchkins ${testCase.label}`, async () => {
parseCaseWithHandle(testCase);
for (const [line, kinstype] of testCase.expected) {
expectKinematicsSwitch(events, line, kinstype, testCase.label);
}
});
}
await runStep("5axiskins RTCP parse", async () => {
reset(handle);
events.length = 0;
const fiveaxisConfig = writeString(
module,
JSON.stringify({
loadJsonConfig(handle, {
backend: "linuxcnc-rs274",
rtcp: { enabled: true, toolLength: 250 },
kinematics: "5axiskins",
pivotLength: 250,
}),
);
try {
if (loadConfig(handle, fiveaxisConfig.ptr, fiveaxisConfig.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(fiveaxisConfig.ptr);
}
const fiveaxisProgram = writeString(module, "M428\nG0 X260 Y20 Z280 B90 C0\n");
try {
if (parseProgram(handle, fiveaxisProgram.ptr, fiveaxisProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(fiveaxisProgram.ptr);
}
});
parseText(handle, "M428\nG0 X260 Y20 Z280 B90 C0\n");
});
await runStep("5axiskins RTCP kinematics switch", async () => {
expectKinematicsSwitch(events, 1, 0, "5axiskins");
if (!events.some((event) => event.type === "rtcp-pivot" &&
event.line === 2 &&
event.reserved === 0 &&
near(event.dwellSeconds, 250) &&
near(event.endX, 10) &&
near(event.endY, 20) &&
near(event.endZ, 30) &&
near(event.endB, 90) &&
near(event.endC, 0))) {
throw new Error("expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse");
}
});
await runStep("5axiskins RTCP pivot event", async () => {
expectRtcpPivot(events, {
line: 2,
reserved: 0,
dwellSeconds: 250,
endX: 10,
endY: 20,
endZ: 30,
endB: 90,
endC: 0,
}, "expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse");
});
await runStep("userk RTCP parse", async () => {
reset(handle);
events.length = 0;
const userkConfig = writeString(
module,
JSON.stringify({
loadJsonConfig(handle, {
backend: "linuxcnc-rs274",
rtcp: { enabled: true, toolLength: 250 },
}),
);
try {
if (loadConfig(handle, userkConfig.ptr, userkConfig.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(userkConfig.ptr);
}
const userkProgram = writeString(module, "M430\nG0 X260 Y20 Z280 B90 C45\n");
try {
if (parseProgram(handle, userkProgram.ptr, userkProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
} finally {
module._free(userkProgram.ptr);
}
});
parseText(handle, "M430\nG0 X260 Y20 Z280 B90 C45\n");
});
await runStep("userk RTCP kinematics switch", async () => {
expectKinematicsSwitch(events, 1, 2, "userk");
if (!events.some((event) => event.type === "rtcp-pivot" &&
event.line === 2 &&
event.reserved === 2 &&
near(event.dwellSeconds, 250) &&
near(event.endX, 260) &&
near(event.endY, 20) &&
near(event.endZ, 280) &&
near(event.endB, 90) &&
near(event.endC, 45))) {
throw new Error("expected LinuxCNC WASM userk RTCP pivot from LinuxCNC userk identity inverse");
}
});
await runStep("userk RTCP pivot event", async () => {
expectRtcpPivot(events, {
line: 2,
reserved: 2,
dwellSeconds: 250,
endX: 260,
endY: 20,
endZ: 280,
endB: 90,
endC: 45,
}, "expected LinuxCNC WASM userk RTCP pivot from LinuxCNC userk identity inverse");
});
events.length = 0;
const configuredTrtHandle = create();
if (!configuredTrtHandle) {
throw new Error("cnc_sim_create returned null for configured xyzbc-trt case");
}
const configuredTrtConfig = writeString(
module,
JSON.stringify({
await runStep("configured xyzbc-trt RTCP parse", async () => {
await withConfiguredHandle(
"configured xyzbc-trt",
{
backend: "linuxcnc-rs274",
rtcp: { enabled: false, toolLength: 11 },
xyzbcTrt: {
@@ -350,54 +354,32 @@ function near(actual, expected) {
zOffset: 7,
conventionalDirections: true,
},
}),
},
"M428\nG43.4\nG0 X30 Y40 Z50 B20 C-30\n",
async () => {
},
);
try {
reset(configuredTrtHandle);
if (setDialect(configuredTrtHandle, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
}
if (setCallback(configuredTrtHandle, callbackPtr, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
}
if (loadConfig(configuredTrtHandle, configuredTrtConfig.ptr, configuredTrtConfig.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
}
} finally {
module._free(configuredTrtConfig.ptr);
}
const configuredTrtProgram = writeString(module, "M428\nG43.4\nG0 X30 Y40 Z50 B20 C-30\n");
try {
if (parseProgram(configuredTrtHandle, configuredTrtProgram.ptr, configuredTrtProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredTrtHandle)));
}
} finally {
module._free(configuredTrtProgram.ptr);
destroy(configuredTrtHandle);
}
});
await runStep("configured xyzbc-trt kinematics switch", async () => {
expectKinematicsSwitch(events, 1, 1, "configured xyzbc-trt");
if (!events.some((event) => event.type === "rtcp-pivot" &&
event.line === 3 &&
event.reserved === 1 &&
near(event.dwellSeconds, 11) &&
near(event.endX, -4.814629) &&
near(event.endY, 47.605118) &&
near(event.endZ, 48.160567) &&
near(event.endB, 20) &&
near(event.endC, -30))) {
throw new Error("expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse");
}
});
await runStep("configured xyzbc-trt RTCP pivot event", async () => {
expectRtcpPivot(events, {
line: 3,
reserved: 1,
dwellSeconds: 11,
endX: -4.814629,
endY: 47.605118,
endZ: 48.160567,
endB: 20,
endC: -30,
}, "expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse");
});
events.length = 0;
const configuredXyzacHandle = create();
if (!configuredXyzacHandle) {
throw new Error("cnc_sim_create returned null for configured xyzac-trt case");
}
const configuredXyzacConfig = writeString(
module,
JSON.stringify({
await runStep("configured xyzac-trt RTCP parse", async () => {
await withConfiguredHandle(
"configured xyzac-trt",
{
backend: "linuxcnc-rs274",
rtcp: { enabled: false, toolLength: 7 },
switchkins: "xyzac-trt",
@@ -405,47 +387,30 @@ function near(actual, expected) {
yOffset: 20,
zOffset: 10,
},
}),
},
"M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n",
async () => {
},
);
try {
reset(configuredXyzacHandle);
if (setDialect(configuredXyzacHandle, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
}
if (setCallback(configuredXyzacHandle, callbackPtr, 0) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
}
if (loadConfig(configuredXyzacHandle, configuredXyzacConfig.ptr, configuredXyzacConfig.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
}
} finally {
module._free(configuredXyzacConfig.ptr);
}
const configuredXyzacProgram = writeString(module, "M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n");
try {
if (parseProgram(configuredXyzacHandle, configuredXyzacProgram.ptr, configuredXyzacProgram.len) !== 0) {
throw new Error(module.UTF8ToString(lastError(configuredXyzacHandle)));
}
} finally {
module._free(configuredXyzacProgram.ptr);
destroy(configuredXyzacHandle);
}
});
await runStep("configured xyzac-trt kinematics switch", async () => {
expectKinematicsSwitch(events, 1, 1, "configured xyzac-trt");
if (!events.some((event) => event.type === "rtcp-pivot" &&
event.line === 3 &&
event.reserved === 1 &&
near(event.dwellSeconds, 7) &&
near(event.endX, 7.494747) &&
near(event.endY, -20.815946) &&
near(event.endZ, 18.939732) &&
near(event.endA, 35) &&
near(event.endB, 0) &&
near(event.endC, -25))) {
throw new Error("expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse");
}
});
await runStep("configured xyzac-trt RTCP pivot event", async () => {
expectRtcpPivot(events, {
line: 3,
reserved: 1,
dwellSeconds: 7,
endX: 7.494747,
endY: -20.815946,
endZ: 18.939732,
endA: 35,
endB: 0,
endC: -25,
}, "expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse");
});
await runStep("web wasm-core wrapper defaults and options", async () => {
globalThis.createCncSimModule = createCncSimModule;
const wasmCoreUrl = pathToFileURL(path.join(__dirname, "web", "src", "wasm-core.js")).href;
const { createWasmSimulator } = await import(wasmCoreUrl);
@@ -461,6 +426,31 @@ function near(actual, expected) {
near(event.end.x, 3))) {
throw new Error("expected web wasm-core default backend to use LinuxCNC RS274");
}
if (webSimulator.fs.opfs !== null) {
throw new Error("expected Node wasm-core wrapper to leave OPFS disabled");
}
await expectErrorContaining(
() => webSimulator.parseFile("programs/node-smoke.ngc", "linuxcnc", { backend: "linuxcnc-rs274" }),
"OPFS workspace is not available",
"expected Node wasm-core parseFile to require OPFS",
);
await expectErrorContaining(
() => webSimulator.parseWithParameterFile("G21 G90\nM30\n", "parameters/node.var", "linuxcnc", {
backend: "linuxcnc-rs274",
}),
"OPFS workspace is not available",
"expected Node wasm-core parseWithParameterFile to require OPFS",
);
await expectErrorContaining(
() => webSimulator.parseFileWithParameterFile(
"programs/node-smoke.ngc",
"parameters/node.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"OPFS workspace is not available",
"expected Node wasm-core parseFileWithParameterFile to require OPFS",
);
const webEvents = webSimulator.parse(
"M428\nG43.4\nG0 X12 Y-8 Z42 A35 C-25\n",
@@ -475,7 +465,7 @@ function near(actual, expected) {
},
},
);
if (!webEvents.some((event) => event.type === "rtcp-pivot" &&
expectEvent(webEvents, (event) => event.type === "rtcp-pivot" &&
event.line === 3 &&
event.reserved === 1 &&
near(event.dwellSeconds, 7) &&
@@ -484,13 +474,13 @@ function near(actual, expected) {
near(event.end.z, 18.939732) &&
near(event.end.a, 35) &&
near(event.end.b, 0) &&
near(event.end.c, -25))) {
throw new Error("expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM");
}
near(event.end.c, -25),
"expected web wasm-core options to pass LinuxCNC xyzac-trt RTCP config into WASM");
} finally {
webSimulator.dispose();
delete globalThis.createCncSimModule;
}
});
} finally {
if (callbackPtr) {
module.removeFunction(callbackPtr);
@@ -498,7 +488,7 @@ function near(actual, expected) {
destroy(handle);
}
console.log("web wasm node smoke passed");
console.log(`web wasm node smoke passed (${completedSteps} steps)`);
})().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);

View File

@@ -4,20 +4,116 @@ set -euo pipefail
cd "$(dirname "$0")"
missing=()
for required in node web/public/cnc_sim.js web/public/cnc_sim.wasm; do
if [[ "$required" == node ]]; then
if ! command -v node >/dev/null 2>&1; then
missing+=("node")
minimum_steps=${WASM_NODE_SMOKE_MIN_STEPS:-50}
if ! [[ "$minimum_steps" =~ ^[1-9][0-9]*$ ]]; then
echo "WASM_NODE_SMOKE_MIN_STEPS must be a positive integer: $minimum_steps" >&2
exit 1
fi
require_command() {
local command_name=$1
if ! command -v "$command_name" >/dev/null 2>&1; then
missing+=("$command_name")
fi
elif [[ ! -f "$required" ]]; then
missing+=("$required")
}
require_file() {
local file_path=$1
if [[ ! -f "$file_path" ]]; then
missing+=("$file_path")
elif [[ ! -r "$file_path" || ! -s "$file_path" ]]; then
missing+=("readable non-empty $file_path")
fi
}
report_missing_prerequisites() {
echo "WASM Node smoke prerequisites are missing: ${missing[*]}." >&2
echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2
}
required_files=(
test-web-wasm-node-smoke.cjs
web/public/cnc_sim.js
web/public/cnc_sim.wasm
web/public/linuxcnc_switchkins_remap_config_cases.json
web/src/app.js
web/src/wasm-core.js
)
for required in \
node \
"${required_files[@]}"
do
if [[ "$required" == node ]]; then
require_command node
else
require_file "$required"
fi
done
if ((${#missing[@]} > 0)); then
echo "WASM Node smoke prerequisites are missing: ${missing[*]}." >&2
echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2
report_missing_prerequisites
exit 1
fi
node test-web-wasm-node-smoke.cjs
if ! node - <<'JS'
const fs = require("fs");
const cases = JSON.parse(fs.readFileSync("web/public/linuxcnc_switchkins_remap_config_cases.json", "utf8"));
if (!Array.isArray(cases) || cases.length === 0) {
throw new Error("expected non-empty LinuxCNC switchkins config case JSON");
}
for (const [index, configCase] of cases.entries()) {
if (
!configCase ||
typeof configCase.field !== "string" ||
typeof configCase.value !== "string" ||
!Number.isInteger(configCase.m428) ||
!Number.isInteger(configCase.m429) ||
!Number.isInteger(configCase.m430)
) {
throw new Error(`invalid LinuxCNC switchkins config case at index ${index}`);
}
}
JS
then
echo "WASM Node smoke switchkins config case JSON check failed" >&2
exit 1
fi
node_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_node_smoke.XXXXXX.log")
cleanup() {
rm -f "$node_log"
}
trap cleanup EXIT
echo "running WASM Node smoke"
echo "WASM Node smoke node version: $(node --version)"
if ! node --check test-web-wasm-node-smoke.cjs >"$node_log" 2>&1; then
echo "WASM Node smoke syntax check failed" >&2
sed -n '1,80p' "$node_log" >&2
exit 1
fi
for web_module in web/public/cnc_sim.js web/src/wasm-core.js web/src/app.js; do
if ! node --check "$web_module" >"$node_log" 2>&1; then
echo "WASM Node smoke web module syntax check failed for $web_module" >&2
sed -n '1,80p' "$node_log" >&2
exit 1
fi
done
if ! node test-web-wasm-node-smoke.cjs >"$node_log" 2>&1; then
echo "WASM Node smoke failed" >&2
sed -n '1,120p' "$node_log" >&2
exit 1
fi
passed_steps=$(grep -o 'web wasm node smoke passed ([0-9][0-9]* steps)' "$node_log" | tail -n 1 | grep -o '[0-9][0-9]*' || true)
if [[ -z "$passed_steps" ]]; then
echo "WASM Node smoke did not report a completed step count" >&2
sed -n '1,120p' "$node_log" >&2
exit 1
fi
if ((passed_steps < minimum_steps)); then
echo "WASM Node smoke covered $passed_steps steps, expected at least $minimum_steps" >&2
sed -n '1,120p' "$node_log" >&2
exit 1
fi
cat "$node_log"

View File

@@ -12,6 +12,7 @@
<div class="brand">CNC SIM</div>
<div class="status-strip">
<span id="wasmState" class="status alarm">WASM OFFLINE</span>
<span id="storageState" class="status">OPFS INIT</span>
<span id="modeState" class="status">MEM</span>
<span id="unitState" class="status">MM</span>
<span id="programState" class="status">RESET</span>
@@ -65,7 +66,6 @@
<span>BACKEND</span>
<select id="backendSelect">
<option value="linuxcnc-rs274">LINUXCNC</option>
<option value="smoke">SMOKE</option>
</select>
</label>
<span id="lineCount">0 LINES</span>

994
web/package-lock.json generated Normal file
View File

@@ -0,0 +1,994 @@
{
"name": "cnc-wasm-simulator-web",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cnc-wasm-simulator-web",
"dependencies": {
"three": "^0.164.0"
},
"devDependencies": {
"@types/three": "^0.164.0",
"typescript": "^5.4.5",
"vite": "^5.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
"integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
"integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
"integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
"integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
"integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
"integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
"integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
"integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
"integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
"integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
"integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
"integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
"integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
"integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
"integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
"integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
"integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
"integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
"integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
"integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
"integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
"integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
"integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
"integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.164.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.164.1.tgz",
"integrity": "sha512-dR/trWDhyaNqJV38rl1TonlCA9DpnX7OPYDWD81bmBGn/+uEc3+zNalFxQcV4FlPTeDBhCY3SFWKvK6EJwL88g==",
"dev": true,
"dependencies": {
"@tweenjs/tween.js": "~23.1.1",
"@types/stats.js": "*",
"@types/webxr": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.60.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
"integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.4",
"@rollup/rollup-android-arm64": "4.60.4",
"@rollup/rollup-darwin-arm64": "4.60.4",
"@rollup/rollup-darwin-x64": "4.60.4",
"@rollup/rollup-freebsd-arm64": "4.60.4",
"@rollup/rollup-freebsd-x64": "4.60.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
"@rollup/rollup-linux-arm-musleabihf": "4.60.4",
"@rollup/rollup-linux-arm64-gnu": "4.60.4",
"@rollup/rollup-linux-arm64-musl": "4.60.4",
"@rollup/rollup-linux-loong64-gnu": "4.60.4",
"@rollup/rollup-linux-loong64-musl": "4.60.4",
"@rollup/rollup-linux-ppc64-gnu": "4.60.4",
"@rollup/rollup-linux-ppc64-musl": "4.60.4",
"@rollup/rollup-linux-riscv64-gnu": "4.60.4",
"@rollup/rollup-linux-riscv64-musl": "4.60.4",
"@rollup/rollup-linux-s390x-gnu": "4.60.4",
"@rollup/rollup-linux-x64-gnu": "4.60.4",
"@rollup/rollup-linux-x64-musl": "4.60.4",
"@rollup/rollup-openbsd-x64": "4.60.4",
"@rollup/rollup-openharmony-arm64": "4.60.4",
"@rollup/rollup-win32-arm64-msvc": "4.60.4",
"@rollup/rollup-win32-ia32-msvc": "4.60.4",
"@rollup/rollup-win32-x64-gnu": "4.60.4",
"@rollup/rollup-win32-x64-msvc": "4.60.4",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/three": {
"version": "0.164.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz",
"integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w=="
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +1,383 @@
[
{"field":"config","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/bridgemill/5axis.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"bridgemill","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axis","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"Sim-5Axis Bridge Mill (xyzbcw)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"Sim-5Axis Bridge Mill","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axiskins coordinates=xyzbcwy","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"5axiskins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"5axisgui.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"5axisgui","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"5axis_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"5axis_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/bridgemill","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"bridgemill","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini","m428":1,"m429":0,"m430":-1},
{"field":"machine","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1},
{"field":"kinematics","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab-tdr","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"sim-xyzab-tdr-kins (switchkins)","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"sim-xyzab-tdr-kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab_tdr_kins","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab_tdr","m428":1,"m429":0,"m430":-1},
{"field":"switchkins","value":"xyzab-tdr-kins","m428":1,"m429":0,"m430":-1},
{"field":"postguiHalFile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postguiHalFile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"postgui_halfile","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postgui_halfile","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"postgui_hal_file","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"postgui_hal_file","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui.hal","m428":1,"m429":0,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"xyzab-tdr-postgui","m428":1,"m429":0,"m430":-1},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"remap","value":"table-dual-rotary","m428":1,"m429":0,"m430":-1},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"machine","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"kinematics","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzac-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzac-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzac-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"postguiHalFile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postguiHalFile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"postgui_halfile","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postgui_halfile","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"postgui_hal_file","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"postgui_hal_file","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"POSTGUI_HALFILE","value":"switchkins_postgui.hal","m428":1,"m429":0,"m430":2},
{"field":"POSTGUI_HALFILE","value":"switchkins_postgui","m428":1,"m429":0,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"remap","value":"table-rotary-tilting","m428":1,"m429":0,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini","m428":1,"m429":0,"m430":2},
{"field":"machine","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"kinematics","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzbc-trt-kins (switchkins)","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"sim-xyzbc-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt-kins sparm=identityfirst","m428":1,"m429":0,"m430":2},
{"field":"switchkins","value":"xyzbc-trt-kins","m428":1,"m429":0,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn_twp","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzacb_trsrn","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzacb-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"table-rotary_spindle-rotary-nutating","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzbca-trsrn_twp/xyzbca-trsrn.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn_twp","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca-trsrn (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"xyzbca_trsrn","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"xyzbca-trsrn_postgui","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/hexapod-sim/hexapod.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"genhexkins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"hexapod (switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"genhexkins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"kinematics.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"kinematics","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"hexapod_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"hexapod_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"hexapod-sim","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/melfa-sim/melfa.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"melfa (mm)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"genserkins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"melfa (mm)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"genserkins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"melfa_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"melfa_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"melfa-postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"melfa-postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"melfa-sim","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"configPath","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"config_path","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"iniFile","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"iniFileName","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini_file","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/millturn/millturn.ini","m428":0,"m429":1,"m430":-1},
{"field":"machine","value":"millturn (mm)","m428":0,"m429":1,"m430":-1},
{"field":"kinematics","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"switchkins","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"switchkins","value":"millturn (mm)","m428":0,"m429":1,"m430":-1},
{"field":"halFile","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"halFile","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"halfile","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"halfile","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"hal_file","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"hal_file","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"HALFILE","value":"millturn.hal","m428":0,"m429":1,"m430":-1},
{"field":"HALFILE","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"postguiHalFile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postguiHalFile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"postgui_halfile","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postgui_halfile","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"postgui_hal_file","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"postgui_hal_file","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"millturn-postgui.hal","m428":0,"m429":1,"m430":-1},
{"field":"POSTGUI_HALFILE","value":"millturn-postgui","m428":0,"m429":1,"m430":-1},
{"field":"remap","value":"configs/sim/axis/vismach/millturn","m428":0,"m429":1,"m430":-1},
{"field":"remap","value":"millturn","m428":0,"m429":1,"m430":-1},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"pumakins","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"PUMA (pumakins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"PUMA","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"pumakins","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/puma","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"puma","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube.ini (pumakins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma_cube.ini","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560 (switchkins) (inch)","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halFile","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"halfile","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"hal_file","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma560_dh.hal","m428":0,"m429":1,"m430":2},
{"field":"HALFILE","value":"puma560_dh","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma560_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"puma560_postgui","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/puma/puma560_uvw.ini","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"puma560_uvw","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/axis/vismach/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"machine","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"kinematics","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scara","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"SCARA (genserkins,switchkins)","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"SCARA","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scarakins coordinates=xyzcab","m428":0,"m429":1,"m430":2},
{"field":"switchkins","value":"scarakins","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postguiHalFile","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_halfile","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"postgui_hal_file","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"scara_postgui.hal","m428":0,"m429":1,"m430":2},
{"field":"POSTGUI_HALFILE","value":"scara_postgui","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/axis/vismach/scara","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"scara","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2}
{"field":"configPath","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/qtaxis/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/qtaxis/non-trivial/scara","m428":0,"m429":1,"m430":2},
{"field":"config","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"configPath","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"config_path","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFile","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"iniFileName","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"ini_file_name","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"INI_FILE_NAME","value":"configs/sim/qtvcp_screens/non-trivial/scara/scara.ini","m428":0,"m429":1,"m430":2},
{"field":"remap","value":"configs/sim/qtvcp_screens/non-trivial/scara","m428":0,"m429":1,"m430":2}
]

View File

@@ -2,6 +2,7 @@ import { createWasmSimulator } from "./wasm-core.js";
const elements = {
wasmState: document.querySelector("#wasmState"),
storageState: document.querySelector("#storageState"),
modeState: document.querySelector("#modeState"),
unitState: document.querySelector("#unitState"),
programState: document.querySelector("#programState"),
@@ -26,12 +27,82 @@ const elements = {
let simulatorPromise = null;
let lastEvents = [];
let isParsing = false;
let hasUserEditedProgram = false;
let saveProgramTimer = 0;
let saveProgramSequence = 0;
let saveProgramPromise = Promise.resolve();
let startupPromise = Promise.resolve();
const programPath = "programs/current.ngc";
// LinuxCNC source basis: interp_internal.hh defines RS274NGC_PARAMETER_FILE_NAME_DEFAULT.
const linuxCncParameterFileName = "rs274ngc.var";
const parameterPath = `parameters/${linuxCncParameterFileName}`;
const linuxCncBackend = "linuxcnc-rs274";
const opfsOptions = { required: true, mountPoint: "/cnc", workspacePath: "cnc-simulator" };
function setStatus(node, text, alarm = false) {
node.textContent = text;
node.classList.toggle("alarm", alarm);
}
function setRuntimeReady(simulator) {
setStatus(elements.wasmState, "WASM ONLINE");
if (simulator.fs?.opfs) {
const storageState = elements.storageState.textContent;
if (storageState === "OPFS SAVING" || storageState.startsWith("OPFS SAVED")) {
setStatus(elements.storageState, storageState);
} else {
setStatus(elements.storageState, "OPFS READY");
}
} else {
setStatus(elements.storageState, "OPFS OFF", true);
}
}
function setRuntimeUnavailable() {
setStatus(elements.wasmState, "WASM OFFLINE", true);
setStatus(elements.storageState, "OPFS OFF", true);
}
function setControlsEnabled(enabled) {
elements.parseBtn.disabled = !enabled;
elements.resetBtn.disabled = !enabled;
elements.holdBtn.disabled = !enabled;
elements.stopBtn.disabled = !enabled;
elements.backendSelect.disabled = !enabled;
}
function setStorageSaved(size) {
setStatus(elements.storageState, Number.isFinite(size) ? `OPFS SAVED ${size}B` : "OPFS SAVED");
}
function setProgramReady() {
if (elements.programState.textContent !== "ALARM") {
setStatus(elements.programState, "READY");
}
}
async function statIfExists(opfs, path) {
if (!(await opfs.exists(path))) {
return null;
}
return opfs.stat(path);
}
async function logWorkspaceState(opfs) {
const programStat = await statIfExists(opfs, programPath);
if (programStat?.kind === "file") {
log(`OPFS PROGRAM ${programStat.size}B`);
}
const parameterStat = await statIfExists(opfs, parameterPath);
if (parameterStat?.kind === "file") {
log(`OPFS PARAMETERS ${parameterStat.size}B`);
} else {
log("OPFS PARAMETERS PENDING");
}
}
function log(message, error = false) {
const line = document.createElement("div");
line.className = `alarm-line${error ? " error" : ""}`;
@@ -163,21 +234,63 @@ function updateLineCount() {
elements.lineCount.textContent = `${count} LINES`;
}
async function getOpfsWorkspace() {
const simulator = await getSimulator();
setRuntimeReady(simulator);
if (!simulator.fs.opfs) {
throw new Error("OPFS workspace is required for browser program parsing");
}
return simulator.fs.opfs;
}
async function getSimulator() {
if (!simulatorPromise) {
simulatorPromise = createWasmSimulator();
simulatorPromise = createWasmSimulator({
opfs: opfsOptions,
}).then((simulator) => {
setRuntimeReady(simulator);
return simulator;
}).catch((error) => {
simulatorPromise = null;
globalThis.__cncSimulatorForTest = null;
setRuntimeUnavailable();
throw error;
});
globalThis.__cncSimulatorForTest = simulatorPromise;
}
return simulatorPromise;
}
async function parseProgram() {
if (isParsing) {
return;
}
isParsing = true;
setControlsEnabled(false);
setStatus(elements.programState, "RUN");
let runtimeReady = false;
let savedProgramSize = null;
try {
await startupPromise.catch(() => {});
const simulator = await getSimulator();
setStatus(elements.wasmState, "WASM ONLINE");
const events = simulator.parse(elements.input.value, "linuxcnc", {
backend: elements.backendSelect.value,
});
runtimeReady = true;
setRuntimeReady(simulator);
const parseOptions = {
backend: linuxCncBackend,
};
if (!simulator.fs.opfs) {
throw new Error("OPFS workspace is required for browser program parsing");
}
elements.backendSelect.value = linuxCncBackend;
await saveProgramPromise.catch(() => {});
await simulator.fs.opfs.writeFile(programPath, elements.input.value);
const programStat = await simulator.fs.opfs.stat(programPath);
savedProgramSize = programStat.kind === "file" ? programStat.size : null;
setStorageSaved(savedProgramSize);
const events = await simulator.parseFileWithParameterFile(programPath, parameterPath, "linuxcnc", parseOptions);
const parameterStat = await simulator.fs.opfs.stat(parameterPath);
setStorageSaved(parameterStat.size);
log(`OPFS PARAMETERS ${parameterStat.size}B`);
lastEvents = events;
drawToolpath(events);
@@ -193,9 +306,82 @@ async function parseProgram() {
setStatus(elements.programState, "END");
log(`OK ${events.length} EVENTS`);
} catch (error) {
setStatus(elements.wasmState, "WASM OFFLINE", true);
if (!runtimeReady) {
setRuntimeUnavailable();
}
if (savedProgramSize !== null) {
log(`OPFS PROGRAM SAVED ${savedProgramSize}B`);
}
setStatus(elements.programState, "ALARM", true);
log(error.message, true);
} finally {
isParsing = false;
setControlsEnabled(true);
}
}
async function saveProgramDraft() {
const sequence = ++saveProgramSequence;
saveProgramPromise = saveProgramPromise.catch(() => {}).then(async () => {
const opfs = await getOpfsWorkspace();
const program = elements.input.value;
setStatus(elements.storageState, "OPFS SAVING");
await opfs.writeFile(programPath, program);
if (sequence === saveProgramSequence) {
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
}
});
try {
await saveProgramPromise;
} catch (error) {
setStatus(elements.storageState, "OPFS ALARM", true);
log(error.message, true);
}
}
function scheduleProgramSave() {
window.clearTimeout(saveProgramTimer);
saveProgramTimer = window.setTimeout(() => {
void saveProgramDraft();
}, 350);
}
function flushProgramSave() {
window.clearTimeout(saveProgramTimer);
if (hasUserEditedProgram) {
void saveProgramDraft();
}
}
async function restoreProgram() {
try {
const opfs = await getOpfsWorkspace();
if (!(await opfs.exists(programPath))) {
await opfs.writeFile(programPath, elements.input.value);
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
log(`OPFS PROGRAM CREATED ${stat.size}B`);
await logWorkspaceState(opfs);
return;
}
const program = new TextDecoder().decode(await opfs.readFile(programPath));
if (hasUserEditedProgram) {
return;
}
elements.input.value = program;
updateLineCount();
const stat = await opfs.stat(programPath);
setStorageSaved(stat.size);
setProgramReady();
log("OPFS PROGRAM RESTORED");
await logWorkspaceState(opfs);
} catch (error) {
if (error?.name !== "NotFoundError") {
log(error.message, true);
}
}
}
@@ -205,6 +391,7 @@ document.querySelectorAll("[data-mode]").forEach((button) => {
});
});
elements.backendSelect.value = linuxCncBackend;
elements.parseBtn.addEventListener("click", parseProgram);
elements.resetBtn.addEventListener("click", () => {
lastEvents = [];
@@ -214,8 +401,32 @@ elements.resetBtn.addEventListener("click", () => {
});
elements.holdBtn.addEventListener("click", () => setStatus(elements.programState, "HOLD"));
elements.stopBtn.addEventListener("click", () => setStatus(elements.programState, "STOP", true));
elements.input.addEventListener("input", updateLineCount);
elements.input.addEventListener("input", () => {
updateLineCount();
hasUserEditedProgram = true;
setStatus(elements.programState, "EDIT");
scheduleProgramSave();
});
document.addEventListener("keydown", (event) => {
if (!(event.ctrlKey || event.metaKey) || event.altKey) {
return;
}
if (event.key === "s") {
event.preventDefault();
void saveProgramDraft();
} else if (event.key === "Enter") {
event.preventDefault();
void parseProgram();
}
});
window.addEventListener("resize", resizeCanvas);
window.addEventListener("pagehide", flushProgramSave);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
flushProgramSave();
}
});
updateLineCount();
resizeCanvas();
startupPromise = restoreProgram();

View File

@@ -84,8 +84,19 @@ export type CncParseOptions = {
config?: string;
configPath?: string;
config_path?: string;
ini?: string;
iniFile?: string;
iniFileName?: string;
ini_file?: string;
ini_file_name?: string;
INI_FILE_NAME?: string;
halFile?: string;
halfile?: string;
hal_file?: string;
postguiHalFile?: string;
postgui_halfile?: string;
postgui_hal_file?: string;
POSTGUI_HALFILE?: string;
kinematics?: string;
trt?: "xyzbc" | "xyzac" | string;
pivotLength?: number;

View File

@@ -2,13 +2,65 @@ import type { CncDialect, CncEvent, CncParseOptions } from "./index";
export type WasmSimulator = {
parse(program: string, dialect?: CncDialect, options?: CncParseOptions): CncEvent[];
parseFile(path: string, dialect?: CncDialect, options?: CncParseOptions): Promise<CncEvent[]>;
parseWithParameterFile(
program: string,
parameterPath: string,
dialect?: CncDialect,
options?: CncParseOptions,
): Promise<CncEvent[]>;
parseFileWithParameterFile(
path: string,
parameterPath: string,
dialect?: CncDialect,
options?: CncParseOptions,
): Promise<CncEvent[]>;
dispose(): void;
fs: {
opfs: WasmOpfsWorkspace | null;
module: unknown;
};
};
export type WasmOpfsWorkspace = {
mountPoint: string;
workspacePath: string;
resolvePath(path: string): string;
readFile(path: string): Promise<Uint8Array>;
readDirectory(path: string): Promise<string[]>;
persistFile(path: string): Promise<Uint8Array>;
persistDirectory(path: string): Promise<string[]>;
writeFile(path: string, data: Uint8Array | string): Promise<void>;
copyFile(fromPath: string, toPath: string): Promise<Uint8Array>;
moveFile(fromPath: string, toPath: string): Promise<void>;
exists(path: string): Promise<boolean>;
stat(path: string): Promise<WasmOpfsEntryStat>;
loadParameterFile(path: string): Promise<Uint8Array | null>;
persistParameterFile(path: string): Promise<Uint8Array>;
removeFile(path: string): Promise<void>;
removeDirectory(path: string): Promise<void>;
clear(): Promise<void>;
};
export type WasmOpfsEntryStat =
| {
kind: "file";
size: number;
}
| {
kind: "directory";
size: null;
};
export type WasmModuleOptions = {
locateFile?: (file: string) => string;
print?: (text: string) => void;
printErr?: (text: string) => void;
opfs?: false | {
required?: boolean;
mountPoint?: string;
workspacePath?: string;
};
};
export function createWasmSimulator(moduleOptions?: WasmModuleOptions): Promise<WasmSimulator>;

View File

@@ -41,6 +41,486 @@ const EVENT_OFFSETS = {
reserved: 268,
};
const DEFAULT_OPFS_MOUNT_POINT = "/cnc";
const LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var";
const LINUXCNC_PARAMETER_SCRATCH_FILES = [
LINUXCNC_DEFAULT_PARAMETER_FILE,
`${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`,
`${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`,
];
function isOpfsAvailable() {
return Boolean(globalThis.navigator?.storage?.getDirectory);
}
function assertRelativePath(path) {
if (!path || typeof path !== "string") {
throw new Error("OPFS path must be a non-empty relative path");
}
if (
path.startsWith("/") ||
path.includes("\\") ||
path.includes("\0") ||
path.split("/").some((part) => part === "" || part === "." || part === "..")
) {
throw new Error(`OPFS path must stay inside the CNC workspace: ${path}`);
}
}
function assertWorkspacePath(path) {
assertRelativePath(path);
}
function assertMountPoint(path) {
if (!path || typeof path !== "string") {
throw new Error("OPFS mount point must be a non-empty absolute path");
}
const parts = path.split("/").slice(1);
if (
!path.startsWith("/") ||
path.includes("\\") ||
path.includes("\0") ||
parts.some((part) => part === "" || part === "." || part === "..")
) {
throw new Error(`OPFS mount point must stay inside the Emscripten filesystem: ${path}`);
}
}
async function getOpfsDirectoryHandle(path, create) {
if (!isOpfsAvailable()) {
throw new Error("OPFS is not available in this browser context");
}
let directory = await globalThis.navigator.storage.getDirectory();
for (const part of path.split("/").filter(Boolean)) {
directory = await directory.getDirectoryHandle(part, { create });
}
return directory;
}
async function readOpfsFile(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const fileName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
const file = await directory.getFileHandle(fileName, { create: false });
return new Uint8Array(await (await file.getFile()).arrayBuffer());
}
async function writeOpfsFile(workspacePath, relativePath, data) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const fileName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), true);
const file = await directory.getFileHandle(fileName, { create: true });
const writable = await file.createWritable();
try {
await writable.truncate(0);
await writable.write(data);
await writable.close();
} catch (error) {
try {
await writable.abort();
} catch (_abortError) {
// Preserve the original OPFS write failure.
}
throw error;
}
}
async function removeOpfsEntry(workspacePath, relativePath, recursive) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
try {
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
await directory.removeEntry(entryName, { recursive });
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
}
async function opfsEntryExists(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
try {
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
try {
await directory.getFileHandle(entryName, { create: false });
return true;
} catch (fileError) {
if (fileError?.name !== "NotFoundError" && fileError?.name !== "TypeMismatchError") {
throw fileError;
}
}
try {
await directory.getDirectoryHandle(entryName, { create: false });
return true;
} catch (directoryError) {
if (directoryError?.name !== "NotFoundError" && directoryError?.name !== "TypeMismatchError") {
throw directoryError;
}
}
return false;
} catch (error) {
if (error?.name === "NotFoundError") {
return false;
}
throw error;
}
}
async function statOpfsEntry(workspacePath, relativePath) {
assertRelativePath(relativePath);
const parts = relativePath.split("/");
const entryName = parts.pop();
const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false);
try {
const file = await directory.getFileHandle(entryName, { create: false });
return { kind: "file", size: (await file.getFile()).size };
} catch (fileError) {
if (fileError?.name !== "TypeMismatchError") {
throw fileError;
}
}
await directory.getDirectoryHandle(entryName, { create: false });
return { kind: "directory", size: null };
}
async function clearOpfsWorkspace(workspacePath) {
const parts = workspacePath.split("/").filter(Boolean);
if (parts.length === 0) {
return;
}
const entryName = parts.pop();
const directory = await getOpfsDirectoryHandle(parts.join("/"), true);
try {
await directory.removeEntry(entryName, { recursive: true });
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
await getOpfsDirectoryHandle(workspacePath, true);
}
async function replaceOpfsDirectory(workspacePath, relativePath) {
assertRelativePath(relativePath);
await removeOpfsEntry(workspacePath, relativePath, true);
const directory = await getOpfsDirectoryHandle([workspacePath, relativePath].filter(Boolean).join("/"), true);
for await (const [name] of directory.entries()) {
await directory.removeEntry(name, { recursive: true });
}
}
async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "") {
assertRelativePath(relativePath);
const directory = await getOpfsDirectoryHandle([workspacePath, relativePath, relativeRoot].filter(Boolean).join("/"), false);
const files = [];
const directories = [];
for await (const [name, handle] of directory.entries()) {
const path = relativeRoot ? `${relativeRoot}/${name}` : name;
if (handle.kind === "directory") {
directories.push(path);
const child = await readOpfsDirectoryTree(workspacePath, relativePath, path);
directories.push(...child.directories);
files.push(...child.files);
} else if (handle.kind === "file") {
files.push(path);
}
}
return {
directories: directories.sort(),
files: files.sort(),
};
}
function ensureWasmDirectory(module, path) {
const parts = path.split("/").filter(Boolean);
let current = "";
for (const part of parts) {
current += `/${part}`;
try {
module.FS.mkdir(current);
} catch (error) {
if (error?.errno !== 20) {
throw error;
}
}
}
}
function readWasmDirectoryTree(module, rootPath, relativeRoot = "") {
const files = [];
const directories = [];
for (const entry of module.FS.readdir(rootPath)) {
if (entry === "." || entry === "..") {
continue;
}
const path = `${rootPath}/${entry}`;
const relativePath = relativeRoot ? `${relativeRoot}/${entry}` : entry;
const mode = module.FS.stat(path).mode;
if (module.FS.isDir(mode)) {
directories.push(relativePath);
const child = readWasmDirectoryTree(module, path, relativePath);
directories.push(...child.directories);
files.push(...child.files);
} else if (module.FS.isFile(mode)) {
files.push(relativePath);
}
}
return {
directories: directories.sort(),
files: files.sort(),
};
}
function removeWasmPath(module, path, recursive) {
let mode;
try {
mode = module.FS.stat(path).mode;
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
return;
}
if (module.FS.isDir(mode)) {
if (!recursive) {
module.FS.rmdir(path);
return;
}
for (const entry of module.FS.readdir(path)) {
if (entry !== "." && entry !== "..") {
removeWasmPath(module, `${path}/${entry}`, true);
}
}
module.FS.rmdir(path);
} else {
module.FS.unlink(path);
}
}
function wasmPathExists(module, path) {
try {
module.FS.stat(path);
return true;
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
return false;
}
}
function readFirstExistingWasmFile(module, paths) {
for (const path of paths) {
try {
return module.FS.readFile(path);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
}
return null;
}
function writeWasmFileReplacingPath(module, path, data) {
removeWasmPath(module, path, true);
const parentIndex = path.lastIndexOf("/");
if (parentIndex > 0) {
ensureWasmDirectory(module, path.slice(0, parentIndex));
}
module.FS.writeFile(path, data);
}
function cleanupLinuxCncParameterFiles(module) {
for (const path of LINUXCNC_PARAMETER_SCRATCH_FILES) {
removeWasmPath(module, path, true);
removeWasmPath(module, `/${path}`, true);
}
}
function installOpfsWorkspace(module, options) {
const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false);
if (opfsOptions === false) {
if (isOpfsAvailable()) {
throw new Error("OPFS cannot be disabled in browser contexts with OPFS support");
}
return null;
}
if (!module.FS) {
throw new Error("cnc_sim.js was not built with Emscripten FS export");
}
if (!isOpfsAvailable()) {
if (opfsOptions.required) {
throw new Error("OPFS is only available in browser contexts");
}
return null;
}
const mountPoint = opfsOptions.mountPoint ?? DEFAULT_OPFS_MOUNT_POINT;
const workspacePath = opfsOptions.workspacePath ?? "cnc-simulator";
assertMountPoint(mountPoint);
assertWorkspacePath(workspacePath);
ensureWasmDirectory(module, mountPoint);
return {
mountPoint,
workspacePath,
resolvePath(path) {
assertRelativePath(path);
return `${mountPoint}/${path}`;
},
async readFile(path) {
const data = await readOpfsFile(workspacePath, path);
const wasmPath = this.resolvePath(path);
writeWasmFileReplacingPath(module, wasmPath, data);
return data;
},
async readDirectory(path) {
assertRelativePath(path);
let tree;
try {
tree = await readOpfsDirectoryTree(workspacePath, path);
} catch (error) {
if (error?.name === "NotFoundError") {
removeWasmPath(module, this.resolvePath(path), true);
}
throw error;
}
const wasmPath = this.resolvePath(path);
removeWasmPath(module, wasmPath, true);
ensureWasmDirectory(module, wasmPath);
for (const directory of tree.directories) {
ensureWasmDirectory(module, `${wasmPath}/${directory}`);
}
for (const file of tree.files) {
await this.readFile(`${path}/${file}`);
}
return tree.files;
},
async persistFile(path) {
const wasmPath = this.resolvePath(path);
const data = module.FS.readFile(wasmPath);
await writeOpfsFile(workspacePath, path, data);
return data;
},
async persistDirectory(path) {
assertRelativePath(path);
const wasmPath = this.resolvePath(path);
const tree = readWasmDirectoryTree(module, wasmPath);
await replaceOpfsDirectory(workspacePath, path);
for (const directory of tree.directories) {
await getOpfsDirectoryHandle([workspacePath, path, directory].filter(Boolean).join("/"), true);
}
for (const file of tree.files) {
await this.persistFile(`${path}/${file}`);
}
return tree.files;
},
async writeFile(path, data) {
await writeOpfsFile(workspacePath, path, data);
const wasmPath = this.resolvePath(path);
writeWasmFileReplacingPath(module, wasmPath, data);
},
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() links the
// current parameter file to filename + ".bak" before replacing it.
async copyFile(fromPath, toPath) {
assertRelativePath(fromPath);
assertRelativePath(toPath);
const data = await readOpfsFile(workspacePath, fromPath);
await writeOpfsFile(workspacePath, toPath, data);
const fromWasmPath = this.resolvePath(fromPath);
const toWasmPath = this.resolvePath(toPath);
writeWasmFileReplacingPath(module, fromWasmPath, data);
writeWasmFileReplacingPath(module, toWasmPath, data);
return data;
},
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes a
// temporary filename + ".new" and renames it over the parameter file.
async moveFile(fromPath, toPath) {
assertRelativePath(fromPath);
assertRelativePath(toPath);
if (fromPath === toPath) {
return;
}
const data = await readOpfsFile(workspacePath, fromPath);
await writeOpfsFile(workspacePath, toPath, data);
await removeOpfsEntry(workspacePath, fromPath, false);
const fromWasmPath = this.resolvePath(fromPath);
const toWasmPath = this.resolvePath(toPath);
writeWasmFileReplacingPath(module, toWasmPath, data);
removeWasmPath(module, fromWasmPath, false);
},
async exists(path) {
return opfsEntryExists(workspacePath, path);
},
async stat(path) {
return statOpfsEntry(workspacePath, path);
},
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the
// configured parameter file, and save_parameters() writes filename + ".new"
// before replacing the main file and managing filename + ".bak".
async loadParameterFile(path) {
assertRelativePath(path);
try {
const data = await readOpfsFile(workspacePath, path);
writeWasmFileReplacingPath(module, LINUXCNC_DEFAULT_PARAMETER_FILE, data);
writeWasmFileReplacingPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, data);
return data;
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
cleanupLinuxCncParameterFiles(module);
return null;
}
},
async persistParameterFile(path) {
assertRelativePath(path);
const data = readFirstExistingWasmFile(module, [
LINUXCNC_DEFAULT_PARAMETER_FILE,
`/${LINUXCNC_DEFAULT_PARAMETER_FILE}`,
]);
if (data === null) {
throw new Error("LinuxCNC parameter file was not produced by the interpreter");
}
await writeOpfsFile(workspacePath, path, data);
await removeOpfsEntry(workspacePath, `${path}.new`, false);
removeWasmPath(module, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false);
removeWasmPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, false);
const backupPath = `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`;
const backupData = readFirstExistingWasmFile(module, [backupPath, `/${backupPath}`]);
if (backupData !== null) {
await writeOpfsFile(workspacePath, `${path}.bak`, backupData);
} else {
await removeOpfsEntry(workspacePath, `${path}.bak`, false);
}
return data;
},
async removeFile(path) {
await removeOpfsEntry(workspacePath, path, false);
removeWasmPath(module, this.resolvePath(path), false);
},
async removeDirectory(path) {
await removeOpfsEntry(workspacePath, path, true);
removeWasmPath(module, this.resolvePath(path), true);
},
async clear() {
await clearOpfsWorkspace(workspacePath);
for (const entry of module.FS.readdir(mountPoint)) {
if (entry !== "." && entry !== "..") {
removeWasmPath(module, `${mountPoint}/${entry}`, true);
}
}
},
};
}
function readPose(view, offset) {
return {
x: view.getFloat64(offset + 0, true),
@@ -82,12 +562,21 @@ async function loadModuleFactory() {
}
export async function createWasmSimulator(moduleOptions = {}) {
if (moduleOptions.opfs === false && isOpfsAvailable()) {
throw new Error("OPFS cannot be disabled in browser contexts with OPFS support");
}
const hasOpfsOptions = Object.prototype.hasOwnProperty.call(moduleOptions, "opfs");
const emscriptenModuleOptions = hasOpfsOptions
? (({ opfs: _opfs, ...rest }) => rest)(moduleOptions)
: moduleOptions;
const createModule = await loadModuleFactory();
if (!createModule) {
throw new Error("cnc_sim.js did not export createCncSimModule");
}
const module = await createModule(moduleOptions);
const module = hasOpfsOptions ? await createModule(emscriptenModuleOptions) : await createModule(moduleOptions);
const opfs = installOpfsWorkspace(module, moduleOptions);
const create = module.cwrap("cnc_sim_create", "number", []);
const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]);
const reset = module.cwrap("cnc_sim_reset", null, ["number"]);
@@ -100,9 +589,9 @@ export async function createWasmSimulator(moduleOptions = {}) {
const handle = create();
let callbackPtr = 0;
return {
parse(program, dialect = "linuxcnc", options = {}) {
const parseText = (program, dialect = "linuxcnc", options = {}, parseOptions = {}) => {
const events = [];
try {
reset(handle);
setDialect(handle, DIALECT[dialect] ?? DIALECT.linuxcnc);
@@ -117,8 +606,19 @@ export async function createWasmSimulator(moduleOptions = {}) {
...(options.config ? { config: options.config } : {}),
...(options.configPath ? { configPath: options.configPath } : {}),
...(options.config_path ? { config_path: options.config_path } : {}),
...(options.ini ? { ini: options.ini } : {}),
...(options.iniFile ? { iniFile: options.iniFile } : {}),
...(options.iniFileName ? { iniFileName: options.iniFileName } : {}),
...(options.ini_file ? { ini_file: options.ini_file } : {}),
...(options.ini_file_name ? { ini_file_name: options.ini_file_name } : {}),
...(options.INI_FILE_NAME ? { INI_FILE_NAME: options.INI_FILE_NAME } : {}),
...(options.halFile ? { halFile: options.halFile } : {}),
...(options.halfile ? { halfile: options.halfile } : {}),
...(options.hal_file ? { hal_file: options.hal_file } : {}),
...(options.postguiHalFile ? { postguiHalFile: options.postguiHalFile } : {}),
...(options.postgui_halfile ? { postgui_halfile: options.postgui_halfile } : {}),
...(options.postgui_hal_file ? { postgui_hal_file: options.postgui_hal_file } : {}),
...(options.POSTGUI_HALFILE ? { POSTGUI_HALFILE: options.POSTGUI_HALFILE } : {}),
...(options.kinematics ? { kinematics: options.kinematics } : {}),
...(options.trt ? { trt: options.trt } : {}),
...(options.pivotLength !== undefined ? { pivotLength: options.pivotLength } : {}),
@@ -135,9 +635,13 @@ export async function createWasmSimulator(moduleOptions = {}) {
});
const configBytes = module.lengthBytesUTF8(config) + 1;
const configPtr = module._malloc(configBytes);
let configRc;
try {
module.stringToUTF8(config, configPtr, configBytes);
const configRc = loadConfig(handle, configPtr, configBytes - 1);
configRc = loadConfig(handle, configPtr, configBytes - 1);
} finally {
module._free(configPtr);
}
if (configRc !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
@@ -153,14 +657,59 @@ export async function createWasmSimulator(moduleOptions = {}) {
const bytes = module.lengthBytesUTF8(program) + 1;
const ptr = module._malloc(bytes);
let rc;
try {
module.stringToUTF8(program, ptr, bytes);
const rc = parseProgram(handle, ptr, bytes - 1);
rc = parseProgram(handle, ptr, bytes - 1);
} finally {
module._free(ptr);
}
if (rc !== 0) {
throw new Error(module.UTF8ToString(lastError(handle)));
}
return events;
} finally {
if (opfs && !parseOptions.keepLinuxCncParameterFiles) {
cleanupLinuxCncParameterFiles(module);
}
}
};
return {
parse(program, dialect = "linuxcnc", options = {}) {
return parseText(program, dialect, options);
},
async parseFile(path, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for program file parsing");
}
const program = new TextDecoder().decode(await opfs.readFile(path));
return parseText(program, dialect, options);
},
async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for parameter file persistence");
}
cleanupLinuxCncParameterFiles(module);
await opfs.loadParameterFile(parameterPath);
try {
const events = parseText(program, dialect, options, { keepLinuxCncParameterFiles: true });
await opfs.persistParameterFile(parameterPath);
return events;
} finally {
cleanupLinuxCncParameterFiles(module);
}
},
async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {}) {
if (!opfs) {
throw new Error("OPFS workspace is not available for program and parameter file parsing");
}
const program = new TextDecoder().decode(await opfs.readFile(path));
return this.parseWithParameterFile(program, parameterPath, dialect, options);
},
dispose() {
@@ -170,5 +719,10 @@ export async function createWasmSimulator(moduleOptions = {}) {
}
destroy(handle);
},
fs: {
opfs,
module,
},
};
}

View File

@@ -0,0 +1,66 @@
export async function runBrowserAppOpfsSections(context) {
const {
browserHasOpfs,
createHiddenAppFrame,
disposeAppFrame,
expectIncludes,
expectText,
getAppFrameSimulator,
parseAppFrameProgram,
runSection,
waitFor,
waitForAppAlarm,
waitForAppControls,
} = context;
if (!browserHasOpfs()) {
return;
}
await runSection("browser app opfs save", async () => {
const appFrame = await createHiddenAppFrame();
try {
await waitForAppControls(appFrame, "app frame did not load CNC controls");
parseAppFrameProgram(appFrame, "G21 G90\nG0 X1\nG28.1\nM30\n");
await waitForAppAlarm(appFrame, "OK ", "app frame did not parse program");
const appSimulator = await getAppFrameSimulator(appFrame, "app frame did not create OPFS simulator workspace");
const savedProgram = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("programs/current.ngc"));
expectText(savedProgram, "G21 G90\nG0 X1\nG28.1\nM30\n",
"app frame did not persist current program into OPFS");
const savedParameterFile = new TextDecoder().decode(await appSimulator.fs.opfs.readFile("parameters/rs274ngc.var"));
expectIncludes(savedParameterFile, "5161\t1.000000",
"app frame did not persist LinuxCNC parameter state into OPFS");
} finally {
await disposeAppFrame(appFrame);
}
});
await runSection("browser app opfs restore", async () => {
const restoredAppFrame = await createHiddenAppFrame();
try {
await waitForAppControls(restoredAppFrame, "restored app frame did not load CNC controls");
await waitFor(() => {
const restoredInput = restoredAppFrame.contentDocument?.querySelector("#programInput");
return restoredInput?.value === "G21 G90\nG0 X1\nG28.1\nM30\n";
}, "app frame did not restore current program from OPFS after reload");
await waitForAppAlarm(
restoredAppFrame,
"OPFS PROGRAM RESTORED",
"app frame did not report OPFS program restore after reload",
);
await getAppFrameSimulator(restoredAppFrame, "restored app frame did not create OPFS simulator workspace");
parseAppFrameProgram(
restoredAppFrame,
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X12\nO10 endif\nM30\n",
);
await waitForAppAlarm(
restoredAppFrame,
"OK ",
"restored app frame did not parse program with reloaded parameter state",
);
await waitFor(
() => restoredAppFrame.contentDocument?.querySelector("#axisX")?.value === "12.000",
"app frame did not reload LinuxCNC parameter state from OPFS after reload",
);
} finally {
await disposeAppFrame(restoredAppFrame);
}
});
}

View File

@@ -0,0 +1,176 @@
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const linuxCncParameterScratchFiles = ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"];
export function createBrowserSmokeHarness(result) {
const sectionResults = [];
const near = (actual, expected) => Math.abs(actual - expected) < 1e-6;
const waitFor = async (predicate, message) => {
for (let i = 0; i < 50; i += 1) {
if (predicate()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(message);
};
const runSection = async (name, action) => {
const sectionNumber = sectionResults.length + 1;
result.textContent = `running: ${sectionNumber}: ${name}`;
try {
await action();
sectionResults.push(name);
} catch (error) {
const detail = error && error.stack ? error.stack : error;
throw new Error(`section "${name}" failed: ${detail}`);
}
};
const completedMessage = () => `browser wasm smoke passed (${sectionResults.length} sections)`;
const browserHasOpfs = () => navigator.storage && navigator.storage.getDirectory;
const expectEvent = (events, predicate, message) => {
if (!events.some(predicate)) {
throw new Error(message);
}
};
const expectNoEvent = (events, predicate, message) => {
if (events.some(predicate)) {
throw new Error(message);
}
};
const expectText = (actual, expected, message) => {
if (actual !== expected) {
throw new Error(message);
}
};
const expectIncludes = (actual, expected, message) => {
if (!actual.includes(expected)) {
throw new Error(message);
}
};
const expectFileList = (actual, expected, message) => {
if (
actual.length !== expected.length ||
expected.some((entry) => !actual.includes(entry))
) {
throw new Error(`${message}: ${actual.join(",")}`);
}
};
const expectErrorContaining = async (action, expectedText, message) => {
try {
await action();
throw new Error(message);
} catch (error) {
if (!String(error?.message ?? error).includes(expectedText)) {
throw error;
}
}
};
const expectMissingOpfsFile = async (action, message) => {
try {
await action();
throw new Error(message);
} catch (error) {
if (error?.name !== "NotFoundError") {
throw error;
}
}
};
const expectMissingWasmPath = (action, message) => {
try {
action();
throw new Error(message);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
};
const expectWasmFilesMissing = (fs, paths, messageForPath) => {
for (const wasmPath of paths) {
expectMissingWasmPath(
() => fs.readFile(wasmPath),
messageForPath(wasmPath),
);
}
};
const decodeBytes = (bytes) => textDecoder.decode(bytes);
const encodeText = (text) => textEncoder.encode(text);
const readOpfsText = async (simulator, path) => decodeBytes(await simulator.fs.opfs.readFile(path));
const readWasmText = (simulator, path) => decodeBytes(simulator.fs.module.FS.readFile(path));
const writeWasmText = (simulator, path, text) => simulator.fs.module.FS.writeFile(path, encodeText(text));
const removeWasmPathIfExists = (simulator, path) => {
try {
simulator.fs.module.FS.unlink(path);
} catch (error) {
if (error?.errno !== 44) {
throw error;
}
}
};
const expectOpfsText = async (simulator, path, expected, message) => {
expectText(await readOpfsText(simulator, path), expected, message);
};
const expectWasmText = (simulator, path, expected, message) => {
expectText(readWasmText(simulator, path), expected, message);
};
const expectOpfsExists = async (simulator, path, message) => {
if (!(await simulator.fs.opfs.exists(path))) {
throw new Error(message);
}
};
const expectOpfsMissing = async (simulator, path, message) => {
if (await simulator.fs.opfs.exists(path)) {
throw new Error(message);
}
};
const expectOpfsFileStat = async (simulator, path, expectedSize, message) => {
const stat = await simulator.fs.opfs.stat(path);
if (stat.kind !== "file" || stat.size !== expectedSize) {
throw new Error(message);
}
};
const expectOpfsDirectoryStat = async (simulator, path, message) => {
const stat = await simulator.fs.opfs.stat(path);
if (stat.kind !== "directory" || stat.size !== null) {
throw new Error(message);
}
};
const expectLinuxCncParameterScratchMissing = (simulator, messageForPath) => {
for (const parameterPath of linuxCncParameterScratchFiles) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
messageForPath(parameterPath),
);
}
};
return {
browserHasOpfs,
completedMessage,
decodeBytes,
encodeText,
expectErrorContaining,
expectEvent,
expectFileList,
expectIncludes,
expectLinuxCncParameterScratchMissing,
expectMissingOpfsFile,
expectMissingWasmPath,
expectNoEvent,
expectOpfsDirectoryStat,
expectOpfsExists,
expectOpfsFileStat,
expectOpfsMissing,
expectOpfsText,
expectText,
expectWasmFilesMissing,
expectWasmText,
near,
readOpfsText,
readWasmText,
removeWasmPathIfExists,
runSection,
waitFor,
writeWasmText,
};
}

View File

@@ -0,0 +1,72 @@
export async function runLinuxCncBrowserSections(context, simulator) {
const {
expectEvent,
expectMissingWasmPath,
near,
runSection,
} = context;
await runSection("linuxcnc browser basic parse", async () => {
const events = simulator.parse("G21 G90\nG0 X0\nG1 X5 F100\nM30\n", "linuxcnc", {
backend: "linuxcnc-rs274",
});
expectEvent(events, (event) => event.type === "linear-feed" && near(event.end.x, 5),
"missing expected LinuxCNC WASM browser linear-feed event");
expectEvent(events, (event) => event.type === "program-end",
"missing expected LinuxCNC WASM browser program-end event");
});
let configCases = [];
await runSection("linuxcnc generated switchkins cases load", async () => {
const configCaseResponse = await fetch("/public/linuxcnc_switchkins_remap_config_cases.json");
if (!configCaseResponse.ok) {
throw new Error(`failed to load LinuxCNC switchkins config cases: ${configCaseResponse.status}`);
}
configCases = await configCaseResponse.json();
if (!Array.isArray(configCases) || configCases.length === 0) {
throw new Error("missing LinuxCNC switchkins config cases");
}
});
for (const configCase of configCases) {
await runSection(`linuxcnc generated switchkins ${configCase.field}=${configCase.value}`, async () => {
const options = {
backend: "linuxcnc-rs274",
[configCase.field]: configCase.value,
};
const program = configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n";
const configEvents = simulator.parse(program, "linuxcnc", options);
for (const [line, kinstype] of [
[1, configCase.m428],
[2, configCase.m429],
...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []),
]) {
expectEvent(configEvents,
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
`missing LinuxCNC WASM browser generated ${configCase.field}=${configCase.value} line ${line} switchkins type ${kinstype}`,
);
}
});
}
await runSection("linuxcnc browser explicit scara switchkins", async () => {
const switchEvents = simulator.parse("M428\nM429\nM430\n", "linuxcnc", {
backend: "linuxcnc-rs274",
switchkins: "scara",
});
for (const [line, kinstype] of [
[1, 0],
[2, 1],
[3, 2],
]) {
expectEvent(switchEvents,
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
`missing LinuxCNC WASM browser M${427 + line} switchkins type ${kinstype}`,
);
}
});
await runSection("linuxcnc browser parse parameter scratch cleanup", async () => {
for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
`browser parse left LinuxCNC parameter state outside OPFS: ${parameterPath}`,
);
}
});
}

View File

@@ -0,0 +1,114 @@
export async function runBrowserOpfsBasicSections(context, simulator) {
const {
expectErrorContaining,
expectEvent,
expectMissingWasmPath,
expectText,
near,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs workspace clear and healthcheck", async () => {
if (!simulator.fs.opfs) {
throw new Error("expected OPFS workspace to be available in browser smoke");
}
await simulator.fs.opfs.clear();
if (simulator.fs.opfs.workspacePath !== "cnc-simulator-smoke") {
throw new Error("unexpected OPFS workspace path");
}
await simulator.fs.opfs.writeFile("healthcheck.ngc", "G21 G90\n");
if (!(await simulator.fs.opfs.exists("healthcheck.ngc"))) {
throw new Error("OPFS exists did not find a persisted file");
}
if (await simulator.fs.opfs.exists("missing-healthcheck.ngc")) {
throw new Error("OPFS exists reported a missing file");
}
const healthcheckStat = await simulator.fs.opfs.stat("healthcheck.ngc");
if (healthcheckStat.kind !== "file" || healthcheckStat.size !== 8) {
throw new Error("OPFS stat did not report the persisted healthcheck file size");
}
const clearCheck = new TextDecoder().decode(await simulator.fs.opfs.readFile("healthcheck.ngc"));
expectText(clearCheck, "G21 G90\n", "OPFS workspace did not survive clear/write round trip");
});
await runSection("opfs default mount check", async () => {
await withSmokeSimulator((defaultMountSimulator) => {
if (!defaultMountSimulator.fs.opfs || defaultMountSimulator.fs.opfs.mountPoint !== "/cnc") {
throw new Error("default OPFS mount point was not preserved");
}
});
});
await runSection("opfs program write and read", async () => {
const encoded = new TextEncoder().encode("G21 G90\nG0 X1\n");
await simulator.fs.opfs.writeFile("programs/browser-smoke.ngc", encoded);
const decoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("programs/browser-smoke.ngc"),
);
expectText(decoded, "G21 G90\nG0 X1\n", "OPFS round trip did not preserve browser smoke program");
});
await runSection("opfs unsafe path rejection", async () => {
for (const unsafePath of ["../escape.ngc", "programs/../escape.ngc", "/escape.ngc", "programs//escape.ngc"]) {
await expectErrorContaining(
() => simulator.fs.opfs.writeFile(unsafePath, "G21\n"),
"inside the CNC workspace",
`OPFS accepted unsafe path ${unsafePath}`,
);
}
for (const [operation, action] of [
["readFile", () => simulator.fs.opfs.readFile("../escape.ngc")],
["readDirectory", () => simulator.fs.opfs.readDirectory("../escape")],
["persistFile", () => simulator.fs.opfs.persistFile("../escape.ngc")],
["persistDirectory", () => simulator.fs.opfs.persistDirectory("../escape")],
["copyFile", () => simulator.fs.opfs.copyFile("../escape.ngc", "programs/escape.ngc")],
["copyFileTarget", () => simulator.fs.opfs.copyFile("healthcheck.ngc", "../escape.ngc")],
["exists", () => simulator.fs.opfs.exists("../escape.ngc")],
["stat", () => simulator.fs.opfs.stat("../escape.ngc")],
["moveFile", () => simulator.fs.opfs.moveFile("../escape.ngc", "programs/escape.ngc")],
["moveFileTarget", () => simulator.fs.opfs.moveFile("healthcheck.ngc", "../escape.ngc")],
["removeFile", () => simulator.fs.opfs.removeFile("../escape.ngc")],
["removeDirectory", () => simulator.fs.opfs.removeDirectory("../escape")],
["parseFile", () => simulator.parseFile("../escape.ngc", "linuxcnc", { backend: "linuxcnc-rs274" })],
[
"parseWithParameterFile",
() =>
simulator.parseWithParameterFile("G21 G90\nM30\n", "../escape.var", "linuxcnc", {
backend: "linuxcnc-rs274",
}),
],
]) {
await expectErrorContaining(
action,
"inside the CNC workspace",
`OPFS ${operation} accepted an unsafe path`,
);
}
});
await runSection("opfs program parse and restore", async () => {
const opfsProgramEvents = await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc", {
backend: "linuxcnc-rs274",
});
expectEvent(opfsProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
"OPFS program file was not parsed through LinuxCNC WASM");
for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) {
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(parameterPath),
`browser parseFile left LinuxCNC parameter state outside OPFS: ${parameterPath}`,
);
}
await withSmokeSimulator(async (restoredProgramSimulator) => {
// LinuxCNC source basis: rs274ngc_pre.cc read_text() reads
// blocks from the program stream after the OPFS mirror is loaded.
const restoredProgramEvents = await restoredProgramSimulator.parseFile(
"programs/browser-smoke.ngc",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
"LinuxCNC OPFS program file was not restored across WASM simulator instances");
});
});
}

View File

@@ -0,0 +1,174 @@
export async function runBrowserOpfsDirectorySections(context, simulator) {
const {
expectFileList,
expectMissingOpfsFile,
expectMissingWasmPath,
expectText,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs directory persist and load", async () => {
const fixturePath = simulator.fs.opfs.resolvePath("fixtures");
simulator.fs.module.FS.mkdirTree(`${fixturePath}/nested`);
simulator.fs.module.FS.mkdirTree(`${fixturePath}/empty/nested`);
simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n");
simulator.fs.module.FS.writeFile(`${fixturePath}/root.ngc`, "G21 G90\nG0 X4\n");
const persistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
const fixtureStat = await simulator.fs.opfs.stat("fixtures");
if (fixtureStat.kind !== "directory" || fixtureStat.size !== null) {
throw new Error("OPFS stat did not report a persisted directory");
}
expectFileList(persistedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected OPFS directory persist file list");
const fixtureDecoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"),
);
expectText(fixtureDecoded, "G21 G90\nG0 X3\n",
"persisted WASM filesystem directory file was not readable from OPFS");
const emptyFixtureStat = await simulator.fs.opfs.stat("fixtures/empty/nested");
if (emptyFixtureStat.kind !== "directory" || emptyFixtureStat.size !== null) {
throw new Error("OPFS persistDirectory did not preserve an empty directory");
}
await withSmokeSimulator(async (staleDirectorySimulator) => {
await staleDirectorySimulator.fs.opfs.writeFile("fixtures/stale.ngc", "G21 G90\nG0 X6\n");
});
simulator.fs.module.FS.unlink(`${fixturePath}/nested/fixture.ngc`);
const refreshedPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
expectFileList(refreshedPersistedFiles, ["root.ngc"],
"unexpected refreshed OPFS directory persist file list");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("fixtures/stale.ngc"),
"stale OPFS directory file remained after directory persist",
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("fixtures/nested/fixture.ngc"),
"removed WASM directory file remained in OPFS after directory persist",
);
simulator.fs.module.FS.writeFile(`${fixturePath}/nested/fixture.ngc`, "G21 G90\nG0 X3\n");
const restoredPersistedFiles = await simulator.fs.opfs.persistDirectory("fixtures");
expectFileList(restoredPersistedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected restored OPFS directory persist file list");
const loadedFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(loadedFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected OPFS directory load file list");
const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc");
const loadedRootDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(loadedRootPath));
expectText(loadedRootDecoded, "G21 G90\nG0 X4\n",
"OPFS directory file was not mirrored into the WASM filesystem");
const loadedEmptyPath = simulator.fs.opfs.resolvePath("fixtures/empty/nested");
const loadedEmptyMode = simulator.fs.module.FS.stat(loadedEmptyPath).mode;
if (!simulator.fs.module.FS.isDir(loadedEmptyMode)) {
throw new Error("OPFS readDirectory did not mirror an empty directory into WASM FS");
}
});
await runSection("opfs directory refresh and type conflicts", async () => {
const loadedRootPath = simulator.fs.opfs.resolvePath("fixtures/root.ngc");
await withSmokeSimulator(async (updateDirectorySimulator) => {
await updateDirectorySimulator.fs.opfs.removeFile("fixtures/root.ngc");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X5\n");
const refreshedFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(refreshedFiles, ["nested/fixture.ngc"],
"unexpected refreshed OPFS directory file list");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(loadedRootPath),
"stale OPFS directory mirror file remained in WASM FS",
);
const refreshedFixturePath = simulator.fs.opfs.resolvePath("fixtures/nested/fixture.ngc");
const refreshedFixture = new TextDecoder().decode(
simulator.fs.module.FS.readFile(refreshedFixturePath),
);
expectText(refreshedFixture, "G21 G90\nG0 X5\n",
"OPFS directory refresh did not replace the WASM mirror file");
await simulator.fs.opfs.writeFile("fixtures/type-conflict/original.ngc", "G21 G90\n");
await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/type-conflict", "G21 G90\nG0 X11\n");
const replacedConflictFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("fixtures/type-conflict"),
);
expectText(replacedConflictFile, "G21 G90\nG0 X11\n",
"OPFS readFile did not load a file that replaced a directory");
const conflictMirrorPath = simulator.fs.opfs.resolvePath("fixtures/type-conflict");
const conflictMirrorMode = simulator.fs.module.FS.stat(conflictMirrorPath).mode;
if (!simulator.fs.module.FS.isFile(conflictMirrorMode)) {
throw new Error("OPFS readFile did not replace the WASM mirror directory with a file");
}
await updateDirectorySimulator.fs.opfs.removeFile("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile(
"fixtures/type-conflict/original.ngc",
"G21 G90\n",
);
await updateDirectorySimulator.fs.opfs.removeDirectory("fixtures/type-conflict");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/root.ngc", "G21 G90\nG0 X4\n");
await updateDirectorySimulator.fs.opfs.writeFile("fixtures/nested/fixture.ngc", "G21 G90\nG0 X3\n");
const restoredFiles = await simulator.fs.opfs.readDirectory("fixtures");
expectFileList(restoredFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected reloaded OPFS directory file list");
});
});
await runSection("opfs directory restore in new instance", async () => {
await withSmokeSimulator(async (restoredDirectorySimulator) => {
const restoredDirectoryFiles = await restoredDirectorySimulator.fs.opfs.readDirectory("fixtures");
expectFileList(restoredDirectoryFiles, ["nested/fixture.ngc", "root.ngc"],
"unexpected restored OPFS directory file list");
const restoredFixtureRootPath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/root.ngc");
const restoredFixtureRoot = new TextDecoder().decode(
restoredDirectorySimulator.fs.module.FS.readFile(restoredFixtureRootPath),
);
expectText(restoredFixtureRoot, "G21 G90\nG0 X4\n",
"OPFS directory was not restored into a new WASM filesystem instance");
const restoredEmptyFixturePath = restoredDirectorySimulator.fs.opfs.resolvePath("fixtures/empty/nested");
const restoredEmptyFixtureMode =
restoredDirectorySimulator.fs.module.FS.stat(restoredEmptyFixturePath).mode;
if (!restoredDirectorySimulator.fs.module.FS.isDir(restoredEmptyFixtureMode)) {
throw new Error("OPFS empty directory was not restored into a new WASM filesystem instance");
}
});
});
await runSection("opfs metadata queries avoid wasm mirror pollution", async () => {
await withSmokeSimulator(async (existsOnlySimulator) => {
await existsOnlySimulator.fs.opfs.writeFile("programs/exists-only.ngc", "G21 G90\nG0 X10\n");
const existsOnlyPath = simulator.fs.opfs.resolvePath("programs/exists-only.ngc");
if (!(await simulator.fs.opfs.exists("programs/exists-only.ngc"))) {
throw new Error("OPFS exists did not see a file written by another simulator instance");
}
const existsOnlyStat = await simulator.fs.opfs.stat("programs/exists-only.ngc");
if (existsOnlyStat.kind !== "file" || existsOnlyStat.size !== 15) {
throw new Error("OPFS stat did not see a file written by another simulator instance");
}
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(existsOnlyPath),
"OPFS metadata queries polluted the WASM filesystem mirror",
);
});
});
await runSection("opfs workspace clear and mirror removal", async () => {
await simulator.fs.opfs.clear();
await withSmokeSimulator(async (clearedWorkspaceSimulator) => {
await expectMissingOpfsFile(
() => clearedWorkspaceSimulator.fs.opfs.readDirectory("fixtures"),
"cleared OPFS workspace still exposed a persisted directory",
);
});
await simulator.fs.opfs.removeFile("programs/browser-smoke.ngc");
if (await simulator.fs.opfs.exists("programs/browser-smoke.ngc")) {
throw new Error("OPFS exists reported a removed file");
}
const removedProgramPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(removedProgramPath),
"OPFS removeFile did not remove the WASM mirror",
);
const fixturePath = simulator.fs.opfs.resolvePath("fixtures");
await simulator.fs.opfs.removeDirectory("fixtures");
await simulator.fs.opfs.removeDirectory("fixtures");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(fixturePath),
"OPFS removeDirectory did not remove the WASM mirror",
);
});
}

View File

@@ -0,0 +1,76 @@
export async function runBrowserOpfsMirrorSections(context, simulator) {
const {
expectMissingWasmPath,
expectText,
runSection,
} = context;
await runSection("opfs wasm mirror persist file", async () => {
const wasmPath = simulator.fs.opfs.resolvePath("programs/browser-smoke.ngc");
const wasmDecoded = new TextDecoder().decode(simulator.fs.module.FS.readFile(wasmPath));
expectText(wasmDecoded, "G21 G90\nG0 X1\n", "OPFS file was not mirrored into the WASM filesystem");
const persistedPath = simulator.fs.opfs.resolvePath("programs/browser-persisted.ngc");
simulator.fs.module.FS.writeFile(persistedPath, new TextEncoder().encode("G21 G90\nG0 X2\n"));
const persisted = new TextDecoder().decode(await simulator.fs.opfs.persistFile("programs/browser-persisted.ngc"));
expectText(persisted, "G21 G90\nG0 X2\n", "WASM filesystem file was not persisted into OPFS");
const persistedDecoded = new TextDecoder().decode(
await simulator.fs.opfs.readFile("programs/browser-persisted.ngc"),
);
expectText(persistedDecoded, "G21 G90\nG0 X2\n",
"persisted WASM filesystem file was not readable from OPFS");
});
await runSection("opfs copy and move mirror files", async () => {
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// links the current parameter file to filename + ".bak", then
// writes filename + ".new" and renames it over the parameter file.
await simulator.fs.opfs.writeFile("parameters/move-main.var", "5161\t1.000000\n");
const copiedParameterBackup = new TextDecoder().decode(
await simulator.fs.opfs.copyFile("parameters/move-main.var", "parameters/move-main.var.bak"),
);
expectText(copiedParameterBackup, "5161\t1.000000\n",
"OPFS copyFile did not return the copied file contents");
const copiedParameterBackupFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/move-main.var.bak"),
);
expectText(copiedParameterBackupFile, "5161\t1.000000\n",
"OPFS copyFile did not create the backup file");
const copiedParameterBackupWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.bak");
const copiedParameterBackupWasm = new TextDecoder().decode(
simulator.fs.module.FS.readFile(copiedParameterBackupWasmPath),
);
expectText(copiedParameterBackupWasm, "5161\t1.000000\n",
"OPFS copyFile did not update the WASM mirror destination");
await simulator.fs.opfs.writeFile("parameters/move-main.var.new", "5161\t2.000000\n");
await simulator.fs.opfs.moveFile("parameters/move-main.var.new", "parameters/move-main.var");
const movedParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/move-main.var"),
);
expectText(movedParameterFile, "5161\t2.000000\n",
"OPFS moveFile did not replace the destination file");
if (await simulator.fs.opfs.exists("parameters/move-main.var.new")) {
throw new Error("OPFS moveFile left the source file behind");
}
const movedParameterWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var");
const movedParameterWasm = new TextDecoder().decode(
simulator.fs.module.FS.readFile(movedParameterWasmPath),
);
expectText(movedParameterWasm, "5161\t2.000000\n",
"OPFS moveFile did not update the WASM mirror destination");
const movedTemporaryWasmPath = simulator.fs.opfs.resolvePath("parameters/move-main.var.new");
expectMissingWasmPath(
() => simulator.fs.module.FS.stat(movedTemporaryWasmPath),
"OPFS moveFile left the WASM mirror source behind",
);
});
await runSection("opfs write truncates wasm mirror", async () => {
await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21 G90\nG0 X12345\nM30\n");
await simulator.fs.opfs.writeFile("programs/truncate.ngc", "G21\n");
const truncatedProgram = new TextDecoder().decode(await simulator.fs.opfs.readFile("programs/truncate.ngc"));
expectText(truncatedProgram, "G21\n", "OPFS writeFile did not truncate stale program bytes");
const truncatedWasmPath = simulator.fs.opfs.resolvePath("programs/truncate.ngc");
const truncatedWasmProgram = new TextDecoder().decode(simulator.fs.module.FS.readFile(truncatedWasmPath));
expectText(truncatedWasmProgram, "G21\n", "OPFS writeFile did not truncate the WASM mirror");
});
}

View File

@@ -0,0 +1,255 @@
export async function runBrowserOpfsParameterSections(context, simulator) {
const {
expectErrorContaining,
expectEvent,
expectIncludes,
expectMissingOpfsFile,
expectMissingWasmPath,
expectNoEvent,
expectText,
expectWasmFilesMissing,
near,
removeWasmPathIfExists,
runSection,
withSmokeSimulator,
} = context;
await runSection("opfs parameter save", async () => {
await simulator.parseWithParameterFile(
"G21 G90\nG0 X1\nG28.1\nM30\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
const opfsParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/rs274ngc.var"),
);
expectIncludes(opfsParameterFile, "5161\t1.000000", "LinuxCNC parameter file was not persisted into OPFS");
});
await runSection("opfs parameter restore in new instance", async () => {
await withSmokeSimulator(async (restoredWorkspaceSimulator) => {
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters()
// loads persisted numbered parameters before interpreting blocks.
const restoredWorkspaceEvents = await restoredWorkspaceSimulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X8\nO10 endif\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredWorkspaceEvents, (event) => event.type === "linear-feed" && near(event.end.x, 8),
"LinuxCNC OPFS parameter file was not restored across WASM simulator instances");
});
});
await runSection("opfs parameter temporary cleanup", async () => {
await simulator.fs.opfs.writeFile("parameters/rs274ngc.var.new", "stale temporary parameter file\n");
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes
// filename + ".new", then renames it over the parameter file.
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary parameter file\n");
const restoredParameterEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(restoredParameterEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC parameter file was not restored from OPFS");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/rs274ngc.var.new"),
"stale LinuxCNC temporary parameter file remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.new"),
"stale LinuxCNC temporary parameter file remained in WASM FS",
);
});
await runSection("opfs parameter backup bridge", async () => {
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// attempts link(filename, filename + ".bak") but treats link
// failure as non-fatal. The browser bridge must persist a backup
// when the WASM filesystem exposes one.
simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t2.000000\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "5161\t1.000000\n");
await simulator.fs.opfs.persistParameterFile("parameters/backup-bridge.var");
const opfsParameterBackup = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/backup-bridge.var.bak"),
);
expectIncludes(opfsParameterBackup, "5161\t1.000000",
"LinuxCNC parameter backup file was not bridged into OPFS");
});
await runSection("opfs parameter stale backup cleanup", async () => {
removeWasmPathIfExists(simulator, "rs274ngc.var.bak");
removeWasmPathIfExists(simulator, "/rs274ngc.var.bak");
await simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/rs274ngc.var.bak"),
"stale LinuxCNC parameter backup file remained in OPFS",
);
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters()
// unlinks stale backup files before replacing the parameter file.
await simulator.fs.opfs.removeFile("parameters/missing-cleanup/rs274ngc.var.bak");
});
await runSection("opfs program file with parameter persistence", async () => {
await simulator.fs.opfs.writeFile("programs/parameter-file-smoke.ngc", "G21 G90\nF100\nG0 X1\nG28.1\nM30\n");
const fileParameterEvents = await simulator.parseFileWithParameterFile(
"programs/parameter-file-smoke.ngc",
"parameters/rs274ngc.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(fileParameterEvents, (event) => event.type === "program-end",
"OPFS program file with LinuxCNC parameter persistence did not parse");
});
await runSection("opfs missing parameter file creation", async () => {
await simulator.fs.opfs.writeFile("parameters/missing-main.var.bak", "stale backup without main file\n");
await simulator.fs.opfs.writeFile("parameters/missing-main.var.new", "stale temporary without main file\n");
// LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks
// filename + RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX before linking,
// then renames filename + ".new" over the parameter file.
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale wasm backup without main file\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary without main file\n");
await simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/missing-main.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
const missingMainParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/missing-main.var"),
);
expectIncludes(missingMainParameterFile, "5220\t1.000000",
"LinuxCNC missing parameter file was not created through OPFS");
expectIncludes(missingMainParameterFile, "5161\t0.000000",
"LinuxCNC missing parameter file did not include required G28 parameter");
const missingMainRestoreEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5220 EQ 1]\nG1 X6\nO10 endif\n",
"parameters/missing-main.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(missingMainRestoreEvents, (event) => event.type === "linear-feed" && near(event.end.x, 6),
"LinuxCNC-created missing OPFS parameter file was not restored on the next parse");
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/missing-main.var.bak"),
"stale LinuxCNC missing-parameter backup remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.bak"),
"stale LinuxCNC missing-parameter backup remained in WASM FS",
);
await expectMissingOpfsFile(
() => simulator.fs.opfs.readFile("parameters/missing-main.var.new"),
"stale LinuxCNC missing-parameter temporary file remained in OPFS",
);
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var.new"),
"stale LinuxCNC missing-parameter temporary file remained in WASM FS",
);
});
await runSection("opfs invalid parameter restore failures", async () => {
await simulator.fs.opfs.writeFile("parameters/out-of-order.var", "5162\t1.000000\n5161\t1.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/out-of-order.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"Parameter file out of order",
"LinuxCNC accepted an out-of-order OPFS parameter file",
);
const outOfOrderParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/out-of-order.var"),
);
expectText(outOfOrderParameterFile, "5162\t1.000000\n5161\t1.000000\n",
"LinuxCNC persisted an out-of-order OPFS parameter file after restore failure");
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var"),
"LinuxCNC out-of-order parameter file remained in WASM FS after restore failure",
);
await simulator.fs.opfs.writeFile("parameters/out-of-range.var", "0\t1.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nM30\n",
"parameters/out-of-range.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"Parameter number out of range",
"LinuxCNC accepted an out-of-range OPFS parameter file",
);
const outOfRangeParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/out-of-range.var"),
);
expectText(outOfRangeParameterFile, "0\t1.000000\n",
"LinuxCNC persisted an out-of-range OPFS parameter file after restore failure");
expectMissingWasmPath(
() => simulator.fs.module.FS.readFile("rs274ngc.var"),
"LinuxCNC out-of-range parameter file remained in WASM FS after restore failure",
);
await simulator.fs.opfs.writeFile("parameters/failed-parse.var", "1\t0.000000\n");
await expectErrorContaining(
() => simulator.parseWithParameterFile(
"G21 G90\nG28.1\n#0 = 1\n",
"parameters/failed-parse.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
),
"",
"LinuxCNC accepted a failing parse that should not persist parameters",
);
const failedParseParameterFile = new TextDecoder().decode(
await simulator.fs.opfs.readFile("parameters/failed-parse.var"),
);
if (failedParseParameterFile.includes("5161\t1.000000")) {
throw new Error("LinuxCNC persisted failed parse parameter updates into OPFS");
}
});
await runSection("opfs stale parameter state cleanup", async () => {
await simulator.fs.opfs.writeFile("parameters/stale-state.var", "5161\t1.000000\n");
const staleStateEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/stale-state.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectEvent(staleStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC did not honor the loaded OPFS parameter state");
await simulator.fs.opfs.removeFile("parameters/stale-state.var");
const missingStateEvents = await simulator.parseWithParameterFile(
"G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n",
"parameters/stale-state.var",
"linuxcnc",
{ backend: "linuxcnc-rs274" },
);
expectNoEvent(missingStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9),
"LinuxCNC stale OPFS parameter state was not cleared when the file was missing");
// LinuxCNC source basis: rs274ngc_pre.cc restore_parameters()
// treats a missing parameter file as OK; the browser OPFS bridge
// must not leave stale default parameter files in the WASM mirror.
simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t1.000000\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale temporary parameter file\n");
simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale backup parameter file\n");
const missingLoad = await simulator.fs.opfs.loadParameterFile("parameters/load-missing.var");
if (missingLoad !== null) {
throw new Error("missing OPFS parameter file did not return null");
}
expectWasmFilesMissing(
simulator.fs.module.FS,
["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"],
(parameterPath) => `missing OPFS parameter load left stale WASM file ${parameterPath}`,
);
});
}

View File

@@ -0,0 +1,49 @@
export async function runBrowserOpfsPolicySections(context) {
const {
browserHasOpfs,
createDefaultBrowserSimulator,
createSmokeSimulator,
expectErrorContaining,
runSection,
} = context;
if (!browserHasOpfs()) {
return;
}
await runSection("browser opfs policy", async () => {
const defaultOpfsSimulator = await createDefaultBrowserSimulator();
try {
if (!defaultOpfsSimulator.fs.opfs) {
throw new Error("default browser simulator did not create an OPFS workspace");
}
if (defaultOpfsSimulator.fs.opfs.workspacePath !== "cnc-simulator") {
throw new Error("default browser simulator used an unexpected OPFS workspace path");
}
} finally {
defaultOpfsSimulator.dispose();
}
await expectErrorContaining(
() => createSmokeSimulator(false),
"OPFS cannot be disabled",
"browser simulator allowed OPFS to be disabled",
);
for (const unsafeWorkspacePath of ["../bad-workspace", "/bad-workspace", "bad//workspace"]) {
await expectErrorContaining(
() => createSmokeSimulator({ workspacePath: unsafeWorkspacePath }),
"inside the CNC workspace",
`browser simulator accepted unsafe OPFS workspace ${unsafeWorkspacePath}`,
);
}
for (const unsafeMountPoint of ["cnc", "/cnc/../escape", "/cnc//escape"]) {
await expectErrorContaining(
() => createSmokeSimulator({ mountPoint: unsafeMountPoint, workspacePath: "cnc-simulator-smoke" }),
"Emscripten filesystem",
`browser simulator accepted unsafe OPFS mount ${unsafeMountPoint}`,
);
}
await expectErrorContaining(
() => createSmokeSimulator({ mountPoint: "", workspacePath: "cnc-simulator-smoke" }),
"non-empty absolute path",
"browser simulator accepted empty OPFS mount",
);
});
}

View File

@@ -0,0 +1,14 @@
import { runBrowserOpfsBasicSections } from "/test-browser-wasm-smoke-opfs-basic-sections.js";
import { runBrowserOpfsDirectorySections } from "/test-browser-wasm-smoke-opfs-directory-sections.js";
import { runBrowserOpfsMirrorSections } from "/test-browser-wasm-smoke-opfs-mirror-sections.js";
import { runBrowserOpfsParameterSections } from "/test-browser-wasm-smoke-opfs-parameter-sections.js";
export async function runBrowserOpfsWorkspaceSections(context, simulator) {
if (!context.browserHasOpfs()) {
return;
}
await runBrowserOpfsBasicSections(context, simulator);
await runBrowserOpfsParameterSections(context, simulator);
await runBrowserOpfsMirrorSections(context, simulator);
await runBrowserOpfsDirectorySections(context, simulator);
}

View File

@@ -0,0 +1,99 @@
import { createBrowserSmokeHarness } from "/test-browser-wasm-smoke-helpers.js";
import { runBrowserAppOpfsSections } from "/test-browser-wasm-smoke-app-sections.js";
import { runLinuxCncBrowserSections } from "/test-browser-wasm-smoke-linuxcnc-sections.js";
import { runBrowserOpfsPolicySections } from "/test-browser-wasm-smoke-opfs-policy-sections.js";
import { runBrowserOpfsWorkspaceSections } from "/test-browser-wasm-smoke-opfs-workspace-sections.js";
export async function runBrowserWasmSmoke({ createWasmSimulator, result }) {
const harness = createBrowserSmokeHarness(result);
const createSmokeSimulator = (opfs = { mountPoint: "/cnc", workspacePath: "cnc-simulator-smoke" }) =>
createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
...(opfs === undefined ? {} : { opfs }),
});
const createDefaultBrowserSimulator = () =>
createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
});
const withSmokeSimulator = async (action, opfs) => {
const nestedSimulator = await createSmokeSimulator(opfs);
try {
return await action(nestedSimulator);
} finally {
nestedSimulator.dispose();
}
};
const createHiddenAppFrame = async () => {
const appFrame = document.createElement("iframe");
const appHtml = await (await fetch("/")).text();
appFrame.srcdoc = appHtml.replace(
"</head>",
'<script src="/public/cnc_sim.js"><\/script></head>',
);
appFrame.style.display = "none";
document.body.appendChild(appFrame);
await new Promise((resolve) => {
appFrame.addEventListener("load", resolve, { once: true });
});
return appFrame;
};
const waitForAppControls = (appFrame, message) =>
harness.waitFor(
() => appFrame.contentDocument?.querySelector("#parseBtn"),
message,
);
const parseAppFrameProgram = (appFrame, program) => {
const appInput = appFrame.contentDocument.querySelector("#programInput");
appInput.value = program;
appInput.dispatchEvent(new Event("input", { bubbles: true }));
appFrame.contentDocument.querySelector("#parseBtn").click();
};
const waitForAppAlarm = (appFrame, text, message) =>
harness.waitFor(
() => appFrame.contentDocument?.querySelector("#alarmList")?.textContent.includes(text),
message,
);
const getAppFrameSimulator = async (appFrame, message) => {
const appSimulator = await appFrame.contentWindow.__cncSimulatorForTest;
if (!appSimulator?.fs?.opfs) {
throw new Error(message);
}
return appSimulator;
};
const disposeAppFrame = async (appFrame) => {
const appSimulator = await appFrame.contentWindow?.__cncSimulatorForTest;
appSimulator?.dispose?.();
appFrame.remove();
};
const context = {
...harness,
createDefaultBrowserSimulator,
createHiddenAppFrame,
createSmokeSimulator,
disposeAppFrame,
getAppFrameSimulator,
parseAppFrameProgram,
waitForAppAlarm,
waitForAppControls,
withSmokeSimulator,
};
try {
const simulator = await createSmokeSimulator();
try {
await runLinuxCncBrowserSections(context, simulator);
await runBrowserOpfsWorkspaceSections(context, simulator);
await runBrowserOpfsPolicySections(context);
await runBrowserAppOpfsSections(context);
result.textContent = harness.completedMessage();
} finally {
simulator.dispose();
}
} catch (error) {
result.textContent = `browser wasm smoke failed: ${error && error.stack ? error.stack : error}`;
}
}

View File

@@ -6,82 +6,16 @@
</head>
<body>
<pre id="result">pending</pre>
<!-- smoke marker: browser wasm smoke passed -->
<script src="/public/cnc_sim.js"></script>
<script type="module">
import { createWasmSimulator } from "/src/wasm-core.js";
import { runBrowserWasmSmoke } from "/test-browser-wasm-smoke-sections.js";
const result = document.querySelector("#result");
const near = (actual, expected) => Math.abs(actual - expected) < 1e-6;
try {
const simulator = await createWasmSimulator({
locateFile: (file) => `/public/${file}`,
print: () => {},
printErr: () => {},
runBrowserWasmSmoke({
createWasmSimulator,
result: document.querySelector("#result"),
});
try {
const events = simulator.parse("G21 G90\nG0 X0\nG1 X5 F100\nM30\n", "linuxcnc", {
backend: "linuxcnc-rs274",
});
const sawFeed = events.some((event) => event.type === "linear-feed" && near(event.end.x, 5));
const sawEnd = events.some((event) => event.type === "program-end");
if (!sawFeed || !sawEnd) {
throw new Error("missing expected LinuxCNC WASM browser events");
}
const switchEvents = simulator.parse("M428\nM429\nM430\n", "linuxcnc", {
backend: "linuxcnc-rs274",
switchkins: "scara",
});
for (const [line, kinstype] of [
[1, 0],
[2, 1],
[3, 2],
]) {
const sawSwitch = switchEvents.some(
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
);
if (!sawSwitch) {
throw new Error(`missing LinuxCNC WASM browser M${427 + line} switchkins type ${kinstype}`);
}
}
const configCaseResponse = await fetch("/public/linuxcnc_switchkins_remap_config_cases.json");
if (!configCaseResponse.ok) {
throw new Error(`failed to load LinuxCNC switchkins config cases: ${configCaseResponse.status}`);
}
const configCases = await configCaseResponse.json();
if (!Array.isArray(configCases) || configCases.length === 0) {
throw new Error("missing LinuxCNC switchkins config cases");
}
for (const configCase of configCases) {
const options = {
backend: "linuxcnc-rs274",
[configCase.field]: configCase.value,
};
const program = configCase.m430 >= 0 ? "M428\nM429\nM430\n" : "M428\nM429\n";
const configEvents = simulator.parse(program, "linuxcnc", options);
for (const [line, kinstype] of [
[1, configCase.m428],
[2, configCase.m429],
...(configCase.m430 >= 0 ? [[3, configCase.m430]] : []),
]) {
const sawSwitch = configEvents.some(
(event) => event.type === "kinematics-switch" && event.line === line && event.reserved === kinstype,
);
if (!sawSwitch) {
throw new Error(
`missing LinuxCNC WASM browser generated ${configCase.field}=${configCase.value} line ${line} switchkins type ${kinstype}`,
);
}
}
}
result.textContent = "browser wasm smoke passed";
} finally {
simulator.dispose();
}
} catch (error) {
result.textContent = `browser wasm smoke failed: ${error && error.stack ? error.stack : error}`;
}
</script>
</body>
</html>