Compare commits

..

7 Commits

Author SHA1 Message Date
Ian Chua
56236f56a8 Add unsupported-command feedback to the device UI 2026-08-04 21:26:56 +08:00
Ian Chua
df5a08517a Keep printer-agent error codes with the interface 2026-08-04 19:44:39 +08:00
Andrew
5d953f915a Keep Bambu AMS dialect out of the agent waist
M620 is Bambu firmware dialect, not a
neutral command. Composing it in
MachineObject let non-Bambu agents
(Moonraker/Klipper) forward it and
report success on firmware that
cannot run it.

Agents now own the dialect: the
default refusal on IPrinterAgent
returns not-supported so the UI
can say so; BBLPrinterAgent keeps
the byte-identical composition.
2026-08-04 18:12:20 +08:00
Andrew
dd2cb92685 Gate agent mode behind use_printer_agents toggle
Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.
2026-08-04 18:12:20 +08:00
Andrew
b2f08c3ff8 Reset device selection on agent swap or unload (#124)
set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.
2026-08-04 18:12:19 +08:00
Andrew
75a2460649 Replace fake-enum printer agent dropdown (#121)
A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.
2026-08-04 18:12:19 +08:00
Ian Chua
01493d4e3a Add developer flag for printer agents 2026-08-04 18:12:19 +08:00
24 changed files with 668 additions and 304 deletions

View File

@@ -2273,6 +2273,8 @@ public:
plugin_picker,
// Raw JSON string value, edited through a dialog behind a button rather than in the row.
plugin_config,
// PrinterAgentChoice
printer_agent_select,
};
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.

View File

@@ -496,6 +496,26 @@ namespace Slic3r
OnSelectedMachineChanged(previous_selected_machine, selected_machine);
}
void DeviceManager::clear_other_devices()
{
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices"
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own.
const auto my = get_my_machine_list();
for (auto it = localMachineList.begin(); it != localMachineList.end();)
{
if (my.find(it->first) == my.end())
{
// not a "My Device" -> an "Other Device"
delete it->second;
it = localMachineList.erase(it);
}
else
{
++it;
}
}
}
bool DeviceManager::set_selected_machine(std::string dev_id)
{
BOOST_LOG_TRIVIAL(info) << "set_selected_machine=" << dev_id
@@ -558,7 +578,6 @@ namespace Slic3r
}
else
{
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
if (m_agent)
{
if (it->second->connection_type() != "lan" || it->second->connection_type().empty())
@@ -592,7 +611,6 @@ namespace Slic3r
}
selected_machine = dev_id;
record_user_last_machine(selected_machine);
return true;
}
@@ -851,44 +869,26 @@ namespace Slic3r
int result = m_agent->get_user_print_info(&http_code, &body, provider);
if (result == 0)
{
parse_user_print_info(body);
// parse_user_print_info and on_machine_alive (SSDP for discovery) both mutate the same userMachineList map.
// on_machine_alive mutates the map on the UI thread, do the same for parse_user_print_info.
Slic3r::GUI::wxGetApp().CallAfter([this, body]() { parse_user_print_info(body); });
}
}
void DeviceManager::record_user_last_machine(const std::string& dev_id)
{
if (Slic3r::GUI::wxGetApp().app_config) {
Slic3r::GUI::wxGetApp().app_config->set("user_last_selected_machine", dev_id);
}
}
std::string DeviceManager::get_user_last_machine() const
{
if (Slic3r::GUI::wxGetApp().app_config) {
const auto& user_last_machine = Slic3r::GUI::wxGetApp().app_config->get("user_last_selected_machine");
if (!user_last_machine.empty()) {
return user_last_machine;
} else if (m_agent) {
return m_agent->get_user_selected_machine();
}
}
return "";
}
void DeviceManager::load_last_machine()
{
if (userMachineList.empty()) return;
else if (userMachineList.size() == 1) {
this->set_selected_machine(userMachineList.begin()->second->get_dev_id());
} else {
const auto& last_monitor_machine = get_user_last_machine();
if (userMachineList.find(last_monitor_machine) != userMachineList.end()) {
set_selected_machine(last_monitor_machine);
} else {
this->set_selected_machine(userMachineList.begin()->second->get_dev_id());
}
}
// Get all available machines, include cloud machines and lan machines that have access right
auto all_machines = get_my_machine_list();
if (all_machines.empty())
return;
// Reconnect the machine the user last selected, if it's still available.
// why: no first-available fallback - auto-connecting an arbitrary machine
// fights the agent-swap reset, which intentionally leaves nothing selected.
const std::string last_monitor_machine = m_agent ? m_agent->get_user_selected_machine() : "";
const auto last_machine = all_machines.find(last_monitor_machine);
if (last_machine != all_machines.end())
this->set_selected_machine(last_machine->second->get_dev_id());
}
void DeviceManager::OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state)

View File

@@ -48,8 +48,9 @@ public:
MachineObject* get_selected_machine();
bool set_selected_machine(std::string dev_id);
void record_user_last_machine(const std::string& dev_id);
std::string get_user_last_machine() const;
// why: clears stale sidebar sync-status / AMS visuals. Public so the printer-agent
// swap path can reuse it instead of duplicating the two sidebar calls.
void OnSelectedMachineLost();
// local machine
void set_local_selected_machine(std::string dev_id) { local_selected_machine = dev_id; };
@@ -70,6 +71,8 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false);
void clear_other_devices();
void load_last_machine();
void update_user_machine_list_info(const std::string& provider);
void parse_user_print_info(std::string body);
@@ -110,7 +113,6 @@ private:
void check_pushing();
void OnMachineBindStateChanged(MachineObject* obj, const std::string& new_state);
void OnSelectedMachineLost();
void OnSelectedMachineChanged(const std::string& pre_dev_id, const std::string& new_dev_id);

View File

