From a0810b859863e2b0161c31fd81b556a4f1870d3e Mon Sep 17 00:00:00 2001 From: cnc Date: Sat, 6 Jun 2026 22:26:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E8=BF=9BLinuxCNC=20WASM=E6=9C=80?= =?UTF-8?q?=E5=B0=8F=E8=A7=A3=E9=87=8A=E5=99=A8=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:新增独立 wasm-port 工作区,修复 native probes include 路径,最小解释器 harness 已可验证 G0 直线快速、G1 进给和 SET_FEED_RATE canonical event。检查:git diff --check 通过;wasm-port/tests/native/verify_native_probes.sh 通过。 --- wasm-port/.gitignore | 2 + wasm-port/AGENTS.md | 116 + wasm-port/README.md | 128 + wasm-port/SKILL.md | 143 + wasm-port/docs/porting-steps-standalone.md | 658 ++ .../runtime/core/include/canon_event_sink.hh | 12 + .../core/linuxcnc_wrap/linuxcnc_ini_probe.cpp | 32 + .../core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp | 21 + .../linuxcnc_interp_minimal_harness.cpp | 82 + .../linuxcnc_interp_minimal_runtime.cpp | 463 ++ .../linuxcnc_interp_state_probe.cpp | 18 + .../linuxcnc_namedparam_harness.cpp | 341 + .../linuxcnc_rs274_compile_probe.cpp | 5 + .../linuxcnc_runtime_state_stubs.cpp | 209 + .../core/shims/boost/python/detail/config.hpp | 1 + .../core/shims/boost/python/detail/prefix.hpp | 1 + .../shims/boost/python/detail/wrap_python.hpp | 4 + .../core/shims/boost/python/object_fwd.hpp | 9 + wasm-port/runtime/core/shims/config.h | 3 + wasm-port/runtime/core/shims/fmt/format.h | 7 + wasm-port/runtime/core/shims/nml_intf/emc.hh | 7 + .../core/shims/pythonplugin/python_plugin.hh | 13 + wasm-port/runtime/core/shims/rtapi.h | 67 + .../runtime/core/shims/tooldata/tooldata.hh | 5 + wasm-port/runtime/ui/ini-panel/app.js | 206 + wasm-port/runtime/ui/ini-panel/index.html | 189 + .../runtime/ui/ini-panel/linuxcnc_ini.js | 16 + .../runtime/ui/ini-panel/linuxcnc_ini.wasm | Bin 0 -> 347954 bytes .../tests/native/verify_native_probes.sh | 45 + wasm-port/tools/build_ini_panel.sh | 24 + wasm-port/tools/build_native_probes.sh | 162 + wasm-port/tools/extract_sources.sh | 35 + wasm-port/tools/serve_ini_panel.sh | 7 + wasm-port/tools/source-manifest.txt | 50 + .../vendor/linuxcnc/src/emc/ini/inifile.cc | 1674 +++++ .../vendor/linuxcnc/src/emc/ini/inifile.h | 61 + .../vendor/linuxcnc/src/emc/ini/inifile.hh | 504 ++ wasm-port/vendor/linuxcnc/src/emc/linuxcnc.h | 27 + .../linuxcnc/src/emc/motion/emcmotcfg.h | 75 + .../linuxcnc/src/emc/motion/state_tag.h | 140 + .../vendor/linuxcnc/src/emc/nml_intf/canon.hh | 1069 +++ .../src/emc/nml_intf/canon_position.hh | 77 + .../linuxcnc/src/emc/nml_intf/debugflags.h | 50 + .../vendor/linuxcnc/src/emc/nml_intf/emc.hh | 483 ++ .../vendor/linuxcnc/src/emc/nml_intf/emcpos.h | 36 + .../linuxcnc/src/emc/nml_intf/emcpose.h | 51 + .../linuxcnc/src/emc/nml_intf/emctool.h | 40 + .../src/emc/nml_intf/interp_return.hh | 40 + .../linuxcnc/src/emc/rs274ngc/interp_array.cc | 309 + .../linuxcnc/src/emc/rs274ngc/interp_base.cc | 62 + .../linuxcnc/src/emc/rs274ngc/interp_base.hh | 76 + .../linuxcnc/src/emc/rs274ngc/interp_check.cc | 389 + .../src/emc/rs274ngc/interp_convert.cc | 6550 +++++++++++++++++ .../src/emc/rs274ngc/interp_cycles.cc | 2272 ++++++ .../src/emc/rs274ngc/interp_execute.cc | 422 ++ .../linuxcnc/src/emc/rs274ngc/interp_find.cc | 758 ++ .../linuxcnc/src/emc/rs274ngc/interp_fwd.hh | 42 + .../src/emc/rs274ngc/interp_internal.cc | 494 ++ .../src/emc/rs274ngc/interp_internal.hh | 1029 +++ .../src/emc/rs274ngc/interp_namedparams.cc | 989 +++ .../src/emc/rs274ngc/interp_o_word.cc | 1222 +++ .../src/emc/rs274ngc/interp_parameter_def.hh | 204 + .../linuxcnc/src/emc/rs274ngc/interp_queue.cc | 692 ++ .../linuxcnc/src/emc/rs274ngc/interp_queue.hh | 149 + .../linuxcnc/src/emc/rs274ngc/interp_read.cc | 3475 +++++++++ .../linuxcnc/src/emc/rs274ngc/interp_write.cc | 370 + .../linuxcnc/src/emc/rs274ngc/modal_state.cc | 70 + .../linuxcnc/src/emc/rs274ngc/modal_state.hh | 53 + .../linuxcnc/src/emc/rs274ngc/rs274ngc.hh | 82 + .../src/emc/rs274ngc/rs274ngc_interp.hh | 701 ++ .../linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc | 2775 +++++++ .../src/emc/rs274ngc/rs274ngc_return.hh | 219 + .../vendor/linuxcnc/src/emc/rs274ngc/units.h | 37 + .../linuxcnc/src/libnml/posemath/gomath.h | 854 +++ .../linuxcnc/src/libnml/posemath/gotypes.h | 169 + .../linuxcnc/src/libnml/posemath/posemath.h | 1015 +++ .../linuxcnc/src/libnml/posemath/sincos.h | 20 + .../linuxcnc/src/rtapi/rtapi_byteorder.h | 44 + .../vendor/linuxcnc/src/rtapi/rtapi_gfp.h | 52 + .../vendor/linuxcnc/src/rtapi/rtapi_math.h | 132 + .../vendor/linuxcnc/src/rtapi/rtapi_stdint.h | 78 + .../vendor/linuxcnc/src/rtapi/rtapi_string.h | 69 + 82 files changed, 33282 insertions(+) create mode 100644 wasm-port/.gitignore create mode 100644 wasm-port/AGENTS.md create mode 100644 wasm-port/README.md create mode 100644 wasm-port/SKILL.md create mode 100644 wasm-port/docs/porting-steps-standalone.md create mode 100644 wasm-port/runtime/core/include/canon_event_sink.hh create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_probe.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_harness.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_runtime.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_state_probe.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_namedparam_harness.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_rs274_compile_probe.cpp create mode 100644 wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_runtime_state_stubs.cpp create mode 100644 wasm-port/runtime/core/shims/boost/python/detail/config.hpp create mode 100644 wasm-port/runtime/core/shims/boost/python/detail/prefix.hpp create mode 100644 wasm-port/runtime/core/shims/boost/python/detail/wrap_python.hpp create mode 100644 wasm-port/runtime/core/shims/boost/python/object_fwd.hpp create mode 100644 wasm-port/runtime/core/shims/config.h create mode 100644 wasm-port/runtime/core/shims/fmt/format.h create mode 100644 wasm-port/runtime/core/shims/nml_intf/emc.hh create mode 100644 wasm-port/runtime/core/shims/pythonplugin/python_plugin.hh create mode 100644 wasm-port/runtime/core/shims/rtapi.h create mode 100644 wasm-port/runtime/core/shims/tooldata/tooldata.hh create mode 100644 wasm-port/runtime/ui/ini-panel/app.js create mode 100644 wasm-port/runtime/ui/ini-panel/index.html create mode 100644 wasm-port/runtime/ui/ini-panel/linuxcnc_ini.js create mode 100755 wasm-port/runtime/ui/ini-panel/linuxcnc_ini.wasm create mode 100755 wasm-port/tests/native/verify_native_probes.sh create mode 100755 wasm-port/tools/build_ini_panel.sh create mode 100755 wasm-port/tools/build_native_probes.sh create mode 100755 wasm-port/tools/extract_sources.sh create mode 100755 wasm-port/tools/serve_ini_panel.sh create mode 100644 wasm-port/tools/source-manifest.txt create mode 100644 wasm-port/vendor/linuxcnc/src/emc/ini/inifile.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/ini/inifile.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/ini/inifile.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/linuxcnc.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/motion/emcmotcfg.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/motion/state_tag.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon_position.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/debugflags.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/emc.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpos.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpose.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/emctool.h create mode 100644 wasm-port/vendor/linuxcnc/src/emc/nml_intf/interp_return.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_array.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_check.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_convert.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_cycles.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_execute.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_find.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_fwd.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_namedparams.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_o_word.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_parameter_def.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_queue.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_queue.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_read.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_write.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/modal_state.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/modal_state.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/rs274ngc.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/rs274ngc_interp.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/rs274ngc_return.hh create mode 100644 wasm-port/vendor/linuxcnc/src/emc/rs274ngc/units.h create mode 100644 wasm-port/vendor/linuxcnc/src/libnml/posemath/gomath.h create mode 100644 wasm-port/vendor/linuxcnc/src/libnml/posemath/gotypes.h create mode 100644 wasm-port/vendor/linuxcnc/src/libnml/posemath/posemath.h create mode 100644 wasm-port/vendor/linuxcnc/src/libnml/posemath/sincos.h create mode 100644 wasm-port/vendor/linuxcnc/src/rtapi/rtapi_byteorder.h create mode 100644 wasm-port/vendor/linuxcnc/src/rtapi/rtapi_gfp.h create mode 100644 wasm-port/vendor/linuxcnc/src/rtapi/rtapi_math.h create mode 100644 wasm-port/vendor/linuxcnc/src/rtapi/rtapi_stdint.h create mode 100644 wasm-port/vendor/linuxcnc/src/rtapi/rtapi_string.h diff --git a/wasm-port/.gitignore b/wasm-port/.gitignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/wasm-port/.gitignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/wasm-port/AGENTS.md b/wasm-port/AGENTS.md new file mode 100644 index 0000000..d3efe0f --- /dev/null +++ b/wasm-port/AGENTS.md @@ -0,0 +1,116 @@ +# AGENTS.md + +## Scope + +This file defines agent operating rules for the standalone LinuxCNC WASM +simulation port located under `wasm-port/`. + +This workspace is separate from the upstream LinuxCNC tree in +`../linuxcnc/`. + +## Primary Objective + +Build a standalone CNC simulation system that: + +- reuses LinuxCNC source as the semantic source of truth; +- compiles core CNC logic to WASM; +- uses HTML + JavaScript for the frontend; +- uses OPFS for browser persistence; +- does not drive real hardware; +- preserves LinuxCNC software behavior as closely as practical. + +## Repository Boundaries + +1. `../linuxcnc/` is upstream and must be treated as read-only input for the + port effort. +2. Port-specific code must live under `wasm-port/`. +3. Vendored LinuxCNC source copies must live under + `wasm-port/vendor/linuxcnc/`. +4. Any source-level adaptation must be applied to vendored copies only. +5. Never treat experimental files under `../linuxcnc/web/` as the official + port target. The official port program is managed here. + +## Engineering Rules + +1. Reuse LinuxCNC source before reimplementing any CNC logic. +2. Prefer wrappers, shims, and extraction scripts over invasive source edits. +3. Preserve LinuxCNC semantics for: + - G-code execution; + - modal state; + - parameter and variable behavior; + - kinematics; + - planner behavior; + - machine and controller state visible to software. +4. Replace only the native runtime edges: + - file IO; + - process model; + - HAL runtime; + - IPC; + - GUI. +5. Frontend code must be implemented with web technology, not migrated from + native GUI code. + +## Required Layout + +The standalone port should use these major areas: + +- `docs/` +- `vendor/` +- `patches/` +- `tools/` +- `runtime/core/` +- `runtime/sdk/` +- `runtime/ui/` +- `runtime/opfs/` +- `tests/` + +Do not collapse these concerns back into the upstream tree. + +## File Ownership + +- `docs/`: planning, architecture, drift tracking, validation +- `vendor/`: copied upstream source +- `patches/`: patches against vendored copies +- `tools/`: extraction and sync scripts +- `runtime/core/`: standalone native/WASM simulation runtime +- `runtime/sdk/`: JS or TS SDK +- `runtime/ui/`: HTML + JavaScript simulation frontend +- `runtime/opfs/`: browser persistence layer +- `tests/`: standalone native and browser regression coverage + +## Validation Requirements + +Every migrated feature should be validated against LinuxCNC-native behavior +using one or more of: + +- existing LinuxCNC tests; +- extracted native harness tests; +- WASM regression tests; +- browser smoke tests. + +Validation should cover: + +- path output; +- machine/controller state; +- parameter and variable behavior; +- kinematic transforms; +- file/config loading. + +## Non-Goals + +This project must not: + +- attempt realtime hardware control; +- port LinuxCNC drivers to the browser; +- recreate LinuxCNC's native process topology; +- rewrite major CNC semantics in JavaScript if LinuxCNC source can be reused; +- depend on LinuxCNC native GUI code as implementation code. + +## Working Style + +When extending this workspace: + +1. Update docs before or alongside structural changes. +2. Keep extraction and patching reproducible. +3. Keep adapters narrow and explicit. +4. Keep LinuxCNC-derived logic traceable to its upstream file origin. diff --git a/wasm-port/README.md b/wasm-port/README.md new file mode 100644 index 0000000..69f3976 --- /dev/null +++ b/wasm-port/README.md @@ -0,0 +1,128 @@ +# LinuxCNC WASM Port Workspace + +This directory is the standalone workspace for the WASM-based simulation port. + +Rules: + +1. `linuxcnc/` is treated as the upstream source tree. +2. The porting work must not modify LinuxCNC source files in-place. +3. Port-specific code, build scripts, adapters, tests, and documentation live here. +4. LinuxCNC source is consumed by reference, copy, generated snapshot, or scripted extraction. +5. The browser/WASM product is managed as a separate program. + +Suggested layout: + +- `docs/` + Porting strategy and execution documents. +- `vendor/` + Controlled copies or generated snapshots of LinuxCNC source files selected for porting. +- `patches/` + Patch sets applied only to vendored copies, never directly to `linuxcnc/`. +- `tools/` + Extraction, sync, verification, and code generation scripts. +- `runtime/` + WASM core, JS SDK, HTML frontend, and OPFS adapters. +- `tests/` + Port-specific regression tests and browser harnesses. + +Recommended target structure after the port takes shape: + +```text +wasm-port/ +├── README.md +├── docs/ +│ ├── scope-and-baseline.md +│ ├── source-reuse-map.md +│ ├── state-porting-strategy.md +│ ├── wasm-build-strategy.md +│ ├── frontend-architecture.md +│ ├── opfs-file-model.md +│ ├── compatibility-validation.md +│ └── drift-report.md +├── vendor/ +│ └── linuxcnc/ +│ └── src/ +│ ├── emc/ +│ │ ├── ini/ +│ │ ├── kinematics/ +│ │ ├── rs274ngc/ +│ │ └── tp/ +│ ├── hal/ +│ │ └── components/ +│ └── libnml/ +│ └── posemath/ +├── patches/ +│ ├── vendor-linuxcnc-ini.patch +│ ├── vendor-linuxcnc-rs274ngc.patch +│ ├── vendor-linuxcnc-tp.patch +│ └── vendor-linuxcnc-kinematics.patch +├── tools/ +│ ├── extract_sources.sh +│ ├── apply_vendor_patches.sh +│ ├── verify_vendor_sync.sh +│ ├── generate_source_manifest.py +│ └── compare_with_upstream.sh +├── runtime/ +│ ├── core/ +│ │ ├── include/ +│ │ ├── shims/ +│ │ ├── adapters/ +│ │ ├── canon/ +│ │ ├── session/ +│ │ ├── simulation/ +│ │ ├── linuxcnc_wrap/ +│ │ └── c_api/ +│ ├── sdk/ +│ │ ├── src/ +│ │ ├── package.json +│ │ └── tsconfig.json +│ ├── ui/ +│ │ ├── public/ +│ │ ├── src/ +│ │ │ ├── panels/ +│ │ │ ├── preview/ +│ │ │ ├── state/ +│ │ │ ├── machine/ +│ │ │ └── files/ +│ │ ├── package.json +│ │ └── index.html +│ └── opfs/ +│ ├── file-service.js +│ ├── snapshot-store.js +│ └── path-model.js +├── tests/ +│ ├── native/ +│ │ ├── ini/ +│ │ ├── params/ +│ │ ├── interp/ +│ │ ├── tp/ +│ │ └── kinematics/ +│ ├── wasm/ +│ │ ├── node/ +│ │ └── browser/ +│ └── fixtures/ +│ ├── ini/ +│ ├── gcode/ +│ ├── tools/ +│ ├── params/ +│ └── machines/ +├── build/ +│ ├── native/ +│ └── wasm/ +└── dist/ + ├── sdk/ + └── web/ +``` + +Directory intent: + +- `vendor/linuxcnc/`: read-only copied LinuxCNC source selected for the port. +- `runtime/core/`: the standalone simulation engine built around vendored LinuxCNC code. +- `runtime/sdk/`: JavaScript/TypeScript API for calling the WASM engine. +- `runtime/ui/`: HTML + JavaScript CNC simulation frontend. +- `runtime/opfs/`: OPFS-backed persistence layer. +- `tests/fixtures/`: stable simulation inputs shared by native and browser tests. +- `build/` and `dist/`: generated outputs only; never hand-edited. + +The presence of this directory means the port is managed independently of the +native LinuxCNC tree even though LinuxCNC remains the semantic source of truth. diff --git a/wasm-port/SKILL.md b/wasm-port/SKILL.md new file mode 100644 index 0000000..aeee40f --- /dev/null +++ b/wasm-port/SKILL.md @@ -0,0 +1,143 @@ +# SKILL.md + +## Skill Name + +LinuxCNC WASM Simulation Port + +## Purpose + +This skill guides work inside `wasm-port/` for building a standalone +LinuxCNC-based CNC simulation program. + +It is intended for tasks involving: + +- source extraction from upstream LinuxCNC; +- vendoring and patching selected LinuxCNC modules; +- wrapping LinuxCNC compute code for native and WASM use; +- preserving interpreter, planner, kinematics, HAL-visible, and INI-visible + semantics; +- building HTML + JavaScript + OPFS frontend integration. + +## When To Use + +Use this skill when the work involves any of the following: + +- deciding whether to reuse or reimplement LinuxCNC code; +- mapping CNC features to LinuxCNC source files; +- creating or updating extraction scripts under `tools/`; +- defining standalone runtime structure under `runtime/core/`; +- building browser-facing WASM APIs; +- integrating OPFS-backed configuration and session persistence; +- validating LinuxCNC-equivalent simulation behavior. + +## Core Principles + +1. Upstream LinuxCNC is the semantic source of truth. +2. The standalone port is a separate program and separate workspace. +3. Reuse comes before rewrite. +4. Native runtime dependencies are replaced at the edges, not copied whole. +5. Machine state and controller-visible behavior are first-class compatibility + targets, not optional nice-to-haves. + +## Source Reuse Priorities + +Highest-priority source reuse targets: + +- `src/emc/rs274ngc` +- `src/emc/tp` +- `src/emc/kinematics` +- `src/libnml/posemath` +- `src/emc/ini` + +Secondary semantic references: + +- `src/hal/hal.h` +- `src/hal/halmodule.cc` +- `src/emc/ini/inihal.cc` + +Reference-only UI sources: + +- `src/emc/usr_intf/axis` +- `src/hal/user_comps/vismach` +- `configs/sim/axis/vismach/5axis` + +## Required Port Boundaries + +The port should preserve or expose these LinuxCNC behaviors: + +- G-code parsing and execution +- canonical motion generation +- parameter file semantics +- named variable lookup semantics +- `_ini[...]` lookup semantics +- `_hal[...]` lookup semantics through simulation adapter +- planner behavior +- 3-axis through 5-axis kinematics +- machine/controller state visible to software + +The port should replace these native dependencies: + +- Linux process orchestration +- NML transport implementation +- HAL runtime internals +- realtime scheduling +- native GUI implementation +- native filesystem assumptions + +## Implementation Pattern + +Preferred order of work: + +1. Identify upstream file owners for the feature. +2. Extract files into `vendor/linuxcnc/`. +3. Add wrappers or shims in `runtime/core/`. +4. Apply minimal patches only to vendored copies if needed. +5. Build a native standalone harness first. +6. Build the WASM export layer second. +7. Build the JS SDK and HTML frontend last. + +## Filesystem And Browser Guidance + +For browser use: + +- WASM core should not directly own OPFS logic. +- JavaScript host should own OPFS and file management. +- WASM core should operate on abstract file services or explicit byte blobs. + +Recommended browser storage targets: + +- INI files +- tool tables +- parameter files +- G-code programs +- preview caches +- session snapshots + +## Validation Pattern + +For each migrated feature, validate in this order: + +1. Upstream LinuxCNC behavior +2. Standalone native extracted-core behavior +3. WASM behavior in Node or CLI harness +4. Browser behavior through the frontend + +Always record: + +- source origin +- any shim introduced +- any vendored patch introduced +- known deviations from upstream behavior + +## Deliverable Bias + +Prefer deliverables that directly improve execution readiness: + +- source reuse maps +- extraction scripts +- standalone runtime wrappers +- regression fixtures +- drift reports + +Prefer not to spend time on speculative abstractions unless they reduce +real migration complexity. diff --git a/wasm-port/docs/porting-steps-standalone.md b/wasm-port/docs/porting-steps-standalone.md new file mode 100644 index 0000000..033f945 --- /dev/null +++ b/wasm-port/docs/porting-steps-standalone.md @@ -0,0 +1,658 @@ +# LinuxCNC WASM Porting Steps + +## Goal + +Build a separate WASM-based CNC simulation program that: + +- uses LinuxCNC source as the semantic source of truth, +- does not modify the original `linuxcnc/` source tree, +- is managed independently from native LinuxCNC, +- provides HTML + JavaScript frontend plus OPFS persistence, +- matches LinuxCNC software behavior as closely as practical except for realtime hardware driving. + +## Non-Negotiable Constraints + +1. `linuxcnc/` is upstream and read-only for the port effort. +2. Any source adaptation needed for WASM happens on copied or generated files under `wasm-port/`. +3. No direct edits inside `linuxcnc/src`, `linuxcnc/lib`, `linuxcnc/tests`, or `linuxcnc/web` are part of the port workflow. +4. LinuxCNC GUI code is reference-only. The migrated UI is rebuilt in HTML + JavaScript. +5. LinuxCNC compute logic should be reused before any reimplementation is considered. + +## Workspace Structure + +Create and keep these directories under `wasm-port/`: + +- `docs/` + Planning, architecture, and validation documents. +- `vendor/linuxcnc/` + Copied source files selected for the port. +- `patches/` + Patch files against the vendored copies. +- `tools/` + Scripts that extract files from `../linuxcnc`, apply patches, and verify drift. +- `runtime/core/` + WASM-targeted C/C++ code and wrappers. +- `runtime/sdk/` + JavaScript/TypeScript wrapper over the WASM module. +- `runtime/ui/` + HTML + JavaScript frontend. +- `runtime/opfs/` + Browser file adapter. +- `tests/native/` + Native extracted-core regression tests. +- `tests/browser/` + Browser/WASM regression and smoke tests. + +## Ported Program Directory Structure + +The migrated program itself should be managed as a standalone product under +`wasm-port/`, with LinuxCNC acting as an upstream source provider. + +The recommended final structure is: + +```text +wasm-port/ +├── docs/ +├── vendor/ +│ └── linuxcnc/ +│ └── src/ +│ ├── emc/ +│ │ ├── ini/ +│ │ ├── kinematics/ +│ │ ├── rs274ngc/ +│ │ └── tp/ +│ ├── hal/ +│ │ └── components/ +│ └── libnml/ +│ └── posemath/ +├── patches/ +├── tools/ +├── runtime/ +│ ├── core/ +│ │ ├── include/ +│ │ ├── shims/ +│ │ ├── adapters/ +│ │ ├── canon/ +│ │ ├── session/ +│ │ ├── simulation/ +│ │ ├── linuxcnc_wrap/ +│ │ └── c_api/ +│ ├── sdk/ +│ │ └── src/ +│ ├── ui/ +│ │ ├── public/ +│ │ └── src/ +│ │ ├── panels/ +│ │ ├── preview/ +│ │ ├── state/ +│ │ ├── machine/ +│ │ └── files/ +│ └── opfs/ +├── tests/ +│ ├── native/ +│ ├── wasm/ +│ └── fixtures/ +├── build/ +│ ├── native/ +│ └── wasm/ +└── dist/ + ├── sdk/ + └── web/ +``` + +### Directory Responsibilities + +- `vendor/linuxcnc/` + Holds extracted upstream LinuxCNC source files. These are copied or + generated from `../linuxcnc` and are the only place where source-level + port patches are applied. +- `patches/` + Stores every patch applied to vendored LinuxCNC files. Patches are tracked + here instead of being mixed into the original source tree. +- `tools/` + Holds extraction, sync, verification, manifest generation, and + compatibility-check scripts. +- `runtime/core/` + The standalone simulation runtime. This is where vendored LinuxCNC source + is wrapped, adapted, and exposed to the rest of the ported program. +- `runtime/core/include/` + Public internal headers for the standalone runtime. +- `runtime/core/shims/` + Small compatibility headers and implementation stubs required to compile + vendored LinuxCNC code outside the native runtime. +- `runtime/core/adapters/` + Host boundary adapters such as file IO, logging, simulation HAL, and + runtime-state providers. +- `runtime/core/canon/` + Canonical motion event collection and serialization. +- `runtime/core/session/` + Per-simulation session ownership for interpreter state, planner state, + machine state, and controller state. +- `runtime/core/simulation/` + Higher-level simulation orchestration built on top of reused LinuxCNC + compute modules. +- `runtime/core/linuxcnc_wrap/` + Thin wrappers around vendored LinuxCNC entry points. Prefer wrappers here + over editing vendored source directly. +- `runtime/core/c_api/` + Stable C ABI exported to the WASM layer. +- `runtime/sdk/` + JavaScript/TypeScript SDK that calls the WASM module and hides memory + management and ABI details from the frontend. +- `runtime/ui/` + The actual CNC simulation web application built with HTML + JavaScript. +- `runtime/ui/src/panels/` + Operator panels, machine configuration panels, parameter editors, and + controller-status panels. +- `runtime/ui/src/preview/` + 2D/3D path preview, 5-axis visualization, joint/world overlays, and + playback views. +- `runtime/ui/src/state/` + Browser-side state management for session lifecycle and UI coordination. +- `runtime/ui/src/machine/` + Machine-model-specific UI logic. +- `runtime/ui/src/files/` + File import/export and project/session handling UI logic. +- `runtime/opfs/` + OPFS-backed file services, snapshot persistence, and path mapping. +- `tests/native/` + Native extracted-core regression tests run before WASM build validation. +- `tests/wasm/` + WASM tests for Node and browser environments. +- `tests/fixtures/` + Shared INI, G-code, parameter, tool-table, and machine fixtures. +- `build/` + Intermediate build output. This is disposable. +- `dist/` + Deliverable artifacts such as generated web bundles and SDK packages. + +### Source Ownership Rule + +The ported program's own source code is expected to live under: + +- `runtime/core/` +- `runtime/sdk/` +- `runtime/ui/` +- `runtime/opfs/` +- `tests/` +- `tools/` +- `docs/` + +Vendored LinuxCNC code must live under: + +- `vendor/linuxcnc/` + +The original upstream tree must remain outside the ported program's source +ownership boundary: + +- `../linuxcnc/` + +### Patch Placement Rule + +If a LinuxCNC source file needs adaptation: + +1. extract it into `vendor/linuxcnc/`; +2. patch the vendored copy only; +3. record the patch in `patches/`; +4. document why the patch exists in `docs/` or in the patch header. + +Do not place LinuxCNC source patches under `runtime/`. + +### Build Graph Rule + +The standalone build should flow like this: + +1. `tools/` extracts upstream files into `vendor/` +2. `patches/` are applied to vendored copies +3. `runtime/core/` compiles vendored files plus wrappers +4. `runtime/sdk/` consumes the generated WASM module +5. `runtime/ui/` consumes the SDK +6. `runtime/opfs/` provides browser persistence services +7. `tests/` validate native and browser behavior + +This ensures the ported program remains independently buildable while still +tracking LinuxCNC as the semantic source of truth. + +## Phase 0: Freeze Upstream Reference + +Purpose: +Make the LinuxCNC source baseline explicit before extraction starts. + +Steps: + +1. Record the exact upstream commit from `linuxcnc/.git`. +2. Record local build assumptions: + - compiler version + - emscripten version + - python version + - node version +3. Record the first supported LinuxCNC fixture set: + - one 3-axis machine + - one non-trivial kinematics case + - one 5-axis machine +4. Record the first G-code feature set: + - linear motion + - arc motion + - canned cycles + - offsets and coordinate systems + - subroutines + - numeric and named variables + - representative 5-axis programs + +Outputs: + +- upstream revision note +- supported feature baseline +- supported machine baseline + +## Phase 1: Build the Source Reuse Map + +Purpose: +Identify exactly which LinuxCNC source files are needed. + +Steps: + +1. Map each required capability to LinuxCNC files: + - G-code interpreter: + `../linuxcnc/src/emc/rs274ngc/*` + - parameter tables and named variables: + `../linuxcnc/src/emc/rs274ngc/interp_*` + - INI parsing: + `../linuxcnc/src/emc/ini/inifile.*` + - planner: + `../linuxcnc/src/emc/tp/*` + - kinematics: + `../linuxcnc/src/emc/kinematics/*` + and selected files from `../linuxcnc/src/hal/components/*.comp` + - posemath: + `../linuxcnc/src/libnml/posemath/*` +2. For each file, classify it: + - copy unchanged + - copy plus shim + - copy plus light patch + - not included in phase 1 +3. For each file, record dependencies: + - HAL + - RTAPI + - NML + - native file IO + - Python + - GUI +4. Save the mapping as a table under `wasm-port/docs/`. + +Outputs: + +- file-level reuse matrix +- dependency matrix + +## Phase 2: Build the Extraction Pipeline + +Purpose: +Keep LinuxCNC source untouched while making port-specific copies available. + +Steps: + +1. Write extraction scripts in `wasm-port/tools/`. +2. Copy selected upstream files into `wasm-port/vendor/linuxcnc/`. +3. Preserve relative structure where useful, for example: + - `vendor/linuxcnc/src/emc/rs274ngc/...` + - `vendor/linuxcnc/src/emc/ini/...` +4. Store every modification as: + - a patch in `wasm-port/patches/`, or + - a thin wrapper outside the vendored file +5. Add a verification script that compares upstream file hashes against the vendored source list. + +Rules: + +- if upstream changes, re-run extraction; +- reapply patches only in the standalone workspace; +- never patch `../linuxcnc` directly. + +Outputs: + +- reproducible extraction pipeline +- vendored source tree + +## Phase 3: Build the Native Extracted Core + +Purpose: +Prove the reusable LinuxCNC compute code works outside the full runtime. + +Steps: + +1. Create `wasm-port/runtime/core/` as the portable core layer. +2. Add wrapper translation units instead of editing vendored files where possible. +3. Start with these subsystems in order: + - INI parser + - numeric parameter table + - named parameter logic + - interpreter core + - planner + - kinematics +4. Build a native CLI harness first, before any WASM build. +5. Replace host edges with abstractions: + - file provider + - HAL adapter + - runtime state provider + - logging adapter + +Outputs: + +- portable native core +- native harness executable + +Current verified progress: + +- `tools/build_native_probes.sh` builds the INI parser probe, interpreter + state probe, named-parameter harness, RS274 compile probe, and the minimal + interpreter harness from the standalone `wasm-port/` workspace. +- `tests/native/verify_native_probes.sh` validates that all native probe + exit codes are zero and that the minimal interpreter harness emits a + `STRAIGHT_TRAVERSE` canonical event for `G0 X1.0 Y2.0`, plus + `SET_FEED_RATE` and `STRAIGHT_FEED` canonical events for + `G1 X3.0 Y4.0 F120.0`. +- This proves the current extracted interpreter slice can parse and execute a + simple traverse and feed move through a standalone canonical event sink. + +## Phase 4: Port INI Parsing Without Editing Upstream + +Purpose: +Move LinuxCNC config parsing into the standalone runtime. + +Steps: + +1. Vendor: + - `../linuxcnc/src/emc/ini/inifile.cc` + - `../linuxcnc/src/emc/ini/inifile.hh` + - `../linuxcnc/src/emc/ini/inifile.h` +2. Add shim headers under `wasm-port/runtime/core/shims/` for small dependencies only. +3. Replace file loading through: + - wrapper-level adapter, preferred + - minimal vendored patch only if unavoidable +4. Preserve: + - `#INCLUDE` + - relative includes + - recursion checks + - line continuation + - duplicate section merge behavior + - typed query behavior +5. Add tests for INI semantics under `tests/native/`. + +Involved LinuxCNC source: + +- `src/emc/ini/inifile.cc` +- `src/emc/ini/inifile.hh` +- `src/emc/ini/inifile.h` + +## Phase 5: Port Parameter Tables and Variable Files + +Purpose: +Preserve LinuxCNC numeric parameter behavior exactly enough for simulation. + +Steps: + +1. Vendor: + - `../linuxcnc/src/emc/rs274ngc/interp_parameter_def.hh` + - `../linuxcnc/src/emc/rs274ngc/interp_array.cc` + - `../linuxcnc/src/emc/rs274ngc/interp_internal.hh` + - `../linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc` +2. Extract: + - `setup.parameters[]` + - `required_parameters[]` + - `readonly_parameters[]` + - parameter file load/save logic +3. Replace native file writes with a standalone file service abstraction. +4. Keep LinuxCNC’s parameter text format in phase 1. +5. Add regression tests for: + - missing file + - out-of-order parameters + - zero-fill behavior + - required parameter persistence + - read-only protection + +Involved LinuxCNC source: + +- `src/emc/rs274ngc/interp_parameter_def.hh` +- `src/emc/rs274ngc/interp_array.cc` +- `src/emc/rs274ngc/interp_internal.hh` +- `src/emc/rs274ngc/rs274ngc_pre.cc` + +## Phase 6: Port Named Parameters and Interpreter State + +Purpose: +Preserve LinuxCNC variable semantics and controller-visible state. + +Steps: + +1. Vendor: + - `../linuxcnc/src/emc/rs274ngc/interp_namedparams.cc` + - `../linuxcnc/src/emc/rs274ngc/interp_internal.hh` + - `../linuxcnc/src/emc/rs274ngc/interp_fwd.hh` + - selected interpreter files needed by setup/state handling +2. Preserve: + - `context.named_params` + - `PA_READONLY` + - `PA_GLOBAL` + - `PA_USE_LOOKUP` + - `PA_FROM_INI` + - built-in named parameters from `init_named_parameters()` +3. Preserve lookup order: + - local + - global + - `_ini[...]` + - `_hal[...]` + - optional Python providers later +4. Export state outward rather than redesigning it in JS. +5. Add regression cases for: + - local/global scoping + - built-in state variables + - `_ini[...]` + - `_hal[...]` + - read-only errors + +Involved LinuxCNC source: + +- `src/emc/rs274ngc/interp_namedparams.cc` +- `src/emc/rs274ngc/interp_internal.hh` +- `src/emc/rs274ngc/interp_fwd.hh` +- `src/emc/rs274ngc/interpmodule.cc` + +## Phase 7: Replace HAL Runtime With a Simulation HAL Adapter + +Purpose: +Keep LinuxCNC interpreter-visible HAL behavior without migrating HAL itself. + +Steps: + +1. Do not vendor the whole HAL runtime as a target runtime dependency. +2. Use LinuxCNC HAL source as reference only for interface semantics: + - `../linuxcnc/src/hal/hal.h` + - `../linuxcnc/src/hal/halmodule.cc` + - `../linuxcnc/src/emc/ini/inihal.cc` +3. Define a standalone HAL adapter interface: + - lookup by name + - typed numeric value + - connection/existence status +4. Make `_hal[...]` reads resolve through this adapter. +5. Seed the adapter from simulation config and runtime state. +6. Add tests for: + - existing names + - missing names + - disconnected signals + - type conversion + +Involved LinuxCNC source: + +- `src/hal/hal.h` +- `src/hal/halmodule.cc` +- `src/emc/rs274ngc/interp_namedparams.cc` +- `src/emc/ini/inihal.cc` + +## Phase 8: Port the Interpreter Core + +Purpose: +Build the standalone G-code execution engine without touching upstream files. + +Steps: + +1. Vendor selected files from `../linuxcnc/src/emc/rs274ngc/`. +2. Keep original source as intact as possible in `vendor/`. +3. Add wrappers or minimal patches only in the standalone area. +4. Replace these external edges: + - file open/read + - world sync + - HAL reads + - logging + - runtime callbacks +5. Preserve: + - modal state + - subroutines + - offsets + - tool handling + - parameter interactions + - error semantics +6. Add a canonical event sink interface in standalone code. + +Involved LinuxCNC source: + +- `src/emc/rs274ngc/interp_execute.cc` +- `src/emc/rs274ngc/interp_read.cc` +- `src/emc/rs274ngc/interp_check.cc` +- `src/emc/rs274ngc/interp_convert.cc` +- `src/emc/rs274ngc/interp_cycles.cc` +- `src/emc/rs274ngc/interp_find.cc` +- `src/emc/rs274ngc/interp_write.cc` +- `src/emc/rs274ngc/interp_o_word.cc` +- `src/emc/rs274ngc/rs274ngc_pre.cc` +- `src/emc/rs274ngc/rs274ngc_interp.hh` + +## Phase 9: Port Planner and Kinematics + +Purpose: +Preserve LinuxCNC motion planning and 5-axis simulation behavior. + +Steps: + +1. Vendor selected planner files from `../linuxcnc/src/emc/tp/`. +2. Vendor selected kinematics files from: + - `../linuxcnc/src/emc/kinematics/` + - selected `.comp` sources mirrored into standalone code where needed +3. Replace loadable-module assumptions with a registry in the standalone runtime. +4. Keep original math and state logic intact as far as practical. +5. Add tests for: + - planner outputs + - forward/inverse kinematics + - 5-axis world/joint transforms + +Involved LinuxCNC source: + +- `src/emc/tp/tp.c` +- `src/emc/tp/tc.c` +- `src/emc/tp/tcq.c` +- `src/emc/tp/blendmath.c` +- `src/emc/tp/sp_scurve.c` +- `src/emc/tp/ruckig_wrapper.c` +- `src/emc/kinematics/*.c` +- `src/hal/components/xyzab_tdr_kins.comp` +- `src/hal/components/xyzacb_trsrn.comp` +- `src/hal/components/xyzbca_trsrn.comp` + +## Phase 10: Build the Standalone WASM Program + +Purpose: +Keep the migrated program completely outside native LinuxCNC management. + +Steps: + +1. Build the standalone native core first. +2. Add a standalone WASM export layer under: + - `wasm-port/runtime/core/` + - `wasm-port/runtime/sdk/` +3. Use `vendor/` sources plus standalone wrappers as build input. +4. Do not compile from `../linuxcnc` directly in the final WASM product build. +5. Emit: + - `wasm` module + - JS loader + - standalone SDK + +Outputs: + +- independently managed WASM simulation core + +## Phase 11: Build the Frontend and OPFS Layer + +Purpose: +Keep the standalone port program separate from LinuxCNC GUI code and storage. + +Steps: + +1. Build frontend files under: + - `wasm-port/runtime/ui/` + - `wasm-port/runtime/opfs/` +2. Implement: + - HTML shell + - JavaScript control panel + - OPFS persistence + - file import/export + - machine/controller state views +3. Keep LinuxCNC GUI code reference-only. +4. Ensure the frontend depends only on the standalone SDK, not on native LinuxCNC binaries. + +Outputs: + +- standalone browser program + +## Phase 12: Validate Drift Against Upstream + +Purpose: +Prove the standalone port still tracks LinuxCNC behavior. + +Steps: + +1. Use native LinuxCNC tests and fixtures as semantic baselines. +2. Compare: + - LinuxCNC native behavior + - standalone native extracted core + - standalone WASM behavior +3. Maintain a drift report. +4. Re-run extraction if upstream source changes. +5. Keep patches small and traceable. + +Outputs: + +- drift report +- compatibility regression suite + +## Management Rules For Daily Development + +Use these rules continuously: + +1. Never edit files under `linuxcnc/` as part of the port. +2. Any needed modification to upstream logic must be applied to vendored copies only. +3. Any upstream sync must be script-driven and repeatable. +4. Every shim must be documented. +5. Every local patch against vendored source must be stored as a patch file or clearly isolated wrapper. + +## Recommended Document Set + +Keep these documents under `wasm-port/docs/`: + +- `scope-and-baseline.md` +- `source-reuse-map.md` +- `state-porting-strategy.md` +- `wasm-build-strategy.md` +- `frontend-architecture.md` +- `opfs-file-model.md` +- `compatibility-validation.md` +- `drift-report.md` + +## Immediate Next Step + +Continue expanding the standalone interpreter core from the verified minimal +traverse path: + +1. introduce a fixture-driven G-code validation script under `tests/fixtures/`, +2. replace the temporary minimal `convert_g()` implementation with thin + wrappers around more vendored interpreter conversion code, +3. keep all source changes inside `wasm-port/` and leave `../linuxcnc/` + read-only. diff --git a/wasm-port/runtime/core/include/canon_event_sink.hh b/wasm-port/runtime/core/include/canon_event_sink.hh new file mode 100644 index 0000000..4951158 --- /dev/null +++ b/wasm-port/runtime/core/include/canon_event_sink.hh @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace standalone { + +void reset_canon_events(); +void push_canon_event(const std::string &event); +const std::vector &canon_events(); + +} // namespace standalone diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_probe.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_probe.cpp new file mode 100644 index 0000000..129c214 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_probe.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include "emc/ini/inifile.hh" + +int main(int argc, char **argv) { + if (argc != 2) { + std::cerr << "usage: linuxcnc_ini_probe \n"; + return 2; + } + + const char *ini_path = argv[1]; + linuxcnc::IniFile ini(ini_path); + if (!ini) { + std::cerr << "failed to open ini file: " << ini_path << "\n"; + return 1; + } + + const auto machine = ini.findString("MACHINE", "EMC"); + const auto display = ini.findString("DISPLAY", "DISPLAY"); + const auto kins = ini.findString("KINEMATICS", "KINS"); + const auto coords = ini.findString("COORDINATES", "TRAJ"); + const auto parameter_file = ini.findString("PARAMETER_FILE", "RS274NGC"); + + std::cout << "EMC.MACHINE=" << (machine ? *machine : "") << "\n"; + std::cout << "DISPLAY.DISPLAY=" << (display ? *display : "") << "\n"; + std::cout << "KINS.KINEMATICS=" << (kins ? *kins : "") << "\n"; + std::cout << "TRAJ.COORDINATES=" << (coords ? *coords : "") << "\n"; + std::cout << "RS274NGC.PARAMETER_FILE=" << (parameter_file ? *parameter_file : "") << "\n"; + + return 0; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp new file mode 100644 index 0000000..7def71c --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp @@ -0,0 +1,21 @@ +#include + +#include "emc/ini/inifile.h" + +extern "C" { + +EMSCRIPTEN_KEEPALIVE +int lcini_get_string(const char *inipath, + const char *section, + const char *tag, + char *buf, + int bufsize) { + return iniFindString(inipath, tag, section, buf, static_cast(bufsize)); +} + +EMSCRIPTEN_KEEPALIVE +int lcini_tilde_expand(const char *path, char *buf, int bufsize) { + return TildeExpansion(path, buf, static_cast(bufsize)); +} + +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_harness.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_harness.cpp new file mode 100644 index 0000000..57f9e54 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_harness.cpp @@ -0,0 +1,82 @@ +#define private public +#include "emc/rs274ngc/rs274ngc_interp.hh" +#undef private + +#include +#include + +#include "canon_event_sink.hh" + +int main() +{ + Interp interp; + standalone::reset_canon_events(); + interp._setup.length_units = CANON_UNITS_MM; + interp._setup.distance_mode = DISTANCE_MODE::ABSOLUTE; + interp._setup.ijk_distance_mode = DISTANCE_MODE::ABSOLUTE; + interp._setup.feed_mode = FEED_MODE::UNITS_PER_MINUTE; + interp._setup.plane = CANON_PLANE::XY; + interp._setup.motion_mode = G_0; + interp._setup.percent_flag = false; + interp._setup.sequence_number = 0; + interp._setup.parameter_occurrence = 0; + std::memcpy(interp._readers, Interp::default_readers, sizeof(Interp::default_readers)); + + char line[] = "G0 X1.0 Y2.0 (Comment)\n"; + char raw_line[LINELEN] = {0}; + char cooked_line[LINELEN] = {0}; + int length = -1; + + const int read_text_rc = interp.read_text(line, nullptr, raw_line, cooked_line, &length); + const int downcase_rc = interp.close_and_downcase(line); + const int read_rc = interp.read("G0 X1.0 Y2.0 (Comment)\n"); + const int execute_rc = interp.execute("G0 X1.0 Y2.0 (Comment)\n"); + const int execute_feed_rc = interp.execute("G1 X3.0 Y4.0 F120.0\n"); + + block block{}; + const int init_block_rc = interp.init_block(&block); + int parse_line_rc = INTERP_ERROR; + if (read_text_rc == INTERP_OK) { + parse_line_rc = interp.parse_line(cooked_line, &block, &interp._setup); + } + + std::cout << "read_text=" << read_text_rc << "\n"; + std::cout << "raw_line=" << raw_line << "\n"; + std::cout << "cooked_line=" << cooked_line << "\n"; + std::cout << "line_length=" << length << "\n"; + std::cout << "read=" << read_rc << "\n"; + std::cout << "execute=" << execute_rc << "\n"; + std::cout << "execute_feed=" << execute_feed_rc << "\n"; + std::cout << "setup.linetext=" << interp._setup.linetext << "\n"; + std::cout << "setup.blocktext=" << interp._setup.blocktext << "\n"; + std::cout << "setup.line_length=" << interp._setup.line_length << "\n"; + std::cout << "setup.sequence_number=" << interp._setup.sequence_number << "\n"; + std::cout << "setup.current_x=" << interp._setup.current_x << "\n"; + std::cout << "setup.current_y=" << interp._setup.current_y << "\n"; + std::cout << "setup.current_z=" << interp._setup.current_z << "\n"; + std::cout << "setup.feed_rate=" << interp._setup.feed_rate << "\n"; + std::cout << "close_and_downcase=" << downcase_rc << "\n"; + std::cout << "normalized_line=" << line << "\n"; + std::cout << "init_block=" << init_block_rc << "\n"; + std::cout << "parse_line=" << parse_line_rc << "\n"; + std::cout << "block.motion_to_be=" << block.motion_to_be << "\n"; + std::cout << "block.g_modes[GM_MOTION]=" << block.g_modes[GM_MOTION] << "\n"; + std::cout << "block.x_flag=" << block.x_flag << " x=" << block.x_number << "\n"; + std::cout << "block.y_flag=" << block.y_flag << " y=" << block.y_number << "\n"; + std::cout << "executing_block.motion_to_be=" << EXECUTING_BLOCK(interp._setup).motion_to_be << "\n"; + std::cout << "executing_block.g_modes[GM_MOTION]=" << EXECUTING_BLOCK(interp._setup).g_modes[GM_MOTION] << "\n"; + std::cout << "executing_block.x_flag=" << EXECUTING_BLOCK(interp._setup).x_flag + << " x=" << EXECUTING_BLOCK(interp._setup).x_number << "\n"; + std::cout << "executing_block.y_flag=" << EXECUTING_BLOCK(interp._setup).y_flag + << " y=" << EXECUTING_BLOCK(interp._setup).y_number << "\n"; + std::cout << "executing_block.f_flag=" << EXECUTING_BLOCK(interp._setup).f_flag + << " f=" << EXECUTING_BLOCK(interp._setup).f_number << "\n"; + std::cout << "call_level=" << interp.call_level() << "\n"; + std::cout << "loggingLevel=" << interp._setup.loggingLevel << "\n"; + std::cout << "canon_event_count=" << standalone::canon_events().size() << "\n"; + for (const auto &event : standalone::canon_events()) { + std::cout << "canon_event=" << event << "\n"; + } + + return 0; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_runtime.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_runtime.cpp new file mode 100644 index 0000000..46deedc --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_minimal_runtime.cpp @@ -0,0 +1,463 @@ +#include +#include +#include +#include +#include +#include +#include + +#define private public +#include "emc/rs274ngc/rs274ngc_interp.hh" +#undef private + +#include "canon_event_sink.hh" + +PythonPlugin *python_plugin = nullptr; + +namespace standalone { + +static std::vector g_canon_events; + +void reset_canon_events() +{ + g_canon_events.clear(); +} + +void push_canon_event(const std::string &event) +{ + g_canon_events.push_back(event); +} + +const std::vector &canon_events() +{ + return g_canon_events; +} + +} // namespace standalone + +bool GET_BLOCK_DELETE(void) { return false; } +void FINISH(void) {} + +void COMMENT(const char *s) +{ + standalone::push_canon_event(std::string("COMMENT: ") + (s ? s : "")); +} + +void STRAIGHT_TRAVERSE(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w) +{ + std::ostringstream oss; + oss << "STRAIGHT_TRAVERSE line=" << lineno + << " x=" << x + << " y=" << y + << " z=" << z + << " a=" << a + << " b=" << b + << " c=" << c + << " u=" << u + << " v=" << v + << " w=" << w; + standalone::push_canon_event(oss.str()); +} + +void STRAIGHT_FEED(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w) +{ + std::ostringstream oss; + oss << "STRAIGHT_FEED line=" << lineno + << " x=" << x + << " y=" << y + << " z=" << z + << " a=" << a + << " b=" << b + << " c=" << c + << " u=" << u + << " v=" << v + << " w=" << w; + standalone::push_canon_event(oss.str()); +} + +void SET_FEED_RATE(double rate) +{ + std::ostringstream oss; + oss << "SET_FEED_RATE rate=" << rate; + standalone::push_canon_event(oss.str()); +} + +InterpBase::~InterpBase() {} + +Interp::Interp() + : log_file(stderr), + _setup{} +{ + _setup.init_once = 1; + memset(_readers, 0, sizeof(_readers)); +} + +Interp::~Interp() { + if (log_file && log_file != stderr) { + fclose(log_file); + } + log_file = nullptr; +} + +void Interp::doLog(unsigned int, const char *, int, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +const char *Interp::interp_status(int status) +{ + static char statustext[64]; + snprintf(statustext, sizeof(statustext), "%d", status); + return statustext; +} + +const char *Interp::getSavedError() +{ + return ""; +} + +int Interp::setSavedError(const char *) +{ + return INTERP_OK; +} + +void Interp::setError(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); + va_end(ap); +} + +int Interp::unwind_call(int status, const char *, int, const char *) +{ + return status; +} + +int Interp::close() { return INTERP_OK; } +int Interp::execute_block(block_pointer block, setup_pointer settings) +{ + int status = INTERP_OK; + + block->line_number = settings->sequence_number; + if ((block->comment[0] != 0) && ONCE(STEP_COMMENT)) { + status = convert_comment(block->comment); + if (status != INTERP_OK) return status; + } + status = convert_m(block, settings); + if (status != INTERP_OK) return status; + status = convert_g(block, settings); + if (status != INTERP_OK) return status; + write_canon_state_tag(block, settings); + return INTERP_OK; +} + +int Interp::execute(const char *command) +{ + int status = read(command); + if (status != INTERP_OK) { + if (status > INTERP_MIN_ERROR) { + unwind_call(status, __FILE__, __LINE__, __FUNCTION__); + } + return status; + } + + if (_setup.line_length != 0) { + status = execute_block(&EXECUTING_BLOCK(_setup), &_setup); + if (status > INTERP_MIN_ERROR) { + unwind_call(status, __FILE__, __LINE__, __FUNCTION__); + } + return status; + } + return INTERP_OK; +} + +int Interp::execute() { return execute(nullptr); } + +int Interp::execute(const char *command, int line_number) +{ + if (command && line_number) { + _setup.sequence_number = line_number; + } + int status = execute(command); + if ((_setup.call_level == 0) && + (status == INTERP_EXECUTE_FINISH) && + (_setup.mdi_interrupt)) { + _setup.mdi_interrupt = false; + } + return status; +} +int Interp::exit() { return INTERP_OK; } +int Interp::init() { return INTERP_OK; } +void Interp::set_loop_on_main_m99(bool state) { _setup.loop_on_main_m99 = state; } +int Interp::open(const char *) { return INTERP_ERROR; } + +int Interp::read_inputs(setup_pointer) +{ + return INTERP_OK; +} + +int Interp::_read(const char *command) +{ + int read_status = INTERP_OK; + block_pointer eblock = &EXECUTING_BLOCK(_setup); + + if ((_setup.call_state > CS_NORMAL) && + (eblock->call_type != CT_NGC_OWORD_SUB) && + (eblock->call_type != CT_NGC_M98_SUB) && + (eblock->call_type != CT_NONE) && + ((eblock->o_type == O_call) || + (eblock->o_type == M_98) || + (eblock->o_type == O_return) || + (eblock->o_type == O_endsub) || + (eblock->o_type == M_99))) { + _setup.line_length = 0; + _setup.linetext[0] = 0; + return INTERP_OK; + } + + _setup.call_state = CS_NORMAL; + if (read_inputs(&_setup) != INTERP_OK) { + return INTERP_ERROR; + } + + if ((command == nullptr) && (_setup.file_pointer == nullptr)) { + return INTERP_FILE_NOT_OPEN; + } + + _setup.parameters[5420] = _setup.current_x; + _setup.parameters[5421] = _setup.current_y; + _setup.parameters[5422] = _setup.current_z; + _setup.parameters[5423] = _setup.AA_current; + _setup.parameters[5424] = _setup.BB_current; + _setup.parameters[5425] = _setup.CC_current; + _setup.parameters[5426] = _setup.u_current; + _setup.parameters[5427] = _setup.v_current; + _setup.parameters[5428] = _setup.w_current; + + if (_setup.file_pointer) { + EXECUTING_BLOCK(_setup).offset = ftell(_setup.file_pointer); + } + + read_status = read_text(command, _setup.file_pointer, _setup.linetext, + _setup.blocktext, &_setup.line_length); + + if ((read_status == INTERP_EXECUTE_FINISH) || (read_status == INTERP_OK)) { + if (_setup.line_length != 0) { + if (parse_line(_setup.blocktext, &(EXECUTING_BLOCK(_setup)), &_setup) != INTERP_OK) { + return INTERP_ERROR; + } + } else { + if (EXECUTING_BLOCK(_setup).o_type != O_none) { + EXECUTING_BLOCK(_setup).o_type = 0; + } + } + } else if (read_status == INTERP_ENDFILE) { + if (_setup.skipping_o != nullptr) { + return INTERP_ERROR; + } + } else { + return read_status; + } + + return read_status; +} + +int Interp::read(const char *command) +{ + int status = _read(command); + if (status > INTERP_MIN_ERROR) { + unwind_call(status, __FILE__, __LINE__, __FUNCTION__); + } + return status; +} + +int Interp::read() +{ + return read(nullptr); +} + +int Interp::reset() { return INTERP_OK; } +int Interp::synch() { return INTERP_OK; } +void Interp::active_g_codes(int *codes) { std::memcpy(codes, _setup.active_g_codes, sizeof(_setup.active_g_codes)); } +void Interp::active_m_codes(int *codes) { std::memcpy(codes, _setup.active_m_codes, sizeof(_setup.active_m_codes)); } +void Interp::active_settings(double *settings) { std::memcpy(settings, _setup.active_settings, sizeof(_setup.active_settings)); } +int Interp::active_modes(int *, int *, double *, StateTag const &) { return INTERP_ERROR; } +void Interp::print_state_tag(StateTag const &) {} +char *Interp::error_text(int, char *buf, size_t max_size) +{ + if (max_size) buf[0] = '\0'; + return buf; +} +char *Interp::file_name(char *buf, size_t max_size) +{ + if (max_size) { + std::strncpy(buf, _setup.filename, max_size - 1); + buf[max_size - 1] = '\0'; + } + return buf; +} +size_t Interp::line_length() { return static_cast(_setup.line_length); } +char *Interp::line_text(char *buf, size_t max_size) +{ + if (max_size) { + std::strncpy(buf, _setup.linetext, max_size - 1); + buf[max_size - 1] = '\0'; + } + return buf; +} +int Interp::sequence_number() { return _setup.sequence_number; } +char *Interp::stack_name(int idx, char *buf, size_t max_size) +{ + if (!max_size) return buf; + if (idx < 0 || idx >= STACK_LEN) { + buf[0] = '\0'; + return buf; + } + std::strncpy(buf, _setup.stack[idx], max_size - 1); + buf[max_size - 1] = '\0'; + return buf; +} +int Interp::ini_load(const char *) { return INTERP_OK; } +int Interp::on_abort(int, const char *) { return INTERP_OK; } +void Interp::set_loglevel(int level) { _setup.loggingLevel = level; } +int Interp::restore_from_tag(StateTag const &) { return INTERP_ERROR; } + +int Interp::find_remappings(block_pointer, setup_pointer) { return 0; } + +int Interp::load_tool_table() { return INTERP_OK; } +int Interp::init_tool_parameters() { return INTERP_OK; } +int Interp::default_tool_parameters() { return INTERP_OK; } +int Interp::set_tool_parameters() { return INTERP_OK; } + +int Interp::convert_comment(char *comment, bool) +{ + COMMENT(comment); + return INTERP_OK; +} +bool Interp::is_pycallable(setup_pointer, const char *, const char *) { return false; } +bool Interp::is_user_defined_g_code(int) { return false; } +bool Interp::is_any_m_code_remapped(block_pointer, setup_pointer) { return false; } +int Interp::convert_m(block_pointer, setup_pointer) { return INTERP_OK; } +int Interp::convert_g(block_pointer block, setup_pointer settings) +{ + if (block->f_flag) { + settings->feed_rate = block->f_number; + SET_FEED_RATE(block->f_number); + } + + if (block->x_flag) { + settings->current_x = block->x_number; + } + if (block->y_flag) { + settings->current_y = block->y_number; + } + if (block->z_flag) { + settings->current_z = block->z_number; + } + + if (block->motion_to_be == G_0) { + STRAIGHT_TRAVERSE(block->line_number, + settings->current_x, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + } else if (block->motion_to_be == G_1) { + STRAIGHT_FEED(block->line_number, + settings->current_x, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + } + return INTERP_OK; +} +int Interp::write_canon_state_tag(block_pointer, setup_pointer) { return INTERP_OK; } + +const char *o_ops[] = { + "O_none", "O_sub", "O_endsub", "O_call", "O_do", "O_while", "O_if", + "O_elseif", "O_else", "O_endif", "O_break", "O_continue", + "O_endwhile", "O_return", "O_repeat", "O_endrepeat", "M_98", "M_99", "O_" +}; + +int Interp::read_named_parameter(char *, int *, double *double_ptr, double *, bool check_exists) +{ + if (check_exists) { + *double_ptr = 0.0; + } + return INTERP_ERROR; +} + +int Interp::find_named_param(const char *, int *status, double *value) +{ + *status = 0; + *value = 0.0; + return INTERP_OK; +} + +int Interp::store_named_param(setup_pointer, const char *, double, int) +{ + return INTERP_ERROR; +} + +int Interp::add_named_param(const char *, int) +{ + return INTERP_OK; +} + +double Interp::inicheck() +{ + return -1.0; +} + +int Interp::execute_binary(double *left, int operation, double *right) +{ + switch (operation) { + case PLUS: *left += *right; return INTERP_OK; + case MINUS: *left -= *right; return INTERP_OK; + case TIMES: *left *= *right; return INTERP_OK; + case DIVIDED_BY: + if (*right == 0.0) return INTERP_ERROR; + *left /= *right; + return INTERP_OK; + default: + return INTERP_ERROR; + } +} + +int Interp::execute_binary1(double *left, int operation, double *right) +{ + return execute_binary(left, operation, right); +} + +int Interp::execute_binary2(double *left, int operation, double *right) +{ + return execute_binary(left, operation, right); +} + +int Interp::execute_unary(double *double_ptr, int operation) +{ + switch (operation) { + case ABS: + if (*double_ptr < 0.0) *double_ptr = -*double_ptr; + return INTERP_OK; + case ROUND: + *double_ptr = static_cast((int)(*double_ptr + ((*double_ptr < 0.0) ? -0.5 : 0.5))); + return INTERP_OK; + case FIX: + *double_ptr = floor(*double_ptr); + return INTERP_OK; + case FUP: + *double_ptr = ceil(*double_ptr); + return INTERP_OK; + default: + return INTERP_ERROR; + } +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_state_probe.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_state_probe.cpp new file mode 100644 index 0000000..acb7b74 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_interp_state_probe.cpp @@ -0,0 +1,18 @@ +#include + +#include "emc/rs274ngc/interp_internal.hh" + +int main() { + std::cout << "RS274NGC_MAX_PARAMETERS=" + << interp_param_global::RS274NGC_MAX_PARAMETERS << "\n"; + std::cout << "ACTIVE_G_CODES=" << ACTIVE_G_CODES << "\n"; + std::cout << "ACTIVE_M_CODES=" << ACTIVE_M_CODES << "\n"; + std::cout << "ACTIVE_SETTINGS=" << ACTIVE_SETTINGS << "\n"; + std::cout << "INTERP_SUB_ROUTINE_LEVELS=" << INTERP_SUB_ROUTINE_LEVELS << "\n"; + std::cout << "MAX_NAMED_PARAMETERS=" << MAX_NAMED_PARAMETERS << "\n"; + std::cout << "sizeof_setup=" << sizeof(setup) << "\n"; + std::cout << "sizeof_context=" << sizeof(context) << "\n"; + std::cout << "sizeof_parameter_value=" << sizeof(parameter_value) << "\n"; + + return 0; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_namedparam_harness.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_namedparam_harness.cpp new file mode 100644 index 0000000..7a045a6 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_namedparam_harness.cpp @@ -0,0 +1,341 @@ +#define private public +#include "emc/rs274ngc/rs274ngc_interp.hh" +#undef private + +#include +#include +#include + +#include "config.h" +#include "emc/ini/inifile.hh" + +namespace { + +enum predefined_named_parameters { + NP_LINE, + NP_MOTION_MODE, + NP_PLANE, + NP_CCOMP, + NP_METRIC, + NP_IMPERIAL, + NP_ABSOLUTE, + NP_INCREMENTAL, + NP_INVERSE_TIME, + NP_UNITS_PER_MINUTE, + NP_UNITS_PER_REV, + NP_COORD_SYSTEM, + NP_TOOL_OFFSET, + NP_RETRACT_R_PLANE, + NP_RETRACT_OLD_Z, + NP_SPINDLE_RPM_MODE, + NP_SPINDLE_CSS_MODE, + NP_IJK_ABSOLUTE_MODE, + NP_LATHE_DIAMETER_MODE, + NP_LATHE_RADIUS_MODE, + NP_SPINDLE_ON, + NP_SPINDLE_CW, + NP_MIST, + NP_FLOOD, + NP_SPEED_OVERRIDE, + NP_FEED_OVERRIDE, + NP_ADAPTIVE_FEED, + NP_FEED_HOLD, + NP_FEED, + NP_RPM, + NP_CURRENT_TOOL, + NP_SELECTED_POCKET, + NP_CURRENT_POCKET, + NP_X, + NP_Y, + NP_Z, + NP_A, + NP_B, + NP_C, + NP_U, + NP_V, + NP_W, + NP_ABS_X, + NP_ABS_Y, + NP_ABS_Z, + NP_ABS_A, + NP_ABS_B, + NP_ABS_C, + NP_VALUE, + NP_CALL_LEVEL, + NP_REMAP_LEVEL, + NP_SELECTED_TOOL, + NP_VALUE_RETURNED, + NP_TASK, +}; + +struct NamedParamRuntime { + setup state; + + int fetch_ini_param(const char *nameBuf, int *status, double *value) { + *status = 0; + const int n = static_cast(strlen(nameBuf)); + if (n < 8) { + return INTERP_OK; + } + + std::string sect = nameBuf + 5; + for (auto &c : sect) { + c = static_cast(toupper(c)); + } + const size_t i = sect.find(']'); + if (i == std::string::npos) { + return INTERP_ERROR; + } + std::string var = sect.substr(i + 1); + sect.erase(i); + + const char *iniFileName = getenv("INI_FILE_NAME"); + if (!iniFileName) { + return INTERP_OK; + } + linuxcnc::IniFile inifile(iniFileName); + if (!inifile) { + return INTERP_OK; + } + + if (auto inival = inifile.findReal(var, sect)) { + *value = *inival; + *status = 1; + } + return INTERP_OK; + } + + int lookup_named_param(const char *nameBuf, double index, double *value) { + const int cmd = round_to_int(index); + switch (cmd) { + case NP_LINE: + *value = state.sequence_number; + break; + case NP_MOTION_MODE: + *value = state.motion_mode; + break; + case NP_METRIC: + *value = (state.length_units == CANON_UNITS_MM); + break; + case NP_IMPERIAL: + *value = (state.length_units == CANON_UNITS_INCHES); + break; + case NP_ABSOLUTE: + *value = (state.distance_mode == DISTANCE_MODE::ABSOLUTE); + break; + case NP_INCREMENTAL: + *value = (state.distance_mode == DISTANCE_MODE::INCREMENTAL); + break; + case NP_FEED: + *value = state.feed_rate; + break; + case NP_RPM: + *value = state.speed[0]; + break; + case NP_X: + *value = state.current_x; + break; + case NP_Y: + *value = state.current_y; + break; + case NP_Z: + *value = state.current_z; + break; + case NP_CURRENT_TOOL: + *value = state.parameters[interp_param_global::TOOL_NUMBER]; + break; + case NP_CALL_LEVEL: + *value = state.call_level; + break; + default: + return INTERP_ERROR; + } + return INTERP_OK; + } + + int find_named_param(const char *nameBuf, int *status, double *value) { + const int level = (nameBuf[0] == '_') ? 0 : state.call_level; + context_pointer frame = &state.sub_context[level]; + *status = 0; + + auto pi = frame->named_params.find(nameBuf); + if (pi == frame->named_params.end()) { + int exists = 0; + double inivalue = 0.0; + if ((state.feature_set & FEATURE_INI_VARS) && (strncasecmp(nameBuf, "_ini[", 5) == 0)) { + fetch_ini_param(nameBuf, &exists, &inivalue); + if (exists) { + *value = inivalue; + *status = 1; + parameter_value param; + param.value = inivalue; + param.attr = PA_GLOBAL | PA_READONLY | PA_FROM_INI; + state.sub_context[0].named_params[strstore(nameBuf)] = param; + return INTERP_OK; + } + } + *value = 0.0; + *status = 0; + } else { + parameter_pointer pv = &pi->second; + if (pv->attr & PA_USE_LOOKUP) { + if (lookup_named_param(nameBuf, pv->value, value) != INTERP_OK) { + return INTERP_ERROR; + } + *status = 1; + } else { + *value = pv->value; + *status = 1; + } + } + return INTERP_OK; + } + + int store_named_param(const char *nameBuf, double value, bool override_readonly) { + const int level = (nameBuf[0] == '_') ? 0 : state.call_level; + context_pointer frame = &state.sub_context[level]; + auto pi = frame->named_params.find(nameBuf); + if (pi == frame->named_params.end()) { + return INTERP_ERROR; + } + parameter_pointer pv = &pi->second; + if ((pv->attr & PA_READONLY) && !override_readonly) { + return INTERP_ERROR; + } + pv->value = value; + pv->attr &= ~PA_UNSET; + return INTERP_OK; + } + + int add_named_param(const char *nameBuf, int attr) { + int findStatus = 0; + double value = 0.0; + find_named_param(nameBuf, &findStatus, &value); + if (findStatus) { + return INTERP_OK; + } + + int level = 0; + if (nameBuf[0] != '_') { + level = state.call_level; + } else { + level = 0; + attr |= PA_GLOBAL; + } + attr |= PA_UNSET; + + parameter_value param; + param.value = 0.0; + param.attr = attr; + state.sub_context[level].named_params[strstore(nameBuf)] = param; + return INTERP_OK; + } + + int init_readonly_param(const char *nameBuf, double value, int attr) { + if (add_named_param(nameBuf, PA_READONLY | attr) != INTERP_OK) { + return INTERP_ERROR; + } + if (store_named_param(nameBuf, value, true) != INTERP_OK) { + return INTERP_ERROR; + } + return INTERP_OK; + } + + double inicheck() { + const char *filename = getenv("INI_FILE_NAME"); + if (!filename) { + return -1.0; + } + linuxcnc::IniFile inifile(filename); + if (!inifile) { + return -1.0; + } + if (auto inistring = inifile.findString("LINEAR_UNITS", "TRAJ")) { + if ((strcasecmp("mm", inistring->c_str()) == 0) || + (strcasecmp("metric", inistring->c_str()) == 0)) { + return 1.0; + } + return 0.0; + } + return -1.0; + } + + int init_named_parameters() { + const char *pkgversion = PACKAGE_VERSION; + const char *version_major = "_vmajor"; + const char *version_minor = "_vminor"; + const char *metric_machine = "_metric_machine"; + double vmajor = 0.0; + double vminor = 0.0; + double munits = 1.0; + sscanf(pkgversion, "%lf%lf", &vmajor, &vminor); + + if (init_readonly_param(version_major, vmajor, 0) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param(version_minor, vminor, 0) != INTERP_OK) return INTERP_ERROR; + + munits = inicheck(); + if (init_readonly_param(metric_machine, munits, 0) != INTERP_OK) return INTERP_ERROR; + + if (init_readonly_param("_line", NP_LINE, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_motion_mode", NP_MOTION_MODE, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_metric", NP_METRIC, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_imperial", NP_IMPERIAL, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_absolute", NP_ABSOLUTE, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_incremental", NP_INCREMENTAL, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_feed", NP_FEED, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_rpm", NP_RPM, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_x", NP_X, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_y", NP_Y, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_z", NP_Z, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_current_tool", NP_CURRENT_TOOL, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + if (init_readonly_param("_call_level", NP_CALL_LEVEL, PA_USE_LOOKUP) != INTERP_OK) return INTERP_ERROR; + + return INTERP_OK; + } +}; + +void print_named_value(NamedParamRuntime &runtime, const char *name) { + int status = 0; + double value = 0.0; + const int rc = runtime.find_named_param(name, &status, &value); + std::cout << name << ": rc=" << rc << " found=" << status << " value=" << value << "\n"; +} + +} // namespace + +int main(int argc, char **argv) { + if (argc == 2) { + setenv("INI_FILE_NAME", argv[1], 1); + } + + NamedParamRuntime runtime; + runtime.state.feature_set = FEATURE_INI_VARS; + runtime.state.length_units = CANON_UNITS_MM; + runtime.state.distance_mode = DISTANCE_MODE::ABSOLUTE; + runtime.state.motion_mode = G_1; + runtime.state.feed_rate = 123.45; + runtime.state.speed[0] = 678.9; + runtime.state.current_x = 1.25; + runtime.state.current_y = 2.5; + runtime.state.current_z = 3.75; + runtime.state.parameters[interp_param_global::TOOL_NUMBER] = 12.0; + + const int rc = runtime.init_named_parameters(); + std::cout << "init_named_parameters=" << rc << "\n"; + std::cout << "global_named_count=" << runtime.state.sub_context[0].named_params.size() << "\n"; + std::cout << "required_parameter_first=" << interp_param_global::G28_X << "\n"; + std::cout << "readonly_tool_number_index=" << interp_param_global::TOOL_NUMBER << "\n"; + + print_named_value(runtime, "_vmajor"); + print_named_value(runtime, "_vminor"); + print_named_value(runtime, "_metric_machine"); + print_named_value(runtime, "_motion_mode"); + print_named_value(runtime, "_metric"); + print_named_value(runtime, "_feed"); + print_named_value(runtime, "_rpm"); + print_named_value(runtime, "_x"); + print_named_value(runtime, "_current_tool"); + print_named_value(runtime, "_ini[traj]max_linear_velocity"); + + return rc == INTERP_OK ? 0 : 1; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_rs274_compile_probe.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_rs274_compile_probe.cpp new file mode 100644 index 0000000..ab8cc5f --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_rs274_compile_probe.cpp @@ -0,0 +1,5 @@ +#include "emc/rs274ngc/rs274ngc_interp.hh" + +int main() { + return 0; +} diff --git a/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_runtime_state_stubs.cpp b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_runtime_state_stubs.cpp new file mode 100644 index 0000000..0ca2cc2 --- /dev/null +++ b/wasm-port/runtime/core/linuxcnc_wrap/linuxcnc_runtime_state_stubs.cpp @@ -0,0 +1,209 @@ +#include +#include +#include +#include +#include + +#include "emc/rs274ngc/interp_internal.hh" + +struct pycontext_impl {}; + +pycontext::pycontext() : impl(new pycontext_impl) {} +pycontext::~pycontext() { delete impl; } +pycontext::pycontext(const pycontext &other) : impl(new pycontext_impl(*other.impl)) {} +pycontext &pycontext::operator=(const pycontext &other) { + if (this == &other) { + return *this; + } + delete impl; + impl = new pycontext_impl(*other.impl); + return *this; +} + +const char *strstore(const char *s) +{ + static std::unordered_set stringtable; + + if (s == nullptr) { + throw std::invalid_argument("strstore(): NULL argument"); + } + auto pair = stringtable.insert(s); + return pair.first->c_str(); +} + +context_struct::context_struct() + : position(0), sequence_number(0), filename(""), subName(""), + m98_loop_counter(-1), context_status(0), call_type(0) +{ + memset(saved_params, 0, sizeof(saved_params)); + memset(saved_g_codes, 0, sizeof(saved_g_codes)); + memset(saved_m_codes, 0, sizeof(saved_m_codes)); + memset(saved_settings, 0, sizeof(saved_settings)); +} + +void context_struct::clear() +{ + new (this) context_struct(); +} + +setup::setup() + : AA_axis_offset(0.0), + AA_current(0.0), + AA_origin_offset(0.0), + BB_axis_offset(0.0), + BB_current(0.0), + BB_origin_offset(0.0), + CC_axis_offset(0.0), + CC_current(0.0), + CC_origin_offset(0.0), + u_axis_offset(0.0), + u_current(0.0), + u_origin_offset(0.0), + v_axis_offset(0.0), + v_current(0.0), + v_origin_offset(0.0), + w_axis_offset(0.0), + w_current(0.0), + w_origin_offset(0.0), + active_g_codes{}, + active_m_codes{-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}, + active_settings{}, + arc_not_allowed(0), + axis_offset_x(0.0), + axis_offset_y(0.0), + axis_offset_z(0.0), + blocks{}, + remap_level(0), + blocktext{}, + control_mode(CANON_EXACT_STOP), + tolerance(0.0), + naivecam_tolerance(0.0), + tolerance_default(0.0), + naivecam_tolerance_default(0.0), + current_pocket(0), + current_x(0.0), + current_y(0.0), + current_z(0.0), + cutter_comp_radius(0.0), + cutter_comp_orientation(0), + cutter_comp_side(CUTTER_COMP::OFF), + cycle_cc(0.0), + cycle_i(0.0), + cycle_j(0.0), + cycle_k(0.0), + cycle_l(0), + cycle_p(0.0), + cycle_q(0.0), + cycle_r(0.0), + cycle_il(0.0), + cycle_il_flag(0), + distance_mode(DISTANCE_MODE::ABSOLUTE), + ijk_distance_mode(DISTANCE_MODE::ABSOLUTE), + feed_mode(FEED_MODE::UNITS_PER_MINUTE), + feed_override(0), + feed_rate(0.0), + filename{}, + file_pointer(nullptr), + flood(0), + length_units(CANON_UNITS_INCHES), + center_arc_radius_tolerance_inch(CENTER_ARC_RADIUS_TOLERANCE_INCH), + center_arc_radius_tolerance_mm(CENTER_ARC_RADIUS_TOLERANCE_MM), + line_length(0), + linetext{}, + mist(0), + motion_mode(0), + origin_index(0), + origin_offset_x(0.0), + origin_offset_y(0.0), + origin_offset_z(0.0), + rotation_xy(0.0), + parameters{0}, + parameter_occurrence(0), + parameter_numbers{0}, + parameter_values{0}, + named_parameter_occurrence(0), + named_parameters{nullptr}, + named_parameter_values{0}, + percent_flag(0), + plane(CANON_PLANE::XY), + probe_flag(0), + input_flag(0), + toolchange_flag(0), + input_index(0), + input_digital(0), + cutter_comp_firstmove(0), + program_x(0.0), + program_y(0.0), + program_z(0.0), + retract_mode(RETRACT_MODE::R_PLANE), + random_toolchanger(0), + selected_pocket(0), + selected_tool(0), + sequence_number(0), + num_spindles(0), + active_spindle(0), + speed{0.0}, + spindle_mode{SPINDLE_MODE::CONSTANT_RPM}, + speed_feed_mode{CANON_INDEPENDENT}, + speed_override{false}, + spindle_turning{CANON_STOPPED}, + stack{}, + stack_index(0), + tool_offset{{0,0,0},0,0,0,0,0,0}, + tool_table{}, + traverse_rate(0.0), + orient_offset(0.0), + g43_with_zero_offset(false), + defining_sub(0), + sub_name(nullptr), + doing_continue(0), + doing_break(0), + executed_if(0), + skipping_o(nullptr), + skipping_to_sub(nullptr), + skipping_start(0), + test_value(0.0), + return_value(0.0), + value_returned(0), + call_level(0), + sub_context{}, + call_state(0), + adaptive_feed(0), + feed_hold(0), + loggingLevel(0), + debugmask(0), + log_file{}, + program_prefix{}, + subroutines{}, + use_lazy_close(0), + lazy_closing(0), + wizard_root{}, + tool_change_at_g30(0), + tool_change_quill_up(0), + tool_change_with_spindle_on(0), + parameter_g73_peck_clearance(0.0), + parameter_g83_peck_clearance(0.0), + a_axis_wrapped(0), + b_axis_wrapped(0), + c_axis_wrapped(0), + a_indexer_jnum(0), + b_indexer_jnum(0), + c_indexer_jnum(0), + lathe_diameter_mode(0), + mdi_interrupt(0), + feature_set(0), + disable_fanuc_style_sub(false), + loop_on_main_m99(false), + disable_g92_persistence(0), + pythis(nullptr), + on_abort_command(nullptr), + init_once(CANON_STOPPED) +{ + std::fill(parameters, parameters + interp_param_global::RS274NGC_MAX_PARAMETERS, 0); +} + +setup::~setup() { + if (pythis) { + delete pythis; + } +} diff --git a/wasm-port/runtime/core/shims/boost/python/detail/config.hpp b/wasm-port/runtime/core/shims/boost/python/detail/config.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/wasm-port/runtime/core/shims/boost/python/detail/config.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/wasm-port/runtime/core/shims/boost/python/detail/prefix.hpp b/wasm-port/runtime/core/shims/boost/python/detail/prefix.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/wasm-port/runtime/core/shims/boost/python/detail/prefix.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/wasm-port/runtime/core/shims/boost/python/detail/wrap_python.hpp b/wasm-port/runtime/core/shims/boost/python/detail/wrap_python.hpp new file mode 100644 index 0000000..485b3b5 --- /dev/null +++ b/wasm-port/runtime/core/shims/boost/python/detail/wrap_python.hpp @@ -0,0 +1,4 @@ +#pragma once + +struct _object; +typedef _object PyObject; diff --git a/wasm-port/runtime/core/shims/boost/python/object_fwd.hpp b/wasm-port/runtime/core/shims/boost/python/object_fwd.hpp new file mode 100644 index 0000000..7771608 --- /dev/null +++ b/wasm-port/runtime/core/shims/boost/python/object_fwd.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace boost { +namespace python { + +class object {}; + +} // namespace python +} // namespace boost diff --git a/wasm-port/runtime/core/shims/config.h b/wasm-port/runtime/core/shims/config.h new file mode 100644 index 0000000..cf1f3cf --- /dev/null +++ b/wasm-port/runtime/core/shims/config.h @@ -0,0 +1,3 @@ +#pragma once + +#define PACKAGE_VERSION "2.10.0~pre1" diff --git a/wasm-port/runtime/core/shims/fmt/format.h b/wasm-port/runtime/core/shims/fmt/format.h new file mode 100644 index 0000000..c8b496f --- /dev/null +++ b/wasm-port/runtime/core/shims/fmt/format.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace fmt { +using std::format; +} diff --git a/wasm-port/runtime/core/shims/nml_intf/emc.hh b/wasm-port/runtime/core/shims/nml_intf/emc.hh new file mode 100644 index 0000000..bb770fa --- /dev/null +++ b/wasm-port/runtime/core/shims/nml_intf/emc.hh @@ -0,0 +1,7 @@ +#pragma once + +// Minimal shim for the standalone INI parser build. +enum EmcJointType : int { + EMC_LINEAR = 1, + EMC_ANGULAR = 2, +}; diff --git a/wasm-port/runtime/core/shims/pythonplugin/python_plugin.hh b/wasm-port/runtime/core/shims/pythonplugin/python_plugin.hh new file mode 100644 index 0000000..476a506 --- /dev/null +++ b/wasm-port/runtime/core/shims/pythonplugin/python_plugin.hh @@ -0,0 +1,13 @@ +#pragma once + +#include + +class PythonPlugin { +public: + bool usable() const { return false; } + int plugin_status() const { return 0; } + const std::string &last_exception() const { + static std::string empty; + return empty; + } +}; diff --git a/wasm-port/runtime/core/shims/rtapi.h b/wasm-port/runtime/core/shims/rtapi.h new file mode 100644 index 0000000..1f1af74 --- /dev/null +++ b/wasm-port/runtime/core/shims/rtapi.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef RTAPI_BEGIN_DECLS +#ifdef __cplusplus +#define RTAPI_BEGIN_DECLS extern "C" { +#define RTAPI_END_DECLS } +#else +#define RTAPI_BEGIN_DECLS +#define RTAPI_END_DECLS +#endif +#endif + +#ifndef RTAPI_NAME_LEN +#define RTAPI_NAME_LEN 31 +#endif + +typedef enum { + RTAPI_MSG_NONE = 0, + RTAPI_MSG_ERR, + RTAPI_MSG_WARN, + RTAPI_MSG_INFO, + RTAPI_MSG_DBG, + RTAPI_MSG_ALL +} msg_level_t; + +RTAPI_BEGIN_DECLS + +static inline int rtapi_snprintf(char *buf, unsigned long size, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int rc = vsnprintf(buf, static_cast(size), fmt, ap); + va_end(ap); + return rc; +} + +static inline int rtapi_vsnprintf(char *buf, unsigned long size, const char *fmt, va_list ap) { + return vsnprintf(buf, static_cast(size), fmt, ap); +} + +static inline void rtapi_print(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +static inline void rtapi_print_msg(msg_level_t, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +static inline int rtapi_set_msg_level(int) { + return 0; +} + +static inline int rtapi_get_msg_level(void) { + return RTAPI_MSG_ALL; +} + +RTAPI_END_DECLS diff --git a/wasm-port/runtime/core/shims/tooldata/tooldata.hh b/wasm-port/runtime/core/shims/tooldata/tooldata.hh new file mode 100644 index 0000000..03ba30c --- /dev/null +++ b/wasm-port/runtime/core/shims/tooldata/tooldata.hh @@ -0,0 +1,5 @@ +#pragma once + +// Standalone header-only compile probe shim. +// The rs274ngc header path only needs this include to exist so that the +// interpreter state types can be parsed without pulling in full tooldata/NML. diff --git a/wasm-port/runtime/ui/ini-panel/app.js b/wasm-port/runtime/ui/ini-panel/app.js new file mode 100644 index 0000000..8401d23 --- /dev/null +++ b/wasm-port/runtime/ui/ini-panel/app.js @@ -0,0 +1,206 @@ +import createLinuxCncIniModule from "./linuxcnc_ini.js"; + +const SAMPLE_PATH = + "../../../linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"; +const OPFS_FILE = "linuxcnc/xyzab-tdr.ini"; +const WASM_FILE = "/work/xyzab-tdr.ini"; + +const editor = document.getElementById("ini-editor"); +const logNode = document.getElementById("log"); +const wasmBadge = document.getElementById("wasm-badge"); +const opfsBadge = document.getElementById("opfs-badge"); + +const fields = { + machine: document.getElementById("field-machine"), + display: document.getElementById("field-display"), + kinematics: document.getElementById("field-kinematics"), + joints: document.getElementById("field-joints"), + coordinates: document.getElementById("field-coordinates"), + parameterFile: document.getElementById("field-parameter-file"), +}; + +let moduleInstance = null; + +function setBadge(node, text, className = "badge") { + node.className = className; + node.textContent = text; +} + +function setLog(message, isError = false) { + logNode.textContent = message; + logNode.className = `log${isError ? " danger" : ""}`; +} + +function setField(name, value) { + fields[name].textContent = value ?? "-"; +} + +function requireModule() { + if (!moduleInstance) { + throw new Error("WASM module is not ready yet."); + } + return moduleInstance; +} + +function allocCString(mod, value) { + const bytes = mod.lengthBytesUTF8(value) + 1; + const ptr = mod._malloc(bytes); + mod.stringToUTF8(value, ptr, bytes); + return ptr; +} + +function iniQuery(mod, wasmPath, section, tag) { + const pathPtr = allocCString(mod, wasmPath); + const sectionPtr = allocCString(mod, section); + const tagPtr = allocCString(mod, tag); + const outSize = 2048; + const outPtr = mod._malloc(outSize); + + try { + const rc = mod._lcini_get_string(pathPtr, sectionPtr, tagPtr, outPtr, outSize); + if (rc !== 0) { + return null; + } + return mod.UTF8ToString(outPtr); + } finally { + mod._free(pathPtr); + mod._free(sectionPtr); + mod._free(tagPtr); + mod._free(outPtr); + } +} + +async function getOpfsRoot() { + if (!navigator.storage?.getDirectory) { + throw new Error("OPFS is not available in this browser."); + } + return navigator.storage.getDirectory(); +} + +async function ensureParentDir(root, path) { + const parts = path.split("/"); + let current = root; + for (const part of parts.slice(0, -1)) { + current = await current.getDirectoryHandle(part, { create: true }); + } + return current; +} + +async function saveToOpfs(path, text) { + const root = await getOpfsRoot(); + const dir = await ensureParentDir(root, path); + const filename = path.split("/").at(-1); + const fileHandle = await dir.getFileHandle(filename, { create: true }); + const writable = await fileHandle.createWritable(); + await writable.write(text); + await writable.close(); +} + +async function loadFromOpfs(path) { + const root = await getOpfsRoot(); + const parts = path.split("/"); + let current = root; + for (const part of parts.slice(0, -1)) { + current = await current.getDirectoryHandle(part); + } + const fileHandle = await current.getFileHandle(parts.at(-1)); + const file = await fileHandle.getFile(); + return file.text(); +} + +function syncEditorToWasmFs() { + const mod = requireModule(); + try { + mod.FS.mkdir("/work"); + } catch { + // already exists + } + mod.FS.writeFile(WASM_FILE, editor.value, { encoding: "utf8" }); +} + +async function loadSample() { + setLog(`Fetching ${SAMPLE_PATH}`); + const response = await fetch(SAMPLE_PATH); + if (!response.ok) { + throw new Error(`Cannot fetch sample INI (${response.status})`); + } + editor.value = await response.text(); + setLog("Loaded LinuxCNC sample INI into the standalone editor."); +} + +async function boot() { + try { + moduleInstance = await createLinuxCncIniModule(); + setBadge(wasmBadge, "WASM: ready"); + } catch (error) { + setBadge(wasmBadge, "WASM: failed", "badge danger"); + setLog(`WASM init failed: ${error.message}`, true); + throw error; + } + + try { + await getOpfsRoot(); + setBadge(opfsBadge, "OPFS: ready"); + } catch (error) { + setBadge(opfsBadge, "OPFS: unavailable", "badge danger"); + setLog(`OPFS check failed: ${error.message}`, true); + } +} + +document.getElementById("load-sample").addEventListener("click", async () => { + try { + await loadSample(); + } catch (error) { + setLog(error.message, true); + } +}); + +document.getElementById("save-opfs").addEventListener("click", async () => { + try { + await saveToOpfs(OPFS_FILE, editor.value); + setLog(`Saved current INI text to OPFS at ${OPFS_FILE}`); + } catch (error) { + setLog(`OPFS save failed: ${error.message}`, true); + } +}); + +document.getElementById("load-opfs").addEventListener("click", async () => { + try { + editor.value = await loadFromOpfs(OPFS_FILE); + setLog(`Loaded INI text from OPFS path ${OPFS_FILE}`); + } catch (error) { + setLog(`OPFS load failed: ${error.message}`, true); + } +}); + +document.getElementById("sync-wasm").addEventListener("click", () => { + try { + syncEditorToWasmFs(); + setLog(`Synced editor contents to WASM filesystem at ${WASM_FILE}`); + } catch (error) { + setLog(`WASM sync failed: ${error.message}`, true); + } +}); + +document.getElementById("query").addEventListener("click", async () => { + try { + syncEditorToWasmFs(); + const mod = requireModule(); + + setField("machine", iniQuery(mod, WASM_FILE, "EMC", "MACHINE")); + setField("display", iniQuery(mod, WASM_FILE, "DISPLAY", "DISPLAY")); + setField("kinematics", iniQuery(mod, WASM_FILE, "KINS", "KINEMATICS")); + setField("joints", iniQuery(mod, WASM_FILE, "KINS", "JOINTS")); + setField("coordinates", iniQuery(mod, WASM_FILE, "TRAJ", "COORDINATES")); + setField( + "parameterFile", + iniQuery(mod, WASM_FILE, "RS274NGC", "PARAMETER_FILE"), + ); + + setLog("Queried LinuxCNC INI fields through the standalone WASM parser."); + } catch (error) { + setLog(`Query failed: ${error.message}`, true); + } +}); + +boot().catch(() => {}); diff --git a/wasm-port/runtime/ui/ini-panel/index.html b/wasm-port/runtime/ui/ini-panel/index.html new file mode 100644 index 0000000..47de24d --- /dev/null +++ b/wasm-port/runtime/ui/ini-panel/index.html @@ -0,0 +1,189 @@ + + + + + + LinuxCNC WASM INI Panel + + + +
+
+

LinuxCNC WASM INI Panel

+

+ This standalone panel uses LinuxCNC's native IniFile parser + compiled to WASM. It is managed under wasm-port/ and reads + upstream LinuxCNC-style machine configs through the browser host. +

+
+ +
+
+

Configuration File

+
+ + + + + +
+ +
+ +
+

Control Panel

+
+
WASM: loading
+
OPFS: checking
+
+ +
+
Machine
+
-
+
Display
+
-
+
Kinematics
+
-
+
Joints
+
-
+
Coordinates
+
-
+
Parameter File
+
-
+
OPFS Path
+
linuxcnc/xyzab-tdr.ini
+
WASM Path
+
/work/xyzab-tdr.ini
+
+ +
+
+
+
+ + + diff --git a/wasm-port/runtime/ui/ini-panel/linuxcnc_ini.js b/wasm-port/runtime/ui/ini-panel/linuxcnc_ini.js new file mode 100644 index 0000000..ad94218 --- /dev/null +++ b/wasm-port/runtime/ui/ini-panel/linuxcnc_ini.js @@ -0,0 +1,16 @@ + +var Module = (() => { + var _scriptName = import.meta.url; + + return ( +function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];function intArrayFromBase64(s){var decoded=atob(s);var bytes=new Uint8Array(decoded.length);for(var i=0;ifilename.startsWith(dataURIPrefix);function findWasmBinary(){if(Module["locateFile"]){var f="linuxcnc_ini.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("linuxcnc_ini.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else abort("initRandomDevice")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{abort()};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffset2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var FS_createPath=FS.createPath;var FS_unlink=path=>FS.unlink(path);var FS_createLazyFile=FS.createLazyFile;var FS_createDevice=FS.createDevice;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["FS_createPath"]=FS.createPath;Module["FS_createDataFile"]=FS.createDataFile;Module["FS_createPreloadedFile"]=FS.createPreloadedFile;Module["FS_unlink"]=FS.unlink;Module["FS_createLazyFile"]=FS.createLazyFile;Module["FS_createDevice"]=FS.createDevice;var wasmImports={__cxa_throw:___cxa_throw,__syscall_fstat64:___syscall_fstat64,__syscall_lstat64:___syscall_lstat64,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_stat64:___syscall_stat64,_abort_js:__abort_js,_emscripten_memcpy_js:__emscripten_memcpy_js,_tzset_js:__tzset_js,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["__wasm_call_ctors"])();var _lcini_get_string=Module["_lcini_get_string"]=(a0,a1,a2,a3,a4)=>(_lcini_get_string=Module["_lcini_get_string"]=wasmExports["lcini_get_string"])(a0,a1,a2,a3,a4);var _lcini_tilde_expand=Module["_lcini_tilde_expand"]=(a0,a1,a2)=>(_lcini_tilde_expand=Module["_lcini_tilde_expand"]=wasmExports["lcini_tilde_expand"])(a0,a1,a2);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["malloc"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["free"])(a0);var __emscripten_tempret_set=a0=>(__emscripten_tempret_set=wasmExports["_emscripten_tempret_set"])(a0);var __emscripten_tempret_get=()=>(__emscripten_tempret_get=wasmExports["_emscripten_tempret_get"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();var dynCall_viijii=Module["dynCall_viijii"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_viijii=Module["dynCall_viijii"]=wasmExports["dynCall_viijii"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_jiji=Module["dynCall_jiji"]=(a0,a1,a2,a3,a4)=>(dynCall_jiji=Module["dynCall_jiji"]=wasmExports["dynCall_jiji"])(a0,a1,a2,a3,a4);var dynCall_iiiiij=Module["dynCall_iiiiij"]=(a0,a1,a2,a3,a4,a5,a6)=>(dynCall_iiiiij=Module["dynCall_iiiiij"]=wasmExports["dynCall_iiiiij"])(a0,a1,a2,a3,a4,a5,a6);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8)=>(dynCall_iiiiijj=Module["dynCall_iiiiijj"]=wasmExports["dynCall_iiiiijj"])(a0,a1,a2,a3,a4,a5,a6,a7,a8);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9)=>(dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=wasmExports["dynCall_iiiiiijj"])(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9);Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["FS_createPreloadedFile"]=FS_createPreloadedFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +export default Module; diff --git a/wasm-port/runtime/ui/ini-panel/linuxcnc_ini.wasm b/wasm-port/runtime/ui/ini-panel/linuxcnc_ini.wasm new file mode 100755 index 0000000000000000000000000000000000000000..c8db7688504eec335a6d2912e346156b4e6ff740 GIT binary patch literal 347954 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 O*v$Y#@yCV-NdJE%Ps7&$ literal 0 HcmV?d00001 diff --git a/wasm-port/tests/native/verify_native_probes.sh b/wasm-port/tests/native/verify_native_probes.sh new file mode 100755 index 0000000..c37176f --- /dev/null +++ b/wasm-port/tests/native/verify_native_probes.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="$ROOT_DIR/build/native" + +"$ROOT_DIR/tools/build_native_probes.sh" + +check_exitcode() { + local name="$1" + local file="$BUILD_DIR/$name.exitcode" + + if [[ ! -f "$file" ]]; then + echo "missing exitcode: $file" >&2 + exit 1 + fi + + local rc + rc="$(tr -d '[:space:]' < "$file")" + if [[ "$rc" != "0" ]]; then + echo "$name failed with exit code $rc" >&2 + if [[ -f "$BUILD_DIR/$name.stderr.log" ]]; then + sed -n '1,160p' "$BUILD_DIR/$name.stderr.log" >&2 + fi + exit 1 + fi +} + +check_exitcode linuxcnc_interp_state_probe +check_exitcode linuxcnc_namedparam_harness +check_exitcode linuxcnc_interp_minimal_harness +check_exitcode linuxcnc_interp_minimal_harness.run +check_exitcode linuxcnc_rs274_compile_probe + +RUN_STDOUT="$BUILD_DIR/linuxcnc_interp_minimal_harness.run.stdout.log" + +grep -Fq "read=0" "$RUN_STDOUT" +grep -Fq "execute=0" "$RUN_STDOUT" +grep -Fq "execute_feed=0" "$RUN_STDOUT" +grep -Fq "parse_line=0" "$RUN_STDOUT" +grep -Fq "canon_event=STRAIGHT_TRAVERSE line=0 x=1 y=2 z=0" "$RUN_STDOUT" +grep -Fq "canon_event=SET_FEED_RATE rate=120" "$RUN_STDOUT" +grep -Fq "canon_event=STRAIGHT_FEED line=0 x=3 y=4 z=0" "$RUN_STDOUT" + +echo "native probe validation complete" diff --git a/wasm-port/tools/build_ini_panel.sh b/wasm-port/tools/build_ini_panel.sh new file mode 100755 index 0000000..74813b9 --- /dev/null +++ b/wasm-port/tools/build_ini_panel.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +OUT_DIR="$ROOT_DIR/runtime/ui/ini-panel" + +emcc \ + -std=c++20 \ + -O2 \ + -I"$ROOT_DIR/runtime/core/shims" \ + -I"$ROOT_DIR/vendor/linuxcnc/src" \ + -I"$ROOT_DIR/vendor/linuxcnc/src/rtapi" \ + -I"$ROOT_DIR/vendor/linuxcnc/src/emc" \ + "$ROOT_DIR/vendor/linuxcnc/src/emc/ini/inifile.cc" \ + "$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_ini_wasm.cpp" \ + -o "$OUT_DIR/linuxcnc_ini.js" \ + -s MODULARIZE=1 \ + -s EXPORT_ES6=1 \ + -s ENVIRONMENT=web \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s NO_EXIT_RUNTIME=1 \ + -s FORCE_FILESYSTEM=1 \ + -s EXPORTED_FUNCTIONS='["_malloc","_free","_lcini_get_string","_lcini_tilde_expand"]' \ + -s EXPORTED_RUNTIME_METHODS='["FS","UTF8ToString","stringToUTF8","lengthBytesUTF8"]' diff --git a/wasm-port/tools/build_native_probes.sh b/wasm-port/tools/build_native_probes.sh new file mode 100755 index 0000000..68e54a2 --- /dev/null +++ b/wasm-port/tools/build_native_probes.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +VENDOR_DIR="$ROOT_DIR/vendor/linuxcnc" +BUILD_DIR="$ROOT_DIR/build/native" +WRAP_DIR="$ROOT_DIR/runtime/core/linuxcnc_wrap" +SHIM_DIR="$ROOT_DIR/runtime/core/shims" +INCLUDE_DIR="$ROOT_DIR/runtime/core/include" + +mkdir -p "$BUILD_DIR" + +rm -f \ + "$BUILD_DIR/linuxcnc_ini_probe" \ + "$BUILD_DIR/linuxcnc_interp_state_probe" \ + "$BUILD_DIR/linuxcnc_interp_state_probe.exitcode" \ + "$BUILD_DIR/linuxcnc_interp_state_probe.stdout.log" \ + "$BUILD_DIR/linuxcnc_interp_state_probe.stderr.log" \ + "$BUILD_DIR/linuxcnc_namedparam_harness" \ + "$BUILD_DIR/linuxcnc_namedparam_harness.exitcode" \ + "$BUILD_DIR/linuxcnc_namedparam_harness.stdout.log" \ + "$BUILD_DIR/linuxcnc_namedparam_harness.stderr.log" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.exitcode" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.stdout.log" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.stderr.log" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.run.exitcode" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.run.stdout.log" \ + "$BUILD_DIR/linuxcnc_interp_minimal_harness.run.stderr.log" \ + "$BUILD_DIR/linuxcnc_rs274_compile_probe.o" \ + "$BUILD_DIR/linuxcnc_rs274_compile_probe.exitcode" \ + "$BUILD_DIR/linuxcnc_rs274_compile_probe.stdout.log" \ + "$BUILD_DIR/linuxcnc_rs274_compile_probe.stderr.log" + +g++ -std=c++20 -O2 \ + -D_GNU_SOURCE \ + -DM_PI=3.14159265358979323846 \ + -DOBJECT_FWD_DWA2002724_HPP \ + -I"$SHIM_DIR" \ + -I"$INCLUDE_DIR" \ + -I"$VENDOR_DIR/src" \ + -I"$VENDOR_DIR/src/rtapi" \ + -I"$VENDOR_DIR/src/emc" \ + -I"$VENDOR_DIR/src/emc/nml_intf" \ + -I"$VENDOR_DIR/src/emc/motion" \ + -I"$VENDOR_DIR/src/libnml/posemath" \ + "$VENDOR_DIR/src/emc/ini/inifile.cc" \ + "$WRAP_DIR/linuxcnc_ini_probe.cpp" \ + -lfmt \ + -o "$BUILD_DIR/linuxcnc_ini_probe" + +set +e +g++ -std=c++20 -O2 \ + -D_GNU_SOURCE \ + -DM_PI=3.14159265358979323846 \ + -DOBJECT_FWD_DWA2002724_HPP \ + -I"$SHIM_DIR" \ + -I"$INCLUDE_DIR" \ + -I"$VENDOR_DIR/src" \ + -I"$VENDOR_DIR/src/rtapi" \ + -I"$VENDOR_DIR/src/emc" \ + -I"$VENDOR_DIR/src/emc/nml_intf" \ + -I"$VENDOR_DIR/src/emc/motion" \ + -I"$VENDOR_DIR/src/libnml/posemath" \ + "$WRAP_DIR/linuxcnc_interp_state_probe.cpp" \ + -o "$BUILD_DIR/linuxcnc_interp_state_probe" \ + >"$BUILD_DIR/linuxcnc_interp_state_probe.stdout.log" \ + 2>"$BUILD_DIR/linuxcnc_interp_state_probe.stderr.log" +STATE_RC=$? +set -e + +echo "$STATE_RC" > "$BUILD_DIR/linuxcnc_interp_state_probe.exitcode" + +set +e +g++ -std=c++20 -O2 \ + -D_GNU_SOURCE \ + -DM_PI=3.14159265358979323846 \ + -DOBJECT_FWD_DWA2002724_HPP \ + -I"$SHIM_DIR" \ + -I"$INCLUDE_DIR" \ + -I"$VENDOR_DIR/src" \ + -I"$VENDOR_DIR/src/rtapi" \ + -I"$VENDOR_DIR/src/emc" \ + -I"$VENDOR_DIR/src/emc/nml_intf" \ + -I"$VENDOR_DIR/src/emc/motion" \ + -I"$VENDOR_DIR/src/libnml/posemath" \ + "$VENDOR_DIR/src/emc/rs274ngc/modal_state.cc" \ + "$WRAP_DIR/linuxcnc_runtime_state_stubs.cpp" \ + "$WRAP_DIR/linuxcnc_namedparam_harness.cpp" \ + "$VENDOR_DIR/src/emc/ini/inifile.cc" \ + -lfmt \ + -o "$BUILD_DIR/linuxcnc_namedparam_harness" \ + >"$BUILD_DIR/linuxcnc_namedparam_harness.stdout.log" \ + 2>"$BUILD_DIR/linuxcnc_namedparam_harness.stderr.log" +NAMEDPARAM_RC=$? +set -e + +echo "$NAMEDPARAM_RC" > "$BUILD_DIR/linuxcnc_namedparam_harness.exitcode" + +set +e +g++ -std=c++20 -O2 -ffunction-sections -fdata-sections \ + -Wl,--gc-sections \ + -D_GNU_SOURCE \ + -DM_PI=3.14159265358979323846 \ + -DOBJECT_FWD_DWA2002724_HPP \ + -I"$SHIM_DIR" \ + -I"$INCLUDE_DIR" \ + -I"$VENDOR_DIR/src" \ + -I"$VENDOR_DIR/src/rtapi" \ + -I"$VENDOR_DIR/src/emc" \ + -I"$VENDOR_DIR/src/emc/nml_intf" \ + -I"$VENDOR_DIR/src/emc/motion" \ + -I"$VENDOR_DIR/src/libnml/posemath" \ + "$VENDOR_DIR/src/emc/rs274ngc/modal_state.cc" \ + "$VENDOR_DIR/src/emc/rs274ngc/interp_array.cc" \ + "$VENDOR_DIR/src/emc/rs274ngc/interp_internal.cc" \ + "$VENDOR_DIR/src/emc/rs274ngc/interp_read.cc" \ + "$VENDOR_DIR/src/emc/rs274ngc/interp_check.cc" \ + "$WRAP_DIR/linuxcnc_runtime_state_stubs.cpp" \ + "$WRAP_DIR/linuxcnc_interp_minimal_runtime.cpp" \ + "$WRAP_DIR/linuxcnc_interp_minimal_harness.cpp" \ + -lfmt \ + -o "$BUILD_DIR/linuxcnc_interp_minimal_harness" \ + >"$BUILD_DIR/linuxcnc_interp_minimal_harness.stdout.log" \ + 2>"$BUILD_DIR/linuxcnc_interp_minimal_harness.stderr.log" +INTERP_MIN_RC=$? +set -e + +echo "$INTERP_MIN_RC" > "$BUILD_DIR/linuxcnc_interp_minimal_harness.exitcode" + +if [[ "$INTERP_MIN_RC" -eq 0 ]]; then + set +e + "$BUILD_DIR/linuxcnc_interp_minimal_harness" \ + >"$BUILD_DIR/linuxcnc_interp_minimal_harness.run.stdout.log" \ + 2>"$BUILD_DIR/linuxcnc_interp_minimal_harness.run.stderr.log" + INTERP_MIN_RUN_RC=$? + set -e + echo "$INTERP_MIN_RUN_RC" > "$BUILD_DIR/linuxcnc_interp_minimal_harness.run.exitcode" +fi + +set +e +g++ -std=c++20 -O2 \ + -D_GNU_SOURCE \ + -DM_PI=3.14159265358979323846 \ + -DOBJECT_FWD_DWA2002724_HPP \ + -I"$SHIM_DIR" \ + -I"$INCLUDE_DIR" \ + -I"$VENDOR_DIR/src" \ + -I"$VENDOR_DIR/src/rtapi" \ + -I"$VENDOR_DIR/src/emc" \ + -I"$VENDOR_DIR/src/emc/nml_intf" \ + -I"$VENDOR_DIR/src/emc/motion" \ + -I"$VENDOR_DIR/src/libnml/posemath" \ + -c "$WRAP_DIR/linuxcnc_rs274_compile_probe.cpp" \ + -o "$BUILD_DIR/linuxcnc_rs274_compile_probe.o" \ + >"$BUILD_DIR/linuxcnc_rs274_compile_probe.stdout.log" \ + 2>"$BUILD_DIR/linuxcnc_rs274_compile_probe.stderr.log" +RS274_RC=$? +set -e + +echo "$RS274_RC" > "$BUILD_DIR/linuxcnc_rs274_compile_probe.exitcode" +echo "native probes complete" diff --git a/wasm-port/tools/extract_sources.sh b/wasm-port/tools/extract_sources.sh new file mode 100755 index 0000000..c7af09e --- /dev/null +++ b/wasm-port/tools/extract_sources.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +UPSTREAM_DIR="$ROOT_DIR/../linuxcnc" +VENDOR_DIR="$ROOT_DIR/vendor/linuxcnc" +MANIFEST_FILE="$ROOT_DIR/tools/source-manifest.txt" + +copy_file() { + local src_rel="$1" + local src="$UPSTREAM_DIR/$src_rel" + local dst="$VENDOR_DIR/$src_rel" + + if [[ ! -f "$src" ]]; then + echo "missing upstream file: $src" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + echo "copied $src_rel" +} + +if [[ ! -f "$MANIFEST_FILE" ]]; then + echo "missing manifest: $MANIFEST_FILE" >&2 + exit 1 +fi + +while IFS= read -r src_rel; do + [[ -z "$src_rel" ]] && continue + [[ "${src_rel:0:1}" == "#" ]] && continue + copy_file "$src_rel" +done < "$MANIFEST_FILE" + +echo "extraction complete" diff --git a/wasm-port/tools/serve_ini_panel.sh b/wasm-port/tools/serve_ini_panel.sh new file mode 100755 index 0000000..0817d0f --- /dev/null +++ b/wasm-port/tools/serve_ini_panel.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PORT="${1:-8011}" + +python3 -m http.server "$PORT" --directory "$ROOT_DIR" diff --git a/wasm-port/tools/source-manifest.txt b/wasm-port/tools/source-manifest.txt new file mode 100644 index 0000000..1441574 --- /dev/null +++ b/wasm-port/tools/source-manifest.txt @@ -0,0 +1,50 @@ +src/emc/ini/inifile.cc +src/emc/ini/inifile.h +src/emc/ini/inifile.hh +src/rtapi/rtapi_stdint.h +src/rtapi/rtapi_string.h +src/rtapi/rtapi_gfp.h +src/rtapi/rtapi_math.h +src/rtapi/rtapi_byteorder.h +src/emc/nml_intf/emcpos.h +src/emc/nml_intf/emcpose.h +src/emc/linuxcnc.h +src/emc/nml_intf/canon.hh +src/emc/nml_intf/canon_position.hh +src/emc/nml_intf/emc.hh +src/emc/nml_intf/emctool.h +src/emc/nml_intf/debugflags.h +src/emc/nml_intf/interp_return.hh +src/emc/motion/state_tag.h +src/emc/motion/emcmotcfg.h +src/emc/rs274ngc/modal_state.hh +src/emc/rs274ngc/modal_state.cc +src/libnml/posemath/posemath.h +src/libnml/posemath/gomath.h +src/libnml/posemath/gotypes.h +src/libnml/posemath/sincos.h +src/emc/rs274ngc/interp_parameter_def.hh +src/emc/rs274ngc/interp_array.cc +src/emc/rs274ngc/interp_namedparams.cc +src/emc/rs274ngc/interp_internal.hh +src/emc/rs274ngc/interp_fwd.hh +src/emc/rs274ngc/interp_base.hh +src/emc/rs274ngc/interp_base.cc +src/emc/rs274ngc/rs274ngc.hh +src/emc/rs274ngc/rs274ngc_interp.hh +src/emc/rs274ngc/rs274ngc_return.hh +src/emc/rs274ngc/rs274ngc_pre.cc +src/emc/rs274ngc/interp_queue.hh +src/emc/rs274ngc/interp_queue.cc +src/emc/rs274ngc/units.h +src/emc/rs274ngc/interp_internal.cc +src/emc/rs274ngc/interp_write.cc +src/emc/rs274ngc/interp_check.cc +src/emc/rs274ngc/interp_read.cc +src/emc/rs274ngc/interp_execute.cc +src/emc/rs274ngc/interp_find.cc +src/emc/rs274ngc/interp_o_word.cc +src/emc/rs274ngc/interp_convert.cc +src/emc/rs274ngc/interp_cycles.cc +src/emc/rs274ngc/interp_check.cc +src/emc/rs274ngc/interp_execute.cc diff --git a/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.cc b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.cc new file mode 100644 index 0000000..26fd75d --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.cc @@ -0,0 +1,1674 @@ +// +// IniFile - Ini-file reader and query class +// Copyright (C) 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +// FIXME: we don't want to pull in libnml.so +//#include "libnml/rcs/rcs_print.hh" + +#include "nml_intf/emc.hh" + +#include "inifile.hh" + +using namespace linuxcnc; + +// FIXME: +// This should not be printed directly to stderr, although the previous ini +// reader did that. We also do not want to pull in libnml. A new consistent +// linuxcnc global print library with channels should be established instead. +#include +static inline void print_msg(const std::string &str) +{ + std::cerr << str << std::endl; + //rcs_print((str + "\n").c_str()); +} + +// Identifier characters +static const char STR_ID[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789"; + +namespace linuxcnc { +// +//***************************************************************************** +// IniFileTag: Encapsulate a tag of type 'name = value' +//***************************************************************************** +// +class IniFileTag +{ +public: + IniFileTag() {}; + IniFileTag(const std::string &p, unsigned l, size_t s, const std::string &t, const std::string &v) + : path(p), lineno(l), secidx(s), tagname(t), tagvalue(v) + {} + + std::string path; // From which file it came + unsigned lineno; // Line number of TAG=... + size_t secidx; // Section index locator (index into 'sections') + std::string tagname; + std::string tagvalue; // Content, everything after the '=', but trimmed +}; + +// +//***************************************************************************** +// IniFileSection: Encapsulate a section of type '[section]' +//***************************************************************************** +// +class IniFileSection +{ +public: + IniFileSection() {}; + IniFileSection(const std::string &p, unsigned l, size_t s, const std::string &n) + : path(p), lineno(l), secidx(s), secname(n), tags{} + {} + + std::string path; // From which file it came + unsigned lineno; // Line number of [section] + size_t secidx; // Section index locator (for duplicates) + std::string secname; + // Note: tags in a section can become discontinuous when equally named + // sections are merged. Therefore, we cannot depend on a start + // reference/count and want to register them all. + std::vector tags; // References of tags into IniFileContent::_tags in this section in-order +}; + +// +//***************************************************************************** +// IniFileContent: Ini-file section/tag content and parsing +//***************************************************************************** +// +class IniFileContent +{ +public: + IniFileContent(const std::string &path); + + IniFileContent(const IniFileContent &) = delete; + IniFileContent& operator=(const IniFileContent &) = delete; + + operator bool() const { return _isvalid; } + + int tagCount() const { return _tags.size(); } + int sectionCount() const { return _sections.size(); } + + std::optional getTagRef(int idx) const + { if(idx < 0 || idx >= tagCount()) return std::nullopt; else return &_tags[idx]; } + + std::optional getSectionRef(int idx) const + { if(idx < 0 || idx >= sectionCount()) return std::nullopt; else return &_sections[idx]; } + + std::optional getSectionRef(const std::string &s) const + { if(!hasSection(s)) return std::nullopt; else return &_sections[sectionIndex(s)]; } + +private: + bool hasSection(const std::string &s) const { return _sectionmap.find(s) != _sectionmap.end(); }; + size_t sectionIndex(const std::string &s) const { return _sectionmap.find(s)->second; }; + + std::string _filepath; // The ini-file path of the loaded file + ssize_t _currentsection; // Used during parsing + bool _isvalid; // Set to true on successful parse + + // Actual data container + std::vector _sections; // All section in sequence as found + std::vector _tags; // All tags in sequence as found + + // Map of sections by name indexing 'sections' vector + std::map _sectionmap; + + // File loader and parser functions + bool readFile(const std::string &path, std::string &content); + bool parseLine(const std::string &line, const std::string &path, unsigned linenr); + bool processValue(std::string &value, const std::string &path, unsigned linenr); + bool parseContent(const std::string &content, const std::string &path); + + const size_t MAX_INCLUDE_DEPTH = 16; // Nesting more than 16 deep is an error + bool pathPush(const std::string &path) { _pathstack.push_back(path); return _pathstack.size() < MAX_INCLUDE_DEPTH; } + void pathPop() { if(!_pathstack.empty()) _pathstack.pop_back(); } + bool pathIsLoading(const std::string &path) { for(auto const &s : _pathstack) { if(s == path) return true; } return false; } + std::vector _pathstack; +}; + +} // namespace linuxcnc + +// +// The constructor is the top-level ini-file loader +// +IniFileContent::IniFileContent(const std::string &path) : + _filepath(path), + _currentsection(-1), + _isvalid(false), + _sections{}, + _tags{}, + _sectionmap{}, + _pathstack{} +{ + std::string content; + if(!readFile(path, content)) + return; + + // We now have the complete file read in 'content' + pathPush(path); + bool rv = parseContent(content, path); + pathPop(); + + // Should not happen. Just checking... + if(!_pathstack.empty()) { + print_msg(fmt::format("{}: internal error: Include stack level stuck at {} and should be 0", path, _pathstack.size())); + return; + } + _isvalid = rv; +} + +// +// Convert a hex digit string into its value +// +static bool fromHex(const std::string &str, uint32_t &val) +{ + val = 0; + for(auto ch : str) { + if(!std::isxdigit(ch & 0xff)) + return false; + unsigned cval; + if(std::isdigit(ch)) + cval = ch - '0'; + else + cval = std::toupper(ch) - 'A' + 10; + val <<= 4; + val += cval; + } + return true; +} + +// +// Convert a Unicode code point into a UTF-8 string +// +static std::string toUTF8(int32_t v) +{ + if(v <= 0x7f) { + return std::string(1, (char)v); + } + if(v <= 0x7ff) { + std::string seq; + seq += (char)( (v >> 6) | 0xc0); + seq += (char)((v & 0x3f) | 0x80); + return seq; + } + if(v <= 0xffff) { + std::string seq; + seq += (char)( (v >> 12) | 0xe0); + seq += (char)(((v >> 6) & 0x3f) | 0x80); + seq += (char)(((v >> 0) & 0x3f) | 0x80); + return seq; + } + if(v <= 0x10ffff) { + std::string seq; + seq += (char)( (v >> 18) | 0xf0); + seq += (char)(((v >> 12) & 0x3f) | 0x80); + seq += (char)(((v >> 6) & 0x3f) | 0x80); + seq += (char)(((v >> 0) & 0x3f) | 0x80); + return seq; + } + // This is an invalid code point. Just ignore. + return std::string{}; +} + +// +// Scan the string and determine UTF-8 validity by finding the correct amount +// of high-bit sequences and code point validity. Detects overlong encodings, +// UTF-16 surrogates and code points out-of-range. +// +static bool isValidUTF8(const std::string &s) +{ +#define C(i, m) ((s[i]) & (m)) +#define M(i) ((s[i]) & 0xff) + + for(size_t i = 0; i < s.size(); i++) { + size_t n; + if(C(i, 0x80) == 0x00) { // 0b0bbbbbbb + n = 0; // 0x00-0x7f + } else if(C(i, 0xe0) == 0xc0) { // 0b110bbbbb + if(C(i, 0xfe) == 0xc0) // 0xc0 and 0xc1 + return false; // Overlong encoding, overlaps 0x00-0x7f + n = 1; // 0xc0-0xdf -> 0x000080-0x0007ff + } else if(M(i) == 0xed && i+1 < s.size() && M(i+1) >= 0xa0) { + return false; // 0xd800-0xdfff (UTF-16 surrogates) + } else if(C(i, 0xf0) == 0xe0) { // 0b1110bbbb + if(M(i) == 0xe0 && i+1 < s.size() && M(i+1) < 0xa0) + return false; // Overlong encoding, overlaps 0x000080-0x0007ff + n = 2; // 0xe0-0xef -> 0x000800-0x00ffff + } else if(C(i, 0xf8) == 0xf0) { // 0b11110bbb + if(M(i) == 0xf0 && i+1 < s.size() && M(i+1) < 0x90) + return false; // Overlong encoding, overlaps 0x000800-0x00ffff + if((M(i) > 0xf4) || (M(i) == 0xf4 && i+1 < s.size() && M(i+1) >= 0x90)) + return false; // Results in invalid code points 0x110000-0x1fffff + n = 3; // 0xf0-0xf7 -> 0x010000-0x10ffff + } else { // 0b11111xxx + return false; // 5/6 byte UTF-8 not supported + } + // Test if there are enough bytes following + for(size_t j = 0; j < n && i < s.size(); j++) { + if(++i >= s.size() || C(i, 0xc0) != 0x80) + return false; // No more chars available or not 0b10bbbbbb + } + } + return true; + +#undef M +#undef C +} + +// +// Process a value to find strings +// Modifies the 'value' argument to its final version +// Embedded quotes are no special characters and are copied verbatim to the +// output. Comments character # and ; are not special in the value and also +// copied verbatim. +// Escape substitution is performed on the entire value. Embedding characters +// with \ooo or \xHH escapes may result in invalid UTF-8 strings but that is +// checked later. Possible escapes: +// - \[abfnrtv] Standard C-type escapes +// - \[0-3][0-7]{0,2} Standard C-type octal escape +// - \x[a-fA-F0-9]{2} Hex 8-bit character escape +// - \u[a-fA-F0-9]{4} UTF-16 escape (with surrogate support) +// - \U[a-fA-F0-9]{8} UTF-32 escape +// +// UTF-16 surrogate support checks high/low surrogate presence and code points +// are converted into UTF-8. UTF-32 code points checked against the valid range +// (code <= 0x10ffff) and converted into UTF-8. +// Embedded NUL characters (\0) are not allowed and flagged as an error. +// +bool IniFileContent::processValue(std::string &value, const std::string &path, unsigned linenr) +{ + if(value.empty()) + return true; + + // We should be l/r trimmed getting here. + + std::string realstr; + for(size_t pos = 0; pos < value.size(); pos++) { + uint32_t hexval; + + char ch = value[pos]; + if(0 == ch) { + // Trying to add a NUL char + print_msg(fmt::format("{}:{}: error: Embedded literal NUL character not supported", path, linenr)); + return false; + } + + // Check for an escape + if('\\' == ch) { + // An escape, see what follows + size_t cleft = value.size() - pos - 1; // Nr of characters after the '\' + if(cleft < 1) { + // Last char is an escape + // This should never happen because it would have been seen + // as a continuation. The error happens when it fails to + // detect the end quote. + print_msg(fmt::format("{}:{}: internal error: End of line while parsing '\\' escape", path, linenr)); + return false; + } + auto nch = value[pos+1]; // The letter after the escape + switch(nch) { + case 'a': realstr += '\a'; break; + case 'b': realstr += '\b'; break; + case 'f': realstr += '\f'; break; + case 'n': realstr += '\n'; break; + case 'r': realstr += '\r'; break; + case 't': realstr += '\t'; break; + case 'v': realstr += '\v'; break; + case '\\': realstr += '\\'; break; + case '0': // Octal value \0 ... \377 + case '1': + case '2': + case '3': + // Have at least \o, can have \oo or \ooo + hexval = nch - '0'; + if(cleft >= 2 && value[pos+2] >= '0' && value[pos+2] <= '7') { + // Have at least \oo, can have \ooo + hexval <<= 3; + hexval += value[pos+2] - '0'; + if(cleft >= 3 && value[pos+3] >= '0' && value[pos+3] <= '7') { + // Have \ooo + hexval <<= 3; + hexval += value[pos+3] - '0'; + pos++; // Eat the third digit + } + pos++; // Eat the second digit + } + if(!hexval) { + // Trying to embed a NUL char + print_msg(fmt::format("{}:{}: error: Embedded octal NUL character not supported", path, linenr)); + return false; + } + // Note that this can result in invalid UTF-8, but we don't care here + realstr += (char)hexval; + break; + case 'x': // Hex value \xHH + if(cleft < 3) { + // Need at least 3 chars for xHH + print_msg(fmt::format("{}:{}: error: Improper hex escape", path, linenr)); + return false; + } + if(!fromHex(value.substr(pos+2, 2), hexval)) { + // Invalid hex number + print_msg(fmt::format("{}:{}: error: Invalid hex '{}' in hex escape", + path, linenr, value.substr(pos+2, 2))); + return false; + } + if(!hexval) { + // Trying to embed a NUL char + print_msg(fmt::format("{}:{}: error: Embedded hex NUL character not supported", path, linenr)); + return false; + } + // Note that this can result in invalid UTF-8, but we don't care here + realstr += (char)hexval; + pos += 2; // Eat the xHH characters + break; + case 'u': // 16-bit hex \uXXXX (actually, this is an UTF-16 abomination) + if(cleft < 5) { + // Need at least 5 chars for uXXXX + print_msg(fmt::format("{}:{}: error: Improper UTF-16 escape", path, linenr)); + return false; + } + if(!fromHex(value.substr(pos+2, 4), hexval)) { + // Invalid hex number + print_msg(fmt::format("{}:{}: error: Invalid hex value '{}' in UTF-16 escape", + path, linenr, value.substr(pos+2, 4))); + return false; + } + if(!hexval) { + // Trying to embed a NUL char + print_msg(fmt::format("{}:{}: error: Embedded UTF-16 NUL character not supported", path, linenr)); + return false; + } + if(hexval >= 0xd800 && hexval <= 0xdfff) { + // We're in UTF-16 surrogate wonderland + if(hexval >= 0xdc00) { + // The high surrogate must be 0xd800..0xdbff + print_msg(fmt::format("{}:{}: error: Invalid high surrogate value u{:04X} in UTF-16", + path, linenr, hexval)); + return false; + } + if(cleft < 5 + 6 || '\\' != value[pos+6] || 'u' != value[pos+7]) { + // We need to have room for the second surrogate: uXXXX\uYYYY + print_msg(fmt::format("{}:{}: error: Missing low surrogate in UTF-16", + path, linenr, pos)); + return false; + } + uint32_t hexsur; + if(!fromHex(value.substr(pos+8, 4), hexsur)) { + // Invalid hex number + print_msg(fmt::format("{}:{}: error: Invalid hex '{}' in UTF-16 low surrogate escape", + path, linenr, value.substr(pos+8, 4))); + return false; + } + if(hexsur < 0xdc00 || hexsur > 0xdfff) { + // The low surrogate must be 0xdc00..0xdfff + print_msg(fmt::format("{}:{}: error: Invalid low surrogate value u{:04X} in UTF-16", + path, linenr, hexsur)); + return false; + } + hexval = 0x10000 + ((hexval & 0x03ff) << 10) + (hexsur & 0x3ff); + pos += 6; // Eat the entire low surrogate \uYYYY + } + realstr += toUTF8(hexval); + pos += 4; // Eat the XXXX characters + break; + case 'U': // 32-bit hex \UXXXXXXXX (UTF-32) + if(cleft < 9) { + // Need at least 9 chars for UXXXXXXXX + print_msg(fmt::format("{}:{}: error: Improper UTF-32 escape", path, linenr)); + return false; + } + if(!fromHex(value.substr(pos+2, 8), hexval)) { + // Invalid hex number + print_msg(fmt::format("{}:{}: error: Invalid hex value '{}' in UTF-32 escape", + path, linenr, value.substr(pos+2, 8))); + return false; + } + if(!hexval) { + // Trying to embed a NUL char + print_msg(fmt::format("{}:{}: error: Embedded UTF-32 NUL character not supported", path, linenr)); + return false; + } + if(hexval > 0x10ffff) { + // Invalid code point + print_msg(fmt::format("{}:{}: error: Invalid code point U{:08X} in UTF-32 escape", + path, linenr, hexval)); + return false; + } + realstr += toUTF8(hexval); + pos += 8; // Eat the XXXXXXXX characters + break; + default: + print_msg(fmt::format("{}:{}: warning: Improper escape of '{}', ignored", path, linenr, nch)); + realstr += '\\'; // Just add the literal backslash + realstr += nch; + break; + } + pos++; // Eat the escape char + } else { + // Not escaped, just copy + realstr += ch; + } + } + + value = realstr; + return true; +} + +bool IniFileContent::parseLine(const std::string &line, const std::string &path, unsigned linenr) +{ + // Handle include files + if(line.starts_with("#INCLUDE")) { + // Must have a blank after the #INCLUDE directive + if(line.size() <= 8 || !(' ' == line[8] || '\t' == line[8])) { + print_msg(fmt::format("{}:{}: error: Missing filename after #INCLUDE", path, linenr)); + return false; + } + size_t start = line.find_first_not_of(IniFile::STR_WS, 8); // Skip whitespace after #INCLUDE + // #INCLUDE myname.inc + // ^ + // start + if(std::string::npos == start) { + // This should not be possible. If npos, then we skipped all + // whitespace and had whitespace left? Should have detected some + // character because the line is right trimmed before we get here. + print_msg(fmt::format("{}:{}: internal error: Missing filename after #INCLUDE", path, linenr)); + return false; + } + std::string arg = line.substr(start); + IniFile::rtrim(arg); + std::string fname; + if(IniFile::tildeExpand(arg, fname)) { + print_msg(fmt::format("{}:{}: error: Failed to tilde-expand '{}'", path, linenr, arg)); + return false; + } + + // Loading from a subdir means that the includes are relative to it too + size_t pslash = path.find_last_of('/'); + size_t fslash = fname.find_first_of('/'); + if(0 != fslash && std::string::npos != pslash) { + // We have no absolute path in the include filename but we have a + // '/' in the current path. Use the dirname of the current path to + // prepend to the new file to ensure they come from the same + // directory. + fname.insert(0, path.substr(0, pslash + 1)); + } + + // Check the file loading stack if this is being processed to detect + // infinite include loops + if(pathIsLoading(fname)) { + print_msg(fmt::format("{}:{}: error: Include file recursion on loading '{}'", path, linenr, fname)); + return false; + } + std::string content; + if(!readFile(fname, content)) + return false; // Has already printed any message + + // Recurse loading + if(!pathPush(fname)) { + print_msg(fmt::format("{}:{}: error: Maximum include depth ({}) reached on loading '{}'", + path, linenr, MAX_INCLUDE_DEPTH, fname)); + return false; + } + bool rv = parseContent(content, fname); + pathPop(); + return rv; + } + + size_t start = line.find_first_not_of(IniFile::STR_WS); + if(std::string::npos == start) { + // Empty line (only white space) + return true; + } + if('#' == line[start] || ';' == line[start]) { + // Comment until end-of-line + return true; + } + if('[' == line[start]) { + // Section header + // [ SECTION ] + // ^ + // start + size_t end = line.find_first_of("]", start); + if(std::string::npos == end) { + // note: end cannot be 0 because that is occupied by '[' + print_msg(fmt::format("{}:{}: error: Invalid section. Missing ']'", path, linenr)); + return false; + } + // Section header + // [ SECTION ] + // ^ ^ + // start end + std::string sect = line.substr(start+1, end - start - 1); + IniFile::trim(sect); + if(sect.empty()) { + // This happens on '[]' or '[ ]', when no section ID is present + print_msg(fmt::format("{}:{}: error: Invalid section. No content between '[' and ']'", path, linenr)); + return false; + } + if(std::string::npos != sect.find_first_not_of(STR_ID)) { + // There are non ID characters in the section identifier + print_msg(fmt::format("{}:{}: error: Invalid section '{}'. Identifier contains invalid character(s)", + path, linenr, sect)); + return false; + } + if(std::isdigit(sect[0] & 0xff)) { + print_msg(fmt::format("{}:{}: error: Invalid section '{}'. Cannot start with a digit", path, linenr, sect)); + return false; + } + + if(_sectionmap.find(sect) != _sectionmap.end()) { + print_msg(fmt::format("{}:{}: warning: Section '{}' already exists. Merging...", path, linenr, sect)); + _currentsection = _sections[_sectionmap[sect]].secidx; + } else { + _sections.push_back(IniFileSection(path, linenr, _sections.size(), sect)); + _sectionmap[sect] = _sections.size() - 1; + _currentsection = _sections.size() - 1; + } + + // We ignore everything on the line that follows the [section] marker + // But we want to warn... + start = line.find_first_not_of(IniFile::STR_WS, end+1); + if(std::string::npos != start && !('#' == line[start] || ';' == line[start])) { + print_msg(fmt::format("{}:{}: warning: Section header has trailing content, ignored", path, linenr)); + } + return true; + } + + // We must have a tag when we get here. Well, actually, we have a + // non-whitespace character, which should be a tag. + // + // TAG = whatever + // ^ + // start + size_t end = line.find_first_of('=', start); + if(std::string::npos == end) { + // Happens on lines like "VAR" + print_msg(fmt::format("{}:{}: error: Invalid tag '{}'. Expected '=' after tag identifier", + path, linenr, line.substr(start))); + return false; + } + if(end == start) { + // Happens on lines like "= value" + print_msg(fmt::format("{}:{}: error: Invalid tag. Missing identifier before '='", path, linenr)); + return false; + } + // + // TAG = whatever + // ^ ^ + // start end + std::string tag = line.substr(start, end - start); + IniFile::rtrim(tag); + if(std::string::npos != tag.find_first_not_of(STR_ID)) { + // There are non ID characters in the tag identifier + print_msg(fmt::format("{}:{}: error: Invalid tag name '{}'. Identifier contains invalid character(s)", + path, linenr, tag)); + return false; + } + if(std::isdigit(tag[0] & 0xff)) { + print_msg(fmt::format("{}:{}: error: Invalid tag '{}'. Tag identifiers cannot start with a digit", path, linenr, tag)); + return false; + } + + start = line.find_first_not_of(IniFile::STR_WS, end); // Skip whitespace before '=' + if(std::string::npos == start || '=' != line[start]) { + // Happens on lines like "VAR x =..." + print_msg(fmt::format("{}:{}: error: Invalid tag '{}'. Expected '=' after tag identifier", path, linenr, tag)); + return false; + } + + if(_currentsection < 0) { + print_msg(fmt::format("{}:{}: error: Tag '{}' found without prior section definition", path, linenr, tag)); + return false; + } + + // TAG = whatever + // ^ + // end + start = line.find_first_not_of(IniFile::STR_WS, end + 1); // Skip whitespace after '=' + // TAG = whatever + // ^ + // start + std::string value; + if(std::string::npos != start) { + // There was something on the line after '=' + // Copy and rtrim whitespace + value = line.substr(start); + IniFile::rtrim(value); + } // else there was no content after the '=' + + // The 'value' we have now should be left/right trimmed and may be empty. + + // Handle strings and comments + if(!processValue(value, path, linenr)) + return false; + + if(!isValidUTF8(value)) { + print_msg(fmt::format("{}:{}: error: Value contains invalid UTF-8 sequence", path, linenr)); + return false; + } + + // Add the tag + _tags.push_back(IniFileTag(path, linenr, (unsigned)_currentsection, tag, value)); + // Add to the section index + _sections[_currentsection].tags.push_back(_tags.size() - 1); + return true; +} + +bool IniFileContent::parseContent(const std::string &content, const std::string &path) +{ + // Split into lines and parse them + unsigned linenr = 1; + std::string line; + for(size_t pos = 0; pos < content.size();) { + // Search the newline to find a line + size_t eol = content.find_first_of("\n", pos); + if(std::string::npos == eol || eol == content.size()-1) { + // This is EOF with or without terminating newline + line += content.substr(pos); + IniFile::rtrim(line); + if(line[line.size()-1] == '\\') { + // This is a continuation on the last line and there are no + // lines left after this data. A clear error. + print_msg(fmt::format("{}:{}: error: Last line has a continuation", path, linenr)); + return false; + } + return parseLine(line, path, linenr); + } + + // \n this line\r\n + // ^ ^ + // pos eol + // Copy line, without newline + std::string thisline = content.substr(pos, eol - pos); + // Remove trailing whitespace, including possible \r. Note that using + // rtrim here also allows for additional spaces after a continuation, + // but it should be fine to be tolerant and makes it easier for us. + IniFile::rtrim(thisline); + + if(!thisline.empty() && thisline[thisline.size()-1] == '\\') { + // Continuation + thisline.pop_back(); // Remove trailing '\\' + line += thisline; // Add to current line + pos = eol + 1; // Prep for next line + // This will cause continued lines to be counted by the last + // non-continued line. But else we need to do double tracking. + linenr++; + continue; + } else { + line += thisline; + } + + // Not a continuation, we have a whole line to process + if(!parseLine(line, path, linenr)) { + return false; // Message already printed + } + linenr++; + line.clear(); + pos = eol + 1; + } + return true; +} + +bool IniFileContent::readFile(const std::string &path, std::string &content) +{ + // We read in using syscall/C into the string for speed + int fd = ::open(path.c_str(), O_RDONLY); + if(fd < 0) { + print_msg(fmt::format("{}: error: Cannot open ini-file (errno={} ({}))", path, errno, strerror(errno))); + return false; + } + + struct stat sb; + if(fstat(fd, &sb) < 0) { + print_msg(fmt::format("{}: error: Cannot stat ini-file (errno={} ({}))", path, errno, strerror(errno))); + return false; + } + + content.resize(sb.st_size); // Make sure we have room + + ssize_t err; +retry_read: + err = ::read(fd, content.data(), sb.st_size); + if(err < 0) { + if(errno == EINTR) + goto retry_read; + ::close(fd); + print_msg(fmt::format("{}: error: Cannot read ini-file (errno={} ({}))", path, errno, strerror(errno))); + return false; + } + ::close(fd); + if(err != (ssize_t)sb.st_size) { + print_msg(fmt::format("{}: error: Cannot read complete ini-file. Data read ({}) != file size ({})", + path, err, sb.st_size)); + return false; // Couldn't read it all + } + return true; +} + +// +//***************************************************************************** +// IniFileCache: caching singleton +//***************************************************************************** +// +namespace linuxcnc { + +class IniFileCache +{ +private: + IniFileCache() {}; + ~IniFileCache(); + + IniFileCache(const IniFileCache &) = delete; + IniFileCache &operator=(const IniFileCache&) = delete; + + static IniFileCache &getInstance() { + static IniFileCache inst; + return inst; + }; + +public: + static const IniFileContent *getIniFile(const std::string &path); + +private: + // Note that the content are pointers because users can hold a pointer when + // they instantiate IniFile(). That pointer must remain valid throughout + // the lifetime of the IniFile instance and we cannot control that. + // When another thread opens a new file, then the map internals can get + // reallocated and that would invalidate pointers to map entries. + std::map cache; + + // Only one thread can load a file. Otherwise they both could load the same + // one if we get unlucky. + std::mutex locker; +}; + +} // namespace linuxcnc + +IniFileCache::~IniFileCache() +{ + // Release all cached objects from the map + for (auto &kv : cache) { + delete kv.second; + kv.second = nullptr; + } + // The map will subsequently deconstruct itself +} + +const IniFileContent *IniFileCache::getIniFile(const std::string &path) +{ + IniFileCache &ifc = IniFileCache::getInstance(); + std::lock_guard lck(ifc.locker); + + auto c = ifc.cache.find(path); + + if(c != ifc.cache.end()) { + // We have a cached version + return c->second; + } else { + // We don't have it in our cache, load it + IniFileContent *x = new IniFileContent(path); + if(!*x) { + // Load resulted in an error. Get rid of it again. + delete x; + return nullptr; + } + // Got it loaded. Now add to the cache and return it + ifc.cache[path] = x; + return x; + } +} + +// +//***************************************************************************** +// IniFile class +//***************************************************************************** +// +IniFile::IniFile(const std::string &path) + : _inifilecontent(nullptr), + _filepath{} +{ + Open(path); +} + +bool IniFile::Open(const std::string &path) +{ + Close(); + if(auto f = IniFileCache::getIniFile(path)) { + _inifilecontent = f; + _filepath = path; + return true; + } + return false; +} + +bool IniFile::hasOpenError(const std::string &tag, const std::string §ion) const +{ + if(!isOpen()) { + print_msg(fmt::format("{}: error: Attempt to extract '[{}]{}' from invalid or not loaded ini-file.", + _filepath, section, tag)); + return false; + } + return true; +} + +// +// Find all the tags with name 'tag' in (optional) section 'section' and return +// references as a vector. The std::nullopt value is returned upon error. +// +std::optional> IniFile::findTags(const std::string &tag, const std::string §ion) const +{ + if(!hasOpenError(tag, section)) + return std::nullopt; + + std::vector tags; + + if(tag.empty() && section.empty()) { + // This returns all tags from all sections in sequence + for(size_t i = 0; true; i++) { + if(auto t = _inifilecontent->getTagRef(i)) { + tags.push_back(*t); + } else { + break; + } + } + return tags; + } + + if(tag.empty()) { + // Return all the section's values + if(auto sect = _inifilecontent->getSectionRef(section)) { + // Within the section, pick all tags + for(auto t : (*sect)->tags) { // Linked by index in master _tags + if(auto tp = _inifilecontent->getTagRef(t)) { + tags.push_back(*tp); + } + } + } + return tags; + } + + if(section.empty()) { + // Return all values matching tag's name + // Loop over all tags and find those with the proper name + for(size_t i = 0; true; i++) { + if(auto t = _inifilecontent->getTagRef(i)) { + if((*t)->tagname == tag) + tags.push_back(*t); + } else { + // We get a std::nullopt when the list is done + // Return what we gathered + return tags; + } + } + } + + // Find the section + if(auto sect = _inifilecontent->getSectionRef(section)) { + // Within the section, pick the all tags of the requested name + for(auto t : (*sect)->tags) { // Linked by index in master _tags + if(auto tp = _inifilecontent->getTagRef(t)) { + if((*tp)->tagname == tag) { + tags.push_back(*tp); + } + } + } + } + return tags; +} + +// +// Find the num'th 'tag' in (optional) section 'section' and return a reference +// to that tag. The std::nullopt is returned upon error or if the tag is not +// found. +// +std::optional IniFile::findTag(const std::string &tag, const std::string §ion, int num) const +{ + if(!hasOpenError(tag, section)) + return std::nullopt; + + if(section.empty()) { + // Must have a count if no section + if(num < 1) + return std::nullopt; + // Loop over all tags and find those with the proper name + // Empty tag means any (so take the num'th). + for(size_t i = 0; true; i++) { + if(auto t = _inifilecontent->getTagRef(i)) { + if((tag.empty() || (*t)->tagname == tag) && !--num) + return *t; + } else { + return std::nullopt; + } + } + } + + if(num < 1) + num = 1; + // Find the section + if(auto sect = _inifilecontent->getSectionRef(section)) { + // Within the section, pick the num'th tag of the requested name + // With an empty tag just take the num'th + for(auto t : (*sect)->tags) { + if(auto tp = _inifilecontent->getTagRef(t)) { + if((tag.empty() || (*tp)->tagname == tag) && !--num) { + return *tp; + } + } + } + } + + return std::nullopt; +} + +// +// Find 'section' and return a reference to it. The std::nullopt is returned +// upon error or if the section is not found. +// Empty section names are *not* valid. +// +std::optional IniFile::findSection(const std::string §ion) const +{ + if(section.empty() || !hasOpenError({}, section)) + return std::nullopt; + + return _inifilecontent->getSectionRef(section); +} + +// +// Return the path and line number of a specified tag or empty/-1 if not found +// +std::pair IniFile::lineOf(int num, const std::string &tag, const std::string §ion) const +{ + if(auto t = findTag(tag, section, num)) + return {(*t)->path, (*t)->lineno}; + return {{}, -1}; +} + +// +// Helper to get the section name from a tag +// +std::string IniFile::sectionFromTag(const IniFileTag *val) const +{ + if(auto sect = _inifilecontent->getSectionRef(val->secidx)) + return (*sect)->secname; + return ""; +} + +// +// Standard conversion routines +// bool - TRUE/YES/1/ON and FALSE/NO/0/OFF (case insensitive) +// s64 - signed value with optional sign and radix prefix +// u64 - signed value with optional sign and radix prefix +// real - floating point value with optional sign and exponent +// +// These routines return std::nullopt when a conversion fails and an +// appropriate message is emitted. +// +std::optional IniFile::convertBool(const std::string &val) +{ + static const std::map booleanMap = { + { "true", true }, + { "yes", true }, + { "1", true }, + { "on", true }, + { "false", false }, + { "no", false }, + { "0", false }, + { "off", false }, + }; + auto const b = booleanMap.find(val); // Case-insensitive map search + if(b != booleanMap.end()) + return b->second; + return std::nullopt; +} + +std::optional IniFile::convertBool(const IniFileTag *val) const +{ + if(auto b = IniFile::convertBool(val->tagvalue)) + return *b; + + print_msg(fmt::format("{}:{}: error: Invalid boolean value [{}]{}='{}'", + val->path, val->lineno, sectionFromTag(val), val->tagname, val->tagvalue)); + + return std::nullopt; +} + +// +// Helper to get different radix numbers parsed properly +// Supported: +// * [+-]?[0-9]+ Decimal +// * [+-]?0[xX][0-9a-fA-F]+ Hexadecimal +// * [+-]?0[oO][0-7]+ Octal +// * [+-]?0[bB][0-1]+ Binary +// +static int radixAndPrefix(std::string &val) +{ + unsigned pm = 0; // Default no +/- + + int base = 10; // Default to decimal + + // String size must be 3 or more for alternate base values. Two for the + // prefix and at least one digit. A leading +/- may also be present. + // We know that the value from the tag has the leading whitespace removed. + if(val.size() > 1 && ('-' == val[0] || '+' == val[0])) { + pm = 1; + } + + // Detect: [+-]?0[xXoObB]. + if(val.size() > pm+2 && '0' == val[pm]) { + // Set the radix and remove the prefix + switch(val[pm+1]) { + case 'x': case 'X': base = 16; val.erase(pm, 2); break; + case 'o': case 'O': base = 8; val.erase(pm, 2); break; + case 'b': case 'B': base = 2; val.erase(pm, 2); break; + } + } + return base; +} + + +std::optional IniFile::convertSInt(const std::string &_val) +{ + std::string tval = _val; + int base = radixAndPrefix(tval); + char *eptr; + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg("internal error: ConvertSInt(): Cannot set locale to \"C\" for strtoll"); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + rtapi_s64 i = strtoll(tval.c_str(), &eptr, base); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == tval.c_str() || errnosave != 0) { + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("warning: Trailing character(s) in signed integer conversion of '{}'", tval)); + } + return i; +} + +std::optional IniFile::convertSInt(const IniFileTag *val) const +{ + std::string tval = val->tagvalue; + int base = radixAndPrefix(tval); + char *eptr; + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg(fmt::format("{}:{}: internal error: Cannot set locale to \"C\" for strtoll", val->path, val->lineno)); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + rtapi_s64 i = strtoll(tval.c_str(), &eptr, base); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == tval.c_str() || errnosave != 0) { + print_msg(fmt::format("{}:{}: error: Invalid signed integer [{}]{}='{}'", + val->path, val->lineno, sectionFromTag(val), val->tagname, tval)); + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("{}:{}: warning: Trailing character(s) in signed integer conversion ([{}]{}='{}')", + val->path, val->lineno, sectionFromTag(val), val->tagname, tval)); + } + return i; +} + +std::optional IniFile::convertUInt(const std::string &_val) +{ + std::string tval = IniFile::trimcpy(_val); + int base = radixAndPrefix(tval); + char *eptr; + + if(!tval.empty() && tval[0] == '-') { + print_msg(fmt::format("warning: Unsigned integer conversion detected a leading minus sign (-)", tval)); + } + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg("internal error: ConvertUInt(): Cannot set locale to \"C\" for strtoull"); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + rtapi_u64 u = strtoull(tval.c_str(), &eptr, base); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == tval.c_str() || errnosave != 0) { + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("warning: Trailing character(s) in unsigned integer conversion of '{}')", tval)); + } + return u; +} + +std::optional IniFile::convertUInt(const IniFileTag *val) const +{ + std::string tval = val->tagvalue; + int base = radixAndPrefix(tval); + char *eptr; + + if(!tval.empty() && tval[0] == '-') { + print_msg(fmt::format("{}:{}: warning: Unsigned integer conversion detected a leading minus sign (-)", + val->path, val->lineno, tval)); + } + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg(fmt::format("{}:{}: internal error: Cannot set locale to \"C\" for strtoull", val->path, val->lineno)); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + rtapi_u64 u = strtoull(tval.c_str(), &eptr, base); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == tval.c_str() || errnosave != 0) { + print_msg(fmt::format("{}:{}: error: Invalid unsigned integer [{}]{}='{}'", + val->path, val->lineno, sectionFromTag(val), val->tagname, tval)); + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("{}:{}: warning: Trailing character(s) in unsigned integer conversion ([{}]{}='{}')", + val->path, val->lineno, sectionFromTag(val), val->tagname, tval)); + } + return u; +} + +std::optional IniFile::convertReal(const std::string &val) +{ + char *eptr; + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg("internal error: ConvertReal(): Cannot set locale to \"C\" for strtod"); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + double r = strtod(val.c_str(), &eptr); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == val.c_str() || errnosave != 0) { + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("warning: Trailing character(s) in floating point conversion of '{}')", val)); + } + return r; +} + +std::optional IniFile::convertReal(const IniFileTag *val) const +{ + char *eptr; + + // Make sure we always use the C locale for conversion (thread local) + locale_t olc = uselocale(static_cast(0)); + locale_t nlc = newlocale(LC_NUMERIC_MASK, "C", static_cast(0)); + if(static_cast(0) == nlc) { + print_msg(fmt::format("{}:{}: internal error: Cannot set locale to \"C\" for strtod", val->path, val->lineno)); + return std::nullopt; + } + uselocale(nlc); + errno = 0; + double r = strtod(val->tagvalue.c_str(), &eptr); + int errnosave = errno; + uselocale(olc); + freelocale(nlc); + + if(eptr == val->tagvalue.c_str() || errnosave != 0) { + print_msg(fmt::format("{}:{}: error: Invalid floating point [{}]{}='{}'", + val->path, val->lineno, sectionFromTag(val), val->tagname, val->tagvalue)); + return std::nullopt; + } + if(*eptr && !IniFile::isSpace(*eptr)) { + print_msg(fmt::format("{}:{}: warning: Trailing character(s) in floating point conversion ([{}]{}='{}')", + val->path, val->lineno, sectionFromTag(val), val->tagname, val->tagvalue)); + } + return r; +} + +// +// Find the num'th instance of '[section]tag' with optional 'section' +// +std::optional IniFile::findString(int num, const std::string &tag, const std::string §ion) const +{ + if(auto t = findTag(tag, section, num)) { + return (*t)->tagvalue; + } + + return std::nullopt; +} + +std::optional IniFile::findBool(int num, const std::string &tag, const std::string §ion) const +{ + if(auto t = findTag(tag, section, num)) { + return convertBool(*t); + } + return std::nullopt; +} + +// +// Find all instances of a '[section]name' with optional 'section' +// +std::vector IniFile::findStringAll(const std::string &tag, const std::string §ion) const +{ + std::vector vals; + + // Find all matching tags + if(auto t = findTags(tag, section)) { + // Get all their values + for(auto const c : (*t)) + vals.push_back(c->tagvalue); + } + return vals; +} + +std::vector IniFile::findBoolAll(const std::string &tag, const std::string §ion) const +{ + std::vector vals; + + // Find all matching tags + if(auto t = findTags(tag, section)) { + // Get all their values + for(auto const c : (*t)) { + if(auto b = convertBool(c)) + vals.push_back(*b); + } + } + return vals; +} + +std::vector IniFile::findSIntAll(const std::string &tag, const std::string §ion) const +{ + std::vector vals; + + // Find all matching tags + if(auto t = findTags(tag, section)) { + // Get all their values + for(auto const c : (*t)) { + if(auto b = convertSInt(c)) + vals.push_back(*b); + } + } + return vals; +} + +std::vector IniFile::findUIntAll(const std::string &tag, const std::string §ion) const +{ + std::vector vals; + + // Find all matching tags + if(auto t = findTags(tag, section)) { + // Get all their values + for(auto const c : (*t)) { + if(auto b = convertUInt(c)) + vals.push_back(*b); + } + } + return vals; +} + +std::vector IniFile::findRealAll(const std::string &tag, const std::string §ion) const +{ + std::vector vals; + + // Find all matching tags + if(auto t = findTags(tag, section)) { + // Get all their values + for(auto const c : (*t)) { + if(auto b = convertReal(c)) + vals.push_back(*b); + } + } + return vals; +} + +// +// Find the num'th instance of '[section]name' with optional 'section' and +// convert to value. +// Optionally limited to range: mini <= value <= maxi +// +std::optional IniFile::findSInt(int num, const std::string &tag, const std::string §ion, rtapi_s64 mini, rtapi_s64 maxi) const +{ + if(auto t = findTag(tag, section, num)) { + if(auto v = convertSInt(*t)) { + if(*v >= mini && *v <= maxi) { + return v; + } + } + } + return std::nullopt; +} + +std::optional IniFile::findUInt(int num, const std::string &tag, const std::string §ion, rtapi_u64 mini, rtapi_u64 maxi) const +{ + if(auto t = findTag(tag, section, num)) { + if(auto v = convertUInt(*t)) { + if(*v >= mini && *v <= maxi) { + return v; + } + } + } + return std::nullopt; +} + +std::optional IniFile::findReal(int num, const std::string &tag, const std::string §ion, double mini, double maxi) const +{ + if(auto t = findTag(tag, section, num)) { + if(auto v = convertReal(*t)) { + if(*v >= mini && *v <= maxi) { + return v; + } + } + } + return std::nullopt; +} + +// +// Section support: Find all section and return to user +// +std::vector IniFile::findSections() const +{ + std::vector sects; + + for(size_t i = 0; true; i++) { + if(auto const s = _inifilecontent->getSectionRef(i)) { + sects.push_back((*s)->secname); + } else { + break; + } + } + return sects; +} + +// +// Variables support: Find all variables in an optional section and return to user +// +std::vector> IniFile::findVariables(const std::string §ion) const +{ + std::vector> vars; + + if(section.empty()) { + // Without section return all variable names from the master pool + for(size_t i = 0; true; i++) { + if(auto t = _inifilecontent->getTagRef(i)) { + vars.push_back({(*t)->tagname, (*t)->tagvalue}); + } else { + break; + } + } + } else { + // If a section is present, only return those variable names + if(auto sect = _inifilecontent->getSectionRef(section)) { + // Within the section, pick all tags + for(auto t : (*sect)->tags) { // Linked by index in master _tags + if(auto tp = _inifilecontent->getTagRef(t)) { + vars.push_back({(*tp)->tagname, (*tp)->tagvalue}); + } + } + } + } + return vars; +} + +// +// Expand ~/filename to $HOME/filename +// +int IniFile::tildeExpand(const std::string &path, std::string &result) +{ + if(path.size() < 2 || '~' != path[0] || '/' != path[1]) { + // Does not start with "~/", so we do not expand + result = path; // Just copy + return 0; + } + + const char *home = getenv("HOME"); + if(!home) + return -ENOENT; + + result = std::string(home) + path.substr(1); + return 0; +} + +// +//***************************************************************************** +// Helper function - mapping enumerated types +//***************************************************************************** +// +std::optional IniFile::mapLinearUnits(const std::string &str) +{ + // The const map holds pairs for linear units which are valid under the + // [TRAJ] section. These are of the form {"name", value}. + // If the name "name" is encountered in the INI, the value will be used. + static const std::map linearUnitsMap = { + { "mm", 1.0 }, + { "metric", 1.0 }, + { "in", 1/25.4 }, + { "inch", 1/25.4 }, + { "imperial", 1/25.4 }, + }; + if(auto c = IniFile::mapMap(linearUnitsMap, str)) + return *c; + return std::nullopt; +} + +std::optional IniFile::mapAngularUnits(const std::string &str) +{ + // The const map holds pairs for angular units which are valid under + // the [TRAJ] section. These are of the form {"name", value}. + // If the name "name" is encountered in the INI, the value will be used. + static const std::map angularUnitsMap = { + { "deg", 1.0 }, + { "degree", 1.0 }, + { "grad", 0.9 }, + { "gon", 0.9 }, + { "rad", M_PI / 180.0 }, + { "radian", M_PI / 180.0 }, + }; + if(auto c = IniFile::mapMap(angularUnitsMap, str)) + return *c; + return std::nullopt; +} + +std::optional IniFile::mapJointType(const std::string &str) +{ + // Usually found in [JOINT_*]TYPE and [AXIS_*]TYPE + static const std::map jointTypeMap = { + { "LINEAR", EMC_LINEAR }, + { "ANGULAR", EMC_ANGULAR} + }; + if(auto c = IniFile::mapMap(jointTypeMap, str)) + return *c; + return std::nullopt; +} + +// +//***************************************************************************** +// C-API interface routines +//***************************************************************************** +// +extern "C" { + +int iniFindString(const char *inipath, const char *tag, const char *section, char *buf, size_t bufsize) +{ + if(!inipath || !tag || !buf || !bufsize) + return -EINVAL; + + IniFile ini(inipath); + if(!ini) + return -EINVAL; + + if(!section) section = ""; + if(auto v = ini.findString(tag, section)) { + if(v->size() >= bufsize) + return -ENOSPC; // buffer cannot hold string + nul char + strcpy(buf, v->c_str()); + return 0; + } + return -ENOENT; +} + +int iniFindBool(const char *inipath, const char *tag, const char *section, bool *result) +{ + if(!inipath || !tag || !result) + return -EINVAL; + + IniFile ini(inipath); + if(!ini) + return -EINVAL; + + if(!section) section = ""; + if(auto v = ini.findBool(tag, section)) { + *result = *v; + return 0; + } + return -ENOENT; +} + +int iniFindSInt(const char *inipath, const char *tag, const char *section, rtapi_s64 *result) +{ + if(!inipath || !tag || !result) + return -EINVAL; + + IniFile ini(inipath); + if(!ini) + return -EINVAL; + + if(!section) section = ""; + if(auto v = ini.findSInt(1, tag, section)) { + *result = *v; + return 0; + } + return -ENOENT; +} + +int iniFindUInt(const char *inipath, const char *tag, const char *section, rtapi_u64 *result) +{ + if(!inipath || !tag || !result) + return -EINVAL; + + IniFile ini(inipath); + if(!ini) + return -EINVAL; + + if(!section) section = ""; + if(auto v = ini.findUInt(1, tag, section)) { + *result = *v; + return 0; + } + return -ENOENT; +} + +int iniFindDouble(const char *inipath, const char *tag, const char *section, double *result) +{ + if(!inipath || !tag || !result) + return -EINVAL; + + IniFile ini(inipath); + if(!ini) + return -EINVAL; + + if(!section) section = ""; + if(auto v = ini.findReal(1, tag, section)) { + *result = *v; + return 0; + } + return -ENOENT; +} + +// Compatibility function +int iniFindInt(const char *inipath, const char *tag, const char *section, int *result) +{ + rtapi_s64 val; + if(int err = iniFindSInt(inipath, tag, section, &val)) + return err; + *result = (int)val; + return 0; +} + +int TildeExpansion(const char *path, char *buf, size_t bufsize) +{ + std::string p; + if(int err = IniFile::tildeExpand(path, p)) + return err; + + if(p.size() >= bufsize) + return -ENOSPC; // buffer cannot hold string + nul char + + strcpy(buf, p.c_str()); + return 0; +} + +// +// Split string 'str' into tokens using 'delim' as delimiters +// +std::vector IniFile::split(const std::string &delim, const std::string &str) +{ + std::vector toks; + size_t start = str.find_first_not_of(delim); // Start-of-token pos (or npos if only delimters) + size_t end = str.find_first_of(delim, start); // End-of-token pos+1 (or npos if last token) + + // While start and end positions are available (meaning there is a token) + while (!(std::string::npos == end && std::string::npos == start)) { + toks.push_back(str.substr(start, end - start)); // Copy token into vector + start = str.find_first_not_of(delim, end); // Find new start-of-token from old end + end = str.find_first_of(delim, start); // and end-of-token + } + return toks; +} + +} // extern "C" + +// vim: ts=4 sw=4 diff --git a/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.h b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.h new file mode 100644 index 0000000..8973bd7 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.h @@ -0,0 +1,61 @@ +// +// IniFile - Ini-file reader and query class +// Copyright (C) 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// +#ifndef __LINUXCNC_INI_INIFILE_H +#define __LINUXCNC_INI_INIFILE_H + +#ifdef __cplusplus +#warning "Including inifile.h in C++ code is inefficient. You should use the C++ API in inifile.hh instead." +#endif + +#include +#include +#include + +#include + +// +// C-API interface functions +// +#ifdef __cplusplus +extern "C" { +#endif + +// There is no real limit in the C++ version. This value is for compatibility. +// It has been increased from the original 256 to PATH_MAX to allow for full +// paths to be properly encapsulated. +#define INI_MAX_LINELEN PATH_MAX + +int TildeExpansion(const char *file, char *path, size_t size); + +int iniFindString(const char *inipath, const char *tag, const char *section, char *buf, size_t bufsize); +int iniFindBool(const char *inipath, const char *tag, const char *section, bool *result); +int iniFindSInt(const char *inipath, const char *tag, const char *section, rtapi_s64 *result); +int iniFindUInt(const char *inipath, const char *tag, const char *section, rtapi_u64 *result); +int iniFindDouble(const char *inipath, const char *tag, const char *section, double *result); + +// Compatibility with existing code +// Maps to iniFindSInt() and truncates the result +int iniFindInt(const char *inipath, const char *tag, const char *section, int *result); + +#ifdef __cplusplus +} +#endif + +#endif +// vim: ts=4 sw=4 diff --git a/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.hh b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.hh new file mode 100644 index 0000000..4c81f4e --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/ini/inifile.hh @@ -0,0 +1,504 @@ +// +// IniFile - Ini-file reader and query class +// Copyright (C) 2026 B.Stultiens +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// +#ifndef __LINUXCNC_INI_INIFILE_HH +#define __LINUXCNC_INI_INIFILE_HH + +#ifndef __cplusplus +#error "inifile.hh cannot be used in C. Please use the C-API in inifile.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// +// IniFile public methods for extraction of values from the ini-file: +// +// Note that selecting the num'th value is the first argument to prevent +// accidents using wrong arguments for the bool/integer/real versions. This way +// you have to select the num'th as the first argument if you really mean to +// use it and cannot be mistaken as the default or min/max values. You can +// just omit the num if you just want the first value as a convenience method. +// +// All find_X_All() methods, except findStringAll(), will perform conversion to +// the requested type. That also means that mixed value content may result in +// errors and dropped results. Use the find_X_All() methods only when you know +// that the values are all supposed to be of the same type. +// +// Helper methods can be used to query the ini-file to get information about +// sections, variables, paths and line numbers: +// bool hasSection(section) +// bool hasVariable(variable, section) +// bool hasVariable(num, variable, section) +// which all map to: +// bool isSet(variable, section) +// bool isSet(num, variable, section) +// +// Ini-file structural content methods: +// std::vector findSections() +// std::vector> findVariables(section); +// std::pair lineOf(variable, section) +// std::pair lineOf(num, variable, section) +// +// General value extraction methods: +// std::vector findStringAll(tag, section) +// std::optional findString(tag, section) +// std::optional findString(num, tag, section) +// std::string findStringV(tag, section, def) +// std::string findStringV(num, tag, section, def) +// +// std::vector findBoolAll(tag, section) +// std::optional findBool(tag, section) +// std::optional findBool(num, tag, section) +// bool findBoolV(tag, section, def) +// bool findBoolV(num, tag, section, def) +// +// std::vector findSIntAll(tag, section) +// std::optional findSInt(tag, section, mini = INT64_MIN, maxi = INT64_MAX) +// std::optional findSInt(num, tag, section, mini = INT64_MIN, maxi = INT64_MAX) +// rtapi_s64 findSIntV(tag, section, def, mini = INT64_MIN, maxi = INT64_MAX) +// rtapi_s64 findSIntV(num, tag, section, def, mini = INT64_MIN, maxi = INT64_MAX) +// +// std::vector findUIntAll(tag, section) +// std::optional findUInt(tag, section, mini = 0, maxi = UINT64_MAX) +// std::optional findUInt(num, tag, section, mini = 0, maxi = UINT64_MAX) +// rtapi_u64 findUIntV(tag, section, def, mini = 0, maxi = UINT64_MAX) +// rtapi_u64 findUIntV(num, tag, section, def, mini = 0, maxi = UINT64_MAX) +// +// std::vector findRealAll(tag, section) +// std::optional findReal(tag, section, mini = -DBL_MAX, maxi = +DBL_MAX) +// std::optional findReal(num, tag, section, mini = -DBL_MAX, maxi = +DBL_MAX) +// double findRealV(tag, section, def, mini = -DBL_MAX, maxi = +DBL_MAX) +// double findRealV(num, tag, section, def, mini = -DBL_MAX, maxi = +DBL_MAX) +// +// Convenience methods using the (usually 32-bit) integer type are provided and +// map to findSInt: +// std::optional findInt(tag, section, mini = INT_MIN, maxi = INT_MAX) +// std::optional findInt(num, tag, section, mini = INT_MIN, maxi = INT_MAX) +// int findIntV(tag, section, def, mini = INT_MIN, maxi = INT_MAX) +// int findIntV(num, tag, section, def, mini = INT_MIN, maxi = INT_MAX) +// +// Implementing (case [in]sensitive) list type values can be done using the +// IniFile::findMap() template function for case sensitive and case insensitive +// compares. The map defined for type T mapping: +// const std::map = {...} +// const std::map = {...} +// for function: +// T findCustom(const IniFile &ini, const std::string &tag, const std::string §ion, T def) +// +// Example: +// double findUnits(const IniFile &ini, const std::string &tag, const std::string §ion, double def) +// { +// static const std::map unitsMap = { +// { "mm", 1.0 }, +// { "metric", 1.0 }, +// { "in", 1/25.4 }, +// { "inch", 1/25.4 }, +// { "imperial", 1/25.4 }, +// }; +// +// if(auto c = ini.findMap(unitsMap, tag, section)) +// return *c; +// return def; +// } +// + +// Forward declaration (must be outside namespace) +// This is found in emc/nml_intf/emc.hh +enum EmcJointType : int; + +namespace linuxcnc { + +// Forward declaration of internal classes +class IniFileContent; +class IniFileTag; +class IniFileSection; + +// +// Public facing IniFile operations +// +class IniFile +{ +public: + IniFile(const std::string &filePath); + + operator bool() const { return isOpen(); } + bool isOpen() const { return _inifilecontent != nullptr; } + + bool hasSection(const std::string §ion) const { + return isSet(1, "", section); + } + bool hasVariable(int num, const std::string &tag, const std::string §ion) const { + return isSet(num, tag, section); + } + bool hasVariable(const std::string &tag, const std::string §ion) const { + return hasVariable(1, tag, section); + } + + // Returns true if the specified [section]tag is present + bool isSet(int num, const std::string &tag, const std::string §ion) const { + if(tag.empty() && section.empty()) { + // No, we don't have nothing + return false; + } + if(tag.empty()) { + // We can have a section that has no variables in it + return (bool)findSection(section); + } + return (bool)findTag(tag, section, num); + } + bool isSet(const std::string &tag, const std::string §ion) const { + return isSet(1, tag, section); + } + + // Returns the ini-file path and line number of the specified variable + std::pair lineOf(int num, const std::string &tag, const std::string §ion) const; + std::pair lineOf(const std::string &tag, const std::string §ion) const { + return lineOf(1, tag, section); + } + + // Get all variables named 'tag' from (optional) section in a vector. + // Returns an empty vector if none found. + std::vector findStringAll(const std::string &tag, const std::string §ion) const; + std::vector findBoolAll(const std::string &tag, const std::string §ion) const; + std::vector findSIntAll(const std::string &tag, const std::string §ion) const; + std::vector findUIntAll(const std::string &tag, const std::string §ion) const; + std::vector findRealAll(const std::string &tag, const std::string §ion) const; + + // Get the num'th variable named 'tag' from (optional) section. + // Returns std::nullopt if not found + std::optional findString(int num, const std::string &tag, const std::string §ion) const; + std::optional findBool(int num, const std::string &tag, const std::string §ion) const; + + std::optional findString(const std::string &tag, const std::string §ion) const { + return findString(1, tag, section); + } + std::optional findBool(const std::string &tag, const std::string §ion) const { + return findBool(1, tag, section); + } + + // Get numerical values with options bounded ranges. + // Returns std::nullopt if not found or out-of-range. + std::optional findSInt(int num, const std::string &tag, const std::string §ion, + rtapi_s64 mini = INT64_MIN, rtapi_s64 maxi = INT64_MAX) const; + std::optional findUInt(int num, const std::string &tag, const std::string §ion, + rtapi_u64 mini = 0, rtapi_u64 maxi = UINT64_MAX) const; + std::optional findReal(int num, const std::string &tag, const std::string §ion, + double mini = -DBL_MAX, double maxi = +DBL_MAX) const; + + std::optional findSInt(const std::string &tag, const std::string §ion, + rtapi_s64 mini = INT64_MIN, rtapi_s64 maxi = INT64_MAX) const { + return findSInt(1, tag, section, mini, maxi); + } + std::optional findUInt(const std::string &tag, const std::string §ion, + rtapi_u64 mini = 0, rtapi_u64 maxi = UINT64_MAX) const { + return findUInt(1, tag, section, mini, maxi); + } + std::optional findReal(const std::string &tag, const std::string §ion, + double mini = -DBL_MAX, double maxi = +DBL_MAX) const { + return findReal(1, tag, section, mini, maxi); + } + + // Get the num'th value with defaults if not found. + std::string findStringV(int num, const std::string &tag, const std::string §ion, const std::string &def) const { + if(auto v = findString(num, tag, section)) + return *v; + return def; + } + bool findBoolV(int num, const std::string &tag, const std::string §ion, bool def) const { + if(auto v = findBool(num, tag, section)) + return *v; + return def; + } + + std::string findStringV(const std::string &tag, const std::string §ion, const std::string &def) const { + return findStringV(1, tag, section, def); + } + bool findBoolV(const std::string &tag, const std::string §ion, bool def) const { + return findBoolV(1, tag, section, def); + } + + // Find the num'th value within min/max range. + // Returns default value if not found or out-of-range. + // These have a suffix 'V' to counter the overloading ambiguity. + rtapi_s64 findSIntV(int num, const std::string &tag, const std::string §ion, rtapi_s64 def, + rtapi_s64 mini = INT64_MIN, rtapi_s64 maxi = INT64_MAX) const { + if(auto v = findSInt(num, tag, section, mini, maxi)) + return *v; + return def; + } + rtapi_u64 findUIntV(int num, const std::string &tag, const std::string §ion, rtapi_u64 def, + rtapi_u64 mini = 0, rtapi_u64 maxi = UINT64_MAX) const { + if(auto v = findUInt(num, tag, section, mini, maxi)) + return *v; + return def; + } + double findRealV(int num, const std::string &tag, const std::string §ion, double def, + double mini = -DBL_MAX, double maxi = +DBL_MAX) const { + if(auto v = findReal(num, tag, section, mini, maxi)) + return *v; + return def; + } + + rtapi_s64 findSIntV(const std::string &tag, const std::string §ion, rtapi_s64 def, + rtapi_s64 mini = INT64_MIN, rtapi_s64 maxi = INT64_MAX) const { + return findSIntV(1, tag, section, def, mini, maxi); + } + rtapi_u64 findUIntV(const std::string &tag, const std::string §ion, rtapi_u64 def, + rtapi_u64 mini = 0, rtapi_u64 maxi = UINT64_MAX) const { + return findUIntV(1, tag, section, def, mini, maxi); + } + double findRealV(const std::string &tag, const std::string §ion, double def, + double mini = -DBL_MAX, double maxi = +DBL_MAX) const { + return findRealV(1, tag, section, def, mini, maxi); + } + + // Convenience methods + std::optional findInt(int num, const std::string &tag, const std::string §ion, + int mini = INT_MIN, int maxi = INT_MAX) const { + if(auto v = findSInt(num, tag, section, mini, maxi)) + return (int)*v; + return std::nullopt; + } + std::optional findInt(const std::string &tag, const std::string §ion, + int mini = INT_MIN, int maxi = INT_MAX) const { + return findInt(1, tag, section, mini, maxi); + } + int findIntV(int num, const std::string &tag, const std::string §ion, int def, + int mini = INT_MIN, int maxi = INT_MAX) const { + return (int)findSIntV(num, tag, section, def, mini, maxi); + } + int findIntV(const std::string &tag, const std::string §ion, int def, + int mini = INT_MIN, int maxi = INT_MAX) const { + return findIntV(1, tag, section, def, mini, maxi); + } + + // Map-search matching of values returning the mapped value. + // Search is case-sensitive. + template + std::optional findMap(int num, const std::map &map, + const std::string &tag, const std::string §ion = "") const { + if(auto s = findString(num, tag, section)) { + auto const m = map.find(*s); + if(m != map.end()) { + return m->second; + } + } + return std::nullopt; + } + + template + std::optional findMap(const std::map &map, + const std::string &tag, const std::string §ion = "") const { + return findMap(1, map, tag, section); + } + // Map compare function without case + struct caseless { + struct caseless_cmp { + bool operator() (const char &a, const char &b) const { + return std::tolower(a & 0xff) < std::tolower(b & 0xff); + } + }; + bool operator() (const std::string &a, const std::string &b) const { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), caseless_cmp()); + } + }; + + // Map-search matching of values returning the mapped value. + // Search is case-insensitive. + template + std::optional findMap(int num, const std::map &map, + const std::string &tag, const std::string §ion = "") const { + if(auto s = findString(num, tag, section)) { + auto const m = map.find(*s); + if(m != map.end()) { + return m->second; + } + } + return std::nullopt; + } + + template + std::optional findMap(const std::map &map, + const std::string &tag, const std::string §ion = "") const { + return findMap(1, map, tag, section); + } + + template + std::optional static mapMap(const std::map &map, + const std::string &str) { + auto const m = map.find(str); + if(m != map.end()) { + return m->second; + } + return std::nullopt; + } + + // + // Mapping functions for enumerated types so they become consistent + // throughout the code base. They take a string argument and match it to + // the mapped values: + // - mapLinearUnits() maps {mm, metric, in, inch, imperial} + // - mapAngularUnits() maps {deg, degree, grad, gon, rad, radian} + // - mapJointType() maps {LINEAR, ANGULAR} + // + static std::optional mapLinearUnits(const std::string &str); + static std::optional mapAngularUnits(const std::string &str); + static std::optional mapJointType(const std::string &str); + + // The following find*() both lookup the ini variable and attempt to + // convert to the associated numerical value. + std::optional findLinearUnits(int num, const std::string &var, const std::string &sec) const { + if(auto c = findString(num, var, sec)) + return mapLinearUnits(*c); + return std::nullopt; + } + std::optional findAngularUnits(int num, const std::string &var, const std::string &sec) const { + if(auto c = findString(num, var, sec)) + return mapAngularUnits(*c); + return std::nullopt; + } + std::optional findJointType(int num, const std::string &var, const std::string &sec) const { + if(auto c = findString(num, var, sec)) + return mapJointType(*c); + return std::nullopt; + } + + double findLinearUnits(const std::string &var, const std::string &sec, double def) const { + if(auto m = findLinearUnits(1, var, sec)) + return *m; + return def; + } + double findAngularUnits(const std::string &var, const std::string &sec, double def) const { + if(auto m = findAngularUnits(1, var, sec)) + return *m; + return def; + } + EmcJointType findJointType(const std::string &var, const std::string &sec, EmcJointType def) const { + if(auto m = findJointType(1, var, sec)) + return *m; + return def; + } + + // Return a list of section names from the ini-file + std::vector findSections() const; + // Return a list of variable name/value pairs from an optional section in the ini-file + std::vector> findVariables(const std::string §ion) const; + + // The fact that this is here is because of compatibility + // Perform tilde expansion using HOME environment variable. + // Returns the filePath "~/path" as "$HOME/path" + // Zero is returned on success or a negative value (-errno) on failure. + static int tildeExpand(const std::string &filePath, std::string &res); + + // Compatibility method + static int TildeExpansion(const std::string &filePath, std::string &res) { + return IniFile::tildeExpand(filePath, res); + } + + static std::optional convertBool(const std::string &val); + static std::optional convertSInt(const std::string &val); + static std::optional convertUInt(const std::string &val); + static std::optional convertReal(const std::string &val); + + // split() Tokenize 'str' based on 'delim' + static std::vector split(const std::string &delim, const std::string &str); + + // Trim leading/trailing or both + static void rtrim(std::string &str) { + size_t n = str.find_last_not_of(IniFile::STR_WS); + if(std::string::npos != n) + str.erase(n+1); + } + static void ltrim(std::string &str) { + if(str.empty()) + return; + size_t n = str.find_first_not_of(IniFile::STR_WS); + if(std::string::npos == n) + str.clear(); // Only whitespace + else + str.erase(0, n); + } + + static void trim(std::string &str) { + rtrim(str); + ltrim(str); + } + + // Trim on a copy and return the trimmed copy + static std::string rtrimcpy(const std::string &str) { + std::string cpy = str; + rtrim(cpy); + return cpy; + } + + static std::string ltrimcpy(const std::string &str) { + std::string cpy = str; + ltrim(cpy); + return cpy; + } + + static std::string trimcpy(const std::string &str) { + std::string cpy = str; + rtrim(cpy); + ltrim(cpy); + return cpy; + } + + // White-space characters for argument to find_first_of() and the like. + // Using static constexpr std::string does not seem to work on Debian 11 + // with clang-19 and older than that. Gcc on debian 11 and before doesn't + // support enough C++20 to build LinuxCNC at all. + static constexpr char STR_WS[] =" \t\v\f\r\n"; + + // This isSpace is guaranteed not to depend on locale + static bool isSpace(char c) { + return std::string::npos != (std::string{STR_WS}).find(c); + } +private: + bool Open(const std::string &filePath); + bool Close() { _inifilecontent = nullptr; _filepath.clear(); return true; } + + bool hasOpenError(const std::string &tag, const std::string §ion) const; + std::string sectionFromTag(const IniFileTag *val) const; + + std::optional findSection(const std::string §ion) const; + std::optional findTag(const std::string &tag, const std::string §ion, int num) const; + std::optional> findTags(const std::string &tag, const std::string §ion) const; + + std::optional convertBool(const IniFileTag *val) const; + std::optional convertSInt(const IniFileTag *val) const; + std::optional convertUInt(const IniFileTag *val) const; + std::optional convertReal(const IniFileTag *val) const; + + const IniFileContent *_inifilecontent; + std::string _filepath; +}; + +} // namespace linuxcnc + +#endif +// vim: ts=4 sw=4 diff --git a/wasm-port/vendor/linuxcnc/src/emc/linuxcnc.h b/wasm-port/vendor/linuxcnc/src/emc/linuxcnc.h new file mode 100644 index 0000000..d9e8b01 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/linuxcnc.h @@ -0,0 +1,27 @@ +/******************************************************************** +* Description: linuxcnc.h +* Common defines used in many emc2 source files. +* +* +* Author: Petter Reinholdtsen +* License: LGPL Version 2 +* System: Any +* +* Copyright (c) 2021 All rights reserved. +********************************************************************/ + +#ifndef __LINUXCNC_LINUXCNC_H +#define __LINUXCNC_LINUXCNC_H + +/* LINELEN is used throughout for buffer sizes, length of file name strings, + etc. Let's just have one instead of a multitude of defines all the same. */ +#define LINELEN 255 + +/* Used in a number of places for sprintf() buffers. */ +#define BUFFERLEN 80 + +/* Imperial/Metric conversion */ +#define MM_PER_INCH 25.4 +#define INCH_PER_MM (1.0/MM_PER_INCH) + +#endif /* LINUXCNC_H */ diff --git a/wasm-port/vendor/linuxcnc/src/emc/motion/emcmotcfg.h b/wasm-port/vendor/linuxcnc/src/emc/motion/emcmotcfg.h new file mode 100644 index 0000000..dff3f60 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/motion/emcmotcfg.h @@ -0,0 +1,75 @@ +/******************************************************************** +* Description: emcmotcfg.h +* Default values for compile-time parameters. +* +* Derived from a work by Fred Proctor & Will Shackleford +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +********************************************************************/ +#ifndef __LINUXCNC_EMCMOTCFG_H +#define __LINUXCNC_EMCMOTCFG_H + +/* default name of EMCMOT INI file */ +#define DEFAULT_EMCMOT_INIFILE "emc.ini" /* same as for EMC-- we're in + touch */ + +/* number of joints supported + Note: this is not a global variable but a compile-time parameter + since it sets array sizes, etc. */ + +// total number of joints available (kinematics_joints + extra_joints) +#define EMCMOT_MAX_JOINTS 16 + +// number of extra joints (NOT used in kinematics calculations): +#define EMCMOT_MAX_EXTRAJOINTS EMCMOT_MAX_JOINTS + +/* number of axes defined by the interp */ //FIXME: shouldn't be here.. +#define EMCMOT_MAX_AXIS 9 + +#define EMCMOT_MAX_SPINDLES 8 +#define EMCMOT_MAX_DIO 64 +#define EMCMOT_MAX_AIO 64 +#define EMCMOT_MAX_MISC_ERROR 64 + +#if (EMCMOT_MAX_DIO > 64) || (EMCMOT_MAX_AIO > 64) +#error A 64 bit bitmask is used in the planner. Don't increase these until that's fixed. +#endif + +#define EMCMOT_ERROR_NUM 32 /* how many errors we can queue */ +#define EMCMOT_ERROR_LEN 1024 /* how long error string can be */ + +/* + Shared memory keys for simulated motion process. No base address + values need to be computed, since operating system does this for us + */ +#define DEFAULT_SHMEM_KEY 100 + +/* default comm timeout, in seconds */ +#define DEFAULT_EMCMOT_COMM_TIMEOUT 1.0 + +/* initial velocity, accel used for coordinated moves */ +#define DEFAULT_VELOCITY 1.0 +#define DEFAULT_ACCELERATION 10.0 + +/* maximum and minimum limit defaults for all axes */ +#define DEFAULT_MAX_LIMIT 1000 +#define DEFAULT_MIN_LIMIT -1000 + +/* default number of motion io pins */ +#define DEFAULT_DIO 4 +#define DEFAULT_AIO 4 +#define DEFAULT_MISC_ERROR 0 + +/* size of motion queue + * a TC_STRUCT is about 512 bytes so this queue is + * about a megabyte. */ +#define DEFAULT_TC_QUEUE_SIZE 2000 + +/* max following error */ +#define DEFAULT_MAX_FERROR 100 + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/motion/state_tag.h b/wasm-port/vendor/linuxcnc/src/emc/motion/state_tag.h new file mode 100644 index 0000000..4d61b3c --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/motion/state_tag.h @@ -0,0 +1,140 @@ +/******************************************************************** +* Description: state_tag.h +* +* A "tag" struct that is used to add interpreter state information to +* a given motion line. This state info isn't actually used by motion +* directly, but indicates the motion state. +* +* Copyright © 2015 Robert W. Ellenberg +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +********************************************************************/ + + +#ifndef STATE_TAG_H +#define STATE_TAG_H + + +/** + * Enum to define bit names for StateTag's flags register. + * The actual position of the flag isn't important, so the actual index doesn't + * matter. However, the bit field should be <=64 bits to fit within a long int. + */ +typedef enum { + GM_FLAG_UNITS, + GM_FLAG_DISTANCE_MODE, + GM_FLAG_TOOL_OFFSETS_ON, + GM_FLAG_RETRACT_OLDZ, + GM_FLAG_BLEND, + GM_FLAG_EXACT_STOP, + GM_FLAG_FEED_INVERSE_TIME, + GM_FLAG_FEED_UPM, + GM_FLAG_CSS_MODE, + GM_FLAG_IJK_ABS, + GM_FLAG_DIAMETER_MODE, + GM_FLAG_G92_IS_APPLIED, + GM_FLAG_SPINDLE_ON, + GM_FLAG_SPINDLE_CW, + GM_FLAG_MIST, + GM_FLAG_FLOOD, + GM_FLAG_FEED_OVERRIDE, + GM_FLAG_SPEED_OVERRIDE, + GM_FLAG_ADAPTIVE_FEED, + GM_FLAG_FEED_HOLD, + GM_FLAG_RESTORABLE, + GM_FLAG_IN_REMAP, + GM_FLAG_IN_SUB, + GM_FLAG_EXTERNAL_FILE, + GM_FLAG_IS_CIRCLE, + GM_FLAG_MAX_FLAGS +} StateFlag; + + +/** + * Enum for various fields of state info that are int type. + * + * WARNING: + * + * 1) Since these are used as array indices, they have to start at 0, + * be monotonic, and the GM_FIELD_MAX_FIELDS enum MUST be last in the list. + * + * 2) If your application needs to pass state tags through NML, then + * you MUST update the corresponding cms->update function for state + * tags. + * + * TODO: make that standalone function a method here for maintainability + */ +typedef enum { + GM_FIELD_LINE_NUMBER, + GM_FIELD_G_MODE_0, + GM_FIELD_CUTTER_COMP, + GM_FIELD_MOTION_MODE, + GM_FIELD_PLANE, + GM_FIELD_M_MODES_4, + GM_FIELD_ORIGIN, + GM_FIELD_TOOLCHANGE, + GM_FIELD_MAX_FIELDS +} StateField; + + +/** + * Enum for indexing state tag `fields_float`, machine state float + * array: feed, speed, etc. + */ +typedef enum { + GM_FIELD_FLOAT_LINE_NUMBER, // eww + GM_FIELD_FLOAT_FEED, + GM_FIELD_FLOAT_SPEED, + GM_FIELD_FLOAT_PATH_TOLERANCE, + GM_FIELD_FLOAT_NAIVE_CAM_TOLERANCE, + GM_FIELD_FLOAT_ARC_RADIUS, + GM_FIELD_FLOAT_ARC_CENTER_X, + GM_FIELD_FLOAT_ARC_CENTER_Y, + GM_FIELD_FLOAT_ARC_CENTER_Z, + GM_FIELD_FLOAT_STRAIGHT_HEADING, + GM_FIELD_FLOAT_NORMAL_HEADING, + GM_FIELD_FLOAT_MAX_FIELDS +} StateFieldFloat; + +/** + * Tag structure that is added to a motion segment so that motion has a copy of + * the relevant interp state. + * + * Previously, this information was stored only in the interpreter, and as + * vectors of g codes, m codes, and settings. Considering that the write_XXX + * and gen_XXX functions had to jump through hoops to translate from a settings + * struct, the extra packing here isn't much more complex to deal with, and + * will cost much less space when copying back and forth. + */ +struct state_tag_t { + + // Float-type machine settings: feed, speed, etc., indexed by the + // StateFieldFloat enum above + float fields_float[GM_FIELD_FLOAT_MAX_FIELDS]; + + // Any G / M code states that doesn't pack nicely into a single bit + // These are an array mostly because it's easier to pass an + // arbitrary-length array through NML than individual fields + int fields[GM_FIELD_MAX_FIELDS]; + + /** G / M mode flags for simple states like inch / mm, feedhold enable, etc. + * This stores packed bits in one field (since we can't use a bitset in a + * pure C struct). + */ + unsigned long int packed_flags; + char filename[256]; +}; + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon.hh b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon.hh new file mode 100644 index 0000000..79b64ca --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon.hh @@ -0,0 +1,1069 @@ +/******************************************************************** +* Description: canon.hh +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +********************************************************************/ +#ifndef CANON_HH +#define CANON_HH + +#include // FILE +#include + +#include +#include "emctool.h" +#include "canon_position.hh" +#include // Just for EMCMOT_NUM_SPINDLES +#include "rs274ngc/modal_state.hh" + +/* + This is the header file that all applications that use the + canonical commands for three- to nine-axis machining should include. + + Three mutually orthogonal (in a right-handed system) X, Y, and Z axes + are always present. In addition, there may be zero to three rotational + axes: A (parallel to the X-axis), B (parallel to the Y-axis), and C + (parallel to the Z-axis). Additionally there may be zero to three linear + axes: U, V and W. + + In the functions that use rotational axes, the axis value is that of a + wrapped linear axis, in degrees. + + It is assumed in these activities that the spindle tip is always at + some location called the 'current location,' and the controller always + knows where that is. It is also assumed that there is always a + 'selected plane' which must be the XY-plane, the YZ-plane, or the + ZX-plane of the machine. +*/ + +enum CanonBool { + OFF, + ON +}; + +struct NURBS_PLANE_POINT { + double NURBS_X, + NURBS_Y; +}; + +struct NURBS_CONTROL_POINT { // type for NURBS control points + double NURBS_X, // in questa struttura vengono dichiarati i seguenti elementi: coordinate X, Y dei punti di controllo + NURBS_Y, // Per l'algoritmo DE-Boor + NURBS_W; // Algoritmo di suddivisione +}; + +struct NURBS_G6_CONTROL_POINT { /* type for NURBS G6 control points */ + double NURBS_X, + NURBS_Y, + NURBS_R, // this is the R value from gcode + NURBS_K; +}; + +struct NURBS_G6_DPLANE_POINT { + double DX, + DY; +}; //è impiegata per salvare le derivate x'(u) y'(u). + + +enum class CANON_PLANE { + XY = 1, + YZ, + XZ, + UV, + VW, + UW, +}; + +enum CANON_UNITS +{ + CANON_UNITS_INCHES = 1, + CANON_UNITS_MM, + CANON_UNITS_CM, +}; + +enum CANON_MOTION_MODE +{ + CANON_EXACT_STOP = 1, + CANON_EXACT_PATH, + CANON_CONTINUOUS, +}; + +enum CANON_SPEED_FEED_MODE { + CANON_SYNCHED = 1, + CANON_INDEPENDENT, +}; + +enum CANON_DIRECTION { + CANON_STOPPED = 1, + CANON_CLOCKWISE, + CANON_COUNTERCLOCKWISE, +}; + +enum CANON_FEED_REFERENCE { + CANON_WORKPIECE = 1, + CANON_XYZ, +}; + +enum CANON_SIDE +{ + CANON_SIDE_RIGHT = 1, + CANON_SIDE_LEFT, + CANON_SIDE_OFF, +}; + +enum CANON_AXIS +{ + CANON_AXIS_X = 1, + CANON_AXIS_Y, + CANON_AXIS_Z, + CANON_AXIS_A, + CANON_AXIS_B, + CANON_AXIS_C, + CANON_AXIS_U, + CANON_AXIS_V, + CANON_AXIS_W, +}; + +struct CANON_VECTOR { + CANON_VECTOR() { + } CANON_VECTOR(double _x, double _y, double _z) { + x = _x; + y = _y; + z = _z; + } + double x, y, z; +}; + +typedef struct { + int feed_mode; + int synched; + double speed; + int dir; + double css_maximum; + double css_factor; +} CanonSpindle_t; + +typedef struct CanonConfig_t { + CanonConfig_t() + : xy_rotation(0.0), + rotary_unlock_for_traverse(-1), + g5xOffset{}, + g92Offset{}, + endPoint{}, + lengthUnits(CANON_UNITS_INCHES), + activePlane(CANON_PLANE::XY), + toolOffset{}, + motionMode(CANON_EXACT_STOP), + motionTolerance(0.0), + naivecamTolerance(0.0), + feed_mode(0), + spindle_num(0), + spindle{}, + linearFeedRate(0.0), + angularFeedRate(0.0), + optional_program_stop(false), + block_delete(false), + cartesian_move(0), + angular_move(0) + {} + + double xy_rotation; + int rotary_unlock_for_traverse; // jointnumber or -1 + + CANON_POSITION g5xOffset; + CANON_POSITION g92Offset; +/* + canonEndPoint is the last programmed end point, stored in case it's + needed for subsequent calculations. It's in absolute frame, mm units. + + note that when segments are queued for the naive cam detector that the + canonEndPoint may not be the last programmed endpoint. get_last_pos() + retrieves the xyz position after the last of the queued segments. these + are also in absolute frame, mm units. + */ + CANON_POSITION endPoint; + CANON_UNITS lengthUnits; + CANON_PLANE activePlane; +/* Tool length offset is saved here */ + EmcPose toolOffset; +/* motion control mode is used to signify blended v. stop-at-end moves. + Set to 0 (invalid) at start, so first call will send command out */ + CANON_MOTION_MODE motionMode; +/* motion path-following tolerance is used to set the max path-following + deviation during CANON_CONTINUOUS. + If this param is 0, then it will behave as emc always did, allowing + almost any deviation trying to keep speed up. */ + double motionTolerance; + double naivecamTolerance; + int feed_mode; + int spindle_num; //current spindle for spindle-synch motion + CanonSpindle_t spindle[EMCMOT_MAX_SPINDLES]; + +/* Prepped tool is saved here */ +// int preppedTool; +/* + Feed rate is saved here; values are in mm/sec or deg/sec. + It will be initially set in INIT_CANON() below. +*/ + double linearFeedRate; + double angularFeedRate; +/* optional program stop */ + bool optional_program_stop; +/* optional block delete */ + bool block_delete; +/* Used to indicate whether the current move is linear, angular, or + a combination of both. */ + //AJ says: linear means axes XYZ move (lines or even circles) + // angular means axes ABC move + int cartesian_move; + int angular_move; +} CanonConfig_t; + +/* Initialization */ + +/* reads world model data into the canonical interface */ +extern void INIT_CANON(); + +/* Representation */ + +extern void SET_G5X_OFFSET(int origin, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w); + +extern void SET_G92_OFFSET(double x, double y, double z, + double a, double b, double c, + double u, double v, double w); + +extern void SET_XY_ROTATION(double t); + +/* Offset the origin to the point with absolute coordinates x, y, z, +a, b, c, u, v, and w. Values of x, y, z, a, b, c, u, v, and w are real +numbers. The units are whatever length units are being used at the time +this command is given. */ + +extern void CANON_UPDATE_END_POINT(double x, double y, double z, + double a, double b, double c, + double u, double v, double w); +/* Called from emctask to update the canon position during skipping through + programs started with start-from-line > 0. */ + +extern void USE_LENGTH_UNITS(CANON_UNITS u); + +/* Use the specified units for length. Conceptually, the units must +be either inches or millimeters. */ + +extern void SELECT_PLANE(CANON_PLANE pl); + +/* Use the plane designated by selected_plane as the selected plane. +Conceptually, the selected_plane must be the XY-plane, the XZ-plane, or +the YZ-plane. */ + +/* Free Space Motion */ + +extern void SET_TRAVERSE_RATE(double rate); + +/* Set the traverse rate that will be used when the spindle traverses. It +is expected that no cutting will occur while a traverse move is being +made. */ + +extern void STRAIGHT_TRAVERSE(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w); +/* + +Move at traverse rate so that at any time during the move, all axes +have covered the same proportion of their required motion. The final +XYZ position is given by x, y, and z. If there is an a-axis, its final +position is given by a_position, and similarly for the b-axis and c-axis. +A more positive value of a rotational axis is in the counterclockwise +direction. + +Clockwise or counterclockwise is from the point of view of the +workpiece. If the workpiece is fastened to a turntable, the turntable +will turn clockwise (from the point of view of the machinist or anyone +else not moving with respect to the machining center) in order to make +the tool move counterclockwise from the point of view of the +workpiece. + +*/ + +/* Machining Attributes */ + +extern void SET_FEED_RATE(double rate); + +/* + +SET_FEED_RATE sets the feed rate that will be used when the spindle is +told to move at the currently set feed rate. The rate is either: +1. the rate of motion of the tool tip in the workpiece coordinate system, + which is used when the feed_reference mode is "CANON_WORKPIECE", or +2. the rate of motion of the tool tip in the XYZ axis system, ignoring + motion of other axes, which is used when the feed_reference mode is + "CANON_XYZ". + +The units of the rate are: + +1. If the feed_reference mode is CANON_WORKPIECE: +length units (inches or millimeters according to the setting of +CANON_UNITS) per minute along the programmed path as seen by the +workpiece. + +2. If the feed_reference mode is CANON_XYZ: +A. For motion including one rotational axis only: degrees per minute. +B. For motion of two or three rotational axes with X, Y, Z, U, V, and W + axes not moving, the rate is applied as follows. Let dA, dB, and dC + be the angles in degrees through which the A, B, and C axes, + respectively, must move. Let D = sqrt(dA*dA + dB*dB + dC*dC). + Conceptually, D is a measure of total angular motion, using the usual + Euclidean metric. Let T be the amount of time required to move through + D degrees at the current feed rate in degrees per minute. The + rotational axes should be moved in coordinated linear motion so that + the elapsed time from the start to the end of the motion is T plus any + time required for acceleration or deceleration. +C. For motion of secondary linear axes (U, V, and/or W) with X, Y, and Z + axes not moving (with or without simultaneous rotational axis motion): + length units (inches or millimeters according to the setting of + CANON_UNITS) per minute in the UVW cartesian system. +D. For motion involving one or more of the XYZ axes (with or without + simultaneous motion of other axes): length units (inches or + millimeters according to the setting of CANON_UNITS) per minute + along the programmed XYZ path. + +*/ + +extern void SET_FEED_REFERENCE(CANON_FEED_REFERENCE reference); + +/* + +This sets the feed_reference mode to either CANON_WORKPIECE or +CANON_XYZ. + +The CANON_WORKPIECE mode is more natural and general, since the rate +at which the tool passes through the material must be controlled for +safe and effective machining. For machines with more than the three +standard XYZ axes, however, computing the feed rate may be +time-consuming because the trajectories that result from motion in +four or more axes may be complex. Computation of path lengths when +only XYZ motion is considered is quite simple for the two standard +motion types (straight lines and helical arcs). + +Some programming languages (rs274kt, in particular) use CANON_XYZ +mode. In these languages, the task of dealing with the rate at which +the tool tip passes through material is pushed back on the NC-program +generator, where the computation of path lengths is (almost always in +1995) an off-line activity where speed of calculation is not critical. + +In CANON_WORKPIECE mode, some motions cannot be carried out as fast as +the programmed feed rate would require because axis motions tend to +cancel each other. For example, an arc in the YZ-plane can exactly +cancel a rotation around the A-axis, so that the location of the tool +tip with respect to the workpiece does not change at all during the +motion; in this case, the motion should take no time, which is +impossible at any finite rate of axis motion. In such cases, the axes +should be moved as fast as possible consistent with accurate +machining. + +It would be possible to omit the SET_FEED_REFERENCE command from the +canonical commands and operate always in one mode or the other, +letting the interpreter issue SET_FEED_RATE commands, if necessary to +compensate if the NC language being interpreted used the other mode. + +This would create two disadvantages when the feed_reference mode +assumed by the canonical commands differed from that assumed by the NC +language being interpreted: + +1. The output code could have a lot of SET_FEED_RATE commands not +found in the input code; this is a relatively minor consideration. + +2. If the interpreter reads a program in language which uses the +CANON_XYZ mode and writes canonical commands in the CANON_WORKPIECE +mode, both the interpreter and the executor of the output canonical +commands would have to perform a lot of complex calculations. With the +SET_FEED_REFERENCE command available, both do only simple calculations +for the same motions. + +*/ + +extern void SET_FEED_MODE(int spindle, int mode); + +/* This sets the feed mode: 0 for feed in units per minute, and 1 for feed in + * units per revolution. In units per revolution mode, the values are in + * inches per revolution (G20 in effect) or mm per minute (G21 in effect) + * The spindle number indicates which spindle the movement is synchronised to */ + +extern void SET_MOTION_CONTROL_MODE(CANON_MOTION_MODE mode, double tolerance); + +extern void SET_NAIVECAM_TOLERANCE(double tolerance); + +/* + +This sets the motion control mode to one of: CANON_EXACT_STOP, +CANON_EXACT_PATH, or CANON_CONTINUOUS. + +For CANON_CONTINUOUS another parameter defines the maximum path deviation. +If tolerance=0 then any path deviation may occur, speed is maximized. + +*/ + +extern void SET_CUTTER_RADIUS_COMPENSATION(double radius); + +/* Set the radius to use when performing cutter radius compensation. */ + +extern void START_CUTTER_RADIUS_COMPENSATION(int direction); + +/* Conceptually, the direction must be left (meaning the cutter +stays to the left of the programmed path) or right. */ + +extern void STOP_CUTTER_RADIUS_COMPENSATION(); + +/* Do not apply cutter radius compensation when executing spindle +translation commands. */ + +/* used for threading */ +extern void START_SPEED_FEED_SYNCH(int spindle, double feed_per_revolution, bool velocity_mode); +extern void STOP_SPEED_FEED_SYNCH(); + + +/* Machining Functions */ + +extern void ARC_FEED(int lineno, + double first_end, double second_end, + double first_axis, double second_axis, int rotation, + double axis_end_point, + double a, double b, double c, + double u, double v, double w); + +/* Move in a helical arc from the current location at the existing feed +rate. The axis of the helix is parallel to the x, y, or z axis, +according to which one is perpendicular to the selected plane. The +helical arc may degenerate to a circular arc if there is no motion +parallel to the axis of the helix. + +1. If the selected plane is the xy-plane: +A. first_end is the x-coordinate of the end of the arc. +B. second_end is the y-coordinate of the end of the arc. +C. first_axis is the x-coordinate of the axis (center) of the arc. +D. second_axis is the y-coordinate of the axis. +E. axis_end_point is the z-coordinate of the end of the arc. + +2. If the selected plane is the yz-plane: +A. first_end is the y-coordinate of the end of the arc. +B. second_end is the z-coordinate of the end of the arc. +C. first_axis is the y-coordinate of the axis (center) of the arc. +D. second_axis is the z-coordinate of the axis. +E. axis_end_point is the x-coordinate of the end of the arc. + +3. If the selected plane is the zx-plane: +A. first_end is the z-coordinate of the end of the arc. +B. second_end is the x-coordinate of the end of the arc. +C. first_axis is the z-coordinate of the axis (center) of the arc. +D. second_axis is the x-coordinate of the axis. +E. axis_end_point is the y-coordinate of the end of the arc. + +If rotation is positive, the arc is traversed counterclockwise as +viewed from the positive end of the coordinate axis perpendicular to +the currently selected plane. If rotation is negative, the arc is +traversed clockwise. If rotation is 0, first_end and second_end must +be the same as the corresponding coordinates of the current point and +no arc is made (but there may be translation parallel to the axis +perpendicular to the selected plane and motion along the rotational axes). +If rotation is 1, more than 0 but not more than 360 degrees of arc +should be made. In general, if rotation is n, the amount of rotation +in the arc should be more than ([n-1] x 360) but not more than (n x +360). + +The radius of the helix is determined by the distance from the current +location to the axis of helix or by the distance from the end location +to the axis of the helix. It is recommended that the executing system +verify that the two radii are the same (within some tolerance) at the +beginning of executing this function. + +While the XYZ motion is going on, move the rotational axes so that +they have always covered the same proportion of their total motion as +a point moving along the arc has of its total motion. + +*/ + +extern void STRAIGHT_FEED(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w); + +/* Additional functions needed to calculate nurbs G5 points */ + +extern std::vector nurbs_G5_knot_vector_creator(unsigned int n, unsigned int k); + +extern double nurbs_G5_Nmix(unsigned int i, unsigned int k, double u, const std::vector& knot_vector); + +extern double nurbs_G5_Rden(double u, unsigned int k, + const std::vector& nurbs_control_points, + const std::vector& knot_vector); + +extern NURBS_PLANE_POINT nurbs_G5_point(double u, unsigned int k, + const std::vector& nurbs_control_points, + const std::vector& knot_vector); + +extern NURBS_PLANE_POINT nurbs_G5_tangent(double u, unsigned int k, + const std::vector& nurbs_control_points, + const std::vector& knot_vector); + +/* Additional functions needed to calculate nurbs points G_6_2*/ + +extern std::vector nurbs_g6_knot_vector_creator(unsigned int n, unsigned int k, const std::vector& nurbs_control_points); + +std::vector nurbs_interval_span_knot_vector_creator(unsigned int n, unsigned int k, const std::vector& knot_vector_); + +extern double nurbs_lderv(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +extern double nurbs_Sa1_b1_length_(double a1, double b1, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +extern std::vector nurbs_lenght_vector_creator(unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_, const std::vector& span_knot_vector); + +extern double nurbs_lenght_tot(int j, const std::vector& span_knot_vector, const std::vector& lenght_vector); + +extern double nurbs_lenght_l_u(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_, const std::vector& span_knot_vector, const std::vector& lenght_vector); + +extern std::vector nurbs_Du_span_knot_vector_creator(unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_, const std::vector& span_knot_vector); + +extern std::vector nurbs_costant_crator(const std::vector& span_knot_vector, const std::vector& lenght_vector, const std::vector& Du_span_knot_vector); + +extern double nurbs_uj_l(double l, const std::vector& span_knot_vector, const std::vector& lenght_vector, const std::vector& nurbs_costant); + +/* Funzioni di supporto all'algoritmo di suddivisione */ +extern std::vector nurbs_G6_new_control_point_nurbs1(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_ ); + +extern std::vector nurbs_G6_new_control_point_nurbs2(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_ ); + +extern std::vector nurbs_G6_knot_vector_new_creator_sgment(unsigned int k, const std::vector& nurbs_control_points); +/* ... */ + +extern double nurbs_G6_Nmix(unsigned int i, unsigned int k, double u, const std::vector& knot_vector_); + +extern std::vector< std::vector > nurbs_G6_Nmix_creator(double u, unsigned int k, double n, const std::vector& knot_vector_); + +extern double nurbs_G6_Rden(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +extern NURBS_PLANE_POINT nurbs_G6_point(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +extern double nurbs_G6_Rdenx(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_, const std::vector< std::vector >& A6); + +extern NURBS_PLANE_POINT nurbs_G6_pointx(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_, const std::vector< std::vector >& A6); + +extern NURBS_PLANE_POINT nurbs_G6_point_x(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +extern NURBS_PLANE_POINT nurbs_G6_tangent_x(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_); + +/*Funzioni di supporto all'algoritmo di suddivisione********************************************************************/ +extern std::vector nurbs_G6_new_control_point_nurbs1(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_ ); + +extern std::vector nurbs_G6_new_control_point_nurbs2(double u, unsigned int k, const std::vector& nurbs_control_points, const std::vector& knot_vector_ ); + +extern std::vector nurbs_G6_knot_vector_new_creator_sgment(unsigned int k, const std::vector& nurbs_control_points); + +/* End of NURBS functions*/ + + +/* Canon calls */ + +extern void NURBS_G5_FEED(int lineno, const std::vector& nurbs_control_points, unsigned int nurbs_order, CANON_PLANE plane); +/* Move at the feed rate along an approximation of a NURBS with a variable number + * of control points + */ + +extern void NURBS_G6_FEED(int lineno, const std::vector& nurbs_control_points, unsigned int k, double feedrate, int l, CANON_PLANE plane); +// this is for G6xx + +extern double alpha_finder(double dx, double dy); + + +/********************************************************************************************************************/ + +/* Move at existing feed rate so that at any time during the move, +all axes have covered the same proportion of their required motion. +The meanings of the parameters is the same as for STRAIGHT_TRAVERSE.*/ + +extern void RIGID_TAP(int lineno, + double x, double y, double z, double scale); + +/* Move linear and synced with the previously set pitch. +Only linear moves are allowed, axes A,B,C are not allowed to move.*/ + + +extern void STRAIGHT_PROBE(int lineno, + double x, double y, double z, + double a, double b, double c, + double u, double v, double w, unsigned char probe_type); + +/* Perform a probing operation. This is a temporary addition to the +canonical machining functions and its semantics are not defined. +When the operation is finished, all axes should be back where they +started. */ + +extern void STOP(); + +/* stop motion after current feed */ + +extern void DWELL(double seconds); + +/* freeze x,y,z for a time */ + +/* Spindle Functions */ + +extern void SET_SPINDLE_MODE(int spindle, double mode); +extern void SPINDLE_RETRACT_TRAVERSE(); + +/* Retract the spindle at traverse rate to the fully retracted position. */ + +extern void START_SPINDLE_CLOCKWISE(int spindle, int wait_for_atspeed = 1); + +/* Turn the spindle clockwise at the currently set speed rate. If the +spindle is already turning that way, this command has no effect. */ + +extern void START_SPINDLE_COUNTERCLOCKWISE(int spindle, int wait_for_atspeed = 1); + +/* Turn the spindle counterclockwise at the currently set speed rate. If +the spindle is already turning that way, this command has no effect. */ + +extern void SET_SPINDLE_SPEED(int spindle, double r); + +/* Set the spindle speed that will be used when the spindle is turning. +This is usually given in rpm and refers to the rate of spindle +rotation. If the spindle is already turning and is at a different +speed, change to the speed given with this command. */ + +extern void STOP_SPINDLE_TURNING(int spindle); + +/* Stop the spindle from turning. If the spindle is already stopped, this +command may be given, but it will have no effect. */ + +extern void SPINDLE_RETRACT(); +extern void ORIENT_SPINDLE(int spindle, double orientation, int mode); +extern void WAIT_SPINDLE_ORIENT_COMPLETE(int spindle, double timeout); +extern void LOCK_SPINDLE_Z(); +extern void USE_SPINDLE_FORCE(); +extern void USE_NO_SPINDLE_FORCE(); + +/* Tool Functions */ +extern void SET_TOOL_TABLE_ENTRY(int pocket, int toolno, const EmcPose& offset, double diameter, + double frontangle, double backangle, int orientation); +extern void USE_TOOL_LENGTH_OFFSET(const EmcPose& offset); + +extern void CHANGE_TOOL(); + +/* It is assumed that each cutting tool in the machine is assigned to a +slot (intended to correspond to a slot number in a tool carousel). +This command results in the tool currently in the spindle (if any) +being returned to its slot, and the tool from the slot designated by +slot_number (if any) being inserted in the spindle. + +If there is no tool in the slot designated by the slot argument, there +will be no tool in the spindle after this command is executed and no +error condition will result in the controller. Similarly, if there is +no tool in the spindle when this command is given, no tool will be +returned to the carousel and no error condition will result in the +controller, whether or not a tool was previously selected in the +program. + +It is expected that when the machine tool controller is initialized, +the designated slot for a tool already in the spindle will be +established. This may be done in any manner deemed fit, including +(for, example) recording that information in a persistent, crash-proof +location so it is always available from the last time the machine was +run, or having the operator enter it. It is expected that the machine +tool controller will remember that information as long as it is +not re-initialized; in particular, it will be remembered between +programs. + +For the purposes of this command, the tool includes the tool holder. + +For machines which can carry out a select_tool command separately from +a change_tool command, the select_tool command must have been given +before the change_tool command, and the value of slot must be the slot +number of the selected tool. */ + +extern void SELECT_TOOL(int tool); + +extern void CHANGE_TOOL_NUMBER(int number); +extern void RELOAD_TOOLDATA(void); + +/* In extension to the comment above - for CHANGE_TOOL, sometimes on +startup one would want to tell emc2 what tool it has loaded. As the last +toolnumber before shutdown isn't currently written, there is no provision +to allow emc2 to safely restart knowing what tool is in the spindle. +Using CHANGE_TOOL_NUMBER one can tell emc2 (without any physical action) +to set the mapping of the currently loaded tool to a certain number */ + +/* Miscellaneous Functions */ + +extern void CLAMP_AXIS(CANON_AXIS axis); + +/* Clamp the given axis. If the machining center does not have a clamp +for that axis, this command should result in an error condition in the +controller. + +An attempt to move an axis while it is clamped should result in an +error condition in the controller. */ + +extern void COMMENT(const char *s); + +/* This function has no physical effect. If commands are being printed or +logged, the comment command is printed or logged, including the string +which is the value of comment_text. This serves to allow formal +comments at specific locations in programs or command files. */ + +/* used for EDM adaptive moves with motion internal feed override (0..1) */ +extern void DISABLE_ADAPTIVE_FEED(); +extern void ENABLE_ADAPTIVE_FEED(); + +/* used to deactivate user control of feed override */ +extern void DISABLE_FEED_OVERRIDE(); +extern void ENABLE_FEED_OVERRIDE(); + +/* used to deactivate user control of spindle speed override */ +extern void DISABLE_SPEED_OVERRIDE(int spindle); +extern void ENABLE_SPEED_OVERRIDE(int spindle); + +/* used to deactivate user control of feed hold */ +extern void DISABLE_FEED_HOLD(); +extern void ENABLE_FEED_HOLD(); + + +extern void FLOOD_OFF(); +/* Turn flood coolant off. */ +extern void FLOOD_ON(); +/* Turn flood coolant on. */ + +extern void MESSAGE(char *s); + +extern void LOG(char *s); +extern void LOGOPEN(char *s); +extern void LOGAPPEND(char *s); +extern void LOGCLOSE(); + +extern void MIST_OFF(); +/* Turn mist coolant off. */ + +extern void MIST_ON(); +/* Turn mist coolant on. */ + +extern void PALLET_SHUTTLE(); + +/* If the machining center has a pallet shuttle mechanism (a mechanism +which switches the position of two pallets), this command should cause +that switch to be made. If either or both of the pallets are missing, +this will not result in an error condition in the controller. + +If the machining center does not have a pallet shuttle, this command +should result in an error condition in the controller. */ + +extern void TURN_PROBE_OFF(); +extern void TURN_PROBE_ON(); + +extern void UNCLAMP_AXIS(CANON_AXIS axis); + +/* Unclamp the given axis. If the machining center does not have a clamp +for that axis, this command should result in an error condition in the +controller. */ + +/* NURB Functions */ +extern void NURB_KNOT_VECTOR(); /* double knot values, -1.0 signals done */ +extern void NURB_CONTROL_POINT(int i, double x, double y, double z, + double w); +extern void NURB_FEED(double sStart, double sEnd); + + +/* Block delete */ +extern void SET_BLOCK_DELETE(bool enabled); +/* Command to set the internal reference of block delete. +The ON value for enabled will cause the interpreter to discard lines +that start with the "/" character. */ + +extern bool GET_BLOCK_DELETE(void); +/* Command to get the internal reference of optional block delete. */ + + +/* Program Functions */ +extern void OPTIONAL_PROGRAM_STOP(); +/* If the machining center has an optional stop switch, and it is on +when this command is read from a program, stop executing the program +at this point, but be prepared to resume with the next line of the +program. If the machining center does not have an optional stop +switch, or commands are being executed with a stop after each one +already (such as when the interpreter is being used with keyboard +input), this command has no effect. */ + +extern void SET_OPTIONAL_PROGRAM_STOP(bool state); +/* Command to set the internal reference of optional program stop. +Any non-zero value for state will cause the execution to stop on +optional stops. */ + +extern bool GET_OPTIONAL_PROGRAM_STOP(); +/* Command to get the internal reference of optional program stop. */ + +extern void PROGRAM_END(); +/* If a program is being read, stop executing the program and be prepared +to accept a new program or to be shut down. */ + +extern void PROGRAM_STOP(); +/* If this command is read from a program, stop executing the program at +this point, but be prepared to resume with the next line of the +program. If commands are being executed with a stop after each one +already (such as when the interpreter is being used with keyboard +input), this command has no effect. */ + + +/* Commands to set/reset output bits and analog values */ +extern void SET_MOTION_OUTPUT_BIT(int index); +extern void CLEAR_MOTION_OUTPUT_BIT(int index); +extern void SET_AUX_OUTPUT_BIT(int index); +extern void CLEAR_AUX_OUTPUT_BIT(int index); + +extern void SET_MOTION_OUTPUT_VALUE(int index, double value); +extern void SET_AUX_OUTPUT_VALUE(int index, double value); + +/* Commands to wait for, query input bits and analog values */ + +#define DIGITAL_INPUT 1 +#define ANALOG_INPUT 0 + +#define WAIT_MODE_IMMEDIATE 0 +#define WAIT_MODE_RISE 1 +#define WAIT_MODE_FALL 2 +#define WAIT_MODE_HIGH 3 +#define WAIT_MODE_LOW 4 + +extern int WAIT(int index, /* index of the motion exported input */ + int input_type, /* 1=DIGITAL_INPUT or 0=ANALOG_INPUT */ + int wait_type, /* 0 - immediate, 1 - rise, 2 - fall, 3 - be high, 4 - be low */ + double timeout); /* time to wait [in seconds], if the input didn't change the value -1 is returned */ +/* WAIT - program execution is stopped until the input selected by index + changed to the needed state (specified by wait_type). + Return value: either wait_type if timeout didn't occur, or -1 otherwise. */ + +/* tell canon the next move needs the rotary to be unlocked */ +extern int UNLOCK_ROTARY(int line_no, int joint_num); + +/* tell canon that is no longer the case */ +extern int LOCK_ROTARY(int line_no, int joint_num); + +/*************************************************************************/ + +/* Canonical "Give me information" functions for the interpreter to call + +In general, returned values are valid only if any canonical do it commands +that may have been called for have been executed to completion. If a function +returns a valid value regardless of execution, that is noted in the comments +below. + +*/ + +/* The interpreter is not using this function +// Returns the system angular unit factor, in units / degree +extern double GET_EXTERNAL_ANGLE_UNIT_FACTOR(); +*/ + +// Returns the system feed rate +extern double GET_EXTERNAL_FEED_RATE(); + +// Returns the system value for flood coolant, zero = off, non-zero = on +extern int GET_EXTERNAL_FLOOD(); + +/* The interpreter is not using this function +// Returns the system length unit factor, in units / mm +extern double GET_EXTERNAL_LENGTH_UNIT_FACTOR(); +*/ + +// Returns the system length unit type +CANON_UNITS GET_EXTERNAL_LENGTH_UNIT_TYPE(); + +extern double GET_EXTERNAL_LENGTH_UNITS(); +extern double GET_EXTERNAL_ANGLE_UNITS(); + +// Returns the system value for mist coolant, zero = off, non-zero = on +extern int GET_EXTERNAL_MIST(); + +// Returns the current motion control mode +extern CANON_MOTION_MODE GET_EXTERNAL_MOTION_CONTROL_MODE(); + +// Returns the current motion path-following tolerance +extern double GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + +// Returns the current motion naive CAM tolerance +extern double GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + +/* The interpreter is not using these six GET_EXTERNAL_ORIGIN functions + +// returns the current a-axis origin offset +extern double GET_EXTERNAL_ORIGIN_A(); + +// returns the current b-axis origin offset +extern double GET_EXTERNAL_ORIGIN_B(); + +// returns the current c-axis origin offset +extern double GET_EXTERNAL_ORIGIN_C(); + +// returns the current x-axis origin offset +extern double GET_EXTERNAL_ORIGIN_X(); + +// returns the current y-axis origin offset +extern double GET_EXTERNAL_ORIGIN_Y(); + +// returns the current z-axis origin offset +extern double GET_EXTERNAL_ORIGIN_Z(); + +*/ + +// returns nothing but copies the name of the parameter file into +// the filename array, stopping at max_size if the name is longer +// An empty string may be placed in filename. +extern void GET_EXTERNAL_PARAMETER_FILE_NAME(char *filename, int max_size); + +extern void SET_PARAMETER_FILE_NAME(const char *name); + +// returns the currently active plane +extern CANON_PLANE GET_EXTERNAL_PLANE(); + +// returns the current a-axis position +extern double GET_EXTERNAL_POSITION_A(); + +// returns the current b-axis position +extern double GET_EXTERNAL_POSITION_B(); + +// returns the current c-axis position +extern double GET_EXTERNAL_POSITION_C(); + +// returns the current x-axis position +extern double GET_EXTERNAL_POSITION_X(); + +// returns the current y-axis position +extern double GET_EXTERNAL_POSITION_Y(); + +// returns the current z-axis position +extern double GET_EXTERNAL_POSITION_Z(); + +// returns the current u-axis position +extern double GET_EXTERNAL_POSITION_U(); + +// returns the current v-axis position +extern double GET_EXTERNAL_POSITION_V(); + +// returns the current w-axis position +extern double GET_EXTERNAL_POSITION_W(); + + +// Returns the position of the specified axis at the last probe trip, +// in the current work coordinate system. +extern double GET_EXTERNAL_PROBE_POSITION_A(); +extern double GET_EXTERNAL_PROBE_POSITION_B(); +extern double GET_EXTERNAL_PROBE_POSITION_C(); +extern double GET_EXTERNAL_PROBE_POSITION_X(); +extern double GET_EXTERNAL_PROBE_POSITION_Y(); +extern double GET_EXTERNAL_PROBE_POSITION_Z(); +extern double GET_EXTERNAL_PROBE_POSITION_U(); +extern double GET_EXTERNAL_PROBE_POSITION_V(); +extern double GET_EXTERNAL_PROBE_POSITION_W(); + +// Returns the value for any analog non-contact probing. +extern double GET_EXTERNAL_PROBE_VALUE(); + +// Whether the probe changed state during the last probing move +extern int GET_EXTERNAL_PROBE_TRIPPED_VALUE(); + +// Returns zero if queue is not empty, non-zero if the queue is empty +// This always returns a valid value +extern int GET_EXTERNAL_QUEUE_EMPTY(); + +// Returns the system value for spindle speed in rpm +extern double GET_EXTERNAL_SPEED(int spindle); + +// Returns the system value for direction of spindle turning +extern CANON_DIRECTION GET_EXTERNAL_SPINDLE(int spindle); + +// returns current tool length offset +extern double GET_EXTERNAL_TOOL_LENGTH_XOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_YOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_ZOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_AOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_BOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_COFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_UOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_VOFFSET(); +extern double GET_EXTERNAL_TOOL_LENGTH_WOFFSET(); + +// Returns the system value for the carousel slot in which the tool +// currently in the spindle belongs. Return value zero means there is no +// tool in the spindle. +extern int GET_EXTERNAL_TOOL_SLOT(); + +// Returns the system value for the selected slot. That one will be the next +// valid tool after a toolchange (m6). Return value -1 means there is no +// selected tool. +extern int GET_EXTERNAL_SELECTED_TOOL_SLOT(); + +// Returns the CANON_TOOL_TABLE structure associated with the tool +// in the given pocket +extern CANON_TOOL_TABLE GET_EXTERNAL_TOOL_TABLE(int pocket); + +// return the value of iocontrol's toolchanger-fault pin +extern int GET_EXTERNAL_TC_FAULT(); + +// return the value of iocontrol's toolchanger-reason pin +int GET_EXTERNAL_TC_REASON(); + +// Returns the system traverse rate +extern double GET_EXTERNAL_TRAVERSE_RATE(); + +// Returns the enabled/disabled status for feed override, spindle +// override, adaptive feed, and feed hold +extern int GET_EXTERNAL_FEED_OVERRIDE_ENABLE(); +extern int GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(int spindle); +extern int GET_EXTERNAL_ADAPTIVE_FEED_ENABLE(); +extern int GET_EXTERNAL_FEED_HOLD_ENABLE(); + +// Functions to query digital/analog Inputs +/* def is a default value which should be returned by canon functions + that can't actually read external hardware - simulators and such */ +extern int GET_EXTERNAL_DIGITAL_INPUT(int index, int def); +/* returns current value of the digital input selected by index.*/ + +extern double GET_EXTERNAL_ANALOG_INPUT(int index, double def); +/* returns current value of the analog input selected by index.*/ + +// Returns the mask of axes present in the system +extern int GET_EXTERNAL_AXIS_MASK(); + + +#define PARAMETER_FILE_NAME_LENGTH 100 + +#define USER_DEFINED_FUNCTION_NUM 100 +typedef void (*USER_DEFINED_FUNCTION_TYPE) (int num, double arg1, + double arg2); +extern USER_DEFINED_FUNCTION_TYPE + USER_DEFINED_FUNCTION[USER_DEFINED_FUNCTION_NUM]; +extern int USER_DEFINED_FUNCTION_ADD(USER_DEFINED_FUNCTION_TYPE func, + int num); + +/* to be called by emcTaskPlanExecute when done interpreting. This causes the + * last segment to be output, if it has been held to do segment merging */ +extern void FINISH(void); + +// to be called when there is an abort, to dump the last segment instead of adding +// it to the interp list in certain cases +extern void ON_RESET(void); + +// expose CANON_ERROR +extern void CANON_ERROR(const char *fmt, ...) __attribute__((format(printf,1,2))); + +extern int GET_EXTERNAL_OFFSET_APPLIED(); +extern EmcPose GET_EXTERNAL_OFFSETS(); +extern void UPDATE_TAG(const StateTag& tag); + +#endif /* ifndef CANON_HH */ diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon_position.hh b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon_position.hh new file mode 100644 index 0000000..0997857 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/canon_position.hh @@ -0,0 +1,77 @@ +/******************************************************************** + * Description: canon_position.hh + * + * CANON position class with operators and common functions + * Derived from a work by Thomas Kramer + * + * Author: Robert W. Ellenberg + * License: GPL Version 2+ + * System: Linux + * + * Copyright (c) 2014 All rights reserved. + ********************************************************************/ + +#ifndef CANON_POSITION_HH +#define CANON_POSITION_HH + +#include // FILE +#include + +#include +#include "emctool.h" +#include // For PM_CARTESIAN type + +struct CANON_POSITION { +#ifndef JAVA_DIAG_APPLET + CANON_POSITION() : + x(0.0), + y(0.0), + z(0.0), + a(0.0), + b(0.0), + c(0.0), + u(0.0), + v(0.0), + w(0.0) {} + + CANON_POSITION(double _x, double _y, double _z, + double _a, double _b, double _c, + double _u, double _v, double _w); + CANON_POSITION(const EmcPose &_pos); + CANON_POSITION(PM_CARTESIAN const &xyz); + CANON_POSITION(PM_CARTESIAN const &xyz, PM_CARTESIAN const &abc); + + bool operator==(const CANON_POSITION &o) const; + bool operator!=(const CANON_POSITION &o) const; + CANON_POSITION & operator+=(const CANON_POSITION &o); + CANON_POSITION & operator+=(const EmcPose &o); + + const CANON_POSITION operator+(const CANON_POSITION &o) const; + const CANON_POSITION operator+(const EmcPose &o) const; + CANON_POSITION & operator-=(const CANON_POSITION &o); + CANON_POSITION & operator-=(const EmcPose &o); + + const CANON_POSITION operator-(const CANON_POSITION &o) const; + const CANON_POSITION operator-(const EmcPose &o) const; + + double &operator[](const int ind); + + const CANON_POSITION abs() const; + const CANON_POSITION absdiff(const CANON_POSITION &o) const; + double max() const; + + const EmcPose toEmcPose() const; + + const PM_CARTESIAN xyz() const; + const PM_CARTESIAN abc() const; + const PM_CARTESIAN uvw() const; + + void set_xyz(const PM_CARTESIAN & xyz); + + void print() const; +#endif + + double x, y, z, a, b, c, u, v, w; +}; + +#endif /* ifndef CANON_POSITION_HH */ diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/debugflags.h b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/debugflags.h new file mode 100644 index 0000000..c661e99 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/debugflags.h @@ -0,0 +1,50 @@ +/* This is a component of LinuxCNC + * Copyright 2011, 2012, 2013 Michael Haberler , + * Sebastian Kuzminsky + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +// factored out from emcglb.h so subsystems not requiring the +// emcglb.h defines may include them as well + + + +#define EMC_DEBUG_CONFIG 0x00000002 +#define EMC_DEBUG_VERSIONS 0x00000008 +#define EMC_DEBUG_TASK_ISSUE 0x00000010 +#define EMC_DEBUG_NML 0x00000040 +#define EMC_DEBUG_MOTION_TIME 0x00000080 +#define EMC_DEBUG_INTERP 0x00000100 +#define EMC_DEBUG_RCS 0x00000200 +#define EMC_DEBUG_INTERP_LIST 0x00000800 +#define EMC_DEBUG_IOCONTROL 0x00001000 +#define EMC_DEBUG_OWORD 0x00002000 +#define EMC_DEBUG_REMAP 0x00004000 +#define EMC_DEBUG_PYTHON 0x00008000 +#define EMC_DEBUG_NAMEDPARAM 0x00010000 +#define EMC_DEBUG_GDBONSIGNAL 0x00020000 +#define EMC_DEBUG_STATE_TAGS 0x00080000 + +// not interpreted by EMC. +#define EMC_DEBUG_USER1 0x10000000 +#define EMC_DEBUG_USER2 0x20000000 + +#define EMC_DEBUG_UNCONDITIONAL 0x40000000 // always logged +#define EMC_DEBUG_ALL 0x7FFFFFFF /* it's an int for %i to work + */ +// debug prefix flags +#define LOG_TIME 1 +#define LOG_PID 2 +#define LOG_FILENAME 4 // and line diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emc.hh b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emc.hh new file mode 100644 index 0000000..20bb4bb --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emc.hh @@ -0,0 +1,483 @@ +/******************************************************************** +* Description: emc.hh +* Declarations for EMC NML vocabulary +* +* Derived from a work by Fred Proctor & Will Shackleford +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#ifndef EMC_HH +#define EMC_HH + +#include // EMC_JOINT_MAX, EMC_AXIS_MAX +#include "libnml/nml/nml_type.hh" +#include "motion_types.h" +#include +#include "rs274ngc/modal_state.hh" + +// Forward class declarations +class EMC_JOINT_STAT; +class EMC_AXIS_STAT; +class EMC_TRAJ_STAT; +class EMC_MOTION_STAT; +class EMC_TASK_STAT; +class EMC_TOOL_STAT; +class EMC_AUX_STAT; +class EMC_SPINDLE_STAT; +class EMC_COOLANT_STAT; +class EMC_IO_STAT; +class EMC_STAT; +class CMS; +class RCS_CMD_CHANNEL; +class RCS_STAT_CHANNEL; +class NML; +struct EmcPose; +struct PM_CARTESIAN; + +// --------------------- +// EMC TYPE DECLARATIONS +// --------------------- + +// NML for base EMC + +#define EMC_OPERATOR_ERROR_TYPE ((NMLTYPE) 11) +#define EMC_OPERATOR_TEXT_TYPE ((NMLTYPE) 12) +#define EMC_OPERATOR_DISPLAY_TYPE ((NMLTYPE) 13) + +#define EMC_NULL_TYPE ((NMLTYPE) 21) + +#define EMC_SET_DEBUG_TYPE ((NMLTYPE) 22) + +#define EMC_SYSTEM_CMD_TYPE ((NMLTYPE) 30) + +// NML for EMC_JOINT + +#define EMC_JOINT_SET_MIN_POSITION_LIMIT_TYPE ((NMLTYPE) 107) +#define EMC_JOINT_SET_MAX_POSITION_LIMIT_TYPE ((NMLTYPE) 108) +#define EMC_JOINT_SET_FERROR_TYPE ((NMLTYPE) 111) +#define EMC_JOINT_SET_HOMING_PARAMS_TYPE ((NMLTYPE) 112) +#define EMC_JOINT_SET_MIN_FERROR_TYPE ((NMLTYPE) 115) +#define EMC_JOINT_HALT_TYPE ((NMLTYPE) 119) +#define EMC_JOINT_HOME_TYPE ((NMLTYPE) 123) +#define EMC_JOG_CONT_TYPE ((NMLTYPE) 124) +#define EMC_JOG_INCR_TYPE ((NMLTYPE) 125) +#define EMC_JOG_ABS_TYPE ((NMLTYPE) 126) +#define EMC_JOINT_OVERRIDE_LIMITS_TYPE ((NMLTYPE) 129) +#define EMC_JOINT_LOAD_COMP_TYPE ((NMLTYPE) 131) +#define EMC_JOINT_SET_BACKLASH_TYPE ((NMLTYPE) 134) +#define EMC_JOINT_UNHOME_TYPE ((NMLTYPE) 135) +#define EMC_JOG_STOP_TYPE ((NMLTYPE) 136) + +#define EMC_JOINT_STAT_TYPE ((NMLTYPE) 198) +#define EMC_AXIS_STAT_TYPE ((NMLTYPE) 199) + +// NML for EMC_TRAJ + +// defs for termination conditions +#define EMC_TRAJ_TERM_COND_STOP 0 +#define EMC_TRAJ_TERM_COND_EXACT 1 +#define EMC_TRAJ_TERM_COND_BLEND 2 + +#define EMC_TRAJ_SET_MODE_TYPE ((NMLTYPE) 204) +#define EMC_TRAJ_SET_VELOCITY_TYPE ((NMLTYPE) 205) +#define EMC_TRAJ_SET_ACCELERATION_TYPE ((NMLTYPE) 206) +#define EMC_TRAJ_SET_MAX_VELOCITY_TYPE ((NMLTYPE) 207) +#define EMC_TRAJ_SET_SCALE_TYPE ((NMLTYPE) 209) +#define EMC_TRAJ_SET_RAPID_SCALE_TYPE ((NMLTYPE) 238) + +#define EMC_TRAJ_ABORT_TYPE ((NMLTYPE) 215) +#define EMC_TRAJ_PAUSE_TYPE ((NMLTYPE) 216) +#define EMC_TRAJ_RESUME_TYPE ((NMLTYPE) 218) +#define EMC_TRAJ_DELAY_TYPE ((NMLTYPE) 219) +#define EMC_TRAJ_LINEAR_MOVE_TYPE ((NMLTYPE) 220) +#define EMC_TRAJ_CIRCULAR_MOVE_TYPE ((NMLTYPE) 221) +#define EMC_TRAJ_SET_TERM_COND_TYPE ((NMLTYPE) 222) +#define EMC_TRAJ_SET_OFFSET_TYPE ((NMLTYPE) 223) +#define EMC_TRAJ_SET_G5X_TYPE ((NMLTYPE) 224) +#define EMC_TRAJ_SET_ROTATION_TYPE ((NMLTYPE) 226) +#define EMC_TRAJ_SET_G92_TYPE ((NMLTYPE) 227) +#define EMC_TRAJ_CLEAR_PROBE_TRIPPED_FLAG_TYPE ((NMLTYPE) 228) +#define EMC_TRAJ_PROBE_TYPE ((NMLTYPE) 229) +#define EMC_TRAJ_SET_TELEOP_ENABLE_TYPE ((NMLTYPE) 230) +#define EMC_TRAJ_SET_SPINDLESYNC_TYPE ((NMLTYPE) 232) +#define EMC_TRAJ_SET_SPINDLE_SCALE_TYPE ((NMLTYPE) 233) +#define EMC_TRAJ_SET_FO_ENABLE_TYPE ((NMLTYPE) 234) +#define EMC_TRAJ_SET_SO_ENABLE_TYPE ((NMLTYPE) 235) +#define EMC_TRAJ_SET_FH_ENABLE_TYPE ((NMLTYPE) 236) +#define EMC_TRAJ_RIGID_TAP_TYPE ((NMLTYPE) 237) + +#define EMC_TRAJ_STAT_TYPE ((NMLTYPE) 299) + +// EMC_MOTION aggregate class type declaration + +#define EMC_MOTION_SET_AOUT_TYPE ((NMLTYPE) 304) +#define EMC_MOTION_SET_DOUT_TYPE ((NMLTYPE) 305) +#define EMC_MOTION_ADAPTIVE_TYPE ((NMLTYPE) 306) + +#define EMC_MOTION_STAT_TYPE ((NMLTYPE) 399) + +// NML for EMC_TASK + +#define EMC_TASK_ABORT_TYPE ((NMLTYPE) 503) +#define EMC_TASK_SET_MODE_TYPE ((NMLTYPE) 504) +#define EMC_TASK_SET_STATE_TYPE ((NMLTYPE) 505) +#define EMC_TASK_PLAN_OPEN_TYPE ((NMLTYPE) 506) +#define EMC_TASK_PLAN_RUN_TYPE ((NMLTYPE) 507) +#define EMC_TASK_PLAN_EXECUTE_TYPE ((NMLTYPE) 509) +#define EMC_TASK_PLAN_PAUSE_TYPE ((NMLTYPE) 510) +#define EMC_TASK_PLAN_STEP_TYPE ((NMLTYPE) 511) +#define EMC_TASK_PLAN_RESUME_TYPE ((NMLTYPE) 512) +#define EMC_TASK_PLAN_END_TYPE ((NMLTYPE) 513) +#define EMC_TASK_PLAN_CLOSE_TYPE ((NMLTYPE) 514) +#define EMC_TASK_PLAN_INIT_TYPE ((NMLTYPE) 515) +#define EMC_TASK_PLAN_SYNCH_TYPE ((NMLTYPE) 516) +#define EMC_TASK_PLAN_SET_OPTIONAL_STOP_TYPE ((NMLTYPE) 517) +#define EMC_TASK_PLAN_SET_BLOCK_DELETE_TYPE ((NMLTYPE) 518) +#define EMC_TASK_PLAN_OPTIONAL_STOP_TYPE ((NMLTYPE) 519) +#define EMC_TASK_PLAN_REVERSE_TYPE ((NMLTYPE) 520) +#define EMC_TASK_PLAN_FORWARD_TYPE ((NMLTYPE) 521) + +#define EMC_TASK_STAT_TYPE ((NMLTYPE) 599) + +// EMC_TOOL type declarations + +#define EMC_TOOL_HALT_TYPE ((NMLTYPE) 1102) +#define EMC_TOOL_ABORT_TYPE ((NMLTYPE) 1103) +#define EMC_TOOL_PREPARE_TYPE ((NMLTYPE) 1104) +#define EMC_TOOL_LOAD_TYPE ((NMLTYPE) 1105) +#define EMC_TOOL_UNLOAD_TYPE ((NMLTYPE) 1106) +#define EMC_TOOL_LOAD_TOOL_TABLE_TYPE ((NMLTYPE) 1107) +#define EMC_TOOL_SET_OFFSET_TYPE ((NMLTYPE) 1108) +#define EMC_TOOL_SET_NUMBER_TYPE ((NMLTYPE) 1109) + +#define EMC_TOOL_STAT_TYPE ((NMLTYPE) 1199) + +// EMC_AUX type declarations +#define EMC_AUX_INPUT_WAIT_TYPE ((NMLTYPE) 1209) + +#define EMC_AUX_STAT_TYPE ((NMLTYPE) 1299) + +// EMC_SPINDLE type declarations +#define EMC_SPINDLE_ON_TYPE ((NMLTYPE) 1304) +#define EMC_SPINDLE_OFF_TYPE ((NMLTYPE) 1305) +#define EMC_SPINDLE_INCREASE_TYPE ((NMLTYPE) 1309) +#define EMC_SPINDLE_DECREASE_TYPE ((NMLTYPE) 1310) +#define EMC_SPINDLE_CONSTANT_TYPE ((NMLTYPE) 1311) +#define EMC_SPINDLE_BRAKE_RELEASE_TYPE ((NMLTYPE) 1312) +#define EMC_SPINDLE_BRAKE_ENGAGE_TYPE ((NMLTYPE) 1313) +#define EMC_SPINDLE_SPEED_TYPE ((NMLTYPE) 1316) +#define EMC_SPINDLE_ORIENT_TYPE ((NMLTYPE) 1317) +#define EMC_SPINDLE_WAIT_ORIENT_COMPLETE_TYPE ((NMLTYPE) 1318) + +#define EMC_SPINDLE_STAT_TYPE ((NMLTYPE) 1399) + +// EMC_COOLANT type declarations +#define EMC_COOLANT_MIST_ON_TYPE ((NMLTYPE) 1404) +#define EMC_COOLANT_MIST_OFF_TYPE ((NMLTYPE) 1405) +#define EMC_COOLANT_FLOOD_ON_TYPE ((NMLTYPE) 1406) +#define EMC_COOLANT_FLOOD_OFF_TYPE ((NMLTYPE) 1407) + +#define EMC_COOLANT_STAT_TYPE ((NMLTYPE) 1499) + +#define EMC_IO_STAT_TYPE ((NMLTYPE) 1699) + +#define EMC_STAT_TYPE ((NMLTYPE) 1999) + +// types for EMC_TASK mode +enum class EMC_TASK_MODE { + MANUAL = 1, + AUTO = 2, + MDI = 3 +}; + +// types for EMC_TASK state +enum class EMC_TASK_STATE { + ESTOP = 1, + ESTOP_RESET = 2, + OFF = 3, + ON = 4 +}; + +// types for EMC_TASK execState +enum class EMC_TASK_EXEC { + ERROR = 1, + DONE = 2, + WAITING_FOR_MOTION = 3, + WAITING_FOR_MOTION_QUEUE = 4, + WAITING_FOR_IO = 5, + WAITING_FOR_MOTION_AND_IO = 7, + WAITING_FOR_DELAY = 8, + WAITING_FOR_SYSTEM_CMD = 9, + WAITING_FOR_SPINDLE_ORIENTED = 10 +}; + +// types for EMC_TASK interpState +enum class EMC_TASK_INTERP { + IDLE = 1, + READING = 2, + PAUSED = 3, + WAITING = 4 +}; + +// types for motion control +enum class EMC_TRAJ_MODE { + FREE = 1, // independent-axis motion, + COORD = 2, // coordinated-axis motion, + TELEOP = 3 // velocity based world coordinates motion, +}; + +// types for emcIoAbort() reasons +enum class EMC_ABORT { + TASK_EXEC_ERROR = 1, + AUX_ESTOP = 2, + MOTION_OR_IO_RCS_ERROR = 3, + TASK_STATE_OFF = 4, + TASK_STATE_ESTOP_RESET = 5, + TASK_STATE_ESTOP = 6, + TASK_STATE_NOT_ON = 7, + TASK_ABORT = 8, + INTERPRETER_ERROR = 9, // interpreter failed during readahead + INTERPRETER_ERROR_MDI = 10, // interpreter failed during MDI execution + USER = 100 // user-defined abort codes start here +}; +// -------------- +// EMC VOCABULARY +// -------------- + +// NML formatting function +extern int emcFormat(NMLTYPE type, void *buffer, CMS * cms); + +// NML Symbol Lookup Function +extern const char *emc_symbol_lookup(uint32_t type); +#define emcSymbolLookup(a) emc_symbol_lookup(a) + +// decls for command line args-- mains are responsible for setting these +// so that other modules can get cmd line args for ad hoc processing +extern int Argc; +extern char **Argv; + +// ------------------------ +// IMPLEMENTATION FUNCTIONS +// ------------------------ + +// implementation functions for EMC error, message types +// intended to be implemented in main() file, by writing to NML buffer + +// print an error +extern int emcOperatorError(const char *fmt, ...) __attribute__((format(printf,1,2))); + +// print general text +extern int emcOperatorText(const char *fmt, ...) __attribute__((format(printf,1,2))); + +// print note to operator +extern int emcOperatorDisplay(const char *fmt, ...) __attribute__((format(printf,1,2))); + +// implementation functions for EMC_AXIS types + +extern int emcAxisSetMinPositionLimit(int axis, double limit); +extern int emcAxisSetMaxPositionLimit(int axis, double limit); +extern int emcAxisSetMaxVelocity(int axis, double vel, double ext_offset_vel); +extern int emcAxisSetMaxAcceleration(int axis, double acc, double ext_offset_acc); +extern double emcAxisGetMaxVelocity(int axis); +extern double emcAxisGetMaxAcceleration(int axis); +extern int emcAxisSetLockingJoint(int axis,int joint); + +extern int emcAxisUpdate(EMC_AXIS_STAT stat[], int numAxes); + +extern int emcAxisSetMaxJerk(int axis,double jerk); +extern int emcAxisHasMaxJerk(int axis); +extern double emcAxisGetMaxJerk(int axis); +// implementation functions for EMC_JOINT types + +extern int emcJointSetType(int joint, unsigned char jointType); +extern int emcJointSetUnits(int joint, double units); +extern int emcJointSetBacklash(int joint, double backlash); +extern int emcJointSetMinPositionLimit(int joint, double limit); +extern int emcJointSetMaxPositionLimit(int joint, double limit); +extern int emcJointSetMotorOffset(int joint, double offset); +extern int emcJointSetFerror(int joint, double ferror); +extern int emcJointSetMinFerror(int joint, double ferror); +extern int emcJointSetHomingParams(int joint, double home, double offset, double home_vel, + double search_vel, double latch_vel, + int use_index, int encoder_does_not_reset, int ignore_limits, + int is_shared, int home_sequence, int volatile_home, int locking_indexer, + int absolute_encoder); +extern int emcJointUpdateHomingParams(int joint, double home, double offset, int sequence); +extern int emcJointSetMaxVelocity(int joint, double vel); +extern int emcJointSetMaxAcceleration(int joint, double acc); + +extern int emcJointInit(int joint); +extern int emcJointHalt(int joint); +extern int emcJointHome(int joint); +extern int emcJointUnhome(int joint); +extern int emcJointActivate(int joint); +extern int emcJointDeactivate(int joint); +extern int emcJointOverrideLimits(int joint); +extern int emcJointLoadComp(int joint, const char *file, int type); +extern int emcJogStop(int nr, int jjogmode); +extern int emcJogCont(int nr, double vel, int jjogmode); +extern int emcJogIncr(int nr, double incr, double vel, int jjogmode); +extern int emcJogAbs(int nr, double pos, double vel, int jjogmode); + + +extern int emcJointUpdate(EMC_JOINT_STAT stat[], int numJoints); + +extern int emcJointSetMaxJerk(int joint, double jerk); + +// implementation functions for EMC_SPINDLE types + +extern int emcSpindleSetParams(int spindle, double max_pos, double min_pos, double max_neg, + double min_neg, double search_vel, double home_angle, int sequence, double increment); + +// implementation functions for EMC_TRAJ types + +extern int emcTrajSetJoints(int joints); +extern int emcTrajUpdateTag(StateTag const &tag); +extern int emcTrajSetAxes(int axismask); +extern int emcTrajSetSpindles(int spindles); +extern int emcTrajSetUnits(double linearUnits, double angularUnits); +extern int emcTrajSetMode(EMC_TRAJ_MODE traj_mode); +extern int emcTrajSetVelocity(double vel, double ini_maxvel); +extern int emcTrajSetAcceleration(double acc); +extern int emcTrajSetMaxVelocity(double vel); +extern int emcTrajSetMaxAcceleration(double acc); +extern int emcTrajSetScale(double scale); +extern int emcTrajSetRapidScale(double scale); +extern int emcTrajSetFOEnable(unsigned char mode); //feed override enable +extern int emcTrajSetFHEnable(unsigned char mode); //feed hold enable +extern int emcTrajSetSpindleScale(int spindle, double scale); +extern int emcTrajSetSOEnable(unsigned char mode); //spindle speed override enable +extern int emcTrajSetAFEnable(unsigned char enable); //adaptive feed enable +extern int emcTrajSetMotionId(int id); +extern double emcTrajGetLinearUnits(); +extern double emcTrajGetAngularUnits(); + +extern int emcTrajInit(); +extern int emcTrajHalt(); +extern int emcTrajEnable(); +extern int emcTrajDisable(); +extern int emcTrajAbort(); +extern int emcTrajPause(); +extern int emcTrajReverse(); +extern int emcTrajForward(); +extern int emcTrajStep(); +extern int emcTrajResume(); +extern int emcTrajDelay(double delay); +extern int emcTrajLinearMove(const EmcPose& end, int type, double vel, + double ini_maxvel, double acc, double ini_maxjerk, int indexer_jnum); +extern int emcTrajCircularMove(const EmcPose& end, const PM_CARTESIAN& center, const PM_CARTESIAN& + normal, int turn, int type, double vel, double ini_maxvel, double acc, double ini_maxjerk); +extern int emcTrajSetTermCond(int cond, double tolerance); +extern int emcTrajSetSpindleSync(int spindle, double feed_per_revolution, bool wait_for_index); +extern int emcTrajSetOffset(const EmcPose& tool_offset); +extern int emcTrajSetHome(const EmcPose& home); +extern int emcTrajClearProbeTrippedFlag(); +extern int emcTrajProbe(const EmcPose& pos, int type, double vel, + double ini_maxvel, double acc, double ini_maxjerk, unsigned char probe_type); +extern int emcTrajRigidTap(const EmcPose& pos, double vel, double ini_maxvel, double acc, double ini_maxjerk, double scale); + +extern int emcTrajUpdate(EMC_TRAJ_STAT * stat); + +extern int emcTrajSetJerk(double jerk); +extern int emcTrajSetMaxJerk(double jerk); +extern int emcTrajPlannerType(int type); +// implementation functions for EMC_MOTION aggregate types + +extern int emcMotionInit(); +extern int emcMotionHalt(); +extern int emcMotionAbort(); +extern int emcMotionSetDebug(int debug); +extern int emcMotionSetAout(unsigned char index, double start, double end, + unsigned char now); +extern int emcMotionSetDout(unsigned char index, unsigned char start, + unsigned char end, unsigned char now); + +extern int emcMotionUpdate(EMC_MOTION_STAT * stat); + +extern int emcAbortCleanup(EMC_ABORT reason,const char *message = ""); + +// implementation functions for EMC_TOOL types + +extern int emcToolPrepare(int tool); +extern int emcToolLoad(); +extern int emcToolUnload(); +extern int emcToolLoadToolTable(const char *file); +extern int emcToolSetOffset(int pocket, int toolno, const EmcPose& offset, double diameter, + double frontangle, double backangle, int orientation); +extern int emcToolSetNumber(int number); + +// implementation functions for EMC_AUX types + +extern int emcAuxEstopOn(); +extern int emcAuxEstopOff(); + +// implementation functions for EMC_SPINDLE types + +extern int emcSpindleAbort(int spindle); +extern int emcSpindleSpeed(int spindle, double speed, double factor, double xoffset); +extern int emcSpindleOn(int spindle, double speed, double factor, double xoffset,int wait_for_atspeed = 1); +extern int emcSpindleOrient(int spindle, double orientation, int direction); +extern int emcSpindleOff(int spindle); +extern int emcSpindleIncrease(int spindle); +extern int emcSpindleDecrease(int spindle); +extern int emcSpindleConstant(int spindle); +extern int emcSpindleBrakeRelease(int spindle); +extern int emcSpindleBrakeEngage(int spindle); + +extern int emcSpindleUpdate(EMC_SPINDLE_STAT stat[], int num_spindles); + +// implementation functions for EMC_COOLANT types + +extern int emcCoolantMistOn(); +extern int emcCoolantMistOff(); +extern int emcCoolantFloodOn(); +extern int emcCoolantFloodOff(); + +// implementation functions for EMC_IO types + +extern int emcIoInit(); +extern int emcIoAbort(EMC_ABORT reason); + +// implementation functions for EMC aggregate types + +int emcSetMaxFeedOverride(double maxFeedScale); +int emcSetupArcBlends(int arcBlendEnable, + int arcBlendFallbackEnable, + int arcBlendOptDepth, + int arcBlendGapCycles, + double arcBlendRampFreq, + double arcBlendTangentKinkRatio); +int emcSetProbeErrorInhibit(int j_inhibit, int h_inhibit); +int emcGetExternalOffsetApplied(void); +EmcPose emcGetExternalOffsets(void); + +extern int emcUpdate(EMC_STAT * stat); +// full EMC status +extern EMC_STAT *emcStatus; + +// EMC IO status +extern EMC_IO_STAT *emcIoStatus; + +// EMC MOTION status +extern EMC_MOTION_STAT *emcMotionStatus; + +// values for EMC_JOINT_SET_JOINT, jointType +enum EmcJointType : int { + EMC_LINEAR = 1, + EMC_ANGULAR = 2, +}; + +/** + * Set the units conversion factor. + * @see EMC_JOINT_SET_INPUT_SCALE + */ +using EmcLinearUnits = double; +using EmcAngularUnits = double; + +#endif // #ifndef EMC_HH diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpos.h b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpos.h new file mode 100644 index 0000000..8249846 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpos.h @@ -0,0 +1,36 @@ +/******************************************************************** +* Description: emcpos.h +* +* Derived from a work by Fred Proctor & Will Shackleford +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#ifndef __LINUXCNC_EMCPOS_H +#define __LINUXCNC_EMCPOS_H + +#include "posemath.h" /* PmCartesian */ + +typedef struct EmcPose { + PmCartesian tran; + double a, b, c; + double u, v, w; +} EmcPose; + +#define ZERO_EMC_POSE(pos) do { \ +(pos).tran.x = 0.0; \ +(pos).tran.y = 0.0; \ +(pos).tran.z = 0.0; \ +(pos).a = 0.0; \ +(pos).b = 0.0; \ +(pos).c = 0.0; \ +(pos).u = 0.0; \ +(pos).v = 0.0; \ +(pos).w = 0.0; } while(0) + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpose.h b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpose.h new file mode 100644 index 0000000..9d39ace --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emcpose.h @@ -0,0 +1,51 @@ +/******************************************************************** +* Description: emcpose.h +* +* Derived from a work by Fred Proctor & Will Shackleford +* +* Author: Robert W. Ellenberg +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +********************************************************************/ +#ifndef __LINUXCNC_EMCPOSE_H +#define __LINUXCNC_EMCPOSE_H + +#include "emcpos.h" + +typedef enum { + EMCPOSE_ERR_OK = 0, + EMCPOSE_ERR_FAIL = -1, + EMCPOSE_ERR_INPUT_MISSING = -2, + EMCPOSE_ERR_OUTPUT_MISSING = -3, + EMCPOSE_ERR_ALL +} EmcPoseErr; + +void emcPoseZero(EmcPose * const pos); + +int emcPoseAdd(EmcPose const * const p1, EmcPose const * const p2, EmcPose * const out); +int emcPoseSub(EmcPose const * const p1, EmcPose const * const p2, EmcPose * const out); + +int emcPoseToPmCartesian(EmcPose const * const pose, + PmCartesian * const xyz, PmCartesian * const abc, PmCartesian * const uvw); +int pmCartesianToEmcPose(PmCartesian const * const xyz, + PmCartesian const * const abc, PmCartesian const * const uvw, EmcPose * const pose); + +int emcPoseSelfAdd(EmcPose * const self, EmcPose const * const p2); +int emcPoseSelfSub(EmcPose * const self, EmcPose const * const p2); + +int emcPoseSetXYZ(PmCartesian const * const xyz, EmcPose * const pose); +int emcPoseSetABC(PmCartesian const * const abc, EmcPose * const pose); +int emcPoseSetUVW(PmCartesian const * const uvw, EmcPose * const pose); + +int emcPoseGetXYZ(EmcPose const * const pose, PmCartesian * const xyz); +int emcPoseGetABC(EmcPose const * const pose, PmCartesian * const abc); +int emcPoseGetUVW(EmcPose const * const pose, PmCartesian * const uvw); + +int emcPoseMagnitude(EmcPose const * const pose, double * const out); + +int emcPoseValid(EmcPose const * const pose); + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emctool.h b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emctool.h new file mode 100644 index 0000000..b877591 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/emctool.h @@ -0,0 +1,40 @@ +// Copyright 2013 Jeff Epler +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef EMCTOOL_H +#define EMCTOOL_H + +#include + +/* pocketno: 0..(CANON_POCKETS_MAX-1) (0: spindle) +** toolno: no restrictions (0: notool) +*/ +#define CANON_POCKETS_MAX 1001 // max size of carousel handled +#define CANON_TOOL_ENTRY_LEN 256 // how long each file line can be +#define CANON_TOOL_COMMENT_SIZE 40 // max comment string (include trailing null) + +struct CANON_TOOL_TABLE { + int toolno; + int pocketno; + EmcPose offset; + double diameter; + double frontangle; + double backangle; + int orientation; + char comment[CANON_TOOL_COMMENT_SIZE]; +}; + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/nml_intf/interp_return.hh b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/interp_return.hh new file mode 100644 index 0000000..73a600e --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/nml_intf/interp_return.hh @@ -0,0 +1,40 @@ +/******************************************************************** +* Description: interp_return.hh +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2005 All rights reserved. +* +* Last change: +* +* This file declares the public interpreter return values. An +* interpreter may extend this list with return values that are +* used internally within the interpreters own code, but these +* constitute the minimum set. +********************************************************************/ +#ifndef INTERP_RETURN_H +#define INTERP_RETURN_H + +enum InterpReturn { + INTERP_OK = 0, + INTERP_EXIT = 1, + INTERP_EXECUTE_FINISH = 2, + INTERP_ENDFILE = 3, + INTERP_FILE_NOT_OPEN = 4, + INTERP_ERROR = 5, +}; + +/* +The return values OK, EXIT, EXECUTE_FINISH, and ENDFILE represent +normal, non-error return conditions. FILE_NOT_OPEN is the first +value that represents an error result. INTERP_MIN_ERROR +is therefore the index of the last non-error return value. +*/ + +static const InterpReturn INTERP_MIN_ERROR = INTERP_ENDFILE; + +#endif /* INTERP_RETURN_H */ diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_array.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_array.cc new file mode 100644 index 0000000..ba233a1 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_array.cc @@ -0,0 +1,309 @@ +/******************************************************************** +* Description: interp_array.cc +* +* This file just allocates space for the static arrays used by the +* interpreter. +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +********************************************************************/ + +#include "rs274ngc_return.hh" +#include "rs274ngc_interp.hh" + +using namespace interp_param_global; + +/* Interpreter arrays for g_codes and m_codes. The nth entry +in each array is the modal group number corresponding to the nth +code. Entries which are -1 represent illegal codes. Remember g_codes +in this interpreter are multiplied by 10. + +The modal g groups and group numbers defined in [NCMS, pages 71 - 73] +(see also [Fanuc, pages 43 - 45]) are used here, except the canned +cycles (g80 - g89), which comprise modal g group 9 in [Fanuc], are +treated here as being in the same modal group (group 1) with the +straight moves and arcs (g0, g1, g2,g3). [Fanuc, page 45] says only +one g_code from any one group may appear on a line, and we are +following that rule. The straight_probe move, g38.2, is in group 1; it +is not defined in [NCMS]. + +Some g_codes are non-modal (g4, g10, g28, g30, g53, g92, g92.1, g92.2, +and g92.3 here - many more in [NCMS]). [Fanuc] and [NCMS] put all +these in the same group 0, so we do also. Logically, there are two +subgroups, those which require coordinate values (g10, g28, g30, and +g92) and those which do not (g4, g53, g92.1, g92.2, and g92.3). +The subgroups are identified by itemization when necessary. + +Those in group 0 which require coordinate values may not be on the +same line as those in group 1 (except g80) because they would be +competing for the coordinate values. Others in group 0 may be used on +the same line as those in group 1. + +A total of 52 G-codes are implemented. + +The groups are: +group 0 = {g4,g10,g28,g30,g52,g53,g92,g92.1,g92.2,g92.3} - NON-MODAL + dwell, setup, return to ref1, return to ref2, + local coordinate system, motion in machine coordinates, + set and unset axis offsets +group 1 = {g0,g1,g2,g3,g33,g33.1,g38.2,g38.3,g38.4,g38.5, + g70,g71,g71.1,g71.2,g72,g72.1,g72.2, + g73,g76,g80, + g81,g82,g83,g84,g85,g86,g87,g88,g89} - motion +group 2 = {g17,g17.1,g18,g18.1,g19,g19.1} - plane selection +group 3 = {g90,g91} - distance mode +group 4 = {g90.1,g91.1} - arc IJK distance mode +group 5 = {g93,g94,g95} - feed rate mode +group 6 = {g20,g21} - units +group 7 = {g40,g41,g42} - cutter diameter compensation +group 8 = {g43,g49} - tool length offset +group 10 = {g98,g99} - return mode in canned cycles +group 12 = {g54,g55,g56,g57,g58,g59,g59.1,g59.2,g59.3} - coordinate system +group 13 = {g61,g61.1,g64} - control mode (path following) +group 14 = {g96,g97} - spindle speed mode +group 15 = {G07,G08} - lathe diameter mode +group 16 = {g92.2,g92.3} - whether g92 offset is applied +*/ +// This stops indent from reformatting the following code. +// *INDENT-OFF* +const int Interp::gees[] = { +/* 0 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 20 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 40 */ //0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 40 */ 0,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1, 1, 1, 0,-1,-1,-1,-1,-1,-1, +/* 60 */ 1, 1, 1, 0,-1,-1,-1,-1,-1,-1,15,-1,-1,-1,-1,-1,-1,-1,-1,-1, // jjf added G6 +/* 80 */ 15,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 100 */ 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 120 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 140 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 160 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 2, 2,-1,-1,-1,-1,-1,-1,-1,-1, +/* 180 */ 2, 2,-1,-1,-1,-1,-1,-1,-1,-1, 2, 2,-1,-1,-1,-1,-1,-1,-1,-1, +/* 200 */ 6,-1,-1,-1,-1,-1,-1,-1,-1,-1, 6,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 220 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 240 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 260 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 280 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 300 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 320 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 340 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 360 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 380 */ -1,-1, 1, 1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 400 */ 7,-1,-1,-1,-1,-1,-1,-1,-1,-1, 7, 7,-1,-1,-1,-1,-1,-1,-1,-1, +/* 420 */ 7, 7,-1,-1,-1,-1,-1,-1,-1,-1, 8, 8, 8,-1,-1,-1,-1,-1,-1,-1, +/* 440 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 460 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 480 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 8,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 500 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 520 */ 0,-1,-1,-1,-1,-1,-1,-1,-1,-1, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 540 */ 12,-1,-1,-1,-1,-1,-1,-1,-1,-1,12,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 560 */ 12,-1,-1,-1,-1,-1,-1,-1,-1,-1,12,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 580 */ 12,-1,-1,-1,-1,-1,-1,-1,-1,-1,12,12,12,12,-1,-1,-1,-1,-1,-1, +/* 600 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,13,13,-1,-1,-1,-1,-1,-1,-1,-1, +/* 620 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 640 */ 13,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 660 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 680 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 700 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1, 1, 1,-1,-1,-1,-1,-1,-1,-1, +/* 720 */ 1, 1, 1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 740 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 760 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 780 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 800 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 820 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 840 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 860 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 880 */ 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 900 */ 3, 4,-1,-1,-1,-1,-1,-1,-1,-1, 3, 4,-1,-1,-1,-1,-1,-1,-1,-1, +/* 920 */ 0,16,16,16,-1,-1,-1,-1,-1,-1, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 940 */ 5,-1,-1,-1,-1,-1,-1,-1,-1,-1, 5,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 960 */ 14,-1,-1,-1,-1,-1,-1,-1,-1,-1,14,-1,-1,-1,-1,-1,-1,-1,-1,-1, +/* 980 */ 10,-1,-1,-1,-1,-1,-1,-1,-1,-1,10,-1,-1,-1,-1,-1,-1,-1,-1,-1}; + +/* + +Modal groups and modal group numbers for M codes are not described in +[Fanuc]. We have used the groups from [NCMS] and added M60, as an +extension of the language for pallet shuttle and stop. This version has +no codes related to axis clamping. + +The groups are: +group 4 = {m0,m1,m2,m30,m60, + m99} - stopping +group 5 = {m62,m63,m64,m65, - turn I/O point on/off + m66} - wait for Input +group 6 = {m6,m61} - tool change +group 7 = {m3,m4,m5,m19} - spindle turning, orient +group 8 = {m7,m8,m9} - coolant +group 9 = {m48,m49, - feed and speed override switch bypass + m50, - feed override switch bypass P1 to turn on, P0 to turn off + m51, - spindle speed override switch bypass P1 to turn on, P0 to turn off + m52, - adaptive feed override switch bypass P1 to turn on, P0 to turn off + m53} - feedstop override switch bypass P1 to turn on, P0 to turn off +group 10 = {m100..m199} - user-defined +*/ + +const int Interp::ems[] = { + 4, 4, 4, 7, 7, 7, 6, 8, 8, 8, // 9 + -1, -1, -1, -1, -1, -1, -1, -1, -1, 7, // 19 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 29 + 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 39 + -1, -1, -1, -1, -1, -1, -1, -1, 9, 9, // 49 + 9, 9, 9, 9, -1, -1, -1, -1, -1, -1, // 59 + 4, 6, 5, 5, 5, 5, 5, 5, 5, -1, // 69 + 7, 7, 7, 7, -1, -1, -1, -1, -1, -1, // 79 + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 89 + -1, -1, -1, -1, -1, -1, -1, -1, -1, 4, // 99 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //109 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //119 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //129 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //139 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //149 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //159 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //169 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //179 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, //189 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};//199 + +/* + +This is an array of the index numbers of system parameters that must +be included in a file used with the Interp::restore_parameters +function. The array is used by that function and by the +Interp::save_parameters function. + +*/ + +const int Interp::required_parameters[] = { + 5161, 5162, 5163, /* G28 home */ + 5164, 5165, 5166, /* A, B, & C */ + 5167, 5168, 5169, /* U, V, & W */ + 5181, 5182, 5183, /* G30 home */ + 5184, 5185, 5186, /* A, B, & C */ + 5187, 5188, 5189, /* U, V, & W */ + 5210, /* G92 is currently applied */ + 5211, 5212, 5213, /* G92 offsets */ + 5214, 5215, 5216, /* A, B, & C */ + 5217, 5218, 5219, /* U, V, & W */ + 5220, /* selected coordinate */ + 5221, 5222, 5223, /* coordinate system 1 */ + 5224, 5225, 5226, /* A, B, & C */ + 5227, 5228, 5229, /* U, V, & W */ + 5230, + 5241, 5242, 5243, /* coordinate system 2 */ + 5244, 5245, 5246, /* A, B, & C */ + 5247, 5248, 5249, /* U, V, & W */ + 5250, + 5261, 5262, 5263, /* coordinate system 3 */ + 5264, 5265, 5266, /* A, B, & C */ + 5267, 5268, 5269, /* U, V, & W */ + 5270, + 5281, 5282, 5283, /* coordinate system 4 */ + 5284, 5285, 5286, /* A, B, & C */ + 5287, 5288, 5289, /* U, V, & W */ + 5290, + 5301, 5302, 5303, /* coordinate system 5 */ + 5304, 5305, 5306, /* A, B, & C */ + 5307, 5308, 5309, /* U, V, & W */ + 5310, + 5321, 5322, 5323, /* coordinate system 6 */ + 5324, 5325, 5326, /* A, B, & C */ + 5327, 5328, 5329, /* U, V, & W */ + 5330, + 5341, 5342, 5343, /* coordinate system 7 */ + 5344, 5345, 5346, /* A, B, & C */ + 5347, 5348, 5349, /* U, V, & W */ + 5350, + 5361, 5362, 5363, /* coordinate system 8 */ + 5364, 5365, 5366, /* A, B, & C */ + 5367, 5368, 5369, /* U, V, & W */ + 5370, + 5381, 5382, 5383, /* coordinate system 9 */ + 5384, 5385, 5386, /* A, B, & C */ + 5387, 5388, 5389, /* U, V, & W */ + 5390, + RS274NGC_MAX_PARAMETERS +}; + +const int Interp::readonly_parameters[] = { + 5400, // tool toolno + 5401, // tool x offset + 5402, // tool y offset + 5403, // tool z offset + 5404, // tool a offset + 5405, // tool b offset + 5406, // tool c offset + 5407, // tool u offset + 5408, // tool v offset + 5409, // tool w offset + 5410, // tool diameter + 5411, // tool frontangle + 5412, // tool backangle + 5413, // tool orientation + 5420, 5421, 5422, 5423, 5424, 5425, 5426, 5427, 5428, // current X Y ... W +}; +const int Interp::n_readonly_parameters = sizeof(readonly_parameters) / sizeof(int); + +/* _readers is an array of pointers to functions that read. + It is used by read_one_item. + + Each read function is placed in the array according to the ASCII character it + corresponds to. Whilst a switch statement could have been used in read_one_item, + using an array of function pointers allows a new read_foo to be added quickly + in this one table. + + At some point, it may be advantageous to add a read_$ or read_n for perhaps + macro or jump labels.. + */ +const read_function_pointer Interp::default_readers[256] = { +/* 00 */ +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* 10 */ +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* 20 */ +0, 0, 0, +&Interp::read_parameter_setting, // reads # or ASCII 0x23 +&Interp::read_dollar, // reads $ or ASCII 0x24 +0, 0, 0, +&Interp::read_comment, // reads ( or ASCII 0x28 +0, 0, 0, 0, 0, 0, 0, +/* 30 */ +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +&Interp::read_semicolon, +0, 0, 0, 0, +/* 40 */ +&Interp::read_atsign, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* 50 */ +0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, &Interp::read_carat, 0, +/* 60 */ +0, +&Interp::read_a, // reads a or ASCII 0x61 +&Interp::read_b, // reads b or ASCII 0x62 +&Interp::read_c, // reads c or ASCII 0x63 +&Interp::read_d, // reads d or ASCII 0x64 +&Interp::read_e, // reads d or ASCII 0x65 +&Interp::read_f, // reads f or ASCII 0x66 +&Interp::read_g, // reads g or ASCII 0x67 +&Interp::read_h, // reads h or ASCII 0x68 +&Interp::read_i, // reads i or ASCII 0x69 +&Interp::read_j, // reads j or ASCII 0x6A +&Interp::read_k, // reads k or ASCII 0x6B +&Interp::read_l, // reads l or ASCII 0x6C +&Interp::read_m, // reads m or ASCII 0x6D +0, 0, +&Interp::read_p, // reads p or ASCII 0x70 +&Interp::read_q, // reads q or ASCII 0x71 +&Interp::read_r, // reads r or ASCII 0x72 +&Interp::read_s, // reads s or ASCII 0x73 +&Interp::read_t, // reads t or ASCII 0x74 +&Interp::read_u, +&Interp::read_v, +&Interp::read_w, +&Interp::read_x, // reads x or ASCII 0x78 +&Interp::read_y, // reads y or ASCII 0x79 +&Interp::read_z}; // reads z or ASCII 0x7A +// *INDENT-ON* +// And now indent can continue. +/****************************************************************************/ diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.cc new file mode 100644 index 0000000..d598b6a --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.cc @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 Jeff Epler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +#include "interp_base.hh" +#include +#include +#include +#include + +InterpBase::~InterpBase() {} + +InterpBase *interp_from_shlib(const char *shlib) { + void * interp_lib; + char relative_interp[PATH_MAX]; + char const * interp_path; + + dlopen(NULL, RTLD_GLOBAL); + + if (shlib[0] == '/') { + // The passed-in .so name is an absolute path, use it directly. + interp_path = shlib; + } else { + // The passed-in .so name is a relative path or just a bare + // filename, look for it in `${EMC2_HOME}/lib/linuxcnc`. + snprintf(relative_interp, sizeof(relative_interp), "%s/%s", EMC2_HOME "/lib/linuxcnc", shlib); + interp_path = relative_interp; + } + + interp_lib = dlopen(interp_path, RTLD_NOW); + if(!interp_lib) { + fprintf(stderr, "emcTaskInit: could not open interpreter '%s': %s\n", interp_path, dlerror()); + return 0; + } + fprintf(stderr, "emcTaskInit: using custom interpreter '%s'\n", interp_path); + + typedef InterpBase* (*Constructor)(); + Constructor constructor = (Constructor)dlsym(interp_lib, "makeInterp"); + if(!constructor) { + fprintf(stderr, "emcTaskInit: could not get symbol makeInterp from interpreter '%s': %s\n", shlib, dlerror()); + return 0; + } + InterpBase *pinterp = constructor(); + if(!pinterp) { + fprintf(stderr, "emcTaskInit: makeInterp() returned NULL from interpreter '%s'\n", shlib); + return 0; + } + return pinterp; +} diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.hh b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.hh new file mode 100644 index 0000000..21ddd71 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_base.hh @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2013 Jeff Epler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef INTERP_BASE_HH +#define INTERP_BASE_HH + +#include +#include +#include +#include +#include "modal_state.hh" + +/* Size of certain arrays */ +#define ACTIVE_G_CODES 17 +#define ACTIVE_M_CODES 10 +#define ACTIVE_SETTINGS 5 + +class InterpBase : boost::noncopyable { +public: + virtual ~InterpBase(); + virtual char *error_text(int errcode, char *buf, size_t buflen) = 0; + virtual char *line_text(char *buf, size_t buflen) = 0; + virtual char *file_name(char *buf, size_t buflen) = 0; + virtual char *stack_name(int index, char *buf, size_t buflen) = 0; + virtual size_t line_length() = 0; + virtual int sequence_number() = 0; + virtual int ini_load(const char *inifile) = 0; + virtual int init() = 0; + virtual int execute() = 0; + virtual int execute(const char *line) = 0; + virtual int execute(const char *line, int line_number) = 0; + virtual int synch() = 0; + virtual int exit() = 0; + virtual int open(const char *filename) = 0; + virtual int read() = 0; + virtual int read(const char *line) = 0; + virtual int close() = 0; + virtual int reset() = 0; + virtual int line() = 0; + virtual int call_level() = 0; + virtual char *command(char *buf, size_t buflen) = 0; + virtual char *file(char *buf, size_t buflen) = 0; + virtual int on_abort(int reason, const char *message) = 0; + virtual void active_g_codes(int active_gcodes[ACTIVE_G_CODES]) = 0; + virtual void active_m_codes(int active_mcodes[ACTIVE_M_CODES]) = 0; + virtual void active_settings(double active_settings[ACTIVE_SETTINGS]) = 0; + virtual int active_modes(int g_codes[ACTIVE_G_CODES], + int m_codes[ACTIVE_M_CODES], + double settings[ACTIVE_SETTINGS], + StateTag const &tag) = 0; + virtual int restore_from_tag(StateTag const &tag) = 0; + virtual void print_state_tag(StateTag const &tag) = 0; + virtual void set_loglevel(int level) = 0; + virtual void set_loop_on_main_m99(bool state) = 0; + virtual FILE* get_stdout() { return stdout; }; +}; + +InterpBase *interp_from_shlib(const char *shlib); +extern "C" InterpBase *makeInterp(); + +#endif diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_check.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_check.cc new file mode 100644 index 0000000..b5c9749 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_check.cc @@ -0,0 +1,389 @@ +/******************************************************************** +* Description: interp_check.cc +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" + +/****************************************************************************/ + +/*! check_g_codes + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. NCE_DWELL_TIME_MISSING_WITH_G4 + 2. NCE_MUST_USE_G0_OR_G1_WITH_G53 + 3. NCE_CANNOT_USE_G53_INCREMENTAL + 4. NCE_LINE_WITH_G10_DOES_NOT_HAVE_L2 + 5. NCE_P_VALUE_NOT_AN_INTEGER_WITH_G10_L2 + 6. NCE_P_VALUE_OUT_OF_RANGE_WITH_G10_L2 + 7. NCE_BUG_BAD_G_CODE_MODAL_GROUP_0 + +Side effects: none + +Called by: check_items + +This runs checks on g_codes from a block of RS274/NGC instructions. +Currently, all checks are on g_codes in modal group 0. + +The read_g function checks for errors which would foul up the reading. +The enhance_block function checks for logical errors in the use of +axis values by G-codes in modal groups 0 and 1. +This function checks for additional logical errors in g_codes. + +[Fanuc, page 45, note 4] says there is no maximum for how many g_codes +may be put on the same line, [NCMS] says nothing one way or the other, +so the test for that is not used. + +We are suspending any implicit motion g_code when a g_code from our +group 0 is used. The implicit motion g_code takes effect again +automatically after the line on which the group 0 g_code occurs. It +is not clear what the intent of [Fanuc] is in this regard. The +alternative is to require that any implicit motion be explicitly +cancelled. + +Not all checks on g_codes are included here. Those checks that are +sensitive to whether other g_codes on the same line have been executed +yet are made by the functions called by convert_g. + +Our reference sources differ regarding what codes may be used for +dwell time. [Fanuc, page 58] says use "p" or "x". [NCMS, page 23] says +use "p", "x", or "u". We are allowing "p" only, since it is consistent +with both sources and "x" would be confusing. However, "p" is also used +with G10, where it must be an integer, so reading "p" values is a bit +more trouble than would be nice. + +*/ + +int Interp::check_g_codes(block_pointer block, //!< pointer to a block to be checked + setup_pointer settings) //!< pointer to machine settings +{ + int mode0, mode1; + int p_int; + + mode0 = block->g_modes[GM_MODAL_0]; + mode1 = block->g_modes[GM_MOTION]; + if (mode0 == -1) { + } else if (mode0 == G_4) { + CHKS((block->p_number == -1.0), NCE_DWELL_TIME_MISSING_WITH_G4); + CHKS((mode1 == G_2 || mode1 == G_3), _("G4 not allowed with G2 or G3 because they both use P")); + } else if (mode0 == G_10) { + (block->p_number >= 0) ? p_int = (int) (block->p_number +0.5) :p_int = (int) (block->p_number -0.5); + CHKS((block->l_number != 0 && block->l_number != 2 && block->l_number != 1 && block->l_number != 20 && block->l_number != 10 && block->l_number != 11), _("Line with G10 does not have L0, L1, L10, L11, L2, or L20")); + CHKS((((block->p_number + 0.0001) - p_int) > 0.0002), _("P value not an integer with G10")); + CHKS((((block->l_number == 2 || block->l_number == 20) && ((p_int < 0) || (p_int > 9)))), _("P value out of range (0-9) with G10 L%d"), block->l_number); + CHKS((((block->l_number == 1 || block->l_number == 10 || block->l_number == 11) && p_int < 1)), _("P value out of range with G10 L%d"), block->l_number); + } else if (mode0 == G_28) { + } else if (mode0 == G_30) { + } else if (mode0 == G_5_3) { + CHKS(((mode1 != G_5_2) && (mode1 != -1)), _("Between G5.2 and G5.3 codes, only additional G5.2 codes are allowed.")); + } else if (mode1 == G_5_2){ + } else if (mode1 == G_6_2){ + } else if (mode0 == G_28_1 || mode0 == G_30_1) { + } else if (mode0 == G_52) { + } else if (mode0 == G_53) { + CHKS(((block->motion_to_be != G_0) && (block->motion_to_be != G_1)), + NCE_MUST_USE_G0_OR_G1_WITH_G53); + CHKS(((block->g_modes[GM_DISTANCE_MODE] == G_91) || + ((block->g_modes[GM_DISTANCE_MODE] != G_90) && + (settings->distance_mode == DISTANCE_MODE::INCREMENTAL))), + NCE_CANNOT_USE_G53_INCREMENTAL); + } else if (mode0 == G_92) { + } else + ERS(NCE_BUG_BAD_G_CODE_MODAL_GROUP_0); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! check_items + +Returned Value: int + If any one of check_g_codes, check_m_codes, and check_other_codes + returns an error code, this returns that code. + Otherwise, it returns INTERP_OK. + +Side effects: none + +Called by: parse_line + +This runs checks on a block of RS274 code. + +The functions named read_XXXX check for errors which would foul up the +reading. This function checks for additional logical errors. + +A block has an array of g_codes, which are initialized to -1 +(meaning no code). This calls check_g_codes to check the g_codes. + +A block has an array of m_codes, which are initialized to -1 +(meaning no code). This calls check_m_codes to check the m_codes. + +Items in the block which are not m or g codes are checked by +check_other_codes. + +*/ + +int Interp::check_items(block_pointer block, //!< pointer to a block to be checked + setup_pointer settings) //!< pointer to machine settings +{ + + CHP(check_g_codes(block, settings)); + CHP(check_m_codes(block)); + CHP(check_other_codes(block)); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! check_m_codes + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. There are too many m codes in the block: NCE_TOO_MANY_M_CODES_ON_LINE + +Side effects: none + +Called by: check_items + +This runs checks on m_codes from a block of RS274/NGC instructions. + +The read_m function checks for errors which would foul up the +reading. This function checks for additional errors in m_codes. + +*/ + +int Interp::check_m_codes(block_pointer block) //!< pointer to a block to be checked +{ + + CHKS((block->m_count > MAX_EMS), NCE_TOO_MANY_M_CODES_ON_LINE); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! check_other_codes + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. An A-axis value is given with a canned cycle (g80 to g89): + NCE_CANNOT_PUT_AN_A_IN_CANNED_CYCLE + 2. A B-axis value is given with a canned cycle (g80 to g89): + NCE_CANNOT_PUT_A_B_IN_CANNED_CYCLE + 3. A C-axis value is given with a canned cycle (g80 to g89): + NCE_CANNOT_PUT_A_C_IN_CANNED_CYCLE + 4. A d word is in a block with no cutter_radius_compensation_on command: + NCE_D_WORD_WITH_NO_G41_OR_G42 + 5. An h_number is in a block with no tool length offset setting: + NCE_H_WORD_WITH_NO_G43 + 6. An i_number is in a block with no G-code that uses it: + NCE_I_WORD_WITH_NO_G2_OR_G3_OR_G87_TO_USE_IT + 7. A j_number is in a block with no G-code that uses it: + NCE_J_WORD_WITH_NO_G2_OR_G3_OR_G87_TO_USE_IT + 8. A k_number is in a block with no G-code that uses it: + NCE_K_WORD_WITH_NO_G2_OR_G3_OR_G87_TO_USE_IT + 9. A l_number is in a block with no G-code that uses it: + NCE_L_WORD_WITH_NO_CANNED_CYCLE_OR_G10 + 10. A p_number is in a block with no G-code that uses it: + NCE_P_WORD_WITH_NO_G4_G10_G64_G82_G86_G88_G89 + 11. A q_number is in a block with no G-code that uses it: + NCE_Q_WORD_WITH_NO_G83_OR_M66 + 12. An r_number is in a block with no G-code that uses it: + NCE_R_WORD_WITH_NO_G_CODE_THAT_USES_IT + 13. A k word is missing from a G33 block: + NCE_K_WORD_MISSING_WITH_G33 + 14. An e word is in a block with no G76 or M66 to use it: + NCE_E_WORD_WITH_NO_G76_OR_M66_TO_USE_IT + +Side effects: none + +Called by: check_items + +This runs checks on codes from a block of RS274/NGC code which are +not m or g codes. + +The functions named read_XXXX check for errors which would foul up the +reading. This function checks for additional logical errors in codes. + +*/ + +int Interp::check_other_codes(block_pointer block) //!< pointer to a block of RS274/NGC instructions +{ + int motion; + + motion = block->motion_to_be; + + // bypass ALL checks, argspec takes care of that + if (is_user_defined_g_code(motion)) { + return INTERP_OK; + } + // bypass ALL checks, argspec takes care of that + if (is_any_m_code_remapped(block, &(_setup))) { + return INTERP_OK; + } + if (block->a_flag) { + CHKS(is_a_cycle(motion), NCE_CANNOT_PUT_AN_A_IN_CANNED_CYCLE); + } + if (block->b_flag) { + CHKS(is_a_cycle(motion), NCE_CANNOT_PUT_A_B_IN_CANNED_CYCLE); + } + if (block->c_flag) { + CHKS(is_a_cycle(motion), NCE_CANNOT_PUT_A_C_IN_CANNED_CYCLE); + } + if (block->d_flag) { + CHKS(((block->g_modes[7] != G_41) && (block->g_modes[7] != G_42) && + (block->g_modes[7] != G_41_1) && (block->g_modes[7] != G_42_1) && + (motion != G_70) && (motion != G_71) && (motion != G_71_1) && + (motion != G_71_2) && (motion != G_72) && (motion != G_72_1) && + (motion != G_72_2) && (motion != G_73) && (motion != G_83) && + (block->g_modes[14] != G_96)), + _("D word with no G41, G41.1, G42, G42.1, G71, G71.1, G71.2 G73, G83 or G96 to use it")); + } + + if (block->dollar_flag) { + CHKS(((motion != G_76) && (motion != G_33) && (motion != G_33_1) && + (block->g_modes[GM_FEED_MODE] != G_95) && + (block->g_modes[GM_SPINDLE_MODE] != G_96) && + (block->g_modes[GM_SPINDLE_MODE] != G_97) && + (block->m_modes[7] != 3) && (block->m_modes[7] != 4) && + (block->m_modes[7] != 5) && (block->m_modes[7] != 19) && + (block->m_modes[9] != 51) && (! block->s_flag)), + _("$ (spindle selection) word with no M3, M4, M5, M19, M51, G33, G33.1, G76, G95, G96 or G97 to use it")); + } + + if (block->e_flag) { + CHKS(((motion != G_76) && (motion != G_33) && (motion != G_33_1) && + (motion != G_70) && (block->m_modes[5] != 66) && + (block->m_modes[5] != 67) && (block->m_modes[5] != 68)), + _("E word with no G70, G76, M66, M67 or M68 to use it")); + } + + if (block->h_flag) { + CHKS((block->g_modes[GM_TOOL_LENGTH_OFFSET] != G_43 && motion != G_76 && block->g_modes[GM_TOOL_LENGTH_OFFSET] != G_43_2), + _("H word with no G43 or G76 to use it")); + } + + if (block->i_flag) { /* could still be useless if yz_plane arc */ + CHKS(((motion != G_2) && (motion != G_3) && (motion != G_5) && (motion != G_5_1) && + (motion != G_6) && (motion != G_6_1) && + (motion != G_71) && (motion != G_71_1) && (motion != G_71_2) && + (motion != G_72) && (motion != G_72_1) && (motion != G_72_2) && + (motion != G_76) && (motion != G_87) && (motion != G_33_1) && (block->g_modes[GM_MODAL_0] != G_10)), + _("I word with no G2, G3, G5, G5.1, G6, G6.1, G10, G33.1, G76, or G87 to use it")); + } + + if (block->j_flag) { /* could still be useless if xz_plane arc */ + CHKS(((motion != G_2) && (motion != G_3) && (motion != G_5) && (motion != G_5_1) && + (motion != G_6) && (motion != G_6_1) && + (motion != G_76) && (motion != G_87) && (block->g_modes[GM_MODAL_0] != G_10)), + _("J word with no G2, G3, G5, G5.1, G6, G6.1, G10, G76 or G87 to use it")); + } + + if (block->k_flag) { /* could still be useless if xy_plane arc */ + CHKS(((motion != G_2) && (motion != G_3) && (motion != G_6_2) && (motion != G_33) && (motion != G_33_1) && (motion != G_76) && (motion != G_87)), + _("K word with no G2, G3, G6.2, G33, G33.1, G76, or G87 to use it")); + } + + if (block->l_number != -1) { + CHKS((((motion < G_81) || (motion > G_89)) && (motion != G_76) && + (motion != G_5_2) && (motion != G_6_2) && (motion != G_73) && + (block->g_modes[GM_MODAL_0] != G_10) && + (block->g_modes[GM_CUTTER_COMP] != G_41) && (block->g_modes[GM_CUTTER_COMP] != G_41_1) && + (block->g_modes[GM_CUTTER_COMP] != G_42) && (block->g_modes[GM_CUTTER_COMP] != G_42_1) && + (block->m_modes[5] != 66) && + (block->o_type != M_98) // m98 repeat + ), + _("L word with no G10, cutter compensation, canned cycle, " + "digital/analog input, M98 or NURBS code")); + } + + if (block->p_flag) { + CHKS(((block->g_modes[GM_MODAL_0] != G_10) && (block->g_modes[GM_MODAL_0] != G_4) && (block->g_modes[GM_CONTROL_MODE] != G_64) && + (motion != G_76) && (motion != G_82) && (motion != G_86) && (motion != G_88) && + (motion != G_89) && (motion != G_5) && (motion != G_5_2) && + (motion != G_70) && + (motion != G_6) && (motion != G_6_2) && + (motion != G_2) && (motion != G_3) && + (motion != G_74) && (motion != G_84) && + (block->m_modes[9] != 50) && (block->m_modes[9] != 51) && (block->m_modes[9] != 52) && + (block->m_modes[9] != 53) && (block->m_modes[5] != 62) && (block->m_modes[5] != 63) && + (block->m_modes[5] != 64) && (block->m_modes[5] != 65) && (block->m_modes[5] != 66) && + (block->m_modes[7] != 19) && (block->user_m != 1) && + (block->o_type != M_98)), + _("P word with no G2 G3 G4 G10 G64 G5 G5.2 G6, G6.2, G76 G82 G86 G88 G89" + " or M50 M51 M52 M53 M62 M63 M64 M65 M66 M98 " + "or user M code to use it")); + int p_value = round_to_int(block->p_number); + CHKS(((motion == G_2 || motion == G_3 || (block->m_modes[7] == 19)) && + fabs(p_value - block->p_number) > 0.001), + _("P value not an integer with M19 G2 or G3")); + CHKS((block->m_modes[7] == 19) && ((p_value > 2) || p_value < 0), + _("P value must be 0,1,or 2 with M19")); + CHKS(((motion == G_2 || motion == G_3) && round_to_int(block->p_number) < 1), + _("P value should be 1 or greater with G2 or G3")); + } + + if (block->q_number != -1.0) { + CHKS((motion != G_83) && (motion != G_73) && (motion != G_5) && (motion != G_6) && (motion != G_6_2) && (block->user_m != 1) && (motion != G_76) && + (block->m_modes[5] != 66) && (block->m_modes[5] != 67) && (block->m_modes[5] != 68) && + (block->g_modes[GM_MODAL_0] != G_10) && (block->m_modes[6] != 61) && (block->g_modes[GM_CONTROL_MODE] != G_64) && + (motion != G_70) && + (motion != G_71) && (motion != G_71_1) && (motion != G_71_2) && + (motion != G_72) && (motion != G_72_1) && (motion != G_72_2) && + (block->m_modes[7] != 19), + _("Q word with no G5, G6, G10, G64, G73, G76, G83, M19, M66, M67, M68 or user M code that uses it")); + } + + if (block->r_flag) { + CHKS(((motion != G_2) && (motion != G_3) && (motion != G_76) && (motion != G_6_2) && + (motion != G_71) && (motion != G_71_1) && (motion != G_71_2) && + (motion != G_72) && (motion != G_72_1) && (motion != G_72_2) && + ((motion < G_81) || (motion > G_89)) && (motion != G_73) && + (motion != G_74) && + (block->g_modes[GM_CUTTER_COMP] != G_41_1) && (block->g_modes[GM_CUTTER_COMP] != G_42_1) && + (block->g_modes[GM_MODAL_0] != G_10) && (block->m_modes[7] != 19) ), + NCE_R_WORD_WITH_NO_G_CODE_THAT_USES_IT); + CHKS((block->m_modes[7] == 19) && ((block->r_number > 360.0) || (block->r_number < 0.0)), + _("R value must be within 0..360 with M19")); + } + + if (!block->s_flag) { + CHKS((block->g_modes[GM_SPINDLE_MODE] == G_96), NCE_S_WORD_MISSING_WITH_G96); + } + + if (motion == G_33 || motion == G_33_1) { + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G33); + CHKS((block->f_flag), NCE_F_WORD_USED_WITH_G33); + } + + if (motion == G_76) { + // pitch + CHKS((block->p_number == -1), NCE_P_WORD_MISSING_WITH_G76); + + CHKS((!block->i_flag || !block->j_flag || !block->k_flag), + NCE_I_J_OR_K_WORDS_MISSING_WITH_G76); + } + + return INTERP_OK; +} diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_convert.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_convert.cc new file mode 100644 index 0000000..dfe87ef --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_convert.cc @@ -0,0 +1,6550 @@ +/* misnomer: settings->current_pocket,selected_pocket +** These variables are actually indexes to sequential tool +** data structs (not real pockets). +** Future renaming will affect current usage in python remaps. +*/ + +/******************************************************************** +* Description: interp_convert.cc +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +********************************************************************/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "rs274ngc_interp.hh" +#include "interp_internal.hh" +#include "interp_queue.hh" +#include "interp_parameter_def.hh" +#include + +#include "units.h" +#define TOOL_INSIDE_ARC(side, turn) (((side)==CUTTER_COMP::LEFT&&(turn)>0)||((side)==CUTTER_COMP::RIGHT&&(turn)<0)) +#define DEBUG_EMC + +using namespace interp_param_global; + +// These four functions help make the rest of cutter comp +// plane-agnostic in much the same way the ARC_FEED canon call is. +// The programmer can gleefully think of only the XY plane when +// reading convert_[straight|arc]_comp[1|2]. +// +// Because the STRAIGHT_[FEED|TRAVERSE] canon calls are not +// plane-agnostic, the opposite plane conversion happens in +// enqueue_STRAIGHT_[FEED|TRAVERSE] when adding to the interp queue. + +int Interp::comp_get_current(setup_pointer settings, double *x, double *y, double *z) { + switch(settings->plane) { + case CANON_PLANE::XY: + *x = settings->current_x; + *y = settings->current_y; + *z = settings->current_z; + break; + case CANON_PLANE::XZ: + *x = settings->current_z; + *y = settings->current_x; + *z = settings->current_y; + break; + default: + ERS("BUG: Invalid plane in comp_get_current"); + } + return INTERP_OK; +} + +int Interp::comp_set_current(setup_pointer settings, double x, double y, double z) { + switch(settings->plane) { + case CANON_PLANE::XY: + settings->current_x = x; + settings->current_y = y; + settings->current_z = z; + break; + case CANON_PLANE::XZ: + settings->current_x = y; + settings->current_y = z; + settings->current_z = x; + break; + default: + ERS("BUG: Invalid plane in comp_set_current"); + } + return INTERP_OK; +} + +int Interp::comp_get_programmed(setup_pointer settings, double *x, double *y, double *z) { + switch(settings->plane) { + case CANON_PLANE::XY: + *x = settings->program_x; + *y = settings->program_y; + *z = settings->program_z; + break; + case CANON_PLANE::XZ: + *x = settings->program_z; + *y = settings->program_x; + *z = settings->program_y; + break; + default: + ERS("BUG: Invalid plane in comp_get_programmed"); + } + return INTERP_OK; +} + +int Interp::comp_set_programmed(setup_pointer settings, double x, double y, double z) { + switch(settings->plane) { + case CANON_PLANE::XY: + settings->program_x = x; + settings->program_y = y; + settings->program_z = z; + break; + case CANON_PLANE::XZ: + settings->program_x = y; + settings->program_y = z; + settings->program_z = x; + break; + default: + ERS("BUG: Invalid plane in comp_set_programmed"); + } + return INTERP_OK; +} + +/* Set *result to the integer nearest to value; return TRUE if value is + * within .0001 of an integer + */ +static int is_near_int(int *result, double value) { + *result = (int)(value + .5); + return fabs(*result - value) < .0001; +} + +/****************************************************************************/ + +/*! convert_nurbs + * + * Returned value: int + * Returns a rs274ngc error code, or INTERP_OK if everything is OK. + * + * Side effects: Generates a nurbs move and updates the position of the tool + +Reference for this code is: +Lo Valvo, E., Drago, S. (2014). +An Efficient NURBS Path Generator for an Open Source CNC. +Recent Advances in Mechanical Engineering (pp.173-180). WSEAS Press. + +Q=3 NICL NURBS interpolation with linear motion +Q=2 NICC NURBS interpolation with circular motion +Q=1 NICU NURBS interpolation with biarch, du=const +*/ +static unsigned int Q_G6_option = 0; // (NICU, NICL, NICC see publication from Lo Valvo and Drago) +static unsigned int nurbs_order; +static std::vector < NURBS_CONTROL_POINT > nurbs_g5_control_points; +static std::vector < NURBS_G6_CONTROL_POINT > nurbs_g6_control_points; +int ff_G6_line_counter = 0; //inizializzo il contatore + +void Interp::nurbs_reset_global_variables(void) +{ // called from open gcode file and Interp::convert_stop + Q_G6_option = 0; + ff_G6_line_counter = 0; + if (!nurbs_g5_control_points.empty()) { + nurbs_g5_control_points.clear(); + } + if (!nurbs_g6_control_points.empty()) { + nurbs_g6_control_points.clear(); + } +} + +int Interp::convert_nurbs(int mode, + block_pointer block, setup_pointer settings) +{ + + double end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, + w_end; + NURBS_CONTROL_POINT CP_G5; + NURBS_G6_CONTROL_POINT CP_G6; + + if (mode == G_5_2) { + + if (settings->plane == CANON_PLANE::XY) { + CHKS((((block->x_flag) && !(block->y_flag)) + || (!(block->x_flag) + && (block->y_flag))), + (_ + ("You must specify both X and Y coordinates for Control Points"))); + CHKS((!(block->x_flag) && !(block->y_flag) + && (block->p_number > 0) + && (!nurbs_g5_control_points.empty())), + (_ + ("Can specify P without X and Y only for the first control point"))); + } + if (settings->plane == CANON_PLANE::YZ) { + CHKS((((block->y_flag) && !(block->z_flag)) + || (!(block->y_flag) + && (block->z_flag))), + (_ + ("You must specify both Y and Z coordinates for Control Points"))); + CHKS((!(block->y_flag) && !(block->z_flag) + && (block->p_number > 0) + && (!nurbs_g5_control_points.empty())), + (_ + ("Can specify P without Y and Z only for the first control point"))); + } + if (settings->plane == CANON_PLANE::XZ) { + CHKS((((block->x_flag) && !(block->z_flag)) + || (!(block->x_flag) + && (block->z_flag))), + (_ + ("You must specify both X and Z coordinates for Control Points"))); + CHKS((!(block->x_flag) && !(block->z_flag) + && (block->p_number > 0) + && (!nurbs_g5_control_points.empty())), + (_ + ("Can specify P without X and Z only for the first control point"))); + } + + CHKS(((block->p_number <= 0) + && (!nurbs_g5_control_points.empty())), + (_ + ("Must specify positive weight P for every Control Point"))); + if (settings->feed_mode == FEED_MODE::UNITS_PER_MINUTE) { + CHKS((settings->feed_rate == 0.0), + (_("Cannot make a NURBS with 0 feedrate"))); + } + if (settings->motion_mode != mode) + nurbs_g5_control_points.clear(); + + if (nurbs_g5_control_points.empty()) { + if (settings->plane == CANON_PLANE::XY) { + CP_G5.NURBS_X = settings->current_x; + CP_G5.NURBS_Y = settings->current_y; + if (!(block->x_flag) && !(block->y_flag) + && (block->p_number > 0)) { + CP_G5.NURBS_W = block->p_number; + } else { + CP_G5.NURBS_W = 1; + } + } + if (settings->plane == CANON_PLANE::YZ) { + CP_G5.NURBS_X = settings->current_y; + CP_G5.NURBS_Y = settings->current_z; + if (!(block->y_flag) && !(block->z_flag) + && (block->p_number > 0)) { + CP_G5.NURBS_W = block->p_number; + } else { + CP_G5.NURBS_W = 1; + } + } + if (settings->plane == CANON_PLANE::XZ) { + CP_G5.NURBS_X = settings->current_z; + CP_G5.NURBS_Y = settings->current_x; + if (!(block->x_flag) && !(block->z_flag) + && (block->p_number > 0)) { + CP_G5.NURBS_W = block->p_number; + } else { + CP_G5.NURBS_W = 1; + } + } + nurbs_order = 3; + nurbs_g5_control_points.push_back(CP_G5); + } + if (block->l_number != -1 && block->l_number > 3) { + nurbs_order = block->l_number; + } + + if (settings->plane == CANON_PLANE::XY) { + if ((block->x_flag) && (block->y_flag)) { + CHP(find_ends + (block, settings, &CP_G5.NURBS_X, + &CP_G5.NURBS_Y, &end_z, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, + &w_end)); + CP_G5.NURBS_W = block->p_number; + nurbs_g5_control_points.push_back(CP_G5); + } + } + if (settings->plane == CANON_PLANE::YZ) { + if ((block->y_flag) && (block->z_flag)) { + CHP(find_ends + (block, settings, &end_x, + &CP_G5.NURBS_X, &CP_G5.NURBS_Y, + &AA_end, &BB_end, &CC_end, &u_end, + &v_end, &w_end)); + CP_G5.NURBS_W = block->p_number; + nurbs_g5_control_points.push_back(CP_G5); + } + } + if (settings->plane == CANON_PLANE::XZ) { + if ((block->x_flag) && (block->z_flag)) { + CHP(find_ends + (block, settings, &CP_G5.NURBS_Y, + &end_y, &CP_G5.NURBS_X, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, + &w_end)); + CP_G5.NURBS_W = block->p_number; + nurbs_g5_control_points.push_back(CP_G5); + } + } + +/* + for (long unsigned int i=0;imotion_mode = mode; + } + + else if (mode == G_5_3) { + CHKS((settings->motion_mode != G_5_2), + (_("Cannot use G5.3 without G5.2 first"))); + CHKS((nurbs_g5_control_points.size() < nurbs_order), + _ + ("You must specify a number of control points at least equal to the order L = %d"), + nurbs_order); + + if (settings->plane == CANON_PLANE::XY) { + settings->current_x = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_X; + settings->current_y = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_Y; + } + if (settings->plane == CANON_PLANE::YZ) { + settings->current_y = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_X; + settings->current_z = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_Y; + } + if (settings->plane == CANON_PLANE::XZ) { + settings->current_z = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_X; + settings->current_x = + nurbs_g5_control_points + [nurbs_g5_control_points.size() - 1].NURBS_Y; + } +/* + for (long unsigned int i=0;iline_number, nurbs_g5_control_points, + nurbs_order, settings->plane); + nurbs_g5_control_points.clear(); + settings->motion_mode = -1; + } +// + if (mode == G_6_2) { + // jjf additional for Q (as replacement for #1 in the original code from Lo Valvo) + // Q=3 NICL NURBS interpolation with linear motion + // Q=2 NICC NURBS interpolation with circular motion + // Q=1 NICU NURBS interpolation with biarch, du=const + + CHKS(((settings->plane != CANON_PLANE::XY) + && (settings->plane != CANON_PLANE::YZ) + && (settings->plane != CANON_PLANE::XZ)), + (_ + ("one plane must be selected for nurbs: XY, YZ, ZX"))); + + if (settings->plane == CANON_PLANE::XY) { + CHKS((((block->x_flag) && !(block->y_flag)) + || (!(block->x_flag) + && (block->y_flag))), + (_ + ("You must specify both X and Y coordinates for Control Points"))); + } + if (settings->plane == CANON_PLANE::YZ) { + CHKS((((block->y_flag) && !(block->z_flag)) + || (!(block->y_flag) + && (block->z_flag))), + (_ + ("You must specify both Y and Z coordinates for Control Points"))); + } + if (settings->plane == CANON_PLANE::XZ) { + CHKS((((block->x_flag) && !(block->z_flag)) + || (!(block->x_flag) + && (block->z_flag))), + (_ + ("You must specify both X and Z coordinates for Control Points"))); + } + + CHKS(((!block->p_flag) + && (nurbs_g6_control_points.empty())), + (_ + ("Program the nurbs order P in the first block of instruction"))); + int p; + if (block->p_flag) { + CHKS((!is_near_int(&p, block->p_number)), + _("nurbs order P number is not an integer")); + } + + CHKS(((block->q_number <= 0) + && (nurbs_g6_control_points.empty())), + (_ + ("Program the nurbs Q value (1,2,3) in the first block of nurbs instruction"))); + int q; + if (block->q_flag) { + CHKS((block->q_number < 1.0) + || (block->q_number > 3.0), + (_("nurbs Q value not in range 1 to 3"))); + CHKS((!is_near_int(&q, block->q_number)), + _("nurbs Q number is not an integer")); + } + + CHKS(((block->r_number <= 0) + && (!nurbs_g6_control_points.empty())), + (_ + ("Must specify positive nurbs weight R for every Control Point"))); + + CHKS(((block->k_number < 0) + && (!nurbs_g6_control_points.empty())), + (_ + ("Must specify nurbs K number for every Control Point"))); + int k; + if (block->k_flag) { + CHKS((!is_near_int(&k, block->k_number)), + _("nurbs K number is not an integer")); + } + + if (settings->feed_mode == FEED_MODE::UNITS_PER_MINUTE) { + CHKS((settings->feed_rate == 0.0), + (_("Cannot make a nurbs with 0 feedrate"))); + } + + if (block->p_number != -1) { + nurbs_order = block->p_number; + } + + if (block->k_flag) { + CP_G6.NURBS_K = block->k_number; + } + // PER DEFINIRE UNO FRA I TRE MODI DI INTERPOLARE (NURBS) + // EINE DER DREI WEGE VON INTERPOLATION (NURBS) AUSWAEHLERN + // TO DEFINE ONE OF THE THREE WAYS OF INTERPOLAR (NURBS) + if (ff_G6_line_counter == 0) { + Q_G6_option = block->q_number; // Q_G6_option=1...3 (NICU, NICL, NICC see publication from Lo Valvo and Drago) + } + + if (settings->plane == CANON_PLANE::XY) { + if (((block->x_flag) && (block->y_flag)) + || block->k_flag) { + CHP(find_ends + (block, settings, &CP_G6.NURBS_X, + &CP_G6.NURBS_Y, &end_z, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, + &w_end)); + CP_G6.NURBS_R = block->r_number; + nurbs_g6_control_points.push_back(CP_G6); + } + } + if (settings->plane == CANON_PLANE::YZ) { + if (((block->y_flag) && (block->z_flag)) + || block->k_flag) { + CHP(find_ends + (block, settings, &end_x, + &CP_G6.NURBS_X, &CP_G6.NURBS_Y, + &AA_end, &BB_end, &CC_end, &u_end, + &v_end, &w_end)); + CP_G6.NURBS_R = block->r_number; + nurbs_g6_control_points.push_back(CP_G6); + } + } + if (settings->plane == CANON_PLANE::XZ) { + if (((block->x_flag) && (block->z_flag)) + || block->k_flag) { + CHP(find_ends + (block, settings, &CP_G6.NURBS_Y, + &end_z, &CP_G6.NURBS_X, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, + &w_end)); + CP_G6.NURBS_R = block->r_number; + nurbs_g6_control_points.push_back(CP_G6); + } + } + + settings->motion_mode = mode; + + ff_G6_line_counter++; + if (nurbs_g6_control_points.size() > nurbs_order) { + if (nurbs_g6_control_points + [ff_G6_line_counter - 1].NURBS_K == + nurbs_g6_control_points[ff_G6_line_counter - + nurbs_order + 1 - + 1].NURBS_K) { + CHKS((nurbs_g6_control_points.size() < + nurbs_order), + _ + ("You must specify a number of control points at least equal to the order P = %d"), + nurbs_order); + settings->current_x = + nurbs_g6_control_points + [nurbs_g6_control_points.size() - + 1].NURBS_X; + settings->current_y = + nurbs_g6_control_points + [nurbs_g6_control_points.size() - + 1].NURBS_Y; +/* + for (long unsigned int i=0;iline_number, + nurbs_g6_control_points, + nurbs_order, + settings->feed_rate, + Q_G6_option, + settings->plane); + nurbs_g6_control_points.clear(); + ff_G6_line_counter = 0; + settings->motion_mode = -1; + } + } + } +// + if (mode == G_6_3) // jjf this is missing from Lo Valvo code! not used there and here! + { + printf("mode == G_6_3 not used (%s %d)\n", __FILE__, + __LINE__); + } + return INTERP_OK; +} + + /****************************************************************************/ + /*! convert_spline + * + * Returned value: int + * Returns a rs274ngc error code, or INTERP_OK if everything is OK. + * + * Side effects: Generates a spline move and updates the position of the tool + */ +int Interp::convert_spline(int mode, block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) +{ //!< pointer to machine settings + double x1, y1, z1, x2, y2, z2, x3, y3, z3; + double end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, + w_end; + NURBS_CONTROL_POINT cp; + + x1 = 0; + y1 = 0; + z1 = 0; //to avoid compiler warning "may be used uninitialized" + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), _("Cannot convert spline with cutter radius compensation")); // XXX + + if (settings->feed_mode == FEED_MODE::UNITS_PER_MINUTE) { + CHKS((settings->feed_rate == 0.0), + NCE_CANNOT_MAKE_ARC_WITH_ZERO_FEED_RATE); + } else if (settings->feed_mode == FEED_MODE::INVERSE_TIME) { + CHKS((!block->f_flag), + NCE_F_WORD_MISSING_WITH_INVERSE_TIME_ARC_MOVE); + } + + CHKS((block->a_flag || block->b_flag + || block->c_flag), + _("Splines may not have motion in A, B, or C")); + + if ((mode == G_5_1) || (mode == G_6_1)) { + printf("mode == G_5_1 or G_6_1 (%s %d)\n", __FILE__, + __LINE__); + CHKS(!block->i_flag + || !block->j_flag, + _("Must specify both I and J with G5.1 or G6.1")); + + if (settings->plane == CANON_PLANE::XY) { + x1 = settings->current_x + block->i_number; + y1 = settings->current_y + block->j_number; + CHP(find_ends + (block, settings, &x2, &y2, &end_z, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, &w_end)); + printf("find_ends x2: %8.4f y2: %8.4f (%s %d)\n", + x2, y2, __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::YZ) { + y1 = settings->current_y + block->i_number; + z1 = settings->current_z + block->j_number; + CHP(find_ends + (block, settings, &end_x, &y2, &z2, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, &w_end)); + printf("find_ends y2: %8.4f z2: %8.4f (%s %d)\n", + y2, z2, __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::XZ) { + x1 = settings->current_x + block->i_number; + z1 = settings->current_z + block->j_number; + CHP(find_ends + (block, settings, &x2, &end_y, &z2, &AA_end, + &BB_end, &CC_end, &u_end, &v_end, &w_end)); + printf("find_ends x2: %8.4f z2: %8.4f (%s %d)\n", + x2, z2, __FILE__, __LINE__); + } + + cp.NURBS_W = 1; + + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = settings->current_x; + cp.NURBS_Y = settings->current_y; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = settings->current_y; + cp.NURBS_Y = settings->current_z; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = settings->current_z; + cp.NURBS_Y = settings->current_x; + } + nurbs_g5_control_points.push_back(cp); + + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = x1; + cp.NURBS_Y = y1; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = y1; + cp.NURBS_Y = z1; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = z1; + cp.NURBS_Y = x1; + } + nurbs_g5_control_points.push_back(cp); + + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = x2; + cp.NURBS_Y = y2; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = y2; + cp.NURBS_Y = z2; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = z2; + cp.NURBS_Y = x2; + } + nurbs_g5_control_points.push_back(cp); + + for (long unsigned int i = 0; + i < nurbs_g5_control_points.size(); i++) { + printf("X %8.4f, Y %8.4f W %8.4f\n", + nurbs_g5_control_points[i].NURBS_X, + nurbs_g5_control_points[i].NURBS_Y, + nurbs_g5_control_points[i].NURBS_W); + } + printf + ("*----------------------------------------- (%s %d)\n", + __FILE__, __LINE__); + + NURBS_G5_FEED(block->line_number, nurbs_g5_control_points, + 3, settings->plane); + nurbs_g5_control_points.clear(); + + if (settings->plane == CANON_PLANE::XY) { + settings->current_x = x2; + settings->current_y = y2; + } + if (settings->plane == CANON_PLANE::YZ) { + settings->current_y = y2; + settings->current_z = z2; + } + if (settings->plane == CANON_PLANE::XZ) { + settings->current_x = x2; + settings->current_z = z2; + } + } else { // !(mode == G_5_1) + if (!block->i_flag || !block->j_flag) { + CHKS(block->i_flag + || block->j_flag, + _("Must specify both I and J, or neither")); + if (settings->plane == CANON_PLANE::XY) { + x1 = settings->current_x + + settings->cycle_i; + y1 = settings->current_y + + settings->cycle_j; + } + if (settings->plane == CANON_PLANE::YZ) { + y1 = settings->current_y + + settings->cycle_i; + z1 = settings->current_z + + settings->cycle_j; + } + if (settings->plane == CANON_PLANE::XZ) { + x1 = settings->current_x + + settings->cycle_i; + z1 = settings->current_z + + settings->cycle_j; + } + } else { + if (settings->plane == CANON_PLANE::XY) { + x1 = settings->current_x + block->i_number; + y1 = settings->current_y + block->j_number; + CHP(find_ends + (block, settings, &x3, &y3, &end_z, + &AA_end, &BB_end, &CC_end, &u_end, + &v_end, &w_end)); + printf("x3: %8.4f y3: %8.4f (%s %d)\n", x3, + y3, __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::YZ) { + y1 = settings->current_y + block->i_number; + z1 = settings->current_z + block->j_number; + CHP(find_ends + (block, settings, &end_x, &y3, &z3, + &AA_end, &BB_end, &CC_end, &u_end, + &v_end, &w_end)); + printf("y3: %8.4f z3: %8.4f (%s %d)\n", y3, + z3, __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::XZ) { + x1 = settings->current_x + block->i_number; + z1 = settings->current_z + block->j_number; + CHP(find_ends + (block, settings, &x3, &end_y, &z3, + &AA_end, &BB_end, &CC_end, &u_end, + &v_end, &w_end)); + printf("x3: %8.4f z3: %8.4f (%s %d)\n", x3, + z3, __FILE__, __LINE__); + } + } + + CHKS(!block->p_flag + || !block->q_flag, + _("Must specify both P and Q with G5")); + if (settings->plane == CANON_PLANE::XY) { + x2 = x3 + block->p_number; + y2 = y3 + block->q_number; + printf("x2: %8.4f y2: %8.4f (%s %d)\n", x2, y2, + __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::YZ) { + y2 = y3 + block->p_number; + z2 = z3 + block->q_number; + printf("y2: %8.4f z2: %8.4f (%s %d)\n", y2, z2, + __FILE__, __LINE__); + } + if (settings->plane == CANON_PLANE::XZ) { + z2 = z3 + block->p_number; + x2 = x3 + block->q_number; + printf("x2: %8.4f z2: %8.4f (%s %d)\n", x2, z2, + __FILE__, __LINE__); + } + + cp.NURBS_W = 1; + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = settings->current_x; + cp.NURBS_Y = settings->current_y; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = settings->current_y; + cp.NURBS_Y = settings->current_z; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = settings->current_z; + cp.NURBS_Y = settings->current_x; + } + nurbs_g5_control_points.push_back(cp); + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = x1; + cp.NURBS_Y = y1; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = y1; + cp.NURBS_Y = z1; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = z1; + cp.NURBS_Y = x1; + } + nurbs_g5_control_points.push_back(cp); + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = x2; + cp.NURBS_Y = y2; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = y2; + cp.NURBS_Y = z2; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = z2; + cp.NURBS_Y = x2; + } + nurbs_g5_control_points.push_back(cp); + if (settings->plane == CANON_PLANE::XY) { + cp.NURBS_X = x3; + cp.NURBS_Y = y3; + } + if (settings->plane == CANON_PLANE::YZ) { + cp.NURBS_X = y3; + cp.NURBS_Y = z3; + } + if (settings->plane == CANON_PLANE::XZ) { + cp.NURBS_X = z3; + cp.NURBS_Y = x3; + } + nurbs_g5_control_points.push_back(cp); + +/* + for (long unsigned int i=0;iline_number, nurbs_g5_control_points, + 4, settings->plane); + nurbs_g5_control_points.clear(); + + settings->cycle_i = -block->p_number; + settings->cycle_j = -block->q_number; + if (settings->plane == CANON_PLANE::XY) { + settings->current_x = x3; + settings->current_y = y3; + } + if (settings->plane == CANON_PLANE::YZ) { + settings->current_y = y3; + settings->current_z = z3; + } + if (settings->plane == CANON_PLANE::XZ) { + settings->current_x = x3; + settings->current_z = z3; + } + } + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_arc + +Returned Value: int + If one of the following functions returns an error code, + this returns that error code. + convert_arc_comp1 + convert_arc_comp2 + convert_arc2 + If any of the following errors occur, this returns the error code shown. + Otherwise, this returns INTERP_OK. + 1. The block has neither an r value nor any i,j,k values: + NCE_R_I_J_K_WORDS_ALL_MISSING_FOR_ARC + 2. The block has both an r value and one or more i,j,k values: + NCE_MIXED_RADIUS_IJK_FORMAT_FOR_ARC + 3. In the ijk format the XY-plane is selected and + the block has a k value: NCE_K_WORD_GIVEN_FOR_ARC_IN_XY_PLANE + 4. In the ijk format the YZ-plane is selected and + the block has an i value: NCE_I_WORD_GIVEN_FOR_ARC_IN_YZ_PLANE + 5. In the ijk format the XZ-plane is selected and + the block has a j value: NCE_J_WORD_GIVEN_FOR_ARC_IN_XZ_PLANE + 6. In either format any of the following occurs. + a. The XY-plane is selected and the block has no x or y value: + NCE_X_AND_Y_WORDS_MISSING_FOR_ARC_IN_XY_PLANE + b. The YZ-plane is selected and the block has no y or z value: + NCE_Y_AND_Z_WORDS_MISSING_FOR_ARC_IN_YZ_PLANE + c. The ZX-plane is selected and the block has no z or x value: + NCE_X_AND_Z_WORDS_MISSING_FOR_ARC_IN_XZ_PLANE + 7. The selected plane is an unknown plane: + NCE_BUG_PLANE_NOT_XY_YZ__OR_XZ + 8. The feed rate mode is UNITS_PER_MINUTE and feed rate is zero: + NCE_CANNOT_MAKE_ARC_WITH_ZERO_FEED_RATE + 9. The feed rate mode is INVERSE_TIME and the block has no f word: + NCE_F_WORD_MISSING_WITH_INVERSE_TIME_ARC_MOVE + +Side effects: + This generates and executes an arc command at feed rate + (and, possibly a second arc command). It also updates the setting + of the position of the tool point to the end point of the move. + +Called by: convert_motion. + +This converts a helical or circular arc. The function calls: +convert_arc2 (when cutter radius compensation is off) or +convert_arc_comp1 (when cutter comp is on and this is the first move) or +convert_arc_comp2 (when cutter comp is on and this is not the first move). + +If the ijk format is used, at least one of the offsets in the current +plane must be given in the block; it is common but not required to +give both offsets. The offsets are always incremental [NCMS, page 21]. + +If cutter compensation is in use, the path's length may increase or +decrease. Also an arc may be added, to go around a corner, before the +original arc move. For the purpose of calculating the feed rate when in +inverse time mode, this length increase or decrease is ignored. The +feed is still set to the original programmed arc length divided by the F +number (with the above lower bound). The new arc (if needed) and the +new longer or shorter original arc are taken at this feed. + +*/ + +int Interp::convert_arc(int move, //!< either G_2 (cw arc) or G_3 (ccw arc) + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + int status; + int first; /* flag set true if this is first move after comp true */ + int ijk_flag; /* flag set true if any of i,j,k present in NC code */ + double end_x; + double end_y; + double end_z; + double AA_end; + double BB_end; + double CC_end; + double u_end, v_end, w_end; + + CHKS((settings->arc_not_allowed), (_("The move just after exiting cutter compensation mode must be straight, not an arc"))); + + ijk_flag = block->i_flag || block->j_flag || block->k_flag; + first = settings->cutter_comp_firstmove; + + CHKS((settings->plane == CANON_PLANE::UV + || settings->plane == CANON_PLANE::VW + || settings->plane == CANON_PLANE::UW), + _("Cannot do an arc in planes G17.1, G18.1, or G19.1")); + CHKS(((!block->r_flag) && (!ijk_flag)), + NCE_R_I_J_K_WORDS_ALL_MISSING_FOR_ARC); + CHKS(((block->r_flag) && (ijk_flag)), + NCE_MIXED_RADIUS_IJK_FORMAT_FOR_ARC); + if (settings->feed_mode == FEED_MODE::UNITS_PER_MINUTE) { + CHKS((settings->feed_rate == 0.0), + NCE_CANNOT_MAKE_ARC_WITH_ZERO_FEED_RATE); + } else if(settings->feed_mode == FEED_MODE::UNITS_PER_REVOLUTION) { + CHKS((settings->feed_rate == 0.0), + NCE_CANNOT_MAKE_ARC_WITH_ZERO_FEED_RATE); + CHKS((settings->speed[settings->active_spindle] == 0.0), + _("Cannot feed with zero spindle speed in feed per rev mode")); + } else if (settings->feed_mode == FEED_MODE::INVERSE_TIME) { + CHKS((!block->f_flag), + NCE_F_WORD_MISSING_WITH_INVERSE_TIME_ARC_MOVE); + } + + if (ijk_flag) { + if (settings->plane == CANON_PLANE::XY) { + CHKS((block->k_flag), NCE_K_WORD_GIVEN_FOR_ARC_IN_XY_PLANE); + if (!block->i_flag) { /* i or j flag on to get here */ + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'I'); + } else { + block->i_number = 0.0; + } + } else if (!block->j_flag) { + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'J'); + } else { + block->j_number = 0.0; + } + } + } else if (settings->plane == CANON_PLANE::YZ) { + CHKS((block->i_flag), NCE_I_WORD_GIVEN_FOR_ARC_IN_YZ_PLANE); + if (!block->j_flag) { /* j or k flag on to get here */ + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'J'); + } else { + block->j_number = 0.0; + } + } else if (!block->k_flag) { + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'K'); + } else { + block->k_number = 0.0; + } + } + } else if (settings->plane == CANON_PLANE::XZ) { + CHKS((block->j_flag), NCE_J_WORD_GIVEN_FOR_ARC_IN_XZ_PLANE); + if (!block->i_flag) { /* i or k flag on to get here */ + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'I'); + } else { + block->i_number = 0.0; + } + } else if (!block->k_flag) { + if (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE) { + ERS(_("%c word missing in absolute center arc"), 'K'); + } else { + block->k_number = 0.0; + } + } + } else { + ERS(NCE_BUG_PLANE_NOT_XY_YZ_OR_XZ); + } + } else { + // in R format, we need some XYZ words specified because a full circle is not allowed. + if (settings->plane == CANON_PLANE::XY) { + CHKS(((!block->x_flag) && (!block->y_flag) && (!block->radius_flag) && (!block->theta_flag)), + NCE_X_AND_Y_WORDS_MISSING_FOR_ARC_IN_XY_PLANE); + } else if (settings->plane == CANON_PLANE::YZ) { + CHKS(((!block->y_flag) && (!block->z_flag)), + NCE_Y_AND_Z_WORDS_MISSING_FOR_ARC_IN_YZ_PLANE); + } else if (settings->plane == CANON_PLANE::XZ) { + CHKS(((!block->x_flag) && (!block->z_flag)), + NCE_X_AND_Z_WORDS_MISSING_FOR_ARC_IN_XZ_PLANE); + } + } + + + CHP(find_ends(block, settings, &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, + &u_end, &v_end, &w_end)); + + settings->motion_mode = move; + + // Should be done with changes to settings here, so we can pack the state + write_canon_state_tag(block, settings); + + if (settings->plane == CANON_PLANE::XY) { + if ((settings->cutter_comp_side == CUTTER_COMP::OFF) || + (settings->cutter_comp_radius == 0.0)) { + status = + convert_arc2(move, block, settings, + &(settings->current_x), &(settings->current_y), + &(settings->current_z), end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end, + block->i_number, block->j_number); + CHP(status); + } else if (first) { + status = convert_arc_comp1(move, block, settings, end_x, end_y, end_z, + block->i_number, block->j_number, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + CHP(status); + } else { + status = convert_arc_comp2(move, block, settings, end_x, end_y, end_z, + block->i_number, block->j_number, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + CHP(status); + } + } else if (settings->plane == CANON_PLANE::XZ) { + if ((settings->cutter_comp_side == CUTTER_COMP::OFF) || + (settings->cutter_comp_radius == 0.0)) { + status = + convert_arc2(move, block, settings, + &(settings->current_z), &(settings->current_x), + &(settings->current_y), end_z, end_x, end_y, + AA_end, BB_end, CC_end, + u_end, v_end, w_end, + block->k_number, block->i_number); + CHP(status); + } else if (first) { + status = convert_arc_comp1(move, block, settings, end_z, end_x, end_y, + block->k_number, block->i_number, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + CHP(status); + } else { + status = convert_arc_comp2(move, block, settings, end_z, end_x, end_y, + block->k_number, block->i_number, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + + CHP(status); + } + } else if (settings->plane == CANON_PLANE::YZ) { + status = + convert_arc2(move, block, settings, + &(settings->current_y), &(settings->current_z), + &(settings->current_x), end_y, end_z, end_x, + AA_end, BB_end, CC_end, + u_end, v_end, w_end, + block->j_number, block->k_number); + CHP(status); + } else + ERS(NCE_BUG_PLANE_NOT_XY_YZ_OR_XZ); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_arc2 + +Returned Value: int + If arc_data_ijk or arc_data_r returns an error code, + this returns that code. + Otherwise, it returns INTERP_OK. + +Side effects: + This executes an arc command at feed rate. It also updates the + setting of the position of the tool point to the end point of the move. + +Called by: convert_arc. + +This converts a helical or circular arc. + +*/ + +int Interp::convert_arc2(int move, //!< either G_2 (cw arc) or G_3 (ccw arc) + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings, //!< pointer to machine settings + double *current1, //!< pointer to current value of coordinate 1 + double *current2, //!< pointer to current value of coordinate 2 + double *current3, //!< pointer to current value of coordinate 3 + double end1, //!< coordinate 1 value at end of arc + double end2, //!< coordinate 2 value at end of arc + double end3, //!< coordinate 3 value at end of arc + double AA_end, //!< a-value at end of arc + double BB_end, //!< b-value at end of arc + double CC_end, //!< c-value at end of arc + double u, double v, double w, //!< values at end of arc + double offset1, //!< center, either abs or offset from current + double offset2) +{ + double center1; + double center2; + int turn; /* number of full or partial turns CCW in arc */ + CANON_PLANE plane = settings->plane; + + // Spiral tolerance is the amount of "spiral" allowed in a given arc segment, or (r2-r1)/theta + double spiral_abs_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? + settings->center_arc_radius_tolerance_inch : settings->center_arc_radius_tolerance_mm; + + // Radius tolerance allows a bit of leeway on the minimum radius for a radius defined arc. + double radius_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? + RADIUS_TOLERANCE_INCH : RADIUS_TOLERANCE_MM; + + if (block->r_flag) { + CHP(arc_data_r(move, plane, *current1, *current2, end1, end2, + block->r_number, block->p_flag? round_to_int(block->p_number) : 1, + ¢er1, ¢er2, &turn, radius_tolerance)); + } else { + CHP(arc_data_ijk(move, plane, *current1, *current2, end1, end2, + (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE), + offset1, offset2, block->p_flag? round_to_int(block->p_number) : 1, + ¢er1, ¢er2, &turn, radius_tolerance, spiral_abs_tolerance, SPIRAL_RELATIVE_TOLERANCE)); + } + inverse_time_rate_arc(*current1, *current2, *current3, center1, center2, + turn, end1, end2, end3, block, settings); + + // We need to determine which 'center' is X and which is Y + double abs_x = 0, abs_y = 0, abs_z =0; + double abs_cx = 0, abs_cy = 0,abs_cz=0; + + if (settings->plane == CANON_PLANE::XY) { + // Plane 1=X, 2=Y, 3=Z + abs_x = end1; + abs_y = end2; + abs_z = end3; + abs_cx = center1; + abs_cy = center2; + abs_cz = *current3; + } else if (settings->plane == CANON_PLANE::XZ) { + // Plane 1=X, 2=Z, 3=Y + abs_x = end1; + abs_y = end3; // Or whatever your system expects for non-active axes + abs_z = end2; + abs_cx = center1; + abs_cy = *current3; + abs_cz = center2; + } else { // YZ plane + // Plane 1=Y, 2=Z, 3=X + abs_x = end3; + abs_y = end1; + abs_z = end2; + abs_cx = *current3; + abs_cy = center1; + abs_cz = center2; + } + // Call tagger with the resolved X, Y, CX, and CY + tag_arc(block, abs_x, abs_y, abs_z, abs_cx, abs_cy, abs_cz, move, settings->plane ); + + ARC_FEED(block->line_number, end1, end2, center1, center2, turn, end3, + AA_end, BB_end, CC_end, u, v, w); + *current1 = end1; + *current2 = end2; + *current3 = end3; + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u; + settings->v_current = v; + settings->w_current = w; + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_arc_comp1 + +Returned Value: int + If arc_data_comp_ijk or arc_data_comp_r returns an error code, + this returns that code. + Otherwise, it returns INTERP_OK. + +Side effects: + This executes an arc command at + feed rate. It also updates the setting of the position of + the tool point to the end point of the move. + +Called by: convert_arc. + +This function converts a helical or circular arc, generating only one +arc. This is called when cutter radius compensation is on and this is +the first cut after the turning on. + +The arc which is generated is derived from a second arc which passes +through the programmed end point and is tangent to the cutter at its +current location. The generated arc moves the tool so that it stays +tangent to the second arc throughout the move. + +*/ + +int Interp::convert_arc_comp1(int move, //!< either G_2 (cw arc) or G_3 (ccw arc) + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings, //!< pointer to machine settings + double end_x, //!< x-value at end of programmed (then actual) arc + double end_y, //!< y-value at end of programmed (then actual) arc + double end_z, //!< z-value at end of arc + double offset_x, double offset_y, + double AA_end, //!< a-value at end of arc + double BB_end, //!< b-value at end of arc + double CC_end, //!< c-value at end of arc + double u_end, double v_end, double w_end) //!< uvw at end of arc +{ + double center_x, center_y; + double gamma; /* direction of perpendicular to arc at end */ + CUTTER_COMP side; /* offset side - right or left */ + double tool_radius; + int turn; /* 1 for counterclockwise, -1 for clockwise */ + double cx, cy, cz; // current + CANON_PLANE plane = settings->plane; + + side = settings->cutter_comp_side; + tool_radius = settings->cutter_comp_radius; /* always is positive */ + + double spiral_abs_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? settings->center_arc_radius_tolerance_inch : settings->center_arc_radius_tolerance_mm; + double radius_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? RADIUS_TOLERANCE_INCH : RADIUS_TOLERANCE_MM; + + comp_get_current(settings, &cx, &cy, &cz); + + CHKS((hypot((end_x - cx), (end_y - cy)) <= tool_radius), + _("Radius of cutter compensation entry arc is not greater than the tool radius")); + + if (block->r_flag) { + CHP(arc_data_comp_r(move, plane, side, tool_radius, cx, cy, end_x, end_y, + block->r_number, block->p_flag? round_to_int(block->p_number): 1, + ¢er_x, ¢er_y, &turn, radius_tolerance)); + } else { + CHP(arc_data_comp_ijk(move, plane, side, tool_radius, cx, cy, end_x, end_y, + (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE), + offset_x, offset_y, block->p_flag? round_to_int(block->p_number): 1, + ¢er_x, ¢er_y, &turn, radius_tolerance, spiral_abs_tolerance, SPIRAL_RELATIVE_TOLERANCE)); + } + + inverse_time_rate_arc(cx, cy, cz, center_x, center_y, + turn, end_x, end_y, end_z, block, settings); + + + // the tool will end up in gamma direction from the programmed arc endpoint + if TOOL_INSIDE_ARC(side, turn) { + // tool inside the arc: ends up toward the center + gamma = atan2((center_y - end_y), (center_x - end_x)); + } else { + // outside: away from the center + gamma = atan2((end_y - center_y), (end_x - center_x)); + } + + settings->cutter_comp_firstmove = false; + + comp_set_programmed(settings, end_x, end_y, end_z); + + // move endpoint to the compensated position. This changes the radius and center. + end_x += tool_radius * cos(gamma); + end_y += tool_radius * sin(gamma); + + /* To find the new center: + imagine a right triangle ABC with A being the endpoint of the + compensated arc, B being the center of the compensated arc, C being + the midpoint between start and end of the compensated arc. AB_ang + is the direction of A->B. A_ang is the angle of the triangle + itself. We need to find a new center for the compensated arc + (point B). */ + + double b_len = hypot(cy - end_y, cx - end_x) / 2.0; + double AB_ang = atan2(center_y - end_y, center_x - end_x); + double A_ang = atan2(cy - end_y, cx - end_x) - AB_ang; + + CHKS((fabs(cos(A_ang)) < TOLERANCE_EQUAL), NCE_TOOL_RADIUS_NOT_LESS_THAN_ARC_RADIUS_WITH_COMP); + + double c_len = b_len/cos(A_ang); + + // center of the arc is c_len from end in direction AB + center_x = end_x + c_len * cos(AB_ang); + center_y = end_y + c_len * sin(AB_ang); + + /* center to endpoint distances matched before - they still should. */ + CHKS((fabs(hypot(center_x-end_x,center_y-end_y) - + hypot(center_x-cx,center_y-cy)) > spiral_abs_tolerance), + NCE_BUG_IN_TOOL_RADIUS_COMP); + + // need this move for lathes to move the tool origin first. otherwise, the arc isn't an arc. + if (settings->cutter_comp_orientation != 0 && settings->cutter_comp_orientation != 9) { + enqueue_STRAIGHT_FEED(settings, block->line_number, + 0, 0, 0, + cx, cy, cz, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + set_endpoint(cx, cy); + } + + enqueue_ARC_FEED(settings, block->line_number, + find_turn(cx, cy, center_x, center_y, turn, end_x, end_y), + end_x, end_y, center_x, center_y, turn, end_z, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + + comp_set_current(settings, end_x, end_y, end_z); + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_arc_comp2 + +Returned Value: int + If arc_data_ijk or arc_data_r returns an error code, + this returns that code. + If any of the following errors occurs, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. A concave corner is found: NCE_CONCAVE_CORNER_WITH_CUTTER_RADIUS_COMP + 2. The tool will not fit inside an arc: + NCE_TOOL_RADIUS_NOT_LESS_THAN_ARC_RADIUS_WITH_COMP + +Side effects: + This executes an arc command feed rate. If needed, at also generates + an arc to go around a convex corner. It also updates the setting of + the position of the tool point to the end point of the move. +Called by: convert_arc. + +This function converts a helical or circular arc. The axis must be +parallel to the z-axis. This is called when cutter radius compensation +is on and this is not the first cut after the turning on. + +If one or more rotary axes is moved in this block and an extra arc is +required to go around a sharp corner, all the rotary axis motion +occurs on the main arc and none on the extra arc. An alternative +might be to distribute the rotary axis motion over the extra arc and +the programmed arc in proportion to their lengths. + +If the Z-axis is moved in this block and an extra arc is required to +go around a sharp corner, all the Z-axis motion occurs on the main arc +and none on the extra arc. An alternative might be to distribute the +Z-axis motion over the extra arc and the main arc in proportion to +their lengths. + +*/ + +int Interp::convert_arc_comp2(int move, //!< either G_2 (cw arc) or G_3 (ccw arc) + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings, //!< pointer to machine settings + double end_x, //!< x-value at end of programmed (then actual) arc + double end_y, //!< y-value at end of programmed (then actual) arc + double end_z, //!< z-value at end of arc + double offset_x, double offset_y, + double AA_end, //!< a-value at end of arc + double BB_end, //!< b-value at end of arc + double CC_end, //!< c-value at end of arc + double u, double v, double w) //!< uvw at end of arc +{ + double alpha; /* direction of tangent to start of arc */ + double arc_radius; + double beta; /* angle between two tangents above */ + double centerx, centery; /* center of arc */ + double delta; /* direction of radius from start of arc to center of arc */ + double gamma; /* direction of perpendicular to arc at end */ + double midx, midy; + CUTTER_COMP side; + double small = TOLERANCE_CONCAVE_CORNER; /* angle for testing corners */ + double opx = 0, opy = 0, opz = 0; + double theta; /* direction of tangent to last cut */ + double tool_radius; + int turn; /* number of full or partial circles CCW */ + CANON_PLANE plane = settings->plane; + double cx, cy, cz; + double new_end_x, new_end_y; + + double spiral_abs_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? settings->center_arc_radius_tolerance_inch : settings->center_arc_radius_tolerance_mm; + double radius_tolerance = (settings->length_units == CANON_UNITS_INCHES) ? RADIUS_TOLERANCE_INCH : RADIUS_TOLERANCE_MM; + + /* find basic arc data: center_x, center_y, and turn */ + + comp_get_programmed(settings, &opx, &opy, &opz); + comp_get_current(settings, &cx, &cy, &cz); + + + if (block->r_flag) { + CHP(arc_data_r(move, plane, opx, opy, end_x, end_y, + block->r_number, block->p_flag? round_to_int(block->p_number): 1, + ¢erx, ¢ery, &turn, radius_tolerance)); + } else { + CHP(arc_data_ijk(move, plane, + opx, opy, end_x, end_y, + (settings->ijk_distance_mode == DISTANCE_MODE::ABSOLUTE), + offset_x, offset_y, block->p_flag? round_to_int(block->p_number): 1, + ¢erx, ¢ery, &turn, radius_tolerance, spiral_abs_tolerance, SPIRAL_RELATIVE_TOLERANCE)); + } + + inverse_time_rate_arc(opx, opy, opz, centerx, centery, + turn, end_x, end_y, end_z, block, settings); + + side = settings->cutter_comp_side; + tool_radius = settings->cutter_comp_radius; /* always is positive */ + arc_radius = hypot((centerx - end_x), (centery - end_y)); + theta = atan2(cy - opy, cx - opx); + theta = (side == CUTTER_COMP::LEFT) ? (theta - M_PI_2l) : (theta + M_PI_2l); + delta = atan2(centery - opy, centerx - opx); + alpha = (move == G_3) ? (delta - M_PI_2l) : (delta + M_PI_2l); + beta = (side == CUTTER_COMP::LEFT) ? (theta - alpha) : (alpha - theta); + + // normalize beta -90 to +270? + beta = (beta > (1.5 * M_PIl)) ? (beta - (2 * M_PIl)) : (beta < -M_PI_2l) ? (beta + (2 * M_PIl)) : beta; + + if (((side == CUTTER_COMP::LEFT) && (move == G_3)) || ((side == CUTTER_COMP::RIGHT) && (move == G_2))) { + // we are cutting inside the arc + gamma = atan2((centery - end_y), (centerx - end_x)); + CHKS((arc_radius <= tool_radius), + NCE_TOOL_RADIUS_NOT_LESS_THAN_ARC_RADIUS_WITH_COMP); + } else { + gamma = atan2((end_y - centery), (end_x - centerx)); + delta = (delta + M_PIl); + } + + // move arc endpoint to the compensated position + new_end_x = end_x + tool_radius * cos(gamma); + new_end_y = end_y + tool_radius * sin(gamma); + + if (beta < -small || + beta > M_PIl + small || + // special detection for convex corner on tangent arc->arc (like atop the middle of "m" shape) + // or tangent line->arc (atop "h" shape) + (fabs(beta - M_PIl) < small && !TOOL_INSIDE_ARC(side, turn)) + ) { + // concave + if (qc().front().type != QARC_FEED) { + // line->arc + double cy = arc_radius * sin(beta - M_PI_2l); + double toward_nominal; + double dist_from_center; + double angle_from_center; + + if TOOL_INSIDE_ARC(side, turn) { + // tool is inside the arc + dist_from_center = arc_radius - tool_radius; + toward_nominal = cy + tool_radius; + double l = toward_nominal / dist_from_center; + CHKS((l > 1.0 || l < -1.0), _("Arc move in concave corner cannot be reached by the tool without gouging")); + if(turn > 0) { + angle_from_center = theta + asin(l); + } else { + angle_from_center = theta - asin(l); + } + } else { + dist_from_center = arc_radius + tool_radius; + toward_nominal = cy - tool_radius; + double l = toward_nominal / dist_from_center; + CHKS((l > 1.0 || l < -1.0), _("Arc move in concave corner cannot be reached by the tool without gouging")); + if(turn > 0) { + angle_from_center = theta + M_PIl - asin(l); + } else { + angle_from_center = theta + M_PIl + asin(l); + } + } + + midx = centerx + dist_from_center * cos(angle_from_center); + midy = centery + dist_from_center * sin(angle_from_center); + + CHP(move_endpoint_and_flush(settings, midx, midy)); + } else { + // arc->arc + struct arc_feed &prev = qc().front().data.arc_feed; + double oldrad = hypot(prev.center2 - prev.end2, prev.center1 - prev.end1); + double newrad; + if TOOL_INSIDE_ARC(side, turn) { + newrad = arc_radius - tool_radius; + } else { + newrad = arc_radius + tool_radius; + } + + double arc_cc, pullback, cc_dir, a; + arc_cc = hypot(prev.center2 - centery, prev.center1 - centerx); + + CHKS((oldrad == 0 || arc_cc == 0), _("Arc to arc motion is invalid because the arcs have the same center")); + a = (SQ(oldrad) + SQ(arc_cc) - SQ(newrad)) / (2 * oldrad * arc_cc); + + CHKS((a > 1.0 || a < -1.0), (_("Arc to arc motion makes a corner the compensated tool can't fit in without gouging"))); + pullback = acos(a); + cc_dir = atan2(centery - prev.center2, centerx - prev.center1); + + double dir; + if TOOL_INSIDE_ARC(side, prev.turn) { + if(turn > 0) + dir = cc_dir + pullback; + else + dir = cc_dir - pullback; + } else { + if(turn > 0) + dir = cc_dir - pullback; + else + dir = cc_dir + pullback; + } + + midx = prev.center1 + oldrad * cos(dir); + midy = prev.center2 + oldrad * sin(dir); + + CHP(move_endpoint_and_flush(settings, midx, midy)); + } + enqueue_ARC_FEED(settings, block->line_number, + find_turn(opx, opy, centerx, centery, turn, end_x, end_y), + new_end_x, new_end_y, centerx, centery, turn, end_z, + AA_end, BB_end, CC_end, u, v, w); + } else if (beta > small) { /* convex, two arcs needed */ + midx = opx + tool_radius * cos(delta); + midy = opy + tool_radius * sin(delta); + dequeue_canons(settings); + enqueue_ARC_FEED(settings, block->line_number, + 0.0, // doesn't matter since we won't move this arc's endpoint + midx, midy, opx, opy, ((side == CUTTER_COMP::LEFT) ? -1 : 1), + cz, + AA_end, BB_end, CC_end, u, v, w); + dequeue_canons(settings); + set_endpoint(midx, midy); + enqueue_ARC_FEED(settings, block->line_number, + find_turn(opx, opy, centerx, centery, turn, end_x, end_y), + new_end_x, new_end_y, centerx, centery, turn, end_z, + AA_end, BB_end, CC_end, u, v, w); + } else { /* convex, one arc needed */ + dequeue_canons(settings); + set_endpoint(cx, cy); + enqueue_ARC_FEED(settings, block->line_number, + find_turn(opx, opy, centerx, centery, turn, end_x, end_y), + new_end_x, new_end_y, centerx, centery, turn, end_z, + AA_end, BB_end, CC_end, u, v, w); + } + + comp_set_programmed(settings, end_x, end_y, end_z); + comp_set_current(settings, new_end_x, new_end_y, end_z); + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u; + settings->v_current = v; + settings->w_current = w; + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_axis_offsets + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The function is called when cutter radius compensation is on: + NCE_CANNOT_CHANGE_AXIS_OFFSETS_WITH_CUTTER_RADIUS_COMP + 2. The g_code argument is not G_52, G_92, G_92_1, G_92_2, or G_92_3 + NCE_BUG_CODE_NOT_IN_G52_G92_SERIES + +Side effects: + SET_G92_OFFSET is called, and the coordinate + values for the axis offsets are reset. The coordinates of the + current point are reset. Parameters may be set. + +Called by: convert_modal_0. + +The action of G92 is described in [NCMS, pages 10 - 11] and {Fanuc, +pages 61 - 63]. [NCMS] is ambiguous about the intent, but [Fanuc] +is clear. When G92 is executed, an offset of the origin is calculated +so that the coordinates of the current point with respect to the moved +origin are as specified on the line containing the G92. If an axis +is not mentioned on the line, the coordinates of the current point +are not changed. The execution of G92 results in an axis offset being +calculated and saved for each of the six axes, and the axis offsets +are always used when motion is specified with respect to absolute +distance mode using any of the nine coordinate systems (those designated +by G54 - G59.3). Thus all nine coordinate systems are affected by G92. + +G92 and G52 offsets are relative to the current G5x offsets so they +are applied after the G5x offsets and G10 R rotations are applied. + +Being in incremental distance mode has no effect on the action of G92 +in this implementation. [NCMS] is not explicit about this, but it is +implicit in the second sentence of [Fanuc, page 61]. + +The offset is the amount the origin must be moved so that the +coordinate of the controlled point has the specified value. For +example, if the current point is at X=4 in the currently specified +coordinate system and the current X-axis offset is zero, then "G92 x7" +causes the X-axis offset to be reset to -3. + +Since a non-zero offset may be already be in effect when the G92 is +called, that must be taken into account. + +In addition to causing the axis offset values in the _setup model to be +set, G52 and G92 set parameters 5211 to 5216 to the x,y,z,a,b,c axis +offsets. + +The action of G92.2 is described in [NCMS, page 12]. There is no +equivalent command in [Fanuc]. G92.2 resets axis offsets to zero. +G92.1, also included in [NCMS, page 12] (but the usage here differs +slightly from the spec), is like G92.2, except that it also causes +the axis offset parameters to be set to zero, whereas G92.2 does not +zero out the parameters. + +G92.3 is not in [NCMS]. It sets the axis offset values to the values +given in the parameters. + +*/ + +int Interp::convert_axis_offsets(int g_code, //!< g_code being executed (must be in G_92 series) + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + double *pars; /* short name for settings->parameters */ + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), /* not "== true" */ + NCE_CANNOT_CHANGE_AXIS_OFFSETS_WITH_CUTTER_RADIUS_COMP); + CHKS((block->a_flag && settings->a_axis_wrapped && + (block->a_number <= -360.0 || block->a_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), + block->a_number, 'A'); + CHKS((block->b_flag && settings->b_axis_wrapped && + (block->b_number <= -360.0 || block->b_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), + block->b_number, 'B'); + CHKS((block->c_flag && settings->c_axis_wrapped && + (block->c_number <= -360.0 || block->c_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), + block->c_number, 'C'); + pars = settings->parameters; + if ((g_code == G_52) || (g_code == G_92)) { + pars[G92_APPLIED] = 1.0; + + if (g_code == G_52) { + if (block->x_flag) { + settings->current_x += settings->axis_offset_x - block->x_number; + settings->axis_offset_x = block->x_number; + } + + if (block->y_flag) { + settings->current_y += settings->axis_offset_y - block->y_number; + settings->axis_offset_y = block->y_number; + } + + if (block->z_flag) { + settings->current_z += settings->axis_offset_z - block->z_number; + settings->axis_offset_z = block->z_number; + } + if (block->a_flag) { + settings->AA_current += settings->AA_axis_offset - block->a_number; + settings->AA_axis_offset = block->a_number; + } + if (block->b_flag) { + settings->BB_current += settings->BB_axis_offset - block->b_number; + settings->BB_axis_offset = block->b_number; + } + if (block->c_flag) { + settings->CC_current += settings->CC_axis_offset - block->c_number; + settings->CC_axis_offset = block->c_number; + } + if (block->u_flag) { + settings->u_current += settings->u_axis_offset - block->u_number; + settings->u_axis_offset = block->u_number; + } + if (block->v_flag) { + settings->v_current += settings->v_axis_offset - block->v_number; + settings->v_axis_offset = block->v_number; + } + if (block->w_flag) { + settings->w_current += settings->w_axis_offset - block->w_number; + settings->w_axis_offset = block->w_number; + } + + } else { + if (block->x_flag) { + settings->axis_offset_x = + (settings->current_x + settings->axis_offset_x - block->x_number); + settings->current_x = block->x_number; + } + + if (block->y_flag) { + settings->axis_offset_y = + (settings->current_y + settings->axis_offset_y - block->y_number); + settings->current_y = block->y_number; + } + + if (block->z_flag) { + settings->axis_offset_z = + (settings->current_z + settings->axis_offset_z - block->z_number); + settings->current_z = block->z_number; + } + if (block->a_flag) { + settings->AA_axis_offset = (settings->AA_current + + settings->AA_axis_offset - block->a_number); + settings->AA_current = block->a_number; + } + if (block->b_flag) { + settings->BB_axis_offset = (settings->BB_current + + settings->BB_axis_offset - block->b_number); + settings->BB_current = block->b_number; + } + if (block->c_flag) { + settings->CC_axis_offset = (settings->CC_current + + settings->CC_axis_offset - block->c_number); + settings->CC_current = block->c_number; + } + if (block->u_flag) { + settings->u_axis_offset = (settings->u_current + + settings->u_axis_offset - block->u_number); + settings->u_current = block->u_number; + } + if (block->v_flag) { + settings->v_axis_offset = (settings->v_current + + settings->v_axis_offset - block->v_number); + settings->v_current = block->v_number; + } + if (block->w_flag) { + settings->w_axis_offset = (settings->w_current + + settings->w_axis_offset - block->w_number); + settings->w_current = block->w_number; + } + } + + SET_G92_OFFSET(settings->axis_offset_x, + settings->axis_offset_y, + settings->axis_offset_z, + settings->AA_axis_offset, + settings->BB_axis_offset, + settings->CC_axis_offset, + settings->u_axis_offset, + settings->v_axis_offset, + settings->w_axis_offset); + + pars[5211] = PROGRAM_TO_USER_LEN(settings->axis_offset_x); + pars[5212] = PROGRAM_TO_USER_LEN(settings->axis_offset_y); + pars[5213] = PROGRAM_TO_USER_LEN(settings->axis_offset_z); + pars[5214] = PROGRAM_TO_USER_ANG(settings->AA_axis_offset); + pars[5215] = PROGRAM_TO_USER_ANG(settings->BB_axis_offset); + pars[5216] = PROGRAM_TO_USER_ANG(settings->CC_axis_offset); + pars[5217] = PROGRAM_TO_USER_LEN(settings->u_axis_offset); + pars[5218] = PROGRAM_TO_USER_LEN(settings->v_axis_offset); + pars[5219] = PROGRAM_TO_USER_LEN(settings->w_axis_offset); + + } else if ((g_code == G_92_1) || (g_code == G_92_2)) { + pars[5210] = 0.0; + settings->current_x = settings->current_x + settings->axis_offset_x; + settings->current_y = settings->current_y + settings->axis_offset_y; + settings->current_z = settings->current_z + settings->axis_offset_z; + settings->AA_current = (settings->AA_current + settings->AA_axis_offset); + settings->BB_current = (settings->BB_current + settings->BB_axis_offset); + settings->CC_current = (settings->CC_current + settings->CC_axis_offset); + settings->u_current = (settings->u_current + settings->u_axis_offset); + settings->v_current = (settings->v_current + settings->v_axis_offset); + settings->w_current = (settings->w_current + settings->w_axis_offset); + + SET_G92_OFFSET(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + settings->axis_offset_x = 0.0; + settings->axis_offset_y = 0.0; + settings->axis_offset_z = 0.0; + settings->AA_axis_offset = 0.0; + settings->BB_axis_offset = 0.0; + settings->CC_axis_offset = 0.0; + settings->u_axis_offset = 0.0; + settings->v_axis_offset = 0.0; + settings->w_axis_offset = 0.0; + if (g_code == G_92_1) { + pars[G92_X] = 0.0; + pars[G92_Y] = 0.0; + pars[G92_Z] = 0.0; + pars[G92_A] = 0.0; + pars[G92_B] = 0.0; + pars[G92_C] = 0.0; + pars[G92_U] = 0.0; + pars[G92_V] = 0.0; + pars[G92_W] = 0.0; + } + } else if (g_code == G_92_3) { + pars[5210] = 1.0; + settings->current_x = + settings->current_x + settings->axis_offset_x - USER_TO_PROGRAM_LEN(pars[5211]); + settings->current_y = + settings->current_y + settings->axis_offset_y - USER_TO_PROGRAM_LEN(pars[5212]); + settings->current_z = + settings->current_z + settings->axis_offset_z - USER_TO_PROGRAM_LEN(pars[5213]); + settings->AA_current = + settings->AA_current + settings->AA_axis_offset - USER_TO_PROGRAM_ANG(pars[5214]); + settings->BB_current = + settings->BB_current + settings->BB_axis_offset - USER_TO_PROGRAM_ANG(pars[5215]); + settings->CC_current = + settings->CC_current + settings->CC_axis_offset - USER_TO_PROGRAM_ANG(pars[5216]); + settings->u_current = + settings->u_current + settings->u_axis_offset - USER_TO_PROGRAM_LEN(pars[5217]); + settings->v_current = + settings->v_current + settings->v_axis_offset - USER_TO_PROGRAM_LEN(pars[5218]); + settings->w_current = + settings->w_current + settings->w_axis_offset - USER_TO_PROGRAM_LEN(pars[5219]); + + settings->axis_offset_x = USER_TO_PROGRAM_LEN(pars[5211]); + settings->axis_offset_y = USER_TO_PROGRAM_LEN(pars[5212]); + settings->axis_offset_z = USER_TO_PROGRAM_LEN(pars[5213]); + settings->AA_axis_offset = USER_TO_PROGRAM_ANG(pars[5214]); + settings->BB_axis_offset = USER_TO_PROGRAM_ANG(pars[5215]); + settings->CC_axis_offset = USER_TO_PROGRAM_ANG(pars[5216]); + settings->u_axis_offset = USER_TO_PROGRAM_LEN(pars[5217]); + settings->v_axis_offset = USER_TO_PROGRAM_LEN(pars[5218]); + settings->w_axis_offset = USER_TO_PROGRAM_LEN(pars[5219]); + + SET_G92_OFFSET(settings->axis_offset_x, + settings->axis_offset_y, + settings->axis_offset_z, + settings->AA_axis_offset, + settings->BB_axis_offset, + settings->CC_axis_offset, + settings->u_axis_offset, + settings->v_axis_offset, + settings->w_axis_offset); + } else + ERS(NCE_BUG_CODE_NOT_IN_G52_G92_SERIES); + + return INTERP_OK; +} + +#define VAL_LEN 30 + +int Interp::convert_param_comment(char *comment, char *expanded, int /*len*/) +{ + FORCE_LC_NUMERIC_C; + int i; + char param[LINELEN+1]; + char format[5] = "%lf"; + int paramNumber; + int stat; + double value; + char valbuf[VAL_LEN]; // max double length + room + char *v; + int found; + + while(*comment) + { + + if(*comment == '%') + { + // skip over the '%' + comment++; + + // convenient integer looking + if(*comment == 'd') + { + comment++; + strcpy(format, "%.0f"); + } + // convenient 4 position float + else if(*comment == 'f') + { + comment++; + strcpy(format, "%.4f"); + } + // arbitrary 0-9 position float + else if(*comment == '.') + { + comment++; + if(isdigit(*comment)) + { + // forward to the (hopefully) letter f + comment++; + if(*comment == 'f') + { + // back up to get the digit into format + comment--; + format[0] = '%'; + format[1] = '.'; + format[2] = *comment; + format[3] = 'f'; + format[4] = 0; + comment++; + comment++; + } + else + { + // not a format string so, + // back up to the digit to continue processing + comment--; + *expanded++ = '.'; + + } + } + else + { + *expanded++ = '.'; + } + + } + else + { + *expanded++ = '%'; + } + } + else if(*comment == '#') + { + found = 0; + logDebug("a parameter"); + + // skip over the '#' + comment++; + CHKS((0 == *comment), NCE_NAMED_PARAMETER_NOT_TERMINATED); + + if(isdigit(*comment)) // is this numeric param? + { + logDebug("numeric parameter"); + for(i=0; isdigit(*comment)&& (i= 0) && + (paramNumber < RS274NGC_MAX_PARAMETERS)) + { + value = _setup.parameters[paramNumber]; + found = 1; + } + } + else if(*comment == '<') + { + logDebug("name parameter"); + // this is a name parameter + // skip over the '<' + comment++; + CHKS((0 == *comment), NCE_NAMED_PARAMETER_NOT_TERMINATED); + + for(i=0; (')' != *comment) && + (i' == *comment) + { + break; // done + } + if(isspace(*comment)) // skip space inside the param + { + comment++; + continue; + } + else + { + // if tolower is a macro, may need this int + int c = *comment++; + if (FEATURE(NO_DOWNCASE_OWORD)) + param[i] = c; + else + param[i] = tolower(c); + i++; + } + } + if('>' != *comment) + { + ERS(NCE_NAMED_PARAMETER_NOT_TERMINATED); + } + else + { + comment++; + } + + // terminate the name + param[i] = 0; + + // now lookup the name + find_named_param(param, &stat, &value); + if(stat) + { + found = 1; + } + } + else + { + // neither numeric or name + logDebug("neither numeric nor name"); + // just store the '#' + *expanded++ = '#'; + + CHKS((*comment == 0), NCE_NAMED_PARAMETER_NOT_TERMINATED); + continue; + } + + // we have a parameter -- now insert it + // we have the value + if(found) + { + // avoid -0.0/0.0 issues + double pvalue = equal(value, 0.0) ? 0.0 : value; + int n = snprintf(valbuf, VAL_LEN, format, pvalue); + bool fail = (n >= VAL_LEN || n < 0); + if(fail) + rtapi_strxcpy(valbuf, "######"); + + } + else + { + rtapi_strxcpy(valbuf, "######"); + } + logDebug("found:%d value:|%s|", found, valbuf); + + v = valbuf; + while(*v) + { + *expanded++ = *v++; + } + } + else // not a '#' + { + *expanded++ = *comment++; + } + } + *expanded = 0; // the final nul + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_comment + +Returned Value: int (INTERP_OK) + +Side effects: + The message function is called if the string starts with "MSG,". + Otherwise, the comment function is called. + +Called by: execute_block + +To be a message, the first four characters of the comment after the +opening left parenthesis must be "MSG,", ignoring the case of the +letters and allowing spaces or tabs anywhere before the comma (to make +the treatment of case and white space consistent with how it is +handled elsewhere). + +Messages are not provided for in [NCMS]. They are implemented here as a +subtype of comment. This is an extension to the rs274NGC language. + +*/ + +static int streq(char *s1, char *s2) { + return !strcmp(s1, s2); +} + +static int startswith(char *haystack, char *needle) { + return !strncmp(haystack, needle, strlen(needle)); +} + +int Interp::convert_comment(char *comment, bool enqueue) //!< string with comment +{ + enum + { LC_SIZE = 256, EX_SIZE = 2*LC_SIZE}; // 256 from comment[256] in rs274ngc.hh + char lc[LC_SIZE+1]; + char expanded[EX_SIZE+1]; + char MSG_STR[] = "msg,"; + + //!!!KL add two -- debug => same as msg + //!!!KL -- print => goes to stdout + char DEBUG_STR[] = "debug,"; + char PRINT_STR[] = "print,"; + char LOG_STR[] = "log,"; + char LOGOPEN_STR[] = "logopen,"; + char LOGAPPEND_STR[] = "logappend,"; + char LOGCLOSE_STR[] = "logclose"; + char PY_STR[] = "py,"; + char PYRUN_STR[] = "pyrun,"; + char PYRELOAD_STR[] = "pyreload"; + char ABORT_STR[] = "abort,"; + int m, n, start; + + // step over leading white space in comment + m = 0; + while (isspace(comment[m])) + m++; + start = m; + // copy lowercase comment to lc[] + for (n = 0; n < LC_SIZE && comment[m] != 0; m++, n++) { + lc[n] = tolower(comment[m]); + } + lc[n] = 0; // null terminate + + // compare with MSG, SYSTEM, DEBUG, PRINT + if (startswith(lc, MSG_STR)) { + MESSAGE(comment + start + strlen(MSG_STR)); + return INTERP_OK; + } + else if (startswith(lc, DEBUG_STR)) + { + convert_param_comment(comment+start+strlen(DEBUG_STR), expanded, + EX_SIZE); + if (_setup.parameters[5599] > 0.0) + MESSAGE(expanded); + return INTERP_OK; + } + else if (startswith(lc, PRINT_STR)) + { + FILE *fd = get_stdout(); + if (fd) { + convert_param_comment(comment+start+strlen(PRINT_STR), expanded, + EX_SIZE); + fprintf(fd, "%s\n", expanded); + fflush(fd); + } + return INTERP_OK; + } + else if (startswith(lc, LOG_STR)) + { + convert_param_comment(comment+start+strlen(LOG_STR), expanded, + EX_SIZE); + LOG(expanded); + return INTERP_OK; + } + else if (startswith(lc, LOGOPEN_STR)) + { + LOGOPEN(comment + start + strlen(LOGOPEN_STR)); + return INTERP_OK; + } + else if (startswith(lc, LOGAPPEND_STR)) + { + LOGAPPEND(comment + start + strlen(LOGAPPEND_STR)); + return INTERP_OK; + } + else if (startswith(lc, PY_STR)) + { + return py_execute(comment + start + strlen(PY_STR), false); + } + else if (startswith(lc, PYRUN_STR)) + { + return py_execute(comment + start + strlen(PYRUN_STR), true); + } + else if (startswith(lc, PYRELOAD_STR)) + { + return py_reload(); + } + else if (startswith(lc, ABORT_STR)) + { + convert_param_comment(comment+start+strlen(ABORT_STR), expanded, + EX_SIZE); + setSavedError(expanded); // avoid printf interpretation + return INTERP_ERROR; + } + else if (streq(lc, LOGCLOSE_STR)) + { + LOGCLOSE(); + return INTERP_OK; + } + // else it's a real comment + if (enqueue) + enqueue_COMMENT(comment + start); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_control_mode + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_61, G_61_1, G_64 : NCE_BUG_CODE_NOT_G61_G61_1_OR_G64 + +Side effects: See below + +Called by: convert_g. + +The interpreter switches the machine settings to indicate the +control mode (CANON_EXACT_STOP, CANON_EXACT_PATH or CANON_CONTINUOUS) + +A call is made to SET_MOTION_CONTROL_MODE(CANON_XXX), where CANON_XXX is +CANON_EXACT_PATH if g_code is G_61, CANON_EXACT_STOP if g_code is G_61_1, +and CANON_CONTINUOUS if g_code is G_64. + +Setting the control mode to CANON_EXACT_STOP on G_61 would correspond +more closely to the meaning of G_61 as given in [NCMS, page 40], but +CANON_EXACT_PATH has the advantage that the tool does not stop if it +does not have to, and no evident disadvantage compared to +CANON_EXACT_STOP, so it is being used for G_61. G_61_1 is not defined +in [NCMS], so it is available and is used here for setting the control +mode to CANON_EXACT_STOP. + +It is OK to call SET_MOTION_CONTROL_MODE(CANON_XXX) when CANON_XXX is +already in force. + +*/ + +int Interp::convert_control_mode( + int g_code, // g_code being executed (G_61, G61_1, G_64) + double tolerance_in, // tolerance for the path following in G64 + double naivecam_tolerance_in, // tolerance for the naivecam + setup_pointer settings) // pointer to machine settings +{ + double tolerance, naivecam_tolerance; + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot change control mode with cutter radius compensation on"))); + if (g_code == G_61) { + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + settings->control_mode = CANON_EXACT_PATH; + } else if (g_code == G_61_1) { + SET_MOTION_CONTROL_MODE(CANON_EXACT_STOP, 0); + settings->control_mode = CANON_EXACT_STOP; + } else if (g_code == G_64) { + if (tolerance_in >= 0){ + tolerance = tolerance_in; + } + else{ + tolerance = _setup.tolerance_default; + } + settings->control_mode = CANON_CONTINUOUS; + settings->tolerance = tolerance; + SET_MOTION_CONTROL_MODE(CANON_CONTINUOUS, tolerance); + + if (naivecam_tolerance_in >= 0){ + naivecam_tolerance = naivecam_tolerance_in; + } + else if (tolerance_in >= 0){ + // if no naivecam_tolerance specified use same for both + naivecam_tolerance = tolerance_in; + } + else{ + naivecam_tolerance = _setup.naivecam_tolerance_default; + } + settings->naivecam_tolerance = naivecam_tolerance; + SET_NAIVECAM_TOLERANCE(naivecam_tolerance); + + } else + ERS(NCE_BUG_CODE_NOT_G61_G61_1_OR_G64); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_coordinate_system + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The value of the g_code argument is not 540, 550, 560, 570, 580, 590 + 591, 592, or 593: + NCE_BUG_CODE_NOT_IN_RANGE_G54_TO_G593 + +Side effects: + If the coordinate system selected by the g_code is not already in + use, the canonical program coordinate system axis offset values are + reset and the coordinate values of the current point are reset. + +Called by: convert_g. + +COORDINATE SYSTEMS (involves g10, g53, g54 - g59.3, g92) + +The canonical machining functions view of coordinate systems is: +1. There are two coordinate systems: absolute and program. +2. All coordinate values are given in terms of the program coordinate system. +3. The offsets of the program coordinate system may be reset. + +The RS274/NGC view of coordinate systems, as given in section 3.2 +of [NCMS] is: +1. there are ten coordinate systems: absolute and 9 program. The + program coordinate systems are numbered 1 to 9. +2. you can switch among the 9 but not to the absolute one. G54 + selects coordinate system 1, G55 selects 2, and so on through + G56, G57, G58, G59, G59.1, G59.2, and G59.3. +3. you can set the offsets of the 9 program coordinate systems + using G10 L2 Pn (n is the number of the coordinate system) with + values for the axes in terms of the absolute coordinate system. +4. the first one of the 9 program coordinate systems is the default. +5. data for coordinate systems is stored in parameters [NCMS, pages 59 - 60]. +6. g53 means to interpret coordinate values in terms of the absolute + coordinate system for the one block in which g53 appears. +7. You can offset the current coordinate system using g92. This offset + will then apply to all nine program coordinate systems. + +The approach used in the interpreter mates the canonical and NGC views +of coordinate systems as follows: + +During initialization, data from the parameters for the first NGC +coordinate system is used in a SET_ORIGIN_OFFSETS function call and +origin_index in the machine model is set to 1. + +If a g_code in the range g54 - g59.3 is encountered in an NC program, +the data from the appropriate NGC coordinate system is copied into the +origin offsets used by the interpreter, a SET_ORIGIN_OFFSETS function +call is made, and the current position is reset. + +If a g10 is encountered, the convert_setup function is called to reset +the offsets of the program coordinate system indicated by the P number +given in the same block. + +If a g53 is encountered, the axis values given in that block are used +to calculate what the coordinates are of that point in the current +coordinate system, and a STRAIGHT_TRAVERSE or STRAIGHT_FEED function +call to that point using the calculated values is made. No offset +values are changed. + +If a g92 is encountered, that is handled by the convert_axis_offsets +function. A g92 results in an axis offset for each axis being calculated +and stored in the machine model. The axis offsets are applied to all +nine coordinate systems. Axis offsets are initialized to zero. + +*/ + +void Interp::rotate(double *x, double *y, double theta) { + double xx, yy; + double t = D2R(theta); + xx = *x * cos(t) - *y * sin(t); + yy = *x * sin(t) + *y * cos(t); + *x = xx; + *y = yy; +} + +int Interp::convert_coordinate_system(int g_code, //!< g_code called (must be one listed above) + setup_pointer settings) //!< pointer to machine settings +{ + int origin; + double *parameters; + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot change coordinate systems with cutter radius compensation on"))); + parameters = settings->parameters; + switch (g_code) { + case G_54: + origin = 1; + break; + case G_55: + origin = 2; + break; + case G_56: + origin = 3; + break; + case G_57: + origin = 4; + break; + case G_58: + origin = 5; + break; + case G_59: + origin = 6; + break; + case G_59_1: + origin = 7; + break; + case G_59_2: + origin = 8; + break; + case G_59_3: + origin = 9; + break; + default: + ERS(NCE_BUG_CODE_NOT_IN_RANGE_G54_TO_G593); + } + + if (origin == settings->origin_index) { /* already using this origin */ +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: continuing to use same coordinate system"); +#endif + return INTERP_OK; + } + + // move the current point into the new system + find_current_in_system(settings, origin, + &settings->current_x, &settings->current_y, &settings->current_z, + &settings->AA_current, &settings->BB_current, &settings->CC_current, + &settings->u_current, &settings->v_current, &settings->w_current); + + // remember that this is new system + settings->origin_index = origin; + parameters[5220] = (double) origin; + + // load the origin of the newly-selected system + settings->origin_offset_x = USER_TO_PROGRAM_LEN(parameters[5201 + (origin * 20)]); + settings->origin_offset_y = USER_TO_PROGRAM_LEN(parameters[5202 + (origin * 20)]); + settings->origin_offset_z = USER_TO_PROGRAM_LEN(parameters[5203 + (origin * 20)]); + settings->AA_origin_offset = USER_TO_PROGRAM_ANG(parameters[5204 + (origin * 20)]); + settings->BB_origin_offset = USER_TO_PROGRAM_ANG(parameters[5205 + (origin * 20)]); + settings->CC_origin_offset = USER_TO_PROGRAM_ANG(parameters[5206 + (origin * 20)]); + settings->u_origin_offset = USER_TO_PROGRAM_LEN(parameters[5207 + (origin * 20)]); + settings->v_origin_offset = USER_TO_PROGRAM_LEN(parameters[5208 + (origin * 20)]); + settings->w_origin_offset = USER_TO_PROGRAM_LEN(parameters[5209 + (origin * 20)]); + settings->rotation_xy = parameters[5210 + (origin * 20)]; + + SET_G5X_OFFSET(origin, + settings->origin_offset_x, + settings->origin_offset_y, + settings->origin_offset_z, + settings->AA_origin_offset, + settings->BB_origin_offset, + settings->CC_origin_offset, + settings->u_origin_offset, + settings->v_origin_offset, + settings->w_origin_offset); + + SET_G92_OFFSET(settings->axis_offset_x, + settings->axis_offset_y, + settings->axis_offset_z, + settings->AA_axis_offset, + settings->BB_axis_offset, + settings->CC_axis_offset, + settings->u_axis_offset, + settings->v_axis_offset, + settings->w_axis_offset); + + SET_XY_ROTATION(settings->rotation_xy); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cutter_compensation + +Returned Value: int + If convert_cutter_compensation_on or convert_cutter_compensation_off + is called and returns an error code, this returns that code. + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. g_code is not G_40, G_41, or G_42: + NCE_BUG_CODE_NOT_G40_G41_OR_G42 + +Side effects: + The value of cutter_comp_side in the machine model mode is + set to RIGHT, LEFT, or false. The currently active tool table index in + the machine model (which is the index of the slot whose diameter + value is used in cutter radius compensation) is updated. + +Since cutter radius compensation is performed in the interpreter, no +call is made to any canonical function regarding cutter radius compensation. + +Called by: convert_g + +*/ + +int Interp::convert_cutter_compensation(int g_code, //!< must be G_40, G_41, or G_42 + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + + if (g_code == G_40) { + CHP(convert_cutter_compensation_off(settings)); + } else if (g_code == G_41) { + CHP(convert_cutter_compensation_on(CUTTER_COMP::LEFT, block, settings)); + } else if (g_code == G_42) { + CHP(convert_cutter_compensation_on(CUTTER_COMP::RIGHT, block, settings)); + } else if (g_code == G_41_1) { + CHP(convert_cutter_compensation_on(CUTTER_COMP::LEFT, block, settings)); + } else if (g_code == G_42_1) { + CHP(convert_cutter_compensation_on(CUTTER_COMP::RIGHT, block, settings)); + } else + ERS("BUG: Code not G40, G41, G41.1, G42, G42.1"); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cutter_compensation_off + +Returned Value: int (INTERP_OK) + +Side effects: + A comment is made that cutter radius compensation is turned off. + The machine model of the cutter radius compensation mode is set to false. + The value of cutter_comp_firstmove in the machine model is set to true. + This serves as a flag when cutter radius compensation is + turned on again. + +Called by: convert_cutter_compensation + +*/ + +int Interp::convert_cutter_compensation_off(setup_pointer settings) //!< pointer to machine settings +{ +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: cutter radius compensation off"); +#endif + if(settings->cutter_comp_side != CUTTER_COMP::OFF && settings->cutter_comp_radius > 0.0 && + !settings->cutter_comp_firstmove) { + double cx, cy, cz; + comp_get_current(settings, &cx, &cy, &cz); + CHP(move_endpoint_and_flush(settings, cx, cy)); + dequeue_canons(settings); + settings->current_x = settings->program_x; + settings->current_y = settings->program_y; + settings->current_z = settings->program_z; + settings->arc_not_allowed = true; + } + settings->cutter_comp_side = CUTTER_COMP::OFF; + settings->cutter_comp_firstmove = true; + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cutter_compensation_on + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The selected plane is not the XY plane: + NCE_CANNOT_TURN_CUTTER_RADIUS_COMP_ON_OUT_OF_XY_PLANE + 2. Cutter radius compensation is already on: + NCE_CANNOT_TURN_CUTTER_RADIUS_COMP_ON_WHEN_ON + +Side effects: + A COMMENT function call is made (conditionally) saying that the + interpreter is switching mode so that cutter radius compensation is on. + The value of cutter_comp_radius in the machine model mode is + set to the absolute value of the radius given in the tool table. + The value of cutter_comp_side in the machine model mode is + set to RIGHT or LEFT. The currently active tool table index in + the machine model is updated. + +Called by: convert_cutter_compensation + +check_other_codes checks that a d word occurs only in a block with g41 +or g42. + +Cutter radius compensation is carried out in the interpreter, so no +call is made to a canonical function (although there is a canonical +function, START_CUTTER_RADIUS_COMPENSATION, that could be called if +the primitive level could execute it). + +This version uses a D word if there is one in the block, but it does +not require a D word, since the sample programs which the interpreter +is supposed to handle do not have them. Logically, the D word is +optional, since the D word is always (except in cases we have never +heard of) the slot number of the tool in the spindle. Not requiring a +D word is contrary to [Fanuc, page 116] and [NCMS, page 79], however. +Both manuals require the use of the D-word with G41 and G42. + +This version handles a negative offset radius, which may be +encountered if the programmed tool path is a center line path for +cutting a profile and the path was constructed using a nominal tool +diameter. Then the value in the tool table for the diameter is set to +be the difference between the actual diameter and the nominal +diameter. If the actual diameter is less than the nominal, the value +in the table is negative. The method of handling a negative radius is +to switch the side of the offset and use a positive radius. This +requires that the profile use arcs (not straight lines) to go around +convex corners. + +*/ + +int Interp::convert_cutter_compensation_on(CUTTER_COMP side, //!< side of path cutter is on (LEFT or RIGHT) + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double radius; + int idx, orientation; + + CHKS((settings->plane != CANON_PLANE::XY && settings->plane != CANON_PLANE::XZ), + NCE_RADIUS_COMP_ONLY_IN_XY_OR_XZ); + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + NCE_CANNOT_TURN_CUTTER_RADIUS_COMP_ON_WHEN_ON); + if(block->g_modes[GM_CUTTER_COMP] == G_41_1 || block->g_modes[GM_CUTTER_COMP] == G_42_1) { + CHKS((!block->d_flag), + _("G%d.1 with no D word"), block->g_modes[GM_CUTTER_COMP]/10 ); + radius = block->d_number_float / 2; + if(block->l_number != -1) { + CHKS((settings->plane != CANON_PLANE::XZ), _("G%d.1 with L word, but plane is not G18"), block->g_modes[GM_CUTTER_COMP]/10); + orientation = block->l_number; + } else { + orientation = 0; + } + } else { + if(!block->d_flag) { + idx = 0; + } else { + int tool; + CHKS(!is_near_int(&tool, block->d_number_float), + _("G%d requires D word to be a whole number"), + block->g_modes[GM_CUTTER_COMP]/10); + CHKS((tool < 0), NCE_NEGATIVE_D_WORD_TOOL_RADIUS_INDEX_USED); + CHP((find_tool_index(settings, tool, &idx))); + } + radius = USER_TO_PROGRAM_LEN(settings->tool_table[idx].diameter) / 2.0; + orientation = settings->tool_table[idx].orientation; + CHKS((settings->plane != CANON_PLANE::XZ && orientation != 0 && orientation != 9), _("G%d with lathe tool, but plane is not G18"), block->g_modes[GM_CUTTER_COMP]/10); + } + if (radius < 0.0) { /* switch side & make radius positive if radius negative */ + radius = -radius; + if (side == CUTTER_COMP::RIGHT) + side = CUTTER_COMP::LEFT; + else + side = CUTTER_COMP::RIGHT; + } +#ifdef DEBUG_EMC + if (side == CUTTER_COMP::RIGHT) + enqueue_COMMENT("interpreter: cutter radius compensation on right"); + else + enqueue_COMMENT("interpreter: cutter radius compensation on left"); +#endif + + settings->cutter_comp_radius = radius; + settings->cutter_comp_orientation = orientation; + settings->cutter_comp_side = side; + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_distance_mode + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_90 or G_91: NCE_BUG_CODE_NOT_G90_OR_G91 + +Side effects: + The interpreter switches the machine settings to indicate the current + distance mode (absolute or incremental). + + The canonical machine to which commands are being sent does not have + an incremental mode, so no command setting the distance mode is + generated in this function. A comment function call explaining the + change of mode is made (conditionally), however, if there is a change. + +Called by: convert_g. + +*/ + +// OK to call this in a concave corner with a deferred move, since it +// doesn't issue any CANONs + +int Interp::convert_distance_mode(int g_code, //!< g_code being executed (must be G_90 or G_91) + setup_pointer settings) //!< pointer to machine settings +{ + if (g_code == G_90) { + if (settings->distance_mode != DISTANCE_MODE::ABSOLUTE) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: distance mode changed to absolute"); +#endif + settings->distance_mode = DISTANCE_MODE::ABSOLUTE; + } + } else if (g_code == G_91) { + if (settings->distance_mode != DISTANCE_MODE::INCREMENTAL) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: distance mode changed to incremental"); +#endif + settings->distance_mode = DISTANCE_MODE::INCREMENTAL; + } + } else + ERS(NCE_BUG_CODE_NOT_G90_OR_G91); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_ijk_distance_mode + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_90.1 or G_91.1: NCE_BUG_CODE_NOT_G90_OR_G91 + +Side effects: + The interpreter switches the machine settings to indicate the current + distance mode for arc centers (absolute or incremental). + + The canonical machine to which commands are being sent does not have + an incremental mode, so no command setting the distance mode is + generated in this function. A comment function call explaining the + change of mode is made (conditionally), however, if there is a change. + +Called by: convert_g. + +*/ + +// OK to call this in a concave corner with a deferred move, since it +// doesn't issue any CANONs except comments (and who cares where the comments are) + +int Interp::convert_ijk_distance_mode(int g_code, //!< g_code being executed (must be G_90_1 or G_91_1) + setup_pointer settings) //!< pointer to machine settings +{ + if (g_code == G_90_1) { + if (settings->ijk_distance_mode != DISTANCE_MODE::ABSOLUTE) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: IJK distance mode changed to absolute"); +#endif + settings->ijk_distance_mode = DISTANCE_MODE::ABSOLUTE; + } + } else if (g_code == G_91_1) { + if (settings->ijk_distance_mode != DISTANCE_MODE::INCREMENTAL) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: IJK distance mode changed to incremental"); +#endif + settings->ijk_distance_mode = DISTANCE_MODE::INCREMENTAL; + } + } else + ERS(NCE_BUG_CODE_NOT_G90_OR_G91); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_lathe_diameter_mode + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_07 or G_08: NCE_BUG_CODE_NOT_G07_OR_G08 + +Side effects: + The interpreter switches the machine settings to indicate the current + distance mode for arc centers (absolute or incremental). + + The canonical machine to which commands are being sent does not have + an incremental mode, so no command setting the distance mode is + generated in this function. A comment function call explaining the + change of mode is made (conditionally), however, if there is a change. + +Called by: convert_g. + +*/ + +int Interp::convert_lathe_diameter_mode(int g_code, //!< g_code being executed (must be G_90_1 or G_91_1) + block_pointer block, //!< pointer to current block + setup_pointer settings) //!< pointer to machine settings +{ + if (g_code == G_7) { + if (!settings->lathe_diameter_mode) { + if(block->x_flag) + { + block->x_number /= 2; //Apply scaling now + } + if(block->motion_to_be == G_76) { + block->i_number /= 2; + block->j_number /= 2; + block->k_number /= 2; + } +#ifdef DEBUG_EMC + COMMENT("interpreter: Lathe diameter mode changed to diameter"); +#endif + settings->lathe_diameter_mode = true; + } + } else if (g_code == G_8) { + if (settings->lathe_diameter_mode) { + if(block->x_flag) + { + block->x_number *= 2; //Remove any existing scaling + } + if(block->motion_to_be == G_76) { + block->i_number *= 2; + block->j_number *= 2; + block->k_number *= 2; + } +#ifdef DEBUG_EMC + COMMENT("interpreter: Lathe diameter mode changed to radius"); +#endif + settings->lathe_diameter_mode = false; + } + } else + ERS("BUG: Code not G7 or G8"); + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! convert_dwell + +Returned Value: int (INTERP_OK) + +Side effects: + A dwell command is executed. + +Called by: convert_g. + +*/ + +int Interp::convert_dwell(setup_pointer /*settings*/, double time) //!< time in seconds to dwell */ +{ + enqueue_DWELL(time); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_feed_mode + +Returned Value: int + If any of the following errors occur, this returns an error code. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_93, G_94 or G_95 + +Side effects: + The interpreter switches the machine settings to indicate the current + feed mode (UNITS_PER_MINUTE or INVERSE_TIME). + + The canonical machine to which commands are being sent does not have + a feed mode, so no command setting the distance mode is generated in + this function. A comment function call is made (conditionally) + explaining the change in mode, however. + +Called by: execute_block. + +*/ + +int Interp::convert_feed_mode(int g_code, //!< g_code being executed (must be G_93, G_94 or G_95) + setup_pointer settings) //!< pointer to machine settings +{ + if (g_code == G_93) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: feed mode set to inverse time"); +#endif + settings->feed_mode = FEED_MODE::INVERSE_TIME; + enqueue_SET_FEED_MODE(0, 0); + } else if (g_code == G_94) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: feed mode set to units per minute"); +#endif + settings->feed_mode = FEED_MODE::UNITS_PER_MINUTE; + enqueue_SET_FEED_MODE(0, 0); + settings->feed_rate = 0.0; + enqueue_SET_FEED_RATE(0); + } else if(g_code == G_95) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: feed mode set to units per revolution"); +#endif + settings->feed_mode = FEED_MODE::UNITS_PER_REVOLUTION; + enqueue_SET_FEED_MODE(settings->active_spindle , 1); + settings->feed_rate = 0.0; + enqueue_SET_FEED_RATE(0); + } else + ERS("BUG: Code not G93, G94, or G95"); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_feed_rate + +Returned Value: int (INTERP_OK) + +Side effects: + The machine feed_rate is set to the value of f_number in the + block by function call. + The machine model feed_rate is set to that value. + +Called by: execute_block + +This is called only if the feed mode is UNITS_PER_MINUTE or UNITS_PER_REVOLUTION. + +*/ + +int Interp::convert_feed_rate(block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + settings->feed_rate = block->f_number; + enqueue_SET_FEED_RATE(block->f_number); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_g + +Returned Value: int + If one of the following functions is called and returns an error code, + this returns that code. + convert_control_mode + convert_coordinate_system + convert_cutter_compensation + convert_distance_mode + convert_ijk_distance_mode + convert_lathe_diameter_mode + convert_dwell + convert_length_units + convert_modal_0 + convert_motion + convert_retract_mode + convert_set_plane + convert_tool_length_offset + Otherwise, it returns INTERP_OK. + +Side effects: + Any g_codes in the block (excluding g93 and 94) and any implicit + motion g_code are executed. + +Called by: execute_block. + +This takes a pointer to a block of RS274/NGC instructions (already +read in) and creates the appropriate output commands corresponding to +any "g" codes in the block. + +Codes g93 and g94, which set the feed mode, are executed earlier by +execute_block before reading the feed rate. + +G-codes are are executed in the following order. +1. mode 0, G4 only - dwell. Left here from earlier versions. +2. mode 2, one of (G17, G18, G19) - plane selection. +3. mode 6, one of (G20, G21) - length units. +4. mode 15 one of (G07,G08) - lathe diameter mode +5. mode 7, one of (G40, G41, G42) - cutter radius compensation. +6. mode 8, one of (G43, G49) - tool length offset +7. mode 12, one of (G54, G55, G56, G57, G58, G59, G59.1, G59.2, G59.3) + - coordinate system selection. +8. mode 13, one of (G61, G61.1, G64, G50, G51) - control mode +9. mode 3, one of (G90, G91) - distance mode. +10. mode 4, one of (G90.1, G91.1) - arc i,j,k mode. +11. mode 10, one of (G98, G99) - retract mode. +12. mode 0, one of (G10, G28, G30, G92, G92.1, G92.2, G92.3) - +13. mode 1, one of (G0, G1, G2, G3, G38.2, G80, G81 to G89, G33, G33.1, G76) - motion or cancel. + G53 from mode 0 is also handled here, if present. + +Some mode 0 and most mode 1 G-codes must be executed after the length units +are set, since they use coordinate values. Mode 1 codes also must wait +until most of the other modes are set. + +*/ + +int Interp::convert_g(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int status; + + if ((block->g_modes[GM_MODAL_0] == G_4) && ONCE(STEP_DWELL)) { + status = convert_dwell(settings, block->p_number); + CHP(status); + } + if ((block->g_modes[GM_SET_PLANE] != -1) && ONCE(STEP_SET_PLANE)) { + status = convert_set_plane(block->g_modes[GM_SET_PLANE], settings); + CHP(status); + } + if ((block->g_modes[GM_LENGTH_UNITS] != -1) && ONCE(STEP_LENGTH_UNITS)) { + status = convert_length_units(block->g_modes[GM_LENGTH_UNITS], settings); + CHP(status); + } + if ((block->g_modes[GM_LATHE_DIAMETER_MODE] != -1) && ONCE(STEP_LATHE_DIAMETER_MODE)) { + status = convert_lathe_diameter_mode(block->g_modes[GM_LATHE_DIAMETER_MODE], block, settings); + CHP(status); + } + if ((block->g_modes[GM_CUTTER_COMP] != -1) && ONCE(STEP_CUTTER_COMP)) { + status = convert_cutter_compensation(block->g_modes[GM_CUTTER_COMP], block, settings); + CHP(status); + } + if ((block->g_modes[GM_TOOL_LENGTH_OFFSET] != -1) && ONCE(STEP_TOOL_LENGTH_OFFSET)){ + status = convert_tool_length_offset(block->g_modes[GM_TOOL_LENGTH_OFFSET], block, settings); + CHP(status); + } + if ((block->g_modes[GM_COORD_SYSTEM] != -1) && ONCE(STEP_COORD_SYSTEM)){ + status = convert_coordinate_system(block->g_modes[GM_COORD_SYSTEM], settings); + CHP(status); + } + if ((block->g_modes[GM_CONTROL_MODE] != -1) && ONCE(STEP_CONTROL_MODE)) { + status = convert_control_mode(block->g_modes[GM_CONTROL_MODE], + block->p_number, block->q_number, settings); + CHP(status); + } + if ((block->g_modes[GM_DISTANCE_MODE] != -1) && ONCE(STEP_DISTANCE_MODE)) { + status = convert_distance_mode(block->g_modes[GM_DISTANCE_MODE], settings); + CHP(status); + } + if ((block->g_modes[GM_IJK_DISTANCE_MODE] != -1) && ONCE(STEP_IJK_DISTANCE_MODE)){ + status = convert_ijk_distance_mode(block->g_modes[GM_IJK_DISTANCE_MODE], settings); + CHP(status); + } + if ((block->g_modes[GM_RETRACT_MODE] != -1) && ONCE(STEP_RETRACT_MODE)){ + status = convert_retract_mode(block->g_modes[GM_RETRACT_MODE], settings); + CHP(status); + } + if ((block->g_modes[GM_MODAL_0] != -1) && ONCE(STEP_MODAL_0)) { + status = convert_modal_0(block->g_modes[GM_MODAL_0], block, settings); + CHP(status); + } + if ((block->g_modes[GM_G92_IS_APPLIED] != -1) && ONCE(STEP_G92_IS_APPLIED)) { + status = convert_g92_is_applied(block->g_modes[GM_G92_IS_APPLIED], block, settings); + } + if ((block->motion_to_be != -1) && ONCE(STEP_MOTION)){ + status = convert_motion(block->motion_to_be, block, settings); + CHP(status); + } + return INTERP_OK; +} + +/*! convert_savehome + +Returned Value: int + Returns an error if cutter_comp_side is set or the routine is called + from a gcode other than G_28_1 or G_30_1 + +Side effects: + None + +Called by: convert_modal_0 + +Saves the absolute coordinates of the current point in parameters 5161-5169 + for G28.1, or 5181-5189 for G30.1 + +*/ + +int Interp::convert_savehome(int code, block_pointer /*block*/, setup_pointer s) { + double *p = s->parameters; + + if(s->cutter_comp_side != CUTTER_COMP::OFF) { + ERS(_("Cannot set reference point with cutter compensation in effect")); + } + + double x = s->current_x + s->axis_offset_x; + double y = s->current_y + s->axis_offset_y; + rotate(&x, &y, s->rotation_xy); + x = PROGRAM_TO_USER_LEN(x + s->tool_offset.tran.x + s->origin_offset_x); + y = PROGRAM_TO_USER_LEN(y + s->tool_offset.tran.y + s->origin_offset_y); + double z = PROGRAM_TO_USER_LEN(s->current_z + s->tool_offset.tran.z + s->origin_offset_z + s->axis_offset_z); + double a = PROGRAM_TO_USER_ANG(s->AA_current + s->tool_offset.a + s->AA_origin_offset + s->AA_axis_offset); + double b = PROGRAM_TO_USER_ANG(s->BB_current + s->tool_offset.b + s->BB_origin_offset + s->BB_axis_offset); + double c = PROGRAM_TO_USER_ANG(s->CC_current + s->tool_offset.c + s->CC_origin_offset + s->CC_axis_offset); + double u = PROGRAM_TO_USER_LEN(s->u_current + s->tool_offset.u + s->u_origin_offset + s->u_axis_offset); + double v = PROGRAM_TO_USER_LEN(s->v_current + s->tool_offset.v + s->v_origin_offset + s->v_axis_offset); + double w = PROGRAM_TO_USER_LEN(s->w_current + s->tool_offset.w + s->w_origin_offset + s->w_axis_offset); + + if(s->a_axis_wrapped) { + a = fmod(a, 360.0); + if(a<0) a += 360.0; + } + + if(s->b_axis_wrapped) { + b = fmod(b, 360.0); + if(b<0) b += 360.0; + } + + if(s->c_axis_wrapped) { + c = fmod(c, 360.0); + if(c<0) c += 360.0; + } + + if(code == G_28_1) { + p[5161] = x; + p[5162] = y; + p[5163] = z; + p[5164] = a; + p[5165] = b; + p[5166] = c; + p[5167] = u; + p[5168] = v; + p[5169] = w; + } else if(code == G_30_1) { + p[5181] = x; + p[5182] = y; + p[5183] = z; + p[5184] = a; + p[5185] = b; + p[5186] = c; + p[5187] = u; + p[5188] = v; + p[5189] = w; + } else { + ERS("BUG: Code not G28.1 or G38.1"); + } + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! convert_home + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. cutter radius compensation is on: + NCE_CANNOT_USE_G28_OR_G30_WITH_CUTTER_RADIUS_COMP + 2. The code is not G28 or G30: NCE_BUG_CODE_NOT_G28_OR_G30 + +Side effects: + This executes a straight traverse to the programmed point, using + the current coordinate system, tool length offset, and motion mode + to interpret the coordinate values. Then it executes a straight + traverse to move one or more axes to the location of reference + point 1 (if G28) or reference point 2 (if G30). If any axis words + are specified in this block, only those axes are moved to the + reference point. If none are specified, all axes are moved. It + also updates the setting of the position of the tool point to the + end point of the move. + + If either move would affect the position of one or more locking + rotaries, the rotaries are unlocked and indexed one at a time, + in the order A,B,C and then the other axes are moved. + + N.B. Many gcode programmers call the reference point a home + position, and that is exactly what it is if the parameters are + zero. Do not confuse this with homing the axis (searching for + a switch or index pulse). + +Called by: convert_modal_0. + +*/ + +int Interp::convert_home(int move, //!< G-code, must be G_28 or G_30 + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double end_x; + double end_y; + double end_z; + double AA_end; + double BB_end; + double CC_end; + double u_end; + double v_end; + double w_end; + double end_x_home; + double end_y_home; + double end_z_home; + double AA_end_home; + double BB_end_home; + double CC_end_home; + double u_end_home; + double v_end_home; + double w_end_home; + double *parameters; + + parameters = settings->parameters; + CHP(find_ends(block, settings, &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, + &u_end, &v_end, &w_end)); + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + NCE_CANNOT_USE_G28_OR_G30_WITH_CUTTER_RADIUS_COMP); + + // waypoint is in currently active coordinate system + + // move indexers first, one at a time + // JOINTS_AXES settings->*_indexer_jnum == -1 means notused + if (AA_end != settings->AA_current && (-1 != settings->a_indexer_jnum) ) + issue_straight_index(3,settings->a_indexer_jnum, AA_end, block->line_number, settings); + if (BB_end != settings->BB_current && (-1 != settings->b_indexer_jnum) ) + issue_straight_index(4,settings->b_indexer_jnum, BB_end, block->line_number, settings); + if (CC_end != settings->CC_current && (-1 != settings->c_indexer_jnum) ) + issue_straight_index(5,settings->c_indexer_jnum, CC_end, block->line_number, settings); + + // Create a state tag and dump it to canon + write_canon_state_tag(block, settings); + + STRAIGHT_TRAVERSE(block->line_number, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + + if (move == G_28) { + find_relative(USER_TO_PROGRAM_LEN(parameters[5161]), + USER_TO_PROGRAM_LEN(parameters[5162]), + USER_TO_PROGRAM_LEN(parameters[5163]), + USER_TO_PROGRAM_ANG(parameters[5164]), + USER_TO_PROGRAM_ANG(parameters[5165]), + USER_TO_PROGRAM_ANG(parameters[5166]), + USER_TO_PROGRAM_LEN(parameters[5167]), + USER_TO_PROGRAM_LEN(parameters[5168]), + USER_TO_PROGRAM_LEN(parameters[5169]), + &end_x_home, &end_y_home, &end_z_home, + &AA_end_home, &BB_end_home, &CC_end_home, + &u_end_home, &v_end_home, &w_end_home, settings); + } else if (move == G_30) { + find_relative(USER_TO_PROGRAM_LEN(parameters[5181]), + USER_TO_PROGRAM_LEN(parameters[5182]), + USER_TO_PROGRAM_LEN(parameters[5183]), + USER_TO_PROGRAM_ANG(parameters[5184]), + USER_TO_PROGRAM_ANG(parameters[5185]), + USER_TO_PROGRAM_ANG(parameters[5186]), + USER_TO_PROGRAM_LEN(parameters[5187]), + USER_TO_PROGRAM_LEN(parameters[5188]), + USER_TO_PROGRAM_LEN(parameters[5189]), + &end_x_home, &end_y_home, &end_z_home, + &AA_end_home, &BB_end_home, &CC_end_home, + &u_end_home, &v_end_home, &w_end_home, settings); + } else + ERS(NCE_BUG_CODE_NOT_G28_OR_G30); + + // if any axes are specified, home only those axes after the waypoint + // (both fanuc & haas, contrary to emc historical operation) + + if (block->x_flag) end_x = end_x_home; + if (block->y_flag) end_y = end_y_home; + if (block->z_flag) end_z = end_z_home; + if (block->a_flag) AA_end = AA_end_home; + if (block->b_flag) BB_end = BB_end_home; + if (block->c_flag) CC_end = CC_end_home; + if (block->u_flag) u_end = u_end_home; + if (block->v_flag) v_end = v_end_home; + if (block->w_flag) w_end = w_end_home; + + // but, if no axes are specified, home all of them + // (haas does this, emc historical did, throws an error in fanuc) + + if (!block->x_flag && !block->y_flag && !block->z_flag && + !block->a_flag && !block->b_flag && !block->c_flag && + !block->u_flag && !block->v_flag && !block->w_flag) { + end_x = end_x_home; + end_y = end_y_home; + end_z = end_z_home; + AA_end = AA_end_home; + BB_end = BB_end_home; + CC_end = CC_end_home; + u_end = u_end_home; + v_end = v_end_home; + w_end = w_end_home; + } + + // move indexers first, one at a time + // JOINTS_AXES settings->*_indexer_jnum == -1 means notused + if (AA_end != settings->AA_current && (-1 != settings->a_indexer_jnum) ) + issue_straight_index(3,settings->a_indexer_jnum, AA_end, block->line_number, settings); + if (BB_end != settings->BB_current && (-1 != settings->b_indexer_jnum) ) + issue_straight_index(4,settings->b_indexer_jnum, BB_end, block->line_number, settings); + if (CC_end != settings->CC_current && (-1 != settings->c_indexer_jnum) ) + issue_straight_index(5,settings->c_indexer_jnum, CC_end, block->line_number, settings); + + STRAIGHT_TRAVERSE(block->line_number, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_length_units + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. The g_code argument isn't G_20 or G_21: + NCE_BUG_CODE_NOT_G20_OR_G21 + 2. Cutter radius compensation is on: + NCE_CANNOT_CHANGE_UNITS_WITH_CUTTER_RADIUS_COMP + +Side effects: + A command setting the length units is executed. The machine + settings are reset regarding length units and current position. + +Called by: convert_g. + +Tool length offset and diameter, work coordinate systems, feed rate, +g28/g30 home positions, and g53 motion in absolute coordinates all work +properly after switching units. Historically these had problems but the +intention is that they work properly now. + +The tool table in settings is not converted here; it is always in +inifile units and the conversion happens when reading an entry. Tool +offsets and feed rate that are in effect are converted by rereading them +from the canon level. + +Cutter diameter is not converted because radius comp is not in effect +when we are changing units. + +XXX Other distance items in the settings (such as the various parameters +for cycles) need testing. + +*/ + +int Interp::convert_length_units(int g_code, //!< g_code being executed (must be G_20 or G_21) + setup_pointer settings) //!< pointer to machine settings +{ + if (g_code == G_20) { + USE_LENGTH_UNITS(CANON_UNITS_INCHES); + if (settings->length_units != CANON_UNITS_INCHES) { + settings->length_units = CANON_UNITS_INCHES; + settings->current_x = (settings->current_x * INCH_PER_MM); + settings->current_y = (settings->current_y * INCH_PER_MM); + settings->current_z = (settings->current_z * INCH_PER_MM); + settings->program_x = (settings->program_x * INCH_PER_MM); + settings->program_y = (settings->program_y * INCH_PER_MM); + settings->program_z = (settings->program_z * INCH_PER_MM); + qc_scale(INCH_PER_MM); + settings->cutter_comp_radius *= INCH_PER_MM; + settings->axis_offset_x = (settings->axis_offset_x * INCH_PER_MM); + settings->axis_offset_y = (settings->axis_offset_y * INCH_PER_MM); + settings->axis_offset_z = (settings->axis_offset_z * INCH_PER_MM); + settings->origin_offset_x = (settings->origin_offset_x * INCH_PER_MM); + settings->origin_offset_y = (settings->origin_offset_y * INCH_PER_MM); + settings->origin_offset_z = (settings->origin_offset_z * INCH_PER_MM); + + settings->u_current = (settings->u_current * INCH_PER_MM); + settings->v_current = (settings->v_current * INCH_PER_MM); + settings->w_current = (settings->w_current * INCH_PER_MM); + settings->u_axis_offset = (settings->u_axis_offset * INCH_PER_MM); + settings->v_axis_offset = (settings->v_axis_offset * INCH_PER_MM); + settings->w_axis_offset = (settings->w_axis_offset * INCH_PER_MM); + settings->u_origin_offset = (settings->u_origin_offset * INCH_PER_MM); + settings->v_origin_offset = (settings->v_origin_offset * INCH_PER_MM); + settings->w_origin_offset = (settings->w_origin_offset * INCH_PER_MM); + + settings->tool_offset.tran.x = GET_EXTERNAL_TOOL_LENGTH_XOFFSET(); + settings->tool_offset.tran.y = GET_EXTERNAL_TOOL_LENGTH_YOFFSET(); + settings->tool_offset.tran.z = GET_EXTERNAL_TOOL_LENGTH_ZOFFSET(); + settings->tool_offset.a = GET_EXTERNAL_TOOL_LENGTH_AOFFSET(); + settings->tool_offset.b = GET_EXTERNAL_TOOL_LENGTH_BOFFSET(); + settings->tool_offset.c = GET_EXTERNAL_TOOL_LENGTH_COFFSET(); + settings->tool_offset.u = GET_EXTERNAL_TOOL_LENGTH_UOFFSET(); + settings->tool_offset.v = GET_EXTERNAL_TOOL_LENGTH_VOFFSET(); + settings->tool_offset.w = GET_EXTERNAL_TOOL_LENGTH_WOFFSET(); + settings->feed_rate = GET_EXTERNAL_FEED_RATE(); + settings->tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + settings->naivecam_tolerance = + GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + } + } else if (g_code == G_21) { + USE_LENGTH_UNITS(CANON_UNITS_MM); + if (settings->length_units != CANON_UNITS_MM) { + settings->length_units = CANON_UNITS_MM; + settings->current_x = (settings->current_x * MM_PER_INCH); + settings->current_y = (settings->current_y * MM_PER_INCH); + settings->current_z = (settings->current_z * MM_PER_INCH); + settings->program_x = (settings->program_x * MM_PER_INCH); + settings->program_y = (settings->program_y * MM_PER_INCH); + settings->program_z = (settings->program_z * MM_PER_INCH); + qc_scale(MM_PER_INCH); + settings->cutter_comp_radius *= MM_PER_INCH; + settings->axis_offset_x = (settings->axis_offset_x * MM_PER_INCH); + settings->axis_offset_y = (settings->axis_offset_y * MM_PER_INCH); + settings->axis_offset_z = (settings->axis_offset_z * MM_PER_INCH); + settings->origin_offset_x = (settings->origin_offset_x * MM_PER_INCH); + settings->origin_offset_y = (settings->origin_offset_y * MM_PER_INCH); + settings->origin_offset_z = (settings->origin_offset_z * MM_PER_INCH); + + settings->u_current = (settings->u_current * MM_PER_INCH); + settings->v_current = (settings->v_current * MM_PER_INCH); + settings->w_current = (settings->w_current * MM_PER_INCH); + settings->u_axis_offset = (settings->u_axis_offset * MM_PER_INCH); + settings->v_axis_offset = (settings->v_axis_offset * MM_PER_INCH); + settings->w_axis_offset = (settings->w_axis_offset * MM_PER_INCH); + settings->u_origin_offset = (settings->u_origin_offset * MM_PER_INCH); + settings->v_origin_offset = (settings->v_origin_offset * MM_PER_INCH); + settings->w_origin_offset = (settings->w_origin_offset * MM_PER_INCH); + + settings->tool_offset.tran.x = GET_EXTERNAL_TOOL_LENGTH_XOFFSET(); + settings->tool_offset.tran.y = GET_EXTERNAL_TOOL_LENGTH_YOFFSET(); + settings->tool_offset.tran.z = GET_EXTERNAL_TOOL_LENGTH_ZOFFSET(); + settings->tool_offset.a = GET_EXTERNAL_TOOL_LENGTH_AOFFSET(); + settings->tool_offset.b = GET_EXTERNAL_TOOL_LENGTH_BOFFSET(); + settings->tool_offset.c = GET_EXTERNAL_TOOL_LENGTH_COFFSET(); + settings->tool_offset.u = GET_EXTERNAL_TOOL_LENGTH_UOFFSET(); + settings->tool_offset.v = GET_EXTERNAL_TOOL_LENGTH_VOFFSET(); + settings->tool_offset.w = GET_EXTERNAL_TOOL_LENGTH_WOFFSET(); + settings->feed_rate = GET_EXTERNAL_FEED_RATE(); + settings->tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + settings->naivecam_tolerance = + GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + } + } else + ERS(NCE_BUG_CODE_NOT_G20_OR_G21); + return INTERP_OK; +} + + +/* + * given two int arrays and two double arrays representing interpreter + * settings as stored in _setup.active_g_codes and + * _setup.active_settings, construct a G-code sequence to synchronize + * their state. + */ +int Interp::gen_settings( + int *int_current, int *int_saved, // G-codes + double * /*float_current*/, double *float_saved, // S, F, other + std::string &cmd) // command buffer +{ + FORCE_LC_NUMERIC_C; + int i, val; + int g64_changed = 0; + char buf[LINELEN]; + + // F, S + for (i = 0; i < ACTIVE_SETTINGS; i++) { + // "if" masked to address https://github.com/LinuxCNC/linuxcnc/issues/1987 + // The setting value is correct, but seems to be mislaid downstream + //if (float_saved[i] != float_current[i]) { + switch (i) { + case GM_FIELD_FLOAT_LINE_NUMBER: + // sequence_number - no point in restoring + break; + case GM_FIELD_FLOAT_FEED: + snprintf(buf,sizeof(buf)," F%.1f", float_saved[i]); + cmd += buf; + break; + case GM_FIELD_FLOAT_SPEED: + snprintf(buf,sizeof(buf)," S%.0f", float_saved[i]); + cmd += buf; + break; + case GM_FIELD_FLOAT_PATH_TOLERANCE: + case GM_FIELD_FLOAT_NAIVE_CAM_TOLERANCE: + // G64 special case; see below + g64_changed = 1; + break; + } + //} + } + + // G-codes + for (i = 0; i < ACTIVE_G_CODES; i++) { + val = int_saved[i]; + if (val != int_current[i]) { + + switch (i) { + case 0: + // // sequence_number - no point in restoring + break; + case 2: // FIXME - I dont understand this: + // gez[2] = ((block == NULL) ? -1 : block->g_modes[0]); + break; + case 12: // mystery slot + break; + case 5: // - length units + // this is treated before all others - see convert_m() + break; + case 1: // - motion_mode + // restoring the motion mode is a real bad idea to start with + break; + case 3: // - plane + case 4: // - cutter compensation + case 6: // - distance mode + case 7: // - feed mode + case 8: // - coordinate system + case 9: // - tool offset (G43/G49) + case 10: // - retract mode + case 13: // - spindle mode + case 14: // - ijk distance mode + case 15: // - lathe diameter mode + case 16: // - whether g92 is applied + + if (val != -1) { // FIXME not sure if this is correct! + // if this was set in sub, and unset in caller, it will + // not be reset + if (val % 10) { + snprintf(buf,sizeof(buf)," G%d.%d", val / 10, val % 10); + } else { + snprintf(buf,sizeof(buf)," G%d", val / 10); + } + cmd += buf; + } else { + // so complain rather loudly + MSG("------ gen_settings BUG: index %d = -1!!\n",i); + } + break; + case 11: // - control mode + // Special case, since G64 requires P and Q flags + // FIXME what about when P or Q changes, even though still G64? + if (val == G_64) + // G64 special case; see below + g64_changed = 1; + else if (val == G_61) { + snprintf(buf,sizeof(buf)," G61"); + cmd += buf; + } else if (val == G_61_1) { + snprintf(buf,sizeof(buf)," G61.1"); + cmd += buf; + } else + MSG("------ gen_settings BUG: index %d = -1!!\n",i); + break; + } + } + } + + + // Special case for restoring G64: `cmd` may contain a `G64` if + // current int settings is `G61` or if current float settings + // contain different `G64 P* Q*` args; in these cases, only add a + // single `G64` command + if ((int_saved[11] == G_64) && g64_changed) { + if (float_saved[GM_FIELD_FLOAT_PATH_TOLERANCE] < 0) + // No P, Q args + snprintf(buf,sizeof(buf)," G64"); + else if ( + float_saved[GM_FIELD_FLOAT_NAIVE_CAM_TOLERANCE] < 0) + // Only P arg + snprintf(buf,sizeof(buf)," G64 P%f", + float_saved[GM_FIELD_FLOAT_PATH_TOLERANCE]); + else // Both P, Q args + snprintf( + buf,sizeof(buf)," G64 P%f Q%f", + float_saved[GM_FIELD_FLOAT_PATH_TOLERANCE], + float_saved[GM_FIELD_FLOAT_NAIVE_CAM_TOLERANCE]); + cmd += buf; + } + return INTERP_OK; +} + +/* + * given two int arrays representing interpreter settings as stored in + * _setup.active_m_codes, construct a M-code sequence to synchronize their state. + * + * use multiple lines here because M7 and M8 may not be on the same line since + * they are in the same modal group. + */ +int Interp::gen_m_codes(int *current, int *saved, std::string &cmd) +{ + FORCE_LC_NUMERIC_C; + int i,val; + char buf[LINELEN]; + for (i = 0; i < ACTIVE_M_CODES; i++) { + val = saved[i]; + if (val != current[i]) { + switch (i) { + case 0: /* 0 seq number */ + break; + case 1: /* 1 stopping */ + // FIXME - is the next line needed at all? + // emz[1] = (block == NULL) ? -1 : block->m_modes[4]; + break; + case 3: /* 3 tool change */ + // FIXME - dont know how to handle this. + // emz[3] = + // (block == NULL) ? -1 : block->m_modes[6]; + break; + case 2: // spindle + case 4: // mist + case 5: // flood + case 6: // speed/feed override + case 7: // adaptive feed + case 8: // feed hold + if (val != -1) { // unsure.. + snprintf(buf,sizeof(buf),"M%d\n", val); + cmd += buf; + } else { + MSG("------ gen_m_codes: index %d = -1!!\n",i); + } + break; + } + } + } + return INTERP_OK; +} + + +/** + * Create a G-code string to restore a modal state on program abort. + * Note: This is only designed to be called on program abort. It restores the + * modal state in the interpreter to the equivalent state at the most recent + * motion line, but does not restore M codes. + */ +int Interp::gen_restore_cmd(int *current_g, + int * /*current_m*/, + double *current_settings, + StateTag const &saved, + std::string &cmd) +{ + int res; + // A local copy of the saved settings, unpacked from a state tag + int saved_g[ACTIVE_G_CODES]; + int saved_m[ACTIVE_M_CODES]; + double saved_settings[ACTIVE_SETTINGS]; + + //Extract saved state to local vectors + // FIXME Use saved_settings to store state tag floats, incl. in + int res_unpack = active_modes(saved_g, saved_m, saved_settings, saved); + if (res_unpack != INTERP_OK) { + logStateTags("gen_restore_cmd() error %d: failed to unpack state tag", + res_unpack); + return INTERP_ERROR; + } + + /* Now we clean up any G / M modes that should be reset when ending a + * program. Note that most of these mode states are based on logic in + * write_XXX functions. + */ + // Force cancellation of tool compensation + saved_g[4] = G_40; + + //Mimic the order of restoration commands used elsewhere + if (current_g[5] != saved_g[5]) { + char buf[LINELEN]; + snprintf(buf,sizeof(buf), "G%d",saved_g[5]/10); + CHKS(execute(buf) != INTERP_OK, _("gen_restore G20/G21 failed: '%s'"), + cmd.c_str()); + } + + if ((res = gen_settings( + current_g, saved_g, current_settings, saved_settings, cmd))) { + logStateTags("gen_restore_cmd(): error restoring settings (%d)", + res); + return INTERP_ERROR; + } + // M codes should not be restored during an abort with gen_m_codes() + + return INTERP_OK; +} + + +int Interp::save_settings(setup_pointer settings) +{ + // the state is sprinkled all over _setup + // collate state in _setup.active_* arrays + write_g_codes((block_pointer) NULL, settings); + write_m_codes((block_pointer) NULL, settings); + write_settings(settings); + write_state_tag((block_pointer) NULL, settings, settings->state_tag); + + // save in the current call frame + active_g_codes((int *)settings->sub_context[settings->call_level].saved_g_codes); + active_m_codes((int *)settings->sub_context[settings->call_level].saved_m_codes); + active_settings((double *)settings->sub_context[settings->call_level].saved_settings); + + // TBD: any other state deemed important to save/restore should be added here + // context_struct might need to be extended too. + + return INTERP_OK; +} + +/* restore global settings/gcodes/mcodes to current call level from a valid context + * used by: + * M72 - restores context from same level + * example: restore_settings(settings->call_level) + * + * an o-word return/endsub if auto-restore (M73) was issued + * issue this like so - after call_level has been decremented: + * restore_settings(settings->call_level + 1) + */ +int Interp::restore_settings(setup_pointer settings, + int from_level) //!< call level of context to restore from +{ + + CHKS((from_level < settings->call_level), + (_("BUG: cannot restore from a lower call level (%d) to a higher call level (%d)")),from_level,settings->call_level); + CHKS((from_level < 0), (_("BUG: restore from level %d !?")),from_level); + CHKS((settings->call_level < 0), (_("BUG: restore to level %d !?")),settings->call_level); + + // linearize state + write_g_codes((block_pointer) NULL, settings); + write_m_codes((block_pointer) NULL, settings); + write_settings(settings); + + std::string cmd; + + // construct gcode from the state difference and execute + // this assures appropriate canon commands are generated if needed - + // just restoring interp variables is not enough + + // G20/G21 switching is special - it is executed beforehand + // so restoring feed lateron is interpreted in the correct context + + if (settings->active_g_codes[5] != settings->sub_context[from_level].saved_g_codes[5]) { + char buf[LINELEN]; + snprintf(buf,sizeof(buf), "G%d",settings->sub_context[from_level].saved_g_codes[5]/10); + CHKS(execute(buf) != INTERP_OK, _("M7x: restore_settings G20/G21 failed: '%s'"), cmd.c_str()); + } + gen_settings( + (int *)settings->active_g_codes, + (int *)settings->sub_context[from_level].saved_g_codes, + (double *)settings->active_settings, + (double *)settings->sub_context[from_level].saved_settings, + cmd); + gen_m_codes( + (int *) settings->active_m_codes, + (int *)settings->sub_context[from_level].saved_m_codes, + cmd); + + if (!cmd.empty()) { + // the sequence can be multiline, separated by nl + // so split and execute each line + std::string cpy = cmd; + char *stateptr = NULL; + char *s = strtok_r(cpy.data(), "\n", &stateptr); + while (s != NULL) { + int status = execute(s); + if (status != INTERP_OK) { + char currentError[LINELEN+1]; + rtapi_strxcpy(currentError,getSavedError()); + CHKS(status, _("M7x: restore_settings failed executing: '%s': %s"), s, currentError); + } + s = strtok_r(NULL, "\n", &stateptr); + } + write_g_codes((block_pointer) NULL, settings); + write_m_codes((block_pointer) NULL, settings); + write_settings(settings); + } + + // TBD: any state deemed important to restore should be restored here + // NB: some state changes might generate canon commands so do that here + // if needed + + return INTERP_OK; +} + + +/** + * Variation of restore_settings to pull state from a StateTag. + */ +int Interp::restore_from_tag(StateTag const &tag) +{ + + if (!tag.is_valid() || !tag.flags[GM_FLAG_RESTORABLE]) { + //Invalid line implies a bad tag, don't restore + logStateTags("restore_from_tag() error: Invalid tag"); + print_state_tag(tag); + return INTERP_ERROR; + } + + // clear queue buster sflags, otherwise the command won't be + // executed - Tormach *dpr 8/17/15 + _setup.input_flag = false; + _setup.toolchange_flag = false; + _setup.probe_flag = false; + + // linearize state + write_g_codes((block_pointer) NULL, &_setup); + write_m_codes((block_pointer) NULL, &_setup); + write_settings(&_setup); + + std::string cmd; + + // construct gcode from the state difference and execute + // this assures appropriate canon commands are generated if needed - + // just restoring interp variables is not enough + + int res_unpack = gen_restore_cmd((int *) _setup.active_g_codes, + (int *) _setup.active_m_codes, + (double *) _setup.active_settings, + tag, + cmd); + if (res_unpack != INTERP_OK) { + logStateTags("restore_from_tag() failed to generate restore command"); + print_state_tag(tag); + return res_unpack; + } + + if (!cmd.empty()) { + // the sequence can be multiline, separated by nl + // so split and execute each line + std::string cpy = cmd; + char *stateptr = NULL; + char *s = strtok_r(cpy.data(), "\n", &stateptr); + while (s != NULL) { + int status = execute(s); + if (status != INTERP_OK) { + char currentError[LINELEN+1]; + strcpy(currentError,getSavedError()); + CHKS(status, _("Failed to restore interp state on abort " + "'%s': %s"), s, currentError); + } + s = strtok_r(NULL, "\n", &stateptr); + } + write_g_codes((block_pointer) NULL, &_setup); + write_m_codes((block_pointer) NULL, &_setup); + write_settings(&_setup); + logStateTags("Restored program pre-abort state from tag: |%s|", + cmd.c_str()); + } else { + logStateTags("Program pre-abort state unchanged; not restoring:"); + print_state_tag(tag); + } + + return INTERP_OK; +} + + + +/****************************************************************************/ + +/*! convert_m + +Returned Value: int + If convert_tool_change returns an error code, this returns that code. + If input-related stuff is needed, it sets the flag input_flag = true. + Otherwise, it returns INTERP_OK. + +Side effects: + m_codes in the block are executed. For each m_code + this consists of making a function call(s) to a canonical machining + function(s) and setting the machine model. + +Called by: execute_block. + +This handles four separate types of activity in order: +1. changing the tool (m6) - which also retracts and stops the spindle. +2. Turning the spindle on or off (m3, m4, and m5) +3. Turning coolant on and off (m7, m8, and m9) +4. turning a-axis clamping on and off (m26, m27) - commented out. +5. enabling or disabling feed and speed overrides (m49, m49). +6. changing the loaded toolnumber (m61). +Within each group, only the first code encountered will be executed. + +This does nothing with m0, m1, m2, m30, or m60 (which are handled in +convert_stop). + +*/ + +int Interp::convert_m(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int type; + double timeout; // timeout for M66 + + /* The M62-65 commands are used for DIO */ + /* M62 sets a DIO synched with motion + M63 clears a DIO synched with motion + M64 sets a DIO immediately + M65 clears a DIO immediately + M66 waits for an input + M67 reads a digital input + M68 reads an analog input*/ + + if (is_user_defined_m_code(block, settings, 5) && + STEP_REMAPPED_IN_BLOCK(block, STEP_M_5) && + ONCE_M(5)) { + return convert_remapped_code(block, settings, STEP_M_5, 'm', + block->m_modes[5]); + } else if ((block->m_modes[5] == 62) && ONCE_M(5)) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set motion output with cutter radius compensation on"))); // XXX + CHKS((!block->p_flag), _("No valid P word with M62")); + SET_MOTION_OUTPUT_BIT(round_to_int(block->p_number)); + } else if ((block->m_modes[5] == 63) && ONCE_M(5)) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set motion digital output with cutter radius compensation on"))); // XXX + CHKS((!block->p_flag), _("No valid P word with M63")); + CLEAR_MOTION_OUTPUT_BIT(round_to_int(block->p_number)); + } else if ((block->m_modes[5] == 64) && ONCE_M(5)){ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set auxiliary digital output with cutter radius compensation on"))); // XXX + CHKS((!block->p_flag), _("No valid P word with M64")); + SET_AUX_OUTPUT_BIT(round_to_int(block->p_number)); + } else if ((block->m_modes[5] == 65) && ONCE_M(5)) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set auxiliary digital output with cutter radius compensation on"))); // XXX + CHKS((!block->p_flag), _("No valid P word with M65")); + CLEAR_AUX_OUTPUT_BIT(round_to_int(block->p_number)); + } else if ((block->m_modes[5] == 66) && ONCE_M(5)){ + + //P-word = digital channel + //E-word = analog channel + //L-word = wait type (immediate, rise, fall, high, low) + //Q-word = timeout + // it is an error if: + + // P and E word are specified together + CHKS(((block->p_flag) && (block->e_flag)), + NCE_BOTH_DIGITAL_AND_ANALOG_INPUT_SELECTED); + + // L-word not 0, and timeout <= 0 + CHKS(((block->q_number <= 0) && (block->l_flag) && (round_to_int(block->l_number) > 0)), + NCE_ZERO_TIMEOUT_WITH_WAIT_NOT_IMMEDIATE); + + // E-word specified (analog input) and wait type not immediate + CHKS(((block->e_flag) && (block->l_flag) && (round_to_int(block->l_number) != 0)), + NCE_ANALOG_INPUT_WITH_WAIT_NOT_IMMEDIATE); + + // missing P or E (or invalid = negative) + CHKS( ((block->p_flag) && (round_to_int(block->p_number) < 0)) || + ((block->e_flag) && (round_to_int(block->e_number) < 0)) || + ((!block->p_flag) && (!block->e_flag)) , + NCE_INVALID_OR_MISSING_P_AND_E_WORDS_FOR_WAIT_INPUT); + + write_canon_state_tag(block, settings); + if (block->p_flag) { // got a digital input + if (round_to_int(block->p_number) < 0) // safety check for negative words + ERS(_("invalid P-word with M66")); + + if (block->l_flag) { + type = round_to_int(block->l_number); + } else { + type = WAIT_MODE_IMMEDIATE; + } + + if (block->q_number > 0) { + timeout = block->q_number; + } else { + timeout = 0; + } + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot wait for digital input with cutter radius compensation on"))); + + int ret = WAIT(round_to_int(block->p_number), DIGITAL_INPUT, type, timeout); + //WAIT returns 0 on success, -1 for out of bounds + CHKS((ret == -1), NCE_DIGITAL_INPUT_INVALID_ON_M66); + if (ret == 0) { + settings->input_flag = true; + settings->input_index = round_to_int(block->p_number); + settings->input_digital = true; + } + } else if (round_to_int(block->e_number) >= 0) { // got an analog input + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot wait for analog input with cutter radius compensation on"))); + + int ret = WAIT(round_to_int(block->e_number), ANALOG_INPUT, 0, 0); //WAIT returns 0 on success, -1 for out of bounds + CHKS((ret == -1), NCE_ANALOG_INPUT_INVALID_ON_M66); + if (ret == 0) { + settings->input_flag = true; + settings->input_index = round_to_int(block->e_number); + settings->input_digital = false; + } + } + } else if ((block->m_modes[5] == 67) && ONCE_M(5)) { + + //E-word = analog channel + //Q-word = analog value + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set motion analog output with cutter radius compensation on"))); // XXX + CHKS((!block->e_flag) || (round_to_int(block->e_number) < 0), (_("Invalid analog index with M67"))); + SET_MOTION_OUTPUT_VALUE(round_to_int(block->e_number), block->q_number); + } else if ((block->m_modes[5] == 68) && ONCE_M(5)) { + //E-word = analog channel + //Q-word = analog value + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot set auxiliary analog output with cutter radius compensation on"))); // XXX + CHKS((!block->e_flag) || (round_to_int(block->e_number) < 0), (_("Invalid analog index with M68"))); + SET_AUX_OUTPUT_VALUE(round_to_int(block->e_number), block->q_number); + } + + if ((block->m_modes[6] != -1) && ONCE_M(6)){ + int toolno; + bool remapped_in_block = STEP_REMAPPED_IN_BLOCK(block, STEP_M_6); + + switch (block->m_modes[6]) { + case 6: + if (is_user_defined_m_code(block, settings, 6) && remapped_in_block) { + return convert_remapped_code(block,settings, + STEP_M_6, + 'm', + block->m_modes[6]); + } else { + // the code was used in its very remap procedure - + // the 'recursion case'; record the fact + CONTROLLING_BLOCK(*settings).builtin_used = !remapped_in_block; + CHP(convert_tool_change(settings)); + } + break; + + case 61: + if (is_user_defined_m_code(block, settings, 6) && remapped_in_block) { + return convert_remapped_code(block, settings, STEP_M_6,'m', + block->m_modes[6]); + } else { + CONTROLLING_BLOCK(*settings).builtin_used = !remapped_in_block; + toolno = round_to_int(block->q_number); + // now also accept M61 Q0 - unload tool + CHKS((toolno < 0), (_("Need non-negative Q-word to specify tool number with M61"))); + + int idx; + + // make sure selected tool exists + CHP((find_tool_index(settings, toolno, &idx))); + settings->current_pocket = idx; + settings->toolchange_flag = true; + CHANGE_TOOL_NUMBER(settings->current_pocket); + set_tool_parameters(); + } + break; + + default: + if (is_user_defined_m_code(block, settings, 6)) { + return convert_remapped_code(block, settings, STEP_M_6,'m', + block->m_modes[6]); + } + } + } + + if (FEATURE(RETAIN_G43)) { + + if ((settings->active_g_codes[9] == G_43) && ONCE(STEP_RETAIN_G43)) { + if(settings->selected_pocket > 0) { + struct block_struct g43; + init_block(&g43); + block->g_modes[gees[G_43]] = G_43; + CHP(convert_tool_length_offset(G_43, &g43, settings)); + } else { + struct block_struct g49; + init_block(&g49); + block->g_modes[gees[G_49]] = G_49; + CHP(convert_tool_length_offset(G_49, &g49, settings)); + } + } + } + + if (is_user_defined_m_code(block, settings, 7) && ONCE_M(7)) { + return convert_remapped_code(block, settings, STEP_M_7, 'm', + block->m_modes[7]); + } else if ((block->m_modes[7] == 3) && ONCE_M(7)) { + if (block->dollar_flag){ + CHKS((block->dollar_number >= settings->num_spindles || block->dollar_number < -1), + (_("Spindle ($) number out of range in M3 Command\nnum_spindles =%i. $=%d\n")),settings->num_spindles,(int)block->dollar_number); + if (block->dollar_number == -1){ // all spindles + for (int i = 0; i < settings->num_spindles; i++){ + enqueue_START_SPINDLE_CLOCKWISE(i); + settings->spindle_turning[i] = CANON_CLOCKWISE; + } + } else { // a specific spindle + enqueue_START_SPINDLE_CLOCKWISE(block->dollar_number); + settings->spindle_turning[(int)block->dollar_number] = CANON_CLOCKWISE; + } + } else { // the default spindle + enqueue_START_SPINDLE_CLOCKWISE(0); + settings->spindle_turning[0] = CANON_CLOCKWISE; + } + } else if ((block->m_modes[7] == 4) && ONCE_M(7)) { + if (block->dollar_flag){ + CHKS((block->dollar_number >= settings->num_spindles || block->dollar_number < -1), + (_("Spindle ($) number out of range in M4 Command\nnum_spindles =%i. $=%d\n")),settings->num_spindles,(int)block->dollar_number); + if (block->dollar_number == -1){ // all spindles + for (int i = 0; i < settings->num_spindles; i++){ + enqueue_START_SPINDLE_COUNTERCLOCKWISE(i); + settings->spindle_turning[i] = CANON_COUNTERCLOCKWISE; + } + } else { // a specific spindle + enqueue_START_SPINDLE_COUNTERCLOCKWISE(block->dollar_number); + settings->spindle_turning[(int)block->dollar_number] = CANON_COUNTERCLOCKWISE; + } + } else { // default spindle + enqueue_START_SPINDLE_COUNTERCLOCKWISE(0); + settings->spindle_turning[0] = CANON_COUNTERCLOCKWISE; + } + } else if ((block->m_modes[7] == 5) && ONCE_M(7)){ + if (block->dollar_flag){ + CHKS((block->dollar_number >= settings->num_spindles || block->dollar_number < -1), + (_("Spindle ($) number out of range in M5 Command\nnum_spindles =%i. $=%d\n")),settings->num_spindles,(int)block->dollar_number); + if (block->dollar_number == -1){ // all spindles + for (int i = 0; i < settings->num_spindles; i++){ + settings->spindle_turning[i] = CANON_STOPPED; + enqueue_STOP_SPINDLE_TURNING(i); + } + } else { // a specific spindle + settings->spindle_turning[block->dollar_number] = CANON_STOPPED; + enqueue_STOP_SPINDLE_TURNING(block->dollar_number); + } + } else { // the default spindle + for (int i = 0; i < settings->num_spindles; i++){ + settings->spindle_turning[i] = CANON_STOPPED; + enqueue_STOP_SPINDLE_TURNING(i); + } + } + } else if ((block->m_modes[7] == 19) && ONCE_M(7)) { + for (int i = 0; i < settings->num_spindles; i++) + settings->spindle_turning[i] = CANON_STOPPED; + if (block->dollar_flag){ + CHKS((block->dollar_number >= settings->num_spindles || block->dollar_number < 0), + (_("Spindle ($) number out of range in M19 Command"))); + } + if (block->r_flag || block->p_flag) + enqueue_ORIENT_SPINDLE(block->dollar_flag ? block->dollar_number : 0, + block->r_flag ? (block->r_number + settings->orient_offset) : settings->orient_offset, + block->p_flag ? block->p_number : 0); + if (block->q_flag) { + CHKS((block->q_number <= 0.0),(_("Q word with M19 requires a value > 0"))); + enqueue_WAIT_ORIENT_SPINDLE_COMPLETE(block->dollar_flag ? block->dollar_number : 0, + block->q_number); + } + } else if ((block->m_modes[7] == 70) || (block->m_modes[7] == 73)) { + + // save state in current stack frame. We borrow the o-word call stack + // and extend it to hold modes & settings. + save_settings(&_setup); + + // flag this frame as containing a valid context + _setup.sub_context[_setup.call_level].context_status |= CONTEXT_VALID; + + // mark as auto-restore context + if (block->m_modes[7] == 73) { + if (_setup.call_level == 0) { + MSG("Warning - M73 at top level: nothing to return to; storing context anyway\n"); + } else { + _setup.sub_context[_setup.call_level].context_status |= CONTEXT_RESTORE_ON_RETURN; + } + } + } else if ((block->m_modes[7] == 71) && ONCE_M(7)) { + // M72 - invalidate context at current level + _setup.sub_context[_setup.call_level].context_status &= ~CONTEXT_VALID; + + } else if ((block->m_modes[7] == 72) && ONCE_M(7)) { + + // restore state from current stack frame. + CHKS((!(_setup.sub_context[_setup.call_level].context_status & CONTEXT_VALID)), + (_("Cannot restore context from invalid stack frame - missing M70/M73?"))); + CHP(restore_settings(&_setup, _setup.call_level)); + } + + if (is_user_defined_m_code(block, settings, 8) && + STEP_REMAPPED_IN_BLOCK(block, STEP_M_8) && ONCE_M(8)) { + return convert_remapped_code(block, settings, STEP_M_8, 'm', block->m_modes[8]); + } else if ((block->m_modes[8] == 7) && ONCE_M(8)){ + enqueue_MIST_ON(); + settings->mist = true; + } else if ((block->m_modes[8] == 8) && ONCE_M(8)) { + enqueue_FLOOD_ON(); + settings->flood = true; + } else if ((block->m_modes[8] == 9) && ONCE_M(8)) { + enqueue_MIST_OFF(); + settings->mist = false; + enqueue_FLOOD_OFF(); + settings->flood = false; + } + +/* No axis clamps in this version + if (block->m_modes[2] == 26) + { +#ifdef DEBUG_EMC + COMMENT("interpreter: automatic A-axis clamping turned on"); +#endif + settings->a_axis_clamping = true; + } + else if (block->m_modes[2] == 27) + { +#ifdef DEBUG_EMC + COMMENT("interpreter: automatic A-axis clamping turned off"); +#endif + settings->a_axis_clamping = false; + } +*/ +if (is_user_defined_m_code(block, settings, 9) && ONCE_M(9)) { + return convert_remapped_code(block, settings, STEP_M_9, 'm', + block->m_modes[9]); + } else if ((block->m_modes[9] == 48) && ONCE_M(9)){ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot enable overrides with cutter radius compensation on"))); // XXX + ENABLE_FEED_OVERRIDE(); + settings->feed_override = true; + for (int s = 0; s < settings->num_spindles; s++){ + settings->speed_override[s] = true; + ENABLE_SPEED_OVERRIDE(s); + } + } else if ((block->m_modes[9] == 49) && ONCE_M(9)){ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot disable overrides with cutter radius compensation on"))); // XXX + DISABLE_FEED_OVERRIDE(); + settings->feed_override = false; + for (int s = 0; s < settings->num_spindles; s++){ + settings->speed_override[s] = false; + DISABLE_SPEED_OVERRIDE(s); + } + } + +if ((block->m_modes[9] == 50) && ONCE_M(9)){ + if (block->p_number != 0) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot enable overrides with cutter radius compensation on"))); // XXX + ENABLE_FEED_OVERRIDE(); + settings->feed_override = true; + } else { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot disable overrides with cutter radius compensation on"))); // XXX + DISABLE_FEED_OVERRIDE(); + settings->feed_override = false; + } + } + +if ((block->m_modes[9] == 51) && ONCE_M(9)){ + int e = -1; + if (block->dollar_flag){ + CHKS((block->dollar_number <= 0 || block->dollar_number >= settings-> num_spindles), + (_("Invalid spindle ($) number in M51 command"))); + e = block->dollar_number; + } + if (block->p_number != 0) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot enable overrides with cutter radius compensation on"))); // XXX + for (int s = 0; s < settings->num_spindles; s++){ + if (e == -1 or s == e){ + ENABLE_SPEED_OVERRIDE(s); + settings->speed_override[s] = true; + } + } + } else { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot disable overrides with cutter radius compensation on"))); // XXX + for (int s = 0; s < settings->num_spindles; s++){ + if (e == -1 or s == e){ + DISABLE_SPEED_OVERRIDE(s); + settings->speed_override[s] = false; + } + } + } + } + +if ((block->m_modes[9] == 52) && ONCE_M(9)){ + if (block->p_number != 0) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot enable overrides with cutter radius compensation on"))); // XXX + ENABLE_ADAPTIVE_FEED(); + settings->adaptive_feed = true; + } else { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot disable overrides with cutter radius compensation on"))); // XXX + DISABLE_ADAPTIVE_FEED(); + settings->adaptive_feed = false; + } + } + +if ((block->m_modes[9] == 53) && ONCE_M(9)){ + if (block->p_number != 0) { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot enable overrides with cutter radius compensation on"))); // XXX + ENABLE_FEED_HOLD(); + settings->feed_hold = true; + } else { + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot disable overrides with cutter radius compensation on"))); // XXX + DISABLE_FEED_HOLD(); + settings->feed_hold = false; + } + } + +if (is_user_defined_m_code(block, settings, 10) && ONCE_M(10)) { + return convert_remapped_code(block,settings,STEP_M_10,'m', + block->m_modes[10]); + + } else if ((block->m_modes[10] != -1) && ONCE_M(10)){ + /* user-defined M codes */ + int index = block->m_modes[10]; + if (USER_DEFINED_FUNCTION[index - 100] == 0) { + CHKS(1, NCE_UNKNOWN_M_CODE_USED,index); + } + enqueue_M_USER_COMMAND(index,block->p_number,block->q_number); + } + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_modal_0 + +Returned Value: int + If one of the following functions is called and returns an error code, + this returns that code. + convert_axis_offsets + convert_home + convert_setup + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. code is not G_4, G_10, G_28, G_30, G_52, G_53, G_92: + NCE_BUG_CODE_NOT_G4_G10_G28_G30_G52_G53_OR_G92_SERIES + +Side effects: See below + +Called by: convert_g + +If the g_code is g10, g28, g30, g52, g92, g92.1, g92.2, or g92.3 (all are in +modal group 0), it is executed. The other two in modal group 0 (G4 and +G53) are executed elsewhere. + +*/ + +int Interp::convert_modal_0(int code, //!< G-code, must be from group 0 + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + + if (code == G_10) { + if(block->l_number == 1 || block->l_number == 10 || block->l_number == 11) + CHP(convert_setup_tool(block, settings)); + else if (block->l_number == 0) { + int tno = settings->tool_table[0].toolno; + if (tno > 0) { ERS("G10 L0 not allowed with loaded tool <%d>\n",tno); } + settings->toolchange_flag = true; // refresh actual pos, sync,etc + RELOAD_TOOLDATA(); // msg to io + } + else + CHP(convert_setup(block, settings)); + } else if ((code == G_28) || (code == G_30)) { + CHP(convert_home(code, block, settings)); + } else if ((code == G_28_1) || (code == G_30_1)) { + CHP(convert_savehome(code, block, settings)); + } else if ((code == G_52) || (code == G_92)) { + CHP(convert_axis_offsets(code, block, settings)); + } else if ((code == G_5_3)||(code == G_6_3)) { // jjf + CHP(convert_nurbs(code, block, settings)); + } else if ((code == G_4) || (code == G_53)); // handled elsewhere + else + ERS(NCE_BUG_CODE_NOT_G4_G10_G28_G30_G52_G53_OR_G92_SERIES); + return INTERP_OK; +} + +int Interp::convert_g92_is_applied(int code, block_pointer block, + setup_pointer settings) +{ + CHP(convert_axis_offsets(code, block, settings)); + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! convert_motion + +Returned Value: int + If one of the following functions is called and returns an error code, + this returns that code. + convert_arc + convert_cycle + convert_probe + convert_straight + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. The motion code is not 0,1,2,3,38.2,80,81,82,83,84,85,86,87, 88, or 89: + NCE_BUG_UNKNOWN_MOTION_CODE + +Side effects: + A g_code from the group causing motion (mode 1) is executed. + +Called by: convert_g. + +*/ + +int Interp::convert_motion(int motion, //!< g_code for a line, arc, canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + int ai = block->a_flag && (-1 != settings->a_indexer_jnum); + int bi = block->b_flag && (-1 != settings->b_indexer_jnum); + int ci = block->c_flag && (-1 != settings->c_indexer_jnum); + + + if (motion != G_0) { + CHKS((ai), (_("Indexing axis %c can only be moved with G0")), 'A'); + CHKS((bi), (_("Indexing axis %c can only be moved with G0")), 'B'); + CHKS((ci), (_("Indexing axis %c can only be moved with G0")), 'C'); + } + + int xyzuvw_flag = (block->x_flag || block->y_flag || block->z_flag || + block->u_flag || block->v_flag || block->w_flag); + + CHKS((ai && (xyzuvw_flag || block->b_flag || block->c_flag)), + (_("Indexing axis %c can only be moved alone")), 'A'); + CHKS((bi && (xyzuvw_flag || block->a_flag || block->c_flag)), + (_("Indexing axis %c can only be moved alone")), 'B'); + CHKS((ci && (xyzuvw_flag || block->a_flag || block->b_flag)), + (_("Indexing axis %c can only be moved alone")), 'C'); + + if (!is_a_cycle(motion)) + settings->cycle_il_flag = false; + + if (ai || bi || ci) { + int anum=-1,jnum=-1; + if ( ai) {anum = 3; jnum = settings->a_indexer_jnum;} + else if (bi) {anum = 4; jnum = settings->b_indexer_jnum;} + else if (ci) {anum = 5; jnum = settings->c_indexer_jnum;} + CHP(convert_straight_indexer(anum, jnum, block, settings)); + } else if ((motion == G_0) || (motion == G_1) || (motion == G_33) || (motion == G_33_1) || (motion == G_76)) { + CHP(convert_straight(motion, block, settings)); + } else if ((motion == G_3) || (motion == G_2)) { + CHP(convert_arc(motion, block, settings)); + } else if (motion == G_38_2 || motion == G_38_3 || + motion == G_38_4 || motion == G_38_5) { + CHP(convert_probe(block, motion, settings)); + } else if (motion == G_80) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: motion mode set to none"); +#endif + settings->motion_mode = G_80; + } else if (is_user_defined_g_code(motion)) { + CHP(convert_remapped_code(block, settings, STEP_MOTION, 'g', motion)); + } else if (is_a_cycle(motion)) { + + CHP(convert_cycle(motion, block, settings)); + } else if ((motion == G_5) || (motion == G_5_1) || (motion == G_6) || (motion == G_6_1) ) { // jjf + write_canon_state_tag(block, settings); + CHP(convert_spline(motion, block, settings)); + } else if ((motion == G_5_2) || (motion == G_6_2)) { // jjf + write_canon_state_tag(block, settings); + CHP(convert_nurbs(motion, block, settings)); + } else if ( + motion == G_70 || + motion == G_71 || motion == G_71_1 || motion == G_71_2 || + motion == G_72 || motion == G_72_1 || motion == G_72_2 + ) { + CHP(convert_g7x(motion, block, settings)); + } else { + ERS(NCE_BUG_UNKNOWN_MOTION_CODE); + } + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_probe + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. No value is given in the block for any of X, Y, or Z: + NCE_X_Y_AND_Z_WORDS_ALL_MISSING_WITH_G38_2 + 3. cutter radius comp is on: NCE_CANNOT_PROBE_WITH_CUTTER_RADIUS_COMP_ON + 4. Feed rate is zero: NCE_CANNOT_PROBE_WITH_ZERO_FEED_RATE + 5. The move is degenerate (already at the specified point) + NCE_START_POINT_TOO_CLOSE_TO_PROBE_POINT + +Side effects: + This executes a straight_probe command. + The probe_flag in the settings is set to true. + The motion mode in the settings is set to G_38_2. + +Called by: convert_motion. + +The approach to operating in incremental distance mode (g91) is to +put the the absolute position values into the block before using the +block to generate a move. + +After probing is performed, the location of the probe cannot be +predicted. This differs from every other command, all of which have +predictable results. The next call to the interpreter (with either +Interp::read or Interp::execute) will result in updating the +current position by calls to get_external_position_x, etc. + +*/ + +int Interp::convert_probe(block_pointer block, //!< pointer to a block of RS274 instructions + int g_code, + setup_pointer settings) //!< pointer to machine settings +{ + double end_x; + double end_y; + double end_z; + double AA_end; + double BB_end; + double CC_end; + double u_end; + double v_end; + double w_end; + + /* probe_type: + ~1 = error if probe operation is unsuccessful (ngc default) + |1 = suppress error, report in # instead + ~2 = move until probe trips (ngc default) + |2 = move until probe clears */ + + unsigned char probe_type = g_code - G_38_2; + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + NCE_CANNOT_PROBE_WITH_CUTTER_RADIUS_COMP_ON); + CHKS((settings->feed_rate == 0.0), NCE_CANNOT_PROBE_WITH_ZERO_FEED_RATE); + CHKS(settings->feed_mode == FEED_MODE::UNITS_PER_REVOLUTION, + _("Cannot probe with feed per rev mode")); + CHP(find_ends(block, settings, &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, + &u_end, &v_end, &w_end)); + CHKS(((!(probe_type & 1)) && + settings->current_x == end_x && settings->current_y == end_y && + settings->current_z == end_z && settings->AA_current == AA_end && + settings->BB_current == BB_end && settings->CC_current == CC_end && + settings->u_current == u_end && settings->v_current == v_end && + settings->w_current == w_end), + NCE_START_POINT_TOO_CLOSE_TO_PROBE_POINT); + + TURN_PROBE_ON(); + STRAIGHT_PROBE(block->line_number, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end, probe_type); + + TURN_PROBE_OFF(); + settings->motion_mode = g_code; + settings->probe_flag = true; + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_retract_mode + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. g_code isn't G_98 or G_99: NCE_BUG_CODE_NOT_G98_OR_G99 + +Side effects: + The interpreter switches the machine settings to indicate the current + retract mode for canned cycles (OLD_Z or R_PLANE). + +Called by: convert_g. + +The canonical machine to which commands are being sent does not have a +retract mode, so no command setting the retract mode is generated in +this function. + +*/ + +int Interp::convert_retract_mode(int g_code, //!< g_code being executed (must be G_98 or G_99) + setup_pointer settings) //!< pointer to machine settings +{ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot change retract mode with cutter radius compensation on"))); + if (g_code == G_98) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: retract mode set to old_z"); +#endif + settings->retract_mode = RETRACT_MODE::OLD_Z; + } else if (g_code == G_99) { +#ifdef DEBUG_EMC + enqueue_COMMENT("interpreter: retract mode set to r_plane"); +#endif + settings->retract_mode = RETRACT_MODE::R_PLANE; + } else + ERS(NCE_BUG_CODE_NOT_G98_OR_G99); + return INTERP_OK; +} + +// G10 L1 P[tool number] R[radius] X[x offset] Z[z offset] Q[orientation] +// G10 L10 P[tool number] R[radius] X[x offset] Z[z offset] Q[orientation] + +int Interp::convert_setup_tool(block_pointer block, setup_pointer settings) { + int idx = -1, toolno; + int q; + double tx, ty, tz, ta, tb, tc, tu, tv, tw; + int direct = block->l_number == 1; + + is_near_int(&toolno, block->p_number); + + CHP((find_tool_index(settings, toolno, &idx))); + + CHKS(!(block->x_flag || block->y_flag || block->z_flag || + block->a_flag || block->b_flag || block->c_flag || + block->u_flag || block->v_flag || block->w_flag || + block->r_flag || block->q_flag || block->i_flag || + block->j_flag), + _("G10 L1 without offsets has no effect")); + + if(direct) { + if(block->x_flag) + settings->tool_table[idx].offset.tran.x = PROGRAM_TO_USER_LEN(block->x_number); + if(block->y_flag) + settings->tool_table[idx].offset.tran.y = PROGRAM_TO_USER_LEN(block->y_number); + if(block->z_flag) + settings->tool_table[idx].offset.tran.z = PROGRAM_TO_USER_LEN(block->z_number); + if(block->a_flag) + settings->tool_table[idx].offset.a = PROGRAM_TO_USER_ANG(block->a_number); + if(block->b_flag) + settings->tool_table[idx].offset.b = PROGRAM_TO_USER_ANG(block->b_number); + if(block->c_flag) + settings->tool_table[idx].offset.c = PROGRAM_TO_USER_ANG(block->c_number); + if(block->u_flag) + settings->tool_table[idx].offset.u = PROGRAM_TO_USER_LEN(block->u_number); + if(block->v_flag) + settings->tool_table[idx].offset.v = PROGRAM_TO_USER_LEN(block->v_number); + if(block->w_flag) + settings->tool_table[idx].offset.w = PROGRAM_TO_USER_LEN(block->w_number); + } else { + int to_fixture = block->l_number == 11; + int destination_system = to_fixture? 9 : settings->origin_index; // maybe 9 (g59.3) should be user configurable? + + find_current_in_system_without_tlo(settings, destination_system, + &tx, &ty, &tz, + &ta, &tb, &tc, + &tu, &tv, &tw); + + if ( to_fixture && settings->parameters[5210]) { + // For G10L11, we don't want to move the origin of the + // fixture according to G92. Since find_current_in_system + // did this for us already, undo it. + tx += USER_TO_PROGRAM_LEN(settings->parameters[5211]); + ty += USER_TO_PROGRAM_LEN(settings->parameters[5212]); + tz += USER_TO_PROGRAM_LEN(settings->parameters[5213]); + ta += USER_TO_PROGRAM_ANG(settings->parameters[5214]); + tb += USER_TO_PROGRAM_ANG(settings->parameters[5215]); + tc += USER_TO_PROGRAM_ANG(settings->parameters[5216]); + tu += USER_TO_PROGRAM_LEN(settings->parameters[5217]); + tv += USER_TO_PROGRAM_LEN(settings->parameters[5218]); + tw += USER_TO_PROGRAM_LEN(settings->parameters[5219]); + } + + + if(block->x_flag && block->y_flag) { + tx -= block->x_number; + ty -= block->y_number; + rotate(&tx, &ty, settings->parameters[5210 + destination_system * 20]); + settings->tool_table[idx].offset.tran.x = PROGRAM_TO_USER_LEN(tx); + settings->tool_table[idx].offset.tran.y = PROGRAM_TO_USER_LEN(ty); + } else if(block->x_flag) { + // keep the component of the tool table's current setting that points + // along our possibly-rotated Y axis + double ox, oy; + ox = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.x); + oy = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.y); + rotate(&ox, &oy, -settings->parameters[5210 + destination_system * 20]); + ox = 0; + rotate(&ox, &oy, settings->parameters[5210 + destination_system * 20]); + + + tx -= block->x_number; + ty = 0; + rotate(&tx, &ty, settings->parameters[5210 + destination_system * 20]); + + settings->tool_table[idx].offset.tran.x = PROGRAM_TO_USER_LEN(tx + ox); + settings->tool_table[idx].offset.tran.y = PROGRAM_TO_USER_LEN(ty + oy); + } else if(block->y_flag) { + double ox, oy; + ox = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.x); + oy = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.y); + rotate(&ox, &oy, -settings->parameters[5210 + destination_system * 20]); + oy = 0; + rotate(&ox, &oy, settings->parameters[5210 + destination_system * 20]); + + ty -= block->y_number; + tx = 0; + + rotate(&tx, &ty, settings->parameters[5210 + destination_system * 20]); + settings->tool_table[idx].offset.tran.x = PROGRAM_TO_USER_LEN(tx + ox); + settings->tool_table[idx].offset.tran.y = PROGRAM_TO_USER_LEN(ty + oy); + } + + + if(block->z_flag) + settings->tool_table[idx].offset.tran.z = PROGRAM_TO_USER_LEN(tz - block->z_number); + if(block->a_flag) + settings->tool_table[idx].offset.a = PROGRAM_TO_USER_ANG(ta - block->a_number); + if(block->b_flag) + settings->tool_table[idx].offset.b = PROGRAM_TO_USER_ANG(tb - block->b_number); + if(block->c_flag) + settings->tool_table[idx].offset.c = PROGRAM_TO_USER_ANG(tc - block->c_number); + if(block->u_flag) + settings->tool_table[idx].offset.u = PROGRAM_TO_USER_LEN(tu - block->u_number); + if(block->v_flag) + settings->tool_table[idx].offset.v = PROGRAM_TO_USER_LEN(tv - block->v_number); + if(block->w_flag) + settings->tool_table[idx].offset.w = PROGRAM_TO_USER_LEN(tw - block->w_number); + } + + if(block->r_flag) settings->tool_table[idx].diameter = PROGRAM_TO_USER_LEN(block->r_number) * 2.; + if(block->i_flag) settings->tool_table[idx].frontangle = block->i_number; + if(block->j_flag) settings->tool_table[idx].backangle = block->j_number; + if(block->q_number != -1.0) { + CHKS((!is_near_int(&q, block->q_number)), _("Q number in G10 is not an integer")); + CHKS((q > 9), _("Invalid tool orientation")); + settings->tool_table[idx].orientation = q; + } + + SET_TOOL_TABLE_ENTRY(idx, + settings->tool_table[idx].toolno, + settings->tool_table[idx].offset, + settings->tool_table[idx].diameter, + settings->tool_table[idx].frontangle, + settings->tool_table[idx].backangle, + settings->tool_table[idx].orientation); + + // + // On non-random tool changers we just updated the tool's "home index" + // in the tool changer carousel, so now, if the tool is currently + // loaded, we need to copy the new tool information to the spindle + // (index 0). This is never needed on random tool changers because + // there tools don't have a home index, and instead we updated index + // 0 (the spindle) directly when modifying the loaded tool. + // + if ((!settings->random_toolchanger) && (settings->current_pocket == idx)) { + settings->tool_table[0] = settings->tool_table[idx]; + } + + // + // Update parameter #5400 with the tool currently in the spindle, or a + // special "invalid tool number" marker if no tool is in the spindle. + // Unfortunately, random and nonrandom toolchangers use a different + // number for "invalid tool number": nonrandom uses 0, random uses -1. + // + if (settings->random_toolchanger) { + if (settings->tool_table[0].toolno >= 0) { + settings->parameters[5400] = settings->tool_table[0].toolno; + } else { + settings->parameters[5400] = -1; + } + } else { + if (settings->tool_table[0].toolno > 0) { + settings->parameters[5400] = settings->tool_table[0].toolno; + } else { + settings->parameters[5400] = 0; + } + } + + // #5401-#5409 reflect the applied tool length offset (G43-family). + // G10 L1/L10/L11 modify the tool table but do not apply offsets to + // motion, so do not update them here. See #2994. + settings->parameters[5410] = settings->tool_table[0].diameter; + settings->parameters[5411] = settings->tool_table[0].frontangle; + settings->parameters[5412] = settings->tool_table[0].backangle; + settings->parameters[5413] = settings->tool_table[0].orientation; + + // if the modified tool is currently in the spindle, then copy its + // information to index 0 of the tool table (which signifies the + // spindle) + if ( !_setup.random_toolchanger + && idx == settings->current_pocket) { + SET_TOOL_TABLE_ENTRY(0, + settings->tool_table[idx].toolno, + settings->tool_table[idx].offset, + settings->tool_table[idx].diameter, + settings->tool_table[idx].frontangle, + settings->tool_table[idx].backangle, + settings->tool_table[idx].orientation); + } + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_setup + +Returned Value: int (INTERP_OK) + +Side effects: + SET_G5X_OFFSET is called, and the coordinate + values for the program origin are reset. + If the program origin is currently in use, the values of the + the coordinates of the current point are updated. + +Called by: convert_modal_0. + +This is called only if g10 is called. g10 L2 may be used to alter the +location of coordinate systems as described in [NCMS, pages 9 - 10] and +[Fanuc, page 65]. [Fanuc] has only six coordinate systems, while +[NCMS] has nine (the first six of which are the same as the six [Fanuc] +has). All nine are implemented here. + +Being in incremental distance mode has no effect on the action of G10 +in this implementation. The manual is not explicit about what is +intended. + +If L is 20 instead of 2, the coordinates are relative to the current +position instead of the origin. Like how G92 offsets are programmed, +the meaning is "set the coordinate system origin such that my current +position becomes the specified value". + +See documentation of convert_coordinate_system for more information. + +*/ + +int Interp::convert_setup(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + double x; + double y; + double z; + double a; + double b; + double c; + double u, v, w; + double r; + double *parameters; + int p_int; + + double cx, cy, cz, ca, cb, cc, cu, cv, cw; + + CHKS((block->i_flag || block->j_flag), _("I J words not allowed with G10 L2")); + + parameters = settings->parameters; + p_int = (int) (block->p_number + 0.0001); + + // if P = 0 then use whatever coordinate system that is currently active + if (p_int == 0) { + p_int = settings->origin_index; + } + + CHKS((block->l_number == 20 && block->a_flag && settings->a_axis_wrapped && + (block->a_number <= -360.0 || block->a_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), block->a_number, 'A'); + CHKS((block->l_number == 20 && block->b_flag && settings->b_axis_wrapped && + (block->b_number <= -360.0 || block->b_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), block->b_number, 'B'); + CHKS((block->l_number == 20 && block->c_flag && settings->c_axis_wrapped && + (block->c_number <= -360.0 || block->c_number >= 360.0)), + (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), block->c_number, 'C'); + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF && p_int == settings->origin_index), + (_("Cannot change the active coordinate system with cutter radius compensation on"))); + + find_current_in_system(settings, p_int, + &cx, &cy, &cz, + &ca, &cb, &cc, + &cu, &cv, &cw); + + if (block->r_flag) { + CHKS((block->l_number == 20), _("R not allowed in G10 L20")); + r = block->r_number; + parameters[5210 + (p_int * 20)] = r; + } else + r = parameters[5210 + (p_int * 20)]; + + if (block->l_number == 20) { + // old position in rotated system + double oldx = cx, oldy = cy; + + // find new desired position in rotated system + x = cx; + y = cy; + + if (block->x_flag) { + x = block->x_number; + } + if (block->y_flag) { + y = block->y_number; + } + + // move old current position into the unrotated system + rotate(&oldx, &oldy, r); + // move desired position into the unrotated system + rotate(&x, &y, r); + + // find new offset + x = oldx + USER_TO_PROGRAM_LEN(parameters[5201 + (p_int * 20)]) - x; + y = oldy + USER_TO_PROGRAM_LEN(parameters[5202 + (p_int * 20)]) - y; + + // parameters are not rotated + parameters[5201 + (p_int * 20)] = PROGRAM_TO_USER_LEN(x); + parameters[5202 + (p_int * 20)] = PROGRAM_TO_USER_LEN(y); + + if (p_int == settings->origin_index) { + // let the code below fix up the current coordinates correctly + rotate(&settings->current_x, &settings->current_y, settings->rotation_xy); + settings->rotation_xy = 0; + } + } else { + if (block->x_flag) { + x = block->x_number; + parameters[5201 + (p_int * 20)] = PROGRAM_TO_USER_LEN(x); + } else { + x = USER_TO_PROGRAM_LEN(parameters[5201 + (p_int * 20)]); + } + if (block->y_flag) { + y = block->y_number; + parameters[5202 + (p_int * 20)] = PROGRAM_TO_USER_LEN(y); + } else { + y = USER_TO_PROGRAM_LEN(parameters[5202 + (p_int * 20)]); + } + } + + if (block->z_flag) { + z = block->z_number; + if (block->l_number == 20) z = cz + USER_TO_PROGRAM_LEN(parameters[5203 + (p_int * 20)]) - z; + parameters[5203 + (p_int * 20)] = PROGRAM_TO_USER_LEN(z); + } else + z = USER_TO_PROGRAM_LEN(parameters[5203 + (p_int * 20)]); + + if (block->a_flag) { + a = block->a_number; + if (block->l_number == 20) a = ca + USER_TO_PROGRAM_ANG(parameters[5204 + (p_int * 20)]) - a; + parameters[5204 + (p_int * 20)] = PROGRAM_TO_USER_ANG(a); + } else + a = USER_TO_PROGRAM_ANG(parameters[5204 + (p_int * 20)]); + + if (block->b_flag) { + b = block->b_number; + if (block->l_number == 20) b = cb + USER_TO_PROGRAM_ANG(parameters[5205 + (p_int * 20)]) - b; + parameters[5205 + (p_int * 20)] = PROGRAM_TO_USER_ANG(b); + } else + b = USER_TO_PROGRAM_ANG(parameters[5205 + (p_int * 20)]); + + if (block->c_flag) { + c = block->c_number; + if (block->l_number == 20) c = cc + USER_TO_PROGRAM_ANG(parameters[5206 + (p_int * 20)]) - c; + parameters[5206 + (p_int * 20)] = PROGRAM_TO_USER_ANG(c); + } else + c = USER_TO_PROGRAM_ANG(parameters[5206 + (p_int * 20)]); + + if (block->u_flag) { + u = block->u_number; + if (block->l_number == 20) u = cu + USER_TO_PROGRAM_LEN(parameters[5207 + (p_int * 20)]) - u; + parameters[5207 + (p_int * 20)] = PROGRAM_TO_USER_LEN(u); + } else + u = USER_TO_PROGRAM_LEN(parameters[5207 + (p_int * 20)]); + + if (block->v_flag) { + v = block->v_number; + if (block->l_number == 20) v = cv + USER_TO_PROGRAM_LEN(parameters[5208 + (p_int * 20)]) - v; + parameters[5208 + (p_int * 20)] = PROGRAM_TO_USER_LEN(v); + } else + v = USER_TO_PROGRAM_LEN(parameters[5208 + (p_int * 20)]); + + if (block->w_flag) { + w = block->w_number; + if (block->l_number == 20) w = cw + USER_TO_PROGRAM_LEN(parameters[5209 + (p_int * 20)]) - w; + parameters[5209 + (p_int * 20)] = PROGRAM_TO_USER_LEN(w); + } else + w = USER_TO_PROGRAM_LEN(parameters[5209 + (p_int * 20)]); + + if (p_int == settings->origin_index) { /* system is currently used */ + + rotate(&settings->current_x, &settings->current_y, settings->rotation_xy); + + settings->current_x += settings->origin_offset_x; + settings->current_y += settings->origin_offset_y; + settings->current_z += settings->origin_offset_z; + settings->AA_current += settings->AA_origin_offset; + settings->BB_current += settings->BB_origin_offset; + settings->CC_current += settings->CC_origin_offset; + settings->u_current += settings->u_origin_offset; + settings->v_current += settings->v_origin_offset; + settings->w_current += settings->w_origin_offset; + + settings->origin_offset_x = x; + settings->origin_offset_y = y; + settings->origin_offset_z = z; + settings->AA_origin_offset = a; + settings->BB_origin_offset = b; + settings->CC_origin_offset = c; + settings->u_origin_offset = u; + settings->v_origin_offset = v; + settings->w_origin_offset = w; + + settings->current_x -= x; + settings->current_y -= y; + settings->current_z -= z; + settings->AA_current -= a; + settings->BB_current -= b; + settings->CC_current -= c; + settings->u_current -= u; + settings->v_current -= v; + settings->w_current -= w; + + SET_G5X_OFFSET(p_int, x, y, z, a, b, c, u, v, w); + + rotate(&settings->current_x, &settings->current_y, - r); + settings->rotation_xy = r; + SET_XY_ROTATION(settings->rotation_xy); + + } +#ifdef DEBUG_EMC + else + enqueue_COMMENT("interpreter: setting coordinate system origin"); +#endif + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_set_plane + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The user tries to change to a different plane while comp is on: + NCE_CANNOT_CHANGE_PLANES_WITH_CUTTER_RADIUS_COMP_ON); + 2. The g_code is not G_17, G_18, or G_19: + NCE_BUG_CODE_NOT_G17_G18_OR_G19 + +Side effects: + A canonical command setting the current plane is executed. + +Called by: convert_g. + +*/ + +int Interp::convert_set_plane(int g_code, //!< must be G_17, G_18, or G_19 + setup_pointer settings) //!< pointer to machine settings +{ + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF && g_code == G_17 && settings->plane != CANON_PLANE::XY), + NCE_CANNOT_CHANGE_PLANES_WITH_CUTTER_RADIUS_COMP_ON); + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF && g_code == G_18 && settings->plane != CANON_PLANE::XZ), + NCE_CANNOT_CHANGE_PLANES_WITH_CUTTER_RADIUS_COMP_ON); + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF && g_code == G_19 && settings->plane != CANON_PLANE::YZ), + NCE_CANNOT_CHANGE_PLANES_WITH_CUTTER_RADIUS_COMP_ON); + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF && g_code == G_19), + NCE_RADIUS_COMP_ONLY_IN_XY_OR_XZ); + + if (g_code == G_17) { + SELECT_PLANE(CANON_PLANE::XY); + settings->plane = CANON_PLANE::XY; + } else if (g_code == G_18) { + SELECT_PLANE(CANON_PLANE::XZ); + settings->plane = CANON_PLANE::XZ; + } else if (g_code == G_19) { + SELECT_PLANE(CANON_PLANE::YZ); + settings->plane = CANON_PLANE::YZ; + } else if (g_code == G_17_1) { + SELECT_PLANE(CANON_PLANE::UV); + settings->plane = CANON_PLANE::UV; + } else if (g_code == G_18_1) { + SELECT_PLANE(CANON_PLANE::UW); + settings->plane = CANON_PLANE::UW; + } else if (g_code == G_19_1) { + SELECT_PLANE(CANON_PLANE::VW); + settings->plane = CANON_PLANE::VW; + } else + ERS(NCE_BUG_CODE_NOT_G17_G18_OR_G19); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_speed + +Returned Value: int (INTERP_OK) + +Side effects: + The machine spindle speed is set to the value of s_number in the + block by a call to SET_SPINDLE_SPEED. + The machine model for spindle speed is set to that value. + +Called by: execute_block. + +*/ + +int Interp::convert_speed(int spindle, //The spindle ($) or -1 if none + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings)//!< pointer to machine settings +{ + if (spindle >= 0 && spindle < settings->num_spindles){ + enqueue_SET_SPINDLE_SPEED(spindle, block->s_number); + settings->speed[spindle] = block->s_number; +} + + return INTERP_OK; +} + +int Interp::convert_spindle_mode(int dollar_number, block_pointer block, setup_pointer settings) +{ + for (int s = 0; s < settings->num_spindles; s++){ + if (dollar_number == -1 || s == dollar_number){ + if(block->g_modes[GM_SPINDLE_MODE] == G_97) { + settings->spindle_mode[s] = SPINDLE_MODE::CONSTANT_RPM; + enqueue_SET_SPINDLE_MODE(s, 0); + } else { /* G_96 */ + settings->spindle_mode[s] = SPINDLE_MODE::CONSTANT_SURFACE; + if(block->d_flag) + enqueue_SET_SPINDLE_MODE(s, fabs(block->d_number_float)); + else + enqueue_SET_SPINDLE_MODE(s, 1e30); + } + } + } + return INTERP_OK; +} +/****************************************************************************/ + +/*! convert_stop + +Returned Value: int + When an m2 or m30 (program_end) is encountered, this returns INTERP_EXIT. + M99 main program endless loop: + if looping is disabled (default, not in task), return INTERP_EXIT; + else in task, return INTERP_EXECUTE_FINISH. + M99 return from subprogram is not handled here, and raises an error. + If the code is not m0, m1, m2, m30, m60, or m99 this returns + NCE_BUG_CODE_NOT_M0_M1_M2_M30_M60_M99 + Otherwise, it returns INTERP_OK. + +Side effects: + An m0, m1, m2, m30, m60 or m99 in the block is executed. + + For m0, m1, and m60, this makes a function call to the PROGRAM_STOP + canonical machining function (which stops program execution). + In addition, m60 calls PALLET_SHUTTLE. + + For m2 and m30, this resets the machine and then calls PROGRAM_END. + In addition, m30 calls PALLET_SHUTTLE. + Clear g92 offset if DISABLE_G92_PERSISTENCE is set in the .ini file. + + For m99 main program endless looping in task, this returns control + to the beginning of the file and outputs any linked segments to the + interp list. The INTERP_EXECUTE_FINISH return code causes any + commands in the interp list to be issued so that the endless loop + doesn't result in an infinite queue. + + For m99 main program endless looping elsewhere, especially preview + where endless looping is not desired, this behaves as m2 and m30 + below. + +Called by: execute_block. + +This handles stopping or ending the program (m0, m1, m2, m30, m60) + +[NCMS] specifies how the following modes should be reset at m2 or +m30. The descriptions are not collected in one place, so this list +may be incomplete. + +G52 offsetting coordinate zero points [NCMS, page 10] +G92 coordinate offset using tool position [NCMS, page 10] + +The following should have reset values, but no description of reset +behavior could be found in [NCMS]. +G17, G18, G19 selected plane [NCMS, pages 14, 20] +G90, G91 distance mode [NCMS, page 15] +G93, G94 feed mode [NCMS, pages 35 - 37] +M48, M49 overrides enabled, disabled [NCMS, pages 37 - 38] +M3, M4, M5 spindle turning [NCMS, page 7] + +The following should be set to some value at machine start-up but +not automatically reset by any of the stopping codes. +1. G20, G21 length units [NCMS, page 15]. This is up to the installer. +2. motion_control_mode. This is set in Interp::init but not reset here. + Might add it here. + +The following resets have been added by calling the appropriate +canonical machining command and/or by resetting interpreter +settings. They occur on M2 or M30. + +1. origin offsets are set to the default (like G54) +2. Selected plane is set to CANON_PLANE::XY (like G17) - SELECT_PLANE +3. Distance mode is set to DISTANCE_MODE::ABSOLUTE (like G90) - no canonical call +4. Feed mode is set to UNITS_PER_MINUTE (like G94) - no canonical call +5. Feed and speed overrides are set to true (like M48) - ENABLE_FEED_OVERRIDE + - ENABLE_SPEED_OVERRIDE +6. Cutter compensation is turned off (like G40) - no canonical call +7. The spindle is stopped (like M5) - STOP_SPINDLE_TURNING +8. The motion mode is set to G_1 (like G1) - no canonical call +9. Coolant is turned off (like M9) - FLOOD_OFF & MIST_OFF +10. G52/G92 is cleared if DISABLE_G92_PERSISTENCE is set in the .ini file + +*/ + +int Interp::convert_stop(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int index; + char *line; + int length; + + double cx = 0, cy = 0, cz = 0; + comp_get_current(settings, &cx, &cy, &cz); + CHP(move_endpoint_and_flush(settings, cx, cy)); + dequeue_canons(settings); + + nurbs_reset_global_variables(); // jjf + + // M99 as subroutine return is handled in interp_o_word.cc + // convert_control_functions() + CHKS((block->m_modes[4] == 99 && settings->call_level > 0), + (_("Bug: Reached convert_stop() from M99 as subprogram return"))); + if (block->m_modes[4] == 0) { + PROGRAM_STOP(); + } else if (block->m_modes[4] == 60) { + PALLET_SHUTTLE(); + PROGRAM_STOP(); + } else if (block->m_modes[4] == 1) { + OPTIONAL_PROGRAM_STOP(); + } else if (block->m_modes[4] == 99 && _setup.loop_on_main_m99) { + + // Fanuc-style M99 main program endless loop + logDebug("M99 main program endless loop"); + + loop_to_beginning(settings); // return control to beginning of file + FINISH(); // Output any final linked segments + return INTERP_EXECUTE_FINISH; // tell task to issue any queued commands + } else if ((block->m_modes[4] == 2) || (block->m_modes[4] == 30) || + (block->m_modes[4] == 99 && !_setup.loop_on_main_m99) + ) { /* reset stuff here */ + +/*1*/ + rotate(&settings->current_x, &settings->current_y, settings->rotation_xy); + settings->current_x += settings->origin_offset_x; + settings->current_y += settings->origin_offset_y; + settings->current_z += settings->origin_offset_z; + settings->AA_current += settings->AA_origin_offset; + settings->BB_current += settings->BB_origin_offset; + settings->CC_current += settings->CC_origin_offset; + settings->u_current += settings->u_origin_offset; + settings->v_current += settings->v_origin_offset; + settings->w_current += settings->w_origin_offset; + + settings->origin_index = 1; + settings->parameters[5220] = 1.0; + settings->origin_offset_x = USER_TO_PROGRAM_LEN(settings->parameters[5221]); + settings->origin_offset_y = USER_TO_PROGRAM_LEN(settings->parameters[5222]); + settings->origin_offset_z = USER_TO_PROGRAM_LEN(settings->parameters[5223]); + settings->AA_origin_offset = USER_TO_PROGRAM_ANG(settings->parameters[5224]); + settings->BB_origin_offset = USER_TO_PROGRAM_ANG(settings->parameters[5225]); + settings->CC_origin_offset = USER_TO_PROGRAM_ANG(settings->parameters[5226]); + settings->u_origin_offset = USER_TO_PROGRAM_LEN(settings->parameters[5227]); + settings->v_origin_offset = USER_TO_PROGRAM_LEN(settings->parameters[5228]); + settings->w_origin_offset = USER_TO_PROGRAM_LEN(settings->parameters[5229]); + settings->rotation_xy = settings->parameters[5230]; + + settings->current_x -= settings->origin_offset_x; + settings->current_y -= settings->origin_offset_y; + settings->current_z -= settings->origin_offset_z; + settings->AA_current -= settings->AA_origin_offset; + settings->BB_current -= settings->BB_origin_offset; + settings->CC_current -= settings->CC_origin_offset; + settings->u_current -= settings->u_origin_offset; + settings->v_current -= settings->v_origin_offset; + settings->w_current -= settings->w_origin_offset; + rotate(&settings->current_x, &settings->current_y, -settings->rotation_xy); + + SET_G5X_OFFSET(settings->origin_index, + settings->origin_offset_x, + settings->origin_offset_y, + settings->origin_offset_z, + settings->AA_origin_offset, + settings->BB_origin_offset, + settings->CC_origin_offset, + settings->u_origin_offset, + settings->v_origin_offset, + settings->w_origin_offset); + SET_XY_ROTATION(settings->rotation_xy); + +/*2*/ if (settings->plane != CANON_PLANE::XY) { + SELECT_PLANE(CANON_PLANE::XY); + settings->plane = CANON_PLANE::XY; + } + +/*3*/ + settings->distance_mode = DISTANCE_MODE::ABSOLUTE; + +/*4*/ settings->feed_mode = FEED_MODE::UNITS_PER_MINUTE; + SET_FEED_MODE(0, 0); + settings->feed_rate = block->f_number; + SET_FEED_RATE(0); + +/*5*/ if (!settings->feed_override) { + ENABLE_FEED_OVERRIDE(); + settings->feed_override = true; + } + +/*6*/ + settings->cutter_comp_side = CUTTER_COMP::OFF; + settings->cutter_comp_firstmove = true; + +/*7*/ + for (int s = 0; s < settings->num_spindles; s++){ + STOP_SPINDLE_TURNING(s); + settings->spindle_turning[s] = CANON_STOPPED; + + settings->speed_override[s] = true; + /* turn off FPR */ + SET_SPINDLE_MODE(s, 0); + } + +/*8*/ settings->motion_mode = G_1; + +/*9*/ if (settings->mist) { + MIST_OFF(); + settings->mist = false; + } + if (settings->flood) { + FLOOD_OFF(); + settings->flood = false; + } + +/*10*/ + if (settings->disable_g92_persistence) + // Clear G92/G52 offset + for (index=5210; index<=5219; index++) + settings->parameters[index] = 0; + + if (block->m_modes[4] == 30) + PALLET_SHUTTLE(); + PROGRAM_END(); + if (_setup.percent_flag && _setup.file_pointer) { + line = _setup.linetext; + for (;;) { /* check for ending percent sign and comment if missing */ + if (fgets(line, LINELEN, _setup.file_pointer) == NULL) { + enqueue_COMMENT("interpreter: percent sign missing from end of file"); + break; + } + length = strlen(line); + if (length == (LINELEN - 1)) { // line is too long. need to finish reading the line + for (; fgetc(_setup.file_pointer) != '\n' && !feof(_setup.file_pointer);); + continue; + } + for (index = (length - 1); // index set on last char + (index >= 0) && (isspace(line[index])); index--); + if (line[index] == '%') // found line with % at end + { + for (index--; (index >= 0) && (isspace(line[index])); index--); + if (index == -1) // found line with only percent sign + break; + } + } + } + unwind_call(INTERP_EXIT, __FILE__,__LINE__,__FUNCTION__); + return INTERP_EXIT; + } else + ERS(NCE_BUG_CODE_NOT_M0_M1_M2_M30_M60_M99); + return INTERP_OK; +} + +/*************************************************************************** */ + +/*! convert_straight + +Returned Value: int + If convert_straight_comp1 or convert_straight_comp2 is called + and returns an error code, this returns that code. + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. The value of move is not G_0 or G_1: + NCE_BUG_CODE_NOT_G0_OR_G1 + 2. A straight feed (g1) move is called with feed rate set to 0: + NCE_CANNOT_DO_G1_WITH_ZERO_FEED_RATE + 3. A straight feed (g1) move is called with inverse time feed in effect + but no f word (feed time) is provided: + NCE_F_WORD_MISSING_WITH_INVERSE_TIME_G1_MOVE + 4. A move is called with G53 and cutter radius compensation on: + NCE_CANNOT_USE_G53_WITH_CUTTER_RADIUS_COMP + 5. A G33 move is called without the necessary support compiled in: + NCE_G33_NOT_SUPPORTED + +Side effects: + This executes a STRAIGHT_FEED command at cutting feed rate + (if move is G_1) or a STRAIGHT_TRAVERSE command (if move is G_0). + It also updates the setting of the position of the tool point to the + end point of the move. If cutter radius compensation is on, it may + also generate an arc before the straight move. Also, in INVERSE_TIME + feed mode, SET_FEED_RATE will be called the feed rate setting changed. + +Called by: convert_motion. + +The approach to operating in incremental distance mode (g91) is to +put the the absolute position values into the block before using the +block to generate a move. + +If the destination point is the same as the current point, the feed rate +will be calculated as zero, so a default of 0.1 is applied in this case +(It doesn't matter how wrong the feed rate is on a zero-length move) + +If cutter compensation is in use, the path's length may increase or +decrease. Also an arc may be added, to go around a corner, before the +straight move. For the purpose of calculating the feed rate when in +inverse time mode, this length increase or decrease is ignored. The +feed is still set to the original programmed straight length divided by +the F number (with the above lower bound). The new arc (if needed) and +the new longer or shorter straight move are taken at this feed. + +*/ + +int Interp::convert_straight(int move, //!< either G_0 or G_1 + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double end_x; + double end_y; + double end_z; + double AA_end; + double BB_end; + double CC_end; + double u_end, v_end, w_end; + int status; + + settings->arc_not_allowed = false; + + if (move == G_1) { + if (settings->feed_mode == FEED_MODE::UNITS_PER_MINUTE) { + CHKS((settings->feed_rate == 0.0), NCE_CANNOT_DO_G1_WITH_ZERO_FEED_RATE); + } else if (settings->feed_mode == FEED_MODE::UNITS_PER_REVOLUTION) { + CHKS((settings->feed_rate == 0.0), NCE_CANNOT_DO_G1_WITH_ZERO_FEED_RATE); + CHKS((settings->speed[settings->active_spindle] == 0.0), + (_("Cannot feed with zero spindle speed in feed per rev mode"))); + } else if (settings->feed_mode == FEED_MODE::INVERSE_TIME) { + CHKS((!block->f_flag), + NCE_F_WORD_MISSING_WITH_INVERSE_TIME_G1_MOVE); + } + } + + settings->motion_mode = move; + CHP(find_ends(block, settings, &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, &u_end, &v_end, &w_end)); + + if (move == G_1) { + inverse_time_rate_straight(end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end, + block, settings); + } + + // Create a state tag and dump it to canon + write_canon_state_tag(block, settings); + + if ((settings->cutter_comp_side != CUTTER_COMP::OFF) && /* ! "== true" */ + (settings->cutter_comp_radius > 0.0)) { /* radius always is >= 0 */ + + CHKS((block->g_modes[GM_MODAL_0] == G_53), + NCE_CANNOT_USE_G53_WITH_CUTTER_RADIUS_COMP); + + if(settings->plane == CANON_PLANE::XZ) { + if (settings->cutter_comp_firstmove) + status = convert_straight_comp1(move, block, settings, end_z, end_x, end_y, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + else + status = convert_straight_comp2(move, block, settings, end_z, end_x, end_y, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + } else if(settings->plane == CANON_PLANE::XY) { + if (settings->cutter_comp_firstmove) + status = convert_straight_comp1(move, block, settings, end_x, end_y, end_z, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + else + status = convert_straight_comp2(move, block, settings, end_x, end_y, end_z, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + } else ERS("BUG: Invalid plane for cutter compensation"); + CHP(status); + } else if (move == G_0) { + tag_straight(block,end_x, end_y); // Update the heading and clear arc data for ANY straight move + STRAIGHT_TRAVERSE(block->line_number, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + } else if (move == G_1) { + tag_straight(block,end_x, end_y); // Update the heading and clear arc data for ANY straight move + STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + } else if (move == G_33) { + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G33 move"))); + settings->active_spindle = (int)block->dollar_number; + } + CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && + (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), + _("Spindle not turning in G33")); + START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); + STRAIGHT_FEED(block->line_number, end_x, end_y, end_z, AA_end, BB_end, CC_end, u_end, v_end, w_end); + STOP_SPEED_FEED_SYNCH(); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + } else if (move == G_33_1) { + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G33.1 move"))); + settings->active_spindle = (int)block->dollar_number; + } + CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && + (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), + _("Spindle not turning in G33.1")); + START_SPEED_FEED_SYNCH(settings->active_spindle, block->k_number, 0); + double scale = 1; + if(block->i_flag){ + scale = block->i_number; + if(scale < 1){ + scale = 1; + } + } + RIGID_TAP(block->line_number, end_x, end_y, end_z, scale); + STOP_SPEED_FEED_SYNCH(); + // after the RIGID_TAP cycle we'll be in the same spot + } else if (move == G_76) { + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid D-number in G76 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CHKS(((settings->spindle_turning[settings->active_spindle] != CANON_CLOCKWISE) && + (settings->spindle_turning[settings->active_spindle] != CANON_COUNTERCLOCKWISE)), + _("Chosen spindle (%i) not turning in G76"), settings->active_spindle); + CHKS((settings->AA_current != AA_end || + settings->BB_current != BB_end || + settings->CC_current != CC_end || + settings->u_current != u_end || + settings->v_current != v_end || + settings->w_current != w_end), NCE_CANNOT_MOVE_ROTARY_AXES_WITH_G76); + int result = convert_threading_cycle(block, settings, end_x, end_y, end_z); + if(result != INTERP_OK) return result; + } else + ERS(NCE_BUG_CODE_NOT_G0_OR_G1); + + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + return INTERP_OK; +} + +int Interp::convert_straight_indexer(int axis, int jnum, block_pointer block, setup_pointer settings) { + double end_x, end_y, end_z; + double AA_end, BB_end, CC_end; + double u_end, v_end, w_end; + + find_ends(block, settings, &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, &u_end, &v_end, &w_end); + + CHKS((end_x != settings->current_x || + end_y != settings->current_y || + end_z != settings->current_z || + u_end != settings->u_current || + v_end != settings->v_current || + w_end != settings->w_current || + (axis != 3 && AA_end != settings->AA_current) || + (axis != 4 && BB_end != settings->BB_current) || + (axis != 5 && CC_end != settings->CC_current)), + _("BUG: An axis incorrectly moved along with an indexer")); + + switch(axis) { + case 3: + issue_straight_index(axis, jnum, AA_end, block->line_number, settings); + break; + case 4: + issue_straight_index(axis, jnum, BB_end, block->line_number, settings); + break; + case 5: + issue_straight_index(axis, jnum, CC_end, block->line_number, settings); + break; + default: + ERS((_("BUG: trying to index incorrect axis"))); + } + return INTERP_OK; +} + +int Interp::issue_straight_index(int axis, int jnum, double target, int lineno, setup_pointer settings) { + CANON_MOTION_MODE save_mode; + double save_tolerance, save_cam_tolerance; + // temporarily switch to exact stop mode for indexing move + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + double AA_end = axis == 3? target: settings->AA_current; + double BB_end = axis == 4? target: settings->BB_current; + double CC_end = axis == 5? target: settings->CC_current; + + // tell canon that this is a special indexing move + UNLOCK_ROTARY(lineno, jnum); + STRAIGHT_TRAVERSE(lineno, settings->current_x, settings->current_y, settings->current_z, + AA_end, BB_end, CC_end, + settings->u_current, settings->v_current, settings->w_current); + LOCK_ROTARY(lineno, jnum); + + // restore path mode + if(save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + return INTERP_OK; +} + + +#define AABBCC settings->AA_current, settings->BB_current, settings->CC_current, settings->u_current, settings->v_current, settings->w_current + +// make one threading pass. only called from convert_threading_cycle. +static void +threading_pass(setup_pointer settings, block_pointer block, + int boring, double safe_x, double depth, double end_depth, + double start_y, double start_z, double zoff, double taper_dist, + int entry_taper, int exit_taper, double taper_pitch, + double pitch, double full_threadheight, double target_z) { + STRAIGHT_TRAVERSE(block->line_number, boring? + safe_x + depth - end_depth: + safe_x - depth + end_depth, + start_y, start_z - zoff, AABBCC); //back + if(taper_dist && entry_taper) { + DISABLE_FEED_OVERRIDE(); + START_SPEED_FEED_SYNCH(settings->active_spindle, taper_pitch, 0); + STRAIGHT_FEED(block->line_number, boring? + safe_x + depth - full_threadheight: + safe_x - depth + full_threadheight, + start_y, start_z - zoff, AABBCC); //in + STRAIGHT_FEED(block->line_number, boring? safe_x + depth: safe_x - depth, //angled in + start_y, start_z - zoff - taper_dist, AABBCC); + START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0); + } else { + STRAIGHT_TRAVERSE(block->line_number, boring? safe_x + depth: safe_x - depth, + start_y, start_z - zoff, AABBCC); //in + DISABLE_FEED_OVERRIDE(); + START_SPEED_FEED_SYNCH(settings->active_spindle, pitch, 0); + } + + if(taper_dist && exit_taper) { + STRAIGHT_FEED(block->line_number, boring? safe_x + depth: safe_x - depth, //over + start_y, target_z - zoff + taper_dist, AABBCC); + START_SPEED_FEED_SYNCH(settings->active_spindle, taper_pitch, 0); + STRAIGHT_FEED(block->line_number, boring? + safe_x + depth - full_threadheight: + safe_x - depth + full_threadheight, + start_y, target_z - zoff, AABBCC); //angled out + } else { + STRAIGHT_FEED(block->line_number, boring? safe_x + depth: safe_x - depth, + start_y, target_z - zoff, AABBCC); //over + } + STOP_SPEED_FEED_SYNCH(); + STRAIGHT_TRAVERSE(block->line_number, boring? + safe_x + depth - end_depth: + safe_x - depth + end_depth, + start_y, target_z - zoff, AABBCC); //out + ENABLE_FEED_OVERRIDE(); +} + +int Interp::convert_threading_cycle(block_pointer block, + setup_pointer settings, + double end_x, double end_y, double end_z) { + + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot use G76 threading cycle with cutter radius compensation on"))); + + CHKS((block->i_number == 0), + (_("In G76, I must not be 0"))); + CHKS((block->j_number <= 0), + (_("In G76, J must be greater than 0"))); + CHKS((block->k_number <= block->j_number), + (_("In G76, K must be greater than J"))); + + double start_x = settings->current_x; + double start_y = settings->current_y; + double start_z = settings->current_z; + + double i_number = block->i_number; + double j_number = block->j_number; + double k_number = block->k_number; + + if(_setup.lathe_diameter_mode){ + i_number /= 2; + j_number /= 2; + k_number /= 2; + } + + + int boring = 0; + + if (i_number > 0.0) + boring = 1; + + double safe_x = start_x; + double full_dia_depth = fabs(i_number); + double start_depth = fabs(i_number) + fabs(j_number); + double cut_increment = fabs(j_number); + double full_threadheight = fabs(k_number); + double end_depth = fabs(k_number) + fabs(i_number); + + double pitch = block->p_number; + double compound_angle = block->q_number; + if(compound_angle == -1) compound_angle = 0; + compound_angle *= M_PIl/180.0; + if(end_z > start_z) compound_angle = -compound_angle; + + int spring_cuts = block->h_flag ? block->h_number: 0; + + double degression = block->r_number; + if(degression < 1.0 || !block->r_flag) degression = 1.0; + + double taper_dist = block->e_flag? block->e_number: 0.0; + if(taper_dist < 0.0) taper_dist = 0.0; + double taper_pitch = taper_dist > 0.0? + pitch * hypot(taper_dist, full_threadheight)/taper_dist: pitch; + + if(end_z > start_z) taper_dist = -taper_dist; + + int taper_flags = block->l_number; + if(taper_flags < 0) taper_flags = 0; + + int entry_taper = taper_flags & 1; + int exit_taper = taper_flags & 2; + + double depth, zoff; + int pass = 1; + + double target_z = end_z + fabs(k_number) * tan(compound_angle); + + depth = start_depth; + zoff = (depth - full_dia_depth) * tan(compound_angle); + while (depth < end_depth) { + threading_pass(settings, block, boring, safe_x, depth, end_depth, start_y, + start_z, zoff, taper_dist, entry_taper, exit_taper, + taper_pitch, pitch, full_threadheight, target_z); + depth = full_dia_depth + cut_increment * pow(++pass, 1.0/degression); + zoff = (depth - full_dia_depth) * tan(compound_angle); + } + // full specified depth now + depth = end_depth; + zoff = (depth - full_dia_depth) * tan(compound_angle); + // cut at least once -- more if spring cuts. + for(int i = 0; iline_number, end_x, end_y, end_z, AABBCC); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; +#undef AABBC + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! convert_straight_comp1 + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. The side is not RIGHT or LEFT: + NCE_BUG_SIDE_NOT_RIGHT_OR_LEFT + 2. The destination tangent point is not more than a tool radius + away (indicating gouging): NCE_CUTTER_GOUGING_WITH_CUTTER_RADIUS_COMP + 3. The value of move is not G_0 or G_1 + NCE_BUG_CODE_NOT_G0_OR_G1 + +Side effects: + This executes a STRAIGHT_MOVE command at cutting feed rate + or a STRAIGHT_TRAVERSE command. + It also updates the setting of the position of the tool point + to the end point of the move and updates the programmed point. + +Called by: convert_straight. + +This is called if cutter radius compensation is on and +settings->cutter_comp_firstmove is true, indicating that this is the +first move after cutter radius compensation is turned on. + +The algorithm used here for determining the path is to draw a straight +line from the destination point which is tangent to a circle whose +center is at the current point and whose radius is the radius of the +cutter. The destination point of the cutter tip is then found as the +center of a circle of the same radius tangent to the tangent line at +the destination point. + +*/ + +int Interp::convert_straight_comp1(int move, //!< either G_0 or G_1 + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings, //!< pointer to machine settings + double px, //!< X coordinate of end point + double py, //!< Y coordinate of end point + double pz, //!< Z coordinate of end point + double AA_end, //!< A coordinate of end point + double BB_end, //!< B coordinate of end point + double CC_end, //!< C coordinate of end point + double u_end, double v_end, double w_end) +{ + double alpha; + double distance; + double radius = settings->cutter_comp_radius; /* always will be positive */ + double end_x, end_y; + + CUTTER_COMP side = settings->cutter_comp_side; + double cx, cy, cz; + + comp_get_current(settings, &cx, &cy, &cz); + distance = hypot((px - cx), (py - cy)); + + CHKS(((side != CUTTER_COMP::LEFT) && (side != CUTTER_COMP::RIGHT)), NCE_BUG_SIDE_NOT_RIGHT_OR_LEFT); + CHKS((distance <= radius), _("Length of cutter compensation entry move is not greater than the tool radius")); + + alpha = atan2(py - cy, px - cx) + (side == CUTTER_COMP::LEFT ? M_PIl/2. : -M_PIl/2.); + + end_x = (px + (radius * cos(alpha))); + end_y = (py + (radius * sin(alpha))); + + // with these moves we don't need to record the direction vector. + // they cannot get reversed because they are guaranteed to be long + // enough. + + set_endpoint(cx, cy); + + if (move == G_0) { + enqueue_STRAIGHT_TRAVERSE(settings, block->line_number, + cos(alpha), sin(alpha), 0, + end_x, end_y, pz, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + } + else if (move == G_1) { + enqueue_STRAIGHT_FEED(settings, block->line_number, + cos(alpha), sin(alpha), 0, + end_x, end_y, pz, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + } else + ERS(NCE_BUG_CODE_NOT_G0_OR_G1); + + settings->cutter_comp_firstmove = false; + + comp_set_current(settings, end_x, end_y, pz); + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + comp_set_programmed(settings, px, py, pz); + return INTERP_OK; +} +/****************************************************************************/ + +/*! convert_straight_comp2 + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. The compensation side is not RIGHT or LEFT: + NCE_BUG_SIDE_NOT_RIGHT_OR_LEFT + 2. A concave corner is found: + NCE_CONCAVE_CORNER_WITH_CUTTER_RADIUS_COMP + +Side effects: + This executes a STRAIGHT_FEED command at cutting feed rate + or a STRAIGHT_TRAVERSE command. + It also generates an ARC_FEED to go around a corner, if necessary. + It also updates the setting of the position of the tool point to + the end point of the move and updates the programmed point. + +Called by: convert_straight. + +This is called if cutter radius compensation is on and +settings->cutter_comp_firstmove is not true, indicating that this is not +the first move after cutter radius compensation is turned on. + +The algorithm used here is: +1. Determine the direction of the last motion. This is done by finding + the direction of the line from the last programmed point to the + current tool tip location. This line is a radius of the tool and is + perpendicular to the direction of motion since the cutter is tangent + to that direction. +2. Determine the direction of the programmed motion. +3. If there is a convex corner, insert an arc to go around the corner. +4. Find the destination point for the tool tip. The tool will be + tangent to the line from the last programmed point to the present + programmed point at the present programmed point. +5. Go in a straight line from the current tool tip location to the + destination tool tip location. + +This uses an angle tolerance of TOLERANCE_CONCAVE_CORNER (0.01 radian) +to determine if: +1) an illegal concave corner exists (tool will not fit into corner), +2) no arc is required to go around the corner (i.e. the current line + is in the same direction as the end of the previous move), or +3) an arc is required to go around a convex corner and start off in + a new direction. + +If a rotary axis is moved in this block and an extra arc is required +to go around a sharp corner, all the rotary axis motion occurs on the +arc. An alternative might be to distribute the rotary axis motion +over the arc and the straight move in proportion to their lengths. + +If the Z-axis is moved in this block and an extra arc is required to +go around a sharp corner, all the Z-axis motion occurs on the straight +line and none on the extra arc. An alternative might be to distribute +the Z-axis motion over the extra arc and the straight line in +proportion to their lengths. + +This handles the case of there being no XY motion. + +This handles G0 moves. Where an arc is inserted to round a corner in a +G1 move, no arc is inserted for a G0 move; a STRAIGHT_TRAVERSE is made +from the current point to the end point. The end point for a G0 +move is the same as the end point for a G1 move, however. + +*/ + +int Interp::convert_straight_comp2(int move, //!< either G_0 or G_1 + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings, //!< pointer to machine settings + double px, //!< X coordinate of programmed end point + double py, //!< Y coordinate of programmed end point + double pz, //!< Z coordinate of end point + double AA_end, //!< A coordinate of end point + double BB_end, //!< B coordinate of end point + double CC_end, //!< C coordinate of end point + double u_end, double v_end, double w_end) +{ + double end_x, end_y, end_z; /* x-coordinate of actual end point */ + double mid_x, mid_y; /* x-coordinate of end of added arc, if needed */ + double small = TOLERANCE_CONCAVE_CORNER; /* radians, testing corners */ + double opx = 0, opy = 0, opz = 0; /* old programmed beginning point */ + double cx, cy, cz; + int concave; + + comp_get_current(settings, &cx, &cy, &cz); + comp_get_current(settings, &end_x, &end_y, &end_z); + comp_get_programmed(settings, &opx, &opy, &opz); + + if ((py == opy) && (px == opx)) { /* no XY motion */ + if (move == G_0) { + enqueue_STRAIGHT_TRAVERSE(settings, block->line_number, + px - opx, py - opy, pz - opz, + cx, cy, pz, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + } else if (move == G_1) { + enqueue_STRAIGHT_FEED(settings, block->line_number, + px - opx, py - opy, pz - opz, + cx, cy, pz, AA_end, BB_end, CC_end, u_end, v_end, w_end); + } else + ERS(NCE_BUG_CODE_NOT_G0_OR_G1); + // end already filled out, above + } else { + // some XY motion + double beta = 0.0; // initializing to avoid confusion over else branch below + double gamma = 0.0; // that does not define value but returns form function. + CUTTER_COMP side = settings->cutter_comp_side; + double radius = settings->cutter_comp_radius; /* will always be positive */ + double theta = atan2(cy - opy, cx - opx); + double alpha = atan2(py - opy, px - opx); + + if (side == CUTTER_COMP::LEFT) { + if (theta < alpha) + theta = (theta + (2 * M_PIl)); + beta = ((theta - alpha) - M_PI_2l); + gamma = M_PI_2l; + } else if (side == CUTTER_COMP::RIGHT) { + if (alpha < theta) + alpha = (alpha + (2 * M_PIl)); + beta = ((alpha - theta) - M_PI_2l); + gamma = -M_PI_2l; + } else { + ERS(NCE_BUG_SIDE_NOT_RIGHT_OR_LEFT); + // the ERS macro will return from this function + } + end_x = (px + (radius * cos(alpha + gamma))); + end_y = (py + (radius * sin(alpha + gamma))); + mid_x = (opx + (radius * cos(alpha + gamma))); + mid_y = (opy + (radius * sin(alpha + gamma))); + + if ((beta < -small) || (beta > (M_PIl + small))) { + concave = 1; + } else if (beta > (M_PIl - small) && + (!qc().empty() && qc().front().type == QARC_FEED && + ((side == CUTTER_COMP::RIGHT && qc().front().data.arc_feed.turn > 0) || + (side == CUTTER_COMP::LEFT && qc().front().data.arc_feed.turn < 0)))) { + // this is an "h" shape, tool on right, going right to left + // over the hemispherical round part, then up next to the + // vertical part (or, the mirror case). there are two ways + // to stay to the "right", either loop down and around, or + // stay above and right. we're forcing above and right. + concave = 1; + } else { + concave = 0; + mid_x = (opx + (radius * cos(alpha + gamma))); + mid_y = (opy + (radius * sin(alpha + gamma))); + } + + if (!concave && (beta > small)) { /* ARC NEEDED */ + CHP(move_endpoint_and_flush(settings, cx, cy)); + if(move == G_1) { + enqueue_ARC_FEED(settings, block->line_number, + 0.0, // doesn't matter, since we will not move this arc's endpoint + mid_x, mid_y, opx, opy, + ((side == CUTTER_COMP::LEFT) ? -1 : 1), cz, + AA_end, BB_end, CC_end, u_end, v_end, w_end); + dequeue_canons(settings); + set_endpoint(mid_x, mid_y); + } else if(move == G_0) { + // we can't go around the corner because there is no + // arc traverse. but, if we do this anyway, at least + // most of our rapid will be parallel to the original + // programmed one. if nothing else, this will look a + // little less confusing in the preview. + enqueue_STRAIGHT_TRAVERSE(settings, block->line_number, + 0.0, 0.0, 0.0, + mid_x, mid_y, cz, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + dequeue_canons(settings); + set_endpoint(mid_x, mid_y); + } else ERS(NCE_BUG_CODE_NOT_G0_OR_G1); + } else if (concave) { + if (qc().front().type != QARC_FEED) { + // line->line + double retreat; + // half the angle of the inside corner + double halfcorner = (beta + M_PIl) / 2.0; + CHKS((halfcorner == 0.0), (_("Zero degree inside corner is invalid for cutter compensation"))); + retreat = radius / tan(halfcorner); + // move back along the compensated path + // this should replace the endpoint of the previous move + mid_x = cx + retreat * cos(theta + gamma); + mid_y = cy + retreat * sin(theta + gamma); + // we actually want to move the previous line's endpoint here. That's the same as + // discarding that line and doing this one instead. + CHP(move_endpoint_and_flush(settings, mid_x, mid_y)); + } else { + // arc->line + // beware: the arc we saved is the compensated one. + arc_feed prev = qc().front().data.arc_feed; + double oldrad = hypot(prev.center2 - prev.end2, prev.center1 - prev.end1); + double oldrad_uncomp; + + // new line's direction + double base_dir = atan2(py - opy, px - opx); + double theta; + double phi; + + theta = (prev.turn > 0) ? base_dir + M_PI_2l : base_dir - M_PI_2l; + phi = atan2(prev.center2 - opy, prev.center1 - opx); + if TOOL_INSIDE_ARC(side, prev.turn) { + oldrad_uncomp = oldrad + radius; + } else { + oldrad_uncomp = oldrad - radius; + } + + double alpha = theta - phi; + // distance to old arc center perpendicular to the new line + double d = oldrad_uncomp * cos(alpha); + double d2; + double angle_from_center; + + if TOOL_INSIDE_ARC(side, prev.turn) { + d2 = d - radius; + double l = d2/oldrad; + CHKS((l > 1.0 || l < -1.0), _("Arc to straight motion makes a corner the compensated tool can't fit in without gouging")); + if(prev.turn > 0) + angle_from_center = - acos(l) + theta + M_PIl; + else + angle_from_center = acos(l) + theta + M_PIl; + } else { + d2 = d + radius; + double l = d2/oldrad; + CHKS((l > 1.0 || l < -1.0), _("Arc to straight motion makes a corner the compensated tool can't fit in without gouging")); + if(prev.turn > 0) + angle_from_center = acos(l) + theta + M_PIl; + else + angle_from_center = - acos(l) + theta + M_PIl; + } + mid_x = prev.center1 + oldrad * cos(angle_from_center); + mid_y = prev.center2 + oldrad * sin(angle_from_center); + CHP(move_endpoint_and_flush(settings, mid_x, mid_y)); + } + } else { + // no arc needed, also not concave (colinear lines or tangent arc->line) + dequeue_canons(settings); + set_endpoint(cx, cy); + } + (move == G_0? enqueue_STRAIGHT_TRAVERSE: enqueue_STRAIGHT_FEED) + (settings, block->line_number, + px - opx, py - opy, pz - opz, + end_x, end_y, pz, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + } + + comp_set_current(settings, end_x, end_y, pz); + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + comp_set_programmed(settings, px, py, pz); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_tool_change + +Returned Value: int (INTERP_OK) + +Side effects: + This makes function calls to canonical machining functions, and sets + the machine model as described below. + +Called by: convert_m + +This function carries out an M6 command, which changes the tool. +If M61 is called, the toolnumber gets changed (without causing an actual toolchange). + +When the CHANGE_TOOL call completes, the specified tool should be +loaded. What this means varies by machine. According to configuration, +the interpreter may also issue commands to do one or more of the +following things before calling CHANGE_TOOL: + +1. stop the spindle +2. move the quill up (Z to machine zero, like G0 G53 Z0) +3. move the axes to reference point #2 (like G30) + +Further, the interpreter makes no assumptions about the axis positions +after the tool change completes. This state is queried and the internal +model is resynched before the program continues. This means CHANGE_TOOL +itself can also issue motion (and it currently may, according to +configuration). + +This implements the "Next tool in T word" approach to tool selection. +The tool is selected when the T word is read (and the carousel may +move at that time) but is changed when M6 is read. + +Note that if a different tool is put into the spindle, the current_z +location setting will be incorrect. It is assumed the program will +contain an appropriate USE_TOOL_LENGTH_OFFSET (G43) command before any +subsequent motion. It is also assumed that the program will restart the +spindle and make new entry moves if necessary. + +*/ + +int Interp::convert_tool_change(setup_pointer settings) //!< pointer to machine settings +{ + + if (settings->selected_pocket < 0) { + ERS(NCE_TXX_MISSING_FOR_M6); + } + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot change tools with cutter radius compensation on"))); + + if (!settings->tool_change_with_spindle_on) { + for (int s = 0; s < settings->num_spindles; s++){ + STOP_SPINDLE_TURNING(s); + settings->spindle_turning[s] = CANON_STOPPED; + } + } + + if (settings->tool_change_quill_up) { + double up_z; + double discard; + find_relative(0., 0., 0., 0., 0., 0., 0., 0., 0., + &discard, &discard, &up_z, + &discard, &discard, &discard, + &discard, &discard, &discard, + settings); + COMMENT("AXIS,hide"); + STRAIGHT_TRAVERSE(-1, settings->current_x, settings->current_y, up_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + COMMENT("AXIS,show"); + settings->current_z = up_z; + } + + if (settings->tool_change_at_g30) { + double end_x; + double end_y; + double end_z; + double AA_end; + double BB_end; + double CC_end; + double u_end; + double v_end; + double w_end; + + find_relative(USER_TO_PROGRAM_LEN(settings->parameters[5181]), + USER_TO_PROGRAM_LEN(settings->parameters[5182]), + USER_TO_PROGRAM_LEN(settings->parameters[5183]), + USER_TO_PROGRAM_ANG(settings->parameters[5184]), + USER_TO_PROGRAM_ANG(settings->parameters[5185]), + USER_TO_PROGRAM_ANG(settings->parameters[5186]), + USER_TO_PROGRAM_LEN(settings->parameters[5187]), + USER_TO_PROGRAM_LEN(settings->parameters[5188]), + USER_TO_PROGRAM_LEN(settings->parameters[5189]), + &end_x, &end_y, &end_z, + &AA_end, &BB_end, &CC_end, + &u_end, &v_end, &w_end, settings); + COMMENT("AXIS,hide"); + + // move indexers first, one at a time + // JOINTS_AXES settings->*_indexer_jnum == -1 means notused + if (AA_end != settings->AA_current && (-1 != settings->a_indexer_jnum) ) + issue_straight_index(3,settings->a_indexer_jnum, AA_end, -1, settings); + if (BB_end != settings->BB_current && (-1 != settings->b_indexer_jnum) ) + issue_straight_index(4,settings->b_indexer_jnum, BB_end, -1, settings); + if (CC_end != settings->CC_current && (-1 != settings->c_indexer_jnum) ) + issue_straight_index(5,settings->c_indexer_jnum, CC_end, -1, settings); + + STRAIGHT_TRAVERSE(-1, end_x, end_y, end_z, + AA_end, BB_end, CC_end, + u_end, v_end, w_end); + COMMENT("AXIS,show"); + settings->current_x = end_x; + settings->current_y = end_y; + settings->current_z = end_z; + settings->AA_current = AA_end; + settings->BB_current = BB_end; + settings->CC_current = CC_end; + settings->u_current = u_end; + settings->v_current = v_end; + settings->w_current = w_end; + } + + CHANGE_TOOL(); + + settings->current_pocket = settings->selected_pocket; + // tool change can move the controlled point. reread it: + settings->toolchange_flag = true; + set_tool_parameters(); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_tool_length_offset + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The block has no offset index (h number): NCE_OFFSET_INDEX_MISSING + 2. The g_code argument is not G_43 or G_49: + NCE_BUG_CODE_NOT_G43_OR_G49 + +Side effects: + A USE_TOOL_LENGTH_OFFSET function call is made. Current_z, + tool_length_offset, and length_offset_index are reset. + +Called by: convert_g + +This is called to execute g43 or g49. + +The g49 RS274/NGC command translates into a USE_TOOL_LENGTH_OFFSET(0.0) +function call. + +The g43 RS274/NGC command translates into a USE_TOOL_LENGTH_OFFSET(length) +function call, where length is the value of the entry in the tool length +offset table whose index is the H number in the block. + +The H number in the block (if present) was checked for being a non-negative +integer when it was read, so that check does not need to be repeated. + +*/ + +int Interp::convert_tool_length_offset(int g_code, //!< g_code being executed (must be G_43 or G_49) + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int idx; + EmcPose tool_offset; + ZERO_EMC_POSE(tool_offset); + settings->g43_with_zero_offset = 0; + + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), + (_("Cannot change tool offset with cutter radius compensation on"))); + if (g_code == G_49) { + idx = 0; + } else if (g_code == G_43) { + logDebug("convert_tool_length_offset h_flag=%d h_number=%d toolchange_flag=%d current_pocket=%d\n", + block->h_flag,block->h_number,settings->toolchange_flag,settings->current_pocket); + if(block->h_flag) { + CHP((find_tool_index(settings, block->h_number, &idx))); + //sequence: + // t12m6 + // g43 should use t12 offset + // g43h12 should also use t12 offset + if (idx==0 && settings->random_toolchanger) { + idx=settings->current_pocket; + } + } else if (settings->toolchange_flag) { + // Tool change is in progress, so the "current tool" is in its + // original index still. + idx = settings->current_pocket; + } else { + // Tool change is done so the current tool is in index 0 (aka the + // spindle). + idx = 0; + } + logDebug("convert_tool_length_offset: using index=%d spindle_toolno=%d pocket_toolno=%d", + idx, settings->tool_table[0].toolno,settings->tool_table[settings->current_pocket].toolno); + + tool_offset.tran.x = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.x); + tool_offset.tran.y = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.y); + tool_offset.tran.z = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.z); + tool_offset.a = USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.a); + tool_offset.b = USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.b); + tool_offset.c = USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.c); + tool_offset.u = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.u); + tool_offset.v = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.v); + tool_offset.w = USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.w); + settings->g43_with_zero_offset = + !(tool_offset.tran.x || tool_offset.tran.y || tool_offset.tran.z || + tool_offset.a || tool_offset.b || tool_offset.c || + tool_offset.u || tool_offset.v || tool_offset.w); + } else if (g_code == G_43_1) { + tool_offset = settings->tool_offset; + idx = -1; + if(block->x_flag) tool_offset.tran.x = block->x_number; + if(block->y_flag) tool_offset.tran.y = block->y_number; + if(block->z_flag) tool_offset.tran.z = block->z_number; + if(block->a_flag) tool_offset.a = block->a_number; + if(block->b_flag) tool_offset.b = block->b_number; + if(block->c_flag) tool_offset.c = block->c_number; + if(block->u_flag) tool_offset.u = block->u_number; + if(block->v_flag) tool_offset.v = block->v_number; + if(block->w_flag) tool_offset.w = block->w_number; + } else if (g_code == G_43_2) { + CHKS((block->h_flag && (block->x_flag || block->y_flag || block->z_flag + || block->a_flag || block->b_flag || block->c_flag || block->u_flag + || block->v_flag || block->w_flag)), (_("G43.2: Can not have both H and axis words"))); + CHKS((!block->h_flag && !block->x_flag && !block->y_flag && !block->z_flag + && !block->a_flag &&!block->b_flag && !block->c_flag && !block->u_flag + && !block->v_flag && !block->w_flag), (_("G43.2: No axes specified and H word missing"))); + tool_offset = settings->tool_offset; + if (block->h_flag){ + CHP((find_tool_index(settings, block->h_number, &idx))); + tool_offset.tran.x += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.x); + tool_offset.tran.y += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.y); + tool_offset.tran.z += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.tran.z); + tool_offset.a += USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.a); + tool_offset.b += USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.b); + tool_offset.c += USER_TO_PROGRAM_ANG(settings->tool_table[idx].offset.c); + tool_offset.u += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.u); + tool_offset.v += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.v); + tool_offset.w += USER_TO_PROGRAM_LEN(settings->tool_table[idx].offset.w); + } else { + if(block->x_flag) tool_offset.tran.x += block->x_number; + if(block->y_flag) tool_offset.tran.y += block->y_number; + if(block->z_flag) tool_offset.tran.z += block->z_number; + if(block->a_flag) tool_offset.a += block->a_number; + if(block->b_flag) tool_offset.b += block->b_number; + if(block->c_flag) tool_offset.c += block->c_number; + if(block->u_flag) tool_offset.u += block->u_number; + if(block->v_flag) tool_offset.v += block->v_number; + if(block->w_flag) tool_offset.w += block->w_number; + } + } else { + ERS("BUG: Code not G43, G43.1, G43.2, or G49"); + } + USE_TOOL_LENGTH_OFFSET(tool_offset); + + double dx, dy; + + dx = settings->tool_offset.tran.x - tool_offset.tran.x; + dy = settings->tool_offset.tran.y - tool_offset.tran.y; + + rotate(&dx, &dy, -settings->rotation_xy); + + settings->current_x += dx; + settings->current_y += dy; + settings->current_z += settings->tool_offset.tran.z - tool_offset.tran.z; + settings->AA_current += settings->tool_offset.a - tool_offset.a; + settings->BB_current += settings->tool_offset.b - tool_offset.b; + settings->CC_current += settings->tool_offset.c - tool_offset.c; + settings->u_current += settings->tool_offset.u - tool_offset.u; + settings->v_current += settings->tool_offset.v - tool_offset.v; + settings->w_current += settings->tool_offset.w - tool_offset.w; + + settings->tool_offset = tool_offset; + + // Update parameters #5401-#5409 to reflect the actually applied tool + // length offset (covers G43Hn with n != loaded tool, G43.1 dynamic + // offsets, and G43.2 additive offsets). Without this, params lag the + // applied offset and only refresh on M6 / G10 L1. See issue #2994. + // tool_offset here is in program units; params follow the user-unit + // convention used elsewhere when populating #5401-#5409. + settings->parameters[5401] = PROGRAM_TO_USER_LEN(tool_offset.tran.x); + settings->parameters[5402] = PROGRAM_TO_USER_LEN(tool_offset.tran.y); + settings->parameters[5403] = PROGRAM_TO_USER_LEN(tool_offset.tran.z); + settings->parameters[5404] = PROGRAM_TO_USER_ANG(tool_offset.a); + settings->parameters[5405] = PROGRAM_TO_USER_ANG(tool_offset.b); + settings->parameters[5406] = PROGRAM_TO_USER_ANG(tool_offset.c); + settings->parameters[5407] = PROGRAM_TO_USER_LEN(tool_offset.u); + settings->parameters[5408] = PROGRAM_TO_USER_LEN(tool_offset.v); + settings->parameters[5409] = PROGRAM_TO_USER_LEN(tool_offset.w); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_tool_select + +Returned Value: int + If the tool number given in the block is not found in the tool table, + it returns INTERP_ERROR. Otherwise (if the tool *is* found) it returns + INTERP_OK. + +Side effects: See below + +Called by: execute_block + +A select tool command is given, which causes the changer chain to move +so that the slot with the tool identified by the t_number given in the +block is next to the tool changer, ready for a tool change. The +settings->selected_tool_slot is set to the given slot. + +A check that the t_number is not negative has already been made in read_t. +A zero t_number is allowed and means no tool should be selected. + +*/ + +// OK to select tool in a concave corner, I think? + +int Interp::convert_tool_select(block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + int idx; + CHP((find_tool_index(settings, block->t_number, &idx))); + SELECT_TOOL(block->t_number); + settings->selected_pocket = idx; + settings->selected_tool = block->t_number; + return INTERP_OK; +} + + +int Interp::update_tag(StateTag &tag) +{ + UPDATE_TAG(tag); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! tag_straight + +Returned Value: int + Applies new geometric tags for straight moves (G0 and G1 segments) + Called from convert_arc2 +*/ + +int Interp::tag_straight(block_pointer block, double x, double y) +{ + // Use a static variable to remember the heading between function calls + // Initialize to 0.0 or your preferred default start angle + static double last_valid_heading = 0.0; + + double start_x = _setup.current_x; + double start_y = _setup.current_y; + + double dx = x - start_x; + double dy = y - start_y; + + // Only update the heading if there is actual XY motion + if (hypot(dx, dy) > 0.0001) + { + double heading = atan2(dy, dx) * (180.0 / M_PI); + + // Normalization + heading = fmod(heading, 360.0); + if (heading < 0.0) heading += 360.0; + + // Store it for this block AND for the next Z-only move + block->arc_heading = heading; + last_valid_heading = heading; + } + else + { + // For Z-only moves, reuse the last calculated XY heading + block->arc_heading = last_valid_heading; + } + + // Reset Arc data for safety + block->radius = 0.0; + block->arc_center_x = 0.0; + block->arc_center_y = 0.0; + block->arc_center_z = 0.0; + block->iscircle = false; + + // Ship the data to the status registers + write_canon_state_tag(block, &_setup); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! tag_arcs + +Returned Value: int + Applies new tags for Arcs (G2 and G3 segments) + Called from convert_arc2 +*/ + +int Interp::tag_arc(block_pointer block, double x, double y, double z, double center_x, double center_y, double center_z, int move, CANON_PLANE plane) + { + + // Initialize variables to be populated by the plane logic + double dx = 0, dy = 0; + bool is_helix = false; + bool is_360 = false; + + // Resolve Plane-Specific Deltas (Heading) and Helix (Perpendicular Move) + if (plane == CANON_PLANE::XY) { + // Heading axes: X=Horiz, Y=Vert + dx = center_x - _setup.current_x; + dy = center_y - _setup.current_y; + + // Helix axis: Z + if (fabs(z - _setup.current_z) > TOLERANCE_EQUAL) + is_helix = true; + + // 360 Check: Do planar endpoints match start? + if (fabs(x - _setup.current_x) < TOLERANCE_EQUAL && fabs(y - _setup.current_y) < TOLERANCE_EQUAL) + is_360 = true; + } + else if (plane == CANON_PLANE::XZ) { + // Heading axes: Z=Horiz, X=Vert (Standard G18 orientation) + dx = center_z - _setup.current_z; + dy = center_x - _setup.current_x; + + // Helix axis: Y + if (fabs(y - _setup.current_y) > TOLERANCE_EQUAL) + is_helix = true; + + if (fabs(x - _setup.current_x) < TOLERANCE_EQUAL && fabs(z - _setup.current_z) < TOLERANCE_EQUAL) + is_360 = true; + } + else { // plane == CANON_PLANE::YZ + // Heading axes: Y=Horiz, Z=Vert + dx = center_y - _setup.current_y; + dy = center_z - _setup.current_z; + + // Helix axis: X + if (fabs(x - _setup.current_x) > TOLERANCE_EQUAL) + is_helix = true; + + if (fabs(y - _setup.current_y) < TOLERANCE_EQUAL && fabs(z - _setup.current_z) < TOLERANCE_EQUAL) + is_360 = true; + } + + // Heading Calculation (Using the dx/dy resolved above) + double radial_angle = atan2(dy, dx); + double tangent_angle = (move == G_3) ? (radial_angle + (M_PI / 2.0)) : (radial_angle - (M_PI / 2.0)); + double heading = tangent_angle * (180.0 / M_PI); + + // Normalise 0-360 + while (heading < 0) heading += 360.0; + while (heading >= 360.0) heading -= 360.0; + + double radius = hypot(dx, dy); + + // We need to scale the radius if the gcode units are not the same as machine units + // If linearUnits > 0.5, it's 1.0 (Metric). If < 0.5, it's 0.03937 (Imperial). + int machineUnits = (emcStatus->motion.traj.linearUnits > 0.5) ? CANON_UNITS_MM : CANON_UNITS_INCHES; + + // 3. Identify the active G-code unit type + int gcodeUnits = _setup.length_units; + + // 4. If they don't match, scale the radius to Machine Units + if (gcodeUnits != machineUnits) { + // longhand below to avoid clang errors + if (gcodeUnits == CANON_UNITS_INCHES) { + // G-code is Inches, Machine is MM -> Scale UP to MM + radius = radius * 25.4; + } else { + // G-code is MM, Machine is Inches -> Scale DOWN to Inches + radius = radius / 25.4; + } + } + + // Final Assignments + block->iscircle = (is_360 && !is_helix) ? 1 : 0; + block->arc_center_x = center_x; + block->arc_center_y = center_y; + block->arc_center_z = center_z; + block->arc_radius = hypot(dx, dy); // Radius in the active plane + block->arc_heading = heading; + + write_canon_state_tag(block, &_setup); + return INTERP_OK; + } diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_cycles.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_cycles.cc new file mode 100644 index 0000000..bbebc0d --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_cycles.cc @@ -0,0 +1,2272 @@ +/******************************************************************** +* Description: interp_cycles.cc +* +* The bulk of the functions here control how canned cycles are +* interpreted. +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +********************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" + +static const char* plane_name(CANON_PLANE p); + +/****************************************************************************/ + +/*! convert_cycle_g81 + +Returned Value: int (INTERP_OK) + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, which +is usually drilling: +1. Move the z-axis only at the current feed rate to the specified bottom_z. +2. Retract the z-axis at traverse rate to clear_z. + +See [NCMS, page 99]. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g81(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double clear_z, //!< z-value of clearance plane + double bottom_z) //!< value of z at bottom of cycle +{ + cycle_feed(block, plane, x, y, bottom_z); + cycle_traverse(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g82 + +Returned Value: int (INTERP_OK) + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, which +is usually drilling: +1. Move the z_axis only at the current feed rate to the specified z-value. +2. Dwell for the given number of seconds. +3. Retract the z-axis at traverse rate to the clear_z. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g82(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + double dwell) //!< dwell time +{ + cycle_feed(block, plane, x, y, bottom_z); + DWELL(dwell); + cycle_traverse(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g83 + +Returned Value: int (INTERP_OK) + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, +which is usually peck drilling: +1. Move the z-axis only at the current feed rate downward by delta or + to the specified bottom_z, whichever is less deep. +2. Rapid back out to the R plane. +3. Rapid back down to the current hole bottom, backed off a bit. +4. Repeat steps 1, 2, and 3 until the specified bottom_z is reached. +5. Retract the z-axis at traverse rate to clear_z. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +The rapid out and back in causes any long stringers (which are common +when drilling in aluminum) to be cut off and clears chips from the +hole. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g83(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double r, //!< initial z-value + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + double delta) //!< size of z-axis feed increment +{ + double current_depth; + double rapid_delta; + + /* Moved the check for negative Q values here as a sign + may be used with user defined M functions + Thanks to Billy Singleton for pointing it out... */ + CHKS((delta <= 0.0), NCE_NEGATIVE_OR_ZERO_Q_VALUE_USED); + + rapid_delta = block->d_flag?block->d_number_float:_setup.parameter_g83_peck_clearance; + + for (current_depth = (r - delta); + current_depth > bottom_z; current_depth = (current_depth - delta)) { + cycle_feed(block, plane, x, y, current_depth); + cycle_traverse(block, plane, x, y, r); + cycle_traverse(block, plane, x, y, current_depth + rapid_delta); + } + cycle_feed(block, plane, x, y, bottom_z); + cycle_traverse(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g73 + +Returned Value: int (INTERP_OK) + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, +which is usually peck drilling: +1. Move the z-axis only at the current feed rate downward by delta or + to the specified bottom_z, whichever is less deep. +2. Rapid back out a bit. +4. Repeat steps 1, 2, and 3 until the specified bottom_z is reached. +5. Retract the z-axis at traverse rate to clear_z. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +The rapid out and back in causes any long stringers (which are common +when drilling in aluminum) to be cut off and clears chips from the +hole. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g73(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double r, //!< initial z-value + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + double delta) //!< size of z-axis feed increment +{ + double current_depth; + double rapid_delta; + /* Moved the check for negative Q values here as a sign + may be used with user defined M functions + Thanks to Billy Singleton for pointing it out... */ + CHKS((delta <= 0.0), NCE_NEGATIVE_OR_ZERO_Q_VALUE_USED); + + rapid_delta = block->d_flag?block->d_number_float:_setup.parameter_g73_peck_clearance; + + for (current_depth = (r - delta); + current_depth > bottom_z; current_depth = (current_depth - delta)) { + cycle_feed(block, plane, x, y, current_depth); + cycle_traverse(block, plane, x, y, current_depth + rapid_delta); + } + cycle_feed(block, plane, x, y, bottom_z); + cycle_traverse(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g74_g84 + +Returned Value: int + If the spindle is not turning clockwise and motion is not g84, this returns + NCE_SPINDLE_NOT_TURNING_CLOCKWISE_IN_G84. + If the spindle is not turning counterclockwise and motion is not g74, this returns + NCE_SPINDLE_NOT_TURNING_COUNTER_CLOCKWISE_IN_G74. + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, ++which is right-hand floating-chuck tapping: +1. start the feed move towards bottom_z. +2. Move the z-axis only at the current feed rate to the specified bottom_z. +3. Stop the spindle. +4. Start the spindle counterclockwise (or cw if g74), suppressing the + wait-for-spindle-at-speed for the following feed move out. +5. Retract the z-axis at current feed rate to clear_z. +6. Stop the spindle. +7. Start the spindle clockwise (or ccw if g74), with normal wait-for-at-speed for next feed move. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. +The direction argument must be clockwise. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +InterpReturn Interp::check_g74_g84_spindle(GCodes motion, CANON_DIRECTION dir) +{ + switch (dir) { + case CANON_STOPPED: + ERS(_("Spindle not turning in %s"), toString((GCodes)motion).c_str()); + case CANON_CLOCKWISE: + CHKS((motion == G_74), _("Spindle turning clockwise in G74")); + return INTERP_OK; + case CANON_COUNTERCLOCKWISE: + CHKS((motion == G_84), _("Spindle turning counterclockwise in G84")); + return INTERP_OK; + } + ERS(_("Spindle state unknown for %s"), toString(motion).c_str()); +} + +int Interp::convert_cycle_g74_g84(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + CANON_DIRECTION direction, //!< direction spindle turning at outset + CANON_SPEED_FEED_MODE /*mode*/, //!< the speed-feed mode at outset + int motion, double dwell, int spindle) +{ + + CHP(check_g74_g84_spindle((GCodes)motion, direction)); + + int save_feed_override_enable; + int save_spindle_override_enable; + + save_feed_override_enable = GET_EXTERNAL_FEED_OVERRIDE_ENABLE(); + save_spindle_override_enable = GET_EXTERNAL_SPINDLE_OVERRIDE_ENABLE(spindle); + + switch (plane) { + + case CANON_PLANE::XY: + DISABLE_FEED_OVERRIDE(); + DISABLE_SPEED_OVERRIDE(spindle); + cycle_feed(block, plane, x, y, bottom_z); + STOP_SPINDLE_TURNING(spindle); + // the zero parameter suppresses the wait for at-speed on next feed + if (motion == G_84) + START_SPINDLE_COUNTERCLOCKWISE(spindle); + else + START_SPINDLE_CLOCKWISE(spindle); + DWELL(dwell); + cycle_feed(block, plane, x, y, clear_z); + STOP_SPINDLE_TURNING(spindle); + if (motion == G_84) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + break; + + case CANON_PLANE::YZ: + DISABLE_FEED_OVERRIDE(); + DISABLE_SPEED_OVERRIDE(spindle); + cycle_feed(block, plane, bottom_z, x, y); + STOP_SPINDLE_TURNING(spindle); + if (motion == G_84) + START_SPINDLE_COUNTERCLOCKWISE(spindle); + else + START_SPINDLE_CLOCKWISE(spindle); + DWELL(dwell); + cycle_feed(block, plane, clear_z, x, y); + STOP_SPINDLE_TURNING(spindle); + if (motion == G_84) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + break; + + case CANON_PLANE::XZ: + DISABLE_FEED_OVERRIDE(); + DISABLE_SPEED_OVERRIDE(spindle); + cycle_feed(block, plane, y, bottom_z, x); + STOP_SPINDLE_TURNING(spindle); + if (motion == G_84) + START_SPINDLE_COUNTERCLOCKWISE(spindle); + else + START_SPINDLE_CLOCKWISE(spindle); + DWELL(dwell); + cycle_feed(block, plane, y, clear_z, x); + STOP_SPINDLE_TURNING(spindle); + if (motion == G_84) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + break; + + default: + ERS("%s for plane %s not implemented", + toString((GCodes)motion).c_str(), plane_name(plane)); + } + if(save_feed_override_enable) + ENABLE_FEED_OVERRIDE(); + if(save_spindle_override_enable) + ENABLE_SPEED_OVERRIDE(spindle); + + return INTERP_OK; + +#if 0 + START_SPEED_FEED_SYNCH(); + cycle_feed(block, plane, x, y, bottom_z); + + cycle_feed(block, plane, x, y, clear_z); + if (mode != CANON_SYNCHED) + STOP_SPEED_FEED_SYNCH(); + STOP_SPINDLE_TURNING(); + START_SPINDLE_CLOCKWISE(); +#endif +} + +/****************************************************************************/ + +/*! convert_cycle_g85 + +Returned Value: int (INTERP_OK) + +Side effects: + A number of moves are made as described below. + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, +which is usually boring or reaming: +1. Move the z-axis only at the current feed rate to the specified z-value. +2. Retract the z-axis at the current feed rate to clear_z. + +CYCLE_MACRO has positioned the tool at (x, y, r, ?, ?) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g85(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double r, // retract plane + double clear_z, //!< z-value of clearance plane + double bottom_z) //!< value of z at bottom of cycle +{ + cycle_feed(block, plane, x, y, bottom_z); + cycle_feed(block, plane, x, y, r); + cycle_traverse(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g86 + +Returned Value: int + If the spindle is not turning clockwise or counterclockwise, + this returns NCE_SPINDLE_NOT_TURNING_IN_G86. + Otherwise, it returns INTERP_OK. + +Side effects: + A number of moves are made as described below. + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the RS274/NGC following cycle, +which is usually boring: +1. Move the z-axis only at the current feed rate to bottom_z. +2. Dwell for the given number of seconds. +3. Stop the spindle turning. +4. Retract the z-axis at traverse rate to clear_z. +5. Restart the spindle in the direction it was going. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g86(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + double dwell, //!< dwell time + CANON_DIRECTION direction, //!< direction spindle turning at outset + int spindle) // the spindle being used +{ + CHKS(((direction != CANON_CLOCKWISE) && + (direction != CANON_COUNTERCLOCKWISE)), + NCE_SPINDLE_NOT_TURNING_IN_G86); + + cycle_feed(block, plane, x, y, bottom_z); + DWELL(dwell); + STOP_SPINDLE_TURNING(spindle); + cycle_traverse(block, plane, x, y, clear_z); + if (direction == CANON_CLOCKWISE) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g87 + +Returned Value: int + If the spindle is not turning clockwise or counterclockwise, + this returns NCE_SPINDLE_NOT_TURNING_IN_G87. + Otherwise, it returns INTERP_OK. + +Side effects: + A number of moves are made as described below. This cycle is a + modified version of [Monarch, page 5-24] since [NCMS, pages 98 - 100] + gives no clue as to what the cycle is supposed to do. [KT] does not + have a back boring cycle. [Fanuc, page 132] in "Canned cycle II" + describes the G87 cycle as given here, except that the direction of + spindle turning is always clockwise and step 7 below is omitted + in [Fanuc]. + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, which +is usually back boring. The situation is that you have a through hole +and you want to counterbore the bottom of hole. To do this you put an +L-shaped tool in the spindle with a cutting surface on the UPPER side +of its base. You stick it carefully through the hole when it is not +spinning and is oriented so it fits through the hole, then you move it +so the stem of the L is on the axis of the hole, start the spindle, +and feed the tool upward to make the counterbore. Then you get the +tool out of the hole. + +1. Move at traverse rate parallel to the XY-plane to the point + with x-value offset_x and y-value offset_y. +2. Stop the spindle in a specific orientation. +3. Move the z-axis only at traverse rate downward to the bottom_z. +4. Move at traverse rate parallel to the XY-plane to the x,y location. +5. Start the spindle in the direction it was going before. +6. Move the z-axis only at the given feed rate upward to the middle_z. +7. Move the z-axis only at the given feed rate back down to bottom_z. +8. Stop the spindle in the same orientation as before. +9. Move at traverse rate parallel to the XY-plane to the point + with x-value offset_x and y-value offset_y. +10. Move the z-axis only at traverse rate to the clear z value. +11. Move at traverse rate parallel to the XY-plane to the specified x,y + location. +12. Restart the spindle in the direction it was going before. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) before this starts. + +It might be useful to add a check that clear_z > middle_z > bottom_z. +Without the check, however, this can be used to counterbore a hole in +material that can only be accessed through a hole in material above it. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g87(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double offset_x, //!< x-axis offset position + double y, //!< y-value where cycle is executed + double offset_y, //!< y-axis offset position + double r, //!< z_value of r_plane + double clear_z, //!< z-value of clearance plane + double middle_z, //!< z-value of top of back bore + double bottom_z, //!< value of z at bottom of cycle + CANON_DIRECTION direction, //!< direction spindle turning at outset + int spindle) // the spindle being used +{ + CHKS(((direction != CANON_CLOCKWISE) && + (direction != CANON_COUNTERCLOCKWISE)), + NCE_SPINDLE_NOT_TURNING_IN_G87); + + cycle_traverse(block, plane, offset_x, offset_y, r); + STOP_SPINDLE_TURNING(spindle); + ORIENT_SPINDLE(spindle, 0.0, direction); + cycle_traverse(block, plane, offset_x, offset_y, bottom_z); + cycle_traverse(block, plane, x, y, bottom_z); + if (direction == CANON_CLOCKWISE) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + cycle_feed(block, plane, x, y, middle_z); + cycle_feed(block, plane, x, y, bottom_z); + STOP_SPINDLE_TURNING(spindle); + ORIENT_SPINDLE(spindle,0.0, direction); + cycle_traverse(block, plane, offset_x, offset_y, bottom_z); + cycle_traverse(block, plane, offset_x, offset_y, clear_z); + cycle_traverse(block, plane, x, y, clear_z); + if (direction == CANON_CLOCKWISE) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g88 + +Returned Value: int + If the spindle is not turning clockwise or counterclockwise, this + returns NCE_SPINDLE_NOT_TURNING_IN_G88. + Otherwise, it returns INTERP_OK. + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +For the XY plane, this implements the following RS274/NGC cycle, +which is usually boring: +1. Move the z-axis only at the current feed rate to the specified z-value. +2. Dwell for the given number of seconds. +3. Stop the spindle turning. +4. Stop the program so the operator can retract the spindle manually. +5. Restart the spindle. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g88(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double bottom_z, //!< value of z at bottom of cycle + double dwell, //!< dwell time + CANON_DIRECTION direction, //!< direction spindle turning at outset + int spindle) // the spindle being used +{ + CHKS(((direction != CANON_CLOCKWISE) && + (direction != CANON_COUNTERCLOCKWISE)), + NCE_SPINDLE_NOT_TURNING_IN_G88); + + cycle_feed(block, plane, x, y, bottom_z); + DWELL(dwell); + STOP_SPINDLE_TURNING(spindle); + PROGRAM_STOP(); /* operator retracts the spindle here */ + if (direction == CANON_CLOCKWISE) + START_SPINDLE_CLOCKWISE(spindle); + else + START_SPINDLE_COUNTERCLOCKWISE(spindle); + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_g89 + +Returned Value: int (INTERP_OK) + +Side effects: See below + +Called by: + convert_cycle_xy + convert_cycle_yz + convert_cycle_zx + +This implements the following RS274/NGC cycle, which is intended for boring: +1. Move the z-axis only at the current feed rate to the specified z-value. +2. Dwell for the given number of seconds. +3. Retract the z-axis at the current feed rate to clear_z. + +CYCLE_MACRO has positioned the tool at (x, y, r, a, b, c) when this starts. + +For the XZ and YZ planes, this makes analogous motions. + +*/ + +int Interp::convert_cycle_g89(block_pointer block, + CANON_PLANE plane, //!< selected plane + double x, //!< x-value where cycle is executed + double y, //!< y-value where cycle is executed + double clear_z, //!< z-value of clearance plane + double bottom_z, //!< value of z at bottom of cycle + double dwell) //!< dwell time +{ + cycle_feed(block, plane, x, y, bottom_z); + DWELL(dwell); + cycle_feed(block, plane, x, y, clear_z); + + return INTERP_OK; +} + +static const char* plane_name(CANON_PLANE p) { + switch(p) { + case CANON_PLANE::XY: return "XY"; + case CANON_PLANE::YZ: return "YZ"; + case CANON_PLANE::XZ: return "XZ"; + case CANON_PLANE::UV: return "UV"; + case CANON_PLANE::VW: return "VW"; + case CANON_PLANE::UW: return "UW"; + default: return "invalid"; + } +} + +/****************************************************************************/ + +/*! convert_cycle + +Returned Value: int + If any of the specific functions called returns an error code, + this returns that code. + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The r-value is not given the first time this code is called after + some other motion mode has been in effect: + NCE_R_CLEARANCE_PLANE_UNSPECIFIED_IN_CYCLE + 2. The l number is zero: NCE_CANNOT_DO_ZERO_REPEATS_OF_CYCLE + 3. The currently selected plane in not XY, YZ, or XZ. + NCE_BUG_PLANE_NOT_XY_YZ_OR_XZ + +Side effects: + A number of moves are made to execute a canned cycle. The current + position is reset. The values of the cycle attributes in the settings + may be reset. + +Called by: convert_motion + +This function makes a couple checks and then calls one of three +functions, according to which plane is currently selected. + +See the documentation of convert_cycle_xy for most of the details. + +*/ + +int Interp::convert_cycle(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + CANON_PLANE plane; + + CHKS((settings->feed_rate == 0.0), _("Cannot feed with zero feed rate")); + CHKS((settings->feed_mode == FEED_MODE::INVERSE_TIME), _("Cannot use inverse time feed with canned cycles")); + CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF), _("Cannot use canned cycles with cutter compensation on")); + + plane = settings->plane; + if (!block->r_flag) { + if (settings->motion_mode == motion) + block->r_number = settings->cycle_r; + else + ERS(NCE_R_CLEARANCE_PLANE_UNSPECIFIED_IN_CYCLE); + } + + CHKS((block->l_number == 0), NCE_CANNOT_DO_ZERO_REPEATS_OF_CYCLE); + if (block->l_number == -1) + block->l_number = 1; + + switch(plane) { + case CANON_PLANE::XY: + case CANON_PLANE::XZ: + case CANON_PLANE::YZ: + CHKS(block->u_flag, "Cannot put a U in a canned cycle in the %s plane", + plane_name(settings->plane)); + CHKS(block->v_flag, "Cannot put a V in a canned cycle in the %s plane", + plane_name(settings->plane)); + CHKS(block->w_flag, "Cannot put a W in a canned cycle in the %s plane", + plane_name(settings->plane)); + break; + + case CANON_PLANE::UV: + case CANON_PLANE::VW: + case CANON_PLANE::UW: + CHKS(block->x_flag, "Cannot put an X in a canned cycle in the %s plane", + plane_name(settings->plane)); + CHKS(block->y_flag, "Cannot put a Y in a canned cycle in the %s plane", + plane_name(settings->plane)); + CHKS(block->z_flag, "Cannot put a Z in a canned cycle in the %s plane", + plane_name(settings->plane)); + } + + //KLUDGE ugly way to save / restore motion mode flag so that state + //tag displays correctly + int save_mode = settings->motion_mode; + settings->motion_mode = motion; + write_canon_state_tag(block, settings); + settings->motion_mode = save_mode; + // end KLUDGE + + if (plane == CANON_PLANE::XY) { + CHP(convert_cycle_xy(motion, block, settings)); + } else if (plane == CANON_PLANE::YZ) { + CHP(convert_cycle_yz(motion, block, settings)); + } else if (plane == CANON_PLANE::XZ) { + CHP(convert_cycle_zx(motion, block, settings)); + } else if (plane == CANON_PLANE::UV) { + CHP(convert_cycle_uv(motion, block, settings)); + } else if (plane == CANON_PLANE::VW) { + CHP(convert_cycle_vw(motion, block, settings)); + } else if (plane == CANON_PLANE::UW) { + CHP(convert_cycle_wu(motion, block, settings)); + } else + ERS(NCE_BUG_PLANE_NOT_XY_YZ_OR_XZ); + + settings->cycle_l = block->l_number; + settings->cycle_r = block->r_number; + settings->motion_mode = motion; + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_xy + +Returned Value: int + If any of the specific functions called returns an error code, + this returns that code. + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The z-value is not given the first time this code is called after + some other motion mode has been in effect: + NCE_Z_VALUE_UNSPECIFIED_IN_XY_PLANE_CANNED_CYCLE + 2. The r clearance plane is below the bottom_z: + NCE_R_LESS_THAN_Z_IN_CYCLE_IN_XY_PLANE + 3. the distance mode is neither absolute or incremental: + NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91 + 4. G82, G86, G88, or G89 is called when it is not already in effect, + and no p number is in the block: + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89 + 5. G83 is called when it is not already in effect, + and no q number is in the block: NCE_Q_WORD_MISSING_WITH_G83_OR_M66 + 6. G87 is called when it is not already in effect, + and any of the i number, j number, or k number is missing: + NCE_I_WORD_MISSING_WITH_G87 + NCE_J_WORD_MISSING_WITH_G87 + NCE_K_WORD_MISSING_WITH_G87 + 7. the G-code is not between G_81 and G_89. + NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED + +Side effects: + A number of moves are made to execute the G-code + +Called by: convert_cycle + +The function does not require that any of x,y,z, or r be specified in +the block, except that if the last motion mode command executed was +not the same as this one, the r-value and z-value must be specified. + +This function is handling the repeat feature of RS274/NGC, wherein +the L word represents the number of repeats [NCMS, page 99]. We are +not allowing L=0, contrary to the manual. We are allowing L > 1 +in absolute distance mode to mean "do the same thing in the same +place several times", as provided in the manual, although this seems +abnormal. + +In incremental distance mode, x, y, and r values are treated as +increments to the current position and z as an increment from r. In +absolute distance mode, x, y, r, and z are absolute. In g87, i and j +will always be increments, regardless of the distance mode setting, as +implied in [NCMS, page 98], but k (z-value of top of counterbore) will +be an absolute z-value in absolute distance mode, and an increment +(from bottom z) in incremental distance mode. + +If the r position of a cycle is above the current_z position, this +retracts the z-axis to the r position before moving parallel to the +XY plane. + +In the code for this function, there is a nearly identical "for" loop +in every case of the switch. The loop is the done with a compiler +macro, "CYCLE_MACRO" so that the code is easy to read, automatically +kept identical from case to case and, and much shorter than it would +be without the macro. The loop could be put outside the switch, but +then the switch would run every time around the loop, not just once, +as it does here. The loop could also be placed in the called +functions, but then it would not be clear that all the loops are the +same, and it would be hard to keep them the same when the code is +modified. The macro would be very awkward as a regular function +because it would have to be passed all of the arguments used by any of +the specific cycles, and, if another switch in the function is to be +avoided, it would have to passed a function pointer, but the different +cycle functions have different arguments so the type of the pointer +could not be declared unless the cycle functions were re-written to +take the same arguments (in which case most of them would have several +unused arguments). + +The motions within the CYCLE_MACRO (but outside a specific cycle) are +a straight traverse parallel to the selected plane to the given +position in the plane and a straight traverse of the third axis only +(if needed) to the r position. + +The CYCLE_MACRO is defined here but is also used in convert_cycle_yz +and convert_cycle_zx. The variables aa, bb, and cc are used in +CYCLE_MACRO and in the other two functions just mentioned. Those +variables represent the first axis of the selected plane, the second +axis of the selected plane, and third axis which is perpendicular to +the selected plane. In this function aa represents x, bb represents +y, and cc represents z. This usage makes it possible to have only one +version of each of the cycle functions. The cycle_traverse and +cycle_feed functions help accomplish this. + +The height of the retract move at the end of each repeat of a cycle is +determined by the setting of the retract_mode: either to the r +position (if the retract_mode is R_PLANE) or to the original +z-position (if that is above the r position and the retract_mode is +not R_PLANE). This is a slight departure from [NCMS, page 98], which +does not require checking that the original z-position is above r. + +The rotary axes may not move during a canned cycle. + +*/ + +int Interp::convert_cycle_xy(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + double save_tolerance, save_cam_tolerance; + double current_cc = settings->current_z; + + plane = CANON_PLANE::XY; + if (settings->motion_mode != motion) { + CHKS((!block->z_flag), + _readers[(int)'z']? NCE_Z_VALUE_UNSPECIFIED_IN_XY_PLANE_CANNED_CYCLE: _("G17 canned cycle is not possible on a machine without Z axis")); + } + block->z_number = + block->z_flag ? block->z_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + double radius, theta; + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->z_number; + if(block->radius_flag) + radius = block->radius; + else + radius = hypot(settings->current_y, settings->current_x); + if(block->theta_flag) + theta = D2R(block->theta); + else + theta = atan2(settings->current_y, settings->current_x); + if(block->radius_flag || block->theta_flag) { + aa = radius * cos(theta); + bb = radius * sin(theta); + } else { + aa = block->x_flag ? block->x_number : settings->current_x; + bb = block->y_flag ? block->y_number : settings->current_y; + } + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->x_flag) aa_increment = block->x_number; + if (block->y_flag) bb_increment = block->y_number; + if (block->radius_flag) radius_increment = block->radius; + if (block->theta_flag) theta_increment = D2R(block->theta); + r = (block->r_number + old_cc); + cc = (r + block->z_number); /* [NCMS, page 98] */ + aa = settings->current_x; + bb = settings->current_y; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_Z_IN_CYCLE_IN_XY_PLANE); + + // First motion of a canned cycle (maybe): if we're below the R plane, + // rapid straight up to the R plane. + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, settings->current_x, settings->current_y, r, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::XY, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::XY, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::XY, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::XY, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::XY, aa, bb, clear_cc, cc, + settings->spindle_turning[settings->active_spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::XY, aa, bb, r, clear_cc, cc)) + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::XY, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + k = (cc + k); /* k always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::XY, aa, (aa + i), bb, + (bb + j), r, clear_cc, k, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::XY, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::XY, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->current_x = aa; /* CYCLE_MACRO updates aa and bb */ + settings->current_y = bb; + settings->current_z = clear_cc; + settings->cycle_cc = block->z_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} + + + +int Interp::convert_cycle_uv(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + int spindle = settings->active_spindle; + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + double save_tolerance, save_cam_tolerance; + double current_cc = settings->w_current; + + plane = CANON_PLANE::UV; + if (settings->motion_mode != motion) { + CHKS((!block->w_flag), + _readers[(int)'w']? NCE_W_VALUE_UNSPECIFIED_IN_UV_PLANE_CANNED_CYCLE: _("G17.1 canned cycle is not possible on a machine without W axis")); + } + block->w_number = + block->w_flag ? block->w_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->w_number; + aa = block->u_flag ? block->u_number : settings->u_current; + bb = block->v_flag ? block->v_number : settings->v_current; + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->u_flag) aa_increment = block->u_number; + if (block->v_flag) bb_increment = block->v_number; + r = (block->r_number + old_cc); + cc = (r + block->w_number); /* [NCMS, page 98] */ + aa = settings->u_current; + bb = settings->v_current; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_W_IN_CYCLE_IN_UV_PLANE); + + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, settings->current_x, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, r); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::UV, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::UV, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::UV, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::UV, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::UV, aa, bb, clear_cc, cc, + settings->spindle_turning[spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::UV, aa, bb, r, clear_cc, cc)) + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::UV, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + k = (cc + k); /* k always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::UV, aa, (aa + i), bb, + (bb + j), r, clear_cc, k, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)) + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::UV, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::UV, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->u_current = aa; /* CYCLE_MACRO updates aa and bb */ + settings->v_current = bb; + settings->w_current = clear_cc; + settings->cycle_cc = block->w_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! convert_cycle_yz + +Returned Value: int + If any of the specific functions called returns an error code, + this returns that code. + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. The x-value is not given the first time this code is called after + some other motion mode has been in effect: + NCE_X_VALUE_UNSPECIFIED_IN_YZ_PLANE_CANNED_CYCLE + 2. The r clearance plane is below the bottom_x: + NCE_R_LESS_THAN_X_IN_CYCLE_IN_YZ_PLANE + 3. the distance mode is neither absolute or incremental: + NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91 + 4. G82, G86, G88, or G89 is called when it is not already in effect, + and no p number is in the block: + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89 + 5. G83 is called when it is not already in effect, + and no q number is in the block: NCE_Q_WORD_MISSING_WITH_G83 + 6. G87 is called when it is not already in effect, + and any of the i number, j number, or k number is missing: + NCE_I_WORD_MISSING_WITH_G87 + NCE_J_WORD_MISSING_WITH_G87 + NCE_K_WORD_MISSING_WITH_G87 + 7. the G-code is not between G_81 and G_89. + NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED + +Side effects: + A number of moves are made to execute a canned cycle. + +Called by: convert_cycle + +See the documentation of convert_cycle_xy. This function is entirely +similar. In this function aa represents y, bb represents z, and cc +represents x. + +The CYCLE_MACRO is defined just before the convert_cycle_xy function. + +Tool length offsets work only when the tool axis is parallel to the +Z-axis, so if this function is used, tool length offsets should be +turned off, and the NC code written to take tool length into account. + +*/ + +int Interp::convert_cycle_yz(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int spindle = settings->active_spindle; + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + // save the current tolerance, to restore it later on + double save_tolerance, save_cam_tolerance; + double current_cc = settings->current_x; + + plane = CANON_PLANE::YZ; + if (settings->motion_mode != motion) { + CHKS((!block->x_flag), + _readers[(int)'x']? NCE_X_VALUE_UNSPECIFIED_IN_YZ_PLANE_CANNED_CYCLE: _("G19 canned cycle is not possible on a machine without X axis")); + } + block->x_number = + block->x_flag ? block->x_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->x_number; + aa = block->y_flag ? block->y_number : settings->current_y; + bb = block->z_flag ? block->z_number : settings->current_z; + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->y_flag) aa_increment = block->y_number; + if (block->z_flag) bb_increment = block->z_number; + r = (block->r_number + old_cc); + cc = (r + block->x_number); /* [NCMS, page 98] */ + aa = settings->current_y; + bb = settings->current_z; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_X_IN_CYCLE_IN_YZ_PLANE); + + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, r, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::YZ, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::YZ, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::YZ, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::YZ, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::YZ, aa, bb, clear_cc, cc, + settings->spindle_turning[spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::YZ, aa, bb, r, clear_cc, cc)) + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::YZ, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + i = (cc + i); /* i always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::YZ, aa, (aa + j), bb, + (bb + k), r, clear_cc, i, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::YZ, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::YZ, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->current_y = aa; /* CYCLE_MACRO updates aa and bb */ + settings->current_z = bb; + settings->current_x = clear_cc; + settings->cycle_cc = block->x_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} + + +int Interp::convert_cycle_vw(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + // save the current tolerance, to restore it later on + double save_tolerance, save_cam_tolerance; + double current_cc = settings->u_current; + + plane = CANON_PLANE::VW; + if (settings->motion_mode != motion) { + CHKS((!block->u_flag), + _readers[(int)'u']? NCE_U_VALUE_UNSPECIFIED_IN_VW_PLANE_CANNED_CYCLE: _("G19.1 canned cycle is not possible on a machine without U axis")); + } + block->u_number = + block->u_flag ? block->u_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->u_number; + aa = block->v_flag ? block->v_number : settings->v_current; + bb = block->w_flag ? block->w_number : settings->w_current; + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->v_flag) aa_increment = block->v_number; + if (block->w_flag) bb_increment = block->w_number; + r = (block->r_number + old_cc); + cc = (r + block->u_number); /* [NCMS, page 98] */ + aa = settings->v_current; + bb = settings->w_current; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_U_IN_CYCLE_IN_VW_PLANE); + + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, settings->current_x, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + r, settings->v_current, settings->w_current); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::VW, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::VW, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::VW, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::VW, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::VW, aa, bb, clear_cc, cc, + settings->spindle_turning[settings->active_spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::VW, aa, bb, r, clear_cc, cc)) + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::VW, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + i = (cc + i); /* i always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::VW, aa, (aa + j), bb, + (bb + k), r, clear_cc, i, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::VW, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::VW, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->v_current = aa; /* CYCLE_MACRO updates aa and bb */ + settings->w_current = bb; + settings->u_current = clear_cc; + settings->cycle_cc = block->u_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! convert_cycle_zx + +Returned Value: int + If any of the specific functions called returns an error code, + this returns that code. + If any of the following errors occur, this returns the ERROR code shown. + Otherwise, it returns INTERP_OK. + 1. The y-value is not given the first time this code is called after + some other motion mode has been in effect: + NCE_Y_VALUE_UNSPECIFIED_IN_XZ_PLANE_CANNED_CYCLE + 2. The r clearance plane is below the bottom_y: + NCE_R_LESS_THAN_Y_IN_CYCLE_IN_XZ_PLANE + 3. the distance mode is neither absolute or incremental: + NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91 + 4. G82, G86, G88, or G89 is called when it is not already in effect, + and no p number is in the block: + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88 + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89 + 5. G83 is called when it is not already in effect, + and no q number is in the block: NCE_Q_WORD_MISSING_WITH_G83 + 6. G87 is called when it is not already in effect, + and any of the i number, j number, or k number is missing: + NCE_I_WORD_MISSING_WITH_G87 + NCE_J_WORD_MISSING_WITH_G87 + NCE_K_WORD_MISSING_WITH_G87 + 7. the G-code is not between G_81 and G_89. + NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED + +Side effects: + A number of moves are made to execute a canned cycle. + +Called by: convert_cycle + +See the documentation of convert_cycle_xy. This function is entirely +similar. In this function aa represents z, bb represents x, and cc +represents y. + +The CYCLE_MACRO is defined just before the convert_cycle_xy function. + +Tool length offsets work only when the tool axis is parallel to the +Z-axis, so if this function is used, tool length offsets should be +turned off, and the NC code written to take tool length into account. + +It is a little distracting that this function uses zx in some places +and xz in others; uniform use of zx would be nice, since that is the +order for a right-handed coordinate system. Also with that usage, +permutation of the symbols x, y, and z would allow for automatically +converting the convert_cycle_xy function (or convert_cycle_yz) into +the convert_cycle_xz function. However, the canonical interface uses +CANON_PLANE::XZ. + +*/ + +int Interp::convert_cycle_zx(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + // save current path-following tolerance, to restore it later on + double save_tolerance, save_cam_tolerance; + double current_cc = settings->current_y; + + plane = CANON_PLANE::XZ; + if (settings->motion_mode != motion) { + CHKS((!block->y_flag), + _readers[(int)'y']? NCE_Y_VALUE_UNSPECIFIED_IN_XZ_PLANE_CANNED_CYCLE: _("G18 canned cycle is not possible on a machine without Y axis")); + } + block->y_number = + block->y_flag ? block->y_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->y_number; + aa = block->z_flag ? block->z_number : settings->current_z; + bb = block->x_flag ? block->x_number : settings->current_x; + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->z_flag) aa_increment = block->z_number; + if (block->x_flag) bb_increment = block->x_number; + r = (block->r_number + old_cc); + cc = (r + block->y_number); /* [NCMS, page 98] */ + aa = settings->current_z; + bb = settings->current_x; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_Y_IN_CYCLE_IN_XZ_PLANE); + + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, settings->current_x, r, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, settings->v_current, settings->w_current); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::XZ, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::XZ, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::XZ, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::XZ, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid E-number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::XZ, aa, bb, clear_cc, cc, + settings->spindle_turning[settings->active_spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::XZ, aa, bb, r, clear_cc, cc)); + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::XZ, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + j = (cc + j); /* j always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::XZ, aa, (aa + k), bb, + (bb + i), r, clear_cc, j, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::XZ, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::XZ, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->current_z = aa; /* CYCLE_MACRO updates aa and bb */ + settings->current_x = bb; + settings->current_y = clear_cc; + settings->cycle_cc = block->y_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} + +int Interp::convert_cycle_wu(int motion, //!< a G-code between G_81 and G_89, a canned cycle + block_pointer block, //!< pointer to a block of RS274 instructions + setup_pointer settings) //!< pointer to machine settings +{ + double aa; + double aa_increment=0.; + double bb; + double bb_increment=0.; + double cc; + double clear_cc; + double i; + double j; + double k; + double old_cc; + double radius_increment = 0.; + double theta_increment = 0.; + CANON_PLANE plane; + double r; + int repeat; + CANON_MOTION_MODE save_mode; + // save current path-following tolerance, to restore it later on + double save_tolerance, save_cam_tolerance; + double current_cc = settings->v_current; + + plane = CANON_PLANE::UW; + if (settings->motion_mode != motion) { + CHKS((!block->v_flag), + _readers[(int)'v']? NCE_V_VALUE_UNSPECIFIED_IN_UW_PLANE_CANNED_CYCLE: _("G18.1 canned cycle is not possible on a machine without V axis")); + } + block->v_number = + block->v_flag ? block->v_number : settings->cycle_cc; + if(settings->cycle_il_flag) { + old_cc = settings->cycle_il; + } else { + old_cc = settings->cycle_il = current_cc; + settings->cycle_il_flag = true; + } + + if (settings->distance_mode == DISTANCE_MODE::ABSOLUTE) { + aa_increment = 0.0; + bb_increment = 0.0; + r = block->r_number; + cc = block->v_number; + aa = block->w_flag ? block->w_number : settings->w_current; + bb = block->u_flag ? block->u_number : settings->u_current; + } else if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + if (block->w_flag) aa_increment = block->w_number; + if (block->u_flag) bb_increment = block->u_number; + r = (block->r_number + old_cc); + cc = (r + block->v_number); /* [NCMS, page 98] */ + aa = settings->w_current; + bb = settings->u_current; + } else + ERS(NCE_BUG_DISTANCE_MODE_NOT_G90_OR_G91); + CHKS((r < cc), NCE_R_LESS_THAN_V_IN_CYCLE_IN_UW_PLANE); + + if (old_cc < r) { + STRAIGHT_TRAVERSE(block->line_number, settings->current_x, settings->current_y, settings->current_z, + settings->AA_current, settings->BB_current, settings->CC_current, + settings->u_current, r, settings->w_current); + old_cc = r; + current_cc = old_cc; + } + clear_cc = (settings->retract_mode == RETRACT_MODE::R_PLANE) ? r : old_cc; + + save_mode = GET_EXTERNAL_MOTION_CONTROL_MODE(); + save_tolerance = GET_EXTERNAL_MOTION_CONTROL_TOLERANCE(); + save_cam_tolerance = GET_EXTERNAL_MOTION_CONTROL_NAIVECAM_TOLERANCE(); + if (save_mode != CANON_EXACT_PATH) + SET_MOTION_CONTROL_MODE(CANON_EXACT_PATH, 0); + + switch (motion) { + case G_81: + CYCLE_MACRO(convert_cycle_g81(block, CANON_PLANE::UW, aa, bb, clear_cc, cc)) + break; + case G_82: + CHKS(((settings->motion_mode != G_82) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G82); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g82(block, CANON_PLANE::UW, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + case G_73: + CHKS(((settings->motion_mode != G_73) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G73); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g73(block, CANON_PLANE::UW, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_83: + CHKS(((settings->motion_mode != G_83) && (block->q_number == -1.0)), + NCE_Q_WORD_MISSING_WITH_G83); + block->q_number = + block->q_number == -1.0 ? settings->cycle_q : block->q_number; + CYCLE_MACRO(convert_cycle_g83(block, CANON_PLANE::UW, aa, bb, r, clear_cc, cc, + block->q_number)) + settings->cycle_q = block->q_number; + break; + case G_74: + case G_84: + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g74_g84(block, CANON_PLANE::UW, aa, bb, clear_cc, cc, + settings->spindle_turning[settings->active_spindle], + settings->speed_feed_mode, + motion, block->p_number, settings->active_spindle)) + settings->cycle_p = block->p_number; + break; + case G_85: + CYCLE_MACRO(convert_cycle_g85(block, CANON_PLANE::UW, aa, bb, r, clear_cc, cc)) + break; + case G_86: + CHKS(((settings->motion_mode != G_86) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G86); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g86(block, CANON_PLANE::UW, aa, bb, clear_cc, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_87: + if (settings->motion_mode != G_87) { + CHKS((!block->i_flag), NCE_I_WORD_MISSING_WITH_G87); + CHKS((!block->j_flag), NCE_J_WORD_MISSING_WITH_G87); + CHKS((!block->k_flag), NCE_K_WORD_MISSING_WITH_G87); + } + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + i = block->i_flag ? block->i_number : settings->cycle_i; + j = block->j_flag ? block->j_number : settings->cycle_j; + k = block->k_flag ? block->k_number : settings->cycle_k; + settings->cycle_i = i; + settings->cycle_j = j; + settings->cycle_k = k; + if (settings->distance_mode == DISTANCE_MODE::INCREMENTAL) { + j = (cc + j); /* j always absolute in function call below */ + } + CYCLE_MACRO(convert_cycle_g87(block, CANON_PLANE::UW, aa, (aa + k), bb, + (bb + i), r, clear_cc, j, cc, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + break; + case G_88: + CHKS(((settings->motion_mode != G_88) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G88); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in G74/G84 cycle"))); + settings->active_spindle = (int)block->dollar_number; + } + CYCLE_MACRO(convert_cycle_g88(block, CANON_PLANE::UW, aa, bb, cc, + block->p_number, + settings->spindle_turning[settings->active_spindle], + settings->active_spindle)); + settings->cycle_p = block->p_number; + break; + case G_89: + CHKS(((settings->motion_mode != G_89) && (block->p_number == -1.0)), + NCE_DWELL_TIME_P_WORD_MISSING_WITH_G89); + block->p_number = + block->p_number == -1.0 ? settings->cycle_p : block->p_number; + CYCLE_MACRO(convert_cycle_g89(block, CANON_PLANE::UW, aa, bb, clear_cc, cc, + block->p_number)) + settings->cycle_p = block->p_number; + break; + default: + ERS(NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + } + settings->w_current = aa; /* CYCLE_MACRO updates aa and bb */ + settings->u_current = bb; + settings->v_current = clear_cc; + settings->cycle_cc = block->v_number; + + if (save_mode != CANON_EXACT_PATH) { + SET_MOTION_CONTROL_MODE(save_mode, save_tolerance); + SET_NAIVECAM_TOLERANCE(save_cam_tolerance); + } + + return INTERP_OK; +} +/****************************************************************************/ + +/*! cycle_feed + +Returned Value: int (INTERP_OK) + +Side effects: + STRAIGHT_FEED is called. + +Called by: + convert_cycle_g81 + convert_cycle_g82 + convert_cycle_g83 + convert_cycle_g74_g84 + convert_cycle_g85 + convert_cycle_g86 + convert_cycle_g87 + convert_cycle_g88 + convert_cycle_g89 + +This writes a STRAIGHT_FEED command appropriate for a cycle move with +respect to the given plane. No rotary axis motion takes place. + +*/ + +int Interp::cycle_feed(block_pointer block, + CANON_PLANE plane, //!< currently selected plane + double end1, //!< first coordinate value + double end2, //!< second coordinate value + double end3) //!< third coordinate value +{ + if (plane == CANON_PLANE::XY) + STRAIGHT_FEED(block->line_number, end1, end2, end3, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::YZ) + STRAIGHT_FEED(block->line_number, end3, end1, end2, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::XZ) + STRAIGHT_FEED(block->line_number, end2, end3, end1, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::UV) + STRAIGHT_FEED(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end1, end2, end3); + else if (plane == CANON_PLANE::VW) + STRAIGHT_FEED(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end3, end1, end2); + else // (plane == CANON_PLANE::UW) + STRAIGHT_FEED(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end2, end3, end1); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! cycle_traverse + +Returned Value: int (INTERP_OK) + +Side effects: + STRAIGHT_TRAVERSE is called. + +Called by: + convert_cycle + convert_cycle_g81 + convert_cycle_g82 + convert_cycle_g83 + convert_cycle_g86 + convert_cycle_g87 + convert_cycle_xy (via CYCLE_MACRO) + convert_cycle_yz (via CYCLE_MACRO) + convert_cycle_zx (via CYCLE_MACRO) + +This writes a STRAIGHT_TRAVERSE command appropriate for a cycle +move with respect to the given plane. No rotary axis motion takes place. + +*/ + +int Interp::cycle_traverse(block_pointer block, + CANON_PLANE plane, //!< currently selected plane + double end1, //!< first coordinate value + double end2, //!< second coordinate value + double end3) //!< third coordinate value +{ + + if (plane == CANON_PLANE::XY) + STRAIGHT_TRAVERSE(block->line_number, end1, end2, end3, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::YZ) + STRAIGHT_TRAVERSE(block->line_number, end3, end1, end2, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::XZ) + STRAIGHT_TRAVERSE(block->line_number, end2, end3, end1, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + _setup.u_current, _setup.v_current, _setup.w_current); + else if (plane == CANON_PLANE::UV) + STRAIGHT_TRAVERSE(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end1, end2, end3); + else if (plane == CANON_PLANE::VW) + STRAIGHT_TRAVERSE(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end3, end1, end2); + else // (plane == CANON_PLANE::UW) + STRAIGHT_TRAVERSE(block->line_number, _setup.current_x, _setup.current_y, _setup.current_z, + _setup.AA_current, _setup.BB_current, _setup.CC_current, + end2, end3, end1); + return INTERP_OK; +} diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_execute.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_execute.cc new file mode 100644 index 0000000..e30635d --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_execute.cc @@ -0,0 +1,422 @@ +/******************************************************************** +* Description: interp_execute.cc +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" + +#define RESULT_OK(x) ((x) == INTERP_OK || (x) == INTERP_EXECUTE_FINISH) + +/****************************************************************************/ + +/*! execute binary + +Returned value: int + If execute_binary1 or execute_binary2 returns an error code, this + returns that code. + Otherwise, it returns INTERP_OK. + +Side effects: The value of left is set to the result of applying + the operation to left and right. + +Called by: read_real_expression + +This just calls either execute_binary1 or execute_binary2. + +*/ + +int Interp::execute_binary(double *left, int operation, double *right) +{ + if (operation < AND2) + CHP(execute_binary1(left, operation, right)); + else + CHP(execute_binary2(left, operation, right)); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! execute_binary1 + +Returned Value: int + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. operation is unknown: NCE_BUG_UNKNOWN_OPERATION + 2. An attempt is made to divide by zero: NCE_ATTEMPT_TO_DIVIDE_BY_ZERO + 3. An attempt is made to raise a negative number to a non-integer power: + NCE_ATTEMPT_TO_RAISE_NEGATIVE_TO_NON_INTEGER_POWER + +Side effects: + The result from performing the operation is put into what left points at. + +Called by: read_real_expression. + +This executes the operations: DIVIDED_BY, MODULO, POWER, TIMES. + +*/ + +int Interp::execute_binary1(double *left, //!< pointer to the left operand + int operation, //!< integer code for the operation + double *right) //!< pointer to the right operand +{ + switch (operation) { + case DIVIDED_BY: + CHKS((*right == 0.0), NCE_ATTEMPT_TO_DIVIDE_BY_ZERO); + *left = (*left / *right); + break; + case MODULO: /* always calculates a positive answer */ + *left = fmod(*left, *right); + if (*left < 0.0) { + *left = (*left + fabs(*right)); + } + break; + case POWER: + CHKS(((*left < 0.0) && (floor(*right) != *right)), + NCE_ATTEMPT_TO_RAISE_NEGATIVE_TO_NON_INTEGER_POWER); + *left = pow(*left, *right); + break; + case TIMES: + *left = (*left * *right); + break; + default: + ERS(NCE_BUG_UNKNOWN_OPERATION); + } + return INTERP_OK; +} + +/****************************************************************************/ + +/*! execute_binary2 + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. operation is unknown: NCE_BUG_UNKNOWN_OPERATION + +Side effects: + The result from performing the operation is put into what left points at. + +Called by: read_real_expression. + +This executes the operations: AND2, EXCLUSIVE_OR, MINUS, +NON_EXCLUSIVE_OR, PLUS. The RS274/NGC manual [NCMS] does not say what +the calculated value of the three logical operations should be. This +function calculates either 1.0 (meaning true) or 0.0 (meaning false). +Any non-zero input value is taken as meaning true, and only 0.0 means +false. + + +*/ + +int Interp::execute_binary2(double *left, //!< pointer to the left operand + int operation, //!< integer code for the operation + double *right) //!< pointer to the right operand +{ + double diff; + switch (operation) { + case AND2: + *left = ((*left == 0.0) || (*right == 0.0)) ? 0.0 : 1.0; + break; + case EXCLUSIVE_OR: + *left = (((*left == 0.0) && (*right != 0.0)) + || ((*left != 0.0) && (*right == 0.0))) ? 1.0 : 0.0; + break; + case MINUS: + *left = (*left - *right); + break; + case NON_EXCLUSIVE_OR: + *left = ((*left != 0.0) || (*right != 0.0)) ? 1.0 : 0.0; + break; + case PLUS: + *left = (*left + *right); + break; + + case LT: + *left = (*left < *right) ? 1.0 : 0.0; + break; + case EQ: + diff = fabs(*left - *right); + *left = (diff < TOLERANCE_EQUAL) ? 1.0 : 0.0; + break; + case NE: + diff = fabs(*left - *right); + *left = (diff >= TOLERANCE_EQUAL) ? 1.0 : 0.0; + break; + case LE: + diff = fabs(*left - *right); + *left = ((diff < TOLERANCE_EQUAL) || (*left <= *right)) ? 1.0 : 0.0; + break; + case GE: + diff = fabs(*left - *right); + *left = ((diff < TOLERANCE_EQUAL) || (*left >= *right)) ? 1.0 : 0.0; + break; + case GT: + *left = (*left > *right) ? 1.0 : 0.0; + break; + + default: + ERS(NCE_BUG_UNKNOWN_OPERATION); + } + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! execute_block + +Returned Value: int + If convert_stop returns INTERP_EXIT, this returns INTERP_EXIT. + If any of the following functions is called and returns an error code, + this returns that code. + convert_comment + convert_feed_mode + convert_feed_rate + convert_g + convert_m + convert_speed + convert_stop + convert_tool_select + Otherwise, if the probe_flag in the settings is true, + or the input_flag is set to true this returns + INTERP_EXECUTE_FINISH. + Otherwise, it returns INTERP_OK. + +Side effects: + One block of RS274/NGC instructions is executed. + +Called by: + Interp::execute + +This converts a block to zero to many actions. The order of execution +of items in a block is critical to safe and effective machine operation, +but is not specified clearly in the RS274/NGC documentation. + +Actions are executed in the following order: +1. any comment. +2. a feed mode setting (g93, g94, g95) +3. a feed rate (f) setting if in units_per_minute feed mode. +4. a spindle speed (s) setting. +5. a tool selection (t). +6. "m" commands as described in convert_m (includes tool change). +7. any g_codes (except g93, g94) as described in convert_g. +8. stopping commands (m0, m1, m2, m30, or m60). + +In inverse time feed mode, the explicit and implicit g code executions +include feed rate setting with g1, g2, and g3. Also in inverse time +feed mode, attempting a canned cycle cycle (g81 to g89) or setting a +feed rate with g0 is illegal and will be detected and result in an +error message. + +*/ + +int Interp::execute_block(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer settings) //!< pointer to machine settings +{ + int status = INTERP_EXIT; + + block->line_number = settings->sequence_number; + if ((block->comment[0] != 0) && ONCE(STEP_COMMENT)) { + status = convert_comment(block->comment); + CHP(status); + } + if ((block->g_modes[GM_SPINDLE_MODE] != -1) && ONCE(STEP_SPINDLE_MODE)) { + settings->active_spindle = 0; //must be single-spindle, default to 0 + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in Spindle Mode command"))); + settings->active_spindle = (int)block->dollar_number; + } + status = convert_spindle_mode(settings->active_spindle, block, settings); + CHP(status); + } + if ((block->g_modes[GM_FEED_MODE] != -1) && ONCE(STEP_FEED_MODE)) { + settings->active_spindle = 0; //must be single-spindle, default to 0 + if (block->dollar_flag){ + CHKS((block->dollar_number < 0 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in Spindle Feed command"))); + settings->active_spindle = (int)block->dollar_number; + } + status = convert_feed_mode(block->g_modes[GM_FEED_MODE], settings); + CHP(status); + + } + if (block->f_flag){ + if ((settings->feed_mode != FEED_MODE::INVERSE_TIME) && ONCE(STEP_SET_FEED_RATE)) { + if (STEP_REMAPPED_IN_BLOCK(block, STEP_SET_FEED_RATE)) { + return (convert_remapped_code(block, settings, STEP_SET_FEED_RATE, 'F')); + } else { + status = convert_feed_rate(block, settings); + CHP(status); + } + } + /* INVERSE_TIME is handled elsewhere */ + } + if ((block->s_flag) && ONCE(STEP_SET_SPINDLE_SPEED)){ + if (STEP_REMAPPED_IN_BLOCK(block, STEP_SET_SPINDLE_SPEED)) { + return (convert_remapped_code(block,settings,STEP_SET_SPINDLE_SPEED,'S')); + } else { + if (block->dollar_flag){ + CHKS((block->dollar_number < -1 || block->dollar_number >= settings->num_spindles), + (_("Invalid spindle ($) number in Spindle speed command"))); + if (block->dollar_number == -1 ){ + for (int i = 0; i < settings->num_spindles; status = convert_speed(i++, block, settings)); + } else { + status = convert_speed(block->dollar_number, block, settings); + } + } else { + status = convert_speed(0, block, settings); + } + CHP(status); + } + } + if ((block->t_flag) && ONCE(STEP_PREPARE)) { + if (STEP_REMAPPED_IN_BLOCK(block, STEP_PREPARE)) { + return (convert_remapped_code(block,settings,STEP_PREPARE,'T')); + } else { + CHP(convert_tool_select(block, settings)); + } + } + CHP(convert_m(block, settings)); + CHP(convert_g(block, settings)); + /* convert m0, m1, m2, m30, m60, or (when main program loops disabled) m99 */ + if ((block->m_modes[4] != -1) && ONCE(STEP_MGROUP4)) { + if (STEP_REMAPPED_IN_BLOCK(block, STEP_MGROUP4)) { + status = convert_remapped_code(block,settings,STEP_MGROUP4,'M',block->m_modes[4]); + } else { + status = convert_stop(block, settings); + } + if (status == INTERP_EXIT) { + return(INTERP_EXIT); + } + else if (status != INTERP_OK) { + ERP(status); + } + } + if (settings->probe_flag) + return (INTERP_EXECUTE_FINISH); + + if (settings->input_flag) + return (INTERP_EXECUTE_FINISH); + + if (settings->toolchange_flag) + return (INTERP_EXECUTE_FINISH); + + // All changes to settings are complete + write_canon_state_tag(block, settings); + return INTERP_OK; +} + +/****************************************************************************/ + +/*! execute_unary + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. the operation is unknown: NCE_BUG_UNKNOWN_OPERATION + 2. the argument to acos is not between minus and plus one: + NCE_ARGUMENT_TO_ACOS_OUT_RANGE + 3. the argument to asin is not between minus and plus one: + NCE_ARGUMENT_TO_ASIN_OUT_RANGE + 4. the argument to the natural logarithm is not positive: + NCE_ZERO_OR_NEGATIVE_ARGUMENT_TO_LN + 5. the argument to square root is negative: + NCE_NEGATIVE_ARGUMENT_TO_SQRT + +Side effects: + The result from performing the operation on the value in double_ptr + is put into what double_ptr points at. + +Called by: read_unary. + +This executes the operations: ABS, ACOS, ASIN, COS, EXP, FIX, FUP, LN +ROUND, SIN, SQRT, TAN + +All angle measures in the input or output are in degrees. + +*/ + +int Interp::execute_unary(double *double_ptr, //!< pointer to the operand + int operation) //!< integer code for the operation +{ + switch (operation) { + case ABS: + if (*double_ptr < 0.0) + *double_ptr = (-1.0 * *double_ptr); + break; + case ACOS: + CHKS(((*double_ptr < -1.0) || (*double_ptr > 1.0)), + NCE_ARGUMENT_TO_ACOS_OUT_OF_RANGE); + *double_ptr = acos(*double_ptr); + *double_ptr = ((*double_ptr * 180.0) / M_PIl); + break; + case ASIN: + CHKS(((*double_ptr < -1.0) || (*double_ptr > 1.0)), + NCE_ARGUMENT_TO_ASIN_OUT_OF_RANGE); + *double_ptr = asin(*double_ptr); + *double_ptr = ((*double_ptr * 180.0) / M_PIl); + break; + case COS: + *double_ptr = cos((*double_ptr * M_PIl) / 180.0); + break; + case EXISTS: + // do nothing here + // result for the EXISTS function is set by Interp:read_unary() + break; + case EXP: + *double_ptr = exp(*double_ptr); + break; + case FIX: + *double_ptr = floor(*double_ptr); + break; + case FUP: + *double_ptr = ceil(*double_ptr); + break; + case LN: + CHKS((*double_ptr <= 0.0), NCE_ZERO_OR_NEGATIVE_ARGUMENT_TO_LN); + *double_ptr = log(*double_ptr); + break; + case ROUND: + *double_ptr = (double) + ((int) (*double_ptr + ((*double_ptr < 0.0) ? -0.5 : 0.5))); + break; + case SIN: + *double_ptr = sin((*double_ptr * M_PIl) / 180.0); + break; + case SQRT: + CHKS((*double_ptr < 0.0), NCE_NEGATIVE_ARGUMENT_TO_SQRT); + *double_ptr = sqrt(*double_ptr); + break; + case TAN: + *double_ptr = tan((*double_ptr * M_PIl) / 180.0); + break; + default: + ERS(NCE_BUG_UNKNOWN_OPERATION); + } + return INTERP_OK; +} + diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_find.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_find.cc new file mode 100644 index 0000000..3eb92c6 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_find.cc @@ -0,0 +1,758 @@ +/******************************************************************** +* Description: interp_find.cc +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "nml_intf/interp_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" +#include "units.h" +#include "tooldata/tooldata.hh" + +/****************************************************************************/ + +/*! find_arc_length + +Returned Value: double (length of path between start and end points) + +Side effects: none + +Called by: + inverse_time_rate_arc + inverse_time_rate_arc2 + inverse_time_rate_as + +This calculates the length of the path that will be made relative to +the XYZ axes for a motion in which the X,Y,Z, motion is a circular or +helical arc with its axis parallel to the Z-axis. If tool length +compensation is on, this is the path of the tool tip; if off, the +length of the path of the spindle tip. Any rotary axis motion is +ignored. + +If the arc is helical, it is coincident with the hypotenuse of a right +triangle wrapped around a cylinder. If the triangle is unwrapped, its +base is [the radius of the cylinder times the number of radians in the +helix] and its height is [z2 - z1], and the path length can be found +by the Pythagorean theorem. + +This is written as though it is only for arcs whose axis is parallel to +the Z-axis, but it will serve also for arcs whose axis is parallel +to the X-axis or Y-axis, with suitable permutation of the arguments. + +This works correctly when turn is zero (find_turn returns 0 in that +case). + +*/ + +double Interp::find_arc_length(double x1, //!< X-coordinate of start point + double y1, //!< Y-coordinate of start point + double z1, //!< Z-coordinate of start point + double center_x, //!< X-coordinate of arc center + double center_y, //!< Y-coordinate of arc center + int turn, //!< no. of full or partial circles CCW + double x2, //!< X-coordinate of end point + double y2, //!< Y-coordinate of end point + double z2) //!< Z-coordinate of end point +{ + double radius; + double theta; /* amount of turn of arc in radians */ + + radius = hypot((center_x - x1), (center_y - y1)); + theta = find_turn(x1, y1, center_x, center_y, turn, x2, y2); + if (z2 == z1) + return (radius * fabs(theta)); + else + return hypot((radius * theta), (z2 - z1)); +} + + +/* Find the real destination, given the axis's current position, the + commanded destination, and the direction to turn (which comes from + the sign of the commanded value in the gcode). Modulo 360 positions + of the axis are considered equivalent and we just need to find the + nearest one. */ + +int Interp::unwrap_rotary(double *r, double sign_of, double commanded, double current, char axis) { + double result; + int neg = copysign(1.0, sign_of) < 0.0; + CHKS((sign_of <= -360.0 || sign_of >= 360.0), (_("Invalid absolute position %5.2f for wrapped rotary axis %c")), sign_of, axis); + + double d = floor(current/360.0); + result = fabs(commanded) + (d*360.0); + if(!neg && result < current) result += 360.0; + if(neg && result > current) result -= 360.0; + *r = result; + + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! find_ends + +Returned Value: int (INTERP_OK) + +Side effects: + The values of px, py, pz, aa_p, bb_p, and cc_p are set + +Called by: + convert_arc + convert_home + convert_probe + convert_straight + +This finds the coordinates of a point, "end", in the currently +active coordinate system, and sets the values of the pointers to the +coordinates (which are the arguments to the function). + +In all cases, if no value for the coordinate is given in the block, the +current value for the coordinate is used. When cutter radius +compensation is on, this function is called before compensation +calculations are performed, so the current value of the programmed +point is used, not the current value of the actual current_point. + +There are three cases for when the coordinate is included in the block: + +1. G_53 is active. This means to interpret the coordinates as machine +coordinates. That is accomplished by adding the three offsets to the +coordinates given in the block. The x,y block coordinates are also +rotated. The end result is the machine coordinates in the block are +converted to coordinates in the current system. + +2. Absolute coordinate mode is in effect. The coordinate in the block +is used. + +3. Incremental coordinate mode is in effect. The coordinate in the +block plus either (i) the programmed current position - when cutter +radius compensation is in progress, or (2) the actual current position. + +*/ + + +int Interp::find_ends(block_pointer block, //!< pointer to a block of RS274/NGC instructions + setup_pointer s, //!< pointer to machine settings + double *px, //!< pointer to end_x + double *py, //!< pointer to end_y + double *pz, //!< pointer to end_z + double *AA_p, //!< pointer to end_a + double *BB_p, //!< pointer to end_b + double *CC_p, //!< pointer to end_c + double *u_p, double *v_p, double *w_p) +{ + bool middle; + CUTTER_COMP comp; + + middle = !s->cutter_comp_firstmove; + comp = s->cutter_comp_side; + + if (block->g_modes[GM_MODAL_0] == G_53) { /* distance mode is absolute in this case */ +#ifdef DEBUG_EMC + COMMENT("interpreter: offsets temporarily suspended"); +#endif + CHKS((block->radius_flag || block->theta_flag), _("Cannot use polar coordinates with G53")); + + double cx = s->current_x + s->axis_offset_x; + double cy = s->current_y + s->axis_offset_y; + rotate(&cx, &cy, s->rotation_xy); + + if(block->x_flag) { + *px = block->x_number - s->origin_offset_x - s->tool_offset.tran.x; + } else { + *px = cx; + } + + if(block->y_flag) { + *py = block->y_number - s->origin_offset_y - s->tool_offset.tran.y; + } else { + *py = cy; + } + + rotate(px, py, -s->rotation_xy); + *px -= s->axis_offset_x; + *py -= s->axis_offset_y; + + if(block->z_flag) { + *pz = block->z_number - s->origin_offset_z - s->axis_offset_z - s->tool_offset.tran.z; + } else { + *pz = s->current_z; + } + + if(block->a_flag) { + if(s->a_axis_wrapped) { + CHP(unwrap_rotary(AA_p, block->a_number, + block->a_number - s->AA_origin_offset - s->AA_axis_offset - s->tool_offset.a, + s->AA_current, 'A')); + } else { + *AA_p = block->a_number - s->AA_origin_offset - s->AA_axis_offset; + } + } else { + *AA_p = s->AA_current; + } + + if(block->b_flag) { + if(s->b_axis_wrapped) { + CHP(unwrap_rotary(BB_p, block->b_number, + block->b_number - s->BB_origin_offset - s->BB_axis_offset - s->tool_offset.b, + s->BB_current, 'B')); + } else { + *BB_p = block->b_number - s->BB_origin_offset - s->BB_axis_offset; + } + } else { + *BB_p = s->BB_current; + } + + if(block->c_flag) { + if(s->c_axis_wrapped) { + CHP(unwrap_rotary(CC_p, block->c_number, + block->c_number - s->CC_origin_offset - s->CC_axis_offset - s->tool_offset.c, + s->CC_current, 'C')); + } else { + *CC_p = block->c_number - s->CC_origin_offset - s->CC_axis_offset; + } + } else { + *CC_p = s->CC_current; + } + + if(block->u_flag) { + *u_p = block->u_number - s->u_origin_offset - s->u_axis_offset - s->tool_offset.u; + } else { + *u_p = s->u_current; + } + + if(block->v_flag) { + *v_p = block->v_number - s->v_origin_offset - s->v_axis_offset - s->tool_offset.v; + } else { + *v_p = s->v_current; + } + + if(block->w_flag) { + *w_p = block->w_number - s->w_origin_offset - s->w_axis_offset - s->tool_offset.w; + } else { + *w_p = s->w_current; + } + } else if (s->distance_mode == DISTANCE_MODE::ABSOLUTE) { + + if(block->x_flag) { + *px = block->x_number; + } else { + // both cutter comp planes affect X ... + *px = (comp != CUTTER_COMP::OFF && middle) ? s->program_x : s->current_x; + } + + if(block->y_flag) { + *py = block->y_number; + } else { + // but only XY affects Y ... + *py = (comp != CUTTER_COMP::OFF && middle && s->plane == CANON_PLANE::XY) ? s->program_y : s->current_y; + } + + if(block->radius_flag && block->theta_flag) { + CHKS((block->x_flag || block->y_flag), _("Cannot specify X or Y words with polar coordinate")); + *px = block->radius * cos(D2R(block->theta)); + *py = block->radius * sin(D2R(block->theta)); + } else if(block->radius_flag) { + double theta; + CHKS((block->x_flag || block->y_flag), _("Cannot specify X or Y words with polar coordinate")); + CHKS((*py == 0 && *px == 0), _("Must specify angle in polar coordinate if at the origin")); + theta = atan2(*py, *px); + *px = block->radius * cos(theta); + *py = block->radius * sin(theta); + } else if(block->theta_flag) { + double radius; + CHKS((block->x_flag || block->y_flag), _("Cannot specify X or Y words with polar coordinate")); + radius = hypot(*py, *px); + *px = radius * cos(D2R(block->theta)); + *py = radius * sin(D2R(block->theta)); + } + + if(block->z_flag) { + *pz = block->z_number; + } else { + // and only XZ affects Z. + *pz = (comp != CUTTER_COMP::OFF && middle && s->plane == CANON_PLANE::XZ) ? s->program_z : s->current_z; + } + + if(block->a_flag) { + if(s->a_axis_wrapped) { + CHP(unwrap_rotary(AA_p, block->a_number, block->a_number, s->AA_current, 'A')); + } else { + *AA_p = block->a_number; + } + } else { + *AA_p = s->AA_current; + } + + if(block->b_flag) { + if(s->b_axis_wrapped) { + CHP(unwrap_rotary(BB_p, block->b_number, block->b_number, s->BB_current, 'B')); + } else { + *BB_p = block->b_number; + } + } else { + *BB_p = s->BB_current; + } + + if(block->c_flag) { + if(s->c_axis_wrapped) { + CHP(unwrap_rotary(CC_p, block->c_number, block->c_number, s->CC_current, 'C')); + } else { + *CC_p = block->c_number; + } + } else { + *CC_p = s->CC_current; + } + + *u_p = (block->u_flag) ? block->u_number : s->u_current; + *v_p = (block->v_flag) ? block->v_number : s->v_current; + *w_p = (block->w_flag) ? block->w_number : s->w_current; + + } else { /* mode is DISTANCE_MODE::INCREMENTAL */ + + // both cutter comp planes affect X ... + *px = (comp != CUTTER_COMP::OFF && middle) ? s->program_x: s->current_x; + if(block->x_flag) *px += block->x_number; + + // but only XY affects Y ... + *py = (comp != CUTTER_COMP::OFF && middle && s->plane == CANON_PLANE::XY) ? s->program_y: s->current_y; + if(block->y_flag) *py += block->y_number; + + if(block->radius_flag) { + double radius, theta; + CHKS((block->x_flag || block->y_flag), _("Cannot specify X or Y words with polar coordinate")); + CHKS((*py == 0 && *px == 0), _("Incremental motion with polar coordinates is indeterminate when at the origin")); + theta = atan2(*py, *px); + radius = hypot(*py, *px) + block->radius; + *px = radius * cos(theta); + *py = radius * sin(theta); + } + + if(block->theta_flag) { + double radius, theta; + CHKS((block->x_flag || block->y_flag), _("Cannot specify X or Y words with polar coordinate")); + CHKS((*py == 0 && *px == 0), _("G91 motion with polar coordinates is indeterminate when at the origin")); + theta = atan2(*py, *px) + D2R(block->theta); + radius = hypot(*py, *px); + *px = radius * cos(theta); + *py = radius * sin(theta); + } + + // and only XZ affects Z. + *pz = (comp != CUTTER_COMP::OFF && middle && s->plane == CANON_PLANE::XZ) ? s->program_z: s->current_z; + if(block->z_flag) *pz += block->z_number; + + *AA_p = s->AA_current; + if(block->a_flag) *AA_p += block->a_number; + + *BB_p = s->BB_current; + if(block->b_flag) *BB_p += block->b_number; + + *CC_p = s->CC_current; + if(block->c_flag) *CC_p += block->c_number; + + *u_p = s->u_current; + if(block->u_flag) *u_p += block->u_number; + + *v_p = s->v_current; + if(block->v_flag) *v_p += block->v_number; + + *w_p = s->w_current; + if(block->w_flag) *w_p += block->w_number; + } + return INTERP_OK; +} + +/****************************************************************************/ + +/*! find_relative + +Returned Value: int (INTERP_OK) + +Side effects: + The values of x2, y2, z2, aa_2, bb_2, and cc_2 are set. + (NOTE: aa_2 etc. are written with lower case letters in this + documentation because upper case would confuse the pre-preprocessor.) + +Called by: + convert_home + +This finds the coordinates in the current system, under the current +tool length offset, of a point (x1, y1, z1, aa_1, bb_1, cc_1) whose absolute +coordinates are known. + +Don't confuse this with the inverse operation. + +*/ + +int Interp::find_relative(double x1, //!< absolute x position + double y1, //!< absolute y position + double z1, //!< absolute z position + double AA_1, //!< absolute a position + double BB_1, //!< absolute b position + double CC_1, //!< absolute c position + double u_1, + double v_1, + double w_1, + double *x2, //!< pointer to relative x + double *y2, //!< pointer to relative y + double *z2, //!< pointer to relative z + double *AA_2, //!< pointer to relative a + double *BB_2, //!< pointer to relative b + double *CC_2, //!< pointer to relative c + double *u_2, + double *v_2, + double *w_2, + setup_pointer settings) //!< pointer to machine settings +{ + *x2 = x1 - settings->origin_offset_x - settings->tool_offset.tran.x; + *y2 = y1 - settings->origin_offset_y - settings->tool_offset.tran.y; + rotate(x2, y2, -settings->rotation_xy); + *x2 -= settings->axis_offset_x; + *y2 -= settings->axis_offset_y; + *z2 = z1 - settings->origin_offset_z - settings->axis_offset_z - settings->tool_offset.tran.z; + + if(settings->a_axis_wrapped) { + CHP(unwrap_rotary(AA_2, AA_1, + AA_1 - settings->AA_origin_offset - settings->AA_axis_offset - settings->tool_offset.a, + settings->AA_current, 'A')); + } else { + *AA_2 = AA_1 - settings->AA_origin_offset - settings->AA_axis_offset; + } + + if(settings->b_axis_wrapped) { + CHP(unwrap_rotary(BB_2, BB_1, + BB_1 - settings->BB_origin_offset - settings->BB_axis_offset - settings->tool_offset.b, + settings->BB_current, 'B')); + } else { + *BB_2 = BB_1 - settings->BB_origin_offset - settings->BB_axis_offset; + } + + if(settings->c_axis_wrapped) { + CHP(unwrap_rotary(CC_2, CC_1, + CC_1 - settings->CC_origin_offset - settings->CC_axis_offset - settings->tool_offset.c, + settings->CC_current, 'C')); + } else { + *CC_2 = CC_1 - settings->CC_origin_offset - settings->CC_axis_offset; + } + + *u_2 = u_1 - settings->u_origin_offset - settings->u_axis_offset - settings->tool_offset.u; + *v_2 = v_1 - settings->v_origin_offset - settings->v_axis_offset - settings->tool_offset.v; + *w_2 = w_1 - settings->w_origin_offset - settings->w_axis_offset - settings->tool_offset.w; + return INTERP_OK; +} + +// find what the current coordinates would be if we were in a different system + +int Interp::find_current_in_system(setup_pointer s, int system, + double *x, double *y, double *z, + double *a, double *b, double *c, + double *u, double *v, double *w) { + double *p = s->parameters; + + *x = s->current_x; + *y = s->current_y; + *z = s->current_z; + *a = s->AA_current; + *b = s->BB_current; + *c = s->CC_current; + *u = s->u_current; + *v = s->v_current; + *w = s->w_current; + + *x += s->axis_offset_x; + *y += s->axis_offset_y; + *z += s->axis_offset_z; + *a += s->AA_axis_offset; + *b += s->BB_axis_offset; + *c += s->CC_axis_offset; + *u += s->u_axis_offset; + *v += s->v_axis_offset; + *w += s->w_axis_offset; + + rotate(x, y, s->rotation_xy); + + *x += s->origin_offset_x; + *y += s->origin_offset_y; + *z += s->origin_offset_z; + *a += s->AA_origin_offset; + *b += s->BB_origin_offset; + *c += s->CC_origin_offset; + *u += s->u_origin_offset; + *v += s->v_origin_offset; + *w += s->w_origin_offset; + + *x -= USER_TO_PROGRAM_LEN(p[5201 + system * 20]); + *y -= USER_TO_PROGRAM_LEN(p[5202 + system * 20]); + *z -= USER_TO_PROGRAM_LEN(p[5203 + system * 20]); + *a -= USER_TO_PROGRAM_ANG(p[5204 + system * 20]); + *b -= USER_TO_PROGRAM_ANG(p[5205 + system * 20]); + *c -= USER_TO_PROGRAM_ANG(p[5206 + system * 20]); + *u -= USER_TO_PROGRAM_LEN(p[5207 + system * 20]); + *v -= USER_TO_PROGRAM_LEN(p[5208 + system * 20]); + *w -= USER_TO_PROGRAM_LEN(p[5209 + system * 20]); + + rotate(x, y, -p[5210 + system * 20]); + + if (p[5210]) { + *x -= USER_TO_PROGRAM_LEN(p[5211]); + *y -= USER_TO_PROGRAM_LEN(p[5212]); + *z -= USER_TO_PROGRAM_LEN(p[5213]); + *a -= USER_TO_PROGRAM_ANG(p[5214]); + *b -= USER_TO_PROGRAM_ANG(p[5215]); + *c -= USER_TO_PROGRAM_ANG(p[5216]); + *u -= USER_TO_PROGRAM_LEN(p[5217]); + *v -= USER_TO_PROGRAM_LEN(p[5218]); + *w -= USER_TO_PROGRAM_LEN(p[5219]); + } + + return INTERP_OK; +} + + +// find what the current coordinates would be if we were in a different system, +// if TLO were unapplied + +int Interp::find_current_in_system_without_tlo(setup_pointer s, int system, + double *x, double *y, double *z, + double *a, double *b, double *c, + double *u, double *v, double *w) { + double *p = s->parameters; + + *x = s->current_x; + *y = s->current_y; + *z = s->current_z; + *a = s->AA_current; + *b = s->BB_current; + *c = s->CC_current; + *u = s->u_current; + *v = s->v_current; + *w = s->w_current; + + *x += s->axis_offset_x; + *y += s->axis_offset_y; + *z += s->axis_offset_z; + *a += s->AA_axis_offset; + *b += s->BB_axis_offset; + *c += s->CC_axis_offset; + *u += s->u_axis_offset; + *v += s->v_axis_offset; + *w += s->w_axis_offset; + + rotate(x, y, s->rotation_xy); + + *x += s->origin_offset_x; + *y += s->origin_offset_y; + *z += s->origin_offset_z; + *a += s->AA_origin_offset; + *b += s->BB_origin_offset; + *c += s->CC_origin_offset; + *u += s->u_origin_offset; + *v += s->v_origin_offset; + *w += s->w_origin_offset; + + *x += s->tool_offset.tran.x; + *y += s->tool_offset.tran.y; + *z += s->tool_offset.tran.z; + *a += s->tool_offset.a; + *b += s->tool_offset.b; + *c += s->tool_offset.c; + *u += s->tool_offset.u; + *v += s->tool_offset.v; + *w += s->tool_offset.w; + + *x -= USER_TO_PROGRAM_LEN(p[5201 + system * 20]); + *y -= USER_TO_PROGRAM_LEN(p[5202 + system * 20]); + *z -= USER_TO_PROGRAM_LEN(p[5203 + system * 20]); + *a -= USER_TO_PROGRAM_ANG(p[5204 + system * 20]); + *b -= USER_TO_PROGRAM_ANG(p[5205 + system * 20]); + *c -= USER_TO_PROGRAM_ANG(p[5206 + system * 20]); + *u -= USER_TO_PROGRAM_LEN(p[5207 + system * 20]); + *v -= USER_TO_PROGRAM_LEN(p[5208 + system * 20]); + *w -= USER_TO_PROGRAM_LEN(p[5209 + system * 20]); + + rotate(x, y, -p[5210 + system * 20]); + + if (p[5210]) { + *x -= USER_TO_PROGRAM_LEN(p[5211]); + *y -= USER_TO_PROGRAM_LEN(p[5212]); + *z -= USER_TO_PROGRAM_LEN(p[5213]); + *a -= USER_TO_PROGRAM_ANG(p[5214]); + *b -= USER_TO_PROGRAM_ANG(p[5215]); + *c -= USER_TO_PROGRAM_ANG(p[5216]); + *u -= USER_TO_PROGRAM_LEN(p[5217]); + *v -= USER_TO_PROGRAM_LEN(p[5218]); + *w -= USER_TO_PROGRAM_LEN(p[5219]); + } + + return INTERP_OK; +} + +/****************************************************************************/ + +/*! find_straight_length + +Returned Value: double (length of path between start and end points) + +Side effects: none + +Called by: + inverse_time_rate_straight + inverse_time_rate_as + +This calculates a number to use in feed rate calculations when inverse +time feed mode is used, for a motion in which X,Y,Z,A,B, and C each change +linearly or not at all from their initial value to their end value. + +This is used when the feed_reference mode is CANON_XYZ, which is +always in rs274NGC. + +If any of the X, Y, or Z axes move or the A-axis, B-axis, and C-axis +do not move, this is the length of the path relative to the XYZ axes +from the first point to the second, and any rotary axis motion is +ignored. The length is the simple Euclidean distance. + +The formula for the Euclidean distance "length" of a move involving +only the A, B and C axes is based on a conversation with Jim Frohardt at +Boeing, who says that the Fanuc controller on their 5-axis machine +interprets the feed rate this way. Note that if only one rotary axis +moves, this formula returns the absolute value of that axis move, +which is what is desired. + +*/ + +double Interp::find_straight_length(double x2, //!< X-coordinate of end point + double y2, //!< Y-coordinate of end point + double z2, //!< Z-coordinate of end point + double AA_2, //!< A-coordinate of end point + double BB_2, //!< B-coordinate of end point + double CC_2, //!< C-coordinate of end point + double u_2, + double v_2, + double w_2, + double x1, //!< X-coordinate of start point + double y1, //!< Y-coordinate of start point + double z1, //!< Z-coordinate of start point + double AA_1, //!< A-coordinate of start point + double BB_1, //!< B-coordinate of start point + double CC_1, //!< C-coordinate of start point + double u_1, + double v_1, + double w_1 + ) +{ +#define tiny 1e-7 + if ( (fabs(x1-x2) > tiny) || (fabs(y1-y2) > tiny) || (fabs(z1-z2) > tiny) ) + return sqrt(pow((x2 - x1), 2) + pow((y2 - y1), 2) + pow((z2 - z1), 2)); + else if ( (fabs(u_1-u_2) > tiny) || (fabs(v_1-v_2) > tiny) || (fabs(w_1-w_2) > tiny) ) + return sqrt(pow((u_2 - u_1), 2) + pow((v_2 - v_1), 2) + pow((w_2 - w_1), 2)); + else + return sqrt(pow((AA_2 - AA_1), 2) + pow((BB_2 - BB_1), 2) + pow((CC_2 - CC_1), 2)); +} + +/****************************************************************************/ + +/*! find_turn + +Returned Value: double (angle in radians between two radii of a circle) + +Side effects: none + +Called by: find_arc_length + +All angles are in radians. + +*/ + +double Interp::find_turn(double x1, //!< X-coordinate of start point + double y1, //!< Y-coordinate of start point + double center_x, //!< X-coordinate of arc center + double center_y, //!< Y-coordinate of arc center + int turn, //!< no. of full or partial circles CCW + double x2, //!< X-coordinate of end point + double y2) //!< Y-coordinate of end point +{ + double alpha; /* angle of first radius */ + double beta; /* angle of second radius */ + double theta; /* amount of turn of arc CCW - negative if CW */ + + if (turn == 0) + return 0.0; + alpha = atan2((y1 - center_y), (x1 - center_x)); + beta = atan2((y2 - center_y), (x2 - center_x)); + if (turn > 0) { + if (beta <= alpha) + beta = (beta + (2 * M_PIl)); + theta = ((beta - alpha) + ((turn - 1) * (2 * M_PIl))); + } else { /* turn < 0 */ + + if (alpha <= beta) + alpha = (alpha + (2 * M_PIl)); + theta = ((beta - alpha) + ((turn + 1) * (2 * M_PIl))); + } + return (theta); +} + +int Interp::find_tool_index(setup_pointer settings, int toolno, int *index) +{ + +#ifdef TOOL_NML //{ + if(!settings->random_toolchanger && toolno == 0) { + *index = 0; + return INTERP_OK; + } +#else //}{ + (void)settings; + // special case is included in tooldata_find_index_for_tool() +#endif //} + + *index = tooldata_find_index_for_tool(toolno); + + CHKS((*index == -1), (_("Requested tool %d not found in the tool table")), toolno); + return INTERP_OK; +} + +int Interp::find_tool_pocket(setup_pointer settings, int toolno, int *pocket) +{ +#ifdef TOOL_NML //{ + if(!settings->random_toolchanger && toolno == 0) { + *pocket = 0; + return INTERP_OK; + } +#else //}{ + (void)settings; + // special case is included in tooldata_find_index_for_tool() +#endif //} + int idx = tooldata_find_index_for_tool(toolno); + *pocket = 0; //not found + CHKS((idx == -1), (_("Requested tool %d not found in the tool table")), toolno); + + CANON_TOOL_TABLE tdata = tooldata_entry_init(); + if (tooldata_get(&tdata,idx) != IDX_OK) { + fprintf(stderr,"UNEXPECTED idx %s %d\n",__FILE__,__LINE__); + } + *pocket = tdata.pocketno; + + return INTERP_OK; +} + diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_fwd.hh b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_fwd.hh new file mode 100644 index 0000000..3a4e096 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_fwd.hh @@ -0,0 +1,42 @@ +/** + * @file interp_fwd.hh + * + * Forward declarations for interp_internal.hh. + * + * @author Robert W. Ellenberg + * + * Copyright (c) 2019, Robert W. Ellenberg + * + * This source code is released for free distribution under the terms of the + * GNU General Public License (V2) as published by the Free Software Foundation. + */ + +#ifndef INTERP_FWD_HH +#define INTERP_FWD_HH + +class Interp; + +struct block_struct; +typedef struct block_struct *block_pointer; +typedef struct block_struct block; + +struct setup; +typedef struct setup *setup_pointer; + +struct remap_struct; +typedef struct remap_struct *remap_pointer; +typedef struct remap_struct remap; + +struct context_struct; +typedef struct context_struct *context_pointer; +typedef struct context_struct context; + +struct parameter_value_struct; +typedef parameter_value_struct *parameter_pointer; +typedef parameter_value_struct parameter_value; + +struct offset_struct; +typedef offset_struct *offset_pointer; +typedef offset_struct offset; + +#endif // INTERP_FWD_HH diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.cc new file mode 100644 index 0000000..5d4156d --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.cc @@ -0,0 +1,494 @@ +/******************************************************************** +* Description: interp_internal.cc +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: +********************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "interp_internal.hh" // interpreter private definitions +#include "rs274ngc_interp.hh" +#include + +/****************************************************************************/ + +/*! close_and_downcase + +Returned Value: int + If any of the following errors occur, this returns the error code shown. + Otherwise, it returns INTERP_OK. + 1. A left parenthesis is found inside a comment: + NCE_NESTED_COMMENT_FOUND + 2. The line ends before an open comment is closed: + NCE_UNCLOSED_COMMENT_FOUND + 3. A newline character is found that is not followed by null: + NCE_NULL_MISSING_AFTER_NEWLINE + +Side effects: See below + +Called by: read_text + +To simplify handling upper case letters, spaces, and tabs, this +function removes spaces and tabs and downcases everything on a +line which is not part of a comment. + +Comments are left unchanged in place. Comments are anything +enclosed in parentheses. Nested comments, indicated by a left +parenthesis inside a comment, are illegal. + +The line must have a null character at the end when it comes in. +The line may have one newline character just before the end. If +there is a newline, it will be removed. + +Although this software system detects and rejects all illegal characters +and illegal syntax, this particular function does not detect problems +with anything but comments. + +We are treating RS274 code here as case-insensitive and spaces and +tabs as if they have no meaning. [RS274D, page 6] says spaces and tabs +are to be ignored by control. + +The KT and NGC manuals say nothing about case or spaces and tabs. + +*/ + +int Interp::close_and_downcase(char *line) //!< string: one line of NC code +{ + int m; + int n; + int comment, semicomment; + char item; + comment = semicomment = 0; + for (n = 0, m = 0; (item = line[m]) != '\0'; m++) { + if ((item == ';') && !comment) + semicomment = 1; + + if (semicomment) { + line[n++] = item; // pass literally + continue; + } + if (comment) { + line[n++] = item; + if (item == ')') { + comment = 0; + } else if (item == '(') + ERS(NCE_NESTED_COMMENT_FOUND); + } else if ((item == ' ') || (item == '\t') || (item == '\r')); + /* don't copy blank or tab or CR */ + else if (item == '\n') { /* don't copy newline *//* but check null follows */ + CHKS((line[m + 1] != 0), NCE_NULL_MISSING_AFTER_NEWLINE); + } else if ((64 < item) && (item < 91)) { /* downcase upper case letters */ + line[n++] = (32 + item); + } else if ((item == '(') && !semicomment) { /* (comment is starting */ + comment = 1; + line[n++] = item; + } else { + line[n++] = item; /* copy anything else */ + } + } + CHKS((comment), NCE_UNCLOSED_COMMENT_FOUND); + line[n] = 0; + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! enhance_block + +Returned Value: + If any of the following errors occur, this returns the error shown. + Otherwise, it returns INTERP_OK. + 1. A g80 is in the block, no modal group 0 code that uses axes + is in the block, and one or more axis values is given: + NCE_CANNOT_USE_AXIS_VALUES_WITH_G80 + 2. A g52 g92 is in the block and no axis value is given: + NCE_ALL_AXES_MISSING_WITH_G52_OR_G92 + 3. One G-code from group 1 and one from group 0, both of which can use + axis values, are in the block: + NCE_CANNOT_USE_TWO_G_CODES_THAT_BOTH_USE_AXIS_VALUES + 4. A G-code (other than 0 or 1, for which we are allowing all axes + missing) from group 1 which can use axis values is in the block, + but no axis value is given: NCE_ALL_AXES_MISSING_WITH_MOTION_CODE + 5. Axis values are given, but there is neither a G-code in the block + nor an active previously given modal G-code that uses axis values: + NCE_CANNOT_USE_AXIS_VALUES_WITHOUT_A_G_CODE_THAT_USES_THEM + +Side effects: + The value of motion_to_be in the block is set. + +Called by: parse_line + +If there is a G-code for motion in the block (in g_modes[1]), +set motion_to_be to that. Otherwise, if there is an axis value in the +block and no G-code to use it (any such would be from group 0 in +g_modes[0]), set motion_to_be to be the last motion saved (in +settings->motion mode). + +This also make the checks described above. + +*/ + +int Interp::enhance_block(block_pointer block, //!< pointer to a block to be checked + setup_pointer settings) //!< pointer to machine settings +{ + int axis_flag; + int ijk_flag; + int polar_flag; + int mode_zero_covets_axes; + int mode0; + int mode1; + + if(block->radius_flag || block->theta_flag) { + // someday, tediously add polar support for other planes here: + CHKS((!_readers[(int)'x'] || !_readers[(int)'y']), _("Cannot use polar coordinate on a machine lacking X or Y axes")); + CHKS(((settings->plane != CANON_PLANE::XY)), _("Cannot use polar coordinate except in G17 plane")); + CHKS(((block->x_flag)), _("Cannot specify both polar coordinate and X word")); + CHKS(((block->y_flag)), _("Cannot specify both polar coordinate and Y word")); + } + + axis_flag = ((block->x_flag) || (block->y_flag) || + (block->z_flag) || (block->a_flag) || + (block->b_flag) || (block->c_flag) || + (block->u_flag) || (block->v_flag) || + (block->w_flag)); + polar_flag = (block->radius_flag) || (block->theta_flag); + ijk_flag = ((block->i_flag) || (block->j_flag) || + (block->k_flag)); + mode0 = block->g_modes[GM_MODAL_0]; + mode1 = block->g_modes[GM_MOTION]; + mode_zero_covets_axes = + ((mode0 == G_10) || (mode0 == G_28) || (mode0 == G_30) + || (mode0 == G_52) || (mode0 == G_92)); + + if (mode1 != -1) { + if (mode1 == G_80) { + CHKS(((polar_flag || axis_flag) && (!mode_zero_covets_axes)), + NCE_CANNOT_USE_AXIS_VALUES_WITH_G80); + CHKS((polar_flag && mode0 == G_92), _("Polar coordinates can only be used for motion")); + CHKS(((!axis_flag) && (mode0 == G_52 || mode0 == G_92)), + NCE_ALL_AXES_MISSING_WITH_G52_OR_G92); + } else { + CHKS(mode_zero_covets_axes, NCE_CANNOT_USE_TWO_G_CODES_THAT_BOTH_USE_AXIS_VALUES); + CHKS(((!axis_flag && !polar_flag) && + mode1 != G_0 && mode1 != G_1 && + mode1 != G_2 && mode1 != G_3 && + mode1 != G_5_2 && + mode1 != G_6_2 && + mode1 != G_70 && + mode1 != G_71 && mode1 != G_71_1 && mode1 != G_71_2 && + mode1 != G_72 && mode1 != G_72_1 && mode1 != G_72_2 && + !is_user_defined_g_code(mode1)), + NCE_ALL_AXES_MISSING_WITH_MOTION_CODE); + } + block->motion_to_be = mode1; + } else if (mode_zero_covets_axes) { /* other 3 can get by without axes but not G92 */ + CHKS((polar_flag && mode0 == G_92), _("Polar coordinates can only be used for motion")); + CHKS(((!axis_flag) && + (block->g_modes[GM_MODAL_0] == G_52 || block->g_modes[GM_MODAL_0] == G_92)), + NCE_ALL_AXES_MISSING_WITH_G52_OR_G92); + } else if (axis_flag || polar_flag) { + CHKS(((settings->motion_mode == -1) + || (settings->motion_mode == G_80)) && (block->g_modes[GM_TOOL_LENGTH_OFFSET] != G_43_1) + && (block->g_modes[GM_TOOL_LENGTH_OFFSET] != G_43_2), + NCE_CANNOT_USE_AXIS_VALUES_WITHOUT_A_G_CODE_THAT_USES_THEM); + if (block->g_modes[GM_TOOL_LENGTH_OFFSET] != G_43_1) { + block->motion_to_be = settings->motion_mode; + } + } else if (!axis_flag && !polar_flag && ijk_flag && (settings->motion_mode == G_2 || settings->motion_mode == G_3)) { + // this is a block like simply "i1" which should be accepted if we're in arc mode + block->motion_to_be = settings->motion_mode; + } + CHKS((polar_flag && block->motion_to_be == -1), _("Polar coordinates can only be used for motion")); + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! init_block + +Returned Value: int (INTERP_OK) + +Side effects: + Values in the block are reset as described below. + +Called by: parse_line + +This system reuses the same block over and over, rather than building +a new one for each line of NC code. The block is re-initialized before +each new line of NC code is read. + +The block contains many slots for values which may or may not be present +on a line of NC code. For some of these slots, there is a flag which +is turned on (at the time time value of the slot is read) if the item +is present. For slots whose values are to be read which do not have a +flag, there is always some excluded range of values. Setting the +initial value of these slot to some number in the excluded range +serves to show that a value for that slot has not been read. + +The rules for the indicators for slots whose values may be read are: +1. If the value may be an arbitrary real number (which is always stored + internally as a double), a flag is needed to indicate if a value has + been read. All such flags are initialized to false. + Note that the value itself is not initialized; there is no point in it. +2. If the value must be a non-negative real number (which is always stored + internally as a double), a value of -1.0 indicates the item is not present. +3. If the value must be an unsigned integer (which is always stored + internally as an int), a value of -1 indicates the item is not present. + (RS274/NGC does not use any negative integers.) +4. If the value is a character string (only the comment slot is one), the + first character is set to 0 (NULL). + +*/ + +int Interp::init_block(block_pointer block) //!< pointer to a block to be initialized or reset +{ + int n; + block->breadcrumbs = 0; // clear execution trail + block->executing_remap = NULL; + block->param_cnt = 0; + block->remappings.clear(); + block->builtin_used = false; + + block->a_flag = false; + block->b_flag = false; + block->c_flag = false; + block->comment[0] = 0; + block->d_flag = false; + block->dollar_flag = false; + block->e_flag = false; + block->f_flag = false; + for (n = 0; n < GM_MAX_MODAL_GROUPS; n++) { + block->g_modes[n] = -1; + } + block->h_flag = false; + block->h_number = -1; + block->i_flag = false; + block->j_flag = false; + block->k_flag = false; + block->l_number = -1; + block->l_flag = false; + block->line_number = -1; + block->n_number = -1; + block->motion_to_be = -1; + block->m_count = 0; + for (n = 0; n < 11; n++) { + block->m_modes[n] = -1; + } + block->user_m = 0; + block->p_number = -1.0; + block->p_flag = false; + block->q_flag = false; + block->q_number = -1.0; + block->r_flag = false; + block->s_flag = false; + block->t_flag = false; + block->u_flag = false; + block->v_flag = false; + block->w_flag = false; + block->x_flag = false; + block->y_flag = false; + block->z_flag = false; + + block->theta_flag = false; + block->radius_flag = false; + + block->o_type = O_none; + block->o_name = 0; + block->call_type = -1; + + return INTERP_OK; +} + + +/****************************************************************************/ + +/*! parse_line + +Returned Value: int + If any of the following functions returns an error code, + this returns that code. + init_block + read_items + enhance_block + check_items + Otherwise, it returns INTERP_OK. + +Side effects: + One RS274 line is read into a block and the block is checked for + errors. System parameters may be reset. + +Called by: Interp::read + +*/ + +int Interp::parse_line(char *line, //!< array holding a line of RS274 code + block_pointer block, //!< pointer to a block to be filled + setup_pointer settings) //!< pointer to machine settings +{ + CHP(init_block(block)); + CHP(read_items(block, line, settings->parameters)); + + if(settings->skipping_o == 0) + { + CHP(enhance_block(block, settings)); + CHP(check_items(block, settings)); + int n = find_remappings(block,settings); + if (n) logRemap("parse_line: found %d remappings",n); + } + return INTERP_OK; +} + +/****************************************************************************/ + +/*! precedence + +Returned Value: int + This returns an integer representing the precedence level of an_operator + +Side Effects: None + +Called by: read_real_expression + +To add additional levels of operator precedence, edit this function. + +*/ + +int Interp::precedence(int an_operator) +{ + switch(an_operator) + { + case RIGHT_BRACKET: + return 1; + + case AND2: + case EXCLUSIVE_OR: + case NON_EXCLUSIVE_OR: + return 2; + + case LT: + case EQ: + case NE: + case LE: + case GE: + case GT: + return 3; + + case MINUS: + case PLUS: + return 4; + + case NO_OPERATION: + case DIVIDED_BY: + case MODULO: + case TIMES: + return 5; + + case POWER: + return 6; + } + // should never happen + return 0; +} + + +int Interp::refresh_actual_position(setup_pointer settings) +{ + settings->current_x = GET_EXTERNAL_POSITION_X(); + settings->current_y = GET_EXTERNAL_POSITION_Y(); + settings->current_z = GET_EXTERNAL_POSITION_Z(); + settings->AA_current = GET_EXTERNAL_POSITION_A(); + settings->BB_current = GET_EXTERNAL_POSITION_B(); + settings->CC_current = GET_EXTERNAL_POSITION_C(); + settings->u_current = GET_EXTERNAL_POSITION_U(); + settings->v_current = GET_EXTERNAL_POSITION_V(); + settings->w_current = GET_EXTERNAL_POSITION_W(); + + return INTERP_OK; +} + + + +/****************************************************************************/ + +/*! set_probe_data + +Returned Value: int (INTERP_OK) + +Side effects: + The current position is set. + System parameters for probe position are set. + +Called by: Interp::read + +*/ + +int Interp::set_probe_data(setup_pointer settings) //!< pointer to machine settings +{ + double a, b, c; + refresh_actual_position(settings); + settings->parameters[5061] = GET_EXTERNAL_PROBE_POSITION_X(); + settings->parameters[5062] = GET_EXTERNAL_PROBE_POSITION_Y(); + settings->parameters[5063] = GET_EXTERNAL_PROBE_POSITION_Z(); + + a = GET_EXTERNAL_PROBE_POSITION_A(); + if(settings->a_axis_wrapped) { + a = fmod(a, 360.0); + if(a<0) a += 360.0; + } + settings->parameters[5064] = a; + + b = GET_EXTERNAL_PROBE_POSITION_B(); + if(settings->b_axis_wrapped) { + b = fmod(b, 360.0); + if(b<0) b += 360.0; + } + settings->parameters[5065] = b; + + c = GET_EXTERNAL_PROBE_POSITION_C(); + if(settings->c_axis_wrapped) { + c = fmod(c, 360.0); + if(c<0) c += 360.0; + } + settings->parameters[5066] = c; + + settings->parameters[5067] = GET_EXTERNAL_PROBE_POSITION_U(); + settings->parameters[5068] = GET_EXTERNAL_PROBE_POSITION_V(); + settings->parameters[5069] = GET_EXTERNAL_PROBE_POSITION_W(); + settings->parameters[5070] = (double) GET_EXTERNAL_PROBE_TRIPPED_VALUE(); + + // was an undocumented feature?: settings->parameters[5067] = GET_EXTERNAL_PROBE_VALUE(); + return INTERP_OK; +} + +int Interp::call_level(void) { return _setup.call_level; } + +std::string toString(GCodes g) +{ + char buf[15]={}; + int dec_value = g%10; + if (dec_value) + { + // Has a decimal + snprintf(buf, sizeof(buf), "G%d.%d", g/10, dec_value); + } else { + snprintf(buf, sizeof(buf), "G%d", g/10); + } + return buf; +} diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.hh b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.hh new file mode 100644 index 0000000..6ab7d5e --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_internal.hh @@ -0,0 +1,1029 @@ +/******************************************************************** +* Description: interp_internal.hh +* +* Derived from a work by Thomas Kramer +* +* Author: +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +********************************************************************/ +#ifndef INTERP_INTERNAL_HH +#define INTERP_INTERNAL_HH + +#include +#include +#include +#include +#include +#include +#include +#include +#include "nml_intf/canon.hh" +#include +#include "libintl.h" +#include +#include +#include // rtapi_strlcpy() +#include "interp_parameter_def.hh" +#include "interp_fwd.hh" +#include "interp_base.hh" +#include "tooldata/tooldata.hh" + + +#define _(s) gettext(s) + +/**********************/ +/* COMPILER MACROS */ +/**********************/ + +template +T R2D(T r) { return r * (180. / M_PI); } +template +T D2R(T r) { return r * (M_PI / 180.); } +template +T SQ(T a) { return a*a; } + +template +inline int round_to_int(T x) { + return (int)std::nearbyint(x); +} + +/* nested remap: a remapped code is found in the body of a subroutine + * which is executing on behalf of another remapped code + * example: a user G-code command executes a tool change + */ +#define MAX_NESTED_REMAPS 10 + +/* numerical constants */ + +/***************************************************************************** +The default tolerance (if none tighter is specified in the INI file) should be: +2 * 0.001 * sqrt(2) for inch, and 2 * 0.01 * sqrt(2) for mm. +This would mean that any valid arc where the endpoints and/or centerpoint +got rounded or truncated to 0.001 inch or 0.01 mm precision would be accepted. + +Tighter tolerance down to a minimum of 1 micron +- also accepted. +******************************************************************************/ + +#define CENTER_ARC_RADIUS_TOLERANCE_INCH (2 * 0.001 * M_SQRT2) +#define MIN_CENTER_ARC_RADIUS_TOLERANCE_INCH 0.00004 + +// Note: started from original tolerance and divided by 10 here (since that was originally done inside the interpreter) +#define RADIUS_TOLERANCE_INCH 0.00005 + +/* Equivalent metric constants */ + +#define CENTER_ARC_RADIUS_TOLERANCE_MM (2 * 0.01 * M_SQRT2) +#define MIN_CENTER_ARC_RADIUS_TOLERANCE_MM 0.001 + +#define RADIUS_TOLERANCE_MM (RADIUS_TOLERANCE_INCH * MM_PER_INCH) + +// Modest relative error +#define SPIRAL_RELATIVE_TOLERANCE 0.001 + +/* angle threshold for concavity for cutter compensation, in radians */ +#define TOLERANCE_CONCAVE_CORNER 0.05 +#define TOLERANCE_EQUAL 1e-6 /* two numbers compare EQ if the + difference is less than this */ + +static inline bool equal(double a, double b) +{ + return (fabs(a - b) < TOLERANCE_EQUAL); +} + +#define TINY 1e-12 /* for arc_data_r */ + +// max number of m codes on one line +#define MAX_EMS 4 + +// feed_mode +enum class FEED_MODE { + UNITS_PER_MINUTE=0, + INVERSE_TIME=1, + UNITS_PER_REVOLUTION=2 +}; + +// cutter radius compensation mode, 0 or false means none +// not using CANON_SIDE since interpreter handles cutter radius comp +enum class CUTTER_COMP { + OFF = 0, + RIGHT = 1, + LEFT = 2, +}; + +// spindle control modes +enum class SPINDLE_MODE { + CONSTANT_RPM, + CONSTANT_SURFACE +}; + +// unary operations +// These are not enums because the "&" operator is used in +// reading the operation names and is illegal with an enum + +enum UnaryOperations +{ + ABS = 1, + ACOS = 2, + ASIN = 3, + ATAN = 4, + COS = 5, + EXP = 6, + FIX = 7, + FUP = 8, + LN = 9, + ROUND = 10, + SIN = 11, + SQRT = 12, + TAN = 13, + EXISTS = 14, +}; + + +// binary operations +enum BinaryOperations +{ + NO_OPERATION = 0, + DIVIDED_BY = 1, + MODULO = 2, + POWER = 3, + TIMES = 4, + AND2 = 5, + EXCLUSIVE_OR = 6, + MINUS = 7, + NON_EXCLUSIVE_OR = 8, + PLUS = 9, + RIGHT_BRACKET = 10, + /* relational operators (are binary operators)*/ + LT = 11, + EQ = 12, + NE = 13, + LE = 14, + GE = 15, + GT = 16, + RELATIONAL_OP_FIRST = 11, + RELATIONAL_OP_LAST = 16, +}; + +// O code +enum OCodes +{ + O_none = 0, + O_sub = 1, + O_endsub = 2, + O_call = 3, + O_do = 4, + O_while = 5, + O_if = 6, + O_elseif = 7, + O_else = 8, + O_endif = 9, + O_break = 10, + O_continue = 11, + O_endwhile = 12, + O_return = 13, + O_repeat = 14, + O_endrepeat = 15, + M_98 = 16, + M_99 = 17, + O_ = 18, +}; + +// G-codes are symbolic to be dialect-independent in source code +enum GCodes +{ + G_0 = 0, + G_1 = 10, + G_2 = 20, + G_3 = 30, + G_4 = 40, + G_5 = 50, + G_5_1 = 51, + G_5_2 = 52, + G_5_3 = 53, + G_6 = 60, + G_6_1 = 61, + G_6_2 = 62, + G_6_3 = 63, + G_7 = 70, + G_8 = 80, + G_10 = 100, + G_17 = 170, + G_17_1 = 171, + G_18 = 180, + G_18_1 = 181, + G_19 = 190, + G_19_1 = 191, + G_20 = 200, + G_21 = 210, + G_28 = 280, + G_28_1 = 281, + G_30 = 300, + G_30_1 = 301, + G_33 = 330, + G_33_1 = 331, + G_38_2 = 382, + G_38_3 = 383, + G_38_4 = 384, + G_38_5 = 385, + G_40 = 400, + G_41 = 410, + G_41_1 = 411, + G_42 = 420, + G_42_1 = 421, + G_43 = 430, + G_43_1 = 431, + G_43_2 = 432, + G_49 = 490, + G_50 = 500, + G_51 = 510, + G_52 = 520, + G_53 = 530, + G_54 = 540, + G_55 = 550, + G_56 = 560, + G_57 = 570, + G_58 = 580, + G_59 = 590, + G_59_1 = 591, + G_59_2 = 592, + G_59_3 = 593, + G_61 = 610, + G_61_1 = 611, + G_64 = 640, + G_70 = 700, + G_71 = 710, + G_71_1 = 711, + G_71_2 = 712, + G_72 = 720, + G_72_1 = 721, + G_72_2 = 722, + G_73 = 730, + G_74 = 740, + G_76 = 760, + G_80 = 800, + G_81 = 810, + G_82 = 820, + G_83 = 830, + G_84 = 840, + G_85 = 850, + G_86 = 860, + G_87 = 870, + G_88 = 880, + G_89 = 890, + G_90 = 900, + G_90_1 = 901, + G_91 = 910, + G_91_1 = 911, + G_92 = 920, + G_92_1 = 921, + G_92_2 = 922, + G_92_3 = 923, + G_93 = 930, + G_94 = 940, + G_95 = 950, + G_96 = 960, + G_97 = 970, + G_98 = 980, + G_99 = 990, +}; + +std::string toString(GCodes g); + +// name of parameter file for saving/restoring interpreter variables +#define RS274NGC_PARAMETER_FILE_NAME_DEFAULT "rs274ngc.var" +#define RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX ".bak" + +// Subroutine parameters +#define INTERP_SUB_PARAMS 30 +#define INTERP_SUB_ROUTINE_LEVELS 10 +#define INTERP_FIRST_SUBROUTINE_PARAM 1 + +// max number of local variables saved (?) +#define MAX_NAMED_PARAMETERS 50 + +/**********************/ +/* TYPEDEFS */ +/**********************/ + +/* distance_mode */ +enum class DISTANCE_MODE +{ + ABSOLUTE, + INCREMENTAL, +}; + +/* retract_mode for cycles */ +enum class RETRACT_MODE +{ + R_PLANE, + OLD_Z, +}; + +// string table - to get rid of strdup/free +const char *strstore(const char *s); + + +// Block execution phases in execution order +// very carefully check code for sequencing when +// adding phases! + +// used to record execution trail in breadcrumbs +enum phases { + NO_REMAPPED_STEPS, + STEP_COMMENT, + STEP_SPINDLE_MODE, + STEP_FEED_MODE, + STEP_SET_FEED_RATE, + STEP_SET_SPINDLE_SPEED, + STEP_PREPARE, + STEP_M_5, + STEP_M_6, + STEP_RETAIN_G43, + STEP_M_7, + STEP_M_8, + STEP_M_9, + STEP_M_10, + STEP_DWELL, + STEP_SET_PLANE, + STEP_LENGTH_UNITS, + STEP_LATHE_DIAMETER_MODE, + STEP_CUTTER_COMP, + STEP_TOOL_LENGTH_OFFSET, + STEP_COORD_SYSTEM, + STEP_CONTROL_MODE, + STEP_DISTANCE_MODE, + STEP_IJK_DISTANCE_MODE, + STEP_RETRACT_MODE, + STEP_MODAL_0, + STEP_G92_IS_APPLIED, + STEP_MOTION, + STEP_MGROUP4, + MAX_STEPS +}; + + +// Modal groups +// also indices into g_modes +// unused: 9,11 +enum ModalGroups +{ + GM_MODAL_0 = 0, + GM_MOTION = 1, + GM_SET_PLANE = 2, + GM_DISTANCE_MODE = 3, + GM_IJK_DISTANCE_MODE = 4, + GM_FEED_MODE = 5, + GM_LENGTH_UNITS = 6, + GM_CUTTER_COMP = 7, + GM_TOOL_LENGTH_OFFSET = 8, + // 9 unused + GM_RETRACT_MODE = 10, + // 11 unused + GM_COORD_SYSTEM = 12, + GM_CONTROL_MODE = 13, + GM_SPINDLE_MODE = 14, + GM_LATHE_DIAMETER_MODE = 15, + GM_G92_IS_APPLIED = 16, + GM_MAX_MODAL_GROUPS +}; + +// the remap configuration descriptor +struct remap_struct { + const char *name; + const char *argspec; + // if no modalgroup= was given in the REMAP= line, use these defaults +#define MCODE_DEFAULT_MODAL_GROUP 10 +#define GCODE_DEFAULT_MODAL_GROUP 1 + int modal_group; + int motion_code; // only for g's - to identify cycles + const char *prolog_func; // Py function or null + const char *remap_py; // Py function maybe null, OR + const char *remap_ngc; // NGC file, maybe null + const char *epilog_func; // Py function or null +}; + + +// case insensitive compare for std::map etc +struct nocase_cmp +{ + bool operator()(const char* s1, const char* s2) const + { + return strcasecmp(s1, s2) < 0; + } +}; + +typedef std::map remap_map; +typedef remap_map::iterator remap_iterator; + +typedef std::map int_remap_map; +typedef int_remap_map::iterator int_remap_iterator; + +#define REMAP_FUNC(r) (r->remap_ngc ? r->remap_ngc: \ + (r->remap_py ? r->remap_py : "BUG-no-remap-func")) + +struct block_struct +{ + char comment[256]{}; + double a_number{}; + double b_number{}; + double c_number{}; + double d_number_float{}; + double e_number{}; + double f_number{}; + int h_number{}; + double i_number{}; + double j_number{}; + double k_number{}; + int l_number{}; + int n_number{}; + double p_number{}; + double q_number{}; + double r_number{}; + double s_number{}; + int t_number{}; + double u_number{}; + double v_number{}; + double w_number{}; + double x_number{}; + double y_number{}; + double z_number{}; + + int line_number{}; + int saved_line_number{}; // value of sequence_number when a remap was encountered + int motion_to_be{}; + int m_count{}; + int m_modes[11]{}; + int user_m{}; + int dollar_number{}; + int g_modes[GM_MAX_MODAL_GROUPS]{}; + + bool a_flag{}; + bool b_flag{}; + bool c_flag{}; + bool d_flag{}; + bool e_flag{}; + bool f_flag{}; + bool h_flag{}; + bool i_flag{}; + bool j_flag{}; + bool k_flag{}; + bool l_flag{}; + bool p_flag{}; + bool q_flag{}; + bool r_flag{}; + bool s_flag{}; + bool t_flag{}; + bool u_flag{}; + bool v_flag{}; + bool w_flag{}; + bool x_flag{}; + bool y_flag{}; + bool z_flag{}; + + bool dollar_flag{}; + + double radius{}; + double theta{}; + int radius_flag{}; + int theta_flag{}; + + // control (o-word) stuff + long offset{}; // start of line in file + int o_type{}; + int call_type{}; // oword-sub, python oword-sub, remap + + // Add Geometic fields + double arc_center_x{}; + double arc_center_y{}; + double arc_center_z{}; + double arc_radius{}; + double arc_heading{}; + double normal_heading{}; + bool iscircle{}; + const char *o_name{}; // !!!KL be sure to free this + double params[INTERP_SUB_PARAMS]{}; + int param_cnt{}; + + // bitmap of phases already executed + // we have some 31 or so different steps in a block. We must remember + // which one is done when we reexecute a block after a remap. + std::bitset breadcrumbs{}; + +#define TICKOFF(step) block->breadcrumbs[step] = 1 +#define TODO(step) (block->breadcrumbs[step] == 0) +#define ONCE(step) (TODO(step) ? TICKOFF(step),1 : 0) +#define ONCE_M(step) (TODO(STEP_M_ ## step) ? TICKOFF(STEP_M_ ## step),1 : 0) + + + // there might be several remapped items in a block, but at any point + // in time there's only one executing + // conceptually blocks[1..n] are also the 'remap frames' + remap_pointer executing_remap{}; // refers to config descriptor + std::set remappings{}; // all remappings in this block (enum phases) + int phase{}; // current remap execution phase + + // the strategy to get the builtin behaviour of a code in a remap procedure is as follows: + // if recursion is detected in find_remappings() (called by parse_line()), that *step* + // (roughly the modal group) is NOT added to the set of remapped steps in a block (block->remappings) + // in the convert_* procedures we test if the step is remapped with the macro below, and whether + // it is the current code which is remapped (IS_USER_MCODE, IS_USER_GCODE etc). If both + // are true, we execute the remap procedure; if not, use the builtin code. +#define STEP_REMAPPED_IN_BLOCK(bp, step) (bp->remappings.find(step) != bp->remappings.end()) + + // true if in a remap procedure the code being remapped was + // referenced, which caused execution of the builtin semantics + // reason for recording the fact: this permits an epilog to do the + // right thing depending on whether the builtin was used or not. + bool builtin_used{}; +}; + +// indicates which type of Python handler yielded, and needs reexecution +// post sync/read_inputs +enum call_states { + CS_NORMAL, + CS_REEXEC_PROLOG, + CS_REEXEC_PYBODY, + CS_REEXEC_EPILOG, + CS_REEXEC_PYOSUB, +}; + +// detail for O_call; tags the frame +enum call_types { + CT_NONE, // not in a call + CT_NGC_OWORD_SUB, // no restartable Python code involved + CT_NGC_M98_SUB, // like above; Fanuc-style, pass in params #1..#30 + CT_PYTHON_OWORD_SUB, // restartable Python code may be involved + CT_REMAP, // restartable Python code may be involved +}; + + +enum retopts { RET_NONE, RET_DOUBLE, RET_INT, RET_YIELD, RET_STOPITERATION, RET_ERRORMSG }; + +// parameters will go to a std::map +struct parameter_value_struct { + double value; + unsigned attr; +}; + +typedef std::map parameter_map; +typedef parameter_map::iterator parameter_map_iterator; + +#define PA_READONLY 1 +#define PA_GLOBAL 2 +#define PA_UNSET 4 +#define PA_USE_LOOKUP 8 // use lookup_named_param() to retrieve value +#define PA_FROM_INI 16 // a variable of the form '_[section]value' was retrieved from the INI file +#define PA_PYTHON 32 // call namedparams.() to retrieve the value + +// optional 3rd arg to store_named_param() +// flag initialization of r/o parameter +#define OVERRIDE_READONLY 1 + +#define MAX_REMAPOPTS 20 +// current implementation limits - legal modal groups +// for M- and G-codes +#define M_MODE_OK(m) ((m > 3) && (m < 11)) +#define G_MODE_OK(m) (m == 1) + +struct pycontext_impl; +struct pycontext { + pycontext(); + pycontext(const struct pycontext &); + pycontext &operator=(const struct pycontext &); + ~pycontext(); + pycontext_impl *impl; +}; + +struct context_struct { + context_struct(); + void clear(); + + long position; // location (ftell) in file + int sequence_number; // location (line number) in file + const char *filename; // name of file for this context + const char *subName; // name of the subroutine (oword) + int m98_loop_counter; // loop counter for Fanuc-style sub calls + double saved_params[INTERP_SUB_PARAMS]; + parameter_map named_params; + unsigned char context_status; // see CONTEXT_ defines below + int saved_g_codes[ACTIVE_G_CODES]; // array of active G-codes + int saved_m_codes[ACTIVE_M_CODES]; // array of active M-codes + double saved_settings[ACTIVE_SETTINGS]; // array of feed, speed, etc. + int call_type; // enum call_types + pycontext pystuff; + // Python-related stuff +}; + +// context.context_status +#define CONTEXT_VALID 1 // this was stored by M7* +#define CONTEXT_RESTORE_ON_RETURN 2 // automatically execute M71 on sub return +#define REMAP_FRAME 4 // a remap call frame + +struct offset_struct { + int type; + const char *filename; // the name of the file + long offset; // the offset in the file + int sequence_number; + int repeat_count; +}; + +typedef std::map offset_map_type; +typedef std::map::iterator offset_map_iterator; + +/* + +The current_x, current_y, and current_z are the location of the tool +in the current coordinate system. current_x and current_y differ from +program_x and program_y when cutter radius compensation is on. +current_z is the position of the tool tip in program coordinates when +tool length compensation is using the actual tool length; it is the +position of the spindle when tool length is zero. + +In a setup, the axis_offset values are set by g92 and the origin_offset +values are set by g54 - g59.3. The net origin offset uses both values +and is not represented here + +*/ +#define STACK_LEN 50 +#define STACK_ENTRY_LEN 256 +#define MAX_SUB_DIRS 10 + +struct setup +{ + setup(); + ~setup(); + + // Not copyable + setup(const setup&) = delete; + setup& operator= (const setup&) = delete; + + double AA_axis_offset; // A-axis g92 offset + double AA_current; // current A-axis position + double AA_origin_offset; // A-axis origin offset + double BB_axis_offset; // B-axis g92offset + double BB_current; // current B-axis position + double BB_origin_offset; // B-axis origin offset + double CC_axis_offset; // C-axis g92offset + double CC_current; // current C-axis position + double CC_origin_offset; // C-axis origin offset + + double u_axis_offset, u_current, u_origin_offset; + double v_axis_offset, v_current, v_origin_offset; + double w_axis_offset, w_current, w_origin_offset; + + int active_g_codes[ACTIVE_G_CODES]; // array of active G-codes + int active_m_codes[ACTIVE_M_CODES]; // array of active M-codes + double active_settings[ACTIVE_SETTINGS]; // array of feed, speed, etc. + StateTag state_tag; + + bool arc_not_allowed; // we just exited cutter compensation, so we error if the next move isn't straight + double axis_offset_x; // X-axis g92 offset + double axis_offset_y; // Y-axis g92 offset + double axis_offset_z; // Z-axis g92 offset + // block block1; // parsed next block + // stack of controlling blocks for remap execution + block blocks[MAX_NESTED_REMAPS]; + // index into blocks, points to currently controlling block + int remap_level; + +#define CONTROLLING_BLOCK(s) ((s).blocks[(s).remap_level]) +#define EXECUTING_BLOCK(s) ((s).blocks[0]) + + char blocktext[LINELEN]; // linetext downcased, white space gone + CANON_MOTION_MODE control_mode; // exact path or cutting mode + double tolerance; // G64 blending tolerance + double naivecam_tolerance; // G64 naive cam tolerance + double tolerance_default; // G64 P Default value, -1 to disable + double naivecam_tolerance_default; // G64 Q Default Value, -1 to disable + int current_pocket; // carousel slot (index) number of current tool + double current_x; // current X-axis position + double current_y; // current Y-axis position + double current_z; // current Z-axis position + double cutter_comp_radius; // current cutter compensation radius + int cutter_comp_orientation; // current cutter compensation tool orientation + CUTTER_COMP cutter_comp_side; // current cutter compensation side + double cycle_cc; // cc-value (normal) for canned cycles + double cycle_i; // i-value for canned cycles + double cycle_j; // j-value for canned cycles + double cycle_k; // k-value for canned cycles + int cycle_l; // l-value for canned cycles + double cycle_p; // p-value (dwell) for canned cycles + double cycle_q; // q-value for canned cycles + double cycle_r; // r-value for canned cycles + double cycle_il; // "initial level" height when switching from non-cycle into cycle, for g98 retract + int cycle_il_flag; // il is currently valid because we're in a series of cycles + DISTANCE_MODE distance_mode; // absolute or incremental + DISTANCE_MODE ijk_distance_mode; // absolute or incremental for IJK in arcs + FEED_MODE feed_mode; // G_93 (inverse time) or G_94 units/min + bool feed_override; // whether feed override is enabled + double feed_rate; // feed rate in current units/min + char filename[PATH_MAX]; // name of currently open NC code file + FILE *file_pointer; // file pointer for open NC code file + bool flood; // whether flood coolant is on + CANON_UNITS length_units; // millimeters or inches + double center_arc_radius_tolerance_inch; // modify with INI setting + double center_arc_radius_tolerance_mm; // modify with INI setting + int line_length; // length of line last read + char linetext[LINELEN]; // text of most recent line read + bool mist; // whether mist coolant is on + int motion_mode; // active G-code for motion + int origin_index; // active origin (1=G54 to 9=G59.3) + double origin_offset_x; // g5x offset x + double origin_offset_y; // g5x offset y + double origin_offset_z; // g5x offset z + double rotation_xy; // rotation of coordinate system around Z, in degrees + double parameters[interp_param_global::RS274NGC_MAX_PARAMETERS]; // system parameters + int parameter_occurrence; // parameter buffer index + int parameter_numbers[MAX_NAMED_PARAMETERS]; // parameter number buffer + double parameter_values[MAX_NAMED_PARAMETERS]; // parameter value buffer + int named_parameter_occurrence; + const char *named_parameters[MAX_NAMED_PARAMETERS]; + double named_parameter_values[MAX_NAMED_PARAMETERS]; + bool percent_flag; // true means first line was percent sign + CANON_PLANE plane; // active plane, XY-, YZ-, or XZ-plane + bool probe_flag; // flag indicating probing done + bool input_flag; // flag indicating waiting for input done + bool toolchange_flag; // flag indicating we just had a tool change + int input_index; // channel queried + bool input_digital; // input queried was digital (false=analog) + bool cutter_comp_firstmove; // this is the first comp move + double program_x; // program x, used when cutter comp on + double program_y; // program y, used when cutter comp on + double program_z; // program y, used when cutter comp on + RETRACT_MODE retract_mode; // for cycles, old_z or r_plane + int random_toolchanger; // tool changer swaps pockets, and pocket 0 is the spindle instead of "no tool" + int selected_pocket; // tool slot (index) selected but not active + int selected_tool; // start switchover to pocket-agnostic interp + int sequence_number; // sequence number of line last read + int num_spindles; // number of spindles available + int active_spindle; // the spindle currently used for CSS, FPR etc. + double speed[EMCMOT_MAX_SPINDLES];// array of spindle speeds + SPINDLE_MODE spindle_mode[EMCMOT_MAX_SPINDLES];// SPINDLE_MODE::CONSTANT_RPM or SPINDLE_MODE::CONSTANT_SURFACE + CANON_SPEED_FEED_MODE speed_feed_mode; // independent or synched + bool speed_override[EMCMOT_MAX_SPINDLES]; // whether speed override is enabled + CANON_DIRECTION spindle_turning[EMCMOT_MAX_SPINDLES]; // direction spindle is turning + char stack[STACK_LEN][STACK_ENTRY_LEN]; // stack of calls for error reporting + int stack_index; // index into the stack + EmcPose tool_offset; // tool length offset + CANON_TOOL_TABLE tool_table[CANON_POCKETS_MAX]; // index is pocket number + double traverse_rate; // rate for traverse motions + double orient_offset; // added to M19 R word, from [RS274NGC]ORIENT_OFFSET + bool g43_with_zero_offset; // added to allow active G43 with tool offset values all zero + + /* stuff for subroutines and control structures */ + int defining_sub; // true if in a subroutine defn + const char *sub_name; // name of sub we are defining (free this) + int doing_continue; // true if doing a continue + int doing_break; // true if doing a break + int executed_if; // true if executed in current if + const char *skipping_o; // o_name we are skipping for (or zero) + const char *skipping_to_sub; // o_name of sub skipping to (or zero) + int skipping_start; // start of skipping (sequence) + double test_value; // value for "if", "while", "elseif" + double return_value; // optional return value for "return", "endsub" + int value_returned; // the last NGC procedure did/did not return a value + int call_level; // current subroutine level + context sub_context[INTERP_SUB_ROUTINE_LEVELS]; + int call_state; // enum call_states - indicate Py handler reexecution + offset_map_type offset_map; // store label x name, file, line + + bool adaptive_feed; // adaptive feed is enabled + bool feed_hold; // feed hold is enabled + int loggingLevel; // 0 means logging is off + int debugmask; // from INI EMC/DEBUG + char log_file[PATH_MAX]; + char program_prefix[PATH_MAX]; // program directory + const char *subroutines[MAX_SUB_DIRS]; // subroutines directories + int use_lazy_close; // wait until next open before closing + // the input file + int lazy_closing; // close has been called + char wizard_root[PATH_MAX]; + int tool_change_at_g30; + int tool_change_quill_up; + int tool_change_with_spindle_on; + double parameter_g73_peck_clearance; + double parameter_g83_peck_clearance; + int a_axis_wrapped; + int b_axis_wrapped; + int c_axis_wrapped; + + int a_indexer_jnum; + int b_indexer_jnum; + int c_indexer_jnum; + + bool lathe_diameter_mode; //Lathe diameter mode (g07/G08) + bool mdi_interrupt; + int feature_set; + + int disable_fanuc_style_sub; + // M99 in main is treated as program end by default; this causes + // control to skip to beginning of file + bool loop_on_main_m99; + + int disable_g92_persistence; + +// add new geometric fields for our new tags + double heading; + double radius; + double center_x; + double center_y; + double center_z; + double normal_heading; + bool iscircle; + +#define FEATURE(x) (_setup.feature_set & FEATURE_ ## x) +#define FEATURE_RETAIN_G43 0x00000001 +#define FEATURE_OWORD_N_ARGS 0x00000002 +#define FEATURE_INI_VARS 0x00000004 +#define FEATURE_HAL_PIN_VARS 0x00000008 + // do not lowercase named params inside comments - for #<_hal[PinName]> +#define FEATURE_NO_DOWNCASE_OWORD 0x00000010 +#define FEATURE_OWORD_WARNONLY 0x00000020 + + boost::python::object *pythis; // boost::cref to 'this' + const char *on_abort_command; + int_remap_map g_remapped,m_remapped; + remap_map remaps; +#define INIT_FUNC "__init__" +#define DELETE_FUNC "__delete__" + + // task calls upon interp.init() repeatedly + // protect init() operations which are not idempotent + int init_once; +}; + + +// the externally visible singleton instance + +extern class PythonPlugin *python_plugin; +#define PYUSABLE (((python_plugin) != NULL) && (python_plugin->usable())) + +inline bool is_a_cycle(int motion) { + return ((motion > G_80) && (motion < G_90)) || (motion == G_73) || (motion == G_74); +} +/* + +The _setup model includes a stack array for the names of function +calls. This stack is written into if an error occurs. Just before each +function returns an error code, it writes its name in the next +available string, initializes the following string, and increments +the array index. The following four macros do the work. + +The size of the stack array is 50. An error in the middle of a very +complex expression would cause the ERP and CHP macros to write past the +bounds of the array if a check were not provided. No real program +would contain such a thing, but the check is included to make the +macros totally crash-proof. If the function call stack is deeper than +49, the top of the stack will be missing. + +*/ + + +// Just set an error string using printf-style formats, do NOT return +#define ERM(fmt, ...) \ + do { \ + setError (fmt, ## __VA_ARGS__); \ + _setup.stack_index = 0; \ + (rtapi_strlcpy(_setup.stack[_setup.stack_index], __PRETTY_FUNCTION__, STACK_ENTRY_LEN)); \ + _setup.stack[_setup.stack_index][STACK_ENTRY_LEN-1] = 0; \ + _setup.stack_index++; \ + _setup.stack[_setup.stack_index][0] = 0; \ + } while(0) + +// Set an error string using printf-style formats and return +#define ERS(fmt, ...) \ + do { \ + setError (fmt, ## __VA_ARGS__); \ + _setup.stack_index = 0; \ + (rtapi_strlcpy(_setup.stack[_setup.stack_index], __PRETTY_FUNCTION__, STACK_ENTRY_LEN)); \ + _setup.stack[_setup.stack_index][STACK_ENTRY_LEN-1] = 0; \ + _setup.stack_index++; \ + _setup.stack[_setup.stack_index][0] = 0; \ + return INTERP_ERROR; \ + } while(0) + +// Return one of the very few numeric errors +#define ERN(error_code) \ + do { \ + _setup.stack_index = 0; \ + (rtapi_strlcpy(_setup.stack[_setup.stack_index], __PRETTY_FUNCTION__, STACK_ENTRY_LEN)); \ + _setup.stack[_setup.stack_index][STACK_ENTRY_LEN-1] = 0; \ + _setup.stack_index++; \ + _setup.stack[_setup.stack_index][0] = 0; \ + return error_code; \ + } while(0) + + +// Propagate an error up the stack +#define ERP(error_code) \ + do { \ + if (_setup.stack_index < STACK_LEN - 1) { \ + (rtapi_strlcpy(_setup.stack[_setup.stack_index], __PRETTY_FUNCTION__, STACK_ENTRY_LEN)); \ + _setup.stack[_setup.stack_index][STACK_ENTRY_LEN-1] = 0; \ + _setup.stack_index++; \ + _setup.stack[_setup.stack_index][0] = 0; \ + } \ + return error_code; \ + } while(0) + + +// If the condition is true, set an error string as with ERS +#define CHKS(bad, fmt, ...) \ + do { \ + if (bad) { \ + ERS(fmt, ## __VA_ARGS__); \ + } \ + } while(0) + +// If the condition is true, return one of the few numeric errors +#define CHKN(bad, error_code) \ + do { \ + if (bad) { \ + ERN(error_code); \ + } \ + } while(0) + + +// Propagate an error up the stack as with ERP if the result of 'call' is not +// INTERP_OK +#define CHP(call) \ + do { \ + int CHP__status = (call); \ + if (CHP__status != INTERP_OK) { \ + ERP(CHP__status); \ + } \ + } while(0) + + +// oword warnings +#define OERR(fmt, ...) \ + do { \ + if (FEATURE(OWORD_WARNONLY)) \ + fprintf(stderr,fmt, ## __VA_ARGS__); \ + else \ + ERS(fmt, ## __VA_ARGS__); \ + } while(0) + + +// +// The traverse (in the active plane) to the location of the canned cycle +// is different on the first repeat vs on all the following repeats. +// +// The first traverse happens in the CURRENT_CC plane (which was raised to +// the R plane earlier, if needed), followed by a traverse down to the R +// plane. +// +// All later positioning moves happen in the CLEAR_CC plane, which is +// either the R plane or the OLD_CC plane depending on G98/G99. +// + +#define CYCLE_MACRO(call) for (repeat = block->l_number; \ + repeat > 0; \ + repeat--) \ + { \ + aa = (aa + aa_increment); \ + bb = (bb + bb_increment); \ + if(radius_increment) { \ + double radius, theta; \ + CHKS((bb == 0 && aa == 0), _("Incremental motion with polar coordinates is indeterminate when at the origin")); \ + theta = atan2(bb, aa); \ + radius = hypot(bb, aa) + radius_increment; \ + aa = radius * cos(theta); \ + bb = radius * sin(theta); \ + } \ + if(theta_increment) { \ + double radius, theta; \ + CHKS((bb == 0 && aa == 0), _("Incremental motion with polar coordinates is indeterminate when at the origin")); \ + theta = atan2(bb, aa) + theta_increment; \ + radius = hypot(bb, aa); \ + aa = radius * cos(theta); \ + bb = radius * sin(theta); \ + } \ + if ((repeat == block->l_number) && (current_cc > r)) { \ + cycle_traverse(block, plane, aa, bb, current_cc); \ + cycle_traverse(block, plane, aa, bb, r); \ + } else { \ + /* we must be at CLEAR_CC already */ \ + cycle_traverse(block, plane, aa, bb, clear_cc); \ + if (clear_cc > r) { \ + cycle_traverse(block, plane, aa, bb, r); \ + } \ + } \ + CHP(call); \ + } + +; + +struct scoped_locale { + scoped_locale(int category_, const char *locale_) : category(category_), oldlocale(setlocale(category, NULL)) { setlocale(category, locale_); } + ~scoped_locale() { setlocale(category, oldlocale.c_str()); } + int category; + std::string oldlocale; +}; + +#define FORCE_LC_NUMERIC_C scoped_locale force_lc_numeric_c(LC_NUMERIC, "C") +#endif // INTERP_INTERNAL_HH diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_namedparams.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_namedparams.cc new file mode 100644 index 0000000..9fcc602 --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_namedparams.cc @@ -0,0 +1,989 @@ +/*misnomer: _setup.current_pocket,selected_pocket +** These are indexes to sequential tooldata entries +** but the names are not changed due to frequent +** legacy usage in py files used for remapping +*/ +/******************************************************************** +* Description: interp_namedparams.cc +* +* collect all code related to named parameter handling +* +* Author: mostly K. Lerman +* rewrite by Michael Haberler to use STL containers +* License: GPL Version 2 +* System: Linux +* +* Copyright (c) 2004 All rights reserved. +* +* Last change: Juli 2011 +********************************************************************/ + +#include "config.h" + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#define BOOST_PYTHON_MAX_ARITY 4 +#include "pythonplugin/python_plugin.hh" +#include +#include +#include +#include +namespace bp = boost::python; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" +#include + +// for HAL pin variables +#include + +using namespace linuxcnc; + +enum predefined_named_parameters { + NP_LINE, + NP_MOTION_MODE, + NP_PLANE, + NP_CCOMP, + NP_METRIC, + NP_IMPERIAL, + NP_ABSOLUTE, + NP_INCREMENTAL, + NP_INVERSE_TIME, + NP_UNITS_PER_MINUTE, + NP_UNITS_PER_REV, + NP_COORD_SYSTEM, + NP_TOOL_OFFSET, + NP_RETRACT_R_PLANE, + NP_RETRACT_OLD_Z, + NP_SPINDLE_RPM_MODE, + NP_SPINDLE_CSS_MODE, + NP_IJK_ABSOLUTE_MODE, + NP_LATHE_DIAMETER_MODE, + NP_LATHE_RADIUS_MODE, + NP_SPINDLE_ON, + NP_SPINDLE_CW, + NP_MIST, + NP_FLOOD, + NP_SPEED_OVERRIDE, + NP_FEED_OVERRIDE, + NP_ADAPTIVE_FEED, + NP_FEED_HOLD, + NP_FEED, + NP_RPM, + NP_CURRENT_TOOL, + NP_SELECTED_POCKET, + NP_CURRENT_POCKET, + NP_X, + NP_Y, + NP_Z, + NP_A, + NP_B, + NP_C, + NP_U, + NP_V, + NP_W, + NP_ABS_X, + NP_ABS_Y, + NP_ABS_Z, + NP_ABS_A, + NP_ABS_B, + NP_ABS_C, + NP_VALUE, + NP_CALL_LEVEL, + NP_REMAP_LEVEL, + NP_SELECTED_TOOL, + NP_VALUE_RETURNED, + NP_TASK, +}; + +/****************************************************************************/ + +/*! read_named_parameter + +Returned Value: int + If read_integer_value returns an error code, this returns that code. + If any of the following errors occur, this returns the error code shown. + Otherwise, this returns INTERP_OK. + 1. The first character read is not a <: + NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED + 2. The named parameter string is not terminated by >: + NCE_NAMED_PARAMETER_NOT_TERSINATED + 3. The named parameter has not been defined before use: + NCE_NAMED_PARAMETER_NOT_DEFINED + +Side effects: + The value of the given parameter is put into what double_ptr points at. + The counter is reset to point to the first character after the + characters which make up the value. + +Called by: read_parameter + +This attempts to read the value of a parameter out of the line, +starting at the index given by the counter. + +According to the RS274/NGC manual [NCMS, p. 62], the characters following +# may be any "parameter expression". Thus, the following are legal +and mean the same thing (the value of the parameter whose number is +stored in parameter 2): + ##2 + #[#2] + + +ADDED by K. Lerman +Named parameters are now supported. +#<_abcd> is a parameter with name "abcd" of global scope +# is a named parameter of local scope. + +*/ + +int Interp::read_named_parameter( + char *line, //!< string: line of RS274/NGC code being processed + int *counter, //!< pointer to a counter for position on the line + double *double_ptr, //!< pointer to double to be read + double * /*parameters*/, //!< array of system parameters + bool check_exists) //!< test for existence, not value +{ + static char name[] = "read_named_parameter"; + char paramNameBuf[LINELEN+1]; + int exists; + double value; + parameter_map_iterator pi; + + CHKS((line[*counter] != '<'), + NCE_BUG_FUNCTION_SHOULD_NOT_HAVE_BEEN_CALLED); + CHP(read_name(line, counter, paramNameBuf)); + + CHP(find_named_param(paramNameBuf, &exists, &value)); + if (check_exists) { + *double_ptr = exists ? 1.0 : 0.0; + return INTERP_OK; + } + if (exists) { + *double_ptr = value; + return INTERP_OK; + } else { + // do not require named parameters to be defined during a + // subroutine definition: + if (_setup.defining_sub) + return INTERP_OK; + + logNP("%s: referencing undefined named parameter '%s' level=%d", + name, paramNameBuf, (paramNameBuf[0] == '_') ? 0 : _setup.call_level); + ERS(_("Named parameter #<%s> not defined"), paramNameBuf); + } + return INTERP_OK; +} + +// if the variable is of the form '_ini[section]name', then treat it as +// an inifile variable. Lookup section/name and cache the value +// as global and read-only. +// the shortest possible INI variable is '_ini[s]n' or 8 chars long . +int Interp::fetch_ini_param( const char *nameBuf, int *status, double *value) +{ + *status = 0; + int n = strlen(nameBuf); + + if(n < 8) { + return INTERP_OK; + } + + std::string sect = nameBuf + 5; // skip the '_ini[' part + // Make it all upper case + for(auto &c : sect) { + c = toupper(c); + } + size_t i = sect.find(']'); + if(std::string::npos == i) { + ERS(_("_ini expansion missing ']'")); + return INTERP_OK; + } + std::string var = sect.substr(i+1); + sect.erase(i); // Remove everything from ']'and after + + const char *iniFileName; + if ((iniFileName = getenv("INI_FILE_NAME")) == NULL) { + logNP("warning: referencing INI parameter '%s': no INI file", nameBuf); + return INTERP_OK; + } + IniFile inifile(iniFileName); + if (!inifile) { + ERS(_("can\'t open INI file '%s'"), iniFileName); + return INTERP_OK; + } + + if (auto inival = inifile.findReal(var, sect)) { + *value = *inival; + *status = 1; + } else { + ERS(_("Named INI parameter #<%s> not found in INI file '%s'"), nameBuf, iniFileName); + } + + return INTERP_OK; +} + +// if the variable is of the form '_hal[hal_name]', then treat it as +// a HAL pin, signal or param. Lookup value, convert to float, and export as global and read-only. +// do not cache. +// the shortest possible INI variable is '_hal[x]' or 7 chars long . +int Interp::fetch_hal_param( const char *nameBuf, int *status, double *value) +{ + static int comp_id; + int retval; + hal_type_t type = HAL_TYPE_UNINITIALIZED; + hal_data_u* ptr; + bool conn; + char hal_name[HAL_NAME_LEN]; + + *status = 0; + if (!comp_id) { + char hal_comp[HAL_NAME_LEN]; + snprintf(hal_comp, sizeof(hal_comp),"interp%d",getpid()); + comp_id = hal_init(hal_comp); // manpage says: NULL ok - which fails miserably + CHKS(comp_id < 0,_("fetch_hal_param: hal_init(%s): %d"), hal_comp,comp_id); + CHKS((retval = hal_ready(comp_id)), _("fetch_hal_param: hal_ready(): %d"),retval); + } + char *s; + int n = strlen(nameBuf); + if ((n > 6) && + ((s = (char *) strchr(&nameBuf[5],']')) != NULL)) { + + int closeBracket = s - nameBuf; + + strncpy(hal_name, &nameBuf[5], closeBracket); + hal_name[closeBracket - 5] = '\0'; + if (nameBuf[closeBracket + 1]) { + logOword("%s: trailing garbage after closing bracket", hal_name); + *status = 0; + ERS("%s: trailing garbage after closing bracket", nameBuf); + } + // the result of these lookups could be cached in the parameter struct, but I'm not sure + // this is a good idea - a removed pin/signal will not be noticed + + // I dont think that's needed - no change in pins/sigs/params + // rtapi_mutex_get(&(hal_data->mutex)); + // rtapi_mutex_give(&(hal_data->mutex)); + + if (hal_get_pin_value_by_name(hal_name, &type, &ptr, &conn) == 0) { + if (!conn) + logOword("%s: no signal connected", hal_name); + goto assign; + } + if (hal_get_signal_value_by_name(hal_name, &type, &ptr, &conn) == 0) { + if (!conn) + logOword("%s: signal has no writer", hal_name); + goto assign; + } + if (hal_get_param_value_by_name(hal_name, &type, &ptr) == 0) { + goto assign; + } + *status = 0; + ERS("Named hal parameter #<%s> not found", nameBuf); + } + return INTERP_OK; + + assign: + switch (type) { + case HAL_BIT: *value = (double) (ptr->b); break; + case HAL_U32: *value = (double) (ptr->u); break; + case HAL_S32: *value = (double) (ptr->s); break; + case HAL_U64: *value = (double) (ptr->lu); break; + case HAL_S64: *value = (double) (ptr->ls); break; + case HAL_FLOAT: *value = (double) (ptr->f); break; + default: return -1; + } + logOword("%s: value=%f", hal_name, *value); + *status = 1; + return INTERP_OK; +} + +int Interp::find_named_param( + const char *nameBuf, //!< pointer to name to be read + int *status, //!< pointer to return status 1 => found + double *value //!< pointer to value of found parameter + ) +{ + context_pointer frame; + parameter_map_iterator pi; + int level; + + level = (nameBuf[0] == '_') ? 0 : _setup.call_level; // determine scope + frame = &_setup.sub_context[level]; + *status = 0; + + pi = frame->named_params.find(nameBuf); + if (pi == frame->named_params.end()) { // not found + int exists = 0; + double inivalue; + if (FEATURE(INI_VARS) && (strncasecmp(nameBuf,"_ini[",5) == 0)) { + fetch_ini_param(nameBuf, &exists, &inivalue); + if (exists) { + logNP("parameter '%s' retrieved from INI: %f",nameBuf,inivalue); + *value = inivalue; + *status = 1; + parameter_value param; // cache the value + param.value = inivalue; + param.attr = PA_GLOBAL | PA_READONLY | PA_FROM_INI; + _setup.sub_context[0].named_params[strstore(nameBuf)] = param; + return INTERP_OK; + } + } + if (FEATURE(HAL_PIN_VARS) && (strncasecmp(nameBuf,"_hal[",5) == 0)) { + fetch_hal_param(nameBuf, &exists, &inivalue); + if (exists) { + logNP("parameter '%s' retrieved from HAL: %f",nameBuf,inivalue); + *value = inivalue; + *status = 1; + return INTERP_OK; + } + } + *value = 0.0; + *status = 0; + } else { + parameter_pointer pv = &pi->second; + if (pv->attr & PA_UNSET) + logNP("warning: referencing unset variable '%s'",nameBuf); + if (pv->attr & PA_USE_LOOKUP) { + CHP(lookup_named_param(nameBuf, pv->value, value)); + *status = 1; + } else if (pv->attr & PA_PYTHON) { + bp::object retval, tupleargs, kwargs; + bp::list plist; + + plist.append(*_setup.pythis); // self + tupleargs = bp::tuple(plist); + kwargs = bp::dict(); + + python_plugin->call(NAMEDPARAMS_MODULE, nameBuf, tupleargs, kwargs, retval); + CHKS(python_plugin->plugin_status() == PLUGIN_EXCEPTION, + "named param - pycall(%s):\n%s", nameBuf, + python_plugin->last_exception().c_str()); + CHKS(retval.ptr() == Py_None, "Python namedparams.%s returns no value", nameBuf); + if (PyUnicode_Check(retval.ptr())) { + // returning a string sets the interpreter error message and aborts + *status = 0; + char *msg = bp::extract(retval); + ERS("%s", msg); + } + if (PyLong_Check(retval.ptr())) { // widen + *value = (double) bp::extract(retval); + *status = 1; + return INTERP_OK; + } + if (PyFloat_Check(retval.ptr())) { + *value = bp::extract(retval); + *status = 1; + return INTERP_OK; + } + // ok, that callable returned something botched. + *status = 0; + PyObject *res_str = PyObject_Str(retval.ptr()); + Py_XDECREF(res_str); + ERS("Python call %s.%s returned '%s' - expected double, int or string, got %s", + NAMEDPARAMS_MODULE, nameBuf, + PyUnicode_AsUTF8(res_str), + retval.ptr()->ob_type->tp_name); + } else { + *value = pv->value; + *status = 1; + } + } + return INTERP_OK; +} + + +int Interp::store_named_param(setup_pointer settings, + const char *nameBuf, //!< pointer to name to be written + double value, //!< value to be written + int override_readonly //!< set to true to init a r/o parameter + ) +{ + context_pointer frame; + int level; + parameter_map_iterator pi; + + level = (nameBuf[0] == '_') ? 0 : _setup.call_level; // determine scope + frame = &settings->sub_context[level]; + + pi = frame->named_params.find(nameBuf); + if (pi == frame->named_params.end()) { + ERS(_("Internal error: Could not assign #<%s>"), nameBuf); + } else { + parameter_pointer pv = &pi->second; + + CHKS(((pv->attr & PA_GLOBAL) && level), + "BUG: variable '%s' marked global, but assigned at level %d", nameBuf, level); + + if ((pv->attr & PA_READONLY) && !override_readonly) { + ERS(_("Cannot assign to read-only parameter #<%s>"), nameBuf); + } else { + pv->value = value; + pv->attr &= ~PA_UNSET; + logNP("store_named_parameter: level[%d] %s value=%lf", + level, nameBuf, value); + } + } + return INTERP_OK; +} + + + +int Interp::add_named_param( + const char *nameBuf, //!< pointer to name to be added + int attr) //!< see PA_* defs in interp_internal.hh +{ + static char name[] = "add_named_param"; + int findStatus; + double value; + int level; + parameter_value param; + + // look it up to see if already exists + CHP(find_named_param(nameBuf, &findStatus, &value)); + + if (findStatus) { + logNP("%s: parameter:|%s| already exists", name, nameBuf); + return INTERP_OK; + } + attr |= PA_UNSET; + + if (nameBuf[0] != '_') { // local scope + level = _setup.call_level; + } else { + level = 0; // call level zero is global scope + attr |= PA_GLOBAL; + } + param.value = 0.0; + param.attr = attr; + _setup.sub_context[level].named_params[strstore(nameBuf)] = param; + return INTERP_OK; +} + + +int Interp::free_named_parameters(context_pointer frame) +{ + frame->named_params.clear(); + return INTERP_OK; +} + + +// just a shorthand +int Interp::init_readonly_param( + const char *nameBuf, //!< pointer to name to be added + double value, //!< initial value + int attr) //!< see PA_* defs in interp_internal.hh +{ + // static char name[] = "init_readonly_param"; + CHKS( add_named_param((char *) nameBuf, PA_READONLY|attr), + "adding r/o '%s'", nameBuf); + CHKS(store_named_param(&_setup, (char *) nameBuf, value, OVERRIDE_READONLY), + "storing r/o '%s' %f", nameBuf, value); + return INTERP_OK; +} + + +int Interp::lookup_named_param(const char *nameBuf, + double index, + double *value) +{ + int cmd = round_to_int(index); + + switch (cmd) { + + // some active_g_codes fields + + case NP_LINE: // _line - sequence number + *value = _setup.sequence_number; + break; + + case NP_MOTION_MODE: // _motion_mode + *value = _setup.motion_mode; + break; + + case NP_PLANE: // _plane + switch(_setup.plane) { + case CANON_PLANE::XY: + *value = G_17; + break; + case CANON_PLANE::XZ: + *value = G_18; + break; + case CANON_PLANE::YZ: + *value = G_19; + break; + case CANON_PLANE::UV: + *value = G_17_1; + break; + case CANON_PLANE::UW: + *value = G_18_1; + break; + case CANON_PLANE::VW: + *value = G_19_1; + break; + } + break; + + case NP_CCOMP: // _ccomp - cutter compensation + *value = + (_setup.cutter_comp_side == CUTTER_COMP::RIGHT) ? G_42 : + (_setup.cutter_comp_side == CUTTER_COMP::LEFT) ? G_41 : G_40; + break; + + case NP_METRIC: // _metric + *value = (_setup.length_units == CANON_UNITS_MM); + break; + + case NP_IMPERIAL: // _imperial + *value = (_setup.length_units == CANON_UNITS_INCHES); + break; + + case NP_ABSOLUTE: // _absolute - distance mode + *value = (_setup.distance_mode == DISTANCE_MODE::ABSOLUTE); + break; + + case NP_INCREMENTAL: // _incremental - distance mode + *value = (_setup.distance_mode == DISTANCE_MODE::INCREMENTAL); + break; + + case NP_INVERSE_TIME: // _inverse_time - feed mode + *value = (_setup.feed_mode == FEED_MODE::INVERSE_TIME); + break; + + case NP_UNITS_PER_MINUTE: // _units_per_minute - feed mode + *value = (_setup.feed_mode == FEED_MODE::UNITS_PER_MINUTE); + break; + + case NP_UNITS_PER_REV: // _units_per_rev - feed mode + *value = (_setup.feed_mode == FEED_MODE::UNITS_PER_REVOLUTION); + break; + + case NP_COORD_SYSTEM: // _coord_system - 0-9 + *value = + (_setup.origin_index < 7) ? (530 + (10 * _setup.origin_index)) : + (584 + _setup.origin_index); + break; + + case NP_TOOL_OFFSET: // _tool_offset + *value = (_setup.tool_offset.tran.x || _setup.tool_offset.tran.y || _setup.tool_offset.tran.z || + _setup.tool_offset.a || _setup.tool_offset.b || _setup.tool_offset.c || + _setup.tool_offset.u || _setup.tool_offset.v || _setup.tool_offset.w) ; + break; + + case NP_RETRACT_R_PLANE: // _retract_r_plane - G98 + *value = (_setup.retract_mode == RETRACT_MODE::R_PLANE); + break; + + case NP_RETRACT_OLD_Z: // _retract_old_z - G99 + *value = (_setup.retract_mode == RETRACT_MODE::OLD_Z); + break; + + case NP_SPINDLE_RPM_MODE: // _spindle_rpm_mode G97 currently only reports for spindle 0 + *value = (_setup.spindle_mode[0] == SPINDLE_MODE::CONSTANT_RPM); + break; + + case NP_SPINDLE_CSS_MODE: // _spindle_css_mode G96 + *value = (_setup.spindle_mode[0] == SPINDLE_MODE::CONSTANT_SURFACE); + break; + + case NP_IJK_ABSOLUTE_MODE: //_ijk_absolute_mode - G90.1 + *value = (_setup.ijk_distance_mode == DISTANCE_MODE::ABSOLUTE); + break; + + case NP_LATHE_DIAMETER_MODE: // _lathe_diameter_mode - G7 + *value = _setup.lathe_diameter_mode; + break; + + case NP_LATHE_RADIUS_MODE: // _lathe_radius_mode - G8 + *value = (_setup.lathe_diameter_mode == 0); + break; + + // some active_m_codes fields + + case NP_SPINDLE_ON: // _spindle_on + *value = (_setup.spindle_turning[0] != CANON_STOPPED); + break; + + case NP_SPINDLE_CW: // spindle_cw + *value = (_setup.spindle_turning[0] == CANON_CLOCKWISE); + break; + + case NP_MIST: // mist + *value = _setup.mist; + break; + + case NP_FLOOD: // flood + *value = _setup.flood; + break; + + case NP_SPEED_OVERRIDE: // speed override + *value = _setup.speed_override[0]; + break; + + case NP_FEED_OVERRIDE: // feed override + *value = _setup.feed_override; + break; + + case NP_ADAPTIVE_FEED: // adaptive feed + *value = _setup.adaptive_feed; + break; + + case NP_FEED_HOLD: // feed hold + *value = _setup.feed_hold; + break; + + // from active_settings: + case NP_FEED: // feed + *value = _setup.feed_rate; + break; + + case NP_RPM: // speed (rpm) + *value = abs(_setup.speed[0]); + break; + + case NP_CURRENT_TOOL: + *value = _setup.parameters[5400]; + break; + + case NP_SELECTED_POCKET: + if(_setup.random_toolchanger){//random changers already report the real pocket number + *value = _setup.selected_pocket; + } + else{//non random get it from the tool table + if(_setup.tool_table[_setup.selected_pocket].pocketno == 0){//pocket 0 is special on non-random changers + *value = -1; + } + else{ + *value = _setup.tool_table[_setup.selected_pocket].pocketno; + } + } + break; + + case NP_CURRENT_POCKET: + if (_setup.current_pocket == -1) { + *value = -1; + break; + } + if(_setup.random_toolchanger){//random changers already report the real pocket number + *value = _setup.current_pocket; + } + else{//non random get it from the tool table + *value = _setup.tool_table[_setup.current_pocket].pocketno; + } + break; + + case NP_SELECTED_TOOL: + *value = _setup.selected_tool; + break; + + case NP_X: // current position + *value = _setup.current_x; + break; + + case NP_Y: // current position + *value = _setup.current_y; + break; + + case NP_Z: // current position + *value = _setup.current_z; + break; + + case NP_A: // current position + *value = _setup.AA_current; + break; + + case NP_B: // current position + *value = _setup.BB_current; + break; + + case NP_C: // current position + *value = _setup.CC_current; + break; + + case NP_U: // current position + *value = _setup.u_current; + break; + + case NP_V: // current position + *value = _setup.v_current; + break; + + case NP_W: // current position + *value = _setup.w_current; + break; + + case NP_ABS_X: // abs position + { + double x = _setup.current_x + _setup.axis_offset_x; + double y = _setup.current_y + _setup.axis_offset_y; + rotate(&x, &y, _setup.rotation_xy); + *value = x + _setup.origin_offset_x + _setup.tool_offset.tran.x; + } + break; + + case NP_ABS_Y: // abs position + { + double x = _setup.current_x + _setup.axis_offset_x; + double y = _setup.current_y + _setup.axis_offset_y; + rotate(&x, &y, _setup.rotation_xy); + *value = y + _setup.origin_offset_y + _setup.tool_offset.tran.y; + } + break; + + + case NP_ABS_Z: // abs position + *value = _setup.current_z + _setup.axis_offset_z + + _setup.origin_offset_z + _setup.tool_offset.tran.z; + break; + + case NP_ABS_A: // abs position + *value = _setup.AA_current + _setup.AA_axis_offset + + _setup.AA_origin_offset + _setup.tool_offset.a; + break; + + case NP_ABS_B: // abs position + *value = _setup.BB_current + _setup.BB_axis_offset + + _setup.BB_origin_offset + _setup.tool_offset.b; + break; + + case NP_ABS_C: // abs position + *value = _setup.CC_current + _setup.CC_axis_offset + + _setup.CC_origin_offset + _setup.tool_offset.c; + break; + + // o-word subs may optionally have an + // expression after endsub and return + // this 'function return value' is accessible as '_value' + case NP_VALUE: + *value = _setup.return_value; + break; + + // predicate: the last NGC procedure did/did not return a value + case NP_VALUE_RETURNED: + *value = _setup.value_returned; + break; + + case NP_CALL_LEVEL: + *value = _setup.call_level; + break; + + case NP_REMAP_LEVEL: + *value = _setup.remap_level; + break; + + case NP_TASK: + extern int _task; // zero in gcodemodule, 1 in milltask + *value = _task; + break; + + default: + ERS(_("BUG: lookup_named_param(%s): unhandled index=%fn"), + nameBuf,index); + } + return INTERP_OK; +} + +int Interp::init_python_predef_parameter(const char *name) +{ + int exists = 0; + double value; + parameter_value param; + + if (name[0] == '_') { // globals only + find_named_param(name, &exists, &value); + if (exists) { + fprintf(stderr, "warning: redefining named parameter %s\n",name); + _setup.sub_context[0].named_params.erase(name); + } + param.value = 0.0; + param.attr = PA_READONLY|PA_PYTHON|PA_GLOBAL; + _setup.sub_context[0].named_params[strstore(name)] = param; + } + return INTERP_OK; +} + +int Interp::init_named_parameters() +{ + +// version major minor Note +// ------------ -------- ---------- ------------------------------------- +// M.N.m M.N 0.m normal format +// M.N.m~xxx M.N 0.m pre-release format + const char *pkgversion = PACKAGE_VERSION; //examples: 2.4.6, 2.5.0~pre + const char *version_major = "_vmajor";// named_parameter name (use lower case) + const char *version_minor = "_vminor";// named_parameter name (use lower case) + const char *metric_machine = "_metric_machine";// named_parameter name (use lower case) + double vmajor=0.0, vminor=0.0, munits = 1.0; + sscanf(pkgversion, "%lf%lf", &vmajor, &vminor); + + init_readonly_param(version_major,vmajor,0); + init_readonly_param(version_minor,vminor,0); + + munits = inicheck(); + init_readonly_param(metric_machine,munits,0); + + // params tagged with PA_USE_LOOKUP will call the lookup_named_param() + // method. The value is used as a index for the switch() statement. + + // the active_g_codes fields + + // I guess this is the line number + init_readonly_param("_line", NP_LINE, PA_USE_LOOKUP); + + // any of G1 G2 G3 G5.2 G73 G80 G82 G83 G86 G87 G88 G89 + // value is number after 'G' multiplied by 10 (10,20,30,52..) + + init_readonly_param("_motion_mode", NP_MOTION_MODE, PA_USE_LOOKUP); + + // G17/18/19/17.1/18.1/19.1 -> return 170/180/190/171/181/191 + init_readonly_param("_plane", NP_PLANE, PA_USE_LOOKUP); + + // return 400,410,420 depending if (G40,G41,G42) is on + init_readonly_param("_ccomp", NP_CCOMP, PA_USE_LOOKUP); + + // 1.0 if G21 is on + init_readonly_param("_metric", NP_METRIC, PA_USE_LOOKUP); + + // 1.0 if G20 is on + init_readonly_param("_imperial", NP_IMPERIAL, PA_USE_LOOKUP); + + //1.0 if G90 is on + init_readonly_param("_absolute", NP_ABSOLUTE, PA_USE_LOOKUP); + + //1.0 if G91 is on + init_readonly_param("_incremental", NP_INCREMENTAL, PA_USE_LOOKUP); + + // 1.0 if G93 is on + init_readonly_param("_inverse_time", NP_INVERSE_TIME, PA_USE_LOOKUP); + + // 1.0 if G94 is on + init_readonly_param("_units_per_minute", NP_UNITS_PER_MINUTE, PA_USE_LOOKUP); + + // 1.0 if G95 is on + init_readonly_param("_units_per_rev", NP_UNITS_PER_REV, PA_USE_LOOKUP); + + // 0..9 for G54..G59.3 + init_readonly_param("_coord_system", NP_COORD_SYSTEM, PA_USE_LOOKUP); + + // 1.0 if G43 is on + init_readonly_param("_tool_offset", NP_TOOL_OFFSET, PA_USE_LOOKUP); + + // 1 if G98 set + init_readonly_param("_retract_r_plane", NP_RETRACT_R_PLANE, PA_USE_LOOKUP); + + // 1 if G99 set + init_readonly_param("_retract_old_z", NP_RETRACT_OLD_Z, PA_USE_LOOKUP); + + // really esoteric + // init_readonly_param("_control_mode", 110, PA_USE_LOOKUP); + + + // 1 if G97 is on + init_readonly_param("_spindle_rpm_mode", NP_SPINDLE_RPM_MODE, PA_USE_LOOKUP); + + init_readonly_param("_spindle_css_mode", NP_SPINDLE_CSS_MODE, PA_USE_LOOKUP); + + // 1 if G90.1 is on + init_readonly_param("_ijk_absolute_mode", NP_IJK_ABSOLUTE_MODE, PA_USE_LOOKUP); + + // 1 if G7 is on + init_readonly_param("_lathe_diameter_mode", NP_LATHE_DIAMETER_MODE, PA_USE_LOOKUP); + + // 1 if G8 is on + init_readonly_param("_lathe_radius_mode", NP_LATHE_RADIUS_MODE, PA_USE_LOOKUP); + + + // the active_m_codes fields + init_readonly_param("_spindle_on", NP_SPINDLE_ON, PA_USE_LOOKUP); + init_readonly_param("_spindle_cw", NP_SPINDLE_CW, PA_USE_LOOKUP); + + init_readonly_param("_mist", NP_MIST, PA_USE_LOOKUP); + init_readonly_param("_flood", NP_FLOOD, PA_USE_LOOKUP); + init_readonly_param("_speed_override", NP_SPEED_OVERRIDE, PA_USE_LOOKUP); + init_readonly_param("_feed_override", NP_FEED_OVERRIDE, PA_USE_LOOKUP); + init_readonly_param("_adaptive_feed", NP_ADAPTIVE_FEED, PA_USE_LOOKUP); + init_readonly_param("_feed_hold", NP_FEED_HOLD, PA_USE_LOOKUP); + + // active_settings + init_readonly_param("_feed", NP_FEED, PA_USE_LOOKUP); + init_readonly_param("_rpm", NP_RPM, PA_USE_LOOKUP); + + + // tool related + init_readonly_param("_current_tool", NP_CURRENT_TOOL, PA_USE_LOOKUP); + init_readonly_param("_current_pocket", NP_CURRENT_POCKET, PA_USE_LOOKUP); + init_readonly_param("_selected_pocket", NP_SELECTED_POCKET, PA_USE_LOOKUP); + init_readonly_param("_selected_tool", NP_SELECTED_TOOL, PA_USE_LOOKUP); + + // current position - alias to #5420-#5429 + init_readonly_param("_x", NP_X, PA_USE_LOOKUP); + init_readonly_param("_y", NP_Y, PA_USE_LOOKUP); + init_readonly_param("_z", NP_Z, PA_USE_LOOKUP); + init_readonly_param("_a", NP_A, PA_USE_LOOKUP); + init_readonly_param("_b", NP_B, PA_USE_LOOKUP); + init_readonly_param("_c", NP_C, PA_USE_LOOKUP); + init_readonly_param("_u", NP_U, PA_USE_LOOKUP); + init_readonly_param("_v", NP_V, PA_USE_LOOKUP); + init_readonly_param("_w", NP_W, PA_USE_LOOKUP); + + // current abs position, does not include any offset + init_readonly_param("_abs_x", NP_ABS_X, PA_USE_LOOKUP); + init_readonly_param("_abs_y", NP_ABS_Y, PA_USE_LOOKUP); + init_readonly_param("_abs_z", NP_ABS_Z, PA_USE_LOOKUP); + init_readonly_param("_abs_a", NP_ABS_A, PA_USE_LOOKUP); + init_readonly_param("_abs_b", NP_ABS_B, PA_USE_LOOKUP); + init_readonly_param("_abs_c", NP_ABS_C, PA_USE_LOOKUP); + + // last (optional) endsub/return value + init_readonly_param("_value", NP_VALUE, PA_USE_LOOKUP); + + // predicate: last NGC procedure did return a value on endsub/return + init_readonly_param("_value_returned", NP_VALUE_RETURNED, PA_USE_LOOKUP); + + // predicate: 1 in milltask instance, 0 in UI - control preview behaviour + init_readonly_param("_task", NP_TASK, PA_USE_LOOKUP); + + // debugging aids + init_readonly_param("_call_level", NP_CALL_LEVEL, PA_USE_LOOKUP); + init_readonly_param("_remap_level", NP_REMAP_LEVEL, PA_USE_LOOKUP); + + return INTERP_OK; +} + +double Interp::inicheck() +{ + const char *filename; + + if ((filename = getenv("INI_FILE_NAME")) == NULL) { + return -1.0; + } + + IniFile inifile(filename); + if (!inifile) { + return -1.0; + } + + if (auto inistring = inifile.findString("LINEAR_UNITS", "TRAJ")) { + if (*inistring == "inch") { + return 0.0; + } else { + return 1.0; + } + } + + return -1.0; +} diff --git a/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_o_word.cc b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_o_word.cc new file mode 100644 index 0000000..77f780b --- /dev/null +++ b/wasm-port/vendor/linuxcnc/src/emc/rs274ngc/interp_o_word.cc @@ -0,0 +1,1222 @@ +/******************************************************************** +* Description: interp_o_word.cc +* +* +* Author: Kenneth Lerman +* License: GPL Version 2 +* System: Linux +* +* Copyright 2005 All rights reserved. +* +* Last change: Michael Haberler 7/2011 +* +********************************************************************/ + +#define BOOST_PYTHON_MAX_ARITY 4 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "rs274ngc.hh" +#include "rs274ngc_return.hh" +#include "nml_intf/interp_return.hh" +#include "interp_internal.hh" +#include "rs274ngc_interp.hh" +#include "pythonplugin/python_plugin.hh" +#include "interp_python.hh" +#include // rtapi_strlcpy() + +namespace bp = boost::python; + +//======================================================================== +// Functions for control stuff (O-words) +//======================================================================== + +/* + Given the root of a directory tree and a file name, + find the path to the file, if any. +*/ + +int Interp::findFile( // ARGUMENTS + char *direct, // the directory to start looking in + char *target, // the name of the file to find + char *foundFileDirect) // where to store the result +{ + FILE *file; + DIR *aDir; + struct dirent *aFile; + char targetPath[PATH_MAX+1]; + + snprintf(targetPath, PATH_MAX, "%s/%s", direct, target); + file = fopen(targetPath, "r"); + if (file) { + rtapi_strlcpy(foundFileDirect, direct, PATH_MAX); + fclose(file); + return INTERP_OK; + } + aDir = opendir(direct); + if (!aDir) { + ERS(NCE_FILE_NOT_OPEN); + } + + while ((aFile = readdir(aDir))) { + if (aFile->d_type == DT_DIR && + (0 != strncmp(aFile->d_name, "..", 3)) && + (0 != strncmp(aFile->d_name, ".", 2))) { + + char path[PATH_MAX+1]; + snprintf(path, PATH_MAX, "%s/%s", direct, aFile->d_name); + if (INTERP_OK == findFile(path, target, foundFileDirect)) { + closedir(aDir); + return INTERP_OK; + } + } + } + closedir(aDir); + ERS(NCE_FILE_NOT_OPEN); +} + + +/* + * this now uses STL maps for offset access + */ +int Interp::control_save_offset(block_pointer block, /* pointer to a block of RS274/NGC instructions */ + setup_pointer settings) /* pointer to machine settings */ +{ + static char name[] = "control_save_offset"; + offset_pointer op = NULL; + + logOword("Entered:%s for o_name:|%s|", name, block->o_name); + + if (control_find_oword(block, settings, &op) == INTERP_OK) { + if (settings->sequence_number -1 == op->sequence_number) + // hit this definition before; must be in illegal location + ERS(_("File:%s line:%d sub: o|%s| found in illegal location"), + settings->filename, settings->sequence_number, block->o_name); + else + // already exists + ERS(_("File:%s line:%d redefining sub: o|%s| already defined in file:%s"), + settings->filename, settings->sequence_number, + block->o_name, + op->filename); + } + offset new_offset; + + new_offset.type = block->o_type; + new_offset.offset = block->offset; + new_offset.filename = strstore(settings->filename); + new_offset.repeat_count = -1; + // the sequence number has already been bumped, so save + // the proper value + new_offset.sequence_number = settings->sequence_number - 1; + settings->offset_map[block->o_name] = new_offset; + return INTERP_OK; +} + + +int Interp::control_find_oword(block_pointer block, // pointer to block + setup_pointer settings, // pointer to machine settings + offset_pointer *op) // pointer to offset descriptor +{ + static char name[] = "control_find_oword"; + offset_map_iterator it; + + it = settings->offset_map.find(block->o_name); + if (it != settings->offset_map.end()) { + *op = &it->second; + return INTERP_OK; + } else { + logOword("%s: Unknown oword name: |%s|", name, block->o_name); + ERS(NCE_UNKNOWN_OWORD_NUMBER); + } +} + +const char *o_ops[] = { + "O_none", + "O_sub", + "O_endsub", + "O_call", + "O_do", + "O_while", + "O_if", + "O_elseif", + "O_else", + "O_endif", + "O_break", + "O_continue", + "O_endwhile", + "O_return", + "O_repeat", + "O_endrepeat", + "M_98", + "M_99", + "O_", + +}; + +const char *call_statenames[] = { + "CS_NORMAL", + "CS_REEXEC_PROLOG", + "CS_REEXEC_PYBODY", + "CS_REEXEC_EPILOG", + "CS_REEXEC_PYOSUB", +}; + +const char *call_typenames[] = { + "CT_NONE", + "CT_NGC_OWORD_SUB", + "CT_NGC_M98_SUB", + "CT_PYTHON_OWORD_SUB", + "CT_REMAP", +}; + +int Interp::execute_call(setup_pointer settings, + context_pointer current_frame, + int call_type) +{ + int status = INTERP_OK; + int i; + bp::list plist; + + context_pointer previous_frame = &settings->sub_context[settings->call_level-1]; + + block_pointer eblock = &EXECUTING_BLOCK(*settings); + + logOword("execute_call %s type=%s state=%s cl=%d rl=%d", + current_frame->subName, + call_typenames[call_type], + call_statenames[settings->call_state], + settings->call_level,settings->remap_level); + + switch (call_type) { + + case CT_NONE: + ERS("BUG: execute_call(): arrived with call_type=CT_NONE"); + break; + + case CT_NGC_OWORD_SUB: + case CT_NGC_M98_SUB: + // if we were skipping, no longer + if (settings->skipping_o) { + logOword("case O_call/M_98 -- no longer skipping to:|%s|", + settings->skipping_o); + settings->skipping_o = NULL; + } + + // copy parameters from context + // save old values of parameters + if (call_type != CT_NGC_M98_SUB) // M98: pass #1..#30 from parent + for (i = 0; i < INTERP_SUB_PARAMS; i++) { + previous_frame->saved_params[i] = + settings->parameters[i + INTERP_FIRST_SUBROUTINE_PARAM]; + settings->parameters[i + INTERP_FIRST_SUBROUTINE_PARAM] = + eblock->params[i]; + } + + // Set up return to next block (may be overridden by M98) + if (settings->file_pointer == NULL) + // if the previous file was NULL, mark position as -1 so as not to + // reopen it on return. + previous_frame->position = -1; + else + previous_frame->position = ftell(settings->file_pointer); + previous_frame->filename = strstore(settings->filename); + previous_frame->sequence_number = settings->sequence_number; + logOword("saving return location[cl=%d]: %s:%d offset=%ld", + settings->call_level-1, + previous_frame->filename, + previous_frame->sequence_number, + previous_frame->position); + + if (FEATURE(OWORD_N_ARGS)) { + // let any Oword sub know the number of parameters + CHP(add_named_param("n_args", PA_READONLY)); + CHP(store_named_param(settings, "n_args", + (double )eblock->param_cnt, + OVERRIDE_READONLY)); + } + + // M98 w/ L-word: Handle looping + if (eblock->o_type == M_98 && eblock->l_flag) { + // Set up loop counter + + // Loop count from L-word + if (previous_frame->m98_loop_counter == -1) + // If m98_loop_counter == -1, this is a new loop + previous_frame->m98_loop_counter = round_to_int(eblock->l_number); + logOword("Looping on O%s; remaining: %d", + eblock->o_name, previous_frame->m98_loop_counter); + + // if repeats remain, set up another loop + previous_frame->m98_loop_counter--; + if (previous_frame->m98_loop_counter > 0) { + + // Set return location to repeat this block for next + // iteration + previous_frame->position = eblock->offset; + // Unbump line number for next iteration + previous_frame->sequence_number--; + + } else + // No loops remain; reset loop counter + previous_frame->m98_loop_counter = -1; + } + + + if (eblock->o_type == M_98 && eblock->l_flag && eblock->l_number == 0) + logOword("M98 L0 instruction; skipping call"); + else if (control_back_to(eblock, settings) == INTERP_ERROR) { + settings->call_level--; + return INTERP_ERROR; + } + + break; + + case CT_PYTHON_OWORD_SUB: + switch (settings->call_state) { + case CS_NORMAL: + settings->return_value = 0.0; + settings->value_returned = 0; + previous_frame->sequence_number = settings->sequence_number; + previous_frame->filename = strstore(settings->filename); + plist.append(*settings->pythis); // self + for(int i = 0; i < eblock->param_cnt; i++) + plist.append(eblock->params[i]); // positional args + current_frame->pystuff.impl->tupleargs = bp::tuple(plist); + current_frame->pystuff.impl->kwargs = bp::dict(); + /* Fallthrough */ + case CS_REEXEC_PYOSUB: + if (settings->call_state == CS_REEXEC_PYOSUB) + CHP(read_inputs(settings)); + status = pycall(settings, current_frame, OWORD_MODULE, + current_frame->subName, + settings->call_state == CS_NORMAL ? PY_OWORDCALL : PY_FINISH_OWORDCALL); + CHKS(status == INTERP_ERROR, "pycall(%s.%s) failed", OWORD_MODULE, current_frame->subName) ; + switch (status = handler_returned(settings, current_frame, current_frame->subName, true)) { + case INTERP_EXECUTE_FINISH: + settings->call_state = CS_REEXEC_PYOSUB; + break; + default: + settings->call_state = CS_NORMAL; + settings->sequence_number = previous_frame->sequence_number; + CHP(status); + // M73 auto-restore is of dubious value in a Python subroutine + CHP(leave_context(settings,false)); + } + break; + } + break; + + case CT_REMAP: + block_pointer cblock = &CONTROLLING_BLOCK(*settings); + remap_pointer remap = cblock->executing_remap; + + switch (settings->call_state) { + case CS_NORMAL: + if (remap->remap_py || remap->prolog_func || remap->epilog_func) { + CHKS(!PYUSABLE, "%s (remapped) uses Python functions, but the Python plugin is not available", + remap->name); + plist.append(*settings->pythis); //self + current_frame->pystuff.impl->tupleargs = bp::tuple(plist); + current_frame->pystuff.impl->kwargs = bp::dict(); + } + if (remap->argspec && (strchr(remap->argspec, '@') == NULL)) { + // add_parameters will decorate kwargs as per argspec + // if named local parameters specified + CHP(add_parameters(settings, cblock, NULL)); + } + // fall through + + case CS_REEXEC_PROLOG: + if (remap->prolog_func) { + status = pycall(settings, current_frame, REMAP_MODULE,remap->prolog_func, + settings->call_state == CS_NORMAL ? PY_PROLOG : PY_FINISH_PROLOG); + CHKS(status == INTERP_ERROR, "pycall(%s.%s) failed", REMAP_MODULE, remap->prolog_func); + switch (status = handler_returned(settings, current_frame, current_frame->subName, false)) { + case INTERP_EXECUTE_FINISH: + settings->call_state = CS_REEXEC_PROLOG; + return status; + default: + settings->call_state = CS_NORMAL; + //settings->sequence_number = previous_frame->sequence_number; + CHP(status); + } + } + // fall through + + case CS_REEXEC_PYBODY: + if (remap->remap_py) { + status = pycall(settings, current_frame, REMAP_MODULE, remap->remap_py, + settings->call_state == CS_NORMAL ? PY_BODY : PY_FINISH_BODY); + CHP(status); + switch (status = handler_returned(settings, current_frame, current_frame->subName, false)) { + case INTERP_EXECUTE_FINISH: + settings->call_state = CS_REEXEC_PYBODY; + return status; + default: + settings->call_state = CS_NORMAL; + settings->sequence_number = previous_frame->sequence_number; + CHP(status); + // epilog is not supported on python body - makes no sense + CHP(leave_context(settings,false)); + ERP(remap_finished(-cblock->phase)); + } + } + + // call the NGC remap procedure + assert(settings->call_state == CS_NORMAL); + if (remap->remap_ngc) { + CHP(execute_call(settings, current_frame, + CT_NGC_OWORD_SUB)); + } + } + } + return status; +} + +// this is executed only for NGC subs, either normal ones or part of a remap +// subs whose name is a Py callable are handled inline in execute_call() +// since there is no corresponding O_return/O_endsub to execute. +int Interp::execute_return(setup_pointer settings, context_pointer current_frame,int call_type) +{ + int status = INTERP_OK; + + logOword("execute_return %s type=%s state=%s", + current_frame->subName, + call_typenames[call_type], + call_statenames[settings->call_state]); + + block_pointer cblock = &CONTROLLING_BLOCK(*settings); + block_pointer eblock = &EXECUTING_BLOCK(*settings); + context_pointer previous_frame = settings->call_level > 0 ? + &settings->sub_context[settings->call_level - 1] : nullptr; + + // if level is not zero, in a call + // otherwise in a defn + // if we were skipping, no longer + if (settings->skipping_o && (eblock->o_type == O_endsub)) { + logOword("case O_%s -- no longer skipping to:|%s|", + (eblock->o_type == O_endsub) ? "endsub" : "return", + settings->skipping_o); + settings->skipping_o = NULL; + } + + switch (call_type) { + + case CT_REMAP: + switch (settings->call_state) { + case CS_NORMAL: + case CS_REEXEC_EPILOG: + if (cblock->executing_remap && cblock->executing_remap->epilog_func) { + if (settings->call_state == CS_REEXEC_EPILOG) + CHP(read_inputs(settings)); + status = pycall(settings, current_frame, REMAP_MODULE, + cblock->executing_remap->epilog_func, + settings->call_state == CS_NORMAL ? PY_EPILOG : PY_FINISH_EPILOG); + CHP(status); + switch (status = handler_returned(settings, current_frame, current_frame->subName, false)) { + case INTERP_EXECUTE_FINISH: + settings->call_state = CS_REEXEC_EPILOG; + eblock->call_type = CT_REMAP; + CHP(status); + break; + default: + settings->call_state = CS_NORMAL; + settings->sequence_number = previous_frame->sequence_number; + CHP(status); + // leave_context() is done by falling through into CT_NGC_OWORD_SUB code + } + } + } + // fall through to normal NGC return handling + /* Fallthrough */ + case CT_NGC_OWORD_SUB: + case CT_NGC_M98_SUB: + case CT_NONE: // sub definition + if (settings->call_level != 0) { + + // restore subroutine parameters. + if (call_type != CT_NGC_M98_SUB) // M98: pass #1..#30 from parent + for(int i = 0; i < INTERP_SUB_PARAMS; i++) { + settings->parameters[i+INTERP_FIRST_SUBROUTINE_PARAM] = + previous_frame->saved_params[i]; + } + + // file at this level was marked as closed, so dont reopen. + if (previous_frame->position == -1) { + if (settings->file_pointer) fclose(settings->file_pointer); + settings->file_pointer = NULL; + rtapi_strxcpy(settings->filename, ""); + } else { + if(settings->file_pointer == NULL) { + ERS(NCE_FILE_NOT_OPEN); + } + //!!!KL must open the new file, if changed + if (0 != strcmp(settings->filename, previous_frame->filename)) { + fclose(settings->file_pointer); + settings->file_pointer = fopen(previous_frame->filename, "r"); + if (settings->file_pointer == NULL) { + ERS(NCE_CANNOT_REOPEN_FILE, + previous_frame->filename, + strerror(errno)); + } + rtapi_strxcpy(settings->filename, previous_frame->filename); + } + fseek(settings->file_pointer, previous_frame->position, SEEK_SET); + settings->sequence_number = previous_frame->sequence_number; + logOword("endsub/return: %s:%d pos=%ld", + settings->filename,previous_frame->sequence_number, + previous_frame->position); + + } + // cleanups on return: + CHP(leave_context(settings, true)); + + // if this was a remap frame we're done + if (current_frame->context_status & REMAP_FRAME) { + CHP(remap_finished(-cblock->phase)); + } + + + settings->sub_name = 0; + if (previous_frame->subName) { + settings->sub_name = previous_frame->subName; + } else { + settings->sub_name = NULL; + } + } else { // call_level == 0 + // a definition + if (eblock->o_type == O_endsub) { + CHKS((settings->defining_sub != 1), NCE_NOT_IN_SUBROUTINE_DEFN); + // no longer skipping or defining + if (settings->skipping_o) { + logOword("case O_endsub in defn -- no longer skipping to:|%s|", + settings->skipping_o); + settings->skipping_o = NULL; + } + settings->defining_sub = 0; + settings->sub_name = NULL; + } + } + } + return status; +} + +// this is executed for m99 in the main program, signifying an endless +// loop; m99 main program endless loop is handled in +// interp_convert.cc, but because this is like an O-word function +// skipping around the file, it's placed here instead. +void Interp::loop_to_beginning(setup_pointer settings) +{ + logOword("loop_to_beginning state=%s file=%s", + call_statenames[settings->call_state], + settings->filename); + + // scroll back to beginning of file/first block + fseek(settings->file_pointer, 0, SEEK_SET); + settings->sequence_number = 0; +} + +// +// TESTME!!! MORE THOROUGHLY !!!KL +// +// In the past, calls had to be to predefined subs +// +// Now they don't. Do things in the following sequence: +// 1 -- if o_word is already defined, just go back to it, else +// 2 -- if there is a file with the name of the o_word, +// open it and start skipping (as in 3, below) +// 3 -- skip to the o_word (will be an error if not found) +// +int Interp::control_back_to( block_pointer block, // pointer to block + setup_pointer settings) // pointer to machine settings +{ + static char name[] = "control_back_to"; + char newFileName[PATH_MAX]; + FILE *newFP; + offset_map_iterator it; + offset_pointer op; + logOword("Entered:%s %s", name,basename(block->o_name)); + it = settings->offset_map.find(basename(block->o_name)); + + // #1 already defined + if (it != settings->offset_map.end()) { + op = &it->second; + if ((settings->filename[0] != 0) & + (settings->file_pointer == NULL)) { + ERS(NCE_FILE_NOT_OPEN); + } + if (0 != strcmp(settings->filename, + op->filename)) { + // open the new file... + newFP = fopen(op->filename, "r"); + // set the line number + settings->sequence_number = 0; + if (strlen(op->filename) >= sizeof(settings->filename)) { + fclose(settings->file_pointer); + logOword("filename too long: %s", op->filename); + ERS(NCE_UNABLE_TO_OPEN_FILE, op->filename); + } + strncpy(settings->filename, op->filename, sizeof(settings->filename)); + + if (newFP) { + // close the old file... + if (settings->file_pointer) // only close if it was open + fclose(settings->file_pointer); + settings->file_pointer = newFP; + } else { + logOword("Unable to open file: %s", settings->filename); + ERS(NCE_UNABLE_TO_OPEN_FILE,settings->filename); + } + } + if (settings->file_pointer) { // only seek if it was open + fseek(settings->file_pointer, + op->offset, SEEK_SET); + } + settings->sequence_number = op->sequence_number; + return INTERP_OK; + } + + // #2 open the File + newFP = find_ngc_file(settings, block->o_name, newFileName); + + if (newFP) { + logOword("fopen: |%s| OK", newFileName); + settings->sequence_number = 0; + + // close the old file... + if (settings->file_pointer) + fclose(settings->file_pointer); + settings->file_pointer = newFP; + if (strlen(newFileName) >= sizeof(settings->filename)) { + logOword("new filename '%s' is too long (max len %zu)\n", newFileName, sizeof(settings->filename)-1); + ERS(NCE_UNABLE_TO_OPEN_FILE, newFileName); + } + strncpy(settings->filename, newFileName, sizeof(settings->filename)); + } else { + // No file found for this sub name. + // Block forward-seek if the current file is not the main program. + // Numbered subs after a named endsub pollute the global offset + // map and silently conflict across files. Forward-seek in the + // main program's own file (call_level 0) is still allowed. + if (settings->call_level > 0) { + context_pointer main_frame = &settings->sub_context[0]; + if (main_frame->filename == NULL || + strcmp(settings->filename, main_frame->filename) != 0) { + ERS(_("Subroutine 'O%s' not found -- " + "not in offset table and no file '%s' found"), + block->o_name, newFileName); + } + } + char *dirname = getcwd(NULL, 0); + logOword("fopen: |%s| failed CWD:|%s|", newFileName, + dirname); + free(dirname); + } + + settings->skipping_o = basename(block->o_name); // start skipping + settings->skipping_to_sub = basename(block->o_name); // start skipping + settings->skipping_start = settings->sequence_number; + return INTERP_OK; +} + + +int Interp::handler_returned( setup_pointer settings, context_pointer active_frame, + const char *name, bool osub) +{ + int status = INTERP_OK; + + switch (active_frame->pystuff.impl->py_return_type) { + case RET_YIELD: + // yield was executed + CHP(active_frame->pystuff.impl->py_returned_int); + break; + + case RET_STOPITERATION: // a bare 'return' in a generator - treat as INTERP_OK + case RET_NONE: + break; + + case RET_DOUBLE: + if (osub) { // float values are ok for osubs + settings->return_value = active_frame->pystuff.impl->py_returned_double; + settings->value_returned = 1; + } else { + ERS("handler_returned: %s returned double: %f - invalid", + name, active_frame->pystuff.impl->py_returned_double); + } + break; + + case RET_INT: + if (osub) { // let's be liberal with types - widen to double return value + settings->return_value = (double) active_frame->pystuff.impl->py_returned_int; + settings->value_returned = 1; + } else + return active_frame->pystuff.impl->py_returned_int; + break; + case RET_ERRORMSG: + status = INTERP_ERROR; + break; + + } + return status; +} + + +// prepare a new call frame. +int Interp::enter_context(setup_pointer settings, block_pointer block) +{ + logOword("enter_context cl=%d->%d type=%s", + settings->call_level, settings->call_level+1, + call_typenames[block->call_type]); + + settings->call_level++; + if (settings->call_level >= INTERP_SUB_ROUTINE_LEVELS) { + ERS(NCE_TOO_MANY_SUBROUTINE_LEVELS); + } + context_pointer frame = &settings->sub_context[settings->call_level]; + frame->clear(); + // mark frame for finishing remap + frame->context_status = (block->call_type == CT_REMAP) ? REMAP_FRAME : 0; + frame->subName = block->o_name; + frame->pystuff.impl->py_returned_int = 0; + frame->pystuff.impl->py_returned_double = 0.0; + frame->pystuff.impl->py_return_type = -1; + // distinguish call frames: oword,m99,python,remap + frame->call_type = block->call_type; + return INTERP_OK; +} + +int Interp::leave_context(setup_pointer settings, bool restore) +{ + context_pointer leaving_frame = &settings->sub_context[settings->call_level]; + + if (settings->call_level < 1) { + ERS(NCE_CALL_STACK_UNDERRUN); + } + logOword("leave_context cl=%d->%d type=%s state=%s" , + settings->call_level, settings->call_level-1, + call_typenames[leaving_frame->call_type], + call_statenames[settings->call_state]); + + free_named_parameters(leaving_frame); + leaving_frame->subName = NULL; + settings->call_level--; // drop back + + if (restore && ((leaving_frame->context_status & + (CONTEXT_RESTORE_ON_RETURN|CONTEXT_VALID)) == + (CONTEXT_RESTORE_ON_RETURN|CONTEXT_VALID))) { + // a valid previous context was marked by an M73 as auto-restore + + // NB: this means an M71 invalidate context will prevent an + // auto-restore on return/endsub + CHP(restore_settings(settings, settings->call_level + 1)); + } + return INTERP_OK; +} + +/************************************************************************/ +/* convert_control_functions + +Returned Value: int (INTERP_OK) +Side effects: + Changes the flow of control. + +Called by: execute + +Calls: control_skip_to + control_back_to + control_save_offset +*/ + +int Interp::convert_control_functions(block_pointer block, // pointer to a block of RS274/NGC instructions + setup_pointer settings) // pointer to machine settings +{ + int status = INTERP_OK; + context_pointer current_frame; + offset_pointer op = NULL; + + logOword("convert_control_functions %s", o_ops[block->o_type]); + + // must skip if skipping + if (settings->skipping_o && (0 != strcmp(settings->skipping_o, block->o_name))) { + logOword("skipping to line: |%s|", settings->skipping_o); + return INTERP_OK; + } + if (settings->skipping_to_sub && (block->o_type != O_sub) + && (block->o_type != O_)) { + logOword("skipping to sub: |%s|", settings->skipping_to_sub); + return INTERP_OK; + } + + // if skipping_o was set, we are now on a line which contains that O-word. + // if skipping_to_sub was set, we are now on the 'O-name sub' definition line. + + switch (block->o_type) { + + case O_none: + // not an error because we use this to signal that we + // are not evaluating functions + break; + + case O_sub: + case O_: + current_frame = &settings->sub_context[settings->call_level]; + + // Mixing Fanuc- and rs274ngc-style sub calls & defs is not allowed: + // - Fanuc: 'O....' subprogram must be called with 'M98' + CHKS((block->o_type == O_ && + current_frame->call_type != CT_NGC_M98_SUB && + current_frame->call_type != CT_NONE), + "Fanuc 'O....' subroutine must be called with 'M98'"); + // - rs274ngc: 'O.... sub' subprogram must be called with 'O.... call' + CHKS((block->o_type == O_sub && + current_frame->call_type != CT_NGC_OWORD_SUB && + current_frame->call_type != CT_PYTHON_OWORD_SUB && + current_frame->call_type != CT_REMAP && + current_frame->call_type != CT_NONE), + "'O.... sub' subroutine must be called with 'O.... call'"); + + // if we were skipping, no longer + if (settings->skipping_o) { + logOword("sub(o_|%s|) was skipping to here", settings->skipping_o); + // skipping to a sub means that we must define this now + CHP(control_save_offset( block, settings)); + logOword("no longer skipping to:|%s|", settings->skipping_o); + settings->skipping_o = NULL; // this IS our block number + } + settings->skipping_to_sub = NULL; // this IS our block number + + if (block->o_type == O_) { + // Done: continue executing into Fanuc-style programs w/o skipping + logOword("Entering Fanuc-style program number %s", block->o_name); + break; + } + + // if the level is not zero, this is a call + // not the definition + if (settings->call_level != 0) { + // Check for nested or extra sub definitions inside a called sub. + // Only the entry sub (matching the call) is allowed; any other + // 'sub' keyword is an error (nested def or numbered sub in file). + context_pointer cf = &settings->sub_context[settings->call_level]; + CHKS((cf->subName && strcmp(cf->subName, block->o_name) != 0), + _("Nested subroutine definition: 'O%s sub' found inside " + "called subroutine 'O%s'"), + block->o_name, cf->subName); + logOword("call:%f:%f:%f", + settings->parameters[1], + settings->parameters[2], + settings->parameters[3]); + } else { + // a definition. We're on the O sub line. + logOword("started a rs274-style sub-program defn: %s", + block->o_name); + CHKS((settings->defining_sub == 1), NCE_NESTED_SUBROUTINE_DEFN); + CHP(control_save_offset( block, settings)); + + // start skipping to the corresponding ensub. + settings->skipping_o = block->o_name; + settings->skipping_start = settings->sequence_number; + settings->defining_sub = 1; + settings->sub_name = block->o_name; + logOword("will now skip to: |%s|", settings->sub_name); + } + break; + + case O_endsub: + case O_return: + case M_99: + + // For M99, this only handles return from a subprogram + CHKS((block->o_type == M_99 && settings->call_level == 0), + "Bug: Reached convert_control_functions() " + "from M99 in main program"); + + if ((settings->call_level == 0) && + (settings->sub_name == NULL)) { + // detect a standalone 'o