Files
grbl-sender/main.cpp
T

633 lines
21 KiB
C++

#include <nanogui/opengl.h>
#include <nanogui/screen.h>
#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/label.h>
#include <nanogui/button.h>
#include <nanogui/popupbutton.h>
#include <nanogui/progressbar.h>
#include <nanogui/messagedialog.h>
#include <nanogui/texture.h>
#include <nanogui/textarea.h>
#include <nanogui/textbox.h>
#include <nanogui/tabwidget.h>
#include <nanogui/shader.h>
#include <nanogui/renderpass.h>
#include <iostream>
#include <memory>
#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#if defined(_MSC_VER)
# pragma warning (disable: 4505) // don't warn about dead code in stb_image.h
#elif defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wunused-function"
#endif
#include <stb_image.h>
#include "grbl.h"
#include <gtest/gtest.h>
#include <regex>
#include "grbl_machine.h"
#include "string_utils.h"
#include "render.h"
#include "glm/gtx/quaternion.hpp"
#include "nanogui/nanogui.h"
#include <glm/ext/quaternion_float.hpp>
#include <glm/ext/quaternion_trigonometric.hpp>
#include <glm/vec3.hpp> // glm::vec3
#include <glm/vec4.hpp> // glm::vec4
#include <glm/mat4x4.hpp> // glm::mat4
#include <glm/ext/matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale
#include <glm/ext/matrix_clip_space.hpp> // glm::perspective
#include <glm/ext/scalar_constants.hpp> // glm::pi
using namespace nanogui;
grbl::machine cnc{};
class SenderApp : public Screen, public grbl::machine_listener {
public:
Window *window;
grbl::jog_state jog;
TextArea *lblStatus, *lblSubstatus;
nanogui::Color colRed = nanogui::Color(255, 0, 0, 255);
nanogui::Color colGreen = nanogui::Color(0, 255, 0, 255);
nanogui::Color colBg;
int last_alarm = 0;
grbl::program pgm;
Button *btnLoadProgram, *btnCheckProgram, *btnRunProgram;
grbl::program_renderer renderer;
glm::vec3 cam_target = glm::vec3(0);
float cam_zoom = 0;
glm::quat cam_src_rotation = glm::quat(1.0, 0.0, 0.0, 0.0); // identity quaternion
TabWidget *tab_widget;
VScrollPanel *settings_vscroll;
Widget *settings_layer;
VScrollPanel *parameters_vscroll;
Widget *parameters_layer;
TextBox *mpos_x_text, *mpos_y_text, *mpos_z_text;
ComboBox *cboOffset, *cboTool;
float offset_x = 0;
float offset_y = 0;
float offset_z = 0;
SenderApp() : Screen(Vector2i(1024, 768), "GRBL Sender") {
inc_ref();
// save regular button color
colBg = theme()->m_button_gradient_bot_focused;
// create main window
window = new Window(this, "Machine status");
window->set_fixed_height((Screen::size().y() - 40) / 2);
window->set_position(Vector2i(0, 0));
window->set_layout(new BoxLayout(nanogui::Orientation::Vertical));
tab_widget = window->add<TabWidget>();
tab_widget->set_callback([&](int index) {
if (index == 1 || index == 2) {
perform_layout();
}
tab_widget->set_selected_index(index);
});
tab_widget->set_fixed_height((this->height() - 100) / 2);
Widget *layer = new Widget(tab_widget);
layer->set_layout(new GroupLayout());
tab_widget->append_tab("Info", layer);
new Label(layer, "Status", "sans-bold");
Widget *status_holder = new Widget(layer);
status_holder->set_layout(new GridLayout());
lblStatus = layer->add<TextArea>();
lblStatus->set_fixed_height(20);
lblStatus->set_font("sans");
lblSubstatus = layer->add<TextArea>();
lblSubstatus->set_font("sans");
lblSubstatus->set_fixed_height(50);
// DRO
layer->add<Label>("DRO", "sans-bold", 20);
Widget *mpos = new Widget(layer);
mpos->set_layout(new GridLayout(Orientation::Horizontal, 1, Alignment::Middle, 0, 6));
auto x_holder = mpos->add<Widget>();
x_holder->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 6));
auto lbl = x_holder->add<Label>("X", "sans-bold", 20);
lbl->set_fixed_width(30);
mpos_x_text = x_holder->add<TextBox>(std::to_string(cnc.get_status().machine_pos[0]));
mpos_x_text->set_fixed_width(200);
auto zero_btn = x_holder->add<ToolButton>(FA_BAN);
zero_btn->set_flags(Button::Flags::NormalButton);
zero_btn->set_tooltip("Zero axis");
zero_btn->set_callback([&]() {
cnc.zero_offset_axis(cboOffset->selected_index(), 0);
});
auto y_holder = mpos->add<Widget>();
y_holder->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 6));
lbl = y_holder->add<Label>("Y", "sans-bold", 20);
lbl->set_fixed_width(30);
mpos_y_text = y_holder->add<TextBox>(std::to_string(cnc.get_status().machine_pos[1]));
mpos_y_text->set_fixed_width(200);
zero_btn = y_holder->add<ToolButton>(FA_BAN);
zero_btn->set_flags(Button::Flags::NormalButton);
zero_btn->set_tooltip("Zero axis");
zero_btn->set_callback([&]() {
cnc.zero_offset_axis(cboOffset->selected_index(), 1);
});
auto z_holder = mpos->add<Widget>();
z_holder->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 6));
lbl = z_holder->add<Label>("Z", "sans-bold", 20);
lbl->set_fixed_width(30);
mpos_z_text = z_holder->add<TextBox>(std::to_string(cnc.get_status().machine_pos[0]));
mpos_z_text->set_fixed_width(200);
zero_btn = z_holder->add<ToolButton>(FA_BAN);
zero_btn->set_flags(Button::Flags::NormalButton);
zero_btn->set_tooltip("Zero axis");
zero_btn->set_callback([&]() {
cnc.zero_offset_axis(cboOffset->selected_index(), 2);
});
auto zero_all_btn = mpos->add<Button>("Zero all");
zero_all_btn->set_callback([&]() {
cnc.zero_offset(cboOffset->selected_index());
});
// work parameters
layer->add<Label>("Work parameters", "sans-bold", 20);
auto x = new Widget(layer);
x->set_layout(new GridLayout(Orientation::Horizontal, 4, Alignment::Middle, 0, 6));
x->add<Label>("Offset:");
std::vector<std::string> offset_items = {"G54", "G55", "G56", "G57", "G58", "G59"};
cboOffset = x->add<ComboBox>(offset_items);
cboOffset->set_callback([&](int idx) {
refresh_offset();
update_dro();
});
x->add<Label>("Tool:");
std::vector<std::string> tool_items = {"None"};
cboTool = x->add<ComboBox>(tool_items);
// buttons to change state
new Label(layer, "Actions", "sans-bold");
auto *actions = new Widget(layer);
actions->set_layout(new BoxLayout(Orientation::Horizontal));
auto *btnUnlock = new Button(actions, "Unlock");
btnUnlock->set_callback([&] {
cnc.request_unlock();
});
auto *btnHome = new Button(actions, "Home");
btnHome->set_callback([&] {
cnc.request_home();
});
auto *btnReset = new Button(actions, "Reset");
btnReset->set_background_color(colRed);
btnReset->set_callback([&] {
cnc.request_reset();
});
auto *btnCycleStart = new Button(actions, "Cycle Start");
btnCycleStart->set_callback([&] {
cnc.request_cycle_start();
});
auto *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(layer, "Program", "sans-bold");
Widget *pgm_actions = new Widget(layer);
pgm_actions->set_layout(new BoxLayout(Orientation::Horizontal));
btnLoadProgram = new Button(pgm_actions, "Load");
btnLoadProgram->set_callback([&] {
auto path = file_dialog(
{{"gcode", "G-Code files"},
{"nc", "G-Code files"},
{"ngc", "G-Code files"}}, true);
btnCheckProgram->set_background_color(colBg);
btnRunProgram->set_background_color(colBg);
if (pgm.load_from_file(path)) {
btnCheckProgram->set_enabled(true);
// btnRunProgram->set_enabled(true);
this->init_program_geometry();
} else {
btnCheckProgram->set_enabled(false);
btnRunProgram->set_enabled(false);
}
});
btnLoadProgram->set_tooltip("Load program");
btnCheckProgram = new Button(pgm_actions, "Check");
btnCheckProgram->set_enabled(false);
btnCheckProgram->set_callback([&] {
cnc.check_program(pgm);
});
btnCheckProgram->set_tooltip("Check program");
btnRunProgram = new Button(pgm_actions, "Run");
btnRunProgram->set_enabled(false);
btnRunProgram->set_callback([&] {
cnc.run_program(pgm, "G" + std::to_string(cboOffset->selected_index() + 54));
});
btnRunProgram->set_tooltip("Execute program");
perform_layout();
m_render_pass = new RenderPass({this});
m_render_pass->set_clear_color(0, Color(0.3f, 0.3f, 0.32f, 1.f));
m_render_pass->set_depth_test(RenderPass::DepthTest::Always, true);
m_render_pass->set_cull_mode(RenderPass::CullMode::Disabled);
}
void refresh_offset() {
auto offset_name = "G" + std::to_string(cboOffset->selected_index() + 54);
auto offset_values = cnc.get_parameters().at(offset_name);
auto offset_pieces = split_string(offset_values, ",");
offset_x = std::stof(offset_pieces[0]);
offset_y = std::stof(offset_pieces[1]);
offset_z = std::stof(offset_pieces[2]);
}
void fill_in_parameters() {
parameters_vscroll = new VScrollPanel(tab_widget);
tab_widget->append_tab("Parameters", parameters_vscroll);
parameters_layer = new Widget(parameters_vscroll);
parameters_layer->set_layout(new GridLayout(Orientation::Horizontal, 1, Alignment::Middle));
auto& parameters = cnc.get_parameters();
for (auto& entry: parameters) {
auto w = parameters_layer->add<Widget>();
w->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 2, 2));
auto x = w->add<Label>(entry.first, "sans-bold", 20);
x->set_fixed_width(50);
auto y = w->add<TextBox>(entry.second);
y->set_editable(true);
y->set_fixed_width(200);
auto z = w->add<ToolButton>(FA_SAVE);
z->set_flags(Button::Flags::NormalButton); // no toggle, please
z->set_tooltip("save");
}
}
void fill_in_settings() {
settings_vscroll = new VScrollPanel(tab_widget);
tab_widget->append_tab("Settings", settings_vscroll);
settings_layer = new Widget(settings_vscroll);
settings_layer->set_layout(new GridLayout(Orientation::Horizontal, 1, Alignment::Middle));
auto& settings = cnc.get_settings();
for (auto& s: settings) {
auto w = settings_layer->add<Widget>();
w->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 2, 2));
auto x = w->add<Label>(s.first, "sans-bold", 20);
x->set_fixed_width(40);
auto desc = grbl::setting_description(std::stoi(s.first.substr(1)));
auto y = w->add<TextBox>(s.second);
y->set_editable(true);
y->set_tooltip(desc);
y->set_fixed_width(200);
auto z = w->add<ToolButton>(FA_SAVE);
z->set_flags(Button::Flags::NormalButton); // no toggle, please
z->set_tooltip("save");
auto t = w->add<TextArea>();
t->set_fixed_width(200);
t->set_fixed_height(20);
t->clear();
t->append(desc.substr(0, desc.find_first_of('\n')));
}
}
void init_program_geometry() {
renderer.update(pgm, cnc);
auto max_pos = renderer.get_extents_max();
auto min_pos = renderer.get_extents_min();
cam_target = (max_pos - min_pos) / 2.0f;
cam_zoom = (max_pos.x - min_pos.x);
cam_src_rotation = glm::quat(1.0, 0.0, 0.0, 0.0);
}
bool resize_event(const Vector2i& size) override {
return Screen::resize_event(size);
}
void on_connected() override {
}
void on_disconnected() override {
}
void on_settings_reloaded() override {
}
void on_parameters_reloaded() override {
refresh_offset();
update_dro();
}
grbl::realtime_status_report last_report;
void on_init_completed() override {
fill_in_settings();
fill_in_parameters();
refresh_offset();
update_dro();
}
void on_realtime_status_report(grbl::realtime_status_report report) override {
// if (report == last_report) return;
if (last_report.status != report.status) {
lblStatus->clear();
lblStatus->append(grbl::status_to_string(cnc.get_status().status));
lblSubstatus->clear();
if (cnc.get_status().status == grbl::machine_status::alarm) {
lblSubstatus->append(grbl::alarm_to_string(last_alarm));
} else {
lblSubstatus->append(cnc.get_status().sub_status);
}
}
update_dro();
last_report = report;
}
void update_dro() {
mpos_x_text->set_value(std::to_string(cnc.get_status().machine_pos[0] - offset_x));
mpos_y_text->set_value(std::to_string(cnc.get_status().machine_pos[1] - offset_y));
mpos_z_text->set_value(std::to_string(cnc.get_status().machine_pos[2] - offset_z));
}
void on_banner(std::string line) override {
}
void on_message(std::string message) override {
set_caption(message);
}
void on_alarm(int alarm) override {
last_alarm = alarm;
}
void on_check_completed(bool success, size_t failed_index, size_t error) override {
btnCheckProgram->set_background_color(success ? colGreen : colRed);
if (success) {
btnRunProgram->set_enabled(true);
new MessageDialog(this, MessageDialog::Type::Information, "Check result", "Program checked successfully");
} else {
btnRunProgram->set_enabled(false);
std::stringstream ss;
auto i = pgm.instruction_at(failed_index);
ss << "Program check failed at line " << i.line << " (" << i.command << "). Error: " << error << ", "
<< grbl::error_to_string(error);
new MessageDialog(this, MessageDialog::Type::Warning, "Check result", ss.str());
}
}
void on_run_completed(bool success, size_t failed_index, size_t error) override {
btnRunProgram->set_background_color(success ? colGreen : colRed);
if (success) {
new MessageDialog(this, MessageDialog::Type::Information, "Run result", "Program executed successfully");
} else {
std::stringstream ss;
auto i = pgm.instruction_at(failed_index);
ss << "Program execution failed at line " << i.line << " (" << i.command << "). Error: " << error << ", "
<< grbl::error_to_string(error);
new MessageDialog(this, MessageDialog::Type::Warning, "Run result", ss.str());
}
}
bool keyboard_event(int key, int scancode, int action, int modifiers) override {
if (Screen::keyboard_event(key, scancode, action, modifiers))
return true;
// if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
// set_visible(false);
// return true;
// }
if (key == GLFW_KEY_SPACE && action == GLFW_PRESS && !window->mouse_focused()) {
// reset trackball rotation
cam_src_rotation = glm::quat(1, 0, 0, 0);
}
auto new_jog = jog;
if (key == GLFW_KEY_LEFT_SHIFT) {
if (action == GLFW_PRESS) {
new_jog.speed_fast_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.speed_fast_pressed = false;
}
}
if (key == GLFW_KEY_LEFT_CONTROL) {
if (action == GLFW_PRESS) {
new_jog.speed_slow_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.speed_slow_pressed = false;
}
}
if (key == GLFW_KEY_UP) {
if (action == GLFW_PRESS) {
new_jog.up_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.up_pressed = false;
}
}
if (key == GLFW_KEY_DOWN) {
if (action == GLFW_PRESS) {
new_jog.down_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.down_pressed = false;
}
}
if (key == GLFW_KEY_LEFT) {
if (action == GLFW_PRESS) {
new_jog.left_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.left_pressed = false;
}
}
if (key == GLFW_KEY_RIGHT) {
if (action == GLFW_PRESS) {
new_jog.right_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.right_pressed = false;
}
}
if (key == GLFW_KEY_PAGE_UP) {
if (action == GLFW_PRESS) {
new_jog.z_up_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.z_up_pressed = false;
}
}
if (key == GLFW_KEY_PAGE_DOWN) {
if (action == GLFW_PRESS) {
new_jog.z_down_pressed = true;
} else if (action == GLFW_RELEASE) {
new_jog.z_down_pressed = false;
}
}
if (jog != new_jog) {
cnc.request_jog(new_jog);
jog = new_jog;
}
return false;
}
bool mouse_motion_event(const Vector2i& p, const Vector2i& rel, int button, int modifiers) override {
auto result = Widget::mouse_motion_event(p, rel, button, modifiers);
if (window->mouse_focused()) {
return result;
} else {
// std::cout << "Mouse motion p:" << p << ", rel:" << rel << ", button:" << button << ", .modifiers:" << modifiers << std::endl;
bool is_rotating = button == 1;
bool is_panning = button == 2;
if (is_rotating) {
if (abs(rel.x()) > abs(rel.y())) {
cam_src_rotation *= glm::angleAxis(rel.x() / 100.0f, glm::vec3(0, 1, 0));
} else {
cam_src_rotation *= glm::angleAxis(rel.y() / 100.0f, glm::vec3(1, 0, 0));
}
} else if (is_panning) {
// cam_src.x += (float) rel.x() / 10.0f;
// cam_src.y += (float) rel.y() / 10.0f;
}
return true;
}
}
bool scroll_event(const Vector2i& p, const Vector2f& rel) override {
if (window->mouse_focused()) {
return Widget::scroll_event(p, rel);
} else {
// std::cout << "Scroll event: p:" << p << ", rel:" << rel << std::endl;
cam_zoom -= rel.y() * cam_zoom / 10.0f;
cam_zoom = std::max(cam_zoom, 0.1f);
return true;
}
}
virtual void draw_contents() {
auto fb_size = framebuffer_size();
// start rendering
m_render_pass->resize(framebuffer_size());
m_render_pass->begin();
if (pgm.is_loaded) {
renderer.update(pgm, cnc);
// compute mvp
glm::mat4 projection = glm::perspective(45.0f * glm::pi<float>() / 180.0f,
(float) fb_size.x() / (float) fb_size.y(),
0.1f,
10000.f);
glm::mat4 view = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, -cam_zoom)) *
glm::toMat4(cam_src_rotation) *
glm::translate(glm::mat4(1.0f), -cam_target);
glm::mat4 model = glm::mat4(1.0f);
auto mvp = projection * view * model;
renderer.render(mvp, glm::vec2(fb_size.x(), fb_size.y()));
}
m_render_pass->end();
}
private:
ProgressBar *m_progress;
ref<RenderPass> m_render_pass;
using ImageHolder = std::unique_ptr<uint8_t[], void (*)(void *)>;
std::vector<std::pair<ref<Texture>, ImageHolder>> m_images;
int m_current_image;
};
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
auto result = RUN_ALL_TESTS();
if (result) {
exit(result);
}
try {
nanogui::init();
// scoped variables
{
ref<SenderApp> app = new SenderApp();
app->dec_ref();
app->draw_all();
app->set_visible(true);
cnc.set_listener(app);
cnc.connect();
nanogui::mainloop(1 / 60.f * 1000);
}
nanogui::shutdown();
} catch (const std::exception& e) {
std::string error_msg = std::string("Caught a fatal error: ") + std::string(e.what());
std::cerr << error_msg << std::endl;
return -1;
} catch (...) {
std::cerr << "Caught an unknown error!" << std::endl;
return -2;
}
return 0;
}