@@ -1733,9 +1733,11 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
int MachineObject::command_ams_calibrate(int ams_id)
{
std::string gcode_cmd = (boost::format("M620 C%1% \n") % ams_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
@@ -1773,9 +1775,11 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
{
std::string gcode_cmd = (boost::format("M620 R%1% \n") % tray_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_refresh_rfid(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
@@ -1791,9 +1795,11 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
int MachineObject::command_ams_select_tray(std::string tray_id)
{
std::string gcode_cmd = (boost::format("M620 P%1% \n") % tray_id).str();
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode_cmd;
return this->publish_gcode(gcode_cmd);
if (!m_agent) return -1;
int rtn = m_agent->command_ams_select_tray(get_dev_id(), tray_id, MachineObject::m_sequence_id++, is_lan_mode_printer());
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE)
show_unsupported_dlg(rtn);
return rtn;
}
int MachineObject::command_ams_control(std::string action)
@@ -4647,6 +4653,40 @@ void MachineObject::set_ctt_dlg( wxString text){
}
}
void MachineObject::show_unsupported_dlg(int code)
{
// why: a dead control invites repeat clicks, and the frame is modeless - without the guard
// every click stacks another one. Same shape as set_ctt_dlg above, including the reset on
// both hide and close so a dismissed dialog can reappear on the next attempt.
if (m_unsupported_dlg_shown) {
return;
}
m_unsupported_dlg_shown = true;
// why: two codes so the user learns which kind of dead end this is - the slicer having no
// translation for the command, or the printer's own config lacking the hardware to run it.
const wxString text = (code == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) ?
_L("This printer is not configured with the hardware this control needs.") :
_L("This control is not supported on this printer.");
// note: constructed directly rather than through CallAfter because every publish_json caller
// is on the UI thread - clicks come from wx handlers, and the agent marshals its own push
// callbacks back to main before parse_json runs. set_ctt_dlg relies on the same property.
auto unsupported_dlg = new GUI::SecondaryCheckDialog(nullptr, wxID_ANY, _L("Warning"),
GUI::SecondaryCheckDialog::VisibleButtons::ONLY_CONFIRM);
unsupported_dlg->update_text(text);
unsupported_dlg->Bind(wxEVT_SHOW, [this](auto& e) {
if (!e.IsShown()) {
m_unsupported_dlg_shown = false;
}
});
unsupported_dlg->Bind(wxEVT_CLOSE_WINDOW, [this](auto& e) {
e.Skip();
m_unsupported_dlg_shown = false;
});
unsupported_dlg->on_show();
}
int MachineObject::publish_gcode(std::string gcode_str)
{
json j;

View File

@@ -272,9 +272,11 @@ public:
bool m_is_online;
bool m_lan_mode_connection_state{false};
bool m_set_ctt_dlg{ false };
bool m_unsupported_dlg_shown{ false };
void set_lan_mode_connection_state(bool state) {m_lan_mode_connection_state = state;};
bool get_lan_mode_connection_state() {return m_lan_mode_connection_state;};
void set_ctt_dlg( wxString text);
void show_unsupported_dlg(int code);
int parse_msg_count = 0;
int keep_alive_count = 0;
std::chrono::system_clock::time_point last_update_time; /* last received print data from machine */

View File

@@ -35,6 +35,7 @@
#include "Widgets/TextCtrl.h"
#include "../Utils/ColorSpaceConvert.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#ifdef __WXOSX__
#define wxOSX true
#else
@@ -1403,39 +1404,6 @@ using choice_ctrl = ::ComboBox; // BBS
static std::map<std::string, DynamicList*> dynamic_lists;
static bool is_plugin_printer_agent_key(const std::string& value)
{
return value.rfind("plugin:", 0) == 0;
}
static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index)
{
if (!field)
return -1;
const unsigned int count = field->GetCount();
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
const int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
if (stored == enum_index)
return static_cast<int>(idx);
}
}
return -1;
}
static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback)
{
if (!field || item_index < 0)
return fallback;
if (void* data = field->GetClientData(item_index))
return static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
return fallback;
}
void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); }
void DynamicList::update()
@@ -1518,33 +1486,7 @@ void Choice::BUILD()
window = dynamic_cast<wxWindow*>(temp);
if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) {
if (m_opt_id == "printer_agent") {
const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return !is_plugin_printer_agent_key(value); });
const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return is_plugin_printer_agent_key(value); });
auto append_agent_rows = [this, temp](bool plugins) {
for (size_t i = 0; i < m_opt.enum_values.size(); ++i) {
const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]);
if (is_plugin != plugins)
continue;
const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]);
const int item = temp->Append(label);
temp->SetClientData(item, reinterpret_cast<void*>(static_cast<uintptr_t>(i + 1)));
}
};
if (has_builtin_agents) {
temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(false);
}
if (has_plugin_agents) {
temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(true);
}
} else if (m_opt.enum_labels.empty()) {
if (m_opt.enum_labels.empty()) {
// Append non-localized enum_values
for (auto el : m_opt.enum_values)
temp->Append(el);
@@ -1651,7 +1593,7 @@ void Choice::set_selection()
switch (m_opt.type) {
case coEnum:{
const int val = m_opt.default_value->getInt();
field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val);
field->SetSelection(val);
break;
}
case coFloat:
@@ -1701,12 +1643,7 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda
}
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
if (m_opt_id == "printer_agent") {
const int enum_index = idx == m_opt.enum_values.size() ?
(m_opt.default_value ? m_opt.default_value->getInt() : 0) :
static_cast<int>(idx);
field->SetSelection(printer_agent_item_for_enum_index(field, enum_index));
} else if (idx == m_opt.enum_values.size())
if (idx == m_opt.enum_values.size())
field->SetValue(value);
else
field->SetSelection(idx);
@@ -1772,33 +1709,11 @@ void Choice::set_value(const boost::any& value, bool change_event)
case coEnum:
// BBS
case coEnums: {
auto printer_agent_index_from_key = [this](const std::string& key) {
auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key);
if (it != m_opt.enum_values.end())
return static_cast<int>(it - m_opt.enum_values.begin());
return m_opt.default_value ? m_opt.default_value->getInt() : 0;
};
int val = 0;
if (m_opt_id == "printer_agent") {
if (const int* int_value = boost::any_cast<int>(&value))
val = *int_value;
else if (const wxString* wx_value = boost::any_cast<wxString>(&value))
val = printer_agent_index_from_key(into_u8(*wx_value));
else if (const std::string* string_value = boost::any_cast<std::string>(&value))
val = printer_agent_index_from_key(*string_value);
else {
m_disable_change_event = false;
return;
}
} else
val = boost::any_cast<int>(value);
int val = boost::any_cast<int>(value);
int selection = val;
if (m_opt_id == "printer_agent") {
selection = printer_agent_item_for_enum_index(field, val);
} else if (m_opt_id == "input_shaping_type") {
if (m_opt_id == "input_shaping_type") {
if (field != nullptr) {
const unsigned int count = field->GetCount();
int match_index = -1;
@@ -1920,12 +1835,6 @@ boost::any& Choice::get_value()
{
if (m_opt.nullable && field->GetSelection() == -1)
m_value = ConfigOptionEnumsGenericNullable::nil_value();
else if (m_opt_id == "printer_agent")
{
const int selection = field->GetSelection();
const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0;
m_value = printer_agent_enum_index_for_item(field, selection, fallback);
}
else if (m_opt_id == "input_shaping_type")
{
int selection = field->GetSelection();
@@ -2067,6 +1976,171 @@ void Choice::msw_rescale()
}
// PrinterAgentChoice
void PrinterAgentChoice::reload_rows()
{
auto* combo = dynamic_cast<choice_ctrl*>(window); // wxWidgets ComboBox
if (!combo)
return;
// clear ComboBox
combo->Clear();
// helpers
const auto agents = NetworkAgentFactory::get_registered_printer_agents();
const bool has_builtin_agents = std::any_of(agents.begin(), agents.end(),
[](const PrinterAgentInfo& a) { return !a.is_plugin(); });
const bool has_plugin_agents = std::any_of(agents.begin(), agents.end(),
[](const PrinterAgentInfo& a) { return a.is_plugin(); });
auto append_agent_rows = [combo](bool is_plugin)
{
const auto agents = NetworkAgentFactory::get_registered_printer_agents();
for (size_t i = 0; i < agents.size(); ++i)
{
if (agents[i].is_plugin() != is_plugin)
continue;
const int item = combo->Append(_(agents[i].display_name));
// why: carry the agent-id string on the row. alias is an owned wxString (auto-freed, never rendered)
combo->SetItemAlias(item, from_u8(agents[i].id));
}
};
// append rows
if (has_builtin_agents)
{
combo->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(false); // append rows for agents that are not plugins
}
if (has_plugin_agents)
{
combo->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(true); // append rows for agents that are plugins
}
}
void PrinterAgentChoice::BUILD()
{
wxSize size(def_width_wider() * m_em_unit, wxDefaultCoord);
if (m_opt.height >= 0) size.SetHeight(m_opt.height * m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
static Builder<choice_ctrl> builder;
choice_ctrl* temp = builder.build(m_parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr,
wxCB_READONLY);
temp->Clear();
temp->GetDropDown().SetUseContentWidth(true);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = (double)temp->GetTextCtrl()->GetSize().GetHeight() / m_em_unit;
temp->SetTextLabel(_L(m_opt.sidetext));
m_combine_side_text = true;
#ifdef __WXGTK3__
wxSize best_sz = temp->GetBestSize();
if (best_sz.x > size.x) temp->SetSize(best_sz);
#endif
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
window = dynamic_cast<wxWindow*>(temp);
reload_rows();
temp->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_change_field(); }, temp->GetId());
temp->SetToolTip(get_tooltip_text(temp->GetValue()));
}
// Resolve CONFIG id string to a matching row in the live REGISTRY. "" uses the vendor default.
// An unregistered id clears selection and shows "<id> (missing)" as free text.
void PrinterAgentChoice::set_value(const std::string& value, bool change_event)
{
m_disable_change_event = !change_event;
auto* field = dynamic_cast<choice_ctrl*>(window);
// check if any row's corresponding id matches the agent id we are attempting to set
const std::string effective_agent_id = wxGetApp().resolve_printer_agent_id(value);
const unsigned int count = field->GetCount();
int match = wxNOT_FOUND;
for (unsigned int i = 0; i < count; ++i)
{
if (into_u8(field->GetItemAlias(i)) == effective_agent_id) // if alias == id
{
match = static_cast<int>(i);
break;
}
}
// based on match or not, set selection and value
// - SetSelection and SetValue are UI to manipulate the display of the ComboBox
// - SetSelection automatically calls SetValue for the same value
// - we can also SetValue separately from SetSelection
if (match == wxNOT_FOUND)
{
field->SetSelection(wxNOT_FOUND); // nothing shows as selected in the dropdown
field->SetValue(from_u8(value + " (missing)")); // set a value not in the selection (upper display field)
}
else
{
// display name of agent shows both in upper display field and appears selected in dropdown
field->SetSelection(match);
}
m_disable_change_event = false;
}
// Accept boost::any values from callers (usually to OptionsGroup/Field parent classes) and normalize them to an agent id.
// Then use PrinterAgentChoice::set_value(std::string& value, ...)
void PrinterAgentChoice::set_value(const boost::any& value, bool change_event)
{
m_disable_change_event = !change_event;
auto* field = dynamic_cast<choice_ctrl*>(window);
if (value.empty())
{
field->SetValue("");
m_value = value;
m_disable_change_event = false;
return;
}
std::string id;
if (const std::string* s = boost::any_cast<std::string>(&value))
id = *s;
else if (const wxString* w = boost::any_cast<wxString>(&value))
id = into_u8(*w);
set_value(id, change_event);
}
// A real row returns its alias, which is the agent id. Header rows, missing rows,
// and no selection return empty boost::any so the custom writer leaves config unchanged.
boost::any& PrinterAgentChoice::get_value()
{
auto* field = dynamic_cast<choice_ctrl*>(window);
const int sel = field->GetSelection();
const std::string id = sel < 0 ? std::string{} : into_u8(field->GetItemAlias(sel));
if (id.empty())
m_value = boost::any{};
else
m_value = id;
return m_value;
}
void PrinterAgentChoice::enable() { dynamic_cast<choice_ctrl*>(window)->Enable(); }
void PrinterAgentChoice::disable() { dynamic_cast<choice_ctrl*>(window)->Disable(); }
void PrinterAgentChoice::msw_rescale()
{
Field::msw_rescale();
auto* field = dynamic_cast<choice_ctrl*>(window)->GetTextCtrl();
wxSize size(wxDefaultSize);
size.SetWidth((m_opt.width > 0 ? m_opt.width : def_width_wider()) * m_em_unit);
field->SetMinSize(wxSize(-1, int(1.5f * field->GetFont().GetPixelSize().y + 0.5f)));
field->SetSize(size);
dynamic_cast<choice_ctrl*>(window)->Rescale();
}
void PluginField::BUILD()
{
auto* panel = new wxPanel(m_parent, wxID_ANY);

View File

@@ -469,6 +469,44 @@ public:
void suppress_scroll();
};
// printer_agent is a coString whose choices come from the live agent registry.
// PrinterAgentChoice uses a ComboBox directly because Choice expects static config enums.
// Real rows carry the stored agent id in the row alias (SetItemAlias/GetItemAlias).
class PrinterAgentChoice : public Field
{
using Field::Field;
public:
PrinterAgentChoice(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id)
{
}
PrinterAgentChoice(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(
parent, opt, id)
{
}
~PrinterAgentChoice()
{
}
wxWindow* window{nullptr};
void BUILD() override;
// Clear and repopulate rows from the live registry (grouped System agents / Plugins).
// Does not change selection; the caller follows with set_value(stored id).
void reload_rows();
void set_value(const std::string& value, bool change_event = false);
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void enable() override;
void disable() override;
void msw_rescale() override;
wxWindow* getWindow() override { return window; }
};
class PluginField : public Field {
using Field::Field;
public:

View File

@@ -2809,16 +2809,58 @@ void GUI_App::init_plugin_gui_wiring()
});
};
// why: a newly loaded plugin only adds a selectable agent
// refresh the dropdown and leave the live agent alone
auto refresh_printer_agent_dropdown_after_load = [](const std::string&)
{
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app]
{
if (!app->is_closing())
app->refresh_printer_agent_dropdown();
});
};
// why: the unloaded plugin may have been the provider of the live agent
// re-run selection, where a now-missing agent will be cleared
// refresh dropdown after
auto switch_printer_agent_after_unload = [](const std::string&)
{
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app] {
if (app->is_closing())
return;
app->switch_printer_agent();
app->refresh_printer_agent_dropdown();
});
};
plugin_mgr.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin);
plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load);
plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload);
plugin_mgr.subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
[refresh_plugins_dialog, refresh_printer_agent_dropdown_after_load](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
refresh_printer_agent_dropdown_after_load(capability.plugin_key);
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
// current plate (on the UI thread) so the notification clears once its plugin is available.
if (wxTheApp && !wxGetApp().is_closing())
@@ -2828,10 +2870,11 @@ void GUI_App::init_plugin_gui_wiring()
});
});
plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
[refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
}
@@ -3873,6 +3916,48 @@ unsigned GUI_App::get_colour_approx_luma(const wxColour &colour)
));
}
void GUI_App::refresh_printer_agent_dropdown()
{
if (Tab* tab = get_tab(Preset::TYPE_PRINTER))
{
if (auto* printer_tab = dynamic_cast<TabPrinter*>(tab))
printer_tab->refresh_printer_agent_dropdown();
}
}
void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
{
if (!m_agent)
return;
// why: tearing down the old machine selection is only ever the prefix of setting the live
// agent (to a new one, or to null when the selection is missing) - so it lives here, not as
// a standalone helper. Pass nullptr to clear the selection.
if (DeviceManager* dev = getDeviceManager())
{
dev->set_selected_machine(""); // why: empty id disconnects and deselects the current machine
m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices
}
m_agent->set_printer_agent(agent);
sidebar().update_all_preset_comboboxes();
}
std::string GUI_App::resolve_printer_agent_id(const std::string& stored_id)
{
if (!stored_id.empty())
return stored_id;
return (preset_bundle && preset_bundle->is_bbl_vendor()) ? BBL_PRINTER_AGENT_ID : ORCA_PRINTER_AGENT_ID;
}
std::string GUI_App::canonical_printer_agent_id(const std::string& picked_id)
{
return picked_id == resolve_printer_agent_id("") ? std::string() : picked_id;
}
void GUI_App::switch_printer_agent()
{
if (!m_agent) {
@@ -3880,24 +3965,17 @@ void GUI_App::switch_printer_agent()
return;
}
// Read printer_agent from config, falling back to default
std::string effective_agent_id = ORCA_PRINTER_AGENT_ID;
if (preset_bundle->is_bbl_vendor())
effective_agent_id = BBL_PRINTER_AGENT_ID;
const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config;
if (config.has("printer_agent")) {
const std::string& value = config.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
effective_agent_id = value;
}
const std::string effective_agent_id = resolve_printer_agent_id(config.opt_string("printer_agent"));
// Check if agent is registered
const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id);
if (!agent_info_ptr) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id
<< "', keeping current agent";
// Keep current agent, don't switch
// why: the selected agent's provider is gone (e.g. plugin unloaded); leaving the old
// live agent up would keep talking to a machine the user can no longer select.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": agent ID '" << effective_agent_id
<< "' is unregistered; clearing live printer agent";
set_live_printer_agent(nullptr);
return;
}
const PrinterAgentInfo agent_info = *agent_info_ptr;
@@ -3911,7 +3989,9 @@ void GUI_App::switch_printer_agent()
NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir);
if (!new_printer_agent) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent";
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id
<< "'; clearing live printer agent";
set_live_printer_agent(nullptr);
return;
}
@@ -3934,9 +4014,9 @@ void GUI_App::switch_printer_agent()
return;
}
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
sidebar().update_all_preset_comboboxes();
// Swap the agent; set_live_printer_agent resets the device selection so the new
// agent starts clean (#124).
set_live_printer_agent(new_printer_agent);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;

