diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2e0e2ff18..7af71fe30 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -72,7 +72,8 @@ static t_config_enum_values s_keys_map_PrintHostType { { "duet", htDuet }, { "flashair", htFlashAir }, { "astrobox", htAstroBox }, - { "repetier", htRepetier } + { "repetier", htRepetier }, + { "mks", htMKS } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType) @@ -1854,12 +1855,14 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("flashair"); def->enum_values.push_back("astrobox"); def->enum_values.push_back("repetier"); + def->enum_values.push_back("mks"); def->enum_labels.push_back("PrusaLink"); def->enum_labels.push_back("OctoPrint"); def->enum_labels.push_back("Duet"); def->enum_labels.push_back("FlashAir"); def->enum_labels.push_back("AstroBox"); def->enum_labels.push_back("Repetier"); + def->enum_labels.push_back("MKS"); def->mode = comAdvanced; def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionEnum(htOctoPrint)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 68dbd68d3..657f34ad1 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -44,7 +44,7 @@ enum class MachineLimitsUsage { }; enum PrintHostType { - htPrusaLink, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier + htPrusaLink, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS }; enum AuthorizationType { diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index f0c2338e9..94b8bae6f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -237,6 +237,10 @@ set(SLIC3R_GUI_SOURCES Utils/UndoRedo.hpp Utils/HexFile.cpp Utils/HexFile.hpp + Utils/TCPConsole.cpp + Utils/TCPConsole.hpp + Utils/MKS.cpp + Utils/MKS.hpp ) if (APPLE) diff --git a/src/slic3r/Utils/MKS.cpp b/src/slic3r/Utils/MKS.cpp new file mode 100644 index 000000000..636a0b3b2 --- /dev/null +++ b/src/slic3r/Utils/MKS.cpp @@ -0,0 +1,152 @@ +#include "MKS.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/PrintConfig.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "Http.hpp" + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; + +namespace Slic3r { + + MKS::MKS(DynamicPrintConfig* config) : + host(config->opt_string("print_host")), console_port("8080") + {} + + const char* MKS::get_name() const { return "MKS"; } + + bool MKS::test(wxString& msg) const + { + Utils::TCPConsole console(host, console_port); + + console.enqueue_cmd("M105"); + bool ret = console.run_queue(); + + if (!ret) { + msg = wxString::FromUTF8(console.error_message().c_str()); + } + + return ret; + } + + wxString MKS::get_test_ok_msg() const + { + return _(L("Connection to MKS works correctly.")); + } + + wxString MKS::get_test_failed_msg(wxString& msg) const + { + return GUI::from_u8((boost::format("%s: %s") + % _utf8(L("Could not connect to MKS")) + % std::string(msg.ToUTF8())).str()); + } + + bool MKS::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const + { + bool res = true; + + auto upload_cmd = get_upload_url(upload_data.upload_path.string()); + BOOST_LOG_TRIVIAL(info) << boost::format("MKS: Uploading file %1%, filepath: %2%, print: %3%, command: %4%") + % upload_data.source_path + % upload_data.upload_path + % upload_data.start_print + % upload_cmd; + + auto http = Http::post(std::move(upload_cmd)); + http.set_post_body(upload_data.source_path); + + http.on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("MKS: File uploaded: HTTP %1%: %2%") % status % body; + + int err_code = get_err_code_from_body(body); + if (err_code != 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Request completed but error code was received: %1%") % err_code; + error_fn(format_error(body, L("Unknown error occured"), 0)); + res = false; + } + else if (upload_data.start_print) { + wxString errormsg; + res = start_print(errormsg, upload_data.upload_path.string()); + if (!res) { + error_fn(std::move(errormsg)); + } + } + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("MKS: Error uploading file: %1%, HTTP %2%, body: `%3%`") % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << "MKS: Upload canceled"; + res = false; + } + }).perform_sync(); + + + return res; + } + + std::string MKS::get_upload_url(const std::string& filename) const + { + return (boost::format("http://%1%/upload?X-Filename=%2%") + % host + % Http::url_encode(filename)).str(); + } + + bool MKS::start_print(wxString& msg, const std::string& filename) const + { + // For some reason printer firmware does not want to respond on gcode commands immediately after file upload. + // So we just introduce artificial delay to workaround it. + // TODO: Inspect reasons + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + Utils::TCPConsole console(host, console_port); + + console.enqueue_cmd(std::string("M23 ") + filename); + console.enqueue_cmd("M24"); + + bool ret = console.run_queue(); + + if (!ret) { + msg = wxString::FromUTF8(console.error_message().c_str()); + } + + return ret; + } + + int MKS::get_err_code_from_body(const std::string& body) const + { + pt::ptree root; + std::istringstream iss(body); // wrap returned json to istringstream + pt::read_json(iss, root); + + return root.get("err", 0); + } + +} // Slic3r diff --git a/src/slic3r/Utils/MKS.hpp b/src/slic3r/Utils/MKS.hpp new file mode 100644 index 000000000..feddd17d8 --- /dev/null +++ b/src/slic3r/Utils/MKS.hpp @@ -0,0 +1,42 @@ +#ifndef slic3r_MKS_hpp_ +#define slic3r_MKS_hpp_ + +#include +#include + +#include "PrintHost.hpp" +#include "TCPConsole.hpp" + +namespace Slic3r { + class DynamicPrintConfig; + class Http; + + class MKS : public PrintHost + { + public: + explicit MKS(DynamicPrintConfig* config); + ~MKS() override = default; + + const char* get_name() const override; + + bool test(wxString& curl_msg) const override; + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString& msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn) const override; + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return true; } + bool can_start_print() const override { return true; } + std::string get_host() const override { return host; } + + private: + std::string host; + std::string console_port; + + std::string get_upload_url(const std::string& filename) const; + bool start_print(wxString& msg, const std::string& filename) const; + int get_err_code_from_body(const std::string& body) const; + }; + +} + +#endif diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 53200a4c9..86f6101b6 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -18,6 +18,7 @@ #include "FlashAir.hpp" #include "AstroBox.hpp" #include "Repetier.hpp" +#include "MKS.hpp" #include "../GUI/PrintHostDialogs.hpp" namespace fs = boost::filesystem; @@ -51,6 +52,7 @@ PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config) case htAstroBox: return new AstroBox(config); case htRepetier: return new Repetier(config); case htPrusaLink: return new PrusaLink(config); + case htMKS: return new MKS(config); default: return nullptr; } } else { diff --git a/src/slic3r/Utils/TCPConsole.cpp b/src/slic3r/Utils/TCPConsole.cpp new file mode 100644 index 000000000..bb834bb5f --- /dev/null +++ b/src/slic3r/Utils/TCPConsole.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "TCPConsole.hpp" + +using boost::asio::steady_timer; +using boost::asio::ip::tcp; + +namespace Slic3r { + namespace Utils { + + TCPConsole::TCPConsole() : resolver_(io_context_), socket_(io_context_) + { + set_defaults(); + } + + TCPConsole::TCPConsole(const std::string& host_name, const std::string& port_name) : + resolver_(io_context_), socket_(io_context_) + { + set_defaults(); + set_remote(host_name, port_name); + } + + void TCPConsole::transmit_next_command() + { + if (cmd_queue_.empty()) { + io_context_.stop(); + return; + } + + std::string cmd = cmd_queue_.front(); + cmd_queue_.pop_front(); + + BOOST_LOG_TRIVIAL(debug) << boost::format("TCPConsole: transmitting '%3%' to %1%:%2%") + % host_name_ + % port_name_ + % cmd; + + + send_buffer_ = cmd + newline_; + + set_deadline_in(write_timeout_); + boost::asio::async_write( + socket_, + boost::asio::buffer(send_buffer_), + boost::bind(&TCPConsole::handle_write, this, _1, _2) + ); + } + + void TCPConsole::wait_next_line() + { + set_deadline_in(read_timeout_); + boost::asio::async_read_until( + socket_, + recv_buffer_, + newline_, + boost::bind(&TCPConsole::handle_read, this, _1, _2) + ); + } + + // TODO: Use std::optional here + std::string TCPConsole::extract_next_line() + { + char linebuf[1024]; + + std::istream is(&recv_buffer_); + is.getline(linebuf, sizeof(linebuf)); + if (is.good()) { + return linebuf; + } + + return ""; + } + + void TCPConsole::handle_read( + const boost::system::error_code& ec, + std::size_t bytes_transferred) + { + error_code_ = ec; + + if (ec) { + BOOST_LOG_TRIVIAL(error) << boost::format("TCPConsole: Can't read from %1%:%2%: %3%") + % host_name_ + % port_name_ + % ec.message(); + + io_context_.stop(); + } + else { + std::string line = extract_next_line(); + boost::trim(line); + + BOOST_LOG_TRIVIAL(debug) << boost::format("TCPConsole: received '%3%' from %1%:%2%") + % host_name_ + % port_name_ + % line; + + boost::to_lower(line); + + if (line == done_string_) { + transmit_next_command(); + } + else { + wait_next_line(); + } + } + } + + void TCPConsole::handle_write( + const boost::system::error_code& ec, + std::size_t) + { + error_code_ = ec; + if (ec) { + BOOST_LOG_TRIVIAL(error) << boost::format("TCPConsole: Can't write to %1%:%2%: %3%") + % host_name_ + % port_name_ + % ec.message(); + + io_context_.stop(); + } + else { + wait_next_line(); + } + } + + void TCPConsole::handle_connect(const boost::system::error_code& ec) + { + error_code_ = ec; + + if (ec) { + BOOST_LOG_TRIVIAL(error) << boost::format("TCPConsole: Can't connect to %1%:%2%: %3%") + % host_name_ + % port_name_ + % ec.message(); + + io_context_.stop(); + } + else { + is_connected_ = true; + BOOST_LOG_TRIVIAL(info) << boost::format("TCPConsole: connected to %1%:%2%") + % host_name_ + % port_name_; + + + transmit_next_command(); + } + } + + void TCPConsole::set_deadline_in(std::chrono::steady_clock::duration d) + { + deadline_ = std::chrono::steady_clock::now() + d; + } + bool TCPConsole::is_deadline_over() + { + return deadline_ < std::chrono::steady_clock::now(); + } + + bool TCPConsole::run_queue() + { + auto now = std::chrono::steady_clock::now(); + try { + // TODO: Add more resets and initializations after previous run (reset() method?..) + set_deadline_in(connect_timeout_); + is_connected_ = false; + io_context_.restart(); + + auto endpoints = resolver_.resolve(host_name_, port_name_); + + socket_.async_connect(endpoints->endpoint(), + boost::bind(&TCPConsole::handle_connect, this, _1) + ); + + // Loop until we get any reasonable result. Negative result is also result. + // TODO: Rewrite to more graceful way using deadlime_timer + bool timeout = false; + while (!(timeout = is_deadline_over()) && !io_context_.stopped()) { + if (error_code_) { + io_context_.stop(); + } + io_context_.run_for(boost::asio::chrono::milliseconds(100)); + } + + // Override error message if timeout is set + if (timeout) { + error_code_ = make_error_code(boost::asio::error::timed_out); + } + + // Socket is not closed automatically by boost + socket_.close(); + + if (error_code_) { + // We expect that message is logged in handler + return false; + } + + // It's expected to have empty queue after successful exchange + if (!cmd_queue_.empty()) { + BOOST_LOG_TRIVIAL(error) << "TCPConsole: command queue is not empty after end of exchange"; + return false; + } + } + catch (std::exception& e) + { + BOOST_LOG_TRIVIAL(error) << boost::format("TCPConsole: Exception while talking with %1%:%2%: %3%") + % host_name_ + % port_name_ + % e.what(); + + return false; + } + + return true; + } + + + } +} diff --git a/src/slic3r/Utils/TCPConsole.hpp b/src/slic3r/Utils/TCPConsole.hpp new file mode 100644 index 000000000..20472750b --- /dev/null +++ b/src/slic3r/Utils/TCPConsole.hpp @@ -0,0 +1,92 @@ +#ifndef slic3r_Utils_TCPConsole_hpp_ +#define slic3r_Utils_TCPConsole_hpp_ + +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + namespace Utils { + + using boost::asio::ip::tcp; + + class TCPConsole + { + public: + TCPConsole(); + TCPConsole(const std::string& host_name, const std::string& port_name); + ~TCPConsole() {} + + void set_defaults() + { + newline_ = "\n"; + done_string_ = "ok"; + connect_timeout_ = std::chrono::milliseconds(5000); + write_timeout_ = std::chrono::milliseconds(10000); + read_timeout_ = std::chrono::milliseconds(10000); + } + + void set_line_delimiter(const std::string& newline) { + newline_ = newline; + } + void set_command_done_string(const std::string& done_string) { + done_string_ = done_string; + } + + void set_remote(const std::string& host_name, const std::string& port_name) + { + host_name_ = host_name; + port_name_ = port_name; + } + + bool enqueue_cmd(const std::string& cmd) { + // TODO: Add multithread protection to queue + cmd_queue_.push_back(cmd); + return true; + } + + bool run_queue(); + std::string error_message() { + return error_code_.message(); + } + + private: + void handle_connect(const boost::system::error_code& ec); + void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred); + void handle_write(const boost::system::error_code& ec, std::size_t bytes_transferred); + + void transmit_next_command(); + void wait_next_line(); + std::string extract_next_line(); + + void set_deadline_in(std::chrono::steady_clock::duration); + bool is_deadline_over(); + + std::string host_name_; + std::string port_name_; + std::string newline_; + std::string done_string_; + std::chrono::steady_clock::duration connect_timeout_; + std::chrono::steady_clock::duration write_timeout_; + std::chrono::steady_clock::duration read_timeout_; + + std::list cmd_queue_; + + boost::asio::io_context io_context_; + tcp::resolver resolver_; + tcp::socket socket_; + boost::asio::streambuf recv_buffer_; + std::string send_buffer_; + + bool is_connected_; + boost::system::error_code error_code_; + std::chrono::steady_clock::time_point deadline_; + }; + + } // Utils +} // Slic3r + +#endif