From 30ccb208468f4b947846ad730c55e04f1a49c748 Mon Sep 17 00:00:00 2001 From: Adrian Scripca Date: Fri, 28 Apr 2023 14:50:58 +0300 Subject: [PATCH] Lots of work on status representation --- CMakeLists.txt | 2 +- grbl_communication.cpp | 23 +- grbl_communication.h | 2 +- grbl_machine.cpp | 178 +++++++++++++- grbl_test.cpp | 24 +- machine.h | 81 +++++-- main.cpp | 534 +++++++++-------------------------------- string_utils.h | 43 ++++ 8 files changed, 430 insertions(+), 457 deletions(-) create mode 100644 string_utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bb235d..a05354f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,5 +23,5 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_VISIBILITY_PRESET hidden) -add_executable(sender main.cpp grbl.h grbl.cpp grbl_test.cpp grbl_communication.h grbl_communication.cpp machine.h grbl_machine.cpp) +add_executable(sender main.cpp grbl.h grbl.cpp grbl_test.cpp grbl_communication.h grbl_communication.cpp machine.h grbl_machine.cpp string_utils.h) target_link_libraries(sender nanogui gtest gtest_main) \ No newline at end of file diff --git a/grbl_communication.cpp b/grbl_communication.cpp index 7687fd0..44a36e3 100644 --- a/grbl_communication.cpp +++ b/grbl_communication.cpp @@ -74,6 +74,8 @@ void grbl::tcp_transport::worker() { std::string received; uint8_t buffer[200]; + auto last_report = std::chrono::high_resolution_clock::now(); + while (!should_quit) { if (is_connected) { // anything to write? @@ -103,12 +105,12 @@ void grbl::tcp_transport::worker() { if (is_eol) { if (!is_empty_line(received)) { // starts with? https://stackoverflow.com/questions/1878001/how-do-i-check-if-a-c-stdstring-starts-with-a-certain-string-and-convert-a - if (received.rfind("Grbl", 0) == 0) { - listener->on_banner(received, this); - - } else { - listener->on_line_received(received, this); - } +// if (received.rfind("Grbl", 0) == 0) { +// listener->on_banner(received, this); +// +// } else { + listener->on_line_received(received, this); +// } } received.clear(); } else { @@ -117,6 +119,15 @@ void grbl::tcp_transport::worker() { } } + auto now = std::chrono::high_resolution_clock::now(); + auto ms_since_last_report = std::chrono::duration_cast(now - last_report); + + // maintain a 5Hz rate + if (ms_since_last_report.count() >= 200) { + request_realtime_report(); + last_report = now; + } + // give some time to others std::this_thread::sleep_for(std::chrono::milliseconds(1)); } diff --git a/grbl_communication.h b/grbl_communication.h index 79a9bc5..5c0a720 100644 --- a/grbl_communication.h +++ b/grbl_communication.h @@ -12,10 +12,10 @@ struct transport_callbacks { virtual void on_connected(transport *) = 0; virtual void on_disconnected(transport *) = 0; virtual void on_line_received(std::string line, transport *) = 0; - virtual void on_banner(std::string version, transport *) = 0; }; struct transport { + virtual ~transport() = default; virtual void open(transport_callbacks& cb) = 0; virtual void close() = 0; virtual void send(std::string line) = 0; diff --git a/grbl_machine.cpp b/grbl_machine.cpp index a664bea..9866b10 100644 --- a/grbl_machine.cpp +++ b/grbl_machine.cpp @@ -1,6 +1,11 @@ #include #include #include "machine.h" +#include "string_utils.h" + +static bool starts_with(const std::string& line, const std::string& prefix) { + return line.rfind(prefix, 0) == 0; +} grbl::machine::machine() { pipe = new tcp_transport("192.168.5.39", 23); @@ -14,7 +19,7 @@ void grbl::machine::connect() { pipe->open(*this); } -void grbl::machine::run_program(grbl::program pgm) { +void grbl::machine::run_program(const grbl::program& pgm) { std::cout << "running program with " << pgm.number_of_instructions() << " instructions" << std::endl; running_program = pgm; state = grbl_machine_state::run_program; @@ -35,23 +40,148 @@ void grbl::machine::on_disconnected(grbl::transport *transport) { std::cout << "grbl machine disconnected" << std::endl; } -void grbl::machine::on_line_received(std::string line, grbl::transport *transport) { - std::cout << ">> " << line << std::endl; - if (state == grbl_machine_state::run_program) { - if (line.rfind("ok", 0) == 0) { - continue_program(); - } else if (line.rfind("error", 0) == 0) { - std::cerr << "Received error" << std::endl; +grbl::realtime_status_report grbl::parse_status_report(std::string line, grbl::realtime_status_report& result) { +// grbl::realtime_status_report result; + + auto l = line.substr(1, -1); + auto pieces = split_string(l, "|"); + for (auto i = 0; i < pieces.size(); i++) { + if (i == 0) { + // status + auto elements = split_string(pieces[i], ":"); + result.status = status_from_string(elements[0]); + result.sub_status = elements.size() > 1 ? elements[1] : ""; } else { + auto elements = split_string(pieces[i], ":"); + if (elements[0] == "WPos") { + auto axis = split_string(elements[1], ","); + result.work_pos[0] = std::stof(axis[0]); + result.work_pos[1] = std::stof(axis[1]); + result.work_pos[2] = std::stof(axis[2]); + } else if (elements[0] == "MPos") { + auto axis = split_string(elements[1], ","); + result.machine_pos[0] = std::stof(axis[0]); + result.machine_pos[1] = std::stof(axis[1]); + result.machine_pos[2] = std::stof(axis[2]); + } else if (elements[0] == "Bf") { + auto p = split_string(elements[1], ","); + result.buffers_free = std::stoi(p[0]); + result.rx_chars_free = std::stoi(p[1]); + } else { + // not implemented + } } - } else { - // evaluate responses when not running a program + } + return result; +} + +grbl::machine_status grbl::status_from_string(const std::string& status) { + if (status == "Idle") return machine_status::idle; + if (status == "Run") return machine_status::run; + if (status == "Hold") return machine_status::hold; + if (status == "Jog") return machine_status::jog; + if (status == "Alarm") return machine_status::alarm; + if (status == "Door") return machine_status::door; + if (status == "Check") return machine_status::check; + if (status == "Home") return machine_status::home; + if (status == "Sleep") return machine_status::sleep; + if (status == "Tool") return machine_status::tool; + return machine_status::unknown; +} + +std::string grbl::status_to_string(const grbl::machine_status& status) { + switch (status) { + case machine_status::idle: + return "Idle"; + case machine_status::run: + return "Run"; + case machine_status::hold: + return "Hold"; + case machine_status::jog: + return "Jog"; + case machine_status::alarm: + return "Alarm"; + case machine_status::door: + return "Door"; + case machine_status::check: + return "Check"; + case machine_status::home: + return "Home"; + case machine_status::sleep: + return "Sleep"; + case machine_status::tool: + return "Tool"; + case machine_status::unknown: + default: + return "Unknown"; } } -void grbl::machine::on_banner(std::string version, grbl::transport *transport) { - std::cout << "grbl machine received banner" << std::endl; +std::string grbl::alarm_to_string(int alarm) { + switch (alarm) { + case 1: + return "Hard limit has been triggered. Machine position is likely lost due to sudden halt. Re-homing is highly recommended."; + case 2: + return "Soft limit alarm. G-code motion target exceeds machine travel. Machine position retained. Alarm may be safely unlocked."; + case 3: + return "Reset while in motion. Machine position is likely lost due to sudden halt. Re-homing is highly recommended."; + case 4: + return "Probe fail. Probe is not in the expected initial state before starting probe cycle when G38.2 and G38.3 is not triggered and G38.4 and G38.5 is triggered."; + case 5: + return "Probe fail. Probe did not contact the workpiece within the programmed travel for G38.2 and G38.4."; + case 6: + return "Homing fail. The active homing cycle was reset."; + case 7: + return "Homing fail. Safety door was opened during homing cycle."; + case 8: + return "Homing fail. Pull off travel failed to clear limit switch. Try increasing pull-off setting or check wiring."; + case 9: + return "Homing fail. Could not find limit switch within search distances. Try increasing max travel, decreasing pull-off distance, or check wiring."; + case 10: + return "Homing fail. Second dual axis limit switch failed to trigger within configured search distance after first. Try increasing trigger fail distance or check wiring."; + default: + return "unknown alarm code"; + } +} + +void grbl::machine::on_line_received(std::string line, grbl::transport *transport) { + if (line.at(0) != '<') + std::cout << ">> " << line << std::endl; + + if (starts_with(line, "ok")) { +// on_ok(); + if (state == grbl_machine_state::run_program) { + continue_program(); + } + + } else if (starts_with(line, "error")) { + size_t error = std::stoi(line.substr(6)); +// on_error(error); + } else { + // we have a push message + if (starts_with(line, "Grbl")) { + listener->on_banner(line); + } else if (starts_with(line, "<")) { + last_report = parse_status_report(line, last_report); + listener->on_realtime_status_report(last_report); + } else if (starts_with(line, "[MSG:")) { + listener->on_message(line.substr(5, line.size() - 6)); + } else if (starts_with(line, "ALARM:")) { + listener->on_alarm(std::stoi(line.substr(6))); + } + } + +// if (state == grbl_machine_state::run_program) { +// if (line.rfind("ok", 0) == 0) { +// continue_program(); +// } else if (line.rfind("error", 0) == 0) { +// std::cerr << "Received error" << std::endl; +// } else { +// } +// } else { +// // evaluate responses when not running a program +// } } void grbl::machine::continue_program() { @@ -72,9 +202,9 @@ void grbl::machine::continue_program() { } void grbl::machine::request_jog(jog_state jog) const { + cancel_jog(); if (jog.no_jogging()) { -// cancel_jog(); return; } @@ -109,8 +239,30 @@ void grbl::machine::request_jog(jog_state jog) const { void grbl::machine::cancel_jog() const { pipe->send_single_char_command(0x85); +} +void grbl::machine::set_listener(grbl::machine_listener *listener) { + machine::listener = listener; +} +void grbl::machine::request_unlock() { + pipe->send("$X"); +} + +void grbl::machine::request_home() { + pipe->send("$H"); +} + +void grbl::machine::request_reset() { + pipe->send_single_char_command(0x18); +} + +void grbl::machine::request_cycle_start() { + pipe->send_single_char_command(0x81); +} + +void grbl::machine::request_feed_hold() { + pipe->send_single_char_command(0x82); } bool grbl::jog_state::no_jogging() const { diff --git a/grbl_test.cpp b/grbl_test.cpp index aeb04ad..2dbf48d 100644 --- a/grbl_test.cpp +++ b/grbl_test.cpp @@ -1,6 +1,8 @@ -#include "grbl.h" #include +#include "grbl.h" +#include "machine.h" + TEST(grbl_program, default_state) { grbl::program pgm; @@ -25,4 +27,24 @@ X3.87739 Y78.52820 EXPECT_EQ("", pgm.filename); EXPECT_EQ(6, pgm.number_of_instructions()); EXPECT_EQ(true, pgm.is_loaded); +} + +TEST(grbl_status_report, parse) { + auto content = ""; + grbl::realtime_status_report r; + grbl::parse_status_report(content, r); + + EXPECT_EQ(grbl::machine_status::idle, r.status); + EXPECT_EQ("", r.sub_status); + + EXPECT_EQ(0, r.machine_pos[0]); + EXPECT_EQ(1, r.machine_pos[1]); + EXPECT_EQ(2, r.machine_pos[2]); + + EXPECT_EQ(35, r.buffers_free); + + // + grbl::parse_status_report("", r); + EXPECT_EQ(grbl::machine_status::home, r.status); + EXPECT_EQ(1022, r.rx_chars_free); } \ No newline at end of file diff --git a/machine.h b/machine.h index 0c17730..2f8da1c 100644 --- a/machine.h +++ b/machine.h @@ -5,6 +5,48 @@ namespace grbl { +enum class machine_status { + idle, + run, + hold, + jog, + alarm, + door, + check, + home, + sleep, + tool, + unknown +}; + +machine_status status_from_string(const std::string& status); +std::string status_to_string(const machine_status& status); +std::string alarm_to_string(int alarm); + +struct realtime_status_report { + machine_status status; + std::string sub_status; + float work_pos[3] = {0}; + float machine_pos[3] = {0}; + size_t buffers_free = 0; + size_t rx_chars_free = 0; + size_t line_number = 0; + float feed_rate = 0; + float programmed_rpm = 0; + float actual_rpm = 0; + std::string signals; + float axis_offsets[3] = {0}; + std::string coordinate_system; + std::string overrides; + std::string accessory_status; + bool mpg = false; + bool homing_complete = false; + std::string scaled_axis; + bool tool_length_reference_offset_set = false; + std::string firmware; +}; + +realtime_status_report parse_status_report(std::string line, grbl::realtime_status_report& result); enum class grbl_machine_state { init, @@ -12,17 +54,6 @@ enum class grbl_machine_state { idle, }; -enum class jog_direction { - up, - down, - left, - right, - up_left, - up_right, - down_left, - down_right -}; - struct jog_state { bool left_pressed = false; bool right_pressed = false; @@ -33,7 +64,7 @@ struct jog_state { bool speed_fast_pressed = false; bool speed_slow_pressed = false; - bool no_jogging() const; + [[nodiscard]] bool no_jogging() const; }; static bool operator==(const jog_state& a, const jog_state& b) { @@ -54,21 +85,41 @@ static bool operator!=(const jog_state& a, const jog_state& b) { return !(a == b); } +struct machine_listener { + virtual void on_connected() = 0; + virtual void on_disconnected() = 0; + virtual void on_realtime_status_report(realtime_status_report) = 0; + virtual void on_banner(std::string line) = 0; + virtual void on_message(std::string message) = 0; + virtual void on_alarm(int alarm) = 0; +}; + struct machine : public transport_callbacks { machine(); ~machine(); + void set_listener(grbl::machine_listener *listener); void connect(); - void run_program(grbl::program pgm); + void run_program(const grbl::program& pgm); void request_jog(jog_state jog) const; void cancel_jog() const; + realtime_status_report get_status() const { return last_report; }; + + void request_unlock(); + void request_home(); + void request_reset(); + + void request_cycle_start(); + void request_feed_hold(); + +protected: void on_connected(transport *transport) override; void on_disconnected(transport *transport) override; void on_line_received(std::string line, transport *transport) override; - - void on_banner(std::string version, transport *transport) override; + realtime_status_report last_report{}; + machine_listener* listener = nullptr; transport *pipe = nullptr; grbl_machine_state state = grbl_machine_state::init; program running_program; diff --git a/main.cpp b/main.cpp index e40cc6f..6266dad 100644 --- a/main.cpp +++ b/main.cpp @@ -58,36 +58,76 @@ using namespace nanogui; grbl::machine cnc{}; -class SenderApp : public Screen { +class SenderApp : public Screen, public grbl::machine_listener { public: Window *window; - Window *left_window; - Window *right_window; grbl::jog_state jog; + Label *m_pos_x, *m_pos_y, *m_pos_z; + TextBox *lblStatus, *lblSubstatus; + nanogui::Color red = nanogui::Color(255, 0, 0, 255); + int last_alarm = 0; SenderApp() : Screen(Vector2i(1024, 768), "GRBL Sender") { inc_ref(); - window = new Window(this, "Button demo"); + window = new Window(this, "Machine status"); // window->set_fixed_height(Screen::size().y()); window->set_position(Vector2i(0, 0)); - window->set_layout(new GridLayout(nanogui::Orientation::Horizontal, 2, Alignment::Fill, 0, 0)); - window->set_size(Screen::size()); + window->set_layout(new GroupLayout()); +// window->set_size(Screen::size()); - left_window = new Window(window, "Left sidebar"); - left_window->set_modal(true); + new Label(window, "Status", "sans-bold"); + Widget *status_holder = new Widget(window); + status_holder->set_layout(new GridLayout()); - right_window = new Window(window, "Right sidebar"); - right_window->set_modal(true); + lblStatus = new TextBox(window, grbl::status_to_string(cnc.get_status().status)); + lblSubstatus = new TextBox(window, cnc.get_status().sub_status); - left_window->set_layout(new GroupLayout()); + // Machine pos + new Label(window, "Machine pos", "sans-bold"); + Widget *mpos = new Widget(window); + mpos->set_layout(new GridLayout()); + + new Label(mpos, "X"); + m_pos_x = new Label(mpos, std::to_string(cnc.get_status().machine_pos[0])); + new Label(mpos, "Y"); + m_pos_y = new Label(mpos, std::to_string(cnc.get_status().machine_pos[1])); + new Label(mpos, "Z"); + m_pos_z = new Label(mpos, std::to_string(cnc.get_status().machine_pos[2])); + + // buttons to change state + new Label(window, "Actions", "sans-bold"); + Widget *actions = new Widget(window); + actions->set_layout(new BoxLayout(Orientation::Horizontal)); + + Button *btnUnlock = new Button(actions, "Unlock"); + btnUnlock->set_callback([&] { + cnc.request_unlock(); + }); + Button *btnHome = new Button(actions, "Home"); + btnHome->set_callback([&] { + cnc.request_home(); + }); + Button *btnReset = new Button(actions, "Reset"); + btnReset->set_background_color(red); + btnReset->set_callback([&] { + cnc.request_reset(); + }); + Button *btnCycleStart = new Button(actions, "Cycle Start"); + btnCycleStart->set_callback([&] { + cnc.request_cycle_start(); + }); + Button *btnFeedHold = new Button(actions, "Feed Hold"); + btnFeedHold->set_callback([&] { + cnc.request_feed_hold(); + }); // No need to store a pointer, the data structure will be automatically // freed when the parent window is deleted - new Label(left_window, "Push buttons", "sans-bold"); + new Label(window, "Program", "sans-bold"); - Button *b = new Button(left_window, "Plain button"); - b->set_callback([&] { + Button *btnLoadProgram = new Button(window, "Load"); + btnLoadProgram->set_callback([&] { auto path = file_dialog( {{"nc", "G-Code files"}, {"ngc", "G-Code files"}}, false); @@ -96,38 +136,38 @@ public: cnc.run_program(pgm); } }); - b->set_tooltip("short tooltip"); + btnLoadProgram->set_tooltip("short tooltip"); - /* Alternative construction notation using variadic template */ - b = left_window->add