View File

@@ -365,9 +365,14 @@ public:
HMSQuery* get_hms_query() { return hms_query; }
NetworkAgent* getAgent() { return m_agent; }
// Dynamic printer agent switching
// Reconcile the live printer agent with the stored preset selection.
void switch_printer_agent();
std::string resolve_printer_agent_id(const std::string& stored_id);
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
// then, all resolve and canonical would just be ORCA<->""
std::string canonical_printer_agent_id(const std::string& picked_id);
FilamentColorCodeQuery* get_filament_color_code_query();
bool is_editor() const { return m_app_mode == EAppMode::Editor; }
bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; }
@@ -798,6 +803,11 @@ private:
void window_pos_center(wxTopLevelWindow *window);
bool select_language();
// Dynamic printer agent selection - internal helpers for switch_printer_agent
// and the plugin load/unload callbacks (init_plugin_gui_wiring).
void refresh_printer_agent_dropdown();
void set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent); // null clears the selection
bool config_wizard_startup();
void check_updates(const bool verbose);

View File

@@ -708,7 +708,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
m_print_enable = get_enable_print_status();
m_print_btn->Enable(m_print_enable);
if (m_print_enable) {
if (wxGetApp().preset_bundle->use_bbl_network())
if (wxGetApp().preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"))
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_PRINT_PLATE));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SEND_GCODE));
@@ -1999,7 +1999,8 @@ wxBoxSizer* MainFrame::create_side_tools()
SidePopup* p = new SidePopup(this);
if (wxGetApp().preset_bundle
&& !wxGetApp().preset_bundle->is_bbl_vendor()) {
&& !wxGetApp().preset_bundle->is_bbl_vendor()
&& !wxGetApp().app_config->get_bool("use_printer_agents")) {
// ThirdParty Buttons
SideButton* export_gcode_btn = new SideButton(p, _L("Export G-code file"), "");
export_gcode_btn->SetCornerRadius(0);
@@ -2132,7 +2133,7 @@ wxBoxSizer* MainFrame::create_side_tools()
const auto preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
if (preset_bundle->use_bbl_network()) {
if (preset_bundle->use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) {
// BBL network support everything
} else {
support_send = false; // All 3rd print hosts do not have the send options
@@ -4253,7 +4254,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
void MainFrame::load_printer_url()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin())
if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"))
return;
auto cfg = preset_bundle.printers.get_edited_preset().config;

View File

@@ -54,6 +54,9 @@ const t_field& OptionsGroup::build_field(const t_config_option_key& id, const Co
case ConfigOptionDef::GUIType::one_string: m_fields.emplace(id, TextCtrl::Create<TextCtrl>(this->ctrl_parent(), opt, id)); break;
case ConfigOptionDef::GUIType::plugin_picker: m_fields.emplace(id, PluginField::Create<PluginField>(this->ctrl_parent(), opt, id)); break;
case ConfigOptionDef::GUIType::plugin_config: m_fields.emplace(id, PluginConfigField::Create<PluginConfigField>(this->ctrl_parent(), opt, id)); break;
case ConfigOptionDef::GUIType::printer_agent_select: m_fields.emplace(
id, PrinterAgentChoice::Create<PrinterAgentChoice>(this->ctrl_parent(), opt, id));
break;
default:
switch (opt.type) {
case coFloatOrPercent:
@@ -654,6 +657,16 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value)
{
if (opt_id == "printer_agent") {
// TODO: Replace this option-specific branch with a generic value adapter if
// more fields need custom field-value to config-value conversion.
if (const std::string* id = boost::any_cast<std::string>(&value))
this->change_opt_value("printer_agent", wxGetApp().canonical_printer_agent_id(*id));
OptionsGroup::on_change_OG(opt_id, value);
return;
}
if (!m_opt_map.empty()) {
auto it = m_opt_map.find(opt_id);
if (it == m_opt_map.end()) {
@@ -772,6 +785,19 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config,
}
}
#endif
else if (opt_key == "printer_agent")
{
// why: printer_agent is a coString kept out of m_opt_map. The generic non-opt_map revert
// below restores the edited config from get_value(), but a deregistered/"(missing)" saved
// id has no selectable row, so the field yields no value and the edited config keeps the
// user's interim pick -> stuck dirty. Restore the SAVED id straight into the edited config
// (displayable or not; config is the saved or system baseline), then repaint and notify.
const std::string saved_id = config.opt_string("printer_agent");
set_value(opt_key, saved_id);
this->change_opt_value(opt_key, saved_id);
OptionsGroup::on_change_OG(opt_key, saved_id);
return;
}
else if (m_opt_map.find(opt_key) == m_opt_map.end() ||
// This option don't have corresponded field
opt_key == "printable_area" || opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "thumbnails" ||

View File

@@ -25,7 +25,6 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "format.hpp"
#include "Tab.hpp"
#include "wxExtensions.hpp"
@@ -128,22 +127,8 @@ PhysicalPrinterDialog::~PhysicalPrinterDialog()
void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgroup)
{
m_optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
// Special handling for printer_agent: convert fake enum index to string agent ID
if (opt_key == "printer_agent") {
try {
int selected_idx = boost::any_cast<int>(value);
auto agents = NetworkAgentFactory::get_registered_printer_agents();
if (selected_idx >= 0 && selected_idx < static_cast<int>(agents.size())) {
m_config->set_key_value("printer_agent",
new ConfigOptionString(agents[selected_idx].id));
}
} catch (const boost::bad_any_cast&) {
// If value is not an int, ignore
}
if (opt_key == "host_type" || opt_key == "printhost_authorization_type")
this->update();
} else if (opt_key == "host_type" || opt_key == "printhost_authorization_type") {
this->update();
}
if (opt_key == "print_host")
this->update_printhost_buttons();
if (opt_key == "printhost_port")
@@ -154,47 +139,6 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
m_optgroup->append_single_option_line("host_type");
// Build printer agent dropdown from registry (only if network agent is available)
if (wxGetApp().getAgent() != nullptr) {
auto agents = NetworkAgentFactory::get_registered_printer_agents();
if (!agents.empty()) {
// Create a fake enum option to force a Choice widget instead of TextCtrl
// (printer_agent is coString in config, but we need a dropdown)
ConfigOptionDef def;
def.type = coEnum;
def.width = Field::def_width_wider();
def.label = L("Printer Agent");
def.tooltip = L("Select the network agent implementation for printer communication. "
"Available agents are registered at startup.");
def.mode = comAdvanced;
// Populate enum values and labels from registered agents
for (const auto& agent : agents) {
def.enum_values.push_back(agent.id);
def.enum_labels.push_back(agent.display_name);
}
// Resolve selected agent: use config value if valid, otherwise fall back to default
std::string selected_agent = m_config->opt_string("printer_agent");
auto it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; });
if (it == agents.end()) {
selected_agent = ORCA_PRINTER_AGENT_ID;
it = std::find_if(agents.begin(), agents.end(), [&selected_agent](const auto& a) { return a.id == selected_agent; });
}
if (it != agents.end()) {
size_t default_idx = std::distance(agents.begin(), it);
def.set_default_value(new ConfigOptionInt(static_cast<int>(default_idx)));
}
// Create and append the option line
auto agent_option = Option(def, "printer_agent");
Line agent_line = m_optgroup->create_single_option_line(agent_option);
m_optgroup->append_line(agent_line);
}
}
auto create_sizer_with_btn = [](wxWindow* parent, Button** btn, const std::string& icon_name, const wxString& label) {
*btn = new Button(parent, label);
(*btn)->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
@@ -725,7 +669,7 @@ void PhysicalPrinterDialog::update(bool printer_change)
}
// For bbl printers, show option to control the device tab
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
if (wxGetApp().preset_bundle->is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) {
m_optgroup->show_field("bbl_use_print_host_webui");
const bool use_print_host_webui = !current_webui.empty();
if (Field* printhost_webui_field = m_optgroup->get_field("bbl_use_print_host_webui"); printhost_webui_field) {
@@ -816,31 +760,6 @@ void PhysicalPrinterDialog::update_host_type(bool printer_change)
}
}
void PhysicalPrinterDialog::update_printer_agent_type()
{
if (m_config == nullptr)
return;
Field* agent_field = m_optgroup->get_field("printer_agent");
if (!agent_field)
return;
Choice* agent_choice = dynamic_cast<Choice*>(agent_field);
if (!agent_choice)
return;
// Sync selection with current config value
const std::string current_agent = m_config->opt_string("printer_agent");
auto agents = NetworkAgentFactory::get_registered_printer_agents();
for (size_t i = 0; i < agents.size(); ++i) {
if (agents[i].id == current_agent) {
agent_choice->set_value(i);
return;
}
}
}
void PhysicalPrinterDialog::update_printers()
{
wxBusyCursor wait;
@@ -894,11 +813,6 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event)
{
wxGetApp().get_tab(Preset::TYPE_PRINTER)->save_preset("", false, false, true, m_preset_name);
event.Skip();
// Defer printer agent switch to ensure preset save completes first
wxGetApp().CallAfter([] {
wxGetApp().switch_printer_agent();
});
}
}} // namespace Slic3r::GUI

View File

@@ -60,7 +60,6 @@ public:
void update(bool printer_change = false);
void update_host_type(bool printer_change);
void update_printer_agent_type();
void update_preset_input();
void update_printhost_buttons();
void update_printers();

View File

@@ -3246,7 +3246,7 @@ void Sidebar::update_all_preset_comboboxes()
auto p_mainframe = wxGetApp().mainframe;
auto cfg = preset_bundle.printers.get_edited_preset().config;
const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin();
const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents");
if (preset_bundle.use_bbl_network()) {
//only show connection button for not-BBL printer
@@ -3258,7 +3258,8 @@ void Sidebar::update_all_preset_comboboxes()
p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate);
} else {
//p->btn_connect_printer->Show();
p->m_printer_connect->Show();
// ORCA: hide the physical-printer connection button when printer agents are enabled
p->m_printer_connect->Show(!wxGetApp().app_config->get_bool("use_printer_agents"));
// ORCA: show/hide sync-ams button based on filament sync mode
auto agent = wxGetApp().getAgent();
@@ -3280,7 +3281,9 @@ void Sidebar::update_all_preset_comboboxes()
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint))
apikey = cfg.opt_string("printhost_apikey");
print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode;
print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents"))
? MainFrame::PrintSelectType::ePrintPlate
: MainFrame::PrintSelectType::eSendGcode;
}
if (!use_native_device_tab)
@@ -3439,7 +3442,10 @@ void Sidebar::update_presets(Preset::Type preset_type)
bool isBBL = preset_bundle.is_bbl_vendor();
bool is_dual_extruder = extruder_variants->size() == 2;
p->layout_printer(preset_bundle.use_bbl_network(), isBBL && is_dual_extruder);
// why: agent mode drives the native device tab, so the sidebar lays out like BBL
// (no physical-printer connect button).
p->layout_printer(preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents"),
isBBL && is_dual_extruder);
// Update nozzle titles from printer config (e.g. "Main Nozzle" / "Auxiliary Nozzle" for N6)
// UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0)
@@ -5625,6 +5631,7 @@ struct Plater::priv
void on_action_slice_all(SimpleEvent&);
void on_action_publish(wxCommandEvent &evt);
void on_action_print_plate(SimpleEvent&);
void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL);
void on_action_print_all(SimpleEvent&);
void on_action_export_gcode(SimpleEvent&);
void on_action_send_gcode(SimpleEvent&);
@@ -11166,18 +11173,23 @@ void Plater::priv::on_action_print_plate(SimpleEvent&)
}
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_network()) {
// BBS
if (!m_select_machine_dlg)
m_select_machine_dlg = new SelectMachineDialog(q);
m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL);
m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index());
m_select_machine_dlg->ShowModal();
if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) {
open_machine_select_dialog(partplate_list.get_curr_plate_index());
} else {
q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr);
}
}
void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print_type)
{
// BBS
if (!m_select_machine_dlg)
m_select_machine_dlg = new SelectMachineDialog(q);
m_select_machine_dlg->set_print_type(print_type);
m_select_machine_dlg->prepare(plate_idx);
m_select_machine_dlg->ShowModal();
}
void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&)
{
if (!m_send_multi_dlg)
@@ -11193,10 +11205,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&)
}
//BBS
if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q);
m_select_machine_dlg->set_print_type(PrintFromType::FROM_SDCARD_VIEW);
m_select_machine_dlg->prepare(0);
m_select_machine_dlg->ShowModal();
open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW);
}
void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
@@ -11211,13 +11220,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
update_sidebar();
int old_sel = e.GetOldSelection();
const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin();
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin);
(wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
e.Veto();
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel;
if (q) {
@@ -11273,13 +11282,8 @@ void Plater::priv::on_action_print_all(SimpleEvent&)
}
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_network()) {
// BBS
if (!m_select_machine_dlg)
m_select_machine_dlg = new SelectMachineDialog(q);
m_select_machine_dlg->set_print_type(PrintFromType::FROM_NORMAL);
m_select_machine_dlg->prepare(PLATE_ALL_IDX);
m_select_machine_dlg->ShowModal();
if (preset_bundle.use_bbl_network() || wxGetApp().app_config->get_bool("use_printer_agents")) {
open_machine_select_dialog(PLATE_ALL_IDX);
} else {
q->send_gcode_legacy(PLATE_ALL_IDX, nullptr);
}

View File

@@ -1135,6 +1135,14 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
}
if (param == "use_printer_agents")
{
// Rebuild the Device tab so the native/web-UI choice reflects the new flag
// immediately, instead of only on the next printer-preset change or restart.
if (wxGetApp().plater())
wxGetApp().plater()->sidebar().update_all_preset_comboboxes();
}
if (param == "enable_high_low_temp_mixed_printing") {
if (checkbox->GetValue()) {
const wxString warning_title = _L("Bed Temperature Difference Warning");

View File

@@ -33,6 +33,7 @@
#include "GUI_App.hpp"
#include "GUI_ObjectList.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "Plater.hpp"
@@ -5018,6 +5019,40 @@ void TabPrinter::build_fff()
optgroup->append_single_option_line("gcode_flavor", "printer_basic_information_advanced#g-code-flavor");
optgroup->append_single_option_line("pellet_modded_printer", "printer_basic_information_advanced#pellet-modded-printer");
optgroup->append_single_option_line("bbl_use_printhost", "printer_basic_information_advanced#use-3rd-party-print-host");
// "Printer Agent" dropdown - printer_agent is a coString; gui_type routes it to
// PrinterAgentChoice instead of a TextCtrl. Rows and values come from the live agent
// registry, and the value is stored as the agent-id string.
if (wxGetApp().getAgent() != nullptr)
{
auto registered_printer_agents = NetworkAgentFactory::get_registered_printer_agents();
if (!registered_printer_agents.empty())
{
ConfigOptionDef def;
def.type = coString;
def.gui_type = ConfigOptionDef::GUIType::printer_agent_select;
def.width = 3 * Field::def_width_wider() / 2;
def.label = L("Printer Agent");
def.tooltip = L("Select the network agent implementation for printer communication. "
"Available agents are registered at startup.");
def.mode = comAdvanced;
// Create the field without get_option() so it is not registered in m_opt_map.
// ConfigOptionsGroup handles printer_agent before the generic mapped write path.
Line agent_line = optgroup->create_single_option_line(Option(def, "printer_agent"));
optgroup->append_line(agent_line);
if (Field* agent_field = get_field("printer_agent"))
{
if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow())
choice->set_value(m_config->opt_string("printer_agent"), false);
}
// Register by hand so the UnsavedChanges dialog can render a row for it.
wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title,
optgroup->config_category());
}
}
optgroup->append_single_option_line("use_3mf");
optgroup->append_single_option_line("scan_first_layer" , "printer_basic_information_advanced#scan-first-layer");
optgroup->append_single_option_line("enable_power_loss_recovery", "printer_basic_information_advanced#power-loss-recovery");
@@ -5884,6 +5919,16 @@ void TabPrinter::reload_config()
// so update it implicitly
if (m_active_page && m_active_page->title() == "Multimaterial")
m_active_page->set_value("extruders_count", int(m_extruders_count));
// m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly.
if (Field* agent_field = get_field("printer_agent"))
{
if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow())
{
const std::string selected_agent = m_config->opt_string("printer_agent");
choice->set_value(selected_agent, false);
}
}
}
void TabPrinter::activate_selected_page(std::function<void()> throw_if_canceled)
@@ -5894,6 +5939,16 @@ void TabPrinter::activate_selected_page(std::function<void()> throw_if_canceled)
// so update it implicitly
if (m_active_page && m_active_page->title() == "Multimaterial")
m_active_page->set_value("extruders_count", int(m_extruders_count));
// m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly.
if (Field* agent_field = get_field("printer_agent"))
{
if (auto* choice = dynamic_cast<PrinterAgentChoice*>(agent_field); choice && choice->getWindow())
{
const std::string selected_agent = m_config->opt_string("printer_agent");
choice->set_value(selected_agent, false);
}
}
}
void TabPrinter::clear_pages()
@@ -7852,6 +7907,24 @@ bool TabPrinter::apply_extruder_cnt_from_cache()
return false;
}
void TabPrinter::refresh_printer_agent_dropdown() const
{
auto* choice = dynamic_cast<PrinterAgentChoice*>(get_field("printer_agent"));
if (!choice || !choice->getWindow())
return;
const auto agents = NetworkAgentFactory::get_registered_printer_agents();
if (agents.empty())
return;
// why: rows live on PrinterAgentChoice now; rebuild them from the live registry and re-select the stored id.
const std::string selected_agent = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.opt_string("printer_agent");
choice->reload_rows();
choice->set_value(selected_agent, false);
this->GetParent()->Layout();
}
bool Tab::validate_custom_gcodes()
{
if (m_type != Preset::TYPE_FILAMENT &&

View File

@@ -675,6 +675,7 @@ public:
wxSizer* create_bed_shape_widget(wxWindow* parent);
void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr);
bool apply_extruder_cnt_from_cache();
void refresh_printer_agent_dropdown() const;
};
class TabSLAMaterial : public Tab

