From 611733a8f4c07560456acc8d91516ea146b0821f Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Tue, 24 Sep 2024 10:39:09 -0700 Subject: [PATCH] Implement GCode M66 (Immediate mode only) - Update M modal group enum to reflect LinuxCNC - Add UserInputs and basic structure for M66 in GCode - Add numbered paramter 5399 - M66 input read - GCode value words need to be cleared, debug logs --- FluidNC/src/Error.cpp | 1 + FluidNC/src/Error.h | 1 + FluidNC/src/GCode.cpp | 154 +++++++++++++++++--- FluidNC/src/GCode.h | 39 ++++-- FluidNC/src/Machine/MachineConfig.cpp | 5 + FluidNC/src/Machine/MachineConfig.h | 30 ++-- FluidNC/src/Machine/UserInputs.cpp | 62 +++++++++ FluidNC/src/Machine/UserInputs.h | 39 ++++++ FluidNC/src/Machine/UserOutputs.cpp | 4 +- FluidNC/src/Machine/UserOutputs.h | 2 +- FluidNC/src/Main.cpp | 2 + FluidNC/src/Parameters.cpp | 122 ++++++++++------ FluidNC/src/Parameters.h | 5 + fixture_tests/fixtures/config.yaml | 9 ++ fixture_tests/fixtures/idle_status.nc | 1 + fixture_tests/fixtures/m66_basic.nc | 12 ++ fixture_tests/requirements.txt | 14 ++ fixture_tests/run_fixture | 193 ++++++++++++++++++++++---- 18 files changed, 577 insertions(+), 118 deletions(-) create mode 100644 FluidNC/src/Machine/UserInputs.cpp create mode 100644 FluidNC/src/Machine/UserInputs.h create mode 100644 fixture_tests/fixtures/config.yaml create mode 100644 fixture_tests/fixtures/m66_basic.nc diff --git a/FluidNC/src/Error.cpp b/FluidNC/src/Error.cpp index 9f5d19bfb..a8884c5ca 100644 --- a/FluidNC/src/Error.cpp +++ b/FluidNC/src/Error.cpp @@ -88,4 +88,5 @@ const std::map ErrorNames = { { Error::FlowControlOutOfMemory, "Flow Control Out of Memory" }, { Error::FlowControlStackOverflow, "Flow Control Stack Overflow" }, { Error::ParameterAssignmentFailed, "Parameter Assignment Failed" }, + { Error::GcodeValueWordInvalid, "Gcode invalid word value" }, }; diff --git a/FluidNC/src/Error.h b/FluidNC/src/Error.h index 153fd274d..7fee94fff 100644 --- a/FluidNC/src/Error.h +++ b/FluidNC/src/Error.h @@ -93,6 +93,7 @@ enum class Error : uint8_t { FlowControlOutOfMemory = 178, FlowControlStackOverflow = 179, ParameterAssignmentFailed = 180, + GcodeValueWordInvalid = 181, }; const char* errorString(Error errorNumber); diff --git a/FluidNC/src/GCode.cpp b/FluidNC/src/GCode.cpp index d027bbc3b..bfccb5730 100644 --- a/FluidNC/src/GCode.cpp +++ b/FluidNC/src/GCode.cpp @@ -13,6 +13,7 @@ #include "Protocol.h" // protocol_buffer_synchronize #include "MotionControl.h" // mc_override_ctrl_update #include "Machine/UserOutputs.h" // setAnalogPercent +#include "Machine/UserInputs.h" // read digital/analog inputs #include "Platform.h" // WEAK_LINK #include "Job.h" // Job::active() and Job::channel() @@ -143,6 +144,9 @@ static void gcode_comment_msg(char* comment) { } } +static std::optional validate_wait_on_input_mode_value(uint8_t); +static Error gc_wait_on_input(bool is_digital, uint8_t input_number, WaitOnInputMode mode, float timeout); + // Edit GCode line in-place, removing whitespace and comments and // converting to uppercase void collapseGCode(char* line) { @@ -252,16 +256,17 @@ Error gc_execute_line(char* line) { uint32_t command_words = 0; // Tracks G and M command words. Also used for modal group violations. uint32_t value_words = 0; // Tracks value words. - bool jogMotion = false; - bool checkMantissa = false; - bool clockwiseArc = false; - bool probeExplicit = false; - bool probeAway = false; - bool probeNoError = false; - bool syncLaser = false; - bool disableLaser = false; - bool laserIsMotion = false; - bool nonmodalG38 = false; // Used for G38.6-9 + bool jogMotion = false; + bool checkMantissa = false; + bool clockwiseArc = false; + bool probeExplicit = false; + bool probeAway = false; + bool probeNoError = false; + bool syncLaser = false; + bool disableLaser = false; + bool laserIsMotion = false; + bool nonmodalG38 = false; // Used for G38.6-9 + bool isWaitOnInputDigital = false; auto n_axis = config->_axes->_numberAxis; float coord_data[MAX_N_AXIS]; // Used by WCO-related commands @@ -682,27 +687,31 @@ Error gc_execute_line(char* line) { break; case 62: gc_block.modal.io_control = IoControl::DigitalOnSync; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; break; case 63: gc_block.modal.io_control = IoControl::DigitalOffSync; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; break; case 64: gc_block.modal.io_control = IoControl::DigitalOnImmediate; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; break; case 65: gc_block.modal.io_control = IoControl::DigitalOffImmediate; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; + break; + case 66: + gc_block.modal.io_control = IoControl::WaitOnInput; + mg_word_bit = ModalGroup::MM5; break; case 67: gc_block.modal.io_control = IoControl::SetAnalogSync; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; break; case 68: gc_block.modal.io_control = IoControl::SetAnalogImmediate; - mg_word_bit = ModalGroup::MM10; + mg_word_bit = ModalGroup::MM5; break; default: FAIL(Error::GcodeUnsupportedCommand); // [Unsupported M command] @@ -1002,6 +1011,51 @@ Error gc_execute_line(char* line) { clear_bitnum(value_words, GCodeWord::E); clear_bitnum(value_words, GCodeWord::Q); } + if ((gc_block.modal.io_control == IoControl::WaitOnInput)) { + // M66 P L Q + // M66 E L Q + // Exactly one of P or E must be present + if (bitnum_is_false(value_words, GCodeWord::P) && bitnum_is_false(value_words, GCodeWord::E)) { + // need at least one of P or E + FAIL(Error::GcodeValueWordMissing); + } + if (bitnum_is_true(value_words, GCodeWord::P) && bitnum_is_true(value_words, GCodeWord::E)) { + // need at most one of P or E + FAIL(Error::GcodeValueWordInvalid); + } + isWaitOnInputDigital = bitnum_is_true(value_words, GCodeWord::P); + clear_bitnum(value_words, GCodeWord::P); + clear_bitnum(value_words, GCodeWord::E); + if (bitnum_is_false(value_words, GCodeWord::L)) { + FAIL(Error::GcodeValueWordMissing); + } + clear_bitnum(value_words, GCodeWord::L); + auto const wait_mode = validate_wait_on_input_mode_value(gc_block.values.l); + if (!wait_mode) { + FAIL(Error::GcodeValueWordInvalid); + } + // Only Immediate mode is valid for analog input + if (!isWaitOnInputDigital && wait_mode != WaitOnInputMode::Immediate) { + FAIL(Error::GcodeValueWordInvalid); + } + // Q is the timeout in seconds (conditionally optional) + // - Ignored if L is 0 (Immediate). + // - Error if value 0 seconds, and L is not 0 (Immediate). + if (bitnum_is_true(value_words, GCodeWord::Q)) { + if (gc_block.values.q != 0.0) { + if (wait_mode != WaitOnInputMode::Immediate) { + // Non-immediate waits must have a non-zero timeout + FAIL(Error::GcodeValueWordInvalid); + } + } + } else { + if (wait_mode != WaitOnInputMode::Immediate) { + // Non-immediate waits must have a timeout + FAIL(Error::GcodeValueWordMissing); + } + } + clear_bitnum(value_words, GCodeWord::Q); + } if (gc_block.modal.set_tool_number == SetToolNumber::Enable) { if (bitnum_is_false(value_words, GCodeWord::Q)) { FAIL(Error::GcodeValueWordMissing); @@ -1656,6 +1710,29 @@ Error gc_execute_line(char* line) { FAIL(Error::PParamMaxExceeded); } } + if (gc_block.modal.io_control == IoControl::WaitOnInput) { + auto const validate_input_number = [&](const float input_number) -> std::optional { + if (input_number < 0) { + return std::nullopt; + } + if (isWaitOnInputDigital) { + if (input_number > MaxUserDigitalPin) { + return std::nullopt; + } else if (input_number > MaxUserAnalogPin) { + return std::nullopt; + } + } + return (uint8_t)input_number; + }; + auto const maybe_input_number = validate_input_number(isWaitOnInputDigital ? gc_block.values.p : gc_block.values.e); + if (!maybe_input_number.has_value()) { + FAIL(Error::PParamMaxExceeded); + } + auto const input_number = *maybe_input_number; + auto const wait_mode = *validate_wait_on_input_mode_value(gc_block.values.l); + auto const timeout = gc_block.values.q; + gc_wait_on_input(isWaitOnInputDigital, input_number, wait_mode, timeout); + } // [9. Override control ]: NOT SUPPORTED. Always enabled, except for parking control. if (config->_enableParkingOverrideControl) { @@ -1917,3 +1994,48 @@ void gc_exec_linef(bool sync_after, Channel& out, const char* format, ...) { group 10 = {G98, G99} return mode canned cycles group 13 = {G61.1, G64} path control mode (G61 is supported) */ + +static std::optional validate_wait_on_input_mode_value(uint8_t value) { + switch (value) { + case 0: + return WaitOnInputMode::Immediate; + case 1: + return WaitOnInputMode::Rise; + case 2: + return WaitOnInputMode::Fall; + case 3: + return WaitOnInputMode::High; + case 4: + return WaitOnInputMode::Low; + default: + return std::nullopt; + } +} + +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + +static Error gc_wait_on_input(bool is_digital, uint8_t input_number, WaitOnInputMode mode, float timeout) { + // TODO - only Immediate read mode is supported + if (mode == WaitOnInputMode::Immediate) { + auto const result = is_digital ? config->_userInputs->readDigitalInput(input_number) : + config->_userInputs->readAnalogInput(input_number); + auto const on_ok = [&](bool result) { + log_debug("M66: " << (is_digital ? "digital" : "analog") << "_input" << input_number << " result=" << result); + set_numbered_param(5399, result ? 1.0 : 0.0); + return Error::Ok; + }; + auto const on_error = [&](Error error) { + log_error("M66: " << (is_digital ? "digital" : "analog") << "_input" << input_number << " failed"); + return error; + }; + return std::visit(overloaded { on_ok, on_error }, result); + } + + // TODO - implement rest of modes + return Error::GcodeValueWordInvalid; +} diff --git a/FluidNC/src/GCode.h b/FluidNC/src/GCode.h index ddc3e6757..6990887f7 100644 --- a/FluidNC/src/GCode.h +++ b/FluidNC/src/GCode.h @@ -10,6 +10,7 @@ #include "SpindleDatatypes.h" #include +#include typedef uint16_t gcodenum_t; @@ -25,7 +26,9 @@ enum class Override : uint8_t { // and are similar/identical to other g-code interpreters by manufacturers (Haas,Fanuc,Mazak,etc). // NOTE: Modal group values must be sequential and starting from zero. +// http://linuxcnc.org/docs/html/gcode/overview.html#gcode:modal-groups enum class ModalGroup : uint8_t { + // Table 5. G-code Modal Groups MG0 = 0, // [G4,G10,G28,G28.1,G30,G30.1,G53,G92,G92.1] Non-modal MG1 = 1, // [G0,G1,G2,G3,G38.2,G38.3,G38.4,G38.5,G80] Motion MG2 = 2, // [G17,G18,G19] Plane selection @@ -37,12 +40,14 @@ enum class ModalGroup : uint8_t { MG8 = 8, // [G43.1,G49] Tool length offset MG12 = 9, // [G54,G55,G56,G57,G58,G59] Coordinate system selection MG13 = 10, // [G61] Control mode + // Table 6. M-code Modal Groups MM4 = 11, // [M0,M1,M2,M30] Stopping - MM6 = 14, // [M6] [M61] Tool change - MM7 = 12, // [M3,M4,M5] Spindle turning - MM8 = 13, // [M7,M8,M9] Coolant control - MM9 = 14, // [M56] Override control - MM10 = 15, // [M62, M63, M64, M65, M67, M68] User Defined http://linuxcnc.org/docs/html/gcode/overview.html#_modal_groups + MM5 = 12, // [M62,M63,M64,M65,M66,M67,M68] Digital/analog output/input + MM6 = 13, // [M6] [M61] Tool change + MM7 = 14, // [M3,M4,M5] Spindle turning + MM8 = 15, // [M7,M8,M9] Coolant control + MM9 = 16, // [M56] Override control + MM10 = 17, // [M100-M199] User Defined }; // Command actions for within execution-type modal groups (motion, stopping, non-modal). Used @@ -151,15 +156,25 @@ struct CoolantState { // Modal Group M8: Coolant control // Modal Group M9: Override control -// Modal Group M10: User I/O control +// Modal Group M5: User I/O control enum class IoControl : gcodenum_t { None = 0, DigitalOnSync = 1, // M62 DigitalOffSync = 2, // M63 DigitalOnImmediate = 3, // M64 DigitalOffImmediate = 4, // M65 - SetAnalogSync = 5, // M67 - SetAnalogImmediate = 6, // M68 + WaitOnInput = 5, // M66 + SetAnalogSync = 6, // M67 + SetAnalogImmediate = 7, // M68 +}; + +// {M66} L word value, indicates wait mode +enum class WaitOnInputMode : int8_t { + Immediate, + Rise, + Fall, + High, + Low, }; static const int MaxUserDigitalPin = 8; @@ -266,14 +281,14 @@ struct gc_modal_t { }; struct gc_values_t { - uint8_t e; // M67 + uint8_t e; // {M66,M67} float f; // Feed float ijk[3]; // I,J,K Axis arc offsets - only 3 are possible - uint8_t l; // G10 or canned cycles parameters + uint8_t l; // {M66,G10}, or canned cycles parameters int32_t n; // Line number uint32_t o; // Subroutine identifier - single-meaning word (not used by the core) - float p; // G10 or dwell parameters - float q; // M67 + float p; // {M66,G10}, or dwell parameters + float q; // {M66,M67} float r; // Arc radius float s; // Spindle speed uint32_t t; // Tool selection diff --git a/FluidNC/src/Machine/MachineConfig.cpp b/FluidNC/src/Machine/MachineConfig.cpp index 87cae1447..48d0ae033 100644 --- a/FluidNC/src/Machine/MachineConfig.cpp +++ b/FluidNC/src/Machine/MachineConfig.cpp @@ -67,6 +67,7 @@ namespace Machine { handler.section("parking", _parking); handler.section("user_outputs", _userOutputs); + handler.section("user_inputs", _userInputs); ConfigurableModuleFactory::factory(handler); ATCs::ATCFactory::factory(handler); @@ -104,6 +105,10 @@ namespace Machine { _userOutputs = new UserOutputs(); } + if (_userInputs == nullptr) { + _userInputs = new UserInputs(); + } + if (_sdCard == nullptr) { _sdCard = new SDCard(); } diff --git a/FluidNC/src/Machine/MachineConfig.h b/FluidNC/src/Machine/MachineConfig.h index 3358088d1..278a9150e 100644 --- a/FluidNC/src/Machine/MachineConfig.h +++ b/FluidNC/src/Machine/MachineConfig.h @@ -25,6 +25,7 @@ #include "I2CBus.h" #include "I2SOBus.h" #include "UserOutputs.h" +#include "UserInputs.h" #include "Macros.h" #include @@ -59,20 +60,21 @@ namespace Machine { public: MachineConfig() = default; - Axes* _axes = nullptr; - Kinematics* _kinematics = nullptr; - SPIBus* _spi = nullptr; - I2CBus* _i2c[MAX_N_I2C] = { nullptr }; - I2SOBus* _i2so = nullptr; - Stepping* _stepping = nullptr; - CoolantControl* _coolant = nullptr; - Probe* _probe = nullptr; - Control* _control = nullptr; - UserOutputs* _userOutputs = nullptr; - SDCard* _sdCard = nullptr; - Macros* _macros = nullptr; - Start* _start = nullptr; - Parking* _parking = nullptr; + Axes* _axes = nullptr; + Kinematics* _kinematics = nullptr; + SPIBus* _spi = nullptr; + I2CBus* _i2c[MAX_N_I2C] = { nullptr }; + I2SOBus* _i2so = nullptr; + Stepping* _stepping = nullptr; + CoolantControl* _coolant = nullptr; + Probe* _probe = nullptr; + Control* _control = nullptr; + UserOutputs* _userOutputs = nullptr; + UserInputs* _userInputs = nullptr; + SDCard* _sdCard = nullptr; + Macros* _macros = nullptr; + Start* _start = nullptr; + Parking* _parking = nullptr; UartChannel* _uart_channels[MAX_N_UARTS] = { nullptr }; Uart* _uarts[MAX_N_UARTS] = { nullptr }; diff --git a/FluidNC/src/Machine/UserInputs.cpp b/FluidNC/src/Machine/UserInputs.cpp new file mode 100644 index 000000000..f7bd966b3 --- /dev/null +++ b/FluidNC/src/Machine/UserInputs.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024 - Dylan Knutson +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#include "UserInputs.h" + +namespace Machine { + UserInputs::UserInputs() {} + UserInputs::~UserInputs() {} + + void UserInputs::group(Configuration::HandlerBase& handler) { + char item_name[64]; + for (size_t i = 0; i < MaxUserAnalogPin; i++) { + snprintf(item_name, sizeof(item_name), "analog%d_pin", i); + handler.item(item_name, _analogInput[i].pin); + _analogInput[i].name = item_name; + } + for (size_t i = 0; i < MaxUserDigitalPin; i++) { + snprintf(item_name, sizeof(item_name), "digital%d_pin", i); + handler.item(item_name, _digitalInput[i].pin); + _digitalInput[i].name = item_name; + } + } + + void UserInputs::init() { + for (auto& input : _analogInput) { + if (input.pin.defined()) { + input.pin.setAttr(Pin::Attr::Input); + log_info("User Analog Input: " << input.name << " on Pin " << input.pin.name()); + } + } + for (auto& input : _digitalInput) { + if (input.pin.defined()) { + input.pin.setAttr(Pin::Attr::Input); + log_info("User Digital Input: " << input.name << " on Pin " << input.pin.name()); + } + } + } + + UserInputs::ReadInputResult UserInputs::readDigitalInput(uint8_t input_number) { + if (input_number >= MaxUserDigitalPin) { + return Error::PParamMaxExceeded; + } + auto& input = _digitalInput[input_number]; + if (!input.pin.defined()) { + return Error::InvalidValue; + } + return input.pin.read(); + } + + UserInputs::ReadInputResult UserInputs::readAnalogInput(uint8_t input_number) { + // TODO - analog pins are read the same as digital. + if (input_number >= MaxUserAnalogPin) { + return Error::PParamMaxExceeded; + } + auto& input = _analogInput[input_number]; + if (!input.pin.defined()) { + return Error::InvalidValue; + } + return input.pin.read(); + } + +} // namespace Machine diff --git a/FluidNC/src/Machine/UserInputs.h b/FluidNC/src/Machine/UserInputs.h new file mode 100644 index 000000000..23921efd6 --- /dev/null +++ b/FluidNC/src/Machine/UserInputs.h @@ -0,0 +1,39 @@ +// Copyright (c) 2024 - Dylan Knutson +// Use of this source code is governed by a GPLv3 license that can be found in the LICENSE file. + +#pragma once + +#include "../Configuration/Configurable.h" +#include "../GCode.h" + +#include +#include + +namespace Machine { + + class UserInputs : public Configuration::Configurable { + struct PinAndName { + std::string name; + Pin pin; + }; + + std::array _digitalInput; + + // TODO - analog pins are read the same as digital. The Pin + // API should either be extended to support analog reads, or + // a new AnalogPin class should be created. + std::array _analogInput; + + public: + UserInputs(); + virtual ~UserInputs(); + + void init(); + void group(Configuration::HandlerBase& handler) override; + + using ReadInputResult = std::variant; + ReadInputResult readDigitalInput(uint8_t input_number); + ReadInputResult readAnalogInput(uint8_t input_number); + }; + +} // namespace Machine diff --git a/FluidNC/src/Machine/UserOutputs.cpp b/FluidNC/src/Machine/UserOutputs.cpp index 6aebe2d38..2d5b51efe 100644 --- a/FluidNC/src/Machine/UserOutputs.cpp +++ b/FluidNC/src/Machine/UserOutputs.cpp @@ -20,7 +20,7 @@ namespace Machine { if (pin.defined()) { pin.setAttr(Pin::Attr::Output); pin.off(); - log_info("User Digital Output:" << i << " on Pin:" << pin.name()); + log_info("User Digital Output: " << i << " on Pin:" << pin.name()); } } // determine the highest resolution (number of precision bits) allowed by frequency @@ -32,7 +32,7 @@ namespace Machine { if (pin.defined()) { _pwm[i] = new PwmPin(pin, _analogFrequency[i]); _pwm[i]->setDuty(0); - log_info("User Analog Output " << i << " on Pin:" << pin.name() << " Freq:" << _pwm[i]->frequency() << "Hz"); + log_info("User Analog Output: " << i << " on Pin:" << pin.name() << " Freq:" << _pwm[i]->frequency() << "Hz"); } } } diff --git a/FluidNC/src/Machine/UserOutputs.h b/FluidNC/src/Machine/UserOutputs.h index 9aa5878b4..1f63b23ae 100644 --- a/FluidNC/src/Machine/UserOutputs.h +++ b/FluidNC/src/Machine/UserOutputs.h @@ -27,6 +27,6 @@ namespace Machine { bool setDigital(size_t io_num, bool isOn); bool setAnalogPercent(size_t io_num, float percent); - ~UserOutputs(); + virtual ~UserOutputs(); }; } diff --git a/FluidNC/src/Main.cpp b/FluidNC/src/Main.cpp index 2c625cb19..3a356840a 100644 --- a/FluidNC/src/Main.cpp +++ b/FluidNC/src/Main.cpp @@ -96,6 +96,8 @@ void setup() { config->_userOutputs->init(); + config->_userInputs->init(); + config->_axes->init(); config->_control->init(); diff --git a/FluidNC/src/Parameters.cpp b/FluidNC/src/Parameters.cpp index bbd0ef498..3a7e7981a 100644 --- a/FluidNC/src/Parameters.cpp +++ b/FluidNC/src/Parameters.cpp @@ -17,14 +17,43 @@ #include "Expression.h" +// See documentation "4.1. Numbered Parameters" for list of numbered parameters +// that LinuxCNC supports. +// http://wiki.fluidnc.com/en/features/gcode_parameters_expressions +// https://linuxcnc.org/docs/stable/html/gcode/overview.html#sub:numbered-parameters + // clang-format off const std::map bool_params = { { 5070, &probe_succeeded }, - // { 5399, &m66okay }, }; -typedef int ngc_param_id_t; -std::map user_params = {}; +std::map float_params = { + { 5399, 0.0 }, // M66 last immediate read input result +}; + +static bool can_write_float_param(ngc_param_id_t id) { + if (id == 5399) { + // M66 last immediate read input result + return true; + } + if(id >= 1 && id <= 5000) { + // User parameters + return true; + } + return false; +} + +static bool can_read_float_param(ngc_param_id_t id) { + if (id == 5399) { + // M66 + return true; + } + if (id >= 31 && id <= 5000) { + // User parameters + return true; + } + return false; +} const std::map axis_params = { { 5161, CoordIndex::G28 }, @@ -42,7 +71,6 @@ const std::map axis_params = { // { 5401, CoordIndex::TLO }, }; - const std::map work_positions = { { "_x", 0 }, { "_y", 1 }, @@ -101,40 +129,6 @@ static float to_mm(int axis, float value) { } return value; } -bool set_numbered_param(ngc_param_id_t id, float value) { - int axis; - for (auto const& [key, coord_index] : axis_params) { - axis = id - key; - if (is_axis(axis)) { - coords[coord_index]->set(axis, to_mm(axis, value)); - gc_ngc_changed(coord_index); - return true; - } - } - // Non-volatile G92 - axis = id - 5211; - if (is_axis(axis)) { - gc_state.coord_offset[axis] = to_mm(axis, value); - gc_ngc_changed(CoordIndex::G92); - return true; - } - if (id == 5220) { - gc_state.modal.coord_select = static_cast(value); - return true; - } - if (id == 5400) { - gc_state.selected_tool = static_cast(value); - return true; - } - if (id >= 1 && id <= 5000) { - // 1-30 are for subroutine arguments, but since we don't - // implement subroutines, we treat them the same as user params - user_params[id] = value; - return true; - } - log_info("N " << id << " is not found"); - return false; -} bool get_numbered_param(ngc_param_id_t id, float& result) { int axis; @@ -181,20 +175,25 @@ bool get_numbered_param(ngc_param_id_t id, float& result) { return true; } - for (const auto& [key, valuep] : bool_params) { - if (key == id) { - result = *valuep; + if (auto param = bool_params.find(id); param != bool_params.end()) { + result = *param->second ? 1.0 : 0.0; + return true; + } + + if (can_read_float_param(id)) { + if (auto param = float_params.find(id); param != float_params.end()) { + result = param->second; return true; + } else { + log_info("param #" << id << " is not found"); + return false; } } - if (id >= 31 && id <= 5000) { - result = user_params[id]; - return true; - } return false; } +// TODO - make this a variant? struct param_ref_t { std::string name; // If non-empty, the parameter is named ngc_param_id_t id; // Valid if name is empty @@ -486,6 +485,39 @@ bool set_named_param(const std::string& name, float value) { return true; } +bool set_numbered_param(ngc_param_id_t id, float value) { + int axis; + for (auto const& [key, coord_index] : axis_params) { + axis = id - key; + if (is_axis(axis)) { + coords[coord_index]->set(axis, to_mm(axis, value)); + gc_ngc_changed(coord_index); + return true; + } + } + // Non-volatile G92 + axis = id - 5211; + if (is_axis(axis)) { + gc_state.coord_offset[axis] = to_mm(axis, value); + gc_ngc_changed(CoordIndex::G92); + return true; + } + if (id == 5220) { + gc_state.modal.coord_select = static_cast(value); + return true; + } + if (id == 5400) { + gc_state.selected_tool = static_cast(value); + return true; + } + if (can_write_float_param(id)) { + float_params[id] = value; + return true; + } + log_info("param #" << id << " is not found"); + return false; +} + bool set_param(const param_ref_t& param_ref, float value) { if (param_ref.name.length()) { // Named parameter auto name = param_ref.name; diff --git a/FluidNC/src/Parameters.h b/FluidNC/src/Parameters.h index b9916d09c..b1996a2ca 100644 --- a/FluidNC/src/Parameters.h +++ b/FluidNC/src/Parameters.h @@ -6,8 +6,13 @@ #include #include +// TODO - make ngc_param_id_t an enum, give names to numbered parameters where +// possible +typedef int ngc_param_id_t; + bool assign_param(const char* line, size_t& pos); bool read_number(const char* line, size_t& pos, float& value, bool in_expression = false); bool perform_assignments(); bool named_param_exists(std::string& name); bool set_named_param(const char* name, float value); +bool set_numbered_param(ngc_param_id_t, float value); diff --git a/fixture_tests/fixtures/config.yaml b/fixture_tests/fixtures/config.yaml new file mode 100644 index 000000000..016512df8 --- /dev/null +++ b/fixture_tests/fixtures/config.yaml @@ -0,0 +1,9 @@ +board: 6 Pack +name: Fixture Tests +kinematics: + Cartesian: +user_outputs: + digital0_pin: gpio.4 +user_inputs: + digital0_pin: gpio.5 +planner_blocks: 16 diff --git a/fixture_tests/fixtures/idle_status.nc b/fixture_tests/fixtures/idle_status.nc index d651bfefa..2d8021400 100644 --- a/fixture_tests/fixtures/idle_status.nc +++ b/fixture_tests/fixtures/idle_status.nc @@ -3,4 +3,5 @@ <- ok -> ?? <| +<| <| diff --git a/fixture_tests/fixtures/m66_basic.nc b/fixture_tests/fixtures/m66_basic.nc new file mode 100644 index 000000000..0b073f2a8 --- /dev/null +++ b/fixture_tests/fixtures/m66_basic.nc @@ -0,0 +1,12 @@ +=> ./config.yaml /littlefs/config.yaml +# restart the machine to clear all variables +-> $Bye +<... * Grbl 3.8* +# parameter should be default initialized to 0 +-> (print,#5399) +<- [MSG:INFO: PRINT,0.000000] +-> M66 P0 L0 +<- ok +-> (print,#5399) +<- ok +<- [MSG:INFO: PRINT,0.000000] diff --git a/fixture_tests/requirements.txt b/fixture_tests/requirements.txt index c08c3c561..c77adb9ae 100644 --- a/fixture_tests/requirements.txt +++ b/fixture_tests/requirements.txt @@ -1,2 +1,16 @@ +attrs==22.2.0 +cffi==1.15.1 +cryptography==38.0.4 +distlib==0.3.6 +filelock==3.12.2 +jsonrpcserver==5.0.9 +jsonschema==4.17.3 +OSlash==0.6.3 +platformdirs==3.8.0 +pycparser==2.21 +pyrsistent==0.19.3 pyserial==3.5 termcolor==2.4.0 +typing_extensions==4.5.0 +virtualenv==20.23.1 +xmodem==0.4.7 diff --git a/fixture_tests/run_fixture b/fixture_tests/run_fixture index 3cda67701..7bffc8757 100755 --- a/fixture_tests/run_fixture +++ b/fixture_tests/run_fixture @@ -5,6 +5,9 @@ from termcolor import colored import argparse import os import serial +from xmodem import XMODEM +import re +import fnmatch parser = argparse.ArgumentParser() parser.add_argument("device") @@ -13,12 +16,16 @@ parser.add_argument("-b", "--baudrate", type=int, default=115200) args = parser.parse_args() OPS = [ - # send to controller + # send command to controller "->", + # send file to controller + "=>", # expect from controller "<-", # expect from controller, but optional "<~", + # consume lines until line is found + "<...", # expect one of "<|", ] @@ -34,15 +41,28 @@ else: fixture_files.append(args.fixture_file) +class OpEntry: + def __init__(self, op, data, lineno): + self.op = op + self.data = data + self.lineno = lineno + self.glob_match = False + + def __str__(self): + return f"OpEntry({self.op}, {str(self.data)}, {self.lineno})" + + def __repr__(self): + return str(self) + + def parse_fixture_lines(fixture_file): - # fixture_lines is a list of tuples: + # op_entries is a list of tuples: # (op, match, lineno) # Read the fixture file with open(fixture_file, "r") as f: - fixture_lines = [] - fixture_file = f.read() - for lineno, line in enumerate(fixture_file.splitlines()): + op_entries = [] + for lineno, line in enumerate(f.read().splitlines()): if line.startswith("#"): # skip comment lines continue @@ -50,47 +70,87 @@ def parse_fixture_lines(fixture_file): for op in OPS: if line.startswith(op + " "): line = line[len(op) + 1 :] + if line.startswith("* "): + line = line[2:] + glob_match = True + else: + glob_match = False + if op == "<|": - if len(fixture_lines) > 0 and fixture_lines[-1][0] == "<|": + if len(op_entries) > 0 and op_entries[-1].op == "<|": # append to previous group of matches - fixture_lines[-1][1].append(line) + op_entries[-1].data.append(line) else: # new group of matches - fixture_lines.append((op, [line], lineno + 1)) + op_entry = OpEntry(op, [line], lineno + 1) + op_entries.append(op_entry) + elif op == "=>": + # make the local path relative to the fixture file + line = line.split(" ") + local_file = line[0] + remote_file = line[1] + local_file = os.path.join( + os.path.dirname(fixture_file), local_file + ) + if not os.path.exists(local_file): + raise ValueError( + f"Fixture {fixture_file} references file that does not exist: {local_file}" + ) + if not remote_file.startswith("/"): + raise ValueError( + f"Remote file path must be absolute: {remote_file}" + ) + op_entries.append( + OpEntry(op, (local_file, remote_file), lineno + 1) + ) + + # expect a message that the file was received + op_entries.append( + OpEntry("<-", "[MSG:Files changed]", lineno + 1) + ) + else: - fixture_lines.append((op, line, lineno + 1)) + op_entry = OpEntry(op, line, lineno + 1) + op_entry.glob_match = glob_match + op_entries.append(op_entry) break else: raise ValueError( f"Invalid line {lineno} in fixture file {fixture_file}: {line}" ) - return fixture_lines + + return op_entries def run_fixture(fixture_file): fixture_lines = parse_fixture_lines(fixture_file) controller = serial.Serial(args.device, args.baudrate, timeout=1) - try: - # last line read from the controller - line = None - for op, fixture_line, lineno in fixture_lines: + # last line read from the controller + line = None + + def ensure_line(): + nonlocal line + if line is None: + line = controller.readline().decode("utf-8").strip() + + try: + for op_entry in fixture_lines: + op = op_entry.op + op_data = op_entry.data + lineno = op_entry.lineno if op == "->": # send the fixture line to the controller print( colored(f"{op} ", "dark_grey") - + colored(fixture_line, "green", attrs=["dark"]) + + colored(op_data, "green", attrs=["dark"]) ) - controller.write(fixture_line.encode("utf-8") + b"\n") + controller.write(op_data.encode("utf-8") + b"\n") elif op == "<-" or op == "<~" or op == "<|": is_optional = op == "<~" - - # read a line, and wait for the expected response - if line is None: - line = controller.readline().decode("utf-8").strip() - + ensure_line() if op == "<|": # match any one of - if line in fixture_line: + if line in op_data: print( colored(f"{op} ", "dark_grey") + colored(line, "green", attrs=["dark", "bold"]) @@ -99,11 +159,11 @@ def run_fixture(fixture_file): else: print(f"Test failed at line {colored(str(lineno), 'red')}") print(f"Expected one of:") - for fline in fixture_line: + for fline in op_data: print(f" `{colored(fline, 'red')}'") print(f"Actual: `{colored(line, 'red')}'") exit(1) - elif line == fixture_line: # exact match + elif line == op_data: # exact match print( colored(f"{op} ", "dark_grey") + colored(line, "green", attrs=["dark", "bold"]) @@ -113,15 +173,91 @@ def run_fixture(fixture_file): if is_optional: # but that's okay if it's an optional line print( colored(f"{op} Did not get optional line ", "dark_grey") - + colored(fixture_line, "dark_grey", attrs=["bold"]) + + colored(op_data, "dark_grey", attrs=["bold"]) ) # do not clear line, so we can try to match it again on # the next op else: print(f"Test failed at line {colored(str(lineno), 'red')}") - print(f"Expected: `{colored(fixture_line, 'red')}'") + print(f"Expected: `{colored(op_data, 'red')}'") print(f"Actual: `{colored(line, 'red')}'") exit(1) + elif op == "=>": + local_file, remote_file = op_data + with open(local_file, "rb") as file_stream: + + def getc(size, timeout=1): + return controller.read(size) or None + + def putc(data, timeout=1): + return controller.write(data) or None + + print(f"Sending {local_file} to {remote_file}") + controller.write(f"$XModem/Receive={remote_file}\n".encode("utf-8")) + while True: + # wait for the 'C' character to start the transfer + controller.timeout = 2 + c = controller.read(1) + if c == b"C": + break + if c == b"": + raise TimeoutError( + f"XModem start timeout at line {lineno} in fixture file {fixture_file}" + ) + controller.timeout = 1 + xmodem = XMODEM(getc, putc) + xmodem.send(file_stream) + rx_ack_line = controller.readline().decode("utf-8").strip() + print( + colored(f"{op} ", "dark_grey") + + colored(rx_ack_line, "green", attrs=["dark", "bold"]) + ) + matcher = re.match( + r"\[MSG:INFO: Received (\d+) bytes to file ([\w\/\.]+)\]", + rx_ack_line, + ) + if matcher is None: + raise ValueError( + f"Transfer failed (ack line): {rx_ack_line} at line {lineno} in fixture file {fixture_file}" + ) + num_tx_bytes = int(matcher.group(1)) + name_tx_file = matcher.group(2) + if name_tx_file != remote_file: + print(f"Expected: {remote_file}") + print(f"Actual: {name_tx_file}") + raise ValueError( + f"Transfer failed (filename mismatch): {rx_ack_line} at line {lineno} in fixture file {fixture_file}" + ) + print( + colored(f"{op} ", "dark_grey") + + colored(local_file, "green", attrs=["bold"]) + + colored(" => ", "dark_grey") + + colored(remote_file, "green", attrs=["bold"]) + + colored(f" ({num_tx_bytes} bytes)", "green") + ) + elif op == "<...": + while True: + ensure_line() + print( + colored( + f"{op} " + ("(*) " if op_entry.glob_match else ""), + "dark_grey", + ) + + colored(line, "green", attrs=["dark", "bold"]) + ) + + matched = False + if op_entry.glob_match: + matched = fnmatch.fnmatch(line, op_data) + else: + matched = line == op_data + line = None + + if matched: + break + + else: + raise ValueError(f"Invalid operation {op}") except KeyboardInterrupt: print("Interrupt") @@ -131,10 +267,11 @@ def run_fixture(fixture_file): controller.close() print( - colored(f"Fixture ", "green") + colored(f"--- Fixture ", "green") + colored(fixture_file, "green", attrs=["bold"]) - + colored(" passed", "green") + + colored(" passed ---", "green") ) + print() for fixture_file in fixture_files: