mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-24 20:18:26 +03:00
Compare commits
1 Commits
feat/print
...
feat/plate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
790a010615 |
@@ -575,7 +575,7 @@ function CapabilityCanRun(plugin, capability) {
|
||||
}
|
||||
|
||||
function IsPluginChecked(plugin) {
|
||||
return plugin.is_loaded;
|
||||
return GetStatus(plugin) === "Activated";
|
||||
}
|
||||
|
||||
function HasMixedCapabilityState(plugin) {
|
||||
@@ -1347,8 +1347,6 @@ function StatusDescription(plugin) {
|
||||
return "This plugin is still loading.";
|
||||
case "Error":
|
||||
return "This plugin is blocked until its error is fixed.";
|
||||
case "RuntimeError":
|
||||
return "This plugin is loaded but a capability reported an error.";
|
||||
case "Inactive":
|
||||
default:
|
||||
return "This plugin is inactive. Activate it to install or load it.";
|
||||
|
||||
@@ -424,11 +424,6 @@ body.pane-resizing {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-runtimeerror {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell.status-loading {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
@@ -685,11 +680,6 @@ body.pane-resizing {
|
||||
color: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-runtimeerror {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
}
|
||||
|
||||
.detail-status-chip.status-loading {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
@@ -3387,27 +3386,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
|
||||
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
|
||||
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
|
||||
// why: project filament_multi_colour stores space-joined components per
|
||||
// filament; decode once so every merge branch seeds from the same view,
|
||||
// falling back to the main color where the project has no components.
|
||||
auto decode_project_multi_colors = [this](const std::vector<std::string> &main_colors) {
|
||||
std::vector<std::vector<std::string>> decoded(main_colors.size());
|
||||
for (size_t i = 0; i < main_colors.size(); ++i) decoded[i] = {main_colors[i]};
|
||||
const ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
if (project_multi_color) {
|
||||
for (size_t i = 0; i < std::min(decoded.size(), project_multi_color->values.size()); ++i) {
|
||||
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
|
||||
// why: a whitespace-only persisted entry splits into empty
|
||||
// tokens, so keep the main-color fallback rather than
|
||||
// serializing an empty component.
|
||||
colors.erase(std::remove_if(colors.begin(), colors.end(),
|
||||
[](const std::string &c) { return c.empty(); }),
|
||||
colors.end());
|
||||
if (!colors.empty()) decoded[i] = colors;
|
||||
}
|
||||
}
|
||||
return decoded;
|
||||
};
|
||||
if (color_only) {
|
||||
auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) {
|
||||
for (int i = 0; i < infos.size(); i++) {
|
||||
@@ -3420,7 +3398,20 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
};
|
||||
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
|
||||
std::vector<std::vector<std::string>> exist_multi_color_filment(exist_colors.size());
|
||||
for (size_t i = 0; i < exist_colors.size(); i++) {
|
||||
exist_multi_color_filment[i] = {exist_colors[i]};
|
||||
}
|
||||
|
||||
ConfigOptionStrings *project_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
if (project_multi_color) {
|
||||
for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) {
|
||||
std::vector<std::string> colors = split_string(project_multi_color->values[i], ' ');
|
||||
if (!colors.empty()) {
|
||||
exist_multi_color_filment[i] = colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool mapped_any = false;
|
||||
if (use_map && !maps.empty()) {
|
||||
@@ -3496,7 +3487,11 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_color_types = filament_color_type->values;
|
||||
auto exist_filament_presets = this->filament_presets;
|
||||
auto exist_multi_color_filment = decode_project_multi_colors(exist_colors);
|
||||
std::vector<std::vector<std::string>> exist_multi_color_filment;
|
||||
exist_multi_color_filment.resize(exist_colors.size());
|
||||
for (int i = 0; i < exist_colors.size(); i++) {
|
||||
exist_multi_color_filment[i] = {exist_colors[i]};
|
||||
}
|
||||
for (size_t i = 0; i < exist_colors.size(); i++) {
|
||||
if (maps.find(i) != maps.end()) {//mapping exist
|
||||
auto valid_index = get_map_index(ams_array_maps, maps[i]);
|
||||
@@ -3504,10 +3499,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
exist_colors[i] = ams_filament_colors[valid_index];
|
||||
exist_color_types[i] = ams_filament_color_types[valid_index];
|
||||
exist_filament_presets[i] = ams_filament_presets[valid_index];
|
||||
// why: a single-color agent tray reports no components, so
|
||||
// fall back to the printer main color rather than keeping
|
||||
// the replaced project filament's components.
|
||||
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index].empty() ? std::vector<std::string>{ams_filament_colors[valid_index]} : ams_multi_color_filment[valid_index];
|
||||
exist_multi_color_filment[i] = ams_multi_color_filment[valid_index];
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)";
|
||||
}
|
||||
@@ -3567,7 +3559,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
||||
exist_colors.push_back(need_append_colors[i].filament_color);
|
||||
exist_color_types.push_back(need_append_colors[i].filament_color_type);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color.empty() ? std::vector<std::string>{need_append_colors[i].filament_color} : need_append_colors[i].mutli_filament_color);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
|
||||
}
|
||||
}
|
||||
filament_color->values = exist_colors;
|
||||
@@ -3585,7 +3577,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto exist_colors = filament_color->values;
|
||||
auto exist_color_types = filament_color_type->values;
|
||||
auto exist_presets = this->filament_presets;
|
||||
auto existing_multi_colors = decode_project_multi_colors(exist_colors);
|
||||
|
||||
size_t tray_count = ams_filament_presets.size();
|
||||
size_t total = std::max(tray_count, exist_presets.size());
|
||||
@@ -3595,34 +3586,25 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
std::vector<std::string> result_presets;
|
||||
std::vector<std::vector<std::string>> result_multi_colors;
|
||||
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
const bool is_loaded = i < ams_infos.size() && ams_infos[i].valid;
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid);
|
||||
|
||||
if (is_loaded) {
|
||||
// The printer replaces the project filament at this index.
|
||||
// Loaded tray: use tray's filament data
|
||||
result_colors.push_back(ams_filament_colors[i]);
|
||||
result_color_types.push_back(ams_filament_color_types[i]);
|
||||
result_presets.push_back(ams_filament_presets[i]);
|
||||
if (i < ams_multi_color_filment.size() && !ams_multi_color_filment[i].empty()) {
|
||||
result_multi_colors.push_back(ams_multi_color_filment[i]);
|
||||
} else {
|
||||
// why: old project components belong to the replaced
|
||||
// filament, so use the new printer filament's main color.
|
||||
result_multi_colors.push_back({ams_filament_colors[i]});
|
||||
}
|
||||
// why: update_multi_material_filament_presets() can grow
|
||||
// filament_presets alone to the extruder count, so presets
|
||||
// may outrun the colour arrays; fall through to a generic
|
||||
// filament rather than read past them.
|
||||
} else if (i < exist_presets.size() && i < exist_colors.size() && i < exist_color_types.size()) {
|
||||
// An empty or absent printer slot retains the project filament.
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
result_color_types.push_back(exist_color_types[i]);
|
||||
result_presets.push_back(exist_presets[i]);
|
||||
// note: already carries the project components or its
|
||||
// main-color fallback.
|
||||
result_multi_colors.push_back(existing_multi_colors[i]);
|
||||
result_multi_colors.push_back({exist_colors[i]});
|
||||
} else {
|
||||
// Neither source has a filament, so create a generic one.
|
||||
// New slot beyond existing count: prefer a generic filament preset
|
||||
auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
|
||||
return f.is_compatible && f.is_system
|
||||
&& boost::algorithm::starts_with(f.name, "Generic ");
|
||||
@@ -3668,28 +3650,23 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
|
||||
void PresetBundle::update_filament_multi_color()
|
||||
{
|
||||
const ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
|
||||
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
// note: cheap defence only. Both keys live in s_project_options and the
|
||||
// constructor applies them unconditionally, so this never fires today.
|
||||
if (!filament_color || !filament_multi_colour) return;
|
||||
// note: this is std::vector::resize(), which safely creates empty inner
|
||||
// vectors. Not ConfigOptionStrings::resize(), which throws without a
|
||||
// default value.
|
||||
ams_multi_color_filment.resize(filament_color->values.size());
|
||||
for (size_t i = 0; i < filament_color->values.size(); ++i) {
|
||||
if (ams_multi_color_filment[i].empty()) {
|
||||
// why: every final main color requires one parallel
|
||||
// multi-color entry.
|
||||
ams_multi_color_filment[i] = {filament_color->values[i]};
|
||||
std::vector<std::string> exsit_multi_colors;
|
||||
for (auto &fil_item : ams_multi_color_filment){
|
||||
if (fil_item.empty()) break;
|
||||
if (fil_item.size() == 1)
|
||||
exsit_multi_colors.push_back(fil_item[0]);
|
||||
else {
|
||||
std::string colors = "";
|
||||
for (auto &color : fil_item){
|
||||
colors += color + " ";
|
||||
}
|
||||
colors.erase(colors.size() - 1); // remove last space
|
||||
exsit_multi_colors.push_back(colors);
|
||||
}
|
||||
}
|
||||
std::vector<std::string> serialized;
|
||||
serialized.reserve(ams_multi_color_filment.size());
|
||||
for (const auto &components : ams_multi_color_filment)
|
||||
serialized.push_back(boost::algorithm::join(components, " "));
|
||||
// why: assignment sets both size and contents, so no resize is needed.
|
||||
filament_multi_colour->values = std::move(serialized);
|
||||
ConfigOptionStrings *filament_multi_colour = project_config.option<ConfigOptionStrings>("filament_multi_colour");
|
||||
filament_multi_colour->resize(exsit_multi_colors.size());
|
||||
filament_multi_colour->values = exsit_multi_colors;
|
||||
}
|
||||
|
||||
std::vector<int> PresetBundle::get_used_tpu_filaments(const std::vector<int> &used_filaments)
|
||||
|
||||
@@ -752,9 +752,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
{
|
||||
curr_tray->remain = -1;
|
||||
}
|
||||
// The tray objects are reused across status updates. Reset this
|
||||
// state when a previously empty slot receives a filament again.
|
||||
curr_tray->is_slot_placeholder = tray_it->contains("tray_slot_placeholder");
|
||||
if (tray_it->contains("tray_slot_placeholder")) {
|
||||
curr_tray->is_slot_placeholder = true;
|
||||
}
|
||||
int ams_id_int = 0;
|
||||
int tray_id_int = 0;
|
||||
try
|
||||
@@ -989,4 +989,4 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -154,4 +154,4 @@ public:
|
||||
protected:
|
||||
virtual void on_timer(wxTimerEvent& event);
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1731,11 +1731,9 @@ int MachineObject::command_ams_user_settings(bool start_read_opt, bool tray_read
|
||||
|
||||
int MachineObject::command_ams_calibrate(int ams_id)
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
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,11 +1771,9 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
|
||||
int MachineObject::command_ams_refresh_rfid(std::string tray_id)
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
|
||||
@@ -1790,22 +1786,12 @@ int MachineObject::command_ams_refresh_rfid2(int ams_id, int slot_id)
|
||||
return this->publish_json(j);
|
||||
}
|
||||
|
||||
int MachineObject::command_start_camera()
|
||||
{
|
||||
if (!m_agent) return -1;
|
||||
// why: this fires from the camera view's renew timer, so a refusal must stay silent -
|
||||
// show_unsupported_dlg() here would pop a dialog every ~5 min on every other printer.
|
||||
return m_agent->command_start_camera(get_dev_id());
|
||||
}
|
||||
|
||||
|
||||
int MachineObject::command_ams_select_tray(std::string tray_id)
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_control(std::string action)
|
||||
@@ -2629,12 +2615,7 @@ void MachineObject::reset()
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
delete subtask_;
|
||||
subtask_ = nullptr;
|
||||
}
|
||||
subtask_ = nullptr;
|
||||
has_extra_flow_type = false;
|
||||
m_partskip_ids.clear();
|
||||
}
|
||||
@@ -2644,20 +2625,6 @@ void MachineObject::set_print_state(std::string status)
|
||||
print_status = status;
|
||||
}
|
||||
|
||||
// why: printer agents can report progress without BBL cloud task identity.
|
||||
void MachineObject::update_print_progress(const json& value)
|
||||
{
|
||||
if (value.is_string())
|
||||
mc_print_percent = stoi(value.get<std::string>());
|
||||
else if (value.is_number_integer())
|
||||
mc_print_percent = value.get<int>();
|
||||
else
|
||||
return;
|
||||
|
||||
if (BBLSubTask* curr_task = get_subtask())
|
||||
curr_task->task_progress = mc_print_percent;
|
||||
}
|
||||
|
||||
int MachineObject::connect(bool use_openssl)
|
||||
{
|
||||
if (get_dev_ip().empty()) return -1;
|
||||
@@ -2771,14 +2738,6 @@ int MachineObject::publish_json(const json& json_item, int qos, int flag)
|
||||
BOOST_LOG_TRIVIAL(error) << "publish_json: " << json_item.dump() << " code: " << rtn;
|
||||
}
|
||||
|
||||
// why: the agent is the only thing that knows what it can translate, so it reports
|
||||
// not-supported in its return value and this - the single funnel every command_* builder
|
||||
// passes through - is the one place that turns it into something the user sees. No list of
|
||||
// unsupported commands is needed anywhere: an agent that has no case for a command says so.
|
||||
if (rtn == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED || rtn == ORCA_NETWORK_ERR_CAP_NOT_AVAILABLE) {
|
||||
show_unsupported_dlg(rtn);
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
@@ -3335,7 +3294,10 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
print_type = jj["print_type"].get<std::string>();
|
||||
}
|
||||
if (jj.contains("mc_percent")) {
|
||||
update_print_progress(jj["mc_percent"]);
|
||||
if (jj["mc_percent"].is_string())
|
||||
mc_print_percent = stoi(j["print"]["mc_percent"].get<std::string>());
|
||||
else if (jj["mc_percent"].is_number_integer())
|
||||
mc_print_percent = j["print"]["mc_percent"].get<int>();
|
||||
}
|
||||
if (jj.contains("mc_print_sub_stage")) {
|
||||
if (jj["mc_print_sub_stage"].is_number_integer())
|
||||
@@ -3505,9 +3467,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
this->task_id_ = jj["task_id"].get<std::string>();
|
||||
}
|
||||
|
||||
if (jj.contains("thumbnail_url") && jj["thumbnail_url"].is_string())
|
||||
m_agent_thumbnail_url = jj["thumbnail_url"].get<std::string>();
|
||||
|
||||
if (jj.contains("job_attr")) {
|
||||
int jobAttr = jj["job_attr"].get<int>();
|
||||
jobState_ = get_flag_bits(jobAttr, 4, 4);
|
||||
@@ -3553,6 +3512,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
update_slice_info(jj["project_id"].get<std::string>(), jj["profile_id"].get<std::string>(), jj["subtask_id"].get<std::string>(), plate_index);
|
||||
BBLSubTask* curr_task = get_subtask();
|
||||
if (curr_task) {
|
||||
curr_task->task_progress = mc_print_percent;
|
||||
curr_task->printing_status = print_status;
|
||||
curr_task->task_id = jj["subtask_id"].get<std::string>();
|
||||
}
|
||||
@@ -3855,7 +3815,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
has_ipcam = true;
|
||||
} else {
|
||||
has_ipcam = false;
|
||||
webcam_stream_url.clear();
|
||||
}
|
||||
}
|
||||
if (ipcam.contains("resolution")) {
|
||||
@@ -3890,9 +3849,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
liveview_local = local_rtsp_url.empty() ? LVL_None : local_rtsp_url == "disable"
|
||||
? LVL_Disable : boost::algorithm::starts_with(local_rtsp_url, "rtsps") ? LVL_Rtsps : LVL_Rtsp;
|
||||
}
|
||||
if (ipcam.contains("stream_url") && ipcam["stream_url"].is_string()) {
|
||||
webcam_stream_url = ipcam["stream_url"].get<std::string>();
|
||||
}
|
||||
if (ipcam.contains("tutk_server")) {
|
||||
tutk_state = ipcam["tutk_server"].get<std::string>();
|
||||
}
|
||||
|
||||
@@ -547,7 +547,6 @@ public:
|
||||
bool xcam_first_layer_inspector { false };
|
||||
time_t xcam_first_layer_hold_start = 0;
|
||||
std::string local_rtsp_url;
|
||||
std::string webcam_stream_url;
|
||||
std::string tutk_state;
|
||||
enum LiveviewLocal {
|
||||
LVL_None,
|
||||
@@ -709,8 +708,6 @@ public:
|
||||
std::string subtask_id_;
|
||||
std::string job_id_;
|
||||
std::string last_subtask_id_;
|
||||
// note: printer-agent-supplied thumbnail url, empty when the agent supplies none.
|
||||
std::string m_agent_thumbnail_url;
|
||||
BBLSliceInfo* slice_info {nullptr};
|
||||
boost::thread* get_slice_info_thread { nullptr };
|
||||
boost::thread* get_model_task_thread { nullptr };
|
||||
@@ -816,7 +813,6 @@ public:
|
||||
int command_ams_select_tray(std::string tray_id);
|
||||
int command_ams_refresh_rfid(std::string tray_id);
|
||||
int command_ams_refresh_rfid2(int ams_id, int slot_id);
|
||||
int command_start_camera();
|
||||
int command_ams_control(std::string action);
|
||||
int command_ams_drying_stop();
|
||||
int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = "");
|
||||
@@ -898,7 +894,6 @@ public:
|
||||
static bool is_in_printing_status(std::string status);
|
||||
|
||||
void set_print_state(std::string status);
|
||||
void update_print_progress(const json& value);
|
||||
|
||||
bool is_connected();
|
||||
bool is_connecting();
|
||||
|
||||
@@ -4336,30 +4336,11 @@ void Sidebar::load_ams_list(MachineObject* obj)
|
||||
filament_ams_list = build_filament_ams_list(obj);
|
||||
}
|
||||
|
||||
bool device_change = false;
|
||||
const std::string& device = obj ? obj->get_dev_id() : "";
|
||||
const bool same_device = p->ams_list_device == device;
|
||||
|
||||
// Keep sync metadata out of the device payload, but preserve it across a
|
||||
// subscription refresh when the physical filament in a slot is unchanged.
|
||||
// Otherwise the refreshed configs differ only by the missing
|
||||
// filament_changed key, causing combo boxes to rebuild and lose their
|
||||
// transient post-sync badges.
|
||||
auto &previous_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list;
|
||||
for (auto &entry : filament_ams_list) {
|
||||
auto previous = previous_filament_ams_list.find(entry.first);
|
||||
const auto *previous_changed = previous == previous_filament_ams_list.end() ? nullptr :
|
||||
dynamic_cast<const ConfigOptionBool *>(previous->second.option("filament_changed"));
|
||||
if (!same_device || previous_changed == nullptr ||
|
||||
previous->second.opt_string("filament_id", 0u) != entry.second.opt_string("filament_id", 0u)) {
|
||||
continue;
|
||||
}
|
||||
entry.second.set_key_value("filament_changed",
|
||||
new ConfigOptionBool{previous_changed->value});
|
||||
}
|
||||
|
||||
bool device_change = !same_device;
|
||||
if (device_change) {
|
||||
if (p->ams_list_device != device) {
|
||||
p->ams_list_device = device;
|
||||
device_change = true;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size();
|
||||
if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change)
|
||||
@@ -4369,27 +4350,9 @@ void Sidebar::load_ams_list(MachineObject* obj)
|
||||
wxGetApp().preset_bundle->filament_ams_list = filament_ams_list;
|
||||
|
||||
for (auto c : p->combos_filament){
|
||||
c->set_sync_badge(false);
|
||||
c->update();
|
||||
}
|
||||
|
||||
if (!device_change) {
|
||||
size_t combo_index = 0;
|
||||
for (const auto &entry : filament_ams_list) {
|
||||
const auto &tray = entry.second;
|
||||
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
|
||||
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
|
||||
tray.opt_bool("filament_slot_placeholder", 0u);
|
||||
if (!has_filament && !is_placeholder) {
|
||||
continue;
|
||||
}
|
||||
if (combo_index >= p->combos_filament.size()) {
|
||||
break;
|
||||
}
|
||||
const auto *filament_changed = dynamic_cast<const ConfigOptionBool *>(tray.option("filament_changed"));
|
||||
p->combos_filament[combo_index]->set_sync_badge(
|
||||
has_filament && !is_placeholder && filament_changed != nullptr && filament_changed->value);
|
||||
++combo_index;
|
||||
if (device_change) {
|
||||
c->ShowBadge(false);//change printer,then clear badge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4563,32 +4526,18 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") :
|
||||
_L("Filament type and color information have been synchronized, but slot information is not included.");
|
||||
c->SetToolTip(tip);
|
||||
c->set_sync_badge(true);
|
||||
c->ShowBadge(true);
|
||||
};
|
||||
{ // badge ams filament
|
||||
clear_combos_filament_badge();
|
||||
if (sync_result.direct_sync) {
|
||||
// A placeholder contributes a preserved project filament to the
|
||||
// overwrite result, but it is not AMS-sourced and must not get a
|
||||
// sync badge. Non-placeholder empty trays are omitted entirely.
|
||||
size_t combo_index = 0;
|
||||
for (const auto &entry : wxGetApp().preset_bundle->filament_ams_list) {
|
||||
const auto &tray = entry.second;
|
||||
const bool has_filament = !tray.opt_string("filament_id", 0u).empty();
|
||||
const bool is_placeholder = tray.has("filament_slot_placeholder") &&
|
||||
tray.opt_bool("filament_slot_placeholder", 0u);
|
||||
if (!has_filament && !is_placeholder) {
|
||||
continue;
|
||||
}
|
||||
if (combo_index >= p->combos_filament.size()) {
|
||||
break;
|
||||
}
|
||||
if (is_placeholder) {
|
||||
p->combos_filament[combo_index]->set_sync_badge(false);
|
||||
} else {
|
||||
badge_combox_filament(p->combos_filament[combo_index]);
|
||||
}
|
||||
++combo_index;
|
||||
// Orca: PresetBundle::sync_ams_list rebuilds combos_filament
|
||||
// 1:1 from the AMS trays that produce a combo (loaded trays + placeholders; non-placeholder
|
||||
// empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The
|
||||
// previous per-tray index walked the full filament_ams_list (including the skipped empties),
|
||||
// so an empty slot before a loaded one dropped the badge for the trailing filaments.
|
||||
for (auto &c : p->combos_filament) {
|
||||
badge_combox_filament(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4855,11 +4804,6 @@ template<typename T> void setup_dialog_position(T& info)
|
||||
|
||||
void Sidebar::pop_sync_nozzle_and_ams_dialog() {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog";
|
||||
auto agent = wxGetApp().getAgent();
|
||||
if (!agent || agent->get_filament_sync_mode() == FilamentSyncMode::none) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " filament synchronization is not supported; skipping dialog";
|
||||
return;
|
||||
}
|
||||
wxTheApp->CallAfter([this]() {
|
||||
SyncNozzleAndAmsDialog::InputInfo temp_na_info;
|
||||
wxPoint big_btn_pt;
|
||||
@@ -4991,14 +4935,17 @@ void Sidebar::clear_combos_filament_badge()
|
||||
{
|
||||
auto &combos_filament = p->combos_filament;
|
||||
for (auto &c : combos_filament) { // clear flag
|
||||
c->set_sync_badge(false);
|
||||
c->ShowBadge(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Sidebar::udpate_combos_filament_badge() {
|
||||
auto &combos_filament = p->combos_filament;
|
||||
for (auto &c : combos_filament) {
|
||||
c->update_badge_according_flag();
|
||||
auto selection = c->GetSelection();
|
||||
auto select_flag = c->GetFlag(selection);
|
||||
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
|
||||
c->ShowBadge(ok);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10540,7 +10487,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
|
||||
sidebar->auto_calc_flushing_volumes(idx);
|
||||
}
|
||||
auto select_flag = combo->GetFlag(selection);
|
||||
combo->set_sync_badge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS);
|
||||
combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS);
|
||||
q->on_filament_change(idx);
|
||||
}
|
||||
bool select_preset = !combo->selection_is_changed_according_to_physical_printers();
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Slic3r
|
||||
// IMPORTANT: ordinal order is the Plugins dialog Status sort priority.
|
||||
Activated,
|
||||
Error,
|
||||
RuntimeError,
|
||||
Inactive,
|
||||
Loading
|
||||
};
|
||||
@@ -22,28 +21,11 @@ namespace Slic3r
|
||||
{
|
||||
case PluginStatus::Activated: return "Activated";
|
||||
case PluginStatus::Error: return "Error";
|
||||
case PluginStatus::RuntimeError: return "RuntimeError";
|
||||
case PluginStatus::Inactive: return "Inactive";
|
||||
case PluginStatus::Loading: return "Loading";
|
||||
}
|
||||
|
||||
return "Inactive";
|
||||
}
|
||||
|
||||
// why: a plugin whose module is live but whose catalog carries an error is a
|
||||
// RUNTIME fault (e.g. a capability rejected at register time) - it stays
|
||||
// loaded/checked and is only flagged, distinct from a load-time Error where
|
||||
// the module never came up. Loading wins over both so an in-flight reload
|
||||
// never flashes an error.
|
||||
inline PluginStatus resolve_plugin_status(bool loading, bool has_error, bool is_loaded)
|
||||
{
|
||||
if (loading)
|
||||
return PluginStatus::Loading;
|
||||
if (has_error)
|
||||
return is_loaded ? PluginStatus::RuntimeError : PluginStatus::Error;
|
||||
if (is_loaded)
|
||||
return PluginStatus::Activated;
|
||||
return PluginStatus::Inactive;
|
||||
}
|
||||
}
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -236,7 +236,6 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
|
||||
payload_item["label"] = dialog_item.display_name;
|
||||
payload_item["source"] = to_string(dialog_item.source);
|
||||
payload_item["status"] = to_string(dialog_item.status);
|
||||
payload_item["is_loaded"] = dialog_item.is_loaded;
|
||||
payload_item["error"] = dialog_item.error_text;
|
||||
payload_item["update_status"] = to_string(dialog_item.update_status);
|
||||
payload_item["unauthorized"] = dialog_item.unauthorized;
|
||||
@@ -381,7 +380,14 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.sharing_token = descriptor.sharing_token;
|
||||
item.thumbnail_url = descriptor.thumbnail_url;
|
||||
|
||||
item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
|
||||
if (item.loading)
|
||||
item.status = PluginStatus::Loading;
|
||||
else if (item.has_error)
|
||||
item.status = PluginStatus::Error;
|
||||
else if (item.is_loaded)
|
||||
item.status = PluginStatus::Activated;
|
||||
else
|
||||
item.status = PluginStatus::Inactive;
|
||||
|
||||
item.available_actions = evaluate_action_policy(item);
|
||||
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
|
||||
@@ -657,9 +663,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
|
||||
// A user-disabled plugin has no meaningful error state.
|
||||
if (!manager.clear_plugin_error(plugin_key))
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)";
|
||||
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
|
||||
if (m_activating_plugin_key == plugin_key)
|
||||
m_activating_plugin_key.clear();
|
||||
|
||||
@@ -994,13 +994,7 @@ void PlaterPresetComboBox::update_badge_according_flag() {
|
||||
auto selection = GetSelection();
|
||||
auto select_flag = GetFlag(selection);
|
||||
auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS;
|
||||
ShowBadge(m_sync_badge || ok);
|
||||
}
|
||||
|
||||
void PlaterPresetComboBox::set_sync_badge(bool show)
|
||||
{
|
||||
m_sync_badge = show;
|
||||
ShowBadge(show);
|
||||
ShowBadge(ok);
|
||||
}
|
||||
|
||||
bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
@@ -205,7 +205,6 @@ public:
|
||||
void msw_rescale() override;
|
||||
void OnSelect(wxCommandEvent& evt) override;
|
||||
void update_badge_according_flag();
|
||||
void set_sync_badge(bool show);
|
||||
|
||||
FilamentColor get_cur_color_info();
|
||||
void show_default_color_picker();
|
||||
@@ -215,7 +214,6 @@ public:
|
||||
private:
|
||||
// BBS
|
||||
wxColor m_color;
|
||||
bool m_sync_badge{false};
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1736,7 +1736,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
|
||||
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
|
||||
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
m_button_ok->Enable(isIp(str_ip.ToStdString()) && str_access_code.Length() >= 8);
|
||||
m_button_ok->Enable(isIp(str_ip.ToStdString()) && str_access_code.Length() == 8);
|
||||
|
||||
Layout();
|
||||
Fit();
|
||||
@@ -2056,6 +2056,10 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
|
||||
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
|
||||
|
||||
if (str_access_code.empty()) {
|
||||
str_access_code = "88888888";
|
||||
}
|
||||
|
||||
auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both);
|
||||
auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both);
|
||||
bool invalid_access_code = true;
|
||||
@@ -2068,8 +2072,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
}
|
||||
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
bool valid_access_code_length = str_access_code.Length() >= 8;
|
||||
bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
|
||||
bool enable_btns = isIp(str_ip.ToStdString()) && str_access_code.Length() == 8 && invalid_access_code;
|
||||
m_button_manual_setup->Enable(enable_btns);
|
||||
m_button_ok->Enable(enable_btns);
|
||||
|
||||
|
||||
@@ -3695,32 +3695,10 @@ void SelectMachineDialog::on_send_print()
|
||||
m_print_job->on_success([this]() { finish_mode(); });
|
||||
|
||||
m_print_job->on_check_ip_address_fail([this]() {
|
||||
// Invoked from the PrintJob worker thread when the LAN pre-flight (file upload
|
||||
// verification) fails. Marshal device/UI access to the main thread.
|
||||
CallAfter([this]()
|
||||
{
|
||||
// Reset the dialog out of sending mode so the user can retry.
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
|
||||
wxQueueEvent(this, evt);
|
||||
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
MachineObject* obj = dev ? dev->get_selected_machine() : nullptr;
|
||||
|
||||
if (obj && obj->is_connected())
|
||||
{
|
||||
// Connected: failed on file upload
|
||||
MessageDialog dlg(this,
|
||||
_L("Failed to upload the file to the printer's storage. Please try again."),
|
||||
_L("Send Failed"), wxOK | wxICON_ERROR);
|
||||
dlg.ShowModal();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not connected: reenter ip and access code
|
||||
wxGetApp().show_ip_address_enter_dialog();
|
||||
}
|
||||
});
|
||||
});
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_CLEAR_IPADDRESS);
|
||||
wxQueueEvent(this, evt);
|
||||
wxGetApp().show_ip_address_enter_dialog();
|
||||
});
|
||||
|
||||
// update ota version
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
@@ -4550,13 +4528,11 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
|
||||
|
||||
// check nozzle data valid
|
||||
{
|
||||
// Commented out the following as ntUndefine and 0.0f are default values
|
||||
// (signifying that the value is not given) that should PASS, not fail
|
||||
// if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
|
||||
// installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
|
||||
// show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
|
||||
// return false;
|
||||
// }
|
||||
if (installed_ext_nozzle.GetNozzleType() == NozzleType::ntUndefine ||
|
||||
installed_ext_nozzle.GetNozzleDiameter() <= 0.0f) {
|
||||
show_status(PrintDialogStatus::PrintStatusNozzleDataInvalid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj_->is_nozzle_flow_type_supported() &&
|
||||
installed_ext_nozzle.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) {
|
||||
@@ -4585,10 +4561,7 @@ bool SelectMachineDialog::CheckErrorExtruderNozzleWithSlicing(MachineObject* obj
|
||||
|
||||
// check nozzle diameter
|
||||
{
|
||||
// 0.0f is default when there is no nozzle diameter is given.
|
||||
// In nozzle_diameter == 0.0f case, it passes and does not require a comparison
|
||||
if (installed_ext_nozzle.GetNozzleDiameter() > 0.0f &&
|
||||
slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
|
||||
if (slicing_ext.nozzle_diameter != installed_ext_nozzle.GetNozzleDiameter()) {
|
||||
std::vector<wxString> msg_params;
|
||||
if (ext_sys->GetTotalExtderCount() == 2) {
|
||||
const wxString& mismatch_nozzle_str = _get_nozzle_name(ext_sys->GetTotalExtderCount(), slicing_ext_idx);
|
||||
|
||||
@@ -300,7 +300,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
|
||||
m_storage_panel->Layout();
|
||||
|
||||
// try to connect
|
||||
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(400), -1), wxALIGN_CENTER_HORIZONTAL);
|
||||
m_statictext_printer_msg = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL);
|
||||
m_statictext_printer_msg->SetFont(::Label::Body_13);
|
||||
m_statictext_printer_msg->SetForegroundColour(*wxBLACK);
|
||||
m_statictext_printer_msg->Hide();
|
||||
@@ -760,25 +760,9 @@ void SendToPrinterDialog::update_priner_status_msg(wxString msg, bool is_warning
|
||||
if (str_new != str_old) {
|
||||
if (m_statictext_printer_msg->GetLabel() != msg) {
|
||||
m_statictext_printer_msg->SetLabel(msg);
|
||||
const int wrap_width = FromDIP(400);
|
||||
m_statictext_printer_msg->Wrap(wrap_width);
|
||||
int line_count = 1;
|
||||
const wxString wrapped_label = m_statictext_printer_msg->GetLabel();
|
||||
for (size_t i = 0; i < wrapped_label.length(); ++i) {
|
||||
if (wrapped_label[i] == '\n')
|
||||
++line_count;
|
||||
}
|
||||
wxCoord text_width = 0;
|
||||
wxCoord text_height = 0;
|
||||
m_statictext_printer_msg->GetTextExtent(msg, &text_width, &text_height);
|
||||
const int extent_line_count = text_width > 0 ?
|
||||
std::max(1, (static_cast<int>(text_width) + wrap_width - 1) / wrap_width) : 1;
|
||||
line_count = std::max(line_count, extent_line_count);
|
||||
const int line_height = std::max(m_statictext_printer_msg->GetCharHeight(), static_cast<int>(text_height));
|
||||
const int min_height = std::max(m_statictext_printer_msg->GetBestSize().GetHeight(),
|
||||
line_count * line_height + FromDIP(2));
|
||||
m_statictext_printer_msg->SetMinSize(wxSize(wrap_width, min_height));
|
||||
m_statictext_printer_msg->SetMaxSize(wxDefaultSize);
|
||||
m_statictext_printer_msg->SetMinSize(wxSize(FromDIP(400), -1));
|
||||
m_statictext_printer_msg->SetMaxSize(wxSize(FromDIP(400), -1));
|
||||
m_statictext_printer_msg->Wrap(FromDIP(400));
|
||||
m_statictext_printer_msg->Show();
|
||||
Layout();
|
||||
Fit();
|
||||
@@ -1504,9 +1488,6 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vector<wxSt
|
||||
Enable_Send_Button(false);
|
||||
Enable_Refresh_Button(true);
|
||||
} else if (status == PrintDialogStatus::PrintStatusPublicInitFailed) {
|
||||
wxString msg_text = _L(
|
||||
"Failed to initialize the printer file transfer. Please check the connection and try again.");
|
||||
update_print_status_msg(msg_text, true, true);
|
||||
Enable_Send_Button(false);
|
||||
Enable_Refresh_Button(true);
|
||||
} else if (status == PrintDialogStatus::PrintStatusPublicUploadFiled) {
|
||||
@@ -1684,18 +1665,30 @@ extern void refresh_agora_url(char const *device, char const *dev_ver, char
|
||||
void SendToPrinterDialog::GetConnection()
|
||||
{
|
||||
DeviceManager *dm = GUI::wxGetApp().getDeviceManager();
|
||||
MachineObject *obj = dm ? dm->get_selected_machine() : nullptr;
|
||||
|
||||
if (!obj)
|
||||
MachineObject *obj = dm->get_selected_machine();
|
||||
if (obj == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : obj is empty";
|
||||
if (obj && !obj->get_file_remote())
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
int remote_proto = obj->get_file_remote();
|
||||
if (!remote_proto) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : remote_proto is not support";
|
||||
if (obj && obj->is_camera_busy_off())
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
if (obj->is_camera_busy_off()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " : camera is busy";
|
||||
m_connection_status = ConnectionStatus::NOT_START;
|
||||
}
|
||||
|
||||
if (m_url_timer && m_url_timer->IsRunning())
|
||||
NetworkAgent *agent = wxGetApp().getAgent();
|
||||
std::string agent_version = agent ? agent->get_version() : "";
|
||||
std::string dev_ver = obj->get_ota_version();
|
||||
std::string dev_id = obj->get_dev_id();
|
||||
|
||||
if (m_url_timer && m_url_timer->IsRunning())
|
||||
{
|
||||
m_url_timer->Stop();
|
||||
}
|
||||
@@ -1718,40 +1711,19 @@ void SendToPrinterDialog::GetConnection()
|
||||
m_url_timer->GetId());
|
||||
m_url_timer->StartOnce(8000);
|
||||
|
||||
if (obj && agent)
|
||||
{
|
||||
std::string dev_ver = obj->get_ota_version();
|
||||
std::string dev_id = obj->get_dev_id();
|
||||
|
||||
if (agent) {
|
||||
if (m_tcp_try_connect) {
|
||||
std::string devIP = obj->get_dev_ip();
|
||||
std::string accessCode = obj->get_access_code();
|
||||
std::string url = "bambu:///local/" + devIP + "?port=6000&user=" + "bblp" + "&passwd=" + accessCode;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp, dev_id=" << dev_id
|
||||
<< ", dev_ip=" << devIP << ", access_code_len=" << accessCode.size();
|
||||
|
||||
try
|
||||
{
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg)
|
||||
{
|
||||
CallAfter([this, is_success, err_code, error_msg]()
|
||||
{
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tcp";
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) {
|
||||
CallAfter([this, is_success, err_code, error_msg]() {
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tcp FileTransferTunnel unavailable for dev_id=" <<
|
||||
dev_id
|
||||
<< " dev_ip=" << devIP << ": " << e.what();
|
||||
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
|
||||
m_filetransfer_tunnel.reset();
|
||||
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
}
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
else if (m_tutk_try_connect)
|
||||
{
|
||||
@@ -1779,28 +1751,11 @@ void SendToPrinterDialog::GetConnection()
|
||||
if (boost::algorithm::starts_with(url, "bambu:///"))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Connect method tutk";
|
||||
try
|
||||
{
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection(
|
||||
[this](bool is_success, int err_code, std::string error_msg)
|
||||
{
|
||||
CallAfter([this, is_success, err_code, error_msg]()
|
||||
{
|
||||
OnConnection(is_success, err_code, error_msg);
|
||||
});
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": tutk FileTransferTunnel unavailable: " << e.
|
||||
what();
|
||||
if (m_url_timer && m_url_timer->IsRunning()) m_url_timer->Stop();
|
||||
m_filetransfer_tunnel.reset();
|
||||
m_connection_status = ConnectionStatus::CONNECTION_FAILED;
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
}
|
||||
m_filetransfer_tunnel = std::make_unique<FileTransferTunnel>(module(), url);
|
||||
m_filetransfer_tunnel->on_connection([this](bool is_success, int err_code, std::string error_msg) {
|
||||
CallAfter([this, is_success, err_code, error_msg]() { OnConnection(is_success, err_code, error_msg); });
|
||||
});
|
||||
m_filetransfer_tunnel->start_connect();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1899,17 +1854,8 @@ void SendToPrinterDialog::ResetTunnelAndJob()
|
||||
|
||||
void SendToPrinterDialog::CreateMediaAbilityJob()
|
||||
{
|
||||
nlohmann::json media_ability = {{"cmd_type", 7}};
|
||||
try
|
||||
{
|
||||
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
|
||||
show_status(PrintDialogStatus::PrintStatusPublicInitFailed);
|
||||
return;
|
||||
}
|
||||
nlohmann::json media_ability = {{"cmd_type", 7}};
|
||||
m_filetransfer_mediability_job = std::make_unique<FileTransferJob>(module(), std::string(media_ability.dump()));
|
||||
m_filetransfer_mediability_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) {
|
||||
//this pl
|
||||
CallAfter([this, res, resp_ec, json_res] {
|
||||
@@ -1964,20 +1910,11 @@ void SendToPrinterDialog::CreateUploadFileJob(const std::string &path, const std
|
||||
{"cmd_type", 5},
|
||||
};
|
||||
upload_params["dest_storage"] = m_selected_storage;
|
||||
upload_params["dest_name"] = name; // filenme no path
|
||||
upload_params["file_path"] = path;
|
||||
upload_params["dest_name"] = name; // filenme no path
|
||||
upload_params["file_path"] = path;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
|
||||
try
|
||||
{
|
||||
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": FileTransferJob unavailable: " << e.what();
|
||||
show_status(PrintDialogStatus::PrintStatusPublicUploadFiled);
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Begin CreateUploadFileJob";
|
||||
m_filetransfer_uploadfile_job = std::make_unique<FileTransferJob>(module(), std::string(upload_params.dump()));
|
||||
m_filetransfer_uploadfile_job->on_result([this](int res, int resp_ec, std::string json_res, std::vector<std::byte> bin_res) { //
|
||||
CallAfter([this, res, resp_ec, json_res, bin_res] {
|
||||
UploadFileRessultCallback(res, resp_ec,json_res, bin_res);
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
#include "MsgDialog.hpp"
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "DeviceErrorDialog.hpp"
|
||||
|
||||
@@ -2312,40 +2311,6 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
|
||||
const bool has_printer_webcam = !obj->webcam_stream_url.empty();
|
||||
if (has_printer_webcam) {
|
||||
if (m_printer_webcam_url != obj->webcam_stream_url) {
|
||||
// why: start timing belongs to the loaded camera.
|
||||
// carrying it across printers suppresses the new camera's initial start.
|
||||
m_camera_start_sent = std::chrono::steady_clock::time_point{};
|
||||
m_custom_camera_view->LoadURL(obj->webcam_stream_url);
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
m_printer_webcam_url = obj->webcam_stream_url;
|
||||
}
|
||||
m_camera_switch_button->Hide();
|
||||
if (!m_custom_camera_view->IsShown()) {
|
||||
// why: do not compare or reload the WebView URL per tick, or redirects can cause a reload loop.
|
||||
m_custom_camera_view->Show();
|
||||
m_media_ctrl->Hide();
|
||||
m_media_play_ctrl->Hide();
|
||||
}
|
||||
// why: printers like the U1 capture only while asked and retire the capture task ~362 s
|
||||
// after each start, so the open camera view has to renew ahead of that. 300 s matches
|
||||
// Snapmaker's own client. Agents that do not need it refuse the call silently.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_camera_start_sent == std::chrono::steady_clock::time_point{} ||
|
||||
now - m_camera_start_sent >= std::chrono::seconds(300)) {
|
||||
obj->command_start_camera();
|
||||
m_camera_start_sent = now;
|
||||
}
|
||||
} else if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
m_camera_start_sent = std::chrono::steady_clock::time_point{};
|
||||
}
|
||||
|
||||
//sdcard
|
||||
auto sdcard_state = obj->GetStorage()->get_sdcard_state();
|
||||
if (m_last_sdcard != sdcard_state) {
|
||||
@@ -2377,12 +2342,7 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
m_last_recording = obj->is_recording() ? 1 : 0;
|
||||
}
|
||||
|
||||
if (has_printer_webcam) {
|
||||
if (m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Hide();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
} else if (!m_bitmap_recording_img->IsShown()) {
|
||||
if (!m_bitmap_recording_img->IsShown()) {
|
||||
m_bitmap_recording_img->Show();
|
||||
m_panel_monitoring_title->Layout();
|
||||
}
|
||||
@@ -2439,8 +2399,6 @@ void StatusPanel::update_camera_state(MachineObject* obj)
|
||||
bool show_vcamera = m_media_play_ctrl->IsStreaming();
|
||||
m_camera_popup->update(show_vcamera);
|
||||
}
|
||||
|
||||
m_setting_button->Show(!has_printer_webcam);
|
||||
}
|
||||
|
||||
StatusPanel::StatusPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
|
||||
@@ -2728,8 +2686,7 @@ void StatusPanel::on_subtask_partskip(wxCommandEvent &event)
|
||||
void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
|
||||
{
|
||||
if (obj) {
|
||||
const bool was_resume = obj->can_resume();
|
||||
if (was_resume) {
|
||||
if (obj->can_resume()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: resume current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_resume();
|
||||
}
|
||||
@@ -2737,13 +2694,6 @@ void StatusPanel::on_subtask_pause_resume(wxCommandEvent &event)
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: pause current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_pause();
|
||||
}
|
||||
if (is_moonraker_agent()) {
|
||||
m_pause_resume_pending = true;
|
||||
m_pause_resume_was_resume = was_resume;
|
||||
m_pause_resume_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
|
||||
m_pause_resume_machine_id = obj->get_dev_id();
|
||||
m_project_task_panel->enable_pause_resume_button(false, was_resume ? "resume_disable" : "pause_disable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2755,12 +2705,6 @@ void StatusPanel::on_subtask_abort(wxCommandEvent &event)
|
||||
if (obj) {
|
||||
BOOST_LOG_TRIVIAL(info) << "monitor: stop current print task dev_id =" << obj->get_dev_id();
|
||||
obj->command_task_abort();
|
||||
if (is_moonraker_agent()) {
|
||||
m_abort_pending = true;
|
||||
m_abort_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(6);
|
||||
m_abort_machine_id = obj->get_dev_id();
|
||||
m_project_task_panel->enable_abort_button(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3734,25 +3678,6 @@ void StatusPanel::update_model_info()
|
||||
void StatusPanel::update_subtask(MachineObject *obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (m_pause_resume_pending) {
|
||||
if (!is_moonraker_agent() || m_pause_resume_machine_id != obj->get_dev_id() ||
|
||||
obj->can_resume() != m_pause_resume_was_resume) {
|
||||
m_pause_resume_pending = false;
|
||||
} else if (now >= m_pause_resume_deadline) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker pause/resume command did not change printer state";
|
||||
m_pause_resume_pending = false;
|
||||
}
|
||||
}
|
||||
if (m_abort_pending) {
|
||||
if (!is_moonraker_agent() || m_abort_machine_id != obj->get_dev_id() || obj->print_status == "FAILED" ||
|
||||
obj->print_status == "FINISH" || obj->print_status == "IDLE") {
|
||||
m_abort_pending = false;
|
||||
} else if (now >= m_abort_deadline) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "StatusPanel: Moonraker abort command did not change printer state";
|
||||
m_abort_pending = false;
|
||||
}
|
||||
}
|
||||
if (m_current_print_mode != PRINGINT) {
|
||||
if (calib_bitmap == nullptr) {
|
||||
m_calib_mode = get_obj_calibration_mode(obj, m_calib_method, cali_stage);
|
||||
@@ -3860,12 +3785,10 @@ void StatusPanel::update_subtask(MachineObject *obj)
|
||||
}
|
||||
update_basic_print_data(false);
|
||||
} else {
|
||||
if (!m_pause_resume_pending) {
|
||||
if (obj->can_resume()) {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "resume");
|
||||
} else {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "pause");
|
||||
}
|
||||
if (obj->can_resume()) {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "resume");
|
||||
} else {
|
||||
m_project_task_panel->enable_pause_resume_button(true, "pause");
|
||||
}
|
||||
m_project_task_panel->enable_partskip_button(obj, true);
|
||||
// update printing stage
|
||||
@@ -3922,9 +3845,7 @@ void StatusPanel::update_subtask(MachineObject *obj)
|
||||
m_project_task_panel->market_scoring_hide();
|
||||
}
|
||||
} else { // model printing is not finished, hide scoring page
|
||||
if (!m_abort_pending) {
|
||||
m_project_task_panel->enable_abort_button(true);
|
||||
}
|
||||
m_project_task_panel->enable_abort_button(true);
|
||||
m_project_task_panel->market_scoring_hide();
|
||||
m_project_task_panel->get_request_failed_panel()->Hide();
|
||||
}
|
||||
@@ -3999,61 +3920,39 @@ void StatusPanel::update_cloud_subtask(MachineObject *obj)
|
||||
update_calib_bitmap();
|
||||
if (obj->slice_info) {
|
||||
m_request_url = wxString(obj->slice_info->thumbnail_url);
|
||||
load_thumbnail_from_url(m_request_url, obj);
|
||||
if (!m_request_url.IsEmpty()) {
|
||||
wxImage img;
|
||||
std::map<wxString, wxImage>::iterator it = img_list.find(m_request_url);
|
||||
if (it != img_list.end()) {
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
|
||||
img = it->second;
|
||||
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
|
||||
m_project_task_panel->set_thumbnail_img(resize_img, "");
|
||||
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
|
||||
}
|
||||
if (this->obj) {
|
||||
m_project_task_panel->set_plate_index(obj->m_plate_index);
|
||||
} else {
|
||||
m_project_task_panel->set_plate_index(-1);
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
|
||||
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
|
||||
} else {
|
||||
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
|
||||
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
|
||||
web_request.Start();
|
||||
m_start_loading_thumbnail = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StatusPanel::load_thumbnail_from_url(const wxString &url, MachineObject *obj)
|
||||
{
|
||||
if (url.IsEmpty())
|
||||
return false;
|
||||
|
||||
wxImage img;
|
||||
std::map<wxString, wxImage>::iterator it = img_list.find(url);
|
||||
if (it != img_list.end()) {
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION ||(m_calib_mode == CalibMode::Calib_Flow_Rate && m_calib_method == CalibrationMethod::CALI_METHOD_MANUAL)) {
|
||||
img = it->second;
|
||||
wxImage resize_img = img.Scale(m_project_task_panel->get_bitmap_thumbnail()->GetSize().x, m_project_task_panel->get_bitmap_thumbnail()->GetSize().y);
|
||||
m_project_task_panel->set_thumbnail_img(resize_img, "");
|
||||
m_project_task_panel->set_brightness_value(get_brightness_value(resize_img));
|
||||
}
|
||||
if (this->obj) {
|
||||
m_project_task_panel->set_plate_index(obj->m_plate_index);
|
||||
} else {
|
||||
m_project_task_panel->set_plate_index(-1);
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::TASK_THUMBNAIL;
|
||||
BOOST_LOG_TRIVIAL(trace) << "web_request: use cache image";
|
||||
} else {
|
||||
m_request_url = url;
|
||||
web_request = wxWebSession::GetDefault().CreateRequest(this, m_request_url);
|
||||
BOOST_LOG_TRIVIAL(trace) << "monitor: start request thumbnail, url = " << m_request_url;
|
||||
web_request.Start();
|
||||
m_start_loading_thumbnail = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void StatusPanel::update_sdcard_subtask(MachineObject *obj)
|
||||
{
|
||||
if (!obj) return;
|
||||
|
||||
const wxString thumbnail_url = wxString(obj->m_agent_thumbnail_url);
|
||||
if (!thumbnail_url.IsEmpty()) {
|
||||
// why: Moonraker has no prediction or weight data, so keep it on the sdcard path.
|
||||
if (m_request_url != thumbnail_url || !m_load_sdcard_thumbnail) {
|
||||
if (web_request.IsOk() && web_request.GetState() == wxWebRequest::State_Active)
|
||||
web_request.Cancel();
|
||||
update_calib_bitmap();
|
||||
m_request_url = thumbnail_url;
|
||||
load_thumbnail_from_url(thumbnail_url, obj);
|
||||
m_load_sdcard_thumbnail = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_load_sdcard_thumbnail || !m_request_url.IsEmpty()) {
|
||||
if (!m_load_sdcard_thumbnail) {
|
||||
update_calib_bitmap();
|
||||
if (m_current_print_mode != PrintingTaskType::CALIBRATION) {
|
||||
m_project_task_panel->get_bitmap_thumbnail()->SetBitmap(m_thumbnail_sdcard.bmp());
|
||||
@@ -4061,14 +3960,11 @@ void StatusPanel::update_sdcard_subtask(MachineObject *obj)
|
||||
}
|
||||
task_thumbnail_state = ThumbnailState::SDCARD_THUMBNAIL;
|
||||
m_load_sdcard_thumbnail = true;
|
||||
m_request_url.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusPanel::reset_printing_values()
|
||||
{
|
||||
m_pause_resume_pending = false;
|
||||
m_abort_pending = false;
|
||||
m_project_task_panel->enable_partskip_button(nullptr, false);
|
||||
m_project_task_panel->enable_pause_resume_button(false, "pause_disable");
|
||||
m_project_task_panel->enable_abort_button(false);
|
||||
@@ -4094,12 +3990,6 @@ void StatusPanel::reset_printing_values()
|
||||
this->Layout();
|
||||
}
|
||||
|
||||
bool StatusPanel::is_moonraker_agent() const
|
||||
{
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
return agent && std::dynamic_pointer_cast<Slic3r::MoonrakerPrinterAgent>(agent->get_printer_agent()) != nullptr;
|
||||
}
|
||||
|
||||
void StatusPanel::on_axis_ctrl_xy(wxCommandEvent &event)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -5281,10 +5171,6 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj)
|
||||
void StatusPanel::set_default()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default";
|
||||
if (!m_printer_webcam_url.empty()) {
|
||||
handle_camera_source_change();
|
||||
m_printer_webcam_url.clear();
|
||||
}
|
||||
obj = nullptr;
|
||||
last_subtask = nullptr;
|
||||
last_tray_exist_bits = -1;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/gbsizer.h>
|
||||
#include <wx/webrequest.h>
|
||||
#include <chrono>
|
||||
#include "wxMediaCtrl2.h"
|
||||
#include "MediaPlayCtrl.h"
|
||||
#include "AMSSetting.hpp"
|
||||
@@ -631,7 +630,6 @@ class StatusPanel : public StatusBasePanel
|
||||
{
|
||||
private:
|
||||
friend class MonitorPanel;
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -665,9 +663,6 @@ protected:
|
||||
int m_last_timelapse = -1;
|
||||
int m_last_extrusion = -1;
|
||||
int m_last_vcamera = -1;
|
||||
std::string m_printer_webcam_url;
|
||||
// note: zero = not started; see update_camera_state() for the renew interval.
|
||||
std::chrono::steady_clock::time_point m_camera_start_sent{};
|
||||
int m_model_mall_request_count = 0;
|
||||
bool m_is_load_with_temp = false;
|
||||
json m_rating_result;
|
||||
@@ -691,13 +686,6 @@ protected:
|
||||
CalibrationMethod m_calib_method;
|
||||
int cali_stage;
|
||||
PrintingTaskType m_current_print_mode = PrintingTaskType::NOT_CLEAR;
|
||||
bool m_pause_resume_pending = false;
|
||||
bool m_pause_resume_was_resume = false;
|
||||
std::chrono::steady_clock::time_point m_pause_resume_deadline;
|
||||
std::string m_pause_resume_machine_id;
|
||||
bool m_abort_pending = false;
|
||||
std::chrono::steady_clock::time_point m_abort_deadline;
|
||||
std::string m_abort_machine_id;
|
||||
|
||||
void init_scaled_buttons();
|
||||
void create_tasklist_info();
|
||||
@@ -800,7 +788,6 @@ protected:
|
||||
void update_calib_bitmap();
|
||||
|
||||
void reset_printing_values();
|
||||
bool is_moonraker_agent() const;
|
||||
void on_webrequest_state(wxWebRequestEvent &evt);
|
||||
bool is_task_changed(MachineObject* obj);
|
||||
|
||||
|
||||
@@ -3176,7 +3176,6 @@ SyncAmsInfoDialog::~SyncAmsInfoDialog() {
|
||||
void SyncAmsInfoDialog::set_info(SyncInfo &info)
|
||||
{
|
||||
m_input_info = info;
|
||||
reinit_dialog();
|
||||
}
|
||||
|
||||
void SyncAmsInfoDialog::update_lan_machine_list()
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
#include "BBLNetworkPlugin.hpp"
|
||||
#include "NetworkAgentFactory.hpp"
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -22,65 +20,6 @@ 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();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "ICloudServiceAgent.hpp"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -29,12 +28,6 @@ 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;
|
||||
@@ -92,9 +85,6 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -233,11 +233,8 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
|
||||
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id)
|
||||
{
|
||||
if (sync_mode != get_filament_sync_mode())
|
||||
return false;
|
||||
|
||||
if (device_info.dev_ip.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#ifndef __CREALITY_PRINT_AGENT_HPP__
|
||||
#define __CREALITY_PRINT_AGENT_HPP__
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "MoonrakerPrinterAgent.hpp"
|
||||
|
||||
#include <string>
|
||||
@@ -42,7 +41,7 @@ public:
|
||||
static AgentInfo get_agent_info_static();
|
||||
AgentInfo get_agent_info() override { return get_agent_info_static(); }
|
||||
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
// Parse the boxsInfo JSON returned by CrealityPrint::query_boxes_info() into
|
||||
// a flat list of loaded slots, plus the count of CFS boxes the printer reports.
|
||||
|
||||
@@ -84,21 +84,6 @@ 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; }
|
||||
// why: some printers emit camera frames only while explicitly asked, and retire the
|
||||
// capture task on their own - the camera view starts it and renews it. Printers with an
|
||||
// always-on stream need nothing here, hence the honest refusal by default.
|
||||
virtual int command_start_camera(std::string)
|
||||
{ return ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED; }
|
||||
|
||||
/**
|
||||
* Establish a direct LAN connection to a printer.
|
||||
*/
|
||||
@@ -304,7 +289,7 @@ public:
|
||||
* Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull.
|
||||
* Populates the MachineObject's DevFilaSystem with fetched filament data.
|
||||
*/
|
||||
virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; }
|
||||
virtual bool fetch_filament_info(std::string dev_id) { return false; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,16 +9,11 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool moonraker_is_light_name(const std::string& name);
|
||||
|
||||
class MoonrakerPrinterAgent : public IPrinterAgent
|
||||
{
|
||||
public:
|
||||
@@ -76,7 +71,7 @@ public:
|
||||
|
||||
// Pull-mode agent (on-demand filament sync)
|
||||
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; }
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
protected:
|
||||
struct MoonrakerDeviceInfo
|
||||
@@ -90,7 +85,6 @@ protected:
|
||||
std::string dev_name;
|
||||
std::string version;
|
||||
std::string klippy_state;
|
||||
float nozzle_diameter = 0.0f;
|
||||
bool use_ssl = false;
|
||||
} device_info;
|
||||
|
||||
@@ -111,21 +105,14 @@ protected:
|
||||
// Methods that derived classes may need to override or access
|
||||
virtual bool init_device_info(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl);
|
||||
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
static float parse_nozzle_diameter(const nlohmann::json& response);
|
||||
|
||||
// State access for derived classes
|
||||
mutable std::recursive_mutex state_mutex;
|
||||
|
||||
// Counts detached fetch_filament_info() background threads currently touching `this`
|
||||
// (see QidiPrinterAgent::fetch_filament_info). Those threads hold a raw `this` with no
|
||||
// other lifetime protection, so the destructor waits for this to reach 0 before any part
|
||||
// of the object is torn down — see ~MoonrakerPrinterAgent().
|
||||
std::atomic<int> filament_fetch_in_flight{0};
|
||||
|
||||
// Helpers
|
||||
bool is_numeric(const std::string& value);
|
||||
std::string normalize_base_url(std::string host, const std::string& port);
|
||||
std::string sanitize_filename(const std::string& filename) const;
|
||||
std::string sanitize_filename(const std::string& filename);
|
||||
std::string join_url(const std::string& base_url, const std::string& path) const;
|
||||
|
||||
// Trim whitespace and convert to uppercase
|
||||
@@ -134,22 +121,6 @@ protected:
|
||||
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility
|
||||
static std::string map_filament_type_to_generic_id(const std::string& filament_type);
|
||||
|
||||
// Send a G-code script via Moonraker (/printer/gcode/script)
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode,
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
bool post_print_action(const std::string& action) const;
|
||||
bool post_print_action(const std::string& action,
|
||||
const std::string& base_url, const std::string& api_key) const;
|
||||
|
||||
// Send one JSON-RPC call over a short-lived Moonraker websocket. Returns true when the
|
||||
// request was written; it never waits for a reply.
|
||||
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
|
||||
|
||||
// why: a printer with no /server/webcams/list entry can still name its stream directly;
|
||||
// returning empty (the default) keeps the normal Moonraker discovery path.
|
||||
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
|
||||
|
||||
private:
|
||||
int handle_request(const std::string& dev_id, const std::string& json_str);
|
||||
int send_version_info(const std::string& dev_id);
|
||||
@@ -157,7 +128,7 @@ private:
|
||||
|
||||
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const;
|
||||
bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const;
|
||||
bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation);
|
||||
bool send_gcode(const std::string& dev_id, const std::string& gcode) const;
|
||||
|
||||
void announce_printhost_device();
|
||||
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
|
||||
@@ -166,8 +137,7 @@ private:
|
||||
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key);
|
||||
void stop_status_stream();
|
||||
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key);
|
||||
void handle_ws_message(std::string dev_id, std::string payload, std::string base_url, std::string api_key);
|
||||
void refresh_thumbnail_url(std::string base_url, std::string api_key);
|
||||
void handle_ws_message(const std::string& dev_id, const std::string& payload);
|
||||
void update_status_cache(const nlohmann::json& updates);
|
||||
nlohmann::json build_print_payload_locked() const;
|
||||
|
||||
@@ -181,10 +151,9 @@ private:
|
||||
const std::string& base_url, const std::string& api_key,
|
||||
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
|
||||
|
||||
// Start a print of a previously uploaded G-code file (path relative to the
|
||||
// Moonraker gcodes root).
|
||||
bool start_print_file(const std::string& base_url, const std::string& api_key,
|
||||
const std::string& filename, std::string& error_msg) const;
|
||||
// JSON-RPC helper
|
||||
bool send_jsonrpc_command(const std::string& base_url, const std::string& api_key,
|
||||
const nlohmann::json& request, std::string& response) const;
|
||||
|
||||
// Connection thread management
|
||||
void perform_connection_async(const std::string& dev_id,
|
||||
@@ -220,26 +189,15 @@ private:
|
||||
|
||||
mutable std::recursive_mutex payload_mutex;
|
||||
nlohmann::json status_cache;
|
||||
// note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found
|
||||
std::string thumbnail_filename;
|
||||
std::string thumbnail_url;
|
||||
std::string webcam_stream_url;
|
||||
unsigned thumbnail_lookup_attempts = 0;
|
||||
|
||||
std::atomic<int> next_jsonrpc_id{1};
|
||||
std::set<std::string> available_objects; // Track for feature detection
|
||||
bool assumed_light_on = false;
|
||||
|
||||
std::atomic<bool> ws_stop{false};
|
||||
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
|
||||
std::atomic<uint64_t> ws_last_emit_ms{0};
|
||||
std::thread ws_thread;
|
||||
|
||||
// AMS/filament refresh cadence, independent of telemetry dispatch so a steady
|
||||
// stream of status updates can't starve it (ws_last_emit_ms is reset by those).
|
||||
static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000;
|
||||
std::atomic<uint64_t> ams_last_fetch_ms{0};
|
||||
|
||||
// Throttling configuration for WebSocket updates
|
||||
// Critical changes (state transitions) dispatch immediately; telemetry is throttled
|
||||
static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry
|
||||
@@ -249,15 +207,7 @@ private:
|
||||
// Connection thread management
|
||||
std::atomic<uint64_t> connect_generation{0};
|
||||
std::thread connect_thread;
|
||||
mutable std::recursive_mutex connect_mutex;
|
||||
|
||||
void enqueue_command(std::function<void()> fn);
|
||||
void run_command_worker();
|
||||
std::thread cmd_thread;
|
||||
std::deque<std::function<void()>> cmd_queue;
|
||||
std::mutex cmd_mutex;
|
||||
std::condition_variable cmd_cv;
|
||||
bool cmd_stop = false;
|
||||
std::recursive_mutex connect_mutex;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -767,34 +767,6 @@ 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::command_start_camera(std::string dev_id)
|
||||
{
|
||||
if (m_printer_agent)
|
||||
return m_printer_agent->command_start_camera(dev_id);
|
||||
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)
|
||||
@@ -949,10 +921,10 @@ FilamentSyncMode NetworkAgent::get_filament_sync_mode() const
|
||||
return FilamentSyncMode::none;
|
||||
}
|
||||
|
||||
bool NetworkAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
|
||||
bool NetworkAgent::fetch_filament_info(std::string dev_id)
|
||||
{
|
||||
if (m_printer_agent) {
|
||||
return m_printer_agent->fetch_filament_info(dev_id, sync_mode);
|
||||
return m_printer_agent->fetch_filament_info(dev_id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,10 +142,6 @@ 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 command_start_camera(std::string dev_id);
|
||||
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);
|
||||
@@ -168,7 +164,7 @@ public:
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
|
||||
FilamentSyncMode get_filament_sync_mode() const;
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull);
|
||||
bool fetch_filament_info(std::string dev_id);
|
||||
int request_bind_ticket(std::string* ticket);
|
||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback);
|
||||
|
||||
|
||||
@@ -16,10 +16,8 @@
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/I18N.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <wx/msgdlg.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
@@ -316,21 +314,6 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
|
||||
std::shared_ptr<IPrinterAgent> cached_agent;
|
||||
|
||||
auto reject_conflicting_capability = [plugin_key, capability_name](const std::string& error_message) {
|
||||
if (!wxTheApp || GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
GUI::wxGetApp().CallAfter([plugin_key, capability_name, error_message]() {
|
||||
if (GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
manager.set_plugin_error(plugin_key, error_message);
|
||||
// note: the unload callback triggered by disabling will call deregister,
|
||||
// which will be a no-op since the printer agent is never registered
|
||||
manager.set_capability_enabled({PluginCapabilityType::PrinterConnection, capability_name, plugin_key}, false);
|
||||
wxMessageBox(wxString::FromUTF8(error_message.c_str()), _L("Plugins"), wxOK | wxICON_WARNING, GUI::wxGetApp().GetTopWindow());
|
||||
});
|
||||
};
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_registry_mutex);
|
||||
|
||||
@@ -349,10 +332,9 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
auto& python_agent_ids = get_python_printer_agent_ids();
|
||||
for (const auto& pair : python_agent_ids) {
|
||||
if (pair.first != capability_key && pair.second == info.id) {
|
||||
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
|
||||
"' is already registered by capability '" + pair.first.second + "' from plugin '" + pair.first.first + "'.";
|
||||
BOOST_LOG_TRIVIAL(warning) << error_message;
|
||||
reject_conflicting_capability(error_message);
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id
|
||||
<< "' already registered by capability '" << pair.first.second << "' from plugin '"
|
||||
<< pair.first.first << "'";
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -372,22 +354,12 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
||||
|
||||
auto& agents = get_printer_agents();
|
||||
auto agent_it = agents.find(info.id);
|
||||
// why: reject only when the ID is owned by SOMEONE ELSE - a built-in has an empty
|
||||
// plugin_identifier, another plugin/capability has a different plugin_full_ref. When it
|
||||
// IS the same plugin_full_ref, this capability is just re-registering itself, so fall
|
||||
// through and refresh.
|
||||
if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) {
|
||||
const std::string error_message = "Printer-agent '" + info.name + "' could not be enabled: agent ID '" + info.id +
|
||||
"' is already registered by '" + agent_it->second.display_name + "'.";
|
||||
BOOST_LOG_TRIVIAL(warning) << error_message;
|
||||
reject_conflicting_capability(error_message);
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id
|
||||
<< "' already registered by '" << agent_it->second.display_name << "'";
|
||||
return;
|
||||
}
|
||||
|
||||
// why: insert_or_assign, not emplace - reaching here means the ID is new, or the same
|
||||
// capability is re-registering (same plugin_full_ref). In the re-register case we WANT to
|
||||
// overwrite so the factory closure points at the current live capability instance; emplace
|
||||
// would silently keep the stale entry.
|
||||
agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory)));
|
||||
python_agent_ids[capability_key] = info.id;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -26,15 +25,6 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
|
||||
return false;
|
||||
}
|
||||
|
||||
// RAII decrement for MoonrakerPrinterAgent::filament_fetch_in_flight — guarantees the
|
||||
// counter drops back down on every exit path (early return or fall-through) inside the
|
||||
// detached fetch thread below, so ~MoonrakerPrinterAgent()'s wait loop can't stall forever.
|
||||
struct InFlightGuard
|
||||
{
|
||||
std::atomic<int>& counter;
|
||||
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
const std::string QidiPrinterAgent_VERSION = "0.0.1";
|
||||
@@ -48,134 +38,42 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
|
||||
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
|
||||
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id)
|
||||
{
|
||||
if (sync_mode != get_filament_sync_mode())
|
||||
std::string error;
|
||||
|
||||
// 1. Fetch device info and infer series_id
|
||||
std::string series_id;
|
||||
{
|
||||
MoonrakerDeviceInfo info;
|
||||
if (fetch_device_info(device_info.base_url, device_info.api_key, info, error)) {
|
||||
series_id = infer_series_id(info.model_id, info.dev_name);
|
||||
}
|
||||
}
|
||||
if (series_id.empty()) {
|
||||
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
|
||||
series_id = infer_series_id(device_info.model_id, device_info.model_name);
|
||||
}
|
||||
|
||||
// 2. Fetch filament dictionary
|
||||
QidiFilamentDict dict;
|
||||
if (!fetch_filament_dict(device_info.base_url, device_info.api_key, dict, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
|
||||
}
|
||||
|
||||
// 3. Fetch slot info and build AmsTrayData directly
|
||||
std::vector<AmsTrayData> trays;
|
||||
int box_count = 0;
|
||||
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snapshot only what the fetch needs, rather than reading device_info live from the
|
||||
// background thread below — device_info can be concurrently rewritten by a reconnect
|
||||
// on another thread while this fetch is still in flight.
|
||||
std::string base_url = device_info.base_url;
|
||||
std::string api_key = device_info.api_key;
|
||||
std::string model_id = device_info.model_id;
|
||||
std::string model_name = device_info.model_name;
|
||||
|
||||
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
std::thread([this, base_url, api_key, model_id, model_name]() {
|
||||
InFlightGuard guard{filament_fetch_in_flight};
|
||||
|
||||
std::string error;
|
||||
|
||||
// 1. Fetch device info and infer series_id
|
||||
std::string series_id;
|
||||
{
|
||||
MoonrakerDeviceInfo info;
|
||||
if (fetch_device_info(base_url, api_key, info, error)) {
|
||||
series_id = infer_series_id(info.model_id, info.dev_name);
|
||||
}
|
||||
}
|
||||
if (series_id.empty()) {
|
||||
// Fall back to the configured Orca model if Moonraker doesn't expose a usable identifier.
|
||||
series_id = infer_series_id(model_id, model_name);
|
||||
}
|
||||
|
||||
// 2. Fetch filament dictionary
|
||||
QidiFilamentDict dict;
|
||||
if (!fetch_filament_dict(base_url, api_key, dict, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch filament dict: " << error;
|
||||
}
|
||||
|
||||
// 3. Fetch slot info and build AmsTrayData directly
|
||||
std::vector<AmsTrayData> trays;
|
||||
int box_count = 0;
|
||||
if (!fetch_slot_info(base_url, api_key, dict, series_id, trays, box_count, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Build the AMS payload
|
||||
build_ams_payload(box_count, box_count * 4 - 1, trays);
|
||||
}).detach();
|
||||
// 4. Build the AMS payload
|
||||
build_ams_payload(box_count, box_count * 4 - 1, trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
|
||||
{
|
||||
// enable_box mirrors task_use_ams: engage the multi-color box only when this
|
||||
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
|
||||
// if firmware treats enable_box as "a box exists" rather than "use it this job",
|
||||
// switch this gate to HasAms()/box_count instead.)
|
||||
const int enable = params.task_use_ams ? 1 : 0;
|
||||
if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box";
|
||||
return false;
|
||||
}
|
||||
|
||||
// When the box isn't used this job, leave the existing value_t<tool> slot
|
||||
// assignments untouched (enable_box=0 is enough to disengage it).
|
||||
if (!enable)
|
||||
return true;
|
||||
|
||||
if (params.ams_mapping.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
|
||||
return true;
|
||||
}
|
||||
|
||||
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
|
||||
// physical box slot (-1 = unmapped). Mirror it onto the printer's value_t<tool>
|
||||
// variables: SAVE_VARIABLE VARIABLE=value_t<tool> VALUE='slot<n>'.
|
||||
auto mapping = nlohmann::json::parse(params.ams_mapping, nullptr, /*allow_exceptions*/ false);
|
||||
if (mapping.is_discarded() || !mapping.is_array()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: invalid ams_mapping: " << params.ams_mapping;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t tool = 0; tool < mapping.size(); ++tool) {
|
||||
if (!mapping[tool].is_number_integer())
|
||||
continue;
|
||||
const int slot = mapping[tool].get<int>();
|
||||
if (slot < 0)
|
||||
continue; // unmapped filament — skip
|
||||
const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
|
||||
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
|
||||
if (!send_gcode(device_info.dev_id, gcode)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_local_print(std::move(params), update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_print(std::move(params), update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_WR_UPLOAD_FTP_FAILED;
|
||||
return MoonrakerPrinterAgent::start_local_print_with_record(std::move(params), update_fn, cancel_fn, wait_fn);
|
||||
}
|
||||
|
||||
int QidiPrinterAgent::start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
|
||||
{
|
||||
if (!apply_box_mapping(params))
|
||||
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_FAILED;
|
||||
return MoonrakerPrinterAgent::start_sdcard_print(std::move(params), update_fn, cancel_fn);
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
const QidiFilamentDict& dict,
|
||||
@@ -220,10 +118,20 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
if (!parse_slot_response(response_body, status, variables, error))
|
||||
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") ||
|
||||
!json["result"]["status"]["save_variables"].contains("variables")) {
|
||||
error = "Unexpected JSON structure";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& variables = json["result"]["status"]["save_variables"]["variables"];
|
||||
auto& status = json["result"]["status"];
|
||||
|
||||
box_count = variables.value("box_count", 1);
|
||||
if (box_count < 0) {
|
||||
@@ -298,31 +206,6 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
|
||||
nlohmann::json& status,
|
||||
nlohmann::json& variables,
|
||||
std::string& error)
|
||||
{
|
||||
auto json = nlohmann::json::parse(response_body, nullptr, false, true);
|
||||
if (json.is_discarded()) {
|
||||
error = "Invalid JSON response";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!json.is_object() || !json.contains("result") || !json["result"].is_object() || !json["result"].contains("status") ||
|
||||
!json["result"]["status"].is_object() || !json["result"]["status"].contains("save_variables") ||
|
||||
!json["result"]["status"]["save_variables"].is_object() || !json["result"]["status"]["save_variables"].contains("variables") ||
|
||||
!json["result"]["status"]["save_variables"]["variables"].is_object()) {
|
||||
// why: Qidi firmware may send null here, but json::value() throws for it.
|
||||
error = "Unexpected JSON structure: save_variables.variables must be an object";
|
||||
return false;
|
||||
}
|
||||
|
||||
status = json["result"]["status"];
|
||||
variables = status["save_variables"]["variables"];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url,
|
||||
const std::string& api_key,
|
||||
QidiFilamentDict& dict,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#ifndef __QIDI_PRINTER_AGENT_HPP__
|
||||
#define __QIDI_PRINTER_AGENT_HPP__
|
||||
|
||||
#include "IPrinterAgent.hpp"
|
||||
#include "MoonrakerPrinterAgent.hpp"
|
||||
#include "nlohmann/json_fwd.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -21,25 +19,9 @@ public:
|
||||
AgentInfo get_agent_info() override { return get_agent_info_static(); }
|
||||
|
||||
// Override filament sync (Qidi-specific implementation)
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
|
||||
|
||||
static bool parse_slot_response(const std::string& response_body,
|
||||
nlohmann::json& status,
|
||||
nlohmann::json& variables,
|
||||
std::string& error);
|
||||
|
||||
// Print operations — emit QiDi multi-color box config, then delegate to base.
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override;
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override;
|
||||
|
||||
FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::subscription; }
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
private:
|
||||
// Push enable_box + value_t<tool> SAVE_VARIABLEs before a print starts.
|
||||
// Returns false if any command fails (caller should abort the print).
|
||||
bool apply_box_mapping(const PrintParams& params) const;
|
||||
struct QidiFilamentDict
|
||||
{
|
||||
std::map<int, std::string> colors;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#include "SnapmakerPrinterAgent.hpp"
|
||||
#include "Http.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <thread>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -71,69 +67,6 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
|
||||
|
||||
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
|
||||
|
||||
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
|
||||
{
|
||||
(void) dev_id;
|
||||
// why: the printer executes this over the websocket but answers only over MQTT, and the
|
||||
// call itself blocks on socket I/O - it fires from the camera view's renew timer on the UI
|
||||
// thread, so run it detached rather than block the caller on a reply that never comes.
|
||||
// note: interval is dead time in SECONDS on top of a ~0.455 s capture, so 0 is the 2.15 fps
|
||||
// ceiling (1 measures 0.63 fps), and it cannot be changed while a capture task is running.
|
||||
std::thread([this] {
|
||||
send_ws_rpc("camera.start_monitor",
|
||||
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
|
||||
}).detach();
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
std::string SnapmakerPrinterAgent::webcam_stream_override(const std::string& base_url) const
|
||||
{
|
||||
const std::string snapshot_url = join_url(base_url, "/server/files/camera/monitor.jpg");
|
||||
|
||||
// why: one wrapper file per printer - two U1s would otherwise overwrite each other's URL.
|
||||
const boost::filesystem::path page = boost::filesystem::path(data_dir()) / "cache" /
|
||||
("snapmaker_camera_" + sanitize_filename(device_info.dev_ip) + ".html");
|
||||
|
||||
// why: the printer writes a still JPEG at ~2 fps, so the page polls it with a cache buster
|
||||
// instead of consuming a stream. Chaining the next request off onload (never a bare
|
||||
// setInterval) keeps requests from piling up when the printer is slow to answer.
|
||||
const std::string html =
|
||||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Camera</title><style>"
|
||||
"html,body{margin:0;height:100%;background:#000;overflow:hidden}"
|
||||
"img{width:100%;height:100%;object-fit:contain;display:block}</style></head>"
|
||||
"<body><img id=\"frame\" alt=\"\"><script>\n"
|
||||
"var src=\"" + snapshot_url + "\";\n"
|
||||
"var img=document.getElementById(\"frame\");\n"
|
||||
"function next(){img.src=src+\"?_nocache=\"+Date.now()+\"_\"+Math.floor(Math.random()*10000);}\n"
|
||||
"img.onload=function(){setTimeout(next,250);};\n"
|
||||
"img.onerror=function(){setTimeout(next,1000);};\n"
|
||||
"next();\n"
|
||||
"</script></body></html>\n";
|
||||
|
||||
std::string write_error;
|
||||
try {
|
||||
boost::filesystem::create_directories(page.parent_path());
|
||||
boost::nowide::ofstream out(page.string().c_str(), std::ios::binary | std::ios::trunc);
|
||||
out << html;
|
||||
out.close();
|
||||
// note: an ofstream reports a failed write in its state, not by throwing.
|
||||
if (!out) {
|
||||
write_error = "write failed";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
write_error = e.what();
|
||||
}
|
||||
if (!write_error.empty()) {
|
||||
// why: no wrapper means no camera - a raw monitor.jpg URL would render one frozen frame
|
||||
// and read as a broken feed, so fall back to showing nothing and say why in the log.
|
||||
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent: could not write camera page " << page.string()
|
||||
<< ": " << write_error;
|
||||
return {};
|
||||
}
|
||||
|
||||
return "file://" + page.generic_string();
|
||||
}
|
||||
|
||||
AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
|
||||
{
|
||||
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
|
||||
@@ -169,11 +102,8 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
|
||||
return base;
|
||||
}
|
||||
|
||||
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
|
||||
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id)
|
||||
{
|
||||
if (sync_mode != get_filament_sync_mode())
|
||||
return false;
|
||||
|
||||
std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect");
|
||||
|
||||
std::string response_body;
|
||||
|
||||
@@ -15,11 +15,7 @@ public:
|
||||
static AgentInfo get_agent_info_static();
|
||||
AgentInfo get_agent_info() override { return get_agent_info_static(); }
|
||||
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
|
||||
int command_start_camera(std::string dev_id) override;
|
||||
|
||||
protected:
|
||||
std::string webcam_stream_override(const std::string& base_url) const override;
|
||||
bool fetch_filament_info(std::string dev_id) override;
|
||||
|
||||
private:
|
||||
// Combine filament_type + filament_sub_type into a unified type string
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <slic3r/GUI/MsgDialog.hpp>
|
||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||
#include <slic3r/GUI/NotificationManager.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/pybind11.h>
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <wx/defs.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
@@ -44,16 +46,20 @@ namespace {
|
||||
struct GilSafeCallable
|
||||
{
|
||||
py::object fn;
|
||||
std::atomic_bool active{true};
|
||||
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
|
||||
void disable()
|
||||
{
|
||||
active.store(false, std::memory_order_release);
|
||||
PythonGILState gil;
|
||||
if (gil)
|
||||
fn = py::object();
|
||||
else
|
||||
(void) fn.release();
|
||||
}
|
||||
~GilSafeCallable()
|
||||
{
|
||||
if (fn) {
|
||||
PythonGILState gil;
|
||||
if (gil)
|
||||
fn = py::object();
|
||||
else
|
||||
(void) fn.release();
|
||||
}
|
||||
disable();
|
||||
}
|
||||
};
|
||||
using CallablePtr = std::shared_ptr<GilSafeCallable>;
|
||||
@@ -166,11 +172,34 @@ public:
|
||||
}
|
||||
return out;
|
||||
}
|
||||
void bind_callback(const CallablePtr& callback, const std::string& plugin_key)
|
||||
{
|
||||
if (!callback)
|
||||
return;
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
m_callbacks[plugin_key].push_back(callback);
|
||||
}
|
||||
std::vector<CallablePtr> take_callbacks_for_plugin(const std::string& plugin_key)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
auto it = m_callbacks.find(plugin_key);
|
||||
if (it == m_callbacks.end())
|
||||
return {};
|
||||
std::vector<CallablePtr> callbacks;
|
||||
callbacks.reserve(it->second.size());
|
||||
for (const std::weak_ptr<GilSafeCallable>& weak_callback : it->second) {
|
||||
if (auto callback = weak_callback.lock())
|
||||
callbacks.push_back(std::move(callback));
|
||||
}
|
||||
m_callbacks.erase(it);
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex m_mtx;
|
||||
std::unordered_map<int, wxWindow*> m_resources;
|
||||
std::unordered_map<int, std::string> m_owners;
|
||||
std::unordered_map<std::string, std::vector<std::weak_ptr<GilSafeCallable>>> m_callbacks;
|
||||
int m_next_id{1};
|
||||
};
|
||||
|
||||
@@ -448,6 +477,46 @@ void progress_close(int id)
|
||||
});
|
||||
}
|
||||
|
||||
void plater_notification(NotificationManager::NotificationLevel notification_level, const std::string& text,
|
||||
const std::string& hypertext, py::object on_click)
|
||||
{
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
CallablePtr holder = make_holder(std::move(on_click));
|
||||
if (holder)
|
||||
UiRegistry::instance().bind_callback(holder, plugin_key);
|
||||
|
||||
std::function<bool(wxEvtHandler*)> callback;
|
||||
if (holder) {
|
||||
callback = [holder](wxEvtHandler*) -> bool {
|
||||
if (!holder->active.load(std::memory_order_acquire))
|
||||
return false;
|
||||
|
||||
PythonGILState gil;
|
||||
if (!gil)
|
||||
return false;
|
||||
try {
|
||||
py::object result = holder->fn();
|
||||
return result.is_none() || result.cast<bool>();
|
||||
} catch (py::error_already_set& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what();
|
||||
return false;
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised an unknown exception";
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
run_on_ui_blocking([notification_level, text, hypertext, callback = std::move(callback)]() mutable {
|
||||
wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::CustomNotification, notification_level, text,
|
||||
hypertext, std::move(callback));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
@@ -530,6 +599,23 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
||||
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
|
||||
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
|
||||
"Create a native progress dialog and return a ProgressDialog handle.");
|
||||
|
||||
py::enum_<NotificationManager::NotificationLevel>(ui, "NotificationLevel")
|
||||
.value("ProgressBarNotificationLevel", NotificationManager::NotificationLevel::ProgressBarNotificationLevel)
|
||||
.value("HintNotificationLevel", NotificationManager::NotificationLevel::HintNotificationLevel)
|
||||
.value("RegularNotificationLevel", NotificationManager::NotificationLevel::RegularNotificationLevel)
|
||||
.value("PrintInfoNotificationLevel", NotificationManager::NotificationLevel::PrintInfoNotificationLevel)
|
||||
.value("PrintInfoShortNotificationLevel", NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel)
|
||||
.value("ImportantNotificationLevel", NotificationManager::NotificationLevel::ImportantNotificationLevel)
|
||||
.value("WarningNotificationLevel", NotificationManager::NotificationLevel::WarningNotificationLevel)
|
||||
.value("SeriousWarningNotificationLevel", NotificationManager::NotificationLevel::SeriousWarningNotificationLevel)
|
||||
.value("ErrorNotificationLevel", NotificationManager::NotificationLevel::ErrorNotificationLevel)
|
||||
.export_values();
|
||||
|
||||
ui.def("push_notification", &plater_notification, py::arg("notification_level"), py::arg("text"),
|
||||
py::arg("hyper_text") = "", py::arg("on_click") = py::none(),
|
||||
"Push a plater notification. hyper_text is an underlined label; on_click() is called when it is clicked "
|
||||
"and may return True to close the notification.");
|
||||
}
|
||||
|
||||
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
@@ -538,6 +624,9 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
||||
return;
|
||||
|
||||
auto teardown = [plugin_key]() {
|
||||
for (auto& callback : UiRegistry::instance().take_callbacks_for_plugin(plugin_key))
|
||||
callback->disable();
|
||||
|
||||
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
|
||||
// forced teardown (intended); the resource destructor still cleans the registry.
|
||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||
|
||||
@@ -30,50 +30,54 @@ public:
|
||||
|
||||
AgentInfo get_agent_info() override = 0;
|
||||
|
||||
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
bool start_discovery(bool start, bool sending) override = 0;
|
||||
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
|
||||
std::string get_user_selected_machine() override = 0;
|
||||
int set_user_selected_machine(std::string dev_id) override = 0;
|
||||
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
FilamentSyncMode get_filament_sync_mode() const override = 0;
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override = 0;
|
||||
int connect_printer(
|
||||
std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
|
||||
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
|
||||
bool start_discovery(bool start, bool sending) override = 0;
|
||||
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
|
||||
std::string get_user_selected_machine() override = 0;
|
||||
int set_user_selected_machine(std::string dev_id) override = 0;
|
||||
int start_send_gcode_to_sdcard(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn) override = 0;
|
||||
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
FilamentSyncMode get_filament_sync_mode() const override = 0;
|
||||
bool fetch_filament_info(std::string dev_id) override = 0;
|
||||
|
||||
int check_cert() override = 0;
|
||||
void install_device_cert(std::string dev_id, bool lan_only) override = 0;
|
||||
int ping_bind(std::string ping_code) override = 0;
|
||||
int check_cert() override = 0;
|
||||
void install_device_cert(std::string dev_id, bool lan_only) override = 0;
|
||||
int ping_bind(std::string ping_code) override = 0;
|
||||
int bind(std::string dev_ip,
|
||||
std::string dev_id,
|
||||
std::string dev_model,
|
||||
std::string sec_link,
|
||||
std::string timezone,
|
||||
bool improved,
|
||||
OnUpdateStatusFn update_fn) override = 0;
|
||||
int unbind(std::string dev_id) override = 0;
|
||||
OnUpdateStatusFn update_fn) override = 0;
|
||||
int unbind(std::string dev_id) override = 0;
|
||||
// request_bind_ticket has a std::string* out-param that cannot round-trip through a
|
||||
// pybind11 override directly; the trampoline wraps it (the Python plugin returns a
|
||||
// (result, ticket) tuple), so it stays pure here like the rest.
|
||||
int request_bind_ticket(std::string* ticket) override = 0;
|
||||
int request_bind_ticket(std::string* ticket) override = 0;
|
||||
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override = 0;
|
||||
int set_server_callback(OnServerErrFn fn) override = 0;
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
|
||||
int set_server_callback(OnServerErrFn fn) override = 0;
|
||||
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
|
||||
int start_local_print_with_record(PrintParams params,
|
||||
OnUpdateStatusFn update_fn,
|
||||
WasCancelledFn cancel_fn,
|
||||
OnWaitFn wait_fn) override = 0;
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
OnWaitFn wait_fn) override = 0;
|
||||
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
|
||||
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
|
||||
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
|
||||
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
|
||||
int set_on_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_user_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
|
||||
int set_on_local_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
|
||||
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
|
||||
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
|
||||
int set_on_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_user_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
|
||||
int set_on_local_message_fn(OnMessageFn fn) override = 0;
|
||||
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -96,7 +96,7 @@ public:
|
||||
get_filament_sync_mode);
|
||||
}
|
||||
|
||||
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override
|
||||
bool fetch_filament_info(std::string dev_id) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
|
||||
|
||||
@@ -2,16 +2,12 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests_main.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_device_progress.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
test_plugin_capability_config.cpp
|
||||
test_plugin_config.cpp
|
||||
test_plugin_capabilities_in_use.cpp
|
||||
test_plugin_status.cpp
|
||||
test_printer_agent.cpp
|
||||
test_qidi_printer_agent.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
@@ -59,22 +55,3 @@ elseif (APPLE)
|
||||
endif()
|
||||
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
# why: the loader runs on a detached worker thread, so its Python interpreter
|
||||
# ownership model cannot share the embedded interpreter in the main test binary.
|
||||
add_executable(printer_agent_plugin_tests test_printer_agent_plugin.cpp)
|
||||
|
||||
if (MSVC)
|
||||
target_link_libraries(printer_agent_plugin_tests Setupapi.lib)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(printer_agent_plugin_tests test_common libslic3r_gui libslic3r pybind11::embed Catch2::Catch2)
|
||||
set_property(TARGET printer_agent_plugin_tests PROPERTY FOLDER "tests")
|
||||
|
||||
# why: the existing target stages the complete bundled Python home under
|
||||
# python/, which is the layout PythonInterpreter discovers beside the test exe.
|
||||
add_dependencies(printer_agent_plugin_tests ${_TEST_NAME}_tests)
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
orcaslicer_discover_tests(printer_agent_plugin_tests)
|
||||
|
||||
@@ -26,37 +26,6 @@
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
const json empty_slot = json::parse(R"({
|
||||
"ams": {
|
||||
"tray_exist_bits": "0",
|
||||
"ams": [ { "id": "0", "info": "00000001", "tray": [
|
||||
{ "id": "0", "tray_slot_placeholder": "1", "tray_color": "00000000" }
|
||||
] } ]
|
||||
}
|
||||
})");
|
||||
DevFilaSystemParser::ParseV1_0(empty_slot, &obj, obj.GetFilaSystem().get(), false);
|
||||
|
||||
DevAmsTray* tray = obj.GetFilaSystem()->GetAmsTray("0", "0");
|
||||
REQUIRE(tray != nullptr);
|
||||
REQUIRE(tray->is_slot_placeholder);
|
||||
|
||||
const json loaded_slot = json::parse(R"({
|
||||
"ams": {
|
||||
"tray_exist_bits": "1",
|
||||
"ams": [ { "id": "0", "info": "00000001", "tray": [
|
||||
{ "id": "0", "tray_color": "FF0000FF" }
|
||||
] } ]
|
||||
}
|
||||
})");
|
||||
DevFilaSystemParser::ParseV1_0(loaded_slot, &obj, obj.GetFilaSystem().get(), false);
|
||||
|
||||
CHECK_FALSE(tray->is_slot_placeholder);
|
||||
}
|
||||
|
||||
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// why: match the GUI include order to avoid rpcndr.h byte/std::byte
|
||||
// ambiguity in the Windows COM headers.
|
||||
// why: wx/timer.h must precede DeviceManager.hpp because
|
||||
// DeviceErrorDialog.hpp uses wxTimerEvent.
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Integer progress reaches the shared subtask", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(37));
|
||||
|
||||
REQUIRE(machine.mc_print_percent == 37);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
CHECK(subtask->task_progress == 37);
|
||||
}
|
||||
|
||||
TEST_CASE("String progress reaches the shared subtask", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json("41"));
|
||||
|
||||
REQUIRE(machine.mc_print_percent == 41);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
CHECK(subtask->task_progress == 41);
|
||||
}
|
||||
|
||||
TEST_CASE("Floating-point progress preserves the previous shared value", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(29));
|
||||
REQUIRE(machine.mc_print_percent == 29);
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
REQUIRE(subtask->task_progress == 29);
|
||||
|
||||
machine.update_print_progress(json(29.5));
|
||||
BBLSubTask* current_subtask = machine.get_subtask();
|
||||
REQUIRE(current_subtask != nullptr);
|
||||
REQUIRE(current_subtask == subtask);
|
||||
CHECK(machine.mc_print_percent == 29);
|
||||
CHECK(current_subtask->task_progress == 29);
|
||||
}
|
||||
|
||||
TEST_CASE("Unsupported progress values leave a fresh machine unchanged", "[DeviceManager][Progress]")
|
||||
{
|
||||
SECTION("boolean") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json(true));
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("null") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json(nullptr));
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("object") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json::object());
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
SECTION("array") {
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
machine.update_print_progress(json::array());
|
||||
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Malformed string progress leaves a fresh machine unchanged", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
CHECK_THROWS_AS(machine.update_print_progress(json("not-a-percent")), std::invalid_argument);
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("Zero progress replaces active shared progress", "[DeviceManager][Progress]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
|
||||
machine.update_print_progress(json(63));
|
||||
BBLSubTask* subtask = machine.get_subtask();
|
||||
REQUIRE(subtask != nullptr);
|
||||
REQUIRE(machine.mc_print_percent == 63);
|
||||
REQUIRE(subtask->task_progress == 63);
|
||||
|
||||
machine.set_print_state("FAILED");
|
||||
machine.update_print_progress(json(0));
|
||||
|
||||
BBLSubTask* current_subtask = machine.get_subtask();
|
||||
REQUIRE(current_subtask != nullptr);
|
||||
REQUIRE(current_subtask == subtask);
|
||||
REQUIRE(machine.mc_print_percent == 0);
|
||||
CHECK(current_subtask->task_progress == 0);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/GUI/PluginStatus.hpp>
|
||||
|
||||
using Slic3r::GUI::PluginStatus;
|
||||
using Slic3r::GUI::resolve_plugin_status;
|
||||
|
||||
TEST_CASE("resolve_plugin_status precedence", "[plugin][status]") {
|
||||
// the new branch: loaded module + error -> runtime fault, not a load failure.
|
||||
REQUIRE(resolve_plugin_status(/*loading*/ false, /*has_error*/ true, /*is_loaded*/ true) == PluginStatus::RuntimeError);
|
||||
|
||||
// error without a live module is a load-time Error.
|
||||
REQUIRE(resolve_plugin_status(false, true, false) == PluginStatus::Error);
|
||||
|
||||
// loading wins over a pending error so a reload never flashes red.
|
||||
REQUIRE(resolve_plugin_status(true, true, true) == PluginStatus::Loading);
|
||||
|
||||
// healthy loaded plugin.
|
||||
REQUIRE(resolve_plugin_status(false, false, true) == PluginStatus::Activated);
|
||||
|
||||
// nothing loaded, no error.
|
||||
REQUIRE(resolve_plugin_status(false, false, false) == PluginStatus::Inactive);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/Utils/BBLPrinterAgent.hpp>
|
||||
#include <slic3r/Utils/MoonrakerPrinterAgent.hpp>
|
||||
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace py = pybind11;
|
||||
|
||||
class MoonrakerParserProbe : public MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
using MoonrakerPrinterAgent::parse_nozzle_diameter;
|
||||
|
||||
explicit MoonrakerParserProbe(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
|
||||
};
|
||||
|
||||
TEST_CASE("Moonraker parses nozzle diameter from configfile settings", "[unit][moonraker]")
|
||||
{
|
||||
const auto response = nlohmann::json::parse(R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"configfile": {
|
||||
"settings": {
|
||||
"extruder": {
|
||||
"nozzle_diameter": 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})");
|
||||
|
||||
CHECK(MoonrakerParserProbe::parse_nozzle_diameter(response) == Catch::Approx(0.6f));
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker parses nozzle diameter from raw config and tolerates missing data", "[unit][moonraker]")
|
||||
{
|
||||
const auto raw_config_response = nlohmann::json::parse(R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"configfile": {
|
||||
"config": {
|
||||
"extruder": {
|
||||
"nozzle_diameter": "0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})");
|
||||
const auto missing_response = nlohmann::json::object();
|
||||
|
||||
CHECK(MoonrakerParserProbe::parse_nozzle_diameter(raw_config_response) == Catch::Approx(0.8f));
|
||||
CHECK(MoonrakerParserProbe::parse_nozzle_diameter(missing_response) == 0.0f);
|
||||
}
|
||||
|
||||
// why: these builders preserve the Bambu firmware dialect byte-for-byte, including its trailing space.
|
||||
TEST_CASE("unit: BBL AMS gcode builders preserve command bytes", "[unit][bbl]")
|
||||
{
|
||||
CHECK(BBLPrinterAgent::ams_refresh_rfid_gcode("123") == "M620 R123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_calibrate_gcode(123) == "M620 C123 \n");
|
||||
CHECK(BBLPrinterAgent::ams_select_tray_gcode("123") == "M620 P123 \n");
|
||||
}
|
||||
|
||||
// why: an agent without a Bambu-dialect translation must refuse these commands before any network or wx path.
|
||||
TEST_CASE("unit: default AMS commands report not supported", "[unit][moonraker]")
|
||||
{
|
||||
MoonrakerPrinterAgent agent("");
|
||||
|
||||
CHECK(agent.command_ams_refresh_rfid("dev", "123", 1, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_calibrate("dev", 1, 2, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.command_ams_select_tray("dev", "123", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
TEST_CASE("unit: Moonraker light name matching", "[unit][moonraker]")
|
||||
{
|
||||
CHECK(moonraker_is_light_name("caselight"));
|
||||
CHECK(moonraker_is_light_name("LED_STRIP"));
|
||||
CHECK_FALSE(moonraker_is_light_name("beeper"));
|
||||
CHECK(moonraker_is_light_name("FLASHLIGHT_SWITCH"));
|
||||
CHECK(moonraker_is_light_name("MODLELIGHT_SWITCH"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// UNIT - handle_request's not-supported default.
|
||||
// The agent is the only thing that knows what it can translate, so an untranslated
|
||||
// command has to say so instead of returning success and letting the UI believe the
|
||||
// control worked. Guards the inverse too: the pushing namespace is genuinely
|
||||
// satisfied by the websocket status stream, and it re-fires from the keepalive timer
|
||||
// roughly once a second, so it must stay a success or it would raise a dialog on a
|
||||
// timer. Only branches that touch neither the network nor wx are exercised.
|
||||
// ===========================================================================
|
||||
TEST_CASE("unit: Moonraker reports untranslated commands as not supported", "[unit][moonraker]")
|
||||
{
|
||||
MoonrakerPrinterAgent agent("");
|
||||
|
||||
CHECK(agent.send_message("dev", R"({"print":{"command":"ams_change_filament"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.send_message("dev", R"({"system":{"command":"set_door_stat"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
CHECK(agent.send_message("dev", R"({"xcam":{"command":"xcam_control_set"}})", 0, 0) ==
|
||||
ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
|
||||
CHECK(agent.send_message("dev", R"({"pushing":{"command":"pushall"}})", 0, 0) == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK(agent.send_message("dev", R"({"pushing":{"command":"start"}})", 0, 0) == BAMBU_NETWORK_SUCCESS);
|
||||
|
||||
// why: malformed input is a different failure than an untranslated command, and the
|
||||
// default must not swallow it into a misleading not-supported verdict.
|
||||
CHECK(agent.send_message("dev", "{not json", 0, 0) == BAMBU_NETWORK_ERR_INVALID_RESULT);
|
||||
}
|
||||
|
||||
TEST_CASE("unit: MoonrakerPrinterAgent::fetch_filament_info is fire-and-forget and dispatches to the derived override",
|
||||
"[unit][moonraker]")
|
||||
{
|
||||
class RecordingAgent : public Slic3r::MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
explicit RecordingAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
|
||||
|
||||
std::atomic<bool> invoked{false};
|
||||
std::promise<void> release_gate;
|
||||
std::promise<void> done_promise;
|
||||
|
||||
bool do_fetch_filament_info(std::string /*dev_id*/) override
|
||||
{
|
||||
invoked.store(true);
|
||||
// Block here until the test explicitly releases us, proving the caller
|
||||
// (fetch_filament_info) does not wait for this to run.
|
||||
release_gate.get_future().wait();
|
||||
done_promise.set_value();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
auto agent = std::make_shared<RecordingAgent>(std::string{});
|
||||
auto done_future = agent->done_promise.get_future();
|
||||
|
||||
bool immediate_result = agent->fetch_filament_info("test-dev");
|
||||
|
||||
// fetch_filament_info must return before do_fetch_filament_info completes — prove
|
||||
// it by confirming the background call is still blocked on the gate right now.
|
||||
REQUIRE(immediate_result == true);
|
||||
REQUIRE(done_future.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout);
|
||||
|
||||
// Now let the background call finish and confirm it actually ran (polymorphic dispatch).
|
||||
agent->release_gate.set_value();
|
||||
REQUIRE(done_future.wait_for(std::chrono::seconds(2)) == std::future_status::ready);
|
||||
REQUIRE(agent->invoked.load() == true);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// UNIT - printer-agent registry duplicate handling.
|
||||
// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in
|
||||
// or previously registered agent.
|
||||
// ===========================================================================
|
||||
TEST_CASE("unit: printer-agent registry register / lookup / duplicate-reject", "[registry][unit]")
|
||||
{
|
||||
// why: the registry is process-global state shared by the test binary, and
|
||||
// Catch2 may run cases in any order. Use an id that cannot collide with
|
||||
// built-ins or other cases. Avoid SECTIONs because each section re-runs the
|
||||
// body and would register the same id twice.
|
||||
const std::string id = "orca-test::registry-probe-7f3a";
|
||||
auto stub_factory = [](std::shared_ptr<ICloudServiceAgent>, const std::string&)
|
||||
-> std::shared_ptr<IPrinterAgent> { return nullptr; };
|
||||
|
||||
REQUIRE_FALSE(NetworkAgentFactory::is_printer_agent_registered(id));
|
||||
|
||||
REQUIRE(NetworkAgentFactory::register_printer_agent(id, "Registry Probe", stub_factory));
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(id));
|
||||
|
||||
// Re-registering the same id is rejected and does not replace the entry.
|
||||
REQUIRE_FALSE(NetworkAgentFactory::register_printer_agent(id, "Impostor", stub_factory));
|
||||
|
||||
// The first registration's display name survives the rejected duplicate.
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(id);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->display_name == "Registry Probe");
|
||||
|
||||
// It appears exactly once in the UI-population list.
|
||||
auto agents = NetworkAgentFactory::get_registered_printer_agents();
|
||||
int count = 0;
|
||||
for (const auto& a : agents)
|
||||
if (a.id == id)
|
||||
++count;
|
||||
CHECK(count == 1);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// INTEGRATION - the orca.printer_agent Python binding surface.
|
||||
// Boots the embedded interpreter and asserts the C++ to Python contract that
|
||||
// every printer-agent plugin subclasses. If a binding is renamed or removed,
|
||||
// plugins fail at runtime even though C++ still compiles.
|
||||
// ===========================================================================
|
||||
namespace {
|
||||
|
||||
void ensure_python_initialized()
|
||||
{
|
||||
// why: the `orca` module is embedded in this binary, so a bare interpreter
|
||||
// can import it without a bundled Python home. The app interpreter expects
|
||||
// that deployed layout, which is not present beside this test binary.
|
||||
if (!Py_IsInitialized()) {
|
||||
static py::scoped_interpreter interpreter;
|
||||
(void) interpreter;
|
||||
}
|
||||
}
|
||||
|
||||
py::module_ import_orca_module()
|
||||
{
|
||||
ensure_python_initialized();
|
||||
// Force PythonPluginBridge.cpp into the binary so the embedded
|
||||
// PYBIND11_EMBEDDED_MODULE(orca, ...) registration (incl. printer_agent) exists.
|
||||
(void) Slic3r::PythonPluginBridge::instance();
|
||||
return py::module_::import("orca");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("integration: orca.printer_agent binding surface", "[integration][Python]")
|
||||
{
|
||||
py::module_ orca = import_orca_module();
|
||||
|
||||
REQUIRE(py::hasattr(orca, "printer_agent"));
|
||||
py::object pa = orca.attr("printer_agent");
|
||||
|
||||
// The base class every printer-agent plugin subclasses.
|
||||
REQUIRE(py::hasattr(pa, "PrinterAgentBase"));
|
||||
py::object base = pa.attr("PrinterAgentBase");
|
||||
for (const char* method : { "get_agent_info", "connect_printer", "disconnect_printer",
|
||||
"send_message", "start_discovery", "bind_detect",
|
||||
"start_print", "get_filament_sync_mode" }) {
|
||||
CAPTURE(method);
|
||||
CHECK(py::hasattr(base, method));
|
||||
}
|
||||
|
||||
// AgentInfo value type - the registry identity the host reads (id is the key).
|
||||
REQUIRE(py::hasattr(pa, "AgentInfo"));
|
||||
py::object info = pa.attr("AgentInfo")("moonraker", "Moonraker", "1.0", "test agent");
|
||||
CHECK(info.attr("id").cast<std::string>() == "moonraker");
|
||||
CHECK(info.attr("name").cast<std::string>() == "Moonraker");
|
||||
|
||||
// FilamentSyncMode enum the host queries to pick pull vs subscription.
|
||||
REQUIRE(py::hasattr(pa, "FilamentSyncMode"));
|
||||
py::object mode = pa.attr("FilamentSyncMode");
|
||||
CHECK(py::hasattr(mode, "Pull"));
|
||||
CHECK(py::hasattr(mode, "Subscription"));
|
||||
CHECK(py::hasattr(mode, "None_"));
|
||||
|
||||
// Plugin-type enum exposed at module root (host reads it without the GIL).
|
||||
CHECK(py::hasattr(orca, "PluginType"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// INTEGRATION - plugin-registration API and discovery-context guards.
|
||||
// These are the symbols every plugin package uses: the @orca.plugin decorator,
|
||||
// orca.base, orca.register_capability, and the capability base modules. Checking
|
||||
// them in the lightweight embedded-interpreter test catches binding breakage
|
||||
// before the plugin-loader test needs to run.
|
||||
// ===========================================================================
|
||||
TEST_CASE("integration: orca plugin-registration API surface + discovery-context guards", "[integration][Python]")
|
||||
{
|
||||
py::module_ orca = import_orca_module();
|
||||
|
||||
// Module-level surface every plugin package relies on.
|
||||
// note: no "gcode" module here - this branch has no G-code capability module;
|
||||
// PostProcessing exists only as a PluginType value.
|
||||
for (const char* name : { "plugin", "register_capability", "base", "PythonPluginBase",
|
||||
"PluginType", "PluginResult", "script", "printer_agent", "host" }) {
|
||||
CAPTURE(name);
|
||||
CHECK(py::hasattr(orca, name));
|
||||
}
|
||||
|
||||
// Plugin package base and capability base contract.
|
||||
CHECK(py::hasattr(orca.attr("base"), "register_capabilities"));
|
||||
py::object cap_base = orca.attr("PythonPluginBase");
|
||||
for (const char* method : { "get_name", "get_type", "on_load", "on_unload" }) {
|
||||
CAPTURE(method);
|
||||
CHECK(py::hasattr(cap_base, method));
|
||||
}
|
||||
|
||||
// The script capability module exposes its own base class.
|
||||
CHECK(py::hasattr(orca.attr("script"), "ScriptPluginCapabilityBase"));
|
||||
|
||||
// PluginType enum carries the values that route a capability, including PrinterConnection.
|
||||
// note: no PostProcessing value on this branch's binding.
|
||||
py::object types = orca.attr("PluginType");
|
||||
for (const char* value : { "PrinterConnection", "Script" }) {
|
||||
CAPTURE(value);
|
||||
CHECK(py::hasattr(types, value));
|
||||
}
|
||||
|
||||
// note: this is testing behavior, not normal plugin loading.
|
||||
// These APIs should only work while Orca is actively loading a plugin.
|
||||
try {
|
||||
// Calls Python's orca.register_capability(0) from C++.
|
||||
// 0 is intentionally bogus. The important part is that there is no active
|
||||
// plugin load context, so the function should reject the call immediately.
|
||||
orca.attr("register_capability")(py::int_(0));
|
||||
|
||||
// If the call above does NOT throw, the test fails here.
|
||||
FAIL("register_capability outside discovery context must raise");
|
||||
} catch (const py::error_already_set& error) {
|
||||
// pybind11 wraps Python exceptions as py::error_already_set.
|
||||
// This checks the Python exception type is ValueError.
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
|
||||
try {
|
||||
// This is the function behind @orca.plugin.
|
||||
// Same logic as above.
|
||||
orca.attr("plugin")(py::int_(0));
|
||||
|
||||
FAIL("@orca.plugin outside discovery context must raise");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||
#include <libslic3r/Utils.hpp> // for set_data_dir
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <catch2/catch_session.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// why: embedding the fake plugin keeps this test self-contained. The PEP 723
|
||||
// block declares a printer-connection plugin, and the decorated plugin package
|
||||
// registers one PrinterAgentBase capability whose AgentInfo.id is the registry
|
||||
// key asserted below.
|
||||
constexpr const char* kFakePluginSource = R"PY(# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = []
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# name = "Lifecycle Test Agent"
|
||||
# description = "Minimal printer-agent plugin for the lifecycle test."
|
||||
# author = "tests"
|
||||
# version = "1.0.0"
|
||||
# type = "printer-connection"
|
||||
# ///
|
||||
import orca
|
||||
|
||||
|
||||
class LifecycleTestAgentCapability(orca.printer_agent.PrinterAgentBase):
|
||||
def get_name(self):
|
||||
return "Lifecycle Test Agent"
|
||||
|
||||
def get_agent_info(self):
|
||||
return orca.printer_agent.AgentInfo(
|
||||
id="lifecycle-test-agent",
|
||||
name="Lifecycle Test Agent",
|
||||
version="1.0.0",
|
||||
description="Lifecycle test printer agent",
|
||||
)
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class LifecycleTestPlugin(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(LifecycleTestAgentCapability)
|
||||
)PY";
|
||||
|
||||
// why: in production GUI_App::init_plugin_gui_wiring subscribes the agent-registry
|
||||
// callbacks; the test binary has no GUI, so install the same UNLOAD-side wiring
|
||||
// once so the tests exercise the production deregister-on-unload path.
|
||||
// note: the load-side (register) wiring is deliberately NOT installed - the two
|
||||
// concurrent load_plugin calls in the duplicate-id test would race for the id;
|
||||
// the tests register manually, in a deterministic order, instead.
|
||||
void install_agent_registry_wiring()
|
||||
{
|
||||
static bool installed = false;
|
||||
if (installed)
|
||||
return;
|
||||
installed = true;
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===========================================================================
|
||||
// PRINTER-AGENT PLUGIN LIFECYCLE: load, register, unload, deregister.
|
||||
//
|
||||
// This test uses its own executable because load_plugin runs on a detached worker
|
||||
// thread that needs the GIL released on the main thread (the PythonInterpreter
|
||||
// model). slic3rutils_tests' other Python tests hold the GIL on the main thread
|
||||
// via a bare scoped_interpreter; the two models can't share one process.
|
||||
//
|
||||
// When bundled Python is unavailable in the test environment, the test is
|
||||
// skipped so source-only or partially staged builds can still run the rest of
|
||||
// the suite.
|
||||
// ===========================================================================
|
||||
TEST_CASE("plugin lifecycle: printer-agent load registers and unload deregisters", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string plugin_key = "LifecycleTestAgent"; // entry-file stem
|
||||
const std::string agent_id = "lifecycle-test-agent"; // AgentInfo.id from the plugin
|
||||
|
||||
// Stage a throwaway data directory. Plugins are discovered under
|
||||
// <data_dir>/orca_plugins, so this controls which plugin is loaded.
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-plugin-lifecycle-test";
|
||||
const fs::path plugin_dir = data_dir / "orca_plugins" / plugin_key;
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec); // clear any stale run
|
||||
}
|
||||
fs::create_directories(plugin_dir);
|
||||
{
|
||||
std::ofstream out((plugin_dir / (plugin_key + ".py")).string(), std::ios::binary);
|
||||
out << kFakePluginSource;
|
||||
}
|
||||
// why: best-effort cleanup even if an assertion throws.
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
// Initialize the plugin system on this thread. If the bundled Python home
|
||||
// is not reachable, skip gracefully.
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
// Discover synchronously so the catalog holds the descriptor before loading.
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
INFO("expected plugin_key (entry-file stem): " << plugin_key);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
// Load on the worker thread and block until it finishes.
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
const bool loaded = mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error);
|
||||
INFO("plugin load error: " << error);
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(mgr.is_plugin_loaded(plugin_key));
|
||||
|
||||
// Resolve the one PrinterConnection capability and register it as an agent.
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
REQUIRE(caps[0] != nullptr);
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
|
||||
// The AgentInfo.id returned by the plugin is now in the registry.
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
|
||||
// Unloading the plugin deregisters its Python-backed agent.
|
||||
REQUIRE(mgr.unload_plugin(plugin_key));
|
||||
|
||||
// The registry no longer contains the agent id after unload.
|
||||
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// why: duplicate-id coverage needs two distinct plugins with different package
|
||||
// keys and classes while both return the same AgentInfo.id.
|
||||
std::string make_agent_plugin_source(const std::string& suffix, const std::string& display_name, const std::string& agent_id)
|
||||
{
|
||||
return std::string{}
|
||||
+ "# /// script\n"
|
||||
+ "# requires-python = \">=3.12\"\n"
|
||||
+ "# dependencies = []\n"
|
||||
+ "#\n"
|
||||
+ "# [tool.orcaslicer.plugin]\n"
|
||||
+ "# name = \"" + display_name + "\"\n"
|
||||
+ "# description = \"Duplicate-id fake printer-agent plugin.\"\n"
|
||||
+ "# author = \"tests\"\n"
|
||||
+ "# version = \"1.0.0\"\n"
|
||||
+ "# type = \"printer-connection\"\n"
|
||||
+ "# ///\n"
|
||||
+ "import orca\n"
|
||||
+ "\n\n"
|
||||
+ "class Cap" + suffix + "(orca.printer_agent.PrinterAgentBase):\n"
|
||||
+ " def get_name(self):\n"
|
||||
+ " return \"" + display_name + "\"\n"
|
||||
+ "\n"
|
||||
+ " def get_agent_info(self):\n"
|
||||
+ " return orca.printer_agent.AgentInfo(\n"
|
||||
+ " id=\"" + agent_id + "\", name=\"" + display_name + "\",\n"
|
||||
+ " version=\"1.0.0\", description=\"duplicate id test\")\n"
|
||||
+ "\n\n"
|
||||
+ "@orca.plugin\n"
|
||||
+ "class Plugin" + suffix + "(orca.base):\n"
|
||||
+ " def register_capabilities(self):\n"
|
||||
+ " orca.register_capability(Cap" + suffix + ")\n";
|
||||
}
|
||||
|
||||
void stage_plugin(const fs::path& data_dir, const std::string& plugin_key, const std::string& source)
|
||||
{
|
||||
const fs::path dir = data_dir / "orca_plugins" / plugin_key;
|
||||
fs::create_directories(dir);
|
||||
std::ofstream out((dir / (plugin_key + ".py")).string(), std::ios::binary);
|
||||
out << source;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ===========================================================================
|
||||
// DUPLICATE PRINTER-AGENT IDS
|
||||
// When two loaded plugins return the same AgentInfo.id, the first registration
|
||||
// keeps ownership. Unloading it removes the id, and the rejected plugin is not
|
||||
// promoted automatically. Manual re-registration is required.
|
||||
// ===========================================================================
|
||||
TEST_CASE("duplicate agent id is rejected and the winner is not clobbered", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string key_a = "DuplicateIdAgentA"; // winner, registered first
|
||||
const std::string key_b = "DuplicateIdAgentB"; // duplicate, rejected
|
||||
const std::string dup_id = "duplicate-id-agent"; // both plugins return this AgentInfo.id
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-duplicate-agent-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, key_a, make_agent_plugin_source("A", "Duplicate Id Agent A", dup_id));
|
||||
stage_plugin(data_dir, key_b, make_agent_plugin_source("B", "Duplicate Id Agent B", dup_id));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor_a;
|
||||
PluginDescriptor descriptor_b;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_a, descriptor_a));
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(key_b, descriptor_b));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(key_a, /*skip_deps=*/true);
|
||||
mgr.load_plugin(key_b, /*skip_deps=*/true);
|
||||
const bool loaded_a = mgr.wait_for_plugin_load(key_a, std::chrono::seconds(60), error);
|
||||
INFO("plugin A load error: " << error);
|
||||
REQUIRE(loaded_a);
|
||||
const bool loaded_b = mgr.wait_for_plugin_load(key_b, std::chrono::seconds(60), error);
|
||||
INFO("plugin B load error: " << error);
|
||||
REQUIRE(loaded_b);
|
||||
|
||||
auto caps_a = mgr.get_plugin_capabilities(key_a, PluginCapabilityType::PrinterConnection);
|
||||
auto caps_b = mgr.get_plugin_capabilities(key_b, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps_a.size() == 1);
|
||||
REQUIRE(caps_b.size() == 1);
|
||||
|
||||
// Register A first as the owner, then B with the duplicate id.
|
||||
NetworkAgentFactory::register_python_printer_agent(key_a, caps_a[0]->name());
|
||||
NetworkAgentFactory::register_python_printer_agent(key_b, caps_b[0]->name());
|
||||
|
||||
// The shared id is registered, and still owned by A; B did not replace it.
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(dup_id));
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(dup_id);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->plugin_identifier.find(key_a) != std::string::npos); // owned by A
|
||||
CHECK(info->plugin_identifier.find(key_b) == std::string::npos); // B never took ownership
|
||||
|
||||
// Unload the owner. The id is not promoted to the still loaded duplicate.
|
||||
REQUIRE(mgr.unload_plugin(key_a));
|
||||
CHECK_FALSE(NetworkAgentFactory::is_printer_agent_registered(dup_id));
|
||||
|
||||
mgr.unload_plugin(key_b); // the duplicate was loaded but never registered
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// NATIVE (BUILT-IN) AGENT ID COLLISION
|
||||
// A plugin may not hijack a built-in agent id (e.g. "bbl"). The built-in keeps
|
||||
// ownership and the plugin's agent is rejected.
|
||||
// ===========================================================================
|
||||
TEST_CASE("printer-agent plugin cannot claim a built-in agent id", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
// Register the native built-ins so "bbl"/"orca" occupy the registry.
|
||||
NetworkAgentFactory::register_all_agents();
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(BBL_PRINTER_AGENT_ID));
|
||||
|
||||
const std::string plugin_key = "BuiltinClashAgent";
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-builtin-clash-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Clash", "Builtin Clash", BBL_PRINTER_AGENT_ID));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
|
||||
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
|
||||
// "bbl" is still the native built-in, not the plugin.
|
||||
const PrinterAgentInfo* info = NetworkAgentFactory::get_printer_agent_info(BBL_PRINTER_AGENT_ID);
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK_FALSE(info->is_plugin());
|
||||
CHECK(info->plugin_identifier.find(plugin_key) == std::string::npos);
|
||||
|
||||
mgr.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// RE-REGISTERING THE SAME CAPABILITY IS A REFRESH, NOT A DUPLICATE
|
||||
// The guard rejects only a DIFFERENT owner. The same capability registering its
|
||||
// own id again must stay registered (insert_or_assign refreshes it in place).
|
||||
// ===========================================================================
|
||||
TEST_CASE("re-registering the same capability keeps its agent registered", "[plugin][lifecycle][Python]")
|
||||
{
|
||||
const std::string plugin_key = "ReRegisterAgent";
|
||||
const std::string agent_id = "re-register-agent";
|
||||
|
||||
const fs::path data_dir = fs::temp_directory_path() / "orca-re-register-test";
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(data_dir, ec);
|
||||
}
|
||||
stage_plugin(data_dir, plugin_key, make_agent_plugin_source("Re", "Re Register", agent_id));
|
||||
struct DirGuard
|
||||
{
|
||||
fs::path p;
|
||||
~DirGuard()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::remove_all(p, ec);
|
||||
}
|
||||
} guard{data_dir};
|
||||
|
||||
Slic3r::set_data_dir(data_dir.string());
|
||||
|
||||
PluginManager& mgr = PluginManager::instance();
|
||||
if (!mgr.initialize())
|
||||
SKIP("PythonInterpreter could not initialize (bundled Python home not found in this environment)");
|
||||
install_agent_registry_wiring();
|
||||
|
||||
mgr.discover_plugins(/*async=*/false, /*clear=*/true);
|
||||
PluginDescriptor descriptor;
|
||||
REQUIRE(mgr.try_get_valid_plugin_descriptor(plugin_key, descriptor));
|
||||
|
||||
std::string error;
|
||||
mgr.load_plugin(plugin_key, /*skip_deps=*/true);
|
||||
REQUIRE(mgr.wait_for_plugin_load(plugin_key, std::chrono::seconds(60), error));
|
||||
|
||||
auto caps = mgr.get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection);
|
||||
REQUIRE(caps.size() == 1);
|
||||
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
REQUIRE(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
const PrinterAgentInfo* first = NetworkAgentFactory::get_printer_agent_info(agent_id);
|
||||
REQUIRE(first != nullptr);
|
||||
const std::string owner = first->plugin_identifier;
|
||||
|
||||
// The same capability registering again is a refresh: still registered, same owner, not rejected.
|
||||
NetworkAgentFactory::register_python_printer_agent(plugin_key, caps[0]->name());
|
||||
CHECK(NetworkAgentFactory::is_printer_agent_registered(agent_id));
|
||||
const PrinterAgentInfo* second = NetworkAgentFactory::get_printer_agent_info(agent_id);
|
||||
REQUIRE(second != nullptr);
|
||||
CHECK(second->plugin_identifier == owner);
|
||||
|
||||
mgr.unload_plugin(plugin_key);
|
||||
}
|
||||
|
||||
// why: this binary embeds CPython through the PythonInterpreter singleton, which
|
||||
// lives for the full process. Normal static destruction can tear Python down
|
||||
// while C++ objects still hold Python handles, producing a Windows heap
|
||||
// corruption after the assertions have finished. The app has an ordered shutdown
|
||||
// path; this test harness does not, so it returns the Catch2 result through
|
||||
// _Exit after flushing output.
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
const int result = Catch::Session().run(argc, argv);
|
||||
std::cout.flush();
|
||||
std::cerr.flush();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(result);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <slic3r/Utils/QidiPrinterAgent.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Qidi slot response rejects null variables without throwing", "[QidiPrinterAgent]")
|
||||
{
|
||||
const std::string response = R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"save_variables": {
|
||||
"variables": null
|
||||
}
|
||||
}
|
||||
}
|
||||
})";
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("variables"));
|
||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("object"));
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response rejects missing and non-object fields without throwing", "[QidiPrinterAgent]")
|
||||
{
|
||||
std::string response;
|
||||
|
||||
SECTION("missing result")
|
||||
{
|
||||
response = R"({})";
|
||||
}
|
||||
|
||||
SECTION("non-object result")
|
||||
{
|
||||
response = R"({"result":null})";
|
||||
}
|
||||
|
||||
SECTION("missing status")
|
||||
{
|
||||
response = R"({"result":{}})";
|
||||
}
|
||||
|
||||
SECTION("non-object status")
|
||||
{
|
||||
response = R"({"result":{"status":null}})";
|
||||
}
|
||||
|
||||
SECTION("missing save_variables")
|
||||
{
|
||||
response = R"({"result":{"status":{}}})";
|
||||
}
|
||||
|
||||
SECTION("non-object save_variables")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":null}}})";
|
||||
}
|
||||
|
||||
SECTION("missing variables")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{}}}})";
|
||||
}
|
||||
|
||||
SECTION("scalar")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{"variables":42}}}})";
|
||||
}
|
||||
|
||||
SECTION("array")
|
||||
{
|
||||
response = R"({"result":{"status":{"save_variables":{"variables":[]}}}})";
|
||||
}
|
||||
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response exposes valid status and variables", "[QidiPrinterAgent]")
|
||||
{
|
||||
const std::string response = R"({
|
||||
"result": {
|
||||
"status": {
|
||||
"save_variables": {
|
||||
"variables": {
|
||||
"box_count": 2,
|
||||
"color_slot0": 3
|
||||
}
|
||||
},
|
||||
"box_stepper slot0": {
|
||||
"runout_button": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})";
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = false;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response(response, status, variables, error));
|
||||
REQUIRE(parsed);
|
||||
CHECK(status.is_object());
|
||||
CHECK(variables.is_object());
|
||||
CHECK(variables.at("box_count") == 2);
|
||||
CHECK(status.contains("box_stepper slot0"));
|
||||
}
|
||||
|
||||
TEST_CASE("Qidi slot response rejects invalid JSON", "[QidiPrinterAgent]")
|
||||
{
|
||||
nlohmann::json status;
|
||||
nlohmann::json variables;
|
||||
std::string error;
|
||||
bool parsed = true;
|
||||
|
||||
REQUIRE_NOTHROW(parsed = QidiPrinterAgent::parse_slot_response("{not json", status, variables, error));
|
||||
CHECK_FALSE(parsed);
|
||||
CHECK(error == "Invalid JSON response");
|
||||
}
|
||||
Reference in New Issue
Block a user