View File

@@ -2,7 +2,9 @@
#include "BBLNetworkPlugin.hpp"
#include "NetworkAgentFactory.hpp"
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
namespace Slic3r {
@@ -20,6 +22,65 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
// Communication
// ============================================================================
std::string BBLPrinterAgent::ams_refresh_rfid_gcode(const std::string& tray_id)
{
return (boost::format("M620 R%1% \n") % tray_id).str();
}
std::string BBLPrinterAgent::ams_calibrate_gcode(int ams_id)
{
return (boost::format("M620 C%1% \n") % ams_id).str();
}
std::string BBLPrinterAgent::ams_select_tray_gcode(const std::string& tray_id)
{
return (boost::format("M620 P%1% \n") % tray_id).str();
}
int BBLPrinterAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_refresh_rfid_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_calibrate_gcode(ams_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
const std::string gcode = ams_select_tray_gcode(tray_id);
BOOST_LOG_TRIVIAL(trace) << "ams_debug: gcode_cmd" << gcode;
nlohmann::json j;
j["print"]["command"] = "gcode_line";
j["print"]["param"] = gcode;
j["print"]["sequence_id"] = std::to_string(sequence_id);
return publish(dev_id, j, lan_mode);
}
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode)
{
const int rtn = lan_mode ? send_message_to_printer(dev_id, j.dump(), 0, 0) : send_message(dev_id, j.dump(), 0, 0);
if (rtn == 0) {
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
} else {
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
}
return rtn;
}
int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag)
{
auto& plugin = BBLNetworkPlugin::instance();

View File

@@ -5,6 +5,7 @@
#include "ICloudServiceAgent.hpp"
#include <string>
#include <memory>
#include <nlohmann/json.hpp>
namespace Slic3r {
@@ -28,6 +29,12 @@ public:
// Communication
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override;
static std::string ams_refresh_rfid_gcode(const std::string& tray_id);
static std::string ams_calibrate_gcode(int ams_id);
static std::string ams_select_tray_gcode(const std::string& tray_id);
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode) override;
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode) override;
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override;
int disconnect_printer() override;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override;
@@ -85,6 +92,9 @@ public:
FilamentSyncMode get_filament_sync_mode() const override;
private:
// why: the lan/cloud DECISION stays machine-side; keep this mechanical branch in sync with publish_json.
int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
std::shared_ptr<ICloudServiceAgent> m_cloud_agent;
};

