diff --git a/configs/sim/axis/axis_9axis_scurve.ini b/configs/sim/axis/axis_9axis_scurve.ini index 6588a0d8cd8..39c082a442b 100644 --- a/configs/sim/axis/axis_9axis_scurve.ini +++ b/configs/sim/axis/axis_9axis_scurve.ini @@ -94,7 +94,8 @@ DEFAULT_LINEAR_VELOCITY = 100.0 MAX_LINEAR_VELOCITY = 300.0 DEFAULT_LINEAR_ACCELERATION = 500.0 MAX_LINEAR_ACCELERATION = 1000.0 -NO_FORCE_HOMING = 1 #to be able to run gcodes and MDI without homing +# to be able to run gcodes and MDI without homing +NO_FORCE_HOMING = 1 ARC_BLEND_ENABLE = 1 diff --git a/configs/sim/axis/axis_mm_scurve.ini b/configs/sim/axis/axis_mm_scurve.ini index 88a20af6c63..13a3e803bbd 100644 --- a/configs/sim/axis/axis_mm_scurve.ini +++ b/configs/sim/axis/axis_mm_scurve.ini @@ -94,7 +94,8 @@ DEFAULT_LINEAR_VELOCITY = 100.0 MAX_LINEAR_VELOCITY = 300.0 DEFAULT_LINEAR_ACCELERATION = 500.0 MAX_LINEAR_ACCELERATION = 1000.0 -NO_FORCE_HOMING = 1 #to be able to run gcodes and MDI without homing +# to be able to run gcodes and MDI without homing +NO_FORCE_HOMING = 1 ARC_BLEND_ENABLE = 1 diff --git a/debian/linuxcnc.install.in b/debian/linuxcnc.install.in index 4bec8d5537d..b99f8da711e 100644 --- a/debian/linuxcnc.install.in +++ b/debian/linuxcnc.install.in @@ -41,6 +41,7 @@ usr/bin/latency-plot usr/bin/latency-test usr/bin/lineardelta usr/bin/linuxcnc +usr/bin/linuxcnc_check_ini usr/bin/linuxcnc_info usr/bin/linuxcnc_module_helper usr/bin/linuxcnc_var diff --git a/debian/linuxcnc.manpages.in b/debian/linuxcnc.manpages.in index 8c48a5eb1fc..a1b0bb3dc86 100644 --- a/debian/linuxcnc.manpages.in +++ b/debian/linuxcnc.manpages.in @@ -39,6 +39,7 @@ usr/share/man/man1/latency-plot.1 usr/share/man/man1/latency-test.1 usr/share/man/man1/lineardelta.1 usr/share/man/man1/linuxcnc.1 +usr/share/man/man1/linuxcnc_check_ini.1 usr/share/man/man1/linuxcnc_info.1 usr/share/man/man1/linuxcnclcd.1 usr/share/man/man1/linuxcncmkdesktop.1 diff --git a/docs/man/.gitignore b/docs/man/.gitignore index dbd3b0aa127..9c50fe631b1 100644 --- a/docs/man/.gitignore +++ b/docs/man/.gitignore @@ -48,6 +48,7 @@ man1/latency-histogram.1 man1/latency-plot.1 man1/latency-test.1 man1/lineardelta.1 +man1/linuxcnc_check_ini.1 man1/linuxcnc_info.1 man1/linuxcnc_module_helper.1 man1/linuxcnc_var.1 diff --git a/docs/src/config/ini-config.adoc b/docs/src/config/ini-config.adoc index 05761ebe3ac..6a301a39cf4 100644 --- a/docs/src/config/ini-config.adoc +++ b/docs/src/config/ini-config.adoc @@ -46,7 +46,6 @@ In this list, the DISPLAY variable will be set to axis because the other one is If someone carelessly edits a list like this and leaves two of the lines uncommented, the first one encountered will be used. Note that inside a variable's value, the "#" and ";" characters are part of the value: - [source,{ini}] ---- # Below does not result in INCORRECT=value @@ -116,26 +115,26 @@ ini.1.max_velocity \ ini.2.max_velocity ---- -A specific variable in a specific sections is often denoted in the documentation as [SECTION]VARIABLE. +A specific variable in a specific sections is often denoted in the documentation as `[SECTION]VARIABLE`. This specification mirrors the same way they are specified in HAL files for expansion. Variable values may embed special characters in literal or escaped forms. Single or double quotes are not treated as special and are a literal part of the value. Values also support all common escape formats and full Unicode: -* control: \\[abfnrtv] -* octal: \\[0-2][0-7]{0,2} -* hex: \\x[0-9a-fA-F]{2} -* UTF-16: \\u[0-9a-fA-F]{4} -* UTF-32: \\U[0-9a-fA-F]{8} +* control: `\[abfnrtv]` +* octal: `\[0-2][0-7]{0,2}` +* hex: `\x[0-9a-fA-F]{2}` +* UTF-16: `\u[0-9a-fA-F]{4}` +* UTF-32: `\U[0-9a-fA-F]{8}` The resulting value is always converted into UTF-8 and checked for validity. It is not allowed to embed NUL characters either literally or by using an escape. Leading and trailing white space is normally removed from a value. -You can add leading and trailing space in a value by using an escaped value for space (\\x20) as first or last character. +You can add leading and trailing space in a value by using an escaped value for space (`\x20`) as first or last character. Spaces inside a value are automatically part of the value. -Tabs and newlines can be added using \\t and \\n. +Tabs and newlines can be added using `\t` and `\n`. .Value Escape Example [source,{ini}] @@ -148,10 +147,10 @@ STRING = Hello\ World\ ! -SMILE = \\370\\237\\230\\200 = 😀 -SMILE = \\xf0\\x9f\\x98\\x80 = 😀 -SMILE = \\ud83d\\ude00 = 😀 -SMILE = \\U0001f600 = 😀 +SMILE = \370\237\230\200 = 😀 +SMILE = \xf0\x9f\x98\x80 = 😀 +SMILE = \ud83d\ude00 = 😀 +SMILE = \U0001f600 = 😀 ---- Variables' value can have types associated when they are read by LinuxCNC. @@ -171,18 +170,18 @@ Plain old 32-bit integers are marked `int` and are always signed. The default number base is decimal. Alternative bases may be specified using a prefix. The complete list of valid integer numbers: -* \[0-9]+ - decimal -* 0x\[0-9A-Fa-f]+ - hexadecimal -* 0o\[0-7]+ - octal -* 0b\[01]+ - binary +* `[0-9]+` (decimal) +* `0x[0-9A-Fa-f]+` (hexadecimal) +* `0o[0-7]+` (octal) +* `0b[01]+` (binary) Signed integers may be preceded by a plus (+) or minus (-) sign, regardless number base. Unsigned integers will generate a warning if they are preceded by a minus (-) sign upon conversion. Boolean values are case insensitive and allow the following words: -* true/enabled - `TRUE`, `YES` `ON` or `1` -* false/disabled - `FALSE`, `NO` `OFF` or `0` +* true/enabled - `TRUE`, `YES`, `ON` or `1` +* false/disabled - `FALSE`, `NO`, `OFF` or `0` Enumerations are a set of keywords defined by LinuxCNC and interpreted to mean a setting, value or functionality. The exact values are declared in the individual variable description and the variables are marked with `enum`. @@ -253,7 +252,8 @@ G10 L20 P0 Z#<_ini[probe]z_offset> [[sub:ini:include]] === Include Files(((INI File,Components,Include))) -An INI file may include the contents of another file by using a #INCLUDE directive. +An INI file may include the contents of another file by using a `#INCLUDE` directive. +The `#INCLUDE` directive must be in upper case, start in the first column of the line and have at least one space after it. .#INCLUDE Format [source,{ini}] @@ -268,7 +268,7 @@ The filename can be specified as: * an absolute file name (starts with a /) * a user-home-relative file name (starts with a ~/) -Multiple #INCLUDE directives are supported. +Multiple `#INCLUDE` directives are supported. .#INCLUDE Examples [source,{ini}] @@ -280,7 +280,7 @@ Multiple #INCLUDE directives are supported. #INCLUDE ~/linuxcnc/myincludes/rs274ngc.inc ---- -The #INCLUDE directives are supported up to 16 levels -- an included file may include additional files up to 16 levels deep. +The `#INCLUDE` directives are supported up to 16 levels -- an included file may include additional files up to 16 levels deep. Recursive inclusion of files is detected and flagged as an error. The recommended file extension is '.inc'. Do _not_ use a file extension of '.ini' for included files. @@ -331,12 +331,12 @@ Descriptions of the interfaces are in the Interfaces section of the User Manual. The example above will display only one decimal digit. Formatting follows Python practice: https://docs.python.org/2/library/string.html#format-specification-mini-language . An error will be raised if the format can not accept a floating-point value. -* `CONE_BASESIZE = .25` - Override the default cone/tool base size of .5 in the graphics display. Valid values are between 0.025 and 2.0. -* `DISABLE_CONE_SCALING = TRUE` - Any non-empty value (including "0") will override the default behavior of scaling the cone/tool size using the extents of the currently loaded G-code program in the graphics display. -* `GCODE_VIEW_TOOL_MIN_DIA = 2.0` - If the tool diameter is very small, but non-zero then the displayed cylinder may be too small to see. +* `CONE_BASESIZE = .25` - (real) Override the default cone/tool base size of .5 in the graphics display. Valid values are between 0.025 and 2.0. +* `DISABLE_CONE_SCALING = TRUE` - (bool) Will override the default behavior of scaling the cone/tool size using the extents of the currently loaded G-code program in the graphics display. +* `GCODE_VIEW_TOOL_MIN_DIA = 2.0` - (real) If the tool diameter is very small, but non-zero then the displayed cylinder may be too small to see. This could happen if the tool diameter was being used as a wear offset. This setting will cause the tool cone to be displayed rather than a tool cylinder. -* `MAX_FEED_OVERRIDE = 1.2` - The maximum feed override the user may select. +* `MAX_FEED_OVERRIDE = 1.2` - (real) The maximum feed override the user may select. 1.2 means 120% of the programmed feed rate. * `MIN_SPINDLE_OVERRIDE = 0.5` - (real) The minimum spindle override the user may select. 0.5 means 50% of the programmed spindle speed. (This is used to set the minimum spindle speed.) @@ -373,7 +373,7 @@ Descriptions of the interfaces are in the Interfaces section of the User Manual. An under powered CPU may see improvement with a longer setting. Usually the default is fine. * `PREVIEW_TIMEOUT = 5` - Timeout (in seconds) for loading graphical preview of G-code. Currently AXIS only. -* `HOMING_PROMPT = TRUE` - Any non-empty value (including "0") will enable showing a prompt message with homing request, when the Power On button is pressed in AXIS GUI. Pressing the "Ok" button in prompt message is equivalent to pressing the "Home All" button(or the Ctrl-HOME key). +* `HOMING_PROMPT = TRUE` - (bool) Will enable showing a prompt message with homing request, when the Power On button is pressed in AXIS GUI. Pressing the "Ok" button in prompt message is equivalent to pressing the "Home All" button(or the Ctrl-HOME key). * `FOAM_W = 1.5` sets the foam W height. * `FOAM_Z = 0` sets the foam Z height. * `GRAPHICAL_MAX_FILE_SIZE = 20` largest size (in mega bytes) that will be displayed graphically. @@ -425,9 +425,9 @@ See <> document for GMOCCAPY details. * `PYVCP_POSITION = BOTTOM` - The placement of the PyVCP panel in the AXIS user interface. If this variable is omitted the panel will default to the right side. The only valid alternative is `BOTTOM`. See the <> for more information. -* `LATHE = 1` - Any non-empty value (including "0") causes axis to use "lathe mode" with a top view and with Radius and Diameter on the DRO. -* `BACK_TOOL_LATHE = 1` - Any non-empty value (including "0") causes axis to use "back tool lathe mode" with inverted X axis. -* `FOAM = 1` - Any non-empty value (including "0") causes axis to change the display for foam-cutter mode. +* `LATHE = 1` - (bool) Causes axis to use "lathe mode" with a top view and with Radius and Diameter on the DRO. +* `BACK_TOOL_LATHE = 1` - (bool) Causes axis to use "back tool lathe mode" with inverted X axis. +* `FOAM = 1` - (bool) Causes axis to change the display for foam-cutter mode. * `GEOMETRY = XYZABCUVW` - Controls the *preview* and *backplot* of motion. This item consists of a sequence of axis letters and control characters, optionally preceded with a "-" sign: @@ -805,26 +805,26 @@ The applications can be started after a specified delay to allow for GUI-depende If no executable file is found using these names, then the user search PATH is used to find the application. + Examples: ** Simulate inputs to HAL pins for testing (using sim_pin -- a simple GUI to set inputs to parameters, unconnected pins, or signals with no writers): -+ + [source,{ini}] ---- APP = sim_pin motion.probe-input halui.abort motion.analog-in-00 ---- ** Invoke halshow with a previuosly saved watchlist. Since LinuxCNC sets the working directory to the directory for the INI file, you can refer to files in that directory (example: my.halshow): -+ + [source,{ini}] ---- APP = halshow my.halshow ---- ** Alternatively, a watchlist file identified with a full pathname could be specified: -+ + [source,{ini}] ---- APP = halshow ~/saved_shows/spindle.halshow ---- ** Open halscope using a previously saved configuration: -+ + [source,{ini}] ---- APP = halscope -i my.halscope @@ -846,9 +846,9 @@ ARC_BLEND_RAMP_FREQ = 100 The [TRAJ] section contains general parameters for the trajectory planning module in 'motion'. -* `ARC_BLEND_ENABLE = 1` - Turn on new TP. +* `ARC_BLEND_ENABLE = 1` - (bool) Turn on new TP. If set to 0 TP uses parabolic blending (1 segment look ahead) (Default: 1). -* `ARC_BLEND_FALLBACK_ENABLE = 0` - Optionally fall back to parabolic blends if the estimated speed is faster. +* `ARC_BLEND_FALLBACK_ENABLE = 0` - (bool) Optionally fall back to parabolic blends if the estimated speed is faster. However, this estimate is rough, and it seems that just disabling it gives better performance (Default: 0). * `ARC_BLEND_OPTIMIZATION_DEPTH = 50` - Look ahead depth in number of segments. + diff --git a/docs/src/man/man1/linuxcnc_check_ini.1.adoc b/docs/src/man/man1/linuxcnc_check_ini.1.adoc new file mode 100644 index 00000000000..1e4f5afac03 --- /dev/null +++ b/docs/src/man/man1/linuxcnc_check_ini.1.adoc @@ -0,0 +1,61 @@ += linuxcnc_check_ini(1) + +== NAME + +linuxcnc_check_ini - LinuxCNC INI-file configuration checker + +== SYNOPSIS + +*linuxcnc_check_ini* [_-e_] [_-h_] _INIfile_ + +== DESCRIPTION + +The *linuxcnc_check_ini* program is used to verify the LinuxCNC primary +configuration. It takes the specified _INIfile_ and performs a set of sanity +checks on the variables in various sections. + +This program is called automatically from *linuxcnc(1)* as to warn the user of +problems that may exist in the configuration. The tests are not exhaustive +and *linuxcnc_check_ini* does _not_ protect your machine or hardware. It merely +ensures some basic consistency in the configuration setup. You must not rely on +passing the *linuxcnc_check_ini* tests to have a functional machine. + +== OPTIONS + +*-e*,*--error*:: + Treat warnings as errors + +*-h*,*--help*:: + Shows brief help message + +== EXIT VALUE + +The exit value is 0 (zero) when no errors are detected and the program runs +successfully. + +The exit value is 1 (one) if any errors are detected. +Additionally, if the *--error* option was specified, then any detected warnings +also cause the exit value be 1 (one). + +The exit value is 2 (two) in case of any other problem. + +== SEE ALSO + +*linuxcnc(1)* + +== AUTHOR + +This man page written by B.Stultiens, as part of the LinuxCNC Enhanced +Machine Controller project. + +== REPORTING BUGS + +Please report any bugs at https://github.com/LinuxCNC/linuxcnc. + +== COPYRIGHT + +Copyright © 2026 The LinuxCNC authors. + +This is free software; see the source for copying conditions. There is +NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. diff --git a/scripts/linuxcnc.in b/scripts/linuxcnc.in index ca46f402138..9bf244fb037 100644 --- a/scripts/linuxcnc.in +++ b/scripts/linuxcnc.in @@ -356,41 +356,6 @@ if [ -z "$INIFILE" ] ; then exit 0 fi -# There is an INI-reader in the Tcl code (parse_ini in tcl/linuxcnc.tcl), but -# that cannot read or enforce the new INI-file format. The Tcl version does not -# handle includes, strings, continuations and comments. The includes were -# previously handled in here by generating a patched-up INI-file. We now do the -# same, but ensure the enforce the new format. We can generate a predigested -# version of the INI-file using the inivalue program. -# FIXME: This hack should be removed as soon as all the legacy Tcl code using -# INI values has been replaced with python code. -function make_ini_for_tcl() { - inifile="$1" - [ -r "$inifile" ] || { echo "E: Cannot read '$inifile'"; exit 1; } - inidir="$(dirname "$inifile")" - outfile="$inidir/$(basename "$inifile").expanded" - # Fall back to a per-user cache dir if the inifile's directory is not - # writable (e.g. installed sample configs under /usr/share/doc/linuxcnc/). - if ! [ -w "$inidir" ]; then - cachedir="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/linuxcnc-$UID" - mkdir -p "$cachedir" || { echo "E: Could not create cache dir '$cachedir'"; exit 1; } - outfile="$cachedir/$(basename "$inifile").expanded" - fi - export LINUXCNC_INI_EXPANDED="$outfile" - true >|"$outfile" || { echo "E: Could not create expanded inifile '$outfile' for Tcl"; exit 1; } - { - echo "#*** Source: $inifile" - echo "#*** Created: $(date)" - echo "#*** Autogenerated INI-file using linuxcnc for tcl/linuxcnc.tcl:parse_ini()" - echo - } >> "$outfile" - # For all sections, output the variables as parsed - for s in $(inivalue --sections "$inifile"); do - echo "[$s]" >> "$outfile" - inivalue --variables --content --sec "$s" "$inifile" >> "$outfile" - done -} - function split_app_items () { app_name=$1 shift @@ -443,10 +408,6 @@ function run_applications () { # a parse error in the file or it is empty. inivalue "$INIFILE" > /dev/null || { echo "E: The INI-file contains errors that need to be fixed."; exit 1; } -# The resulting INI-file for Tcl will be called "${INIFILE}.expanded" -# See comment above the function why this is necessary. -make_ini_for_tcl "$INIFILE" - # delete directories from path, save name only INI_NAME="${INIFILE##*/}" INI_DIR="${INIFILE%/*}" @@ -525,12 +486,12 @@ if [ "$retval" != "1.1" ]; then esac fi -@TCLSH@ "$HALLIB_DIR/check_config.tcl" "$INIFILE" +linuxcnc_check_ini "$INIFILE" exitval=$? case "$exitval" in 0) ;; - 1) echo "check_config validation failed"; exit $exitval ;; - *) echo "check_config validation failed in an unexpected way."; exit $exitval ;; + 1) echo "linuxcnc_check_ini validation failed"; exit $exitval ;; + *) echo "linuxcnc_check_ini validation failed in an unexpected way."; exit $exitval ;; esac # 2.2. get param file diff --git a/src/Makefile b/src/Makefile index 15c9000d13c..c564f77e843 100644 --- a/src/Makefile +++ b/src/Makefile @@ -688,6 +688,7 @@ install-kernel-indep: install-dirs $(EXE) ../scripts/gladevcp_demo $(DESTDIR)$(bindir) $(EXE) ../scripts/linuxcncmkdesktop $(DESTDIR)$(bindir) $(EXE) ../bin/update_ini $(DESTDIR)$(bindir) + $(EXE) ../bin/linuxcnc_check_ini $(DESTDIR)$(bindir) $(EXE) ../scripts/halreport $(DESTDIR)$(bindir) $(FILE) $(filter ../lib/%.a ../lib/%.so.0 ../lib/%.so.1,$(TARGETS)) $(DESTDIR)$(libdir) cp --no-dereference $(wildcard ../lib/*.so) $(DESTDIR)$(libdir) diff --git a/src/emc/ini/Submakefile b/src/emc/ini/Submakefile index 2b5aad56836..250d2075c49 100644 --- a/src/emc/ini/Submakefile +++ b/src/emc/ini/Submakefile @@ -46,4 +46,12 @@ TARGETS += ../bin/inivar $(ECHO) Copying python script $(notdir $@) $(Q)(echo '#!$(PYTHON)'; sed '1 { /^#!/d; }' $<) > $@.tmp && chmod +x $@.tmp && mv -f $@.tmp $@ +# Ini-file checker at linuxcnc start +../bin/linuxcnc_check_ini: emc/ini/linuxcnc_check_ini.py + @$(ECHO) Syntax checking python script $(notdir $@) + $(Q)$(PYTHON) -m py_compile $< + $(ECHO) Copying python script $(notdir $@) + $(Q)(echo '#!$(PYTHON)'; sed '1 { /^#!/d; }' $<) > $@.tmp && chmod +x $@.tmp && mv -f $@.tmp $@ + PYTARGETS += ../bin/update_ini +PYTARGETS += ../bin/linuxcnc_check_ini diff --git a/src/emc/ini/iniaxis.cc b/src/emc/ini/iniaxis.cc index 90dbe759b58..e15744185a3 100644 --- a/src/emc/ini/iniaxis.cc +++ b/src/emc/ini/iniaxis.cc @@ -62,7 +62,7 @@ static int loadAxis(int axis, const IniFile &ini) std::string axisSection = fmt::format("AXIS_{}", "XYZABCUVW"[axis]); // set min position limit - double limit = ini.findRealV("MIN_LIMIT", axisSection, -1e99); + double limit = ini.findRealV("MIN_LIMIT", axisSection, DEFAULT_AXIS_MIN_LIMIT); if (0 != emcAxisSetMinPositionLimit(axis, limit)) { print_dbg_config("emcAxisSetMinPositionLimit"); return -1; @@ -70,7 +70,7 @@ static int loadAxis(int axis, const IniFile &ini) old_inihal_data.axis_min_limit[axis] = limit; // set max position limit - limit = ini.findRealV("MAX_LIMIT", axisSection, 1e99); + limit = ini.findRealV("MAX_LIMIT", axisSection, DEFAULT_AXIS_MAX_LIMIT); if (0 != emcAxisSetMaxPositionLimit(axis, limit)) { print_dbg_config("emcAxisSetMaxPositionLimit"); return -1; diff --git a/src/emc/ini/inifile.cc b/src/emc/ini/inifile.cc index 5616a2ff317..26fd75d4990 100644 --- a/src/emc/ini/inifile.cc +++ b/src/emc/ini/inifile.cc @@ -31,6 +31,8 @@ // 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; @@ -1480,6 +1482,58 @@ int IniFile::tildeExpand(const std::string &path, std::string &result) 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 diff --git a/src/emc/ini/inifile.hh b/src/emc/ini/inifile.hh index 76a325bd917..4c81f4ee3cd 100644 --- a/src/emc/ini/inifile.hh +++ b/src/emc/ini/inifile.hh @@ -126,6 +126,10 @@ // } // +// 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 @@ -343,6 +347,62 @@ public: 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 diff --git a/src/emc/ini/inijoint.cc b/src/emc/ini/inijoint.cc index f9c01e4a007..37dff22f622 100644 --- a/src/emc/ini/inijoint.cc +++ b/src/emc/ini/inijoint.cc @@ -33,17 +33,6 @@ static void inline print_dbg_config(const std::string &s) } } -static EmcJointType getJointType(const IniFile &ini, const std::string &var, const std::string &sec, EmcJointType def) -{ - static const std::map jointTypeMap = { - {"LINEAR", EMC_LINEAR }, - {"ANGULAR", EMC_ANGULAR} - }; - if(auto c = ini.findMap(jointTypeMap, var, sec)) - return *c; - return def; -} - // // Load INI file params for joint // @@ -78,7 +67,7 @@ static int loadJoint(int joint, const IniFile &ini) { std::string jointSection = fmt::format("JOINT_{}", joint); - EmcJointType jointType = getJointType(ini, "TYPE", jointSection, EMC_LINEAR); + EmcJointType jointType = ini.findJointType("TYPE", jointSection, EMC_LINEAR); if (0 != emcJointSetType(joint, jointType)) { print_dbg_config("emcJointSetType"); return -1; @@ -102,14 +91,14 @@ static int loadJoint(int joint, const IniFile &ini) } old_inihal_data.joint_backlash[joint] = backlash; - double limit = ini.findRealV("MIN_LIMIT", jointSection, -1e99); + double limit = ini.findRealV("MIN_LIMIT", jointSection, DEFAULT_JOINT_MIN_LIMIT); if (0 != emcJointSetMinPositionLimit(joint, limit)) { print_dbg_config("emcJointSetMinPositionLimit"); return -1; } old_inihal_data.joint_min_limit[joint] = limit; - limit = ini.findRealV("MAX_LIMIT", jointSection, 1e99); + limit = ini.findRealV("MAX_LIMIT", jointSection, DEFAULT_JOINT_MAX_LIMIT); if (0 != emcJointSetMaxPositionLimit(joint, limit)) { print_dbg_config("emcJointSetMaxPositionLimit"); return -1; diff --git a/src/emc/ini/initraj.cc b/src/emc/ini/initraj.cc index 671b33b362c..b3140082271 100644 --- a/src/emc/ini/initraj.cc +++ b/src/emc/ini/initraj.cc @@ -34,43 +34,6 @@ static void inline print_dbg_config(const std::string &s) } } -static double findLinearUnits(const IniFile &ini, const std::string &var, const std::string &sec, double def) -{ - // 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 = ini.findMap(linearUnitsMap, var, sec)) - return *c; - return def; -} - -static double findAngularUnits(const IniFile &ini, const std::string &var, const std::string &sec, double def) -{ - // 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 = ini.findMap(angularUnitsMap, var, sec)) - return *c; - return def; -} - // // loadKins() // @@ -139,8 +102,8 @@ static int loadTraj(const IniFile &ini) } - double linearUnits = findLinearUnits(ini, "LINEAR_UNITS", "TRAJ", 0.0); - double angularUnits = findAngularUnits(ini, "ANGULAR_UNITS", "TRAJ", 0.0); + double linearUnits = ini.findLinearUnits("LINEAR_UNITS", "TRAJ", 0.0); + double angularUnits = ini.findAngularUnits("ANGULAR_UNITS", "TRAJ", 0.0); if (0 != emcTrajSetUnits(linearUnits, angularUnits)) { rcs_print("emcTrajSetUnits failed to set [TRAJ]LINEAR_UNITS or [TRAJ]ANGULAR_UNITS\n"); return -1; diff --git a/src/emc/ini/linuxcnc_check_ini.py b/src/emc/ini/linuxcnc_check_ini.py new file mode 100755 index 00000000000..a2d991b3a16 --- /dev/null +++ b/src/emc/ini/linuxcnc_check_ini.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 +# +# Check the INI-file configuration +# Copyright (C) 2026 B. Stultiens +# +# Based on check_config.tcl +# +# 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. +# +import sys +import os +import getopt +import linuxcnc + +inifilename = "" +error_on_warning = False + +# See: emc/motion/emcmotcfg.h +EMCMOT_MAX_JOINTS = 16 +EMCMOT_MAX_AXIS = 9 # XYZABCUVW +EMCMOT_MAX_SPINDLES = 8 + +# See: emc/nml_intf/emccfg.h +DEFAULT_AXIS_MAX_VELOCITY = 1.0 +DEFAULT_AXIS_MAX_ACCELERATION = 1.0 +DEFAULT_AXIS_MAX_JERK = 0.0 +DEFAULT_AXIS_MIN_LIMIT = -1e99 +DEFAULT_AXIS_MAX_LIMIT = +1e99 +DEFAULT_JOINT_MAX_VELOCITY = 1.0 +DEFAULT_JOINT_MAX_ACCELERATION = 1.0 +DEFAULT_JOINT_MAX_JERK = 0.0 +DEFAULT_JOINT_MIN_LIMIT = -1e99 +DEFAULT_JOINT_MAX_LIMIT = +1e99 + + +def usage(): + print("""Check LinuxCNC INI-file configuration. +Usage: + linuxcnc_check_ini [-e] [-h] inifile.ini + +Options: + -e|--error Treat warnings as errors + -h|--help This message +""") + sys.exit(2) + +imessages = [] +wmessages = [] +emessages = [] +# +# Collect messages +# +def perr(msg): + global emessages + emessages.append("{}: error: {}".format(inifilename, msg)) + +def pwarn(msg): + global wmessages + wmessages.append("{}: warning: {}".format(inifilename, msg)) + +def pmsg(msg): + global imessages + imessages.append("{}: info: {}".format(inifilename, msg)) + +# +# Flush the messages to stderr. Returns zero only when no error messages are +# present and no warnings are present when treated as errors. +# +def flush_messages(): + for m in imessages: + print(m, file=sys.stderr); + for m in wmessages: + print(m, file=sys.stderr); + for m in emessages: + print(m, file=sys.stderr); + if len(emessages) > 0 or (error_on_warning and len(wmessages) > 0): + return 1 + else: + return 0 + +# +# Check the ini-file for mandatory settings and the type of those settings +# +def check_mandatory_items(): + # These must be present in the INI-file + MANDATORY_ITEMS = [ + # Use None for Mini and/or Maxi if not used + # Section, Variable, Type(s,r,i,u,b), Mini, Maxi + ("KINS", "KINEMATICS", 's', None, None), + ("KINS", "JOINTS", 'i', 0, EMCMOT_MAX_JOINTS) + ] + + rv = True + for s, v, t, mini, maxi in MANDATORY_ITEMS: + if not ini.hasvariable(s, v): + perr("[{}]{}: Missing entry".format(s, v)) + rv = False + else: + if 's' == t: + if not ini.getstring(s, v): # There must be string content + perr("[{}]{}: Expected a string value".format(s, v)) + rv = False + elif 'i' == t: + val = ini.getsint(s, v) + if None == val: + perr("[{}]{}: Expected an integer value".format(s, v)) + rv = False + if (None != mini and val < mini) or (None != maxi and val > maxi): + perr("[{}]{}: Integer value '{}' out of range [{},{}]".format(s, v, val, mini, maxi)) + rv = False + elif 'u' == t: + val = ini.getuint(s, v) + if None == val: + perr("[{}]{}: Expected an unsigned value".format(s, v)) + rv = False + if (None != mini and val < mini) or (None != maxi and val > maxi): + perr("[{}]{}: Unsigned value '{}' out of range [{},{}]".format(s, v, val, mini, maxi)) + rv = False + elif 'b' == t: + if None == ini.getbool(s, v): + perr("[{}]{}: Expected an boolean value".format(s, v)) + rv = False + elif 'r' == t: + val = ini.getreal(s, v) + if None == val: + perr("[{}]{}: Expected a real value".format(s, v)) + rv = False + if (None != mini and val < mini) or (None != maxi and val > maxi): + perr("[{}]{}: Real value '{}' out of range [{},{}]".format(s, v, val, mini, maxi)) + rv = False + else: + perr("Internal error: check_mandatory_items(): invalid type '{}' for check".format(t)) + return False + return rv + +# +# Get and split a value of a variable in the specified section according to: +# [SECTION]VARIABLE = base [opt=val [opt=val [...]]] +# +def split_opts(section, variable): + parts = ini.getstring(section, variable, fallback="").split() + opts = {} + if len(parts) < 1: + perr("[{}]{}: Missing content".format(section, variable)) + return (None, None) + for opt in parts[1:]: + if '=' in opt: + o, v = opt.split("=") + if o in opts: + perr("[{}}{}: Duplicate option key '{}'".format(section, variable, o)) + return (None, None) + opts[o] = v + else: + perr("[{}]{}: Option '{}' is missing a '=' and value".format(section, variable, opt)) + return (None, None) + return (parts[0], opts) + +# +# Checks the enumerated values of: +# - [TRAJ]LINEAR_UNITS +# - [TRAJ]ANGULAR_UNITS +# - [AXIS_*]TYPE +# - [JOINT_*]TYPE +# - [DISPLAY]POSITION_OFFSET +# - [DISPLAY]POSITION_FEEDBACK +# - [EMC]RCS_DEBUG_DEST +# +def check_enums(): + # Linear and angular units enums + if not ini.hasvariable("TRAJ", "LINEAR_UNITS"): + perr("[TRAJ]LINEAR_UNITS: Missing") + else: + if None == ini.getlinearunits("TRAJ", "LINEAR_UNITS"): + perr("[TRAJ]LINEAR_UNITS: Must be one of [mm,metric,in,inch,imperial]") + + if not ini.hasvariable("TRAJ", "ANGULAR_UNITS"): + perr("[TRAJ]ANGULAR_UNITS: Missing") + else: + if None == ini.getangularunits("TRAJ", "ANGULAR_UNITS"): + perr("[TRAJ]ANGULAR_UNITS: Must be one of [deg,degree,grad,gon,rad,radian]") + + # Get all JOINT_* and AXIS_* section names for the type + sects = list(filter(lambda s: (s.startswith("JOINT_") or s.startswith("AXIS_")), ini.getsections())) + for sec in sects: + # If it is undefined, it will have a default + # Joints map to the axis default if not set + if not ini.hasvariable(sec, "TYPE"): + continue + # But if set, it must be the correct enum + if None == ini.getjointtype(sec, "TYPE"): + perr("[{}]TYPE: Must be one of [LINEAR,ANGULAR]".format(sec)) + + if ini.hasvariable("DISPLAY", "POSITION_OFFSET"): + if ini.getstring("DISPLAY", "POSITION_OFFSET").upper() not in ["RELATIVE", "MACHINE"]: + perr("[DISPLAY]POSITION_OFFSET: Must be one of [RELATIVE,MACHINE]") + if ini.hasvariable("DISPLAY", "POSITION_FEEDBACK"): + if ini.getstring("DISPLAY", "POSITION_FEEDBACK").upper() not in ["COMMANDED", "ACTUAL"]: + perr("[DISPLAY]POSITION_FEEDBACK: Must be one of [COMMANDED,ACTUAL]") + + if ini.hasvariable("EMC", "RCS_DEBUG_DEST"): + if ini.getstring("EMC", "RCS_DEBUG_DEST").upper() not in ["NULL", "STDOUT", "STDERR", "FILE", "LOGGER", "MSGBOX"]: + perr("[EMC]RCS_DEBUG_DEST: Must be one of [NULL,STDOUT,STDERR,FILE,LOGGER,MSGBOX]") + +# +# Check a set of variables for having the right type if they are present +# +def check_bool(s, v): + if ini.hasvariable(s, v): + if None == ini.getbool(s, v): + perr("[{}]{}: Invalid boolean value".format(s, v)) + +def check_bools(): + BOOLVARS = [("TRAJ", "NO_FORCE_HOMING"), + ("TRAJ", "ARC_BLEND_ENABLE"), + ("TRAJ", "ARC_BLEND_FALLBACK_ENABLE"), + ("EMCIO", "TOOL_CHANGE_WITH_SPINDLE_ON"), + ("EMCIO", "TOOL_CHANGE_QUILL_UP"), + ("EMCIO", "TOOL_CHANGE_AT_G30"), + ("EMCIO", "RANDOM_TOOLCHANGER"), + ("RS274NGC", "INI_VARS"), + ("RS274NGC", "HAL_PIN_VARS"), + ("RS274NGC", "RETAIN_G43"), + ("RS274NGC", "OWORD_NARGS"), + ("RS274NGC", "NO_DOWNCASE_OWORD"), + ("RS274NGC", "OWORD_WARN_ONLY"), + ("RS274NGC", "DISABLE_G92_PERSISTENCE"), + ("RS274NGC", "DISABLE_FANUC_STYLE_SUB"), + ("DISPLAY", "DISABLE_CONE_SCALING"), + ("DISPLAY", "HOMING_PROMPT"), + ("DISPLAY", "LATHE"), + ("DISPLAY", "BACK_TOOL_LATHE"), + ("DISPLAY", "FOAM") + ] + # Variables in [AXIS_*] + BOOLAVARS = ["WRAPPED_ROTARY"] + # Variables in [JOINT_*] + BOOLJVARS = ["LOCKING_INDEXER", + "HOME_USE_INDEX", + "HOME_INDEX_NO_ENCODER_RESET", + "HOME_IGNORE_LIMITS", + "HOME_IS_SHARED", + "VOLATILE_HOME" + ] + for s, v in BOOLVARS: + check_bool(s, v) + for asect in list(filter(lambda s: s.startswith("AXIS_"), ini.getsections())): + for v in BOOLAVARS: + check_bool(asect, v) + for jsect in list(filter(lambda s: s.startswith("JOINT_"), ini.getsections())): + for v in BOOLJVARS: + check_bool(jsect, v) + +# +# Integer checks also include a range check +# +def check_int(s, v, mini, maxi): + if ini.hasvariable(s, v): + val = ini.getsint(s, v) + if None == val: + perr("[{}]{}: Invalid integer value".format(s, v)) + if (None != mini and val < mini) or (None != maxi and val > maxi): + if None == mini: mini = "" + if None == maxi: maxi = "" + perr("[{}]{}: Integer value '{}' out of range [{},{}]".format(s, v, val, mini, maxi)) + +def check_ints(): + INTVARS = [ + # section, variable, minimum, maximum + # Use None for min/max if not used + ("EMCMOT", "BASE_PERIOD", 0, None), + ("EMCMOT", "SERVO_PERIOD", 0, None), + ("EMCMOT", "TRAJ_PERIOD", 0, None), + ("TRAJ", "SPINDLES", 0, EMCMOT_MAX_SPINDLES), + ("TRAJ", "PLANNER_TYPE", 0, 1) + ] + # Variables in [JOINT_*] + INTJVARS = [ + ("COMP_FILE_TYPE", 0, 1), + ("HOME_ABSOLUTE_ENCODER", 0, 2) + ] + for s, v, mi, ma in INTVARS: + check_int(s, v, mi, ma) + for jsect in list(filter(lambda s: s.startswith("JOINT_"), ini.getsections())): + for v, mi, ma in INTJVARS: + check_int(jsect, v, mi, ma) + +# +# Kinematics is specified as: +# [KINS]KINEMATICS = module [coordinates=] [kinstype=] [...] +# +def parse_kinematics(): + kins, opts = split_opts("KINS", "KINEMATICS") + if None == kins: + return (None, None) + # Check the axis->joint mapping + if "coordinates" in opts: + for c in opts["coordinates"].upper(): + if c not in "XYZABCUVW": + perr("Invalid axis '{}' in kinematics coordinates option".format(c)) + return (None, None) + return (kins, opts) + +# +# Motion controller is specified like: +# [EMCMOT]EMCMOT = module [num_extrajoints=N] [unlock_joints_mask=N] [...] +# +def check_extrajoints(): + if not ini.hasvariable("EMCMOT", "EMCMOT"): + return # Will default in the linuxcnc script + motmod, motopts = split_opts("EMCMOT", "EMCMOT") + if None == motmod: + return # Message was emitted in split_opts() + + if "num_extrajoints" in motopts: + pmsg("Extra joints specified={}; [KINS]JOINTS={} must accommodate kinematic joints *plus* extra joints" + .format(motopts["num_extrajoints"], ini.find("KINS", "JOINTS"))) + +# +# Traverse all JOINT_* and AXIS_* sections and see if there are any duplicate +# variables in them. It could be a serious problem if duplicates are detected. +# +def warn_for_multiple_ini_values(): + # Select all JOINT_* and AXIS_* sections + # We could be more specific with a regex, but that is optional... + sects = list(filter(lambda s: (s.startswith("JOINT_") or s.startswith("AXIS_")), ini.getsections())) + for sec in sects: + # Get all variable names from the section + # ini.getvariables returns list of tuples (name,value) + varnames = [n for n,v in ini.getvariables(sec)] + # Get the set of duplicates + dups = [x for x in set(varnames) if varnames.count(x) > 1] + # Warn all duplicates + for d in dups: + v = ini.findall(sec, d) # Get all values to show the values + pwarn("[{}]{}: Duplicate entry found, values: {}".format(sec, d, v)) + +# +# Check maximum joint and axis velocity/acceleration +# Check consistency of axis/joint min/max limits +# +def validate_identity_kins_limits(coords, kinematics): + planjerk = ini.getuint("TRAJ", "PLANNER_TYPE", fallback=0) > 0 + # Check joints velocity and acceleration + for joint in range(0, ini.getsint("KINS", "JOINTS")): + jsec = "JOINT_{}".format(joint) + if not ini.hasvariable(jsec, "MAX_VELOCITY"): + pwarn("[{}]MAX_VELOCITY: Unspecified, default used: {}".format(jsec, DEFAULT_JOINT_MAX_VELOCITY)) + if not ini.hasvariable(jsec, "MAX_ACCELERATION"): + pwarn("[{}]MAX_ACCELERATION: Unspecified, default used: {}".format(jsec, DEFAULT_JOINT_MAX_ACCELERATION)) + if planjerk and not ini.hasvariable(jsec, "MAX_JERK"): + pwarn("[{}]MAX_JERK: Unspecified, default used: {}".format(jsec, DEFAULT_JOINT_MAX_JERK)) + + # Check axis velocity and acceleration + for a in list(set(coords)): # Make axis letters unique + asec = "AXIS_{}".format(a) + if not ini.hassection(asec): + continue + if not ini.hasvariable(asec, "MAX_VELOCITY"): + pwarn("[{}]MAX_VELOCITY: Unspecified, default used: {}".format(asec, DEFAULT_AXIS_MAX_VELOCITY)) + if not ini.hasvariable(asec, "MAX_ACCELERATION"): + pwarn("[{}]MAX_ACCELERATION: Unspecified, default used: {}".format(asec, DEFAULT_AXIS_MAX_ACCELERATION)) + if planjerk and not ini.hasvariable(asec, "MAX_JERK"): + pwarn("[{}]MAX_JERK: Unspecified, default used: {}".format(asec, DEFAULT_AXIS_MAX_JERK)) + + # Check limits + # Iterate all joints of axes of the same name (f.ex. XYYZ for Y gives [1, 2]) + for joint in [u for u,v in filter(lambda x: x[1] == a, enumerate(coords))]: + jsec = "JOINT_{}".format(joint) + jlim = ini.getreal(jsec, "MIN_LIMIT", fallback=DEFAULT_JOINT_MIN_LIMIT) + alim = ini.getreal(asec, "MIN_LIMIT", fallback=DEFAULT_AXIS_MIN_LIMIT) + if jlim > alim: + if not ini.hasvariable(asec, "MIN_LIMIT"): + pwarn("[{}]MIN_LIMIT: Unspecified, default used: {}".format(asec, alim)) + perr("[{}]MIN_LIMIT > [{}]MIN_LIMIT ({} > {})".format(jsec, asec, jlim, alim)) + jlim = ini.getreal(jsec, "MAX_LIMIT", fallback=DEFAULT_JOINT_MAX_LIMIT) + alim = ini.getreal(asec, "MAX_LIMIT", fallback=DEFAULT_AXIS_MAX_LIMIT) + if jlim < alim: + if not ini.hasvariable(asec, "MAX_LIMIT"): + pwarn("[{}]MAX_LIMIT: Unspecified, default used: {}".format(asec, alim)) + perr("[{}]MAX_LIMIT < [{}]MAX_LIMIT ({} < {})".format(jsec, asec, jlim, alim)) + +# +# Ensure coordinates are consistent +# +def consistent_coords_for_trivkins(coords): + if "XYZABCUVW" == coords: + return # No coordinates were specified + + trajcoords = ini.getstring("TRAJ", "COORDINATES", fallback="").replace(" ", "").replace("\t", "").upper() + if coords != trajcoords: + pwarn("Inconsistent coordinates specifications: trivkins coordinates={} vs. [TRAJ]COORDINATES={}" + .format(coords, trajcoords)) + +# +# Main program entry +# +def main(): + # Get the command-line options + global progname + progname = os.path.basename(sys.argv[0]) + + try: + opts, args = getopt.getopt(sys.argv[1:], "eh", ["error", "help"]) + except getopt.GetoptError as err: + print(err, file=sys.stderr) # Something like "option -a not recognized" + sys.exit(2) + + for o, a in opts: + if o in ("-e", "--error"): + global error_on_warning + error_on_warning = True + if o in ("-h", "--help"): + usage() # no return from here + else: + print("Unhandled option: '{}'".format(o), file=sys.stderr); + sys.exit(2) + + if 1 != len(args): + print("Must have exactly one argument specifying the INI-file path.", file=sys.stderr) + sys.exit(2) + + # Open the INI-file + global inifilename, ini + inifilename = args[0] + try: + ini = linuxcnc.ini(inifilename) + except linuxcnc.error as err: + print(err, file=sys.stderr) + sys.exit(2) + + # From here on we collect message using perr(), pwarn() and pmsg(). The + # messages get flushed when we are done. The program's return value depends + # on whether there are errors or not. + + # Check 1: Check mandatory items that must exist + if not check_mandatory_items(): + return # No point in continuing when this fails + + # Check 2: Check all JOINT_* and AXIS_* sections for duplicate variables + warn_for_multiple_ini_values() # we accept warnings + + # Check 3: Check variable types and value ranges + check_enums() + check_bools() + check_ints() + + # Check 4: Warn when num_extrajoints is specified + check_extrajoints() # An error message exists if parsing [EMCMOT]EMCMOT failed + + # Check 5: Get the [KINS]KINEMATICS + kinematics, kinsopts = parse_kinematics() + if None == kinematics: + return # There is no point in continuing if parsing [KINS]KINEMATICS failed + + # Only trivial kinematics are checked. Others need different checks + if "trivkins" == kinematics: + # Provide a full coordinate string if not defined in kinematics entry + if "coordinates" in kinsopts: + coords = kinsopts["coordinates"].upper() + else: + coords = "XYZABCUVW" + + # Check trivkins 1: Velocity, acceleration and min/max limits + validate_identity_kins_limits(coords, kinematics) + + # Check trivkins 2: Ensure coordinate specification consistency + consistent_coords_for_trivkins(coords) + else: + pmsg("[KINS]KINEMATICS={}: Unchecked".format(kinematics)) + + return + +if __name__ == "__main__": + main() + sys.exit(flush_messages()) # Exit value depends on presence of error messages diff --git a/src/emc/nml_intf/emc.hh b/src/emc/nml_intf/emc.hh index 0637940362f..20bb4bba564 100644 --- a/src/emc/nml_intf/emc.hh +++ b/src/emc/nml_intf/emc.hh @@ -468,7 +468,7 @@ extern EMC_IO_STAT *emcIoStatus; extern EMC_MOTION_STAT *emcMotionStatus; // values for EMC_JOINT_SET_JOINT, jointType -enum EmcJointType { +enum EmcJointType : int { EMC_LINEAR = 1, EMC_ANGULAR = 2, }; diff --git a/src/emc/nml_intf/emccfg.h b/src/emc/nml_intf/emccfg.h index 567a26e0e1f..1b68005f976 100644 --- a/src/emc/nml_intf/emccfg.h +++ b/src/emc/nml_intf/emccfg.h @@ -69,6 +69,10 @@ extern const char * DEFAULT_EMC_NMLFILE; */ #define DEFAULT_JOINT_MAX_JERK 0.0 +/* default joint limits in either direction */ +#define DEFAULT_JOINT_MIN_LIMIT (-1e99) +#define DEFAULT_JOINT_MAX_LIMIT (1e99) + /* default axis velocity, in user units per second */ #define DEFAULT_AXIS_MAX_VELOCITY 1.0 @@ -82,6 +86,10 @@ extern const char * DEFAULT_EMC_NMLFILE; */ #define DEFAULT_AXIS_MAX_JERK 0.0 +/* default axis limits in either direction */ +#define DEFAULT_AXIS_MIN_LIMIT (-1e99) +#define DEFAULT_AXIS_MAX_LIMIT (1e99) + #ifdef __cplusplus } /* matches extern "C" at top */ #endif diff --git a/src/emc/usr_intf/axis/extensions/emcmodule.cc b/src/emc/usr_intf/axis/extensions/emcmodule.cc index e63d9b83c6f..dfccab55962 100644 --- a/src/emc/usr_intf/axis/extensions/emcmodule.cc +++ b/src/emc/usr_intf/axis/extensions/emcmodule.cc @@ -168,7 +168,8 @@ static PyObject *Ini_has_section(pyIniFile *self, PyObject *args) static PARSE_KW_CONST char kw_empty[] = ""; static PARSE_KW_CONST char kw_fallback[] = "fallback"; -static PARSE_KW_CONST char *kw_eefn[] = { kw_empty, kw_empty, kw_empty, kw_fallback, NULL }; +static PARSE_KW_CONST char *kw_eeefn[] = { kw_empty, kw_empty, kw_empty, kw_fallback, NULL }; +static PARSE_KW_CONST char *kw_efn[] = { kw_empty, kw_fallback, NULL }; #undef PARSE_KW_CONST @@ -186,7 +187,7 @@ static PyObject *Ini_get_bool(pyIniFile *self, PyObject *args, PyObject *kwargs) const char *sect, *var; int num = 1; PyObject *def = Py_None; - if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getbool", kw_eefn, §, &var, &num, &def)) + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getbool", kw_eeefn, §, &var, &num, &def)) return NULL; if(num < 1) { @@ -222,7 +223,7 @@ static PyObject *Ini_get_sint(pyIniFile *self, PyObject *args, PyObject *kwargs) const char *sect, *var; int num = 1; PyObject *def = Py_None; - if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getsint", kw_eefn, §, &var, &num, &def)) + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getsint", kw_eeefn, §, &var, &num, &def)) return NULL; if(num < 1) { @@ -258,7 +259,7 @@ static PyObject *Ini_get_uint(pyIniFile *self, PyObject *args, PyObject *kwargs) const char *sect, *var; int num = 1; PyObject *def = Py_None; - if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getuint", kw_eefn, §, &var, &num, &def)) + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getuint", kw_eeefn, §, &var, &num, &def)) return NULL; if(num < 1) { @@ -294,7 +295,7 @@ static PyObject *Ini_get_real(pyIniFile *self, PyObject *args, PyObject *kwargs) const char *sect, *var; int num = 1; PyObject *def = Py_None; - if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getreal", kw_eefn, §, &var, &num, &def)) + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getreal", kw_eeefn, §, &var, &num, &def)) return NULL; if(num < 1) { @@ -330,7 +331,7 @@ static PyObject *Ini_get_string(pyIniFile *self, PyObject *args, PyObject *kwarg const char *sect, *var; int num = 1; PyObject *def = Py_None; - if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getstring", kw_eefn, §, &var, &num, &def)) + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getstring", kw_eeefn, §, &var, &num, &def)) return NULL; if(num < 1) { @@ -509,6 +510,183 @@ static PyObject *Ini_lineof(pyIniFile *self, PyObject *args) { return result; } +// +// PyFloat|None linuxcnc.ini.maplinearunits(string:enumstr [, fallback=val]) +// +// Use argument enumstr and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_map_linearunits(pyIniFile * /*self*/, PyObject *args, PyObject *kwargs) +{ + const char *str; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "s|$O:maplinearunits", kw_efn, &str, &def)) + return NULL; + + if(auto v = IniFile::mapLinearUnits(str)) + return PyFloat_FromDouble(*v); + + // Not found, a fallback set by argument or as None + Py_INCREF(def); + return def; +} + +// +// PyFloat|None linuxcnc.ini.mapangularunits(string:enumstr [, fallback=val]) +// +// Use argument enumstr and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_map_angularunits(pyIniFile * /*self*/, PyObject *args, PyObject *kwargs) +{ + const char *str; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "s|$O:mapangularunits", kw_efn, &str, &def)) + return NULL; + + if(auto v = IniFile::mapAngularUnits(str)) + return PyFloat_FromDouble(*v); + + // Not found, a fallback set by argument or as None + Py_INCREF(def); + return def; +} + +// +// PyFloat|None linuxcnc.ini.mapjointtype(string:enumstr [, fallback=val]) +// +// Use argument enumstr and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_map_jointtype(pyIniFile * /*self*/, PyObject *args, PyObject *kwargs) +{ + const char *str; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "s|$O:mapjointtype", kw_efn, &str, &def)) + return NULL; + + if(auto v = IniFile::mapJointType(str)) + return PyLong_FromLong((long)*v); + + // Not found, a fallback set by argument or as None + Py_INCREF(def); + return def; +} + +// +// PyFloat|None linuxcnc.ini.getlinearunits(string:section, string:variable [, int:num] [, fallback=val]) +// +// Find [section]variable and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_get_linearunits(pyIniFile *self, PyObject *args, PyObject *kwargs) +{ + const char *sect, *var; + int num = 1; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getlinearunits", kw_eeefn, §, &var, &num, &def)) + return NULL; + + if(num < 1) { + PyErr_Format(error, "Argument 'num' must be >= 1"); + return NULL; + } + + IniFile ini(self->inifile); + if(!ini) { + PyErr_Format(error, "Internal: ini-file could not be reopened"); + return NULL; + } + + if(auto v = ini.findLinearUnits(num, var, sect)) + return PyFloat_FromDouble(*v); + + // Not found or error + // We have a fallback set by argument or as None + Py_INCREF(def); + return def; +} + +// +// PyFloat|None linuxcnc.ini.getangularunits(string:section, string:variable [, int:num] [, fallback=val]) +// +// Find [section]variable and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_get_angularunits(pyIniFile *self, PyObject *args, PyObject *kwargs) +{ + const char *sect, *var; + int num = 1; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getangularunits", kw_eeefn, §, &var, &num, &def)) + return NULL; + + if(num < 1) { + PyErr_Format(error, "Argument 'num' must be >= 1"); + return NULL; + } + + IniFile ini(self->inifile); + if(!ini) { + PyErr_Format(error, "Internal: ini-file could not be reopened"); + return NULL; + } + + if(auto v = ini.findAngularUnits(num, var, sect)) + return PyFloat_FromDouble(*v); + + // Not found or error + // We have a fallback set by argument or as None + Py_INCREF(def); + return def; +} + +// +// PyFloat|None linuxcnc.ini.getjointtype(string:section, string:variable [, int:num] [, fallback=val]) +// +// Find [section]variable and convert the enumeration to its numerical value. +// The optional named argument fallback= defines the value to return when the +// variable is not found or invalid. The optional named option num= selects the +// num'th variable of the section (default to 1). +// +static PyObject *Ini_get_jointtype(pyIniFile *self, PyObject *args, PyObject *kwargs) +{ + const char *sect, *var; + int num = 1; + PyObject *def = Py_None; + if(!PyArg_ParseTupleAndKeywords(args, kwargs, "ss|i$O:getjointtype", kw_eeefn, §, &var, &num, &def)) + return NULL; + + if(num < 1) { + PyErr_Format(error, "Argument 'num' must be >= 1"); + return NULL; + } + + IniFile ini(self->inifile); + if(!ini) { + PyErr_Format(error, "Internal: ini-file could not be reopened"); + return NULL; + } + + if(auto v = ini.findJointType(num, var, sect)) + return PyLong_FromLong((long)*v); + + // Not found or error + // We have a fallback set by argument or as None + Py_INCREF(def); + return def; +} + static void Ini_dealloc(pyIniFile *self) { PyObject_Del(self); } @@ -596,6 +774,39 @@ static PyMethodDef Ini_methods[] = { "variable in the section. The first matching section variable is " "returned if num if not provided. The tuple (None, None) is returned " "if the variable is not found." }, + {"maplinearunits", (PyCFunction)Ini_map_linearunits, METH_VARARGS|METH_KEYWORDS, + "PyFloat|None maplinearunits(enumstr [, fallback=])\n" + "Take the enumeration string argument and try to convert. " + "Returns the value associated with enumerated type defined by " + "[mm, metric, in, inch, imperial]." }, + {"mapangularunits", (PyCFunction)Ini_map_angularunits, METH_VARARGS|METH_KEYWORDS, + "PyFloat|None mapangularunits(enumstr [, fallback=])\n" + "Take the enumeration string argument and try to convert. " + "Returns the value associated with enumerated type defined by " + "[deg, degree, grad, gon, rad, radian]." }, + {"mapjointtype", (PyCFunction)Ini_map_jointtype, METH_VARARGS|METH_KEYWORDS, + "PyInt|None mapjointtype(enumstr [, fallback=])\n" + "Take the enumeration string argument and try to convert. " + "Returns the value associated with enumerated type defined by " + "[LINEAR, ANGULAR]." }, + {"getlinearunits", (PyCFunction)Ini_get_linearunits, METH_VARARGS|METH_KEYWORDS, + "PyFloat|None getlinearunits(section, variable [, num] [, fallback=])\n" + "Get the ini variable from the section and convert the enumerated type. " + "The optional num argument may be used to select the num'th variable of " + "that name in the section. Returns the value associated with enumerated " + "type defined by [mm, metric, in, inch, imperial]." }, + {"getangularunits", (PyCFunction)Ini_get_angularunits, METH_VARARGS|METH_KEYWORDS, + "PyFloat|None getangularunits(section, variable [, num] [, fallback=])\n" + "Get the ini variable from the section and convert the enumerated type. " + "The optional num argument may be used to select the num'th variable of " + "that name in the section. Returns the value associated with enumerated " + "type defined by [deg, degree, grad, gon, rad, radian]." }, + {"getjointtype", (PyCFunction)Ini_get_jointtype, METH_VARARGS|METH_KEYWORDS, + "PyInt|None getjointtype(section, variable [, num] [, fallback=])\n" + "Get the ini variable from the section and convert the enumerated type. " + "The optional num argument may be used to select the num'th variable of " + "that name in the section. Returns the value associated with enumerated " + "type defined by [LINEAR, ANGULAR]." }, {} }; #pragma GCC diagnostic pop @@ -617,6 +828,12 @@ static const char linuxcncinidoc[] = " PyList(PyTuple(name,value)) getvariables([section])\n" " PyList findall(section [,variable])\n" " PyTuple(filename,lineno) lineof(section, variable [, num])\n" + " PyFloat|None getlinearunits(section, variable [, num] [, fallback=])\n" + " PyFloat|None getangularunits(section, variable [, num] [, fallback=])\n" + " PyInt|None getjointtype(section, variable [, num] [, fallback=])\n" + " PyFloat|None maplinearunits(enumstr [, fallback=])\n" + " PyFloat|None mapangularunits(enumstr [, fallback=])\n" + " PyInt|None mapjointtype(enumstr [, fallback=])\n" "\n" "Several convenience methods are provided as aliases to above methods:\n" " PyInt|None getint(section, variable [, num] [, fallback=])\n" diff --git a/src/emc/usr_intf/axis/scripts/axis.py b/src/emc/usr_intf/axis/scripts/axis.py index 3eff037ccea..83bb0133eca 100755 --- a/src/emc/usr_intf/axis/scripts/axis.py +++ b/src/emc/usr_intf/axis/scripts/axis.py @@ -3377,13 +3377,6 @@ def bind_axis(a, b, d): open_directory = "programs" -unit_values = {'inch': 1/25.4, 'mm': 1} -def units(s, d=1.0): - try: - return float(s) - except ValueError: - return unit_values.get(s, d) - random_toolchanger = inifile.getbool("EMCIO", "RANDOM_TOOLCHANGER", fallback=False) vars.emcini.set(sys.argv[2]) jointcount = inifile.getint("KINS", "JOINTS", fallback=0) @@ -3556,10 +3549,13 @@ def units(s, d=1.0): if inifile.find("RS274NGC", "PARAMETER_FILE") is None: raise SystemExit("Missing INI file setting for [RS274NGC]PARAMETER_FILE") -try: - lu = units(inifile.find("TRAJ", "LINEAR_UNITS")) -except TypeError: - raise SystemExit("Missing [TRAJ]LINEAR_UNITS or ANGULAR_UNITS") +# FIXME: The GUI is apparently fixed to work in degrees only and doesn't even +# read [TRAJ]ANGULAR_UNITS. The GUI should support all angular units. +if not inifile.hasvariable("TRAJ", "LINEAR_UNITS"): + raise SystemExit("Missing [TRAJ]LINEAR_UNITS") +lu = inifile.getlinearunits("TRAJ", "LINEAR_UNITS") +if None == lu: + raise SystemExit("Invalid [TRAJ]LINEAR_UNITS") a_axis_wrapped = inifile.getbool("AXIS_A", "WRAPPED_ROTARY", fallback=False) b_axis_wrapped = inifile.getbool("AXIS_B", "WRAPPED_ROTARY", fallback=False) c_axis_wrapped = inifile.getbool("AXIS_C", "WRAPPED_ROTARY", fallback=False) @@ -3734,8 +3730,7 @@ def aletter_for_jnum(jnum): a = "XYZABCUVW"[a] if s.axis_mask & (1< 4) { + Tcl_SetObjResult(interp, Tcl_ObjPrintf("%s: need 'variable' and 'section' arguments (and optional default)", pfx)); + return false; + } + if (!inifile) { + Tcl_SetObjResult(interp, Tcl_ObjPrintf("%s: failed to open ini-file", pfx)); + return false; + } + return true; +} + +// +// emc_ini "VARIABLE" "SECTION" ["DEFAULT"] +// Return the string value of [SECTION]VARIABLE +// Returns an empty string if not found and no default is provided +// static int emc_ini(ClientData /*clientdata*/, Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) { IniFile inifile(emc_inifile); - const char *varstr, *secstr, *defaultstr; - defaultstr = 0; + const char *varstr, *secstr; - if (objc != 3 && objc != 4) { - setresult(interp,"emc_ini: need 'var' and 'section'"); + if (!test_ini_args(interp, inifile, objc, "emc_ini")) return TCL_ERROR; + + varstr = Tcl_GetStringFromObj(objv[1], 0); + secstr = Tcl_GetStringFromObj(objv[2], 0); + + if (auto inival = inifile.findString(varstr, secstr)) { + setresult(interp, inival->c_str()); + } else { + const char *defaultstr = NULL; + if (4 == objc) + defaultstr = Tcl_GetStringFromObj(objv[3], 0); + + if (NULL != defaultstr) + setresult(interp, defaultstr); + else + setresult(interp, ""); } - if (!inifile) { - setresult(interp, "emc_ini: failed to open ini-file'"); - return TCL_OK; + return TCL_OK; +} + +// +// emc_ini_real "VARIABLE" "SECTION" [] +// Return the double value of [SECTION]VARIABLE +// Returns an error if not found and no default is provided +// +static int emc_ini_real(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + IniFile inifile(emc_inifile); + const char *varstr, *secstr; + + if (!test_ini_args(interp, inifile, objc, "emc_ini_real")) + return TCL_ERROR; + + varstr = Tcl_GetStringFromObj(objv[1], 0); + secstr = Tcl_GetStringFromObj(objv[2], 0); + + if (auto inival = inifile.findReal(varstr, secstr)) { + Tcl_SetObjResult(interp, Tcl_NewDoubleObj(*inival)); + } else { + if (4 == objc) + Tcl_SetObjResult(interp, objv[3]); + else + return TCL_ERROR; } + return TCL_OK; +} + +// +// emc_ini_int "VARIABLE" "SECTION" [] +// Return the integer value of [SECTION]VARIABLE +// Returns an error if not found and no default is provided +// +static int emc_ini_int(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + IniFile inifile(emc_inifile); + const char *varstr, *secstr; + + if (!test_ini_args(interp, inifile, objc, "emc_ini_int")) + return TCL_ERROR; + varstr = Tcl_GetStringFromObj(objv[1], 0); secstr = Tcl_GetStringFromObj(objv[2], 0); - if (objc == 4) { - defaultstr = Tcl_GetStringFromObj(objv[3], 0); + if (auto inival = inifile.findInt(varstr, secstr)) { + Tcl_SetObjResult(interp, Tcl_NewIntObj(*inival)); + } else { + if (4 == objc) + Tcl_SetObjResult(interp, objv[3]); + else + return TCL_ERROR; } - auto inistring = inifile.findString(varstr, secstr); - if (!inistring) { - if (defaultstr != 0) { - setresult(interp,(char *) defaultstr); - } - return TCL_OK; + return TCL_OK; +} + +// +// emc_ini_wideint "VARIABLE" "SECTION" [] +// Return the wideint value of [SECTION]VARIABLE +// Returns an error if not found and no default is provided +// +static int emc_ini_wideint(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + IniFile inifile(emc_inifile); + const char *varstr, *secstr; + + if (!test_ini_args(interp, inifile, objc, "emc_ini_wideint")) + return TCL_ERROR; + + varstr = Tcl_GetStringFromObj(objv[1], 0); + secstr = Tcl_GetStringFromObj(objv[2], 0); + + if (auto inival = inifile.findSInt(varstr, secstr)) { + Tcl_SetObjResult(interp, Tcl_NewWideIntObj(*inival)); + } else { + if (4 == objc) + Tcl_SetObjResult(interp, objv[3]); + else + return TCL_ERROR; + } + + return TCL_OK; +} + +// +// emc_ini_bool "VARIABLE" "SECTION" [] +// Return the boolean value of [SECTION]VARIABLE +// Returns an error if not found and no default is provided +// +static int emc_ini_bool(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + IniFile inifile(emc_inifile); + const char *varstr, *secstr; + + if (!test_ini_args(interp, inifile, objc, "emc_ini_bool")) + return TCL_ERROR; + + varstr = Tcl_GetStringFromObj(objv[1], 0); + secstr = Tcl_GetStringFromObj(objv[2], 0); + + if (auto inival = inifile.findBool(varstr, secstr)) { + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(*inival)); + } else { + if (4 == objc) + Tcl_SetObjResult(interp, objv[3]); + else + return TCL_ERROR; } - setresult(interp, inistring->c_str()); + return TCL_OK; +} +// +// emc_ini_sections +// Return a list of section names +// Returns an empty list if none found or the ini-file is invalid +// +static int emc_ini_sections(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + (void)objv; + if (objc > 1) { + setresult(interp, "emc_ini_sections: no arguments supported"); + return TCL_ERROR; + } + + Tcl_Obj *list = Tcl_NewListObj(32, NULL); + if (NULL == list) { + setresult(interp, "emc_ini_sections: failed to create list object"); + return TCL_ERROR; + } + + IniFile inifile(emc_inifile); + if (!inifile) { + // Simply return empty list on ini-file read error + Tcl_SetObjResult(interp, list); + return TCL_OK; + } + + // Append each section name to the list + for (auto const &sec : inifile.findSections()) { + Tcl_Obj *str = Tcl_NewStringObj(sec.c_str(), -1); + if (NULL == str) { + setresult(interp, "emc_ini_sections: failed to create string object"); + return TCL_ERROR; + } + Tcl_ListObjAppendElement(interp, list, str); + } + Tcl_SetObjResult(interp, list); + return TCL_OK; +} + +// +// emc_ini_variables "SECTION" +// Return a list of variables with value from named section +// Returns an empty list if section is not found or the ini-file is invalid +// +static int emc_ini_variables(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + if (2 != objc) { + setresult(interp, "emc_ini_variables: section argument missing or too many arguments"); + return TCL_ERROR; + } + + Tcl_Obj *list = Tcl_NewListObj(32, NULL); + if (NULL == list) { + setresult(interp, "emc_ini_variables: failed to create list object"); + return TCL_ERROR; + } + + IniFile inifile(emc_inifile); + if (!inifile) { + // Simply return empty list on ini-file read error + Tcl_SetObjResult(interp, list); + return TCL_OK; + } + + const char *section = Tcl_GetStringFromObj(objv[1], 0); + + // Append each variable and value to the list + for (auto const &var : inifile.findVariables(section)) { + Tcl_Obj *varval[2] = { + Tcl_NewStringObj(var.first.c_str(), -1), // name + Tcl_NewStringObj(var.second.c_str(), -1) // value + }; + if (NULL == varval[0] || NULL == varval[1]) { + setresult(interp, "emc_ini_variables: failed to create string object(s)"); + return TCL_ERROR; + } + Tcl_Obj *l = Tcl_NewListObj(2, varval); + if (NULL == l) { + setresult(interp, "emc_ini_variables: failed to create list content object"); + return TCL_ERROR; + } + Tcl_ListObjAppendElement(interp, list, l); + } + Tcl_SetObjResult(interp, list); + return TCL_OK; +} + +// +// emc_ini_filename +// Return the INI filename used +// +static int emc_ini_filename(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + (void)objv; + if (objc > 1) { + setresult(interp, "emc_ini_filename: no arguments supported"); + return TCL_ERROR; + } + + setresult(interp, emc_inifile); + return TCL_OK; +} + +// +// emc_ini_load "inifilename" +// Loads the specified INI-file and sets the internal static name 'emc_inifile' +// if the load was successful. +// Return true is the INI-file was read successfully or false otherwise. +// +// Note: This hack is only required to support the 'parse_ini' call from Tcl +// code with a different filename than the real ini-file. This happens when no +// 'emc_init -ini fname' is issued. This is the case for the twopass tests. +// +static int emc_ini_load(ClientData /*clientdata*/, + Tcl_Interp * interp, int objc, Tcl_Obj * CONST objv[]) +{ + if (2 != objc) { + setresult(interp, "emc_ini_load: needs exactly one filename argument"); + return TCL_ERROR; + } + + const char *fname = Tcl_GetStringFromObj(objv[1], 0); + if (!fname) { + setresult(interp, "emc_ini_load: failed to read filename argument"); + return TCL_ERROR; + } + + IniFile inifile(fname); + if (!inifile) { + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(0)); + } else { + // Successfully switched to a new ini-file + // We don't want to call 'emc_init' or iniLoad() because we do not want + // any NML connection to be established or overwritten from a previous + // call or any internals previously set to change. We only want to be + // able to query the new ini-file. + // Set the global filename + rtapi_strxcpy(emc_inifile, fname); + // Update Tcl's filename variable + Tcl_SetVar(interp, "EMC_INIFILE", emc_inifile, TCL_GLOBAL_ONLY); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(1)); + } return TCL_OK; } @@ -3435,6 +3712,22 @@ int Linuxcnc_Init(Tcl_Interp * interp) Tcl_CreateObjCommand(interp, "emc_ini", emc_ini, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_bool", emc_ini_bool, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_real", emc_ini_real, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_int", emc_ini_int, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_wideint", emc_ini_wideint, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_sections", emc_ini_sections, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_variables", emc_ini_variables, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_filename", emc_ini_filename, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); + Tcl_CreateObjCommand(interp, "emc_ini_load", emc_ini_load, (ClientData) NULL, + (Tcl_CmdDeleteProc *) NULL); Tcl_CreateObjCommand(interp, "emc_debug", emc_Debug, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL); diff --git a/src/emc/usr_intf/gscreen/gscreen.py b/src/emc/usr_intf/gscreen/gscreen.py index 74331f92a0e..a8d10ca6058 100755 --- a/src/emc/usr_intf/gscreen/gscreen.py +++ b/src/emc/usr_intf/gscreen/gscreen.py @@ -631,7 +631,8 @@ def __init__(self): units=self.inifile.find("AXIS_X","UNITS") if units==None: self.add_alarm_entry(_("No UNITS entry found in [TRAJ] or [AXIS_X] of INI file")) - if units=="mm" or units=="metric" or units == "1.0": + units = inifile.maplinearunits(units, fallback=1.0) + if 1.0 == units: self.machine_units_mm=1 conversion=[1.0/25.4]*3+[1]*3+[1.0/25.4]*3 else: @@ -731,77 +732,77 @@ def __init__(self): # set default jog rate # must convert from INI's units per second to gscreen's units per minute - temp = self.inifile.find("DISPLAY","DEFAULT_LINEAR_VELOCITY") + temp = self.inifile.getreal("DISPLAY","DEFAULT_LINEAR_VELOCITY") if temp: - temp = float(temp)*60 + temp = temp*60 else: temp = self.data.jog_rate self.add_alarm_entry(_("No DEFAULT_LINEAR_VELOCITY entry found in [DISPLAY] of INI file: using internal default of %s"%temp)) - self.data.jog_rate = float(temp) - self.emc.continuous_jog_velocity(float(temp),None) + self.data.jog_rate = temp + self.emc.continuous_jog_velocity(temp,None) # set max jog rate # must convert from INI's units per second to gscreen's units per minute - temp = self.inifile.find("DISPLAY","MAX_LINEAR_VELOCITY") + temp = self.inifile.getreal("DISPLAY","MAX_LINEAR_VELOCITY") if temp: - temp = float(temp)*60 + temp = temp*60 else: temp = self.data.jog_rate_max self.add_alarm_entry(_("No MAX_LINEAR_VELOCITY entry found in [DISPLAY] of INI file: using internal default of %s"%temp)) - self.data.jog_rate_max = float(temp) + self.data.jog_rate_max = temp # max velocity settings: more then one place to check # This is the maximum velocity of the machine - temp = self.inifile.find("TRAJ","MAX_LINEAR_VELOCITY") + temp = self.inifile.getreal("TRAJ","MAX_LINEAR_VELOCITY") if temp == None: self.add_alarm_entry(_("No MAX_LINEAR_VELOCITY found in [TRAJ] of the INI file")) temp = 1.0 - self.data._maxvelocity = float(temp) + self.data._maxvelocity = temp # look for angular defaults if there is angular axis if "a" in self.data.axis_list or "b" in self.data.axis_list or "c" in self.data.axis_list: # set default angular jog rate # must convert from INI's units per second to gscreen's units per minute - temp = self.inifile.find("DISPLAY","DEFAULT_ANGULAR_VELOCITY") + temp = self.inifile.getreal("DISPLAY","DEFAULT_ANGULAR_VELOCITY") if temp: - temp = float(temp)*60 + temp = temp*60 else: temp = self.data.angular_jog_rate self.add_alarm_entry(_("No DEFAULT_ANGULAR_VELOCITY entry found in [DISPLAY] of INI file: using internal default of %s"%temp)) - self.data.angular_jog_rate = float(temp) - self.emc.continuous_jog_velocity(None,float(temp)) + self.data.angular_jog_rate = temp + self.emc.continuous_jog_velocity(None,temp) # set default angular jog rate # must convert from INI's units per second to gscreen's units per minute - temp = self.inifile.find("DISPLAY","MAX_ANGULAR_VELOCITY") + temp = self.inifile.getreal("DISPLAY","MAX_ANGULAR_VELOCITY") if temp: - temp = float(temp)*60 + temp = temp*60 else: temp = self.data.angular_jog_rate_max self.add_alarm_entry(_("No MAX_ANGULAR_VELOCITY entry found in [DISPLAY] of INI file: using internal default of %s"%temp)) - self.data.angular_jog_rate_max = float(temp) + self.data.angular_jog_rate_max = temp # check for override settings - temp = self.inifile.find("DISPLAY","MAX_SPINDLE_OVERRIDE") + temp = self.inifile.getreal("DISPLAY","MAX_SPINDLE_OVERRIDE") if temp: - self.data.spindle_override_max = float(temp) + self.data.spindle_override_max = temp else: self.add_alarm_entry(_("No MAX_SPINDLE_OVERRIDE entry found in [DISPLAY] of INI file")) - temp = self.inifile.find("DISPLAY","MIN_SPINDLE_OVERRIDE") + temp = self.inifile.getreal("DISPLAY","MIN_SPINDLE_OVERRIDE") if temp: - self.data.spindle_override_min = float(temp) + self.data.spindle_override_min = temp else: self.add_alarm_entry(_("No MIN_SPINDLE_OVERRIDE entry found in [DISPLAY] of INI file")) - temp = self.inifile.find("DISPLAY","MAX_FEED_OVERRIDE") + temp = self.inifile.getreal("DISPLAY","MAX_FEED_OVERRIDE") if temp: - self.data.feed_override_max = float(temp) + self.data.feed_override_max = temp else: self.add_alarm_entry(_("No MAX_FEED_OVERRIDE entry found in [DISPLAY] of INI file")) # if it's a lathe config, set the tooleditor style - self.data.lathe_mode = bool(self.inifile.find("DISPLAY", "LATHE")) + self.data.lathe_mode = self.inifile.getbool("DISPLAY", "LATHE", fallback=False) if self.data.lathe_mode: self.add_alarm_entry(_("This screen will be orientated for Lathe options")) @@ -850,11 +851,11 @@ def __init__(self): self.halcomp.ready() # timers for display updates - temp = self.inifile.find("DISPLAY","CYCLE_TIME") + temp = self.inifile.getreal("DISPLAY","CYCLE_TIME") if not temp: self.add_alarm_entry(_("CYCLE_TIME in [DISPLAY] of INI file is missing: defaulting to 100ms")) temp = 100 - elif float(temp) < 50: + elif temp < 50: self.add_alarm_entry(_("CYCLE_TIME in [DISPLAY] of INI file is too small: defaulting to 100ms")) temp = 100 #print(_("timeout %d" % int(temp))) diff --git a/src/emc/usr_intf/shcom.cc b/src/emc/usr_intf/shcom.cc index d14bb10addc..cf32fdb743c 100644 --- a/src/emc/usr_intf/shcom.cc +++ b/src/emc/usr_intf/shcom.cc @@ -418,9 +418,6 @@ double convertAngularUnits(double u) return u; } -// polarities for joint jogging, from INI file -static int jogPol[EMCMOT_MAX_JOINTS]; - int sendDebug(int level) { EMC_SET_DEBUG debug_msg; @@ -1007,26 +1004,19 @@ int iniLoad(const char *filename) if (auto inistring = inifile.findString("NML_FILE", "EMC")) { // copy to global rtapi_strxcpy(emc_nmlfile, inistring->c_str()); - } else { - // not found, use default - } - - for (int t = 0; t < EMCMOT_MAX_JOINTS; t++) { - jogPol[t] = 1; // set to default - auto inival = inifile.findSInt("JOGGING_POLARITY", fmt::format("JOINT_{}", t)); - if (inival && *inival == 0) { - // it read as 0, so override default - jogPol[t] = 0; - } - } + } // else not found, use default or previously set if (auto inival = mapLinearUnits(inifile, "LINEAR_UNITS", "DISPLAY")) { linearUnitConversion = *inival; - } // else not found, leave default alone + } else { + linearUnitConversion = LINEAR_UNITS_AUTO; + } if (auto inival = mapAngularUnits(inifile, "ANGULAR_UNITS", "DISPLAY")) { angularUnitConversion = *inival; - } // else not found, leave default alone + } else { + angularUnitConversion = ANGULAR_UNITS_AUTO; + } return 0; } diff --git a/tcl/linuxcnc.tcl.in b/tcl/linuxcnc.tcl.in index de5c97e4910..10ab7d8e0ec 100644 --- a/tcl/linuxcnc.tcl.in +++ b/tcl/linuxcnc.tcl.in @@ -138,36 +138,48 @@ proc linuxcnc::standard_fixed_font {} { # # FIXME: # This parse_ini() code should be removed. It is inadequate for the new -# INI-file parser and requires preprocessing. Either it needs to be mapped to -# the new C++ parser or all the Tcl code depending on INI-file entries need to -# be ported to python. +# INI-file parser and has no sense of types. It is only here for compatibility +# with old code that expects global INI-file arrays in the Tcl namespace. This +# routine maps all section/variables to the sections, variables and values of +# the real INI-file using the new C++ parser. +# All Tcl code depending on global array INI-file entries needs to be ported to +# use the typed interface (using the emc_ini_* routines) or, better, the code +# should be rewritten in python. # +# Create associative arrays for all ini file sections like: ::EMC(VERSION), +# ::KINS(JOINTS), ... etc +# The data is extracted from the INI-file parser and iterates all sections +# and variables to export the sections as global namespace array variables. proc parse_ini {filename} { - # create associative arrays for all ini file sections - # like: ::EMC(VERSION), ::KINS(JOINTS), ... etc - # The INI-file for this routine has been pre-formatted by the linuxcnc - # startup script to handle includes, strings, comments and continuations. - # LINUXCNC_INI_EXPANDED is set by the startup script when the inifile's - # directory is not writable (e.g. installed sample configs), so the - # expanded file lives in a per-user cache dir instead of next to the INI. - if {[info exists ::env(LINUXCNC_INI_EXPANDED)] - && [file readable $::env(LINUXCNC_INI_EXPANDED)]} { - set f [open $::env(LINUXCNC_INI_EXPANDED)] - } elseif [catch {set f [open "${filename}.expanded"]} msg] { - # This is a fallback open. It happens in test twopass-personality. - # When this open is necessary, then a lot of errors are possible. - set f [open "${filename}"] + # You can have a mismatch between the INI-file names when this gets called + # without other parts of LinuxCNC being initialized. Notably the twopass* + # tests have this problem. + # The LinuxCNC internals default to a statically set name "emc.ini" and, + # f.ex., the haltcl program does not set the underlying filename before + # calling here. This creates a problem because the emc_ini_* calls are all + # dependent on the static filename. Thus a mismatch here will read the + # wrong INI-file. + if {$filename != [emc_ini_filename]} { + # This guards against INI-file mismatches + if {[info exists ::env(INI_FILE_NAME)]} { + # If there is an environment var, then we need to use that + if {$filename != $::env(INI_FILE_NAME)} { + error "parse_ini: fatal: requested filename (${filename}) and env(INI_FILE_NAME) (${::env(INI_FILE_NAME)}) are not the same." + } + # The requested name matches the env variable, load it + if {![emc_ini_load $::env(INI_FILE_NAME)]} { + error "parse_ini: fatal: cannot parse (${::env(INI_FILE_NAME)})." + } + } else { + error "parse_ini: fatal: requested filename (${filename}) and underlying filename ([emc_ini_filename]) are not the same." + } } - - while {[gets $f line] >= 0} { - set line [string trim $line] - if {[regexp {^\[(.*)\]\s*$} $line _ section]} { - # nothing - } elseif {[regexp {^([^#]+?)\s*=\s*(.*?)\s*$} $line _ k v]} { - upvar $section s - lappend s([string trim $k]) $v + # Take all sections + foreach section [emc_ini_sections] { + upvar $section s + # and add each variable in that section to the section array + foreach varval [emc_ini_variables $section] { + lappend s([lindex $varval 0]) [lindex $varval 1] } } - - close $f } diff --git a/tests/twopass-personality/test.sh b/tests/twopass-personality/test.sh index 4ddf67bed64..6a30687b368 100644 --- a/tests/twopass-personality/test.sh +++ b/tests/twopass-personality/test.sh @@ -1,2 +1,5 @@ #!/bin/sh -halrun tp.ini +INI_FILE_NAME=tp.ini +# Need the export for the parse_ini call to work in haltcl +export INI_FILE_NAME +halrun "$INI_FILE_NAME" diff --git a/tests/twopass/test.sh b/tests/twopass/test.sh index 4ddf67bed64..6a30687b368 100644 --- a/tests/twopass/test.sh +++ b/tests/twopass/test.sh @@ -1,2 +1,5 @@ #!/bin/sh -halrun tp.ini +INI_FILE_NAME=tp.ini +# Need the export for the parse_ini call to work in haltcl +export INI_FILE_NAME +halrun "$INI_FILE_NAME"