mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 18:01:14 +00:00
* Stop Malformed Network Responses from Crashing the App Duet, MKS and UltiMaker parsed print host replies with boost read_json inside the HTTP completion callback with no try, so an HTML or truncated reply threw out of the Physical Printer Test button and terminated the app, or killed the upload queue thread. The five identical copies of the parser (ESP3D's and Flashforge's were unused) are replaced by one shared PrintHost::get_err_code_from_body that reports a non-JSON reply as an error. The upload queue now catches a failing job per job, so one bad upload no longer leaves later jobs queued forever. Flashforge read material station slots with nlohmann value(), which throws on off-type fields or non-object entries. The parsing moves into Flashforge::parse_material_slots, which reads fields leniently with the existing try_parse_json_int and skips bad entries. UserManager::parse_json parsed the payload before its try block; the parse now happens inside it. * Keep UploadFinished Paired with UploadStarted When an Upload Throws The exception from a throwing upload was caught around perform_job, so the UploadFinished lifecycle event was skipped and plugins saw an upload start that never finished. The catch now sits around the upload call. The error is reported through the job's error callback and UploadFinished is fired with an error code, as for any other failed upload. The worker keeps running for the next job. The started, upload and finished sequence moved to PrintHostJobQueue::upload_job so it can be tested without the dialog.
693 lines
26 KiB
C++
693 lines
26 KiB
C++
#include "Flashforge.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <ctime>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <sstream>
|
|
#include <fstream>
|
|
#include <set>
|
|
#include <map>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <boost/format.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/property_tree/json_parser.hpp>
|
|
#include <boost/asio.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <wx/frame.h>
|
|
#include <wx/event.h>
|
|
#include <wx/progdlg.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/stattext.h>
|
|
#include <wx/textctrl.h>
|
|
#include <wx/checkbox.h>
|
|
|
|
#include <boost/beast/core/detail/base64.hpp>
|
|
#include <curl/curl.h>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "slic3r/GUI/GUI.hpp"
|
|
#include "slic3r/GUI/I18N.hpp"
|
|
#include "slic3r/GUI/MsgDialog.hpp"
|
|
#include "Http.hpp"
|
|
#include "TCPConsole.hpp"
|
|
#include "SerialMessage.hpp"
|
|
#include "SerialMessageType.hpp"
|
|
|
|
namespace fs = boost::filesystem;
|
|
namespace pt = boost::property_tree;
|
|
using json = nlohmann::json;
|
|
|
|
namespace Slic3r {
|
|
|
|
namespace {
|
|
|
|
constexpr unsigned short FLASHFORGE_DISCOVERY_PORT = 48899;
|
|
constexpr unsigned short FLASHFORGE_DISCOVERY_LISTEN_PORT = 18007;
|
|
|
|
const std::array<unsigned char, 20> FLASHFORGE_DISCOVERY_MESSAGE = {
|
|
0x77, 0x77, 0x77, 0x2e, 0x75, 0x73, 0x72, 0x22,
|
|
0x65, 0x36, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00
|
|
};
|
|
|
|
std::string trim_null_terminated_ascii(const char* data, size_t len)
|
|
{
|
|
std::string out(data, data + len);
|
|
const auto pos = out.find('\0');
|
|
if (pos != std::string::npos)
|
|
out.resize(pos);
|
|
boost::trim(out);
|
|
return out;
|
|
}
|
|
|
|
bool parse_discovery_response(const std::vector<unsigned char>& response, const std::string& ip_address, FlashforgeDiscoveredPrinter& printer)
|
|
{
|
|
if (response.size() < 0xC4)
|
|
return false;
|
|
|
|
printer.name = trim_null_terminated_ascii(reinterpret_cast<const char*>(response.data()), 32);
|
|
printer.serial_number = trim_null_terminated_ascii(reinterpret_cast<const char*>(response.data() + 0x92), 32);
|
|
printer.ip_address = ip_address;
|
|
return !(printer.name.empty() && printer.serial_number.empty());
|
|
}
|
|
|
|
std::vector<std::string> get_discovery_broadcast_addresses()
|
|
{
|
|
std::set<std::string> addresses = {"255.255.255.255", "192.168.0.255", "192.168.1.255"};
|
|
|
|
try {
|
|
boost::asio::io_context io_context;
|
|
boost::asio::ip::tcp::resolver resolver(io_context);
|
|
boost::system::error_code ec;
|
|
const auto host_name = boost::asio::ip::host_name(ec);
|
|
if (!ec) {
|
|
const auto results = resolver.resolve(boost::asio::ip::tcp::v4(), host_name, "", ec);
|
|
if (!ec) {
|
|
for (const auto& entry : results) {
|
|
const auto addr = entry.endpoint().address();
|
|
if (!addr.is_v4())
|
|
continue;
|
|
|
|
const auto bytes = addr.to_v4().to_bytes();
|
|
if (bytes[0] == 127)
|
|
continue;
|
|
|
|
addresses.insert((boost::format("%1%.%2%.%3%.255") % static_cast<int>(bytes[0]) % static_cast<int>(bytes[1]) % static_cast<int>(bytes[2])).str());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
|
|
return {addresses.begin(), addresses.end()};
|
|
}
|
|
|
|
std::string safe_config_string(DynamicPrintConfig* config, const char* key)
|
|
{
|
|
if (config == nullptr)
|
|
return {};
|
|
|
|
if (const auto* opt = config->option<ConfigOptionString>(key); opt != nullptr)
|
|
return opt->value;
|
|
|
|
return {};
|
|
}
|
|
|
|
bool try_parse_json_int(const json& value, int& out)
|
|
{
|
|
try {
|
|
if (value.is_number_integer() || value.is_number_unsigned()) {
|
|
out = value.get<int>();
|
|
return true;
|
|
}
|
|
|
|
if (value.is_boolean()) {
|
|
out = value.get<bool>() ? 1 : 0;
|
|
return true;
|
|
}
|
|
|
|
if (value.is_string()) {
|
|
std::string text = value.get<std::string>();
|
|
boost::trim(text);
|
|
if (text.empty())
|
|
return false;
|
|
|
|
size_t pos = 0;
|
|
const long parsed = std::stol(text, &pos, 10);
|
|
if (pos == text.size()) {
|
|
out = static_cast<int>(parsed);
|
|
return true;
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool validate_local_api_response(const std::string& response_body, wxString& error_msg)
|
|
{
|
|
const auto parsed = json::parse(response_body, nullptr, false, true);
|
|
if (parsed.is_discarded() || !parsed.is_object()) {
|
|
error_msg = _(L("Flashforge returned an invalid JSON response."));
|
|
return false;
|
|
}
|
|
|
|
int result_code = 0;
|
|
bool has_code = false;
|
|
|
|
if (parsed.contains("code"))
|
|
has_code = try_parse_json_int(parsed["code"], result_code);
|
|
if (!has_code && parsed.contains("err"))
|
|
has_code = try_parse_json_int(parsed["err"], result_code);
|
|
|
|
if (has_code && result_code != 0) {
|
|
std::string message;
|
|
if (parsed.contains("message") && parsed["message"].is_string())
|
|
message = parsed["message"].get<std::string>();
|
|
else if (parsed.contains("msg") && parsed["msg"].is_string())
|
|
message = parsed["msg"].get<std::string>();
|
|
|
|
if (message.empty())
|
|
message = "Request failed";
|
|
|
|
error_msg = GUI::from_u8((boost::format("Flashforge local API error %1%: %2%") % result_code % message).str());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string sanitize_flashforge_filename(const std::string& filename, const std::string& fallback_extension = {})
|
|
{
|
|
std::string basename = fs::path(filename).filename().string();
|
|
if (basename.empty()) {
|
|
basename = "print";
|
|
if (!fallback_extension.empty())
|
|
basename += fallback_extension;
|
|
}
|
|
|
|
for (char& ch : basename) {
|
|
const bool is_ascii_alnum = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
|
|
if (!is_ascii_alnum && ch != '.' && ch != '_' && ch != '-') {
|
|
ch = '_';
|
|
}
|
|
}
|
|
|
|
return basename;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Flashforge::Flashforge(DynamicPrintConfig* config)
|
|
: m_host()
|
|
, m_serial_number()
|
|
, m_check_code()
|
|
, m_console_port("8899")
|
|
, m_gcFlavor(gcfMarlinLegacy)
|
|
, m_bufferSize(4096) // 4K buffer size
|
|
{
|
|
m_host = safe_config_string(config, "print_host");
|
|
m_serial_number = safe_config_string(config, "flashforge_serial_number");
|
|
m_check_code = safe_config_string(config, "printhost_apikey");
|
|
|
|
if (config != nullptr) {
|
|
if (const auto* gcode_flavor = config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor"); gcode_flavor != nullptr)
|
|
m_gcFlavor = gcode_flavor->value;
|
|
}
|
|
}
|
|
|
|
const char* Flashforge::get_name() const { return "Flashforge"; }
|
|
|
|
bool Flashforge::discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms, int idle_timeout_ms, int max_retries)
|
|
{
|
|
printers.clear();
|
|
|
|
try {
|
|
const auto broadcast_addresses = get_discovery_broadcast_addresses();
|
|
std::map<std::string, FlashforgeDiscoveredPrinter> by_ip;
|
|
|
|
for (int attempt = 0; attempt < std::max(1, max_retries); ++attempt) {
|
|
boost::asio::io_context io_context;
|
|
boost::asio::ip::udp::socket socket(io_context);
|
|
boost::system::error_code ec;
|
|
|
|
socket.open(boost::asio::ip::udp::v4(), ec);
|
|
if (ec) {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
|
|
socket.set_option(boost::asio::socket_base::broadcast(true), ec);
|
|
if (ec) {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
|
|
socket.set_option(boost::asio::socket_base::reuse_address(true), ec);
|
|
if (ec) {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
|
|
socket.bind({boost::asio::ip::udp::v4(), FLASHFORGE_DISCOVERY_LISTEN_PORT}, ec);
|
|
if (ec) {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
|
|
for (const auto& addr : broadcast_addresses) {
|
|
socket.send_to(boost::asio::buffer(FLASHFORGE_DISCOVERY_MESSAGE),
|
|
{boost::asio::ip::make_address_v4(addr, ec), FLASHFORGE_DISCOVERY_PORT}, 0, ec);
|
|
ec.clear();
|
|
}
|
|
|
|
socket.non_blocking(true, ec);
|
|
if (ec) {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
|
|
const auto start = std::chrono::steady_clock::now();
|
|
auto last_reply = start;
|
|
|
|
while (true) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() >= timeout_ms)
|
|
break;
|
|
if (!by_ip.empty() && std::chrono::duration_cast<std::chrono::milliseconds>(now - last_reply).count() >= idle_timeout_ms)
|
|
break;
|
|
|
|
std::vector<unsigned char> buffer(512);
|
|
boost::asio::ip::udp::endpoint remote_endpoint;
|
|
const auto received = socket.receive_from(boost::asio::buffer(buffer), remote_endpoint, 0, ec);
|
|
if (!ec) {
|
|
buffer.resize(received);
|
|
FlashforgeDiscoveredPrinter printer;
|
|
if (parse_discovery_response(buffer, remote_endpoint.address().to_string(), printer)) {
|
|
by_ip[printer.ip_address] = std::move(printer);
|
|
last_reply = std::chrono::steady_clock::now();
|
|
}
|
|
} else if (ec == boost::asio::error::would_block || ec == boost::asio::error::try_again) {
|
|
ec.clear();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
} else {
|
|
msg = wxString::FromUTF8(ec.message().c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!by_ip.empty())
|
|
break;
|
|
}
|
|
|
|
for (auto& [_, printer] : by_ip)
|
|
printers.emplace_back(std::move(printer));
|
|
|
|
std::sort(printers.begin(), printers.end(), [](const FlashforgeDiscoveredPrinter& lhs, const FlashforgeDiscoveredPrinter& rhs) {
|
|
if (lhs.name != rhs.name)
|
|
return lhs.name < rhs.name;
|
|
return lhs.ip_address < rhs.ip_address;
|
|
});
|
|
|
|
if (printers.empty()) {
|
|
msg = _(L("No Flashforge printers were discovered on the local network."));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (const std::exception& ex) {
|
|
msg = wxString::FromUTF8(ex.what());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Flashforge::test(wxString& msg) const
|
|
{
|
|
if (!m_serial_number.empty() && !m_check_code.empty())
|
|
return test_local_api(msg);
|
|
|
|
BOOST_LOG_TRIVIAL(debug) << boost::format("[Flashforge Serial] testing connection");
|
|
// Utils::TCPConsole console(m_host, m_console_port);
|
|
Utils::TCPConsole client(m_host, m_console_port);
|
|
client.enqueue_cmd(controlCommand);
|
|
bool res = client.run_queue();
|
|
if (!res) {
|
|
msg = wxString::FromUTF8(client.error_message().c_str());
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] testing connection failed");
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] testing connection success");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
wxString Flashforge::get_test_ok_msg() const
|
|
{
|
|
if (!m_serial_number.empty() && !m_check_code.empty())
|
|
return _(L("Connected to Flashforge local API successfully."));
|
|
return _(L("Serial connection to Flashforge is working correctly."));
|
|
}
|
|
|
|
wxString Flashforge::get_test_failed_msg(wxString& msg) const
|
|
{
|
|
const std::string prefix = (!m_serial_number.empty() && !m_check_code.empty()) ?
|
|
_utf8(L("Could not connect to Flashforge local API")) :
|
|
_utf8(L("Could not connect to Flashforge via serial"));
|
|
return GUI::from_u8((boost::format("%s: %s") % prefix % std::string(msg.ToUTF8())).str());
|
|
}
|
|
|
|
|
|
bool Flashforge::connect(wxString& msg) const
|
|
{
|
|
|
|
Utils::TCPConsole client(m_host, m_console_port);
|
|
|
|
client.enqueue_cmd(controlCommand);
|
|
client.enqueue_cmd(deviceInfoCommand);
|
|
|
|
if (m_gcFlavor == gcfKlipper)
|
|
client.enqueue_cmd(connectKlipperCommand);
|
|
else {
|
|
client.enqueue_cmd(connectLegacyCommand);
|
|
|
|
}
|
|
|
|
client.enqueue_cmd(statusCommand);
|
|
|
|
|
|
bool res = client.run_queue();
|
|
|
|
if (!res) {
|
|
msg = wxString::FromUTF8(client.error_message().c_str());
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Failed to initiate connection");
|
|
} else
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Successfully initiated Connection");
|
|
|
|
return res;
|
|
}
|
|
|
|
bool Flashforge::start_print(wxString& msg, const std::string& filename) const
|
|
{
|
|
Utils::TCPConsole client(m_host, m_console_port);
|
|
const std::string safe_filename = sanitize_flashforge_filename(filename);
|
|
Slic3r::Utils::SerialMessage startPrintCommand = {(boost::format("~M23 0:/user/%1%") % safe_filename).str(), Slic3r::Utils::Command};
|
|
client.enqueue_cmd(startPrintCommand);
|
|
bool res = client.run_queue();
|
|
|
|
if (!res) {
|
|
msg = wxString::FromUTF8(client.error_message().c_str());
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Failed to start print %1%") % safe_filename;
|
|
} else
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Started print %1%") % safe_filename;
|
|
|
|
return res;
|
|
}
|
|
|
|
bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const
|
|
{
|
|
if (!m_serial_number.empty() && !m_check_code.empty())
|
|
return upload_local_api(std::move(upload_data), std::move(progress_fn), std::move(error_fn));
|
|
|
|
bool res = true;
|
|
wxString errormsg;
|
|
|
|
Utils::TCPConsole client(m_host, m_console_port);
|
|
|
|
try {
|
|
|
|
res = connect(errormsg);
|
|
const std::string fallback_extension = upload_data.source_path.extension().string().empty() ? ".gcode" : upload_data.source_path.extension().string();
|
|
const std::string upload_filename = sanitize_flashforge_filename(upload_data.upload_path.string(), fallback_extension);
|
|
|
|
std::ifstream newfile;
|
|
newfile.open(upload_data.source_path.c_str(), std::ios::binary); // open a file to perform read operation using file object
|
|
std::string gcodeFile;
|
|
if (newfile.is_open()) { // checking whether the file is open
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Reading file...");
|
|
newfile.seekg(0, std::ios::end);
|
|
std::ifstream::pos_type pos = newfile.tellg();
|
|
|
|
std::vector<char> result(pos);
|
|
|
|
newfile.seekg(0, std::ios::beg);
|
|
newfile.read(&result[0], pos);
|
|
|
|
gcodeFile = std::string(result.begin(), result.end()); // TODO: Find more efficient way of breaking ifstream
|
|
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Reading file...done size is %1%") % gcodeFile.size();
|
|
|
|
newfile.close(); // close the file object.
|
|
}
|
|
Slic3r::Utils::SerialMessage fileuploadCommand =
|
|
{(boost::format("~M28 %1% 0:/user/%2%") % gcodeFile.size() % upload_filename).str(),
|
|
Slic3r::Utils::Command};
|
|
client.enqueue_cmd(fileuploadCommand);
|
|
|
|
//client.set_tcp_queue_delay(std::chrono::nanoseconds(10000));
|
|
|
|
for (int bytePos = 0; bytePos < gcodeFile.size(); bytePos += m_bufferSize) { // TODO: Find more efficient way of breaking ifstream
|
|
|
|
int bytePosEnd = (gcodeFile.size() - bytePos > m_bufferSize - 1) ? m_bufferSize : gcodeFile.size();
|
|
Slic3r::Utils::SerialMessage dataCommand = {gcodeFile.substr(bytePos, bytePosEnd), Slic3r::Utils::Data}; // Break into smaller byte chunks
|
|
|
|
client.enqueue_cmd(dataCommand);
|
|
|
|
}
|
|
|
|
res = client.run_queue();
|
|
|
|
if (res)
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Sent %1% ") % gcodeFile.size();
|
|
|
|
|
|
if (!res) {
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] error %1%") % client.error_message().c_str();
|
|
errormsg = wxString::FromUTF8(client.error_message().c_str());
|
|
error_fn(std::move(errormsg));
|
|
} else {
|
|
|
|
client.set_tcp_queue_delay(std::chrono::milliseconds(3000));
|
|
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Sending file save command ");
|
|
|
|
client.enqueue_cmd(saveFileCommand);
|
|
|
|
res = client.run_queue();
|
|
|
|
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint)
|
|
res = start_print(errormsg, upload_filename);
|
|
}
|
|
|
|
} catch (const std::exception& e) {
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] error %1%") % e.what();
|
|
errormsg = wxString::FromUTF8(e.what());
|
|
error_fn(std::move(errormsg));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
bool Flashforge::test_local_api(wxString& msg) const
|
|
{
|
|
std::string body;
|
|
return request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg);
|
|
}
|
|
|
|
bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const
|
|
{
|
|
slots.clear();
|
|
|
|
if (m_serial_number.empty() || m_check_code.empty()) {
|
|
msg = _(L("Flashforge local API requires both serial number and access code."));
|
|
return false;
|
|
}
|
|
|
|
std::string body;
|
|
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
|
|
return false;
|
|
|
|
if (!parse_material_slots(body, slots, supports_material_station)) {
|
|
msg = _(L("Flashforge returned an invalid JSON response."));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Flashforge::parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station)
|
|
{
|
|
slots.clear();
|
|
|
|
const auto parsed = json::parse(body, nullptr, false, true);
|
|
if (parsed.is_discarded())
|
|
return false;
|
|
|
|
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
|
|
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
|
|
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
|
|
const auto& slot_infos = station.contains("slotInfos") ? station["slotInfos"] :
|
|
station.contains("SlotInfos") ? station["SlotInfos"] : json::array();
|
|
|
|
bool reports_material_station = false;
|
|
|
|
int has_material_station_flag = 0;
|
|
if (detail.contains("hasMatlStation") && try_parse_json_int(detail["hasMatlStation"], has_material_station_flag))
|
|
reports_material_station = has_material_station_flag != 0;
|
|
else if (detail.contains("HasMatlStation") && try_parse_json_int(detail["HasMatlStation"], has_material_station_flag))
|
|
reports_material_station = has_material_station_flag != 0;
|
|
|
|
int slot_count = 0;
|
|
if (station.contains("slotCnt") && try_parse_json_int(station["slotCnt"], slot_count))
|
|
reports_material_station = reports_material_station || slot_count > 0;
|
|
else if (station.contains("SlotCnt") && try_parse_json_int(station["SlotCnt"], slot_count))
|
|
reports_material_station = reports_material_station || slot_count > 0;
|
|
|
|
if (slot_infos.is_array() && !slot_infos.empty())
|
|
reports_material_station = true;
|
|
|
|
if (supports_material_station != nullptr)
|
|
*supports_material_station = reports_material_station;
|
|
|
|
// Fields are read leniently: firmware may send numbers as strings or flags as numbers.
|
|
for (const auto& slot : slot_infos) {
|
|
if (!slot.is_object())
|
|
continue;
|
|
FlashforgeMaterialSlot info;
|
|
info.slot_id = static_cast<int>(slots.size()) + 1;
|
|
if (const auto it = slot.find("slotId"); it != slot.end())
|
|
try_parse_json_int(*it, info.slot_id);
|
|
int has_filament = 0;
|
|
if (const auto it = slot.find("hasFilament"); it != slot.end() && try_parse_json_int(*it, has_filament))
|
|
info.has_filament = has_filament != 0;
|
|
if (const auto it = slot.find("materialName"); it != slot.end() && it->is_string())
|
|
info.material_name = it->get<std::string>();
|
|
if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string())
|
|
info.material_color = it->get<std::string>();
|
|
slots.emplace_back(std::move(info));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Flashforge::upload_local_api(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn) const
|
|
{
|
|
bool res = true;
|
|
std::string material_map_b64;
|
|
std::string material_map_json = "[]";
|
|
auto leveling_before_print = upload_data.extended_info["levelingBeforePrint"] == "1";
|
|
auto time_lapse_video = upload_data.extended_info["timeLapseVideo"] == "1";
|
|
auto use_material_station = upload_data.extended_info["useMatlStation"] == "1";
|
|
|
|
if (auto it = upload_data.extended_info.find("materialMappings"); it != upload_data.extended_info.end())
|
|
material_map_json = it->second;
|
|
|
|
material_map_b64.resize(boost::beast::detail::base64::encoded_size(material_map_json.size()));
|
|
material_map_b64.resize(boost::beast::detail::base64::encode(material_map_b64.data(), material_map_json.data(), material_map_json.size()));
|
|
|
|
auto url = make_http_url("uploadGcode");
|
|
const std::string fallback_extension = upload_data.source_path.extension().string().empty() ? (upload_data.use_3mf ? ".3mf" : ".gcode") : upload_data.source_path.extension().string();
|
|
auto filename = sanitize_flashforge_filename(upload_data.upload_path.string(), fallback_extension);
|
|
std::string file_size;
|
|
try {
|
|
file_size = std::to_string(fs::file_size(upload_data.source_path));
|
|
} catch (...) {
|
|
file_size = "0";
|
|
}
|
|
|
|
auto http = Http::post(url);
|
|
http.header("serialNumber", m_serial_number)
|
|
.header("checkCode", m_check_code)
|
|
.header("fileSize", file_size)
|
|
.header("printNow", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
|
|
.header("levelingBeforePrint", leveling_before_print ? "true" : "false")
|
|
.header("flowCalibration", "false")
|
|
.header("firstLayerInspection", "false")
|
|
.header("timeLapseVideo", time_lapse_video ? "true" : "false")
|
|
.header("useMatlStation", use_material_station ? "true" : "false")
|
|
.header("gcodeToolCnt", upload_data.extended_info["gcodeToolCnt"])
|
|
.header("materialMappings", material_map_b64)
|
|
.form_add_file("gcodeFile", upload_data.source_path.string(), filename)
|
|
.on_complete([&](std::string body, unsigned status) {
|
|
wxString msg;
|
|
if (!validate_local_api_response(body, msg)) {
|
|
BOOST_LOG_TRIVIAL(error) << boost::format("[Flashforge HTTP] upload rejected by printer: HTTP %1% body: `%2%`") % status % body;
|
|
error_fn(msg);
|
|
res = false;
|
|
} else {
|
|
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge HTTP] upload complete: HTTP %1% body: %2%") % status % body;
|
|
}
|
|
})
|
|
.on_error([&](std::string body, std::string error, unsigned status) {
|
|
BOOST_LOG_TRIVIAL(error) << boost::format("[Flashforge HTTP] upload failed: %1%, HTTP %2%, body: `%3%`") % error % status % body;
|
|
error_fn(format_error(body, error, status));
|
|
res = false;
|
|
})
|
|
.on_progress([&](Http::Progress progress, bool& cancel) {
|
|
progress_fn(std::move(progress), cancel);
|
|
if (cancel)
|
|
res = false;
|
|
})
|
|
.perform_sync();
|
|
|
|
return res;
|
|
}
|
|
|
|
bool Flashforge::request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const
|
|
{
|
|
bool ok = true;
|
|
auto http = Http::post(make_http_url(path));
|
|
http.header("Content-Type", "application/json")
|
|
.set_post_body(body)
|
|
.on_complete([&](std::string body_text, unsigned) {
|
|
response_body = std::move(body_text);
|
|
if (!validate_local_api_response(response_body, error_msg))
|
|
ok = false;
|
|
})
|
|
.on_error([&](std::string body_text, std::string error, unsigned status) {
|
|
response_body = std::move(body_text);
|
|
error_msg = format_error(response_body, error, status);
|
|
ok = false;
|
|
})
|
|
.perform_sync();
|
|
return ok;
|
|
}
|
|
|
|
std::string Flashforge::make_http_url(const std::string& path) const
|
|
{
|
|
return (boost::format("http://%1%:8898/%2%") % extract_host_name() % path).str();
|
|
}
|
|
|
|
std::string Flashforge::extract_host_name() const
|
|
{
|
|
std::string host = m_host;
|
|
if (host.find("://") == std::string::npos) {
|
|
const auto slash_pos = host.find('/');
|
|
if (slash_pos != std::string::npos)
|
|
host = host.substr(0, slash_pos);
|
|
return host;
|
|
}
|
|
|
|
std::string out = host;
|
|
CURLU* hurl = curl_url();
|
|
if (!hurl)
|
|
return host;
|
|
|
|
const auto rc = curl_url_set(hurl, CURLUPART_URL, host.c_str(), 0);
|
|
if (rc == CURLUE_OK) {
|
|
char* raw_host = nullptr;
|
|
if (curl_url_get(hurl, CURLUPART_HOST, &raw_host, 0) == CURLUE_OK && raw_host != nullptr) {
|
|
out = raw_host;
|
|
curl_free(raw_host);
|
|
}
|
|
}
|
|
|
|
curl_url_cleanup(hurl);
|
|
return out;
|
|
}
|
|
|
|
} // namespace Slic3r
|