View File

@@ -2,6 +2,13 @@
#define __I_PRINTER_AGENT_HPP__
#include "bambu_networking.hpp"
// why: these extend the BAMBU_NETWORK_* return space rather than opening a new one - the value
// flows through the same int domain callers already compare against BAMBU_NETWORK_SUCCESS.
// They live here and not in bambu_networking.hpp because that file is a vendor header replaced
// wholesale by header-sync commits (see c09252ce11), which would silently clobber them.
// -70xx is free: the vendor occupies -1..-25 and -10xx through -60xx.
#define ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED -7010 // no translation exists for this command
#define ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE -7020 // a translation exists; this printer lacks the capability
#include <string>
#include <memory>
@@ -77,6 +84,16 @@ public:
*/
virtual int send_message(std::string dev_id, std::string json_str, int qos, int flag) = 0;
// why: gcode is firmware dialect, not a waist concept - commands whose body is Bambu-dialect
// gcode live on the agent that speaks it; the default is an honest refusal that MachineObject's
// publish funnel turns into a dialog.
virtual int command_ams_refresh_rfid(std::string, std::string, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_ams_calibrate(std::string, int, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
virtual int command_ams_select_tray(std::string, std::string, int, bool)
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
/**
* Establish a direct LAN connection to a printer.
*/

View File

@@ -767,6 +767,27 @@ int NetworkAgent::send_message(std::string dev_id, std::string json_str, int qos
return -1;
}
int NetworkAgent::command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_refresh_rfid(dev_id, tray_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_calibrate(dev_id, ams_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode)
{
if (m_printer_agent)
return m_printer_agent->command_ams_select_tray(dev_id, tray_id, sequence_id, lan_mode);
return -1;
}
int NetworkAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl)
{
if (m_printer_agent)

View File

@@ -142,6 +142,9 @@ public:
int set_on_local_message_fn(OnMessageFn fn);
int set_server_callback(OnServerErrFn fn);
int send_message(std::string dev_id, std::string json_str, int qos, int flag);
int command_ams_refresh_rfid(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
int command_ams_calibrate(std::string dev_id, int ams_id, int sequence_id, bool lan_mode);
int command_ams_select_tray(std::string dev_id, std::string tray_id, int sequence_id, bool lan_mode);
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
int disconnect_printer();
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag);

View File

@@ -465,25 +465,5 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu
<< plugin_key << "' with agent ID '" << agent_id << "'";
}
bool NetworkAgentFactory::is_current_printer_agent_plugin()
{
auto* preset_bundle = GUI::wxGetApp().preset_bundle;
if (!preset_bundle)
return false;
std::string agent_key = ORCA_PRINTER_AGENT_ID;
if (preset_bundle->is_bbl_vendor())
agent_key = BBL_PRINTER_AGENT_ID;
const auto& cfg = preset_bundle->printers.get_edited_preset().config;
if (cfg.has("printer_agent")) {
const std::string& value = cfg.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
agent_key = value;
}
const PrinterAgentInfo* info = get_printer_agent_info(agent_key);
return info && info->is_plugin();
}
} // namespace Slic3r

View File

@@ -166,8 +166,6 @@ public:
static void register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name);
static void deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name);
static bool is_current_printer_agent_plugin();
private:
// Factory is not instantiable
NetworkAgentFactory() = delete;