diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index cd58e90e97..4e7234d1bc 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1329,7 +1329,7 @@ static std::vector s_Preset_printer_options { "scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode", "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", "best_object_pos", "head_wrap_detect_zone", - "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent", + "host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent", "print_host_webui", "printhost_cafile","printhost_port","printhost_authorization_type", "printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format", @@ -3839,6 +3839,7 @@ static std::vector s_PhysicalPrinter_opts { "print_host", "print_host_webui", "printhost_apikey", + "flashforge_serial_number", "printhost_cafile", "printhost_port", "printhost_authorization_type", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b706283d07..9617c9866f 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -828,6 +828,13 @@ void PrintConfigDef::init_common_params() def->cli = ConfigOptionDef::nocli; def->set_default_value(new ConfigOptionString()); + def = this->add("flashforge_serial_number", coString); + def->label = L("Serial Number"); + def->tooltip = L("Flashforge local API requires the printer serial number."); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionString()); + def = this->add("printhost_port", coString); def->label = L("Printer"); def->tooltip = L("Name of the printer."); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4b41cc6722..d4c5399142 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -338,6 +338,7 @@ public: std::vector get_extruders_without_support(bool conside_custom_gcode = false) const; // get used filaments from gcode result, 1 based idx std::vector get_used_filaments(); + const std::vector& get_slice_filaments_info() const { return slice_filaments_info; } int get_physical_extruder_by_filament_id(const DynamicConfig& g_config, int idx) const; bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message); bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector &tpu_filaments); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 667919aca3..7a85a4c1f2 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,7 @@ #include "PrintHostDialogs.hpp" #include "../Utils/ASCIIFolding.hpp" #include "../Utils/PrintHost.hpp" +#include "../Utils/Flashforge.hpp" #include "../Utils/UndoRedo.hpp" #include "RemovableDriveManager.hpp" #include "BitmapCache.hpp" @@ -204,10 +206,37 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr { auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "printer_host_browser", _L("Browse") + " " + dots); m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) { - BonjourDialog dialog(this, Preset::printer_technology(*m_config)); - if (dialog.show_and_lookup()) { - m_optgroup->set_value("print_host", dialog.get_selected(), true); - m_optgroup->get_field("print_host")->field_changed(); + const auto host_type = m_config->opt_enum("host_type"); + if (host_type == htFlashforge) { + wxBusyCursor wait; + std::vector printers; + wxString error_msg; + if (!Flashforge::discover_printers(printers, error_msg)) { + show_error(this, error_msg); + return; + } + + wxArrayString choices; + for (const auto& printer : printers) + choices.Add(from_u8((boost::format("%1% (%2%) [%3%]") % printer.name % printer.ip_address % printer.serial_number).str())); + + wxSingleChoiceDialog dialog(this, _L("Select a Flashforge printer"), _L("Discovered Printers"), choices); + if (dialog.ShowModal() == wxID_OK) { + const int idx = dialog.GetSelection(); + if (idx >= 0 && idx < static_cast(printers.size())) { + m_optgroup->set_value("print_host", from_u8(printers[idx].ip_address), true); + m_optgroup->set_value("flashforge_serial_number", from_u8(printers[idx].serial_number), true); + m_config->opt_string("print_host") = printers[idx].ip_address; + m_config->opt_string("flashforge_serial_number") = printers[idx].serial_number; + update_printhost_buttons(); + } + } + } else { + BonjourDialog dialog(this, Preset::printer_technology(*m_config)); + if (dialog.show_and_lookup()) { + m_optgroup->set_value("print_host", dialog.get_selected(), true); + m_optgroup->get_field("print_host")->field_changed(); + } } }); @@ -329,6 +358,10 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr option.opt.width = Field::def_width_wider(); m_optgroup->append_single_option_line(option); + option = m_optgroup->get_option("flashforge_serial_number"); + option.opt.width = Field::def_width_wider(); + m_optgroup->append_single_option_line(option); + option = m_optgroup->get_option("printhost_port"); option.opt.width = Field::def_width_wider(); Line port_line = m_optgroup->create_single_option_line(option); @@ -685,13 +718,17 @@ void PhysicalPrinterDialog::update(bool printer_change) } if (opt->value == htFlashforge) { - m_optgroup->hide_field("printhost_apikey"); - m_optgroup->hide_field("printhost_authorization_type"); - } + m_optgroup->show_field("printhost_apikey"); + m_optgroup->show_field("flashforge_serial_number"); + m_optgroup->hide_field("printhost_authorization_type"); + } else { + m_optgroup->hide_field("flashforge_serial_number"); + } } else { m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false); m_optgroup->hide_field("host_type"); + m_optgroup->hide_field("flashforge_serial_number"); m_optgroup->show_field("printhost_authorization_type"); @@ -809,7 +846,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) void PhysicalPrinterDialog::check_host_key_valid() { - std::vector keys = {"print_host", "print_host_webui", "printhost_apikey", "printhost_cafile", "printhost_user", "printhost_password", "printhost_port"}; + std::vector keys = {"print_host", "print_host_webui", "printhost_apikey", "flashforge_serial_number", "printhost_cafile", "printhost_user", "printhost_password", "printhost_port"}; for (auto &key : keys) { auto it = m_config->option(key); if (!it) m_config->set_key_value(key, new ConfigOptionString("")); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 00c2ed65ff..8d46b6dc3b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16081,6 +16081,18 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us if (upload_job.empty()) return; + const auto host_type_opt = physical_printer_config->option>("host_type"); + const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink; + const auto* ff_serial_opt = physical_printer_config->option("flashforge_serial_number"); + const auto* ff_code_opt = physical_printer_config->option("printhost_apikey"); + const bool flashforge_local_api = + host_type == htFlashforge && + ff_serial_opt != nullptr && !ff_serial_opt->value.empty() && + ff_code_opt != nullptr && !ff_code_opt->value.empty(); + + if (flashforge_local_api) + use_3mf = true; + upload_job.upload_data.use_3mf = use_3mf; // Obtain default output path @@ -16128,8 +16140,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us { auto preset_bundle = wxGetApp().preset_bundle; - const auto opt = physical_printer_config->option>("host_type"); - const auto host_type = opt != nullptr ? opt->value : htElegooLink; auto config = get_app_config(); std::unique_ptr pDlg; @@ -16137,6 +16147,77 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); + } else if (flashforge_local_api) { + auto* flashforge_host = dynamic_cast(upload_job.printhost.get()); + if (flashforge_host == nullptr) { + show_error(this, _L("Flashforge host is not available."), false); + return; + } + + std::vector slots; + bool supports_material_station = false; + { + wxBusyCursor wait; + wxString msg; + if (!flashforge_host->fetch_material_slots(slots, &supports_material_station, msg)) { + show_error(this, msg.empty() ? _L("Unable to log in to the Flashforge printer.") : msg, false); + return; + } + } + + std::vector project_filaments; + PlateDataPtrs plate_data_list; + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config(); + const auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + const auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + const int resolved_plate_idx = plate_idx == PLATE_CURRENT_IDX ? get_partplate_list().get_curr_plate_index() : plate_idx; + auto enrich_project_filaments = [&](std::vector& filaments) { + for (auto& filament : filaments) { + if (filament.id < 0) + continue; + + std::string display_filament_type; + try { + filament.type = cfg.get_filament_type(display_filament_type, filament.id); + } catch (...) { + } + + if (filament.type.empty()) + filament.type = display_filament_type; + if (filament.type.empty()) + filament.type = "Unknown"; + + filament.filament_id = filament_id_opt ? filament_id_opt->get_at(static_cast(filament.id)) : ""; + filament.color = filament_color ? filament_color->get_at(static_cast(filament.id)) : "#FFFFFF"; + if (filament.color.empty()) + filament.color = "#FFFFFF"; + } + }; + + p->partplate_list.store_to_3mf_structure(plate_data_list, true, plate_idx); + PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast(plate_data_list.size())) ? plate_data_list[resolved_plate_idx] : nullptr; + if (selected_plate_data == nullptr && !plate_data_list.empty()) + selected_plate_data = plate_data_list.front(); + + if (selected_plate_data != nullptr) + project_filaments = selected_plate_data->slice_filaments_info; + + if (project_filaments.empty()) { + if (PartPlate* plate = get_partplate_list().get_plate(resolved_plate_idx); plate != nullptr) + project_filaments = plate->get_slice_filaments_info(); + } + + if (!project_filaments.empty()) + enrich_project_filaments(project_filaments); + release_PlateData_list(plate_data_list); + + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, + config->get_bool("open_device_tab_post_upload"), + flashforge_host, + supports_material_station, + std::move(slots), + project_filaments); } else { pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); @@ -16166,7 +16247,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us if (use_3mf) { // Process gcode - const int result = send_gcode(plate_idx, nullptr); + const int export_plate_idx = plate_idx == PLATE_CURRENT_IDX ? get_partplate_list().get_curr_plate_index() : plate_idx; + const int result = send_gcode(export_plate_idx, nullptr); if (result < 0) { wxString msg = _L("Abnormal print file data. Please slice again"); diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp index f59ff23e9d..1d9b30fd1b 100644 --- a/src/slic3r/GUI/PrintHostDialogs.cpp +++ b/src/slic3r/GUI/PrintHostDialogs.cpp @@ -1,7 +1,9 @@ #include "PrintHostDialogs.hpp" #include +#include #include +#include #include #include @@ -11,6 +13,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -18,6 +23,8 @@ #include #include #include +#include +#include #include "GUI.hpp" #include "GUI_App.hpp" @@ -30,10 +37,383 @@ #include "format.hpp" namespace fs = boost::filesystem; +using json = nlohmann::json; namespace Slic3r { namespace GUI { +namespace { + +wxColour contrasting_text_color(const wxColour& background) +{ + return background.GetLuminance() < 0.60 ? *wxWHITE : wxColour("#303030"); +} + +long long color_distance_sq(const wxColour& lhs, const wxColour& rhs) +{ + const long long dr = static_cast(lhs.Red()) - static_cast(rhs.Red()); + const long long dg = static_cast(lhs.Green()) - static_cast(rhs.Green()); + const long long db = static_cast(lhs.Blue()) - static_cast(rhs.Blue()); + return dr * dr + dg * dg + db * db; +} + +class FlashforgeSlotCard : public wxPanel +{ +public: + FlashforgeSlotCard(wxWindow* parent) + : wxPanel(parent, wxID_ANY) + { + SetDoubleBuffered(true); + SetMinSize(wxSize(FromDIP(68), FromDIP(92))); + SetMaxSize(GetMinSize()); + Bind(wxEVT_PAINT, &FlashforgeSlotCard::on_paint, this); + Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { + m_hover = true; + SetCursor(wxCursor(m_enabled ? wxCURSOR_HAND : wxCURSOR_NO_ENTRY)); + Refresh(); + e.Skip(); + }); + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { + m_hover = false; + SetCursor(wxCursor(wxCURSOR_ARROW)); + Refresh(); + e.Skip(); + }); + Bind(wxEVT_LEFT_DOWN, &FlashforgeSlotCard::on_left_down, this); + } + + void set_slot(const Slic3r::FlashforgeMaterialSlot& slot, bool enabled) + { + m_slot_id = slot.slot_id; + m_color = parse_color(slot.material_color); + m_name = slot.material_name.empty() ? _L("Unknown") : from_u8(slot.material_name); + m_empty = !slot.has_filament; + m_enabled = enabled && !m_empty; + Refresh(); + } + +private: + static wxColour parse_color(const std::string& raw) + { + wxColour color(from_u8(raw)); + if (color.IsOk()) + return color; + + std::string value = raw; + boost::trim(value); + if (!value.empty() && value.front() != '#') + value.insert(value.begin(), '#'); + color = wxColour(from_u8(value)); + return color.IsOk() ? color : wxColour("#D0D0D0"); + } + + void on_left_down(wxMouseEvent& e) + { + if (!m_enabled) + return; + + wxCommandEvent evt(wxEVT_BUTTON, GetId()); + evt.SetInt(m_slot_id); + evt.SetString(m_color.GetAsString(wxC2S_HTML_SYNTAX)); + wxPostEvent(this, evt); + e.Skip(); + } + + void on_paint(wxPaintEvent&) + { + wxPaintDC dc(this); + std::unique_ptr gc(wxGraphicsContext::Create(dc)); + if (gc == nullptr) + return; + + const wxSize size = GetSize(); + const int circle_size = FromDIP(24); + const int outline_width = (m_hover && m_enabled) ? FromDIP(2) : FromDIP(1); + const wxRect body_rect(FromDIP(7), FromDIP(26), size.x - FromDIP(14), size.y - FromDIP(32)); + const wxColour badge_color = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT) + : wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); + const wxColour body_border = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW) + : wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); + + gc->SetPen(*wxTRANSPARENT_PEN); + gc->SetBrush(wxBrush((m_hover && m_enabled) ? badge_color.ChangeLightness(130) : badge_color)); + gc->DrawEllipse((size.x - circle_size) / 2.0, 0, circle_size, circle_size); + + dc.SetFont(::Label::Body_13); + dc.SetTextForeground(*wxWHITE); + const wxString slot_txt = wxString::Format("%d", m_slot_id); + const wxSize slot_size = dc.GetTextExtent(slot_txt); + dc.DrawText(slot_txt, (size.x - slot_size.x) / 2, (circle_size - slot_size.y) / 2); + + const wxColour bg_color = m_empty ? wxColour("#F6F6F6") : m_color; + gc->SetPen(wxPen(body_border, outline_width)); + gc->SetBrush(wxBrush(bg_color)); + gc->DrawRoundedRectangle(body_rect.x, body_rect.y, body_rect.width, body_rect.height, FromDIP(8)); + + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(contrasting_text_color(bg_color)); + + wxString label = m_empty ? _L("Empty") : m_name; + if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8)) + dc.SetFont(::Label::Body_10); + if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8)) { + while (!label.empty() && dc.GetTextExtent(label + "...").x > body_rect.width - FromDIP(8)) + label.RemoveLast(); + label += "..."; + } + const wxSize label_size = dc.GetTextExtent(label); + dc.DrawText(label, body_rect.x + (body_rect.width - label_size.x) / 2, body_rect.y + (body_rect.height - label_size.y) / 2); + } + +private: + int m_slot_id {0}; + wxColour m_color {*wxWHITE}; + wxString m_name; + bool m_empty {true}; + bool m_enabled {false}; + bool m_hover {false}; +}; + +class FlashforgeSlotDialog : public DPIDialog +{ +public: + FlashforgeSlotDialog(wxWindow* parent, const wxString& material_name) + : DPIDialog(parent, wxID_ANY, _L("Choose a slot for the selected color"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) + , m_material_name(material_name) + { + SetFont(wxGetApp().normal_font()); + SetBackgroundColour(*wxWHITE); + + auto* root = new wxBoxSizer(wxVERTICAL); + auto* title = new wxStaticText(this, wxID_ANY, _L("Material in the material station")); + title->SetFont(::Label::Head_13); + root->Add(title, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12)); + + m_grid = new wxGridSizer(1, 4, FromDIP(10), FromDIP(12)); + auto* grid_row = new wxBoxSizer(wxHORIZONTAL); + grid_row->AddStretchSpacer(); + grid_row->Add(m_grid, 0); + grid_row->AddStretchSpacer(); + root->Add(grid_row, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(18)); + + auto* tip = new wxStaticText(this, wxID_ANY, _L("Only materials of the same type can be selected.")); + tip->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + root->Add(tip, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12)); + + SetSizer(root); + SetMinSize(wxSize(FromDIP(460), FromDIP(240))); + + for (int i = 0; i < 4; ++i) { + auto* card = new FlashforgeSlotCard(this); + card->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + m_selected_slot_id = e.GetInt(); + m_selected_color = wxColour(e.GetString()); + EndModal(wxID_OK); + }); + m_cards.push_back(card); + m_grid->Add(card, 0); + } + + wxGetApp().UpdateDlgDarkUI(this); + Layout(); + Fit(); + CenterOnParent(); + Refresh(); + } + + void update_slots(const std::vector& slots, const std::function& matcher) + { + for (size_t i = 0; i < m_cards.size(); ++i) { + Slic3r::FlashforgeMaterialSlot slot; + slot.slot_id = static_cast(i) + 1; + if (const auto it = std::find_if(slots.begin(), slots.end(), [&](const Slic3r::FlashforgeMaterialSlot& item) { return item.slot_id == slot.slot_id; }); it != slots.end()) + slot = *it; + const bool enabled = slot.has_filament && matcher(slot); + m_cards[i]->set_slot(slot, enabled); + } + Layout(); + Fit(); + } + + bool has_selection() const { return m_selected_slot_id > 0; } + int selected_slot_id() const { return m_selected_slot_id; } + wxColour selected_color() const { return m_selected_color; } + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override + { + Fit(); + Refresh(); + if (suggested_rect.IsEmpty()) + return; + SetSize(suggested_rect.GetSize()); + } + +private: + wxString m_material_name; + wxGridSizer* m_grid {nullptr}; + std::vector m_cards; + int m_selected_slot_id {0}; + wxColour m_selected_color; +}; + +class FlashforgeMaterialMapWidget : public wxPanel +{ +public: + using SelectFn = std::function; + + FlashforgeMaterialMapWidget(wxWindow* parent, int tool_id, const wxColour& color, const wxString& material_name, SelectFn on_select) + : wxPanel(parent, wxID_ANY) + , m_tool_id(tool_id) + , m_color(color) + , m_name(material_name.Strip()) + , m_select_fn(std::move(on_select)) + { + SetDoubleBuffered(true); + const wxSize size(FromDIP(72), FromDIP(58)); + SetSize(size); + SetMinSize(size); + SetMaxSize(size); + Bind(wxEVT_PAINT, &FlashforgeMaterialMapWidget::on_paint, this); + Bind(wxEVT_LEFT_DOWN, &FlashforgeMaterialMapWidget::on_left_down, this); + + } + + int tool_id() const { return m_tool_id; } + int selected_slot_id() const { return m_slot_id; } + bool is_slot_selected() const { return m_slot_id > 0; } + wxString material_name() const { return m_name; } + + void set_enable_mapping(bool enable) + { + if (m_mapping_enabled == enable) + return; + m_mapping_enabled = enable; + if (!enable) + reset_slot(); + Enable(enable); + Refresh(); + } + + void set_slot_selection(int slot_id, const wxColour& slot_color) + { + m_slot_id = slot_id; + m_slot_color = slot_color; + Refresh(); + } + + void reset_slot() + { + m_slot_id = 0; + m_slot_color = wxColour("#DDDDDD"); + Refresh(); + } + + void update_popup_slots(const std::vector& slots, const std::function& matcher) + { + m_slots_snapshot = slots; + m_matcher = matcher; + } + + wxSize DoGetBestSize() const override + { + return wxSize(FromDIP(72), FromDIP(58)); + } + +private: + void on_left_down(wxMouseEvent& e) + { + if (!m_mapping_enabled) + return; + + FlashforgeSlotDialog dlg(this, m_name); + dlg.update_slots(m_slots_snapshot, m_matcher); + m_selected = true; + Refresh(); + if (dlg.ShowModal() == wxID_OK && dlg.has_selection()) { + m_slot_id = dlg.selected_slot_id(); + m_slot_color = dlg.selected_color(); + if (m_select_fn) + m_select_fn(this); + } + m_selected = false; + Refresh(); + e.Skip(); + } + + void on_paint(wxPaintEvent&) + { + wxPaintDC dc(this); + std::unique_ptr gc(wxGraphicsContext::Create(dc)); + if (gc == nullptr) + return; + + const wxSize size = GetSize(); + const int half_h = size.y / 2; + gc->SetPen(*wxTRANSPARENT_PEN); + gc->SetBrush(wxBrush(m_color)); + gc->DrawRoundedRectangle(0, 0, size.x, half_h, FromDIP(3)); + gc->DrawRectangle(0, half_h - FromDIP(3), size.x, FromDIP(3)); + + gc->SetBrush(wxBrush(m_mapping_enabled ? m_slot_color : wxColour("#DDDDDD"))); + gc->DrawRoundedRectangle(0, half_h, size.x, half_h, FromDIP(3)); + gc->DrawRectangle(0, half_h, size.x, FromDIP(3)); + + if (m_selected) { + gc->SetPen(wxPen(wxColour("#00AE42"), FromDIP(2))); + gc->SetBrush(*wxTRANSPARENT_BRUSH); + gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3)); + } else if (m_color.GetLuminance() > 0.95 || m_slot_color.GetLuminance() > 0.95) { + gc->SetPen(wxPen(wxColour("#ACACAC"), FromDIP(1))); + gc->SetBrush(*wxTRANSPARENT_BRUSH); + gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3)); + } + + dc.SetFont(::Label::Body_13); + dc.SetTextForeground(contrasting_text_color(m_color)); + wxString top_text = m_name; + if (dc.GetTextExtent(top_text).x > size.x - FromDIP(10)) { + dc.SetFont(::Label::Body_10); + } + wxSize top_size = dc.GetTextExtent(top_text); + dc.DrawText(top_text, (size.x - top_size.x) / 2, (half_h - top_size.y) / 2); + + dc.SetFont(::Label::Body_13); + dc.SetTextForeground(contrasting_text_color(m_slot_color)); + const wxString bottom_text = m_slot_id > 0 ? wxString::Format("%d", m_slot_id) : "-"; + const wxSize bottom_size = dc.GetTextExtent(bottom_text); + dc.DrawText(bottom_text, (size.x - bottom_size.x - FromDIP(10)) / 2, half_h + (half_h - bottom_size.y) / 2); + + wxPoint pts[3] = { + wxPoint(size.x - FromDIP(18), half_h + half_h / 2 - FromDIP(2)), + wxPoint(size.x - FromDIP(10), half_h + half_h / 2 - FromDIP(2)), + wxPoint(size.x - FromDIP(14), half_h + half_h / 2 + FromDIP(3)) + }; + dc.SetBrush(wxBrush(contrasting_text_color(m_slot_color))); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawPolygon(3, pts); + } + +private: + int m_tool_id {-1}; + wxColour m_color; + wxString m_name; + wxColour m_slot_color {wxColour("#DDDDDD")}; + int m_slot_id {0}; + bool m_selected {false}; + bool m_mapping_enabled {true}; + SelectFn m_select_fn; + std::vector m_slots_snapshot; + std::function m_matcher; +}; + +static FlashforgeMaterialMapWidget* as_ff_map_widget(wxWindow* window) +{ + return dynamic_cast(window); +} + +} // namespace + static const char *CONFIG_KEY_PATH = "printhost_path"; static const char *CONFIG_KEY_GROUP = "printhost_group"; static const char* CONFIG_KEY_STORAGE = "printhost_storage"; @@ -257,6 +637,528 @@ void PrintHostSendDialog::EndModal(int ret) MsgDialog::EndModal(ret); } +FlashforgePrintHostSendDialog::FlashforgePrintHostSendDialog(const fs::path& path, + PrintHostPostUploadActions post_actions, + const wxArrayString& groups, + const wxArrayString& storage_paths, + const wxArrayString& storage_names, + bool switch_to_device_tab, + const Slic3r::Flashforge* host, + bool supports_material_station, + std::vector slots, + const std::vector& project_filaments) + : PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab) + , m_host(host) + , m_slots(std::move(slots)) + , m_project_filaments(project_filaments) +{ + m_supports_material_station = supports_material_station; + m_slots_loaded = !m_slots.empty(); +} + +void FlashforgePrintHostSendDialog::init() +{ + const AppConfig* app_config = wxGetApp().app_config; + const auto& path = m_path; + + std::string leveling = app_config->get("recent", CONFIG_KEY_LEVELING); + if (!leveling.empty()) + m_leveling_before_print = leveling == "1"; + + std::string timelapse = app_config->get("recent", CONFIG_KEY_TIMELAPSE); + if (!timelapse.empty()) + m_time_lapse_video = timelapse == "1"; + + // Flashforge local printing should default to IFS enabled when supported. + // We don't revive an old stale "0" here. + m_use_material_station = m_supports_material_station; + if (m_supports_material_station && !app_config->has("recent", CONFIG_KEY_IFS)) + const_cast(app_config)->set("recent", CONFIG_KEY_IFS, "1"); + + this->SetMinSize(wxSize(560, 420)); + + auto* label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed.")); + label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); + content_sizer->Add(txt_filename, 0, wxEXPAND); + content_sizer->Add(label_dir_hint); + content_sizer->AddSpacer(VERT_SPACING); + + wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH)); + if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/') + recent_path += '/'; + const auto recent_path_len = recent_path.Length(); + recent_path += path.filename().wstring(); + wxString stem(path.stem().wstring()); + const auto stem_len = stem.Length(); + txt_filename->SetValue(recent_path); + + { + auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL); + auto checkbox = new ::CheckBox(this, wxID_APPLY); + checkbox->SetValue(m_switch_to_device_tab); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { + auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject()); + if (source != nullptr) + source->SetValue(e.IsChecked()); + m_switch_to_device_tab = e.IsChecked(); + e.Skip(); + }); + checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + + auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload.")); + checkbox_text->SetFont(::Label::Body_13); + checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + content_sizer->Add(checkbox_sizer); + content_sizer->AddSpacer(VERT_SPACING); + } + + m_flashforge_options_sizer = new wxBoxSizer(wxVERTICAL); + + auto add_option_checkbox = [this](wxBoxSizer* parent, const wxString& label, bool value, std::function setter, ::CheckBox** out = nullptr) { + auto row = new wxBoxSizer(wxHORIZONTAL); + auto checkbox = new ::CheckBox(this); + checkbox->SetValue(value); + checkbox->Bind(wxEVT_TOGGLEBUTTON, [setter](wxCommandEvent& e) { + auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject()); + if (source != nullptr) + source->SetValue(e.IsChecked()); + setter(e.IsChecked()); + e.Skip(); + }); + row->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + + auto text = new wxStaticText(this, wxID_ANY, label); + text->SetFont(::Label::Body_13); + text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D"))); + row->Add(text, 0, wxALL | wxALIGN_CENTER, FromDIP(2)); + parent->Add(row); + parent->AddSpacer(FromDIP(6)); + + if (out != nullptr) + *out = checkbox; + }; + + add_option_checkbox(m_flashforge_options_sizer, _L("Leveling before print"), m_leveling_before_print, + [this](bool checked) { m_leveling_before_print = checked; }, &m_checkbox_leveling); + add_option_checkbox(m_flashforge_options_sizer, _L("Time-lapse"), m_time_lapse_video, + [this](bool checked) { m_time_lapse_video = checked; }, &m_checkbox_timelapse); + add_option_checkbox(m_flashforge_options_sizer, _L("Enable IFS"), m_use_material_station, + [this](bool checked) { + m_use_material_station = checked; + if (checked) { + ensure_slots_loaded(); + rebuild_mapping_rows(); + } + sync_mapping_section_visibility(); + }, &m_checkbox_ifs); + + if (m_checkbox_ifs != nullptr && !m_supports_material_station) + m_checkbox_ifs->Enable(false); + + m_status_text = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_status_text->SetFont(::Label::Body_12); + m_flashforge_options_sizer->Add(m_status_text, 0, wxTOP | wxBOTTOM, FromDIP(4)); + + m_mapping_section_sizer = new wxBoxSizer(wxVERTICAL); + m_mapping_wrap_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS); + m_mapping_section_sizer->Add(m_mapping_wrap_sizer, 0, wxTOP | wxALIGN_LEFT, FromDIP(10)); + m_flashforge_options_sizer->Add(m_mapping_section_sizer, 0, wxEXPAND); + + content_sizer->Add(m_flashforge_options_sizer, 0, wxEXPAND); + + if (m_supports_material_station) + m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast(m_slots.size()))); + else + m_status_text->SetLabel(_L("This printer does not report a material station.")); + + rebuild_mapping_rows(); + sync_mapping_section_visibility(); + + if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos) + m_valid_suffix = recent_path.substr(extension_start); + + auto validate_path = [this](const wxString& filename) -> bool { + if (!filename.Lower().EndsWith(m_valid_suffix.Lower())) { + MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix), + wxString(SLIC3R_APP_NAME), wxYES | wxNO); + if (msg_wingow.ShowModal() == wxID_NO) + return false; + } + return validate_before_close(); + }; + + auto* btn_ok = add_button(wxID_OK, true, _L("Upload")); + btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { + if (validate_path(txt_filename->GetValue())) { + post_upload_action = PrintHostPostUploadAction::None; + EndDialog(wxID_OK); + } + }); + + if (m_post_actions.has(PrintHostPostUploadAction::StartPrint)) { + auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print")); + btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) { + if (validate_path(txt_filename->GetValue())) { + post_upload_action = PrintHostPostUploadAction::StartPrint; + EndDialog(wxID_OK); + } + }); + } + + add_button(wxID_CANCEL, false, _L("Cancel")); + finalize(); + txt_filename->SetFocus(); + +#ifdef __linux__ + txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) { + e.Skip(); + txt_filename->SetInsertionPoint(txt_filename->GetLastPosition()); + }, txt_filename->GetId()); +#endif /* __linux__ */ + + Bind(wxEVT_SHOW, [=](const wxShowEvent&) { + CallAfter([=]() { + txt_filename->SetInsertionPoint(0); + txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len); + }); + }); +} + +void FlashforgePrintHostSendDialog::EndModal(int ret) +{ + if (ret == wxID_OK) { + AppConfig* app_config = wxGetApp().app_config; + app_config->set("recent", CONFIG_KEY_LEVELING, m_leveling_before_print ? "1" : "0"); + app_config->set("recent", CONFIG_KEY_TIMELAPSE, m_time_lapse_video ? "1" : "0"); + app_config->set("recent", CONFIG_KEY_IFS, m_use_material_station ? "1" : "0"); + } + + PrintHostSendDialog::EndModal(ret); +} + +std::map FlashforgePrintHostSendDialog::extendedInfo() const +{ + json mappings = json::array(); + int mapped_count = 0; + + if (m_use_material_station) { + for (const auto& row : m_mapping_rows) { + auto* card = as_ff_map_widget(row.card); + if (card == nullptr || row.tool_id < 0) + continue; + + const int slot_id = card->selected_slot_id(); + if (slot_id <= 0) + continue; + + const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& item) { return item.id == row.tool_id; }); + const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return slot.slot_id == slot_id; }); + if (filament_it == m_project_filaments.end() || slot_it == m_slots.end()) + continue; + + mappings.push_back({ + {"toolId", filament_it->id}, + {"slotId", slot_it->slot_id}, + {"materialName", slot_it->material_name}, + {"toolMaterialColor", filament_it->color}, + {"slotMaterialColor", slot_it->material_color} + }); + ++mapped_count; + } + } + + return { + {"levelingBeforePrint", m_leveling_before_print ? "1" : "0"}, + {"timeLapseVideo", m_time_lapse_video ? "1" : "0"}, + {"useMatlStation", m_use_material_station ? "1" : "0"}, + {"gcodeToolCnt", std::to_string(mapped_count)}, + {"materialMappings", mappings.dump()} + }; +} + +void FlashforgePrintHostSendDialog::load_slots() +{ + m_slots.clear(); + m_slots_loaded = false; + m_supports_material_station = false; + + if (m_host == nullptr) { + m_status_text->SetLabel(_L("Flashforge host is not available.")); + return; + } + + wxString msg; + bool supports_material_station = false; + if (!m_host->fetch_material_slots(m_slots, &supports_material_station, msg)) { + m_status_text->SetLabel(msg.empty() ? _L("Unable to read IFS slots from printer.") : msg); + return; + } + + m_supports_material_station = supports_material_station; + m_slots_loaded = !m_slots.empty(); + m_use_material_station = m_supports_material_station; + + if (m_supports_material_station) + m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast(m_slots.size()))); + else + m_status_text->SetLabel(_L("This printer does not report a material station.")); +} + +bool FlashforgePrintHostSendDialog::ensure_slots_loaded(bool force_reload) +{ + if (!force_reload && (m_slots_loaded || !m_supports_material_station)) + return m_slots_loaded; + + if (m_status_text != nullptr) + m_status_text->SetLabel(_L("Loading IFS slots from printer...")); + + wxBusyCursor wait; + load_slots(); + return m_slots_loaded; +} + +void FlashforgePrintHostSendDialog::rebuild_mapping_rows() +{ + if (m_mapping_wrap_sizer == nullptr) + return; + + m_mapping_wrap_sizer->Clear(true); + m_mapping_rows.clear(); + + if (m_project_filaments.empty()) { + m_mapping_wrap_sizer->Add(new wxStaticText(this, wxID_ANY, _L("Slice the plate first to get project material information.")), 0, wxALL, FromDIP(2)); + return; + } + + for (const auto& filament : m_project_filaments) { + auto* card = new FlashforgeMaterialMapWidget(this, filament.id, to_wx_colour(filament.color), from_u8(filament.get_display_filament_type()), + [this](FlashforgeMaterialMapWidget* changed_card) { + if (changed_card == nullptr) + return; + for (auto& row : m_mapping_rows) { + if (row.card == changed_card) { + refresh_mapping_card(row); + break; + } + } + }); + m_mapping_wrap_sizer->Add(card, 0, wxRIGHT | wxBOTTOM | wxFIXED_MINSIZE, FromDIP(10)); + + MappingRow row; + row.tool_id = filament.id; + row.card = card; + m_mapping_rows.push_back(row); + } + + auto_assign_mappings(); +} + +void FlashforgePrintHostSendDialog::auto_assign_mappings() +{ + for (size_t idx = 0; idx < m_project_filaments.size() && idx < m_mapping_rows.size(); ++idx) { + auto& filament = m_project_filaments[idx]; + auto* card = as_ff_map_widget(m_mapping_rows[idx].card); + if (card == nullptr) + continue; + + const wxColour filament_color = to_wx_colour(filament.color); + const Slic3r::FlashforgeMaterialSlot* best_slot = nullptr; + long long best_distance = std::numeric_limits::max(); + + for (const auto& slot : m_slots) { + if (!slot.has_filament || !slot_matches_filament(slot, filament)) + continue; + + const long long distance = color_distance_sq(filament_color, to_wx_colour(slot.material_color)); + if (best_slot == nullptr || distance < best_distance) { + best_slot = &slot; + best_distance = distance; + } + } + + if (best_slot != nullptr) + card->set_slot_selection(best_slot->slot_id, to_wx_colour(best_slot->material_color)); + else + card->reset_slot(); + + refresh_mapping_card(m_mapping_rows[idx]); + } +} + +void FlashforgePrintHostSendDialog::refresh_mapping_card(MappingRow& row) +{ + auto* card = as_ff_map_widget(row.card); + if (card == nullptr) + return; + + const auto* filament = find_filament_by_tool_id(row.tool_id); + card->set_enable_mapping(m_use_material_station); + card->update_popup_slots(m_slots, [this, filament](const FlashforgeMaterialSlot& slot) { + return filament != nullptr && slot_matches_filament(slot, *filament); + }); + + if (card->selected_slot_id() <= 0) { + card->reset_slot(); + return; + } + + const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id())); + if (slot == nullptr) { + card->reset_slot(); + return; + } + + card->set_slot_selection(slot->slot_id, to_wx_colour(slot->material_color)); +} + +void FlashforgePrintHostSendDialog::sync_mapping_section_visibility() +{ + if (m_mapping_section_sizer == nullptr) + return; + + m_mapping_section_sizer->ShowItems(m_use_material_station && m_supports_material_station); + if (wxSizer* sizer = GetSizer(); sizer != nullptr) { + sizer->Layout(); + sizer->Fit(this); + SetMinSize(GetBestSize()); + } + Layout(); + Fit(); +} + +const Slic3r::FlashforgeMaterialSlot* FlashforgePrintHostSendDialog::find_slot_by_id(const std::string& slot_id_text) const +{ + const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return std::to_string(slot.slot_id) == slot_id_text; }); + return slot_it == m_slots.end() ? nullptr : &(*slot_it); +} + +const FilamentInfo* FlashforgePrintHostSendDialog::find_filament_by_tool_id(int tool_id) const +{ + const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& filament) { return filament.id == tool_id; }); + return filament_it == m_project_filaments.end() ? nullptr : &(*filament_it); +} + +bool FlashforgePrintHostSendDialog::slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const +{ + if (!slot.has_filament) + return false; + + const std::string project_material = normalize_material(!filament.type.empty() ? filament.type : filament.get_display_filament_type()); + const std::string slot_material = normalize_material(slot.material_name); + return !project_material.empty() && !slot_material.empty() && project_material == slot_material; +} + +bool FlashforgePrintHostSendDialog::validate_before_close() +{ + if (!m_use_material_station && m_project_filaments.size() > 1) { + show_error(this, _L("This plate uses multiple materials. Enable IFS and assign each tool to a printer slot.")); + return false; + } + + if (!m_use_material_station) + return true; + + for (const auto& row : m_mapping_rows) { + auto* card = as_ff_map_widget(row.card); + if (card == nullptr || !card->is_slot_selected()) { + show_error(this, _L("Each project material must be assigned to an IFS slot before printing.")); + return false; + } + + const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id())); + const auto* filament = find_filament_by_tool_id(row.tool_id); + if (slot == nullptr || filament == nullptr || !slot->has_filament) { + show_error(this, _L("Each project material must be assigned to a loaded IFS slot before printing.")); + return false; + } + + if (!slot_matches_filament(*slot, *filament)) { + show_error(this, _L("Each project material must match the material loaded in the selected IFS slot.")); + return false; + } + } + + return true; +} + +std::string FlashforgePrintHostSendDialog::normalize_material(const std::string& material) const +{ + std::string normalized = boost::to_upper_copy(material); + normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char ch) { return !std::isalnum(ch); }), normalized.end()); + + if (normalized.empty()) + return {}; + + if (normalized.find("SILK") != std::string::npos) + return "SILK"; + + if (normalized.find("PLA") != std::string::npos && normalized.find("CF") != std::string::npos) + return "PLACF"; + if (normalized.find("PETG") != std::string::npos && normalized.find("CF") != std::string::npos) + return "PETGCF"; + + if (normalized == "PLA" || normalized == "PLA+" || normalized == "PLAPLUS") + return "PLA"; + if (normalized.find("PLA") != std::string::npos) + return "PLA"; + + if (normalized == "ABS" || normalized.find("ABS") != std::string::npos) + return "ABS"; + if (normalized == "ASA" || normalized.find("ASA") != std::string::npos) + return "ABS"; + + if (normalized.find("PETG") != std::string::npos) + return "PETG"; + + if (normalized.find("TPU") != std::string::npos || normalized.find("TPE") != std::string::npos || normalized.find("FLEX") != std::string::npos) + return "TPU"; + + return normalized; +} + +wxColour FlashforgePrintHostSendDialog::to_wx_colour(const std::string& color) const +{ + wxColour wx_color(from_u8(color)); + if (wx_color.IsOk()) + return wx_color; + + std::string normalized = boost::trim_copy(color); + if (boost::istarts_with(normalized, "0x")) + normalized = normalized.substr(2); + if (!normalized.empty() && normalized.front() == '#') + normalized.erase(normalized.begin()); + + if (normalized.size() == 8) { + auto hex_to_byte = [](char hi, char lo) -> int { + auto hex_val = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + const int h = hex_val(hi); + const int l = hex_val(lo); + return (h < 0 || l < 0) ? -1 : h * 16 + l; + }; + + const int r = hex_to_byte(normalized[0], normalized[1]); + const int g = hex_to_byte(normalized[2], normalized[3]); + const int b = hex_to_byte(normalized[4], normalized[5]); + const int a = hex_to_byte(normalized[6], normalized[7]); + if (r >= 0 && g >= 0 && b >= 0 && a >= 0) + return wxColour(r, g, b, a); + } + + if (normalized.size() == 6) { + wx_color = wxColour("#" + from_u8(normalized)); + if (wx_color.IsOk()) + return wx_color; + } + + return wxColour("#999999"); +} + wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event); wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event); diff --git a/src/slic3r/GUI/PrintHostDialogs.hpp b/src/slic3r/GUI/PrintHostDialogs.hpp index e648537ccb..cc4670f514 100644 --- a/src/slic3r/GUI/PrintHostDialogs.hpp +++ b/src/slic3r/GUI/PrintHostDialogs.hpp @@ -1,8 +1,8 @@ #ifndef slic3r_PrintHostSendDialog_hpp_ #define slic3r_PrintHostSendDialog_hpp_ -#include #include +#include #include #include @@ -12,12 +12,18 @@ #include "GUI_Utils.hpp" #include "MsgDialog.hpp" #include "../Utils/PrintHost.hpp" +#include "../Utils/Flashforge.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/ProjectTask.hpp" class wxButton; class wxTextCtrl; -class wxChoice; class wxComboBox; +class ComboBox; class wxDataViewListCtrl; +class wxFlexGridSizer; +class wxStaticText; +class wxWrapSizer; +class CheckBox; namespace Slic3r { @@ -180,6 +186,66 @@ private: BedType m_BedType; }; +class FlashforgePrintHostSendDialog : public PrintHostSendDialog +{ +public: + FlashforgePrintHostSendDialog(const boost::filesystem::path& path, + PrintHostPostUploadActions post_actions, + const wxArrayString& groups, + const wxArrayString& storage_paths, + const wxArrayString& storage_names, + bool switch_to_device_tab, + const Slic3r::Flashforge* host, + bool supports_material_station, + std::vector slots, + const std::vector& project_filaments); + + virtual void init() override; + virtual void EndModal(int ret) override; + virtual std::map extendedInfo() const override; + +private: + struct MappingRow { + int tool_id {-1}; + wxWindow* card {nullptr}; + }; + + void load_slots(); + bool ensure_slots_loaded(bool force_reload = false); + void rebuild_mapping_rows(); + void auto_assign_mappings(); + void refresh_mapping_card(MappingRow& row); + void sync_mapping_section_visibility(); + const Slic3r::FlashforgeMaterialSlot* find_slot_by_id(const std::string& slot_id_text) const; + const FilamentInfo* find_filament_by_tool_id(int tool_id) const; + bool slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const; + bool validate_before_close(); + std::string normalize_material(const std::string& material) const; + wxColour to_wx_colour(const std::string& color) const; + +private: + const Slic3r::Flashforge* m_host {nullptr}; + std::vector m_project_filaments; + std::vector m_slots; + std::vector m_mapping_rows; + wxBoxSizer* m_flashforge_options_sizer {nullptr}; + wxBoxSizer* m_mapping_section_sizer {nullptr}; + wxWrapSizer* m_mapping_wrap_sizer {nullptr}; + wxStaticText* m_status_text {nullptr}; + ::CheckBox* m_checkbox_leveling {nullptr}; + ::CheckBox* m_checkbox_timelapse {nullptr}; + ::CheckBox* m_checkbox_ifs {nullptr}; + bool m_leveling_before_print {true}; + bool m_time_lapse_video {false}; + bool m_use_material_station {false}; + bool m_supports_material_station {false}; + bool m_slots_loaded {false}; + + const char* CONFIG_KEY_LEVELING = "flashforge_leveling_before_print"; + const char* CONFIG_KEY_TIMELAPSE = "flashforge_timelapse_video"; + const char* CONFIG_KEY_IFS = "flashforge_use_material_station"; +}; + wxDECLARE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event); wxDECLARE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event); wxDECLARE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event); diff --git a/src/slic3r/Utils/Flashforge.cpp b/src/slic3r/Utils/Flashforge.cpp index 6fe3a83083..88b3eb3f69 100644 --- a/src/slic3r/Utils/Flashforge.cpp +++ b/src/slic3r/Utils/Flashforge.cpp @@ -1,8 +1,13 @@ #include "Flashforge.hpp" #include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -19,6 +24,10 @@ #include #include +#include +#include +#include + #include "libslic3r/PrintConfig.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" @@ -30,20 +39,298 @@ 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 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& response, const std::string& ip_address, FlashforgeDiscoveredPrinter& printer) +{ + if (response.size() < 0xC4) + return false; + + printer.name = trim_null_terminated_ascii(reinterpret_cast(response.data()), 32); + printer.serial_number = trim_null_terminated_ascii(reinterpret_cast(response.data() + 0x92), 32); + printer.ip_address = ip_address; + return !(printer.name.empty() && printer.serial_number.empty()); +} + +std::vector get_discovery_broadcast_addresses() +{ + std::set 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(bytes[0]) % static_cast(bytes[1]) % static_cast(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(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(); + return true; + } + + if (value.is_boolean()) { + out = value.get() ? 1 : 0; + return true; + } + + if (value.is_string()) { + std::string text = value.get(); + 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(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(); + else if (parsed.contains("msg") && parsed["msg"].is_string()) + message = parsed["msg"].get(); + + 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(config->opt_string("print_host")) + : m_host() + , m_serial_number() + , m_check_code() , m_console_port("8899") - , m_gcFlavor(config->option>("gcode_flavor")->value) + , 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>("gcode_flavor"); gcode_flavor != nullptr) + m_gcFlavor = gcode_flavor->value; + } +} const char* Flashforge::get_name() const { return "Flashforge"; } +bool Flashforge::discover_printers(std::vector& 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 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(now - start).count() >= timeout_ms) + break; + if (!by_ip.empty() && std::chrono::duration_cast(now - last_reply).count() >= idle_timeout_ms) + break; + + std::vector 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); @@ -58,11 +345,19 @@ bool Flashforge::test(wxString& msg) const return res; } -wxString Flashforge::get_test_ok_msg() const { return _(L("Serial connection to Flashforge is working correctly.")); } +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 { - return GUI::from_u8((boost::format("%s: %s") % _utf8(L("Could not connect to Flashforge via serial")) % std::string(msg.ToUTF8())).str()); + 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()); } @@ -98,21 +393,25 @@ bool Flashforge::connect(wxString& msg) const bool Flashforge::start_print(wxString& msg, const std::string& filename) const { Utils::TCPConsole client(m_host, m_console_port); - Slic3r::Utils::SerialMessage startPrintCommand = {(boost::format("~M23 0:/user/%1%") % filename).str(), Slic3r::Utils::Command}; + 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%") % filename; + 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%") % filename; + 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; @@ -121,6 +420,8 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err 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 @@ -142,7 +443,7 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err newfile.close(); // close the file object. } Slic3r::Utils::SerialMessage fileuploadCommand = - {(boost::format("~M28 %1% 0:/user/%2%") % gcodeFile.size() % upload_data.upload_path.generic_string()).str(), + {(boost::format("~M28 %1% 0:/user/%2%") % gcodeFile.size() % upload_filename).str(), Slic3r::Utils::Command}; client.enqueue_cmd(fileuploadCommand); @@ -178,7 +479,7 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err res = client.run_queue(); if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) - res = start_print(errormsg, upload_data.upload_path.string()); + res = start_print(errormsg, upload_filename); } } catch (const std::exception& e) { @@ -190,6 +491,185 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err 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& 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; + + const auto parsed = json::parse(body, nullptr, false, true); + if (parsed.is_discarded()) { + msg = _(L("Flashforge returned an invalid JSON response.")); + 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; + + for (const auto& slot : slot_infos) { + FlashforgeMaterialSlot info; + info.slot_id = slot.value("slotId", static_cast(slots.size()) + 1); + info.has_filament = slot.value("hasFilament", false); + info.material_name = slot.value("materialName", std::string()); + info.material_color = slot.value("materialColor", 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; +} + int Flashforge::get_err_code_from_body(const std::string& body) const { pt::ptree root; diff --git a/src/slic3r/Utils/Flashforge.hpp b/src/slic3r/Utils/Flashforge.hpp index 82f9092753..ea7acdcb11 100644 --- a/src/slic3r/Utils/Flashforge.hpp +++ b/src/slic3r/Utils/Flashforge.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_FlashForge_hpp_ #define slic3r_FlashForge_hpp_ +#include #include #include #include "PrintHost.hpp" @@ -12,6 +13,21 @@ namespace Slic3r { class DynamicPrintConfig; class Http; +struct FlashforgeMaterialSlot +{ + int slot_id {0}; // API is 1-based. + bool has_filament {false}; + std::string material_name; + std::string material_color; +}; + +struct FlashforgeDiscoveredPrinter +{ + std::string name; + std::string serial_number; + std::string ip_address; +}; + class Flashforge : public PrintHost { public: @@ -24,13 +40,17 @@ public: 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, InfoFn info_fn) const override; - bool has_auto_discovery() const override { return false; } + bool has_auto_discovery() const override { return true; } bool can_test() const override { return true; } PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } std::string get_host() const override { return m_host; } + bool fetch_material_slots(std::vector& slots, bool* supports_material_station, wxString& msg) const; + static bool discover_printers(std::vector& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3); private: std::string m_host; + std::string m_serial_number; + std::string m_check_code; std::string m_console_port; const int m_bufferSize; GCodeFlavor m_gcFlavor; @@ -43,6 +63,11 @@ private: Slic3r::Utils::SerialMessage tempStatusCommand = {"~M105\r\n", Slic3r::Utils::Command}; Slic3r::Utils::SerialMessage printStatusCommand = {"~M27\r\n", Slic3r::Utils::Command}; Slic3r::Utils::SerialMessage saveFileCommand = {"~M29\r\n",Slic3r::Utils::Command}; + bool upload_local_api(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn) const; + bool test_local_api(wxString& msg) const; + bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const; + std::string make_http_url(const std::string& path) const; + std::string extract_host_name() const; int get_err_code_from_body(const std::string &body) const; bool connect(wxString& msg) const; bool start_print(wxString& msg, const std::string& filename) const;