Compare commits

..
Author SHA1 Message Date
Ian Chua e6db086729 Merge branch 'main' into feat/printer-agent-impl 2026-10-07 19:16:58 +08:00
Ian Chua f8a718b338 fix: remove dead code 2026-10-07 19:16:50 +08:00
Ian Chua a0f2ea0c29 fix: clang-tidy 2026-10-07 18:59:15 +08:00
Ian Chua 3225bfea9d fix: batch box mapping, de-dup fetches and reject unsupported starts 2026-10-07 18:58:02 +08:00
Ian Chua 5f78baafe2 fix: name Moonraker default ports and warn when a custom CA is ignored 2026-10-07 18:28:56 +08:00
Ian Chua 376ad61a5e fix: fetch webcam info in ws loop 2026-10-07 18:11:44 +08:00
yw4z a38e6c61f2 Show dummy image for printers without cover on printer selectiondialog (#16235)
init
2026-10-07 12:57:28 +03:00
Ian Chua 5b81536be2 fix: snapshot Moonraker connection state and make agent teardown responsive 2026-10-07 16:58:13 +08:00
Ian Chua 5e3a526b72 fix: pick the direction-correct lamp macro and report control-command failures 2026-10-07 16:42:50 +08:00
Ian Chua 5f0534ffd4 fix: verify TLS hostname on Moonraker wss and apply TLS config to Snapmaker fetch 2026-10-07 16:40:25 +08:00
Ian Chua 33894bb46b fix: harden printer-agent async callbacks and detached fetch threads 2026-10-07 16:08:17 +08:00
HanifKoh f9ddb23804 Keep Each Filament's Device Drying Values When the CLI Merges Filaments (#16157)
The device drying options hold several values per filament, as many as
the filament preset gives, and a project stores them as the filaments'
values one after another. The CLI filament merge wrote them like an
option with one value per filament, putting each preset's first value at
the filament's own index, so a project with three filaments whose preset
gives "1", "0" was exported with 1;1;1;0;1;0 where the GUI writes
1;0;1;0;1;0.

The merge now leaves these options out of the per-filament pass and
rebuilds them afterwards from every filament's values in slot order.
Without a fixed number of values per filament one slot cannot be
replaced in place, so the stored values are kept when any slot has no
config to rebuild from.
2026-10-07 15:54:53 +08:00
HanifKoh ff8aebe76f Allow Unsigned Executable Memory in the macOS Entitlements (#16203)
The Bambu network plug-in's code protector rewrites one page of its own signed __TEXT after loading. The hardened runtime tolerates that until the page is evicted; the next read of it then kills OrcaSlicer with CODESIGNING Invalid Page. Bambu Studio signs with allow-unsigned-executable-memory for this reason; with it added, the same build survives critical memory pressure that killed it in 30 s without.
2026-10-07 15:52:18 +08:00
HanifKoh a56d1bf33e Keep a Project's Changed Values on Extruder Variants It Doesn't List (#16107)
A project's listed settings are carried onto its base preset by update_non_diff_values_to_base_config, which matched variants by exact name and id. A variant the base gained after the project was saved got the base's value, while the same value in a user preset now falls back to the preset's first variant of that extruder. So an old project opened with its printer preset already modified, and saving it wrote the base's values into the 3MF.

The function now maps variants with map_variant_indices, as update_diff_values_to_child_config does: a base variant the project does not list takes the project's first variant of the same extruder. The variant lists themselves stay the base's, so a fallback never writes one variant's name over another's.
2026-10-07 15:51:49 +08:00
HanifKoh 662a8e340d Support the 02.08.04 Bambu Network Plug-in Series (#16202)
The 02.08.02 series appended queue_plate_id to PrintParams and nothing
after it changed the ABI OrcaSlicer calls, so adding the field brings
the current layout up to 02.08.04. Make 02.08.04 the latest series and
drop 02.08.01 from the whitelist: its PrintParams no longer matches, and
its malformed bind table is refused by dyld on macOS 27, so it cannot
load there. A stored 02.08.01 falls back to the latest series through
the existing unsupported-version path.
2026-10-07 15:51:21 +08:00
HanifKoh 6639a32b0c Remove using namespace std from json_diff.hpp (#16222)
The directive sat at global scope in a header that DeviceManager.hpp
includes, so most of the GUI compiled with all of std in the global
namespace. 42 files had come to rely on it, mostly for string, vector
and unordered_map, four of them for the ""sv and ""ms literals.

Those sites are qualified. GCodeViewer.cpp spelled the type as
std::vector<::string>, which only resolved through the directive. The
files that use the ""sv and ""ms literals get a file-scope
"using namespace std::string_view_literals;" or
"using namespace std::chrono_literals;", as other sources already do.
2026-10-07 14:58:49 +08:00
SoftFever 1ec195a221 Update build commands in AGENTS.md to use Release configuration 2026-10-07 14:44:04 +08:00
Ian Chua 17b0eda7f8 fix: clang-tidy 2026-10-06 21:06:31 +08:00
Ian Chua ec22321a4b fix: move connect_mutex to protected 2026-10-06 21:02:27 +08:00
Ian Chua 5e861b31f9 fix: use-after-free in MoonrakerPrinterAgent teardown 2026-10-06 20:54:47 +08:00
Ian Chua 334ba8f84d chore: reconcile with PR 15710 squash base 2026-10-06 20:05:39 +08:00
Ian Chua 418623e2c5 chore: remove orcasonar test changes
(cherry picked from commit c6d3e755ec)
2026-10-06 20:05:38 +08:00
Ian Chua cf476637d4 refactor: move orcasonar changes to dedicated branch
(cherry picked from commit 797c2c1b33)
2026-10-06 20:05:38 +08:00
Ian Chua ceadec4e2d chore: port add_platform_root_certificates from 16027
(cherry picked from commit 6194c4f8bb)
2026-10-06 20:05:37 +08:00
peachismomo 92b3068cf6 refactor: separate out networking infra
(cherry picked from commit 1cac04eae8)
2026-10-06 20:05:37 +08:00
peachismomo e45f5846b2 fix: printer agent and camera shutdown races
(cherry picked from commit a6cad8a6e5)
2026-10-06 20:05:36 +08:00
Ian Chua f0be15df9e fix: remove redundant earlier expires_never call
(cherry picked from commit 50c5ec2862)
2026-10-06 20:05:36 +08:00
Ian Chua 0ddb86bec7 fix: orca mqtt session timeout after connack
(cherry picked from commit b36a72e16a)
2026-10-06 20:05:35 +08:00
Ian Chua fe30bfe7c7 fix: serialize MQTT ws I/O and dewcouple keepalive
(cherry picked from commit 87eb8de001)
2026-10-06 20:05:35 +08:00
Ian Chua 44675778e2 fix: cancel inflight cloud signaling requests
(cherry picked from commit 220508534a)
2026-10-06 20:05:34 +08:00
Ian Chua b870835b01 fix: clarify OrcaCloud signaling endpoint contract
(cherry picked from commit 5a0810365b)
2026-10-06 20:05:34 +08:00
Ian Chua ea464401be feat: add ssl and ca_cert for OrcaPrinterAgent
(cherry picked from commit fa98ec5afe)
2026-10-06 20:05:34 +08:00
Ian Chua 4540a98c58 feat: add https compatibility for moonraker based printer agents
(cherry picked from commit 936b0bbc17)
2026-10-06 20:05:33 +08:00
Ian Chua 76cf64d6c9 fix: command_ams_refresh_rfid function signature in test_printer_agent.cpp
(cherry picked from commit 5222a37eff)
2026-10-06 20:05:33 +08:00
peachismomo 8d7df2684d fix: compilation after merge
(cherry picked from commit 6da938b660)
2026-10-06 20:05:32 +08:00
Ian Chua 62bd8c3743 fix: refresh token before configuring printer MQTT
(cherry picked from commit 2dcdfe2d32)
2026-10-06 20:05:32 +08:00
Ian Chua 936daee0e6 feat: add ssl compatibility with moonrakerprinteragent
(cherry picked from commit 6c353d9596)
2026-10-06 20:05:31 +08:00
Ian Chua 9a161692b5 fix: snapmakerprinteragent use moonraker's lifecycle-owned command worker
(cherry picked from commit ce0c759ded)
2026-10-06 20:05:31 +08:00
Ian Chua eb071f30f8 fix: re-implement missing commands on BBLPrinterAgent
(cherry picked from commit 9a7e925790)
2026-10-06 20:05:30 +08:00
Ian Chua 931b07364a fix: camera signaling API
(cherry picked from commit d9f1f8b207)
2026-10-06 20:05:30 +08:00
peachismomo 34c2c926d1 fix: load windows roots for cloud MQTT and camera signaling
(cherry picked from commit b91f48141d)
2026-10-06 20:05:29 +08:00
Ian Chua fc56ad002a fix: swtich start print command from http to mqtt command
(cherry picked from commit 5287fd0160)
2026-10-06 20:05:29 +08:00
Ian Chua 7b75fe02c8 fix: remove unimplemented pure virtual functions
(cherry picked from commit a7ab01b815)
2026-10-06 20:05:28 +08:00
Ian Chua 2f6de2ed86 fix: updated stale docs
(cherry picked from commit dcddb9283c)
2026-10-06 20:05:28 +08:00
Ian Chua 172f492b74 Revert "fix: latent ams in use bug"
This reverts commit 910dbcd2e6.

(cherry picked from commit ba9cf6e897)
2026-10-06 20:05:28 +08:00
Ian Chua b74fac92a5 feat: restore printer agent implementations
(cherry picked from commit d1f2ecb903)
2026-10-06 20:05:27 +08:00
66 changed files with 2878 additions and 610 deletions
+6 -4
View File
@@ -4,15 +4,17 @@ OrcaSlicer — open-source C++17 3D slicer. wxWidgets GUI, CMake build system.
## Build Commands ## Build Commands
Build the Release configuration unless asked otherwise.
```bash ```bash
# macOS # macOS
cmake --build build/arm64 --config RelWithDebInfo --target all -- cmake --build build/arm64 --config Release --target all --
# Linux # Linux
cmake --build build --config RelWithDebInfo --target all -- cmake --build build --config Release --target all --
# Windows (replace %build_type% with Debug/Release/RelWithDebInfo) # Windows
cmake --build . --config %build_type% --target ALL_BUILD -- -m cmake --build . --config Release --target ALL_BUILD -- -m
``` ```
## Testing ## Testing
+1 -1
View File
@@ -311,7 +311,7 @@ function CreatePrinterBlock(OneModel)
return '<div class="PrinterBlock" onClick="ChooseModel(\''+vendor+'\',\''+OneModel['model']+'\')">'+ return '<div class="PrinterBlock" onClick="ChooseModel(\''+vendor+'\',\''+OneModel['model']+'\')">'+
' <div class="PImg">'+ ' <div class="PImg">'+
' <img class="ModelThumbnail" src="' + OneModel['cover'] + '" />'+ ' <img class="ModelThumbnail" src="' + OneModel['cover'] + '" onerror="this.onerror=null;this.src=\'../img/printer-dummy.png\';"/>'+
' </div>'+ ' </div>'+
' <div class="PrinterInfoMark">?</div>'+ ' <div class="PrinterInfoMark">?</div>'+
' <div class="PrinterInfo">'+ ' <div class="PrinterInfo">'+
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+5
View File
@@ -4,5 +4,10 @@
<dict> <dict>
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<!-- The Bambu network plug-in's code protector rewrites one of its own signed code pages
after loading. Under the hardened runtime macOS kills the process when that page is
paged back in; this lets it run, as Bambu Studio's signature does. -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict> </dict>
</plist> </plist>
+11
View File
@@ -3618,6 +3618,9 @@ int CLI::run(int argc, char **argv)
{ {
if (opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "model_id" || opt_key == "dev_model_name" || opt_key == "filament_settings_id") if (opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "model_id" || opt_key == "dev_model_name" || opt_key == "filament_settings_id")
continue; continue;
// rebuilt from every filament after this loop
if (filament_dev_options.find(opt_key) != filament_dev_options.end())
continue;
ConfigOption *opt = m_print_config.option(opt_key, true); ConfigOption *opt = m_print_config.option(opt_key, true);
if (opt == nullptr) { if (opt == nullptr) {
// opt_key does not exist in this ConfigBase and it cannot be created, because it is not defined by this->def(). // opt_key does not exist in this ConfigBase and it cannot be created, because it is not defined by this->def().
@@ -3681,6 +3684,14 @@ int CLI::run(int argc, char **argv)
} }
} }
// The stored values cannot be told apart per filament, so they are kept as they are unless every slot has a config.
std::vector<const DynamicPrintConfig *> filament_configs(filament_count, nullptr);
for (size_t index = 0; index < load_filaments_config.size(); index++)
if (load_filaments_index[index] >= 1 && load_filaments_index[index] <= filament_count)
filament_configs[load_filaments_index[index] - 1] = &load_filaments_config[index];
if (std::find(filament_configs.begin(), filament_configs.end(), nullptr) == filament_configs.end())
set_filament_dev_options(m_print_config, filament_configs);
if (m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")) { if (m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")) {
std::vector<int>& filament_self_indice = m_print_config.option<ConfigOptionInts>("filament_self_index", true)->values; std::vector<int>& filament_self_indice = m_print_config.option<ConfigOptionInts>("filament_self_index", true)->values;
int index_size = m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")->size(); int index_size = m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")->size();
@@ -5,5 +5,10 @@
<!-- for dynamic loading of libraries without signature validation. Used for 3dconnection drivers.--> <!-- for dynamic loading of libraries without signature validation. Used for 3dconnection drivers.-->
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<!-- The Bambu network plug-in's code protector rewrites one of its own signed code pages
after loading. Under the hardened runtime macOS kills the process when that page is
paged back in; this lets it run, as Bambu Studio's signature does. -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict> </dict>
</plist> </plist>
+31 -13
View File
@@ -10822,6 +10822,22 @@ void normalize_filament_values_to_variants(DynamicPrintConfig &config)
} }
} }
void set_filament_dev_options(DynamicPrintConfig &config, const std::vector<const DynamicPrintConfig *> &filament_configs)
{
for (const std::string &key : filament_dev_options) {
if (std::none_of(filament_configs.begin(), filament_configs.end(), [&key](const DynamicPrintConfig *filament) { return filament->has(key); }))
continue;
const ConfigOption *default_value = print_config_def.get(key)->default_value.get();
auto *dst = static_cast<ConfigOptionVectorBase *>(config.option(key, true));
dst->clear();
for (const DynamicPrintConfig *filament : filament_configs) {
const auto *src = static_cast<const ConfigOptionVectorBase *>(filament->has(key) ? filament->option(key) : default_value);
if (!src->empty())
dst->append(src);
}
}
}
//used for object/region config //used for object/region config
//use the smallest of multiple to single //use the smallest of multiple to single
@@ -11520,12 +11536,18 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
int cur_variant_count = cur_extruder_variants.size(); int cur_variant_count = cur_extruder_variants.size();
int target_variant_count = target_extruder_variants.size(); int target_variant_count = target_extruder_variants.size();
// A base variant this config does not list (the base gained it after the config was saved, or the
// config lists none) takes this config's first variant of the same extruder, as a user preset's
// values do in update_diff_values_to_child_config. Left unmatched, the base's value would silently
// replace the user's.
variant_index.resize(target_variant_count, -1); variant_index.resize(target_variant_count, -1);
if (cur_variant_count == 0) { if (cur_variant_count == 0) {
// Defensive: target_variant_count may be 0 if the preset doesn't carry extruder_variant_name. // Defensive: target_variant_count may be 0 if the preset doesn't carry extruder_variant_name.
// In that case keep variant_index empty and let the downstream size checks produce a useful error. // In that case keep variant_index empty and let the downstream size checks produce a useful error.
if (!variant_index.empty()) if (!variant_index.empty())
variant_index[0] = 0; // This config's one value belongs to the extruder of the base's first variant.
variant_index = map_variant_indices(target_extruder_variants, target_extruder_ids, {},
target_extruder_ids.empty() ? std::vector<int>() : std::vector<int>{target_extruder_ids[0]});
} }
else if ((cur_extruder_ids.size() > 0) && cur_variant_count != cur_extruder_ids.size()){ else if ((cur_extruder_ids.size() > 0) && cur_variant_count != cur_extruder_ids.size()){
//should not happen //should not happen
@@ -11538,18 +11560,7 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
%extruder_variant_name %target_variant_count %extruder_id_name %target_extruder_ids.size(); %extruder_variant_name %target_variant_count %extruder_id_name %target_extruder_ids.size();
} }
else { else {
for (int i = 0; i < target_variant_count; i++) variant_index = map_variant_indices(target_extruder_variants, target_extruder_ids, cur_extruder_variants, cur_extruder_ids);
{
for (int j = 0; j < cur_variant_count; j++)
{
if ((target_extruder_variants[i] == cur_extruder_variants[j])
&&(target_extruder_ids.empty() || (target_extruder_ids[i] == cur_extruder_ids[j])))
{
variant_index[i] = j;
break;
}
}
}
} }
for (auto& opt : keys) { for (auto& opt : keys) {
@@ -11574,6 +11585,13 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
if (cur_variant_count > target_variant_count) if (cur_variant_count > target_variant_count)
continue; continue;
// The variant lists are the base's layout itself, which every other value is
// carried onto: a variant this config lacks keeps its own name and id.
if (opt == extruder_id_name || opt == extruder_variant_name) {
opt_src->set(opt_target);
continue;
}
int stride = 1; int stride = 1;
if (key_set2.find(opt) != key_set2.end()) if (key_set2.find(opt) != key_set2.end())
stride = 2; stride = 2;
+5
View File
@@ -928,6 +928,11 @@ void normalize_filament_values_to_variants(DynamicPrintConfig &config);
extern std::set<std::string> filament_dev_options; extern std::set<std::string> filament_dev_options;
// Orca: a filament_dev_options option holds several values per filament, and how many is up to the
// filament preset, so one filament's values cannot be replaced in place. This rebuilds each option from
// filament_configs, one config per filament in slot order, as the filaments' values one after another.
void set_filament_dev_options(DynamicPrintConfig &config, const std::vector<const DynamicPrintConfig *> &filament_configs);
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1); extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config, extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config,
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_map_indices); t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_map_indices);
+2 -2
View File
@@ -974,8 +974,8 @@ bool AMSMaterialsSetting::Show(bool show)
static void _collect_filament_info(const wxString& shown_name, static void _collect_filament_info(const wxString& shown_name,
const Preset& filament, const Preset& filament,
unordered_map<wxString, wxString>& query_filament_vendors, std::unordered_map<wxString, wxString>& query_filament_vendors,
unordered_map<wxString, wxString>& query_filament_types) std::unordered_map<wxString, wxString>& query_filament_types)
{ {
query_filament_vendors[shown_name] = filament.config.get_filament_vendor(); query_filament_vendors[shown_name] = filament.config.get_filament_vendor();
query_filament_types[shown_name] = filament.config.get_filament_type(); query_filament_types[shown_name] = filament.config.get_filament_type();
+3 -3
View File
@@ -856,14 +856,14 @@ void AuxiliaryPanel::Split(const std::string &src, const std::string &separator,
dest.clear(); dest.clear();
index = str.find_first_of(separator, start); index = str.find_first_of(separator, start);
do { do {
if (index != string::npos) { if (index != std::string::npos) {
substring = str.substr(start, index - start); substring = str.substr(start, index - start);
dest.push_back(substring); dest.push_back(substring);
start = index + separator.size(); start = index + separator.size();
index = str.find(separator, start); index = str.find(separator, start);
if (start == string::npos) break; if (start == std::string::npos) break;
} }
} while (index != string::npos); } while (index != std::string::npos);
// the last part // the last part
substring = str.substr(start); substring = str.substr(start);
@@ -1307,7 +1307,7 @@ void CalibrationPresetPage::stripWhiteSpace(std::string& str)
{ {
if (str == "") { return; } if (str == "") { return; }
string::iterator cur_it; std::string::iterator cur_it;
cur_it = str.begin(); cur_it = str.begin();
while (cur_it != str.end()) { while (cur_it != str.end()) {
@@ -2597,7 +2597,7 @@ void CalibrationPresetPage::update_multi_extruder_filament_combobox(const std::s
int ams_id_int = 0; int ams_id_int = 0;
try { try {
if (!ams_id.empty()) if (!ams_id.empty())
ams_id_int = stoi(ams_id.c_str()); ams_id_int = std::stoi(ams_id.c_str());
} catch (...) {} } catch (...) {}
@@ -2685,7 +2685,7 @@ void CalibrationPresetPage::update_filament_combobox(std::string ams_id)
int ams_id_int = 0; int ams_id_int = 0;
try { try {
if (!ams_id.empty()) if (!ams_id.empty())
ams_id_int = stoi(ams_id.c_str()); ams_id_int = std::stoi(ams_id.c_str());
} catch (...) {} } catch (...) {}
+4 -4
View File
@@ -347,7 +347,7 @@ static std::string get_curr_timestmp()
// return timestampString; // return timestampString;
} }
static void get_filament_compatible_printer(Preset* preset, vector<std::string>& printers) static void get_filament_compatible_printer(Preset* preset, std::vector<std::string>& printers)
{ {
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset->config.option("compatible_printers")); auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset->config.option("compatible_printers"));
if (compatible_printers == nullptr) return; if (compatible_printers == nullptr) return;
@@ -1581,7 +1581,7 @@ void CreateFilamentPresetDialog::sort_printer_by_nozzle(std::vector<std::pair<st
{ {
std::unordered_map<std::string, float> nozzle_diameter = nozzle_diameter_map; std::unordered_map<std::string, float> nozzle_diameter = nozzle_diameter_map;
std::sort(printer_name_to_filament_preset.begin(), printer_name_to_filament_preset.end(), std::sort(printer_name_to_filament_preset.begin(), printer_name_to_filament_preset.end(),
[&nozzle_diameter](const std::pair<string, T> &a, const std::pair<string, T> &b) { [&nozzle_diameter](const std::pair<std::string, T> &a, const std::pair<std::string, T> &b) {
size_t nozzle_index_a = a.first.find(" nozzle"); size_t nozzle_index_a = a.first.find(" nozzle");
size_t nozzle_index_b = b.first.find(" nozzle"); size_t nozzle_index_b = b.first.find(" nozzle");
if (nozzle_index_a == std::string::npos || nozzle_index_b == std::string::npos) return a.first < b.first; if (nozzle_index_a == std::string::npos || nozzle_index_b == std::string::npos) return a.first < b.first;
@@ -3143,7 +3143,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
wxArrayString CreatePrinterPresetDialog::printer_preset_sort_with_nozzle_diameter(const VendorProfile &vendor_profile, float nozzle_diameter) wxArrayString CreatePrinterPresetDialog::printer_preset_sort_with_nozzle_diameter(const VendorProfile &vendor_profile, float nozzle_diameter)
{ {
std::vector<pair<float, std::string>> preset_sort; std::vector<std::pair<float, std::string>> preset_sort;
auto get_nozzle_size_for_printer_model = [this](const std::string & model_name) -> size_t { auto get_nozzle_size_for_printer_model = [this](const std::string & model_name) -> size_t {
auto iter = m_printer_name_to_preset.find(model_name); auto iter = m_printer_name_to_preset.find(model_name);
@@ -4964,7 +4964,7 @@ wxBoxSizer *CreatePresetForPrinterDialog::create_selected_filament_preset_sizer(
m_selected_printer->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) { m_selected_printer->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) {
wxString printer_name = m_selected_printer->GetStringSelection(); wxString printer_name = m_selected_printer->GetStringSelection();
std::unordered_map<string, std::vector<std::shared_ptr<Preset>>>::iterator filament_iter = m_printer_compatible_filament_presets.find(into_u8(printer_name)); std::unordered_map<std::string, std::vector<std::shared_ptr<Preset>>>::iterator filament_iter = m_printer_compatible_filament_presets.find(into_u8(printer_name));
if (m_printer_compatible_filament_presets.end() != filament_iter) { if (m_printer_compatible_filament_presets.end() != filament_iter) {
filament_choice_to_filament_preset.clear(); filament_choice_to_filament_preset.clear();
wxArrayString filament_choices; wxArrayString filament_choices;
+1 -1
View File
@@ -160,7 +160,7 @@ DevAms::~DevAms()
m_trays.clear(); m_trays.clear();
} }
static unordered_map<int, wxString> s_ams_display_formats = { static std::unordered_map<int, wxString> s_ams_display_formats = {
{DevAms::AMS, "AMS-%d"}, {DevAms::AMS, "AMS-%d"},
{DevAms::AMS_LITE, "AMS Lite-%d"}, {DevAms::AMS_LITE, "AMS Lite-%d"},
{DevAms::N3F, "AMS 2 PRO-%d"}, {DevAms::N3F, "AMS 2 PRO-%d"},
@@ -250,7 +250,7 @@ int DevNozzleMappingCtrl::CtrlGetAutoNozzleMappingV1(Slic3r::GUI::Plater* plater
void DevNozzleMappingCtrl::ParseAutoNozzleMapping(const json& print_jj) void DevNozzleMappingCtrl::ParseAutoNozzleMapping(const json& print_jj)
{ {
if (print_jj.contains("command") && print_jj["command"].get<string>() == "get_auto_nozzle_mapping") { if (print_jj.contains("command") && print_jj["command"].get<std::string>() == "get_auto_nozzle_mapping") {
if (print_jj.contains("sequence_id") && print_jj["sequence_id"] == m_sequence_id) { if (print_jj.contains("sequence_id") && print_jj["sequence_id"] == m_sequence_id) {
Clear(); Clear();
DevJsonValParser::ParseVal(print_jj, "result", m_result); DevJsonValParser::ParseVal(print_jj, "result", m_result);
@@ -426,7 +426,7 @@ void DevNozzleSystem::ClearNozzles()
// ---- parsing ---------------------------------------------------------------------------------------- // ---- parsing ----------------------------------------------------------------------------------------
static unordered_map<string, NozzleFlowType> _str2_nozzle_flow_type = { static std::unordered_map<std::string, NozzleFlowType> _str2_nozzle_flow_type = {
{"S", NozzleFlowType::S_FLOW}, {"S", NozzleFlowType::S_FLOW},
{"H", NozzleFlowType::H_FLOW}, {"H", NozzleFlowType::H_FLOW},
{"A", NozzleFlowType::S_FLOW}, {"A", NozzleFlowType::S_FLOW},
@@ -436,7 +436,7 @@ static unordered_map<string, NozzleFlowType> _str2_nozzle_flow_type = {
{"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow {"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow
}; };
static unordered_map<string, NozzleType> _str2_nozzle_type = { static std::unordered_map<std::string, NozzleType> _str2_nozzle_type = {
{"00", NozzleType::ntStainlessSteel}, {"00", NozzleType::ntStainlessSteel},
{"01", NozzleType::ntHardenedSteel}, {"01", NozzleType::ntHardenedSteel},
{"05", NozzleType::ntTungstenCarbide} {"05", NozzleType::ntTungstenCarbide}
+1 -1
View File
@@ -368,7 +368,7 @@ wxString DeviceErrorDialog::parse_error_level(int error_code)
} }
} }
static const std::unordered_set<string> s_jump_liveview_error_codes = { "0300-8003", "0300-8002", "0300-800A"}; static const std::unordered_set<std::string> s_jump_liveview_error_codes = { "0300-8003", "0300-8002", "0300-800A"};
wxString DeviceErrorDialog::show_error_code(int error_code) wxString DeviceErrorDialog::show_error_code(int error_code)
{ {
if (m_error_code == error_code) { return wxEmptyString;} if (m_error_code == error_code) { return wxEmptyString;}
+2 -1
View File
@@ -110,6 +110,7 @@ using json = nlohmann::json;
class wxWindow; class wxWindow;
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
using namespace std::chrono_literals;
#define CALI_DEBUG #define CALI_DEBUG
#define MINUTE_30 1800000 //ms #define MINUTE_30 1800000 //ms
@@ -3103,7 +3104,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
DevFirmwareVersionInfo ver_info; DevFirmwareVersionInfo ver_info;
ver_info.name = (*it)["name"].get<std::string>(); ver_info.name = (*it)["name"].get<std::string>();
if ((*it).contains("product_name")) if ((*it).contains("product_name"))
ver_info.product_name = wxString::FromUTF8((*it)["product_name"].get<string>()); ver_info.product_name = wxString::FromUTF8((*it)["product_name"].get<std::string>());
if ((*it).contains("sw_ver")) if ((*it).contains("sw_ver"))
ver_info.sw_ver = (*it)["sw_ver"].get<std::string>(); ver_info.sw_ver = (*it)["sw_ver"].get<std::string>();
if ((*it).contains("sw_new_ver")) if ((*it).contains("sw_new_ver"))
+3 -3
View File
@@ -294,8 +294,8 @@ wxDataViewItem EditGCodeDialog::add_presets_placeholders()
// Orca: create subgroups from the pages of the tabs // Orca: create subgroups from the pages of the tabs
auto init_from_tab = [this, full_config](wxDataViewItem parent, Tab* tab, const set<string>& preset_keys){ auto init_from_tab = [this, full_config](wxDataViewItem parent, Tab* tab, const std::set<std::string>& preset_keys){
set extra_keys(preset_keys); std::set extra_keys(preset_keys);
for (const auto& page : tab->m_pages) { for (const auto& page : tab->m_pages) {
// ORCA: Pull icons from tabs for subgroups, icons are hidden on tabs // ORCA: Pull icons from tabs for subgroups, icons are hidden on tabs
std::string icon_name = "empty"; // use empty icon if not defined std::string icon_name = "empty"; // use empty icon if not defined
@@ -549,7 +549,7 @@ void ParamsNode::RefreshSearch(const wxString& search_text)
if (GetEnabledChildren().empty()) if (GetEnabledChildren().empty())
if (auto pos = text.find(search_text); IsParamNode() && pos != wxString::npos) { if (auto pos = text.find(search_text); IsParamNode() && pos != wxString::npos) {
m_highlight_index = make_unique<pair<int, int>>(pos, search_text.Len()); m_highlight_index = make_unique<std::pair<int, int>>(pos, search_text.Len());
Enable(); Enable();
} else { } else {
Disable(); Disable();
+4 -4
View File
@@ -702,11 +702,11 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
set_value(str, true); set_value(str, true);
} }
} else if (m_opt.opt_key == "sparse_infill_rotate_template" || m_opt.opt_key == "solid_infill_rotate_template") { } else if (m_opt.opt_key == "sparse_infill_rotate_template" || m_opt.opt_key == "solid_infill_rotate_template") {
string ustr(str.utf8_string()); std::string ustr(str.utf8_string());
if (!ConfigOptionFloats::validate_string(ustr)) { if (!ConfigOptionFloats::validate_string(ustr)) {
string v; std::string v;
std::smatch match; std::smatch match;
string ps = (m_opt.opt_key == "sparse_infill_rotate_template") ? std::string ps = (m_opt.opt_key == "sparse_infill_rotate_template") ?
u8"[BT][!]?|[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[BT]?[!*]?" : u8"[BT][!]?|[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[BT]?[!*]?" :
u8"[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[!*]?"; u8"[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[!*]?";
@@ -729,7 +729,7 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
} }
break; break;
} else if (m_opt.opt_key == "extra_solid_infills") { } else if (m_opt.opt_key == "extra_solid_infills") {
string ustr(str.utf8_string()); std::string ustr(str.utf8_string());
// New rule: accept either interval form (N or N#K) or explicit list (e.g. 1,7,9), with optional quotes. // New rule: accept either interval form (N or N#K) or explicit list (e.g. 1,7,9), with optional quotes.
const std::regex rx_interval(u8R"(^\s*['"]?\s*\d+\s*(?:#\s*\d*)?\s*['"]?\s*$)"); const std::regex rx_interval(u8R"(^\s*['"]?\s*\d+\s*(?:#\s*\d*)?\s*['"]?\s*$)");
// List entries may be plain numbers or number with optional #K count, e.g., 5, 9#2, 18 // List entries may be plain numbers or number with optional #K count, e.g., 5, 9#2, 18
+3 -1
View File
@@ -94,6 +94,8 @@
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" #include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/PartPlate.hpp"
using namespace std::string_view_literals;
namespace Slic3r { class PrintBase; } namespace Slic3r { class PrintBase; }
@@ -3184,7 +3186,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
float ams_item_height = 0; float ams_item_height = 0;
float filament_group_item_align_width = 0; float filament_group_item_align_width = 0;
{ {
float three_words_width = imgui.calc_text_size(std::string_view("ABC")).x; float three_words_width = imgui.calc_text_size("ABC"sv).x;
const int line_capacity = 4; const int line_capacity = 4;
for (const auto& extruder_filaments : {m_left_extruder_filament,m_right_extruder_filament }) for (const auto& extruder_filaments : {m_left_extruder_filament,m_right_extruder_filament })
+2
View File
@@ -177,6 +177,8 @@ extern wxPopupWindow* wxCurrentPopupWindow;
#endif #endif
#endif #endif
using namespace std::string_view_literals;
static constexpr const float TRACKBALLSIZE = 0.8f; static constexpr const float TRACKBALLSIZE = 0.8f;
static Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.906f, 0.906f, 0.906f, 1.0f }; static Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.906f, 0.906f, 0.906f, 1.0f };
+3 -3
View File
@@ -9040,7 +9040,7 @@ std::map<std::string, std::string> GUI_App::get_delete_cache_presets_lock()
void GUI_App::process_delete_presets() void GUI_App::process_delete_presets()
{ {
std::map<string, string> delete_cache_presets = get_delete_cache_presets_lock(); std::map<std::string, std::string> delete_cache_presets = get_delete_cache_presets_lock();
for (auto it = delete_cache_presets.begin(); it != delete_cache_presets.end();) { for (auto it = delete_cache_presets.begin(); it != delete_cache_presets.end();) {
if (it->first.empty()) continue; if (it->first.empty()) continue;
std::string del_setting_id = it->first; std::string del_setting_id = it->first;
@@ -10068,7 +10068,7 @@ bool is_soluble_filament(int extruder_id)
return support_option->get_at(0); return support_option->get_at(0);
}; };
bool has_filaments(const std::vector<string>& model_filaments) { bool has_filaments(const std::vector<std::string>& model_filaments) {
auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets; auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets;
if (!Slic3r::GUI::wxGetApp().plater()) return false; if (!Slic3r::GUI::wxGetApp().plater()) return false;
auto model_objects = Slic3r::GUI::wxGetApp().plater()->model().objects; auto model_objects = Slic3r::GUI::wxGetApp().plater()->model().objects;
@@ -10103,7 +10103,7 @@ bool is_support_filament(int extruder_id, bool strict_check)
Slic3r::ConfigOptionBools *support_option = dynamic_cast<Slic3r::ConfigOptionBools *>(filament->config.option("filament_is_support")); Slic3r::ConfigOptionBools *support_option = dynamic_cast<Slic3r::ConfigOptionBools *>(filament->config.option("filament_is_support"));
if(!strict_check &&(filament_type == "PETG" || filament_type == "PLA")) { if(!strict_check &&(filament_type == "PETG" || filament_type == "PLA")) {
std::vector<string> model_filaments; std::vector<std::string> model_filaments;
if (filament_type == "PETG") if (filament_type == "PETG")
model_filaments.emplace_back("PLA"); model_filaments.emplace_back("PLA");
else { else {
+2 -2
View File
@@ -1108,7 +1108,7 @@ void ObjectList::update_name_column_width() const
} }
} }
GetColumn(colName)->SetWidth(max(0, client_size.x - (others_width)*em)); GetColumn(colName)->SetWidth(std::max(0, client_size.x - (others_width)*em));
} }
void ObjectList::set_filament_column_hidden(const bool hide) const void ObjectList::set_filament_column_hidden(const bool hide) const
@@ -3970,7 +3970,7 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
if (config->opt_float("layer_height") == object_cfg->opt_float("layer_height")) { if (config->opt_float("layer_height") == object_cfg->opt_float("layer_height")) {
SettingsFactory::Bundle new_cat_options; SettingsFactory::Bundle new_cat_options;
for (auto cat_opt : cat_options) { for (auto cat_opt : cat_options) {
std::vector<string> temp; std::vector<std::string> temp;
for (auto value : cat_opt.second) { for (auto value : cat_opt.second) {
if (value != "layer_height") if (value != "layer_height")
temp.push_back(value); temp.push_back(value);
@@ -44,6 +44,8 @@
#define MAX_NUM 9999.99 #define MAX_NUM 9999.99
#define MAX_SIZE std::string_view{"9999.99"} #define MAX_SIZE std::string_view{"9999.99"}
using namespace std::string_view_literals;
namespace Slic3r namespace Slic3r
{ {
namespace GUI namespace GUI
+7 -7
View File
@@ -33,7 +33,7 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
// the local HMS info // the local HMS info
// Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for // Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for
static unordered_set<string> package_dev_id_types {"094", "239", "093", "22E"}; static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E"};
// Orca: HMS should be disabled when stealth mode is on or networking is not installed // Orca: HMS should be disabled when stealth mode is on or networking is not installed
static bool should_disable_hms() static bool should_disable_hms()
@@ -345,17 +345,17 @@ wxString HMSQuery::query_hms_msg(const std::string& dev_id, const std::string& l
return _query_hms_msg(dev_id.substr(0, 3), long_error_code, lang_code); return _query_hms_msg(dev_id.substr(0, 3), long_error_code, lang_code);
} }
string HMSQuery::get_dev_id_type(const MachineObject* obj) const std::string HMSQuery::get_dev_id_type(const MachineObject* obj) const
{ {
if (obj) if (obj)
{ {
return obj->get_dev_id().substr(0, 3); return obj->get_dev_id().substr(0, 3);
} }
return string(); return std::string();
} }
wxString HMSQuery::_query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code) wxString HMSQuery::_query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code)
{ {
if (long_error_code.empty()) if (long_error_code.empty())
{ {
@@ -426,9 +426,9 @@ wxString HMSQuery::_query_hms_msg(const string& dev_id_type, const string& long_
return wxEmptyString; return wxEmptyString;
} }
bool HMSQuery::_is_internal_error(const string &dev_id_type, bool HMSQuery::_is_internal_error(const std::string &dev_id_type,
const string &error_code, const std::string &error_code,
const string &lang_code) const std::string &lang_code)
{ {
init_hms_info(dev_id_type); init_hms_info(dev_id_type);
auto iter = m_hms_info_jsons.find(dev_id_type); auto iter = m_hms_info_jsons.find(dev_id_type);
+1 -1
View File
@@ -335,7 +335,7 @@ void PrintJob::process(Ctl &ctl)
auto origin_profile_id = model_info->metadata_items.find(BBL_DESIGNER_PROFILE_ID_TAG); auto origin_profile_id = model_info->metadata_items.find(BBL_DESIGNER_PROFILE_ID_TAG);
if (origin_profile_id != model_info->metadata_items.end()) { if (origin_profile_id != model_info->metadata_items.end()) {
try { try {
params.origin_profile_id = stoi(origin_profile_id->second.c_str()); params.origin_profile_id = std::stoi(origin_profile_id->second.c_str());
} }
catch(...) {} catch(...) {}
} }
+5 -5
View File
@@ -536,12 +536,12 @@ void NotificationManager::PopNotification::count_lines()
return; return;
// handle with marks // handle with marks
if (pos_start == string::npos && pos_end == string::npos) { if (pos_start == std::string::npos && pos_end == std::string::npos) {
pos_start = text.find(error_start); pos_start = text.find(error_start);
if (pos_start != string::npos) { if (pos_start != std::string::npos) {
text.erase(pos_start, error_start.length()); text.erase(pos_start, error_start.length());
pos_end = text.find(error_end); pos_end = text.find(error_end);
if (pos_end != string::npos) { if (pos_end != std::string::npos) {
text.erase(pos_end, error_end.length()); text.erase(pos_end, error_end.length());
} }
} }
@@ -675,7 +675,7 @@ void NotificationManager::PopNotification::bbl_render_block_notif_text(ImGuiWrap
if (m_text1.size() > m_endlines[i]) if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0); last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
if (pos_start != string::npos && pos_end != string::npos && m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) { if (pos_start != std::string::npos && pos_end != std::string::npos && m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity); push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity);
imgui.text(line.c_str()); imgui.text(line.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -737,7 +737,7 @@ void NotificationManager::PopNotification::render_text(ImGuiWrapper& imgui, cons
if (m_text1.size() > m_endlines[i]) if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0); last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
if (pos_start != string::npos && pos_end != string::npos&& m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) { if (pos_start != std::string::npos && pos_end != std::string::npos&& m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity); push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity);
imgui.text(line.c_str()); imgui.text(line.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
+4 -4
View File
@@ -86,7 +86,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
{ {
std::time_t t = std::time(0); std::time_t t = std::time(0);
std::stringstream buf; std::stringstream buf;
buf << put_time(std::localtime(&t), "%a_%b_%d_%H_%M_%S/"); buf << std::put_time(std::localtime(&t), "%a_%b_%d_%H_%M_%S/");
m_timestamp = buf.str(); m_timestamp = buf.str();
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
@@ -402,7 +402,7 @@ std::string PartSkipDialog::create_tmp_path()
return tmp_path; return tmp_path;
} }
bool PartSkipDialog::is_local_file_existed(const std::vector<string> &local_paths) bool PartSkipDialog::is_local_file_existed(const std::vector<std::string> &local_paths)
{ {
for (auto path : local_paths) { for (auto path : local_paths) {
if (!std::filesystem::exists(path)) { return false; } if (!std::filesystem::exists(path)) { return false; }
@@ -703,8 +703,8 @@ void PartSkipDialog::InitDialogUI()
m_parts_state.clear(); m_parts_state.clear();
m_parts_name.clear(); m_parts_name.clear();
string pick_img = m_local_paths[0]; std::string pick_img = m_local_paths[0];
string slice_info = m_local_paths[2]; std::string slice_info = m_local_paths[2];
m_switch_drag_btn->SetIcon("canvas_drag"); m_switch_drag_btn->SetIcon("canvas_drag");
m_switch_drag_btn->SetBackgroundColor(*wxWHITE); m_switch_drag_btn->SetBackgroundColor(*wxWHITE);
+3 -3
View File
@@ -139,11 +139,11 @@ private:
boost::shared_ptr<PrinterFileSystem> m_file_sys; boost::shared_ptr<PrinterFileSystem> m_file_sys;
std::string m_timestamp; std::string m_timestamp;
std::string m_tmp_path; std::string m_tmp_path;
std::vector<string> m_local_paths; std::vector<std::string> m_local_paths;
std::vector<string> m_target_paths; std::vector<std::string> m_target_paths;
std::string create_tmp_path(); std::string create_tmp_path();
bool is_local_file_existed(const std::vector<string> &local_paths); bool is_local_file_existed(const std::vector<std::string> &local_paths);
void DownloadPartsFile(); void DownloadPartsFile();
void OnFileSystemEvent(wxCommandEvent &event); void OnFileSystemEvent(wxCommandEvent &event);
+4 -4
View File
@@ -357,7 +357,7 @@ wxDEFINE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent);
#define PRINTER_PANEL_RADIUS (6) // ORCA #define PRINTER_PANEL_RADIUS (6) // ORCA
#define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98))) #define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98)))
static string get_diameter_string(float diameter) static std::string get_diameter_string(float diameter)
{ {
std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1) std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1)
stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably
@@ -6323,7 +6323,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
} }
// BBS:Record consumables information before synchronization // BBS:Record consumables information before synchronization
std::vector<string> color_before_sync; std::vector<std::string> color_before_sync;
std::vector<bool> is_support_before; std::vector<bool> is_support_before;
DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config;
ConfigOptionStrings* color_opt = project_config.option<ConfigOptionStrings>("filament_colour"); ConfigOptionStrings* color_opt = project_config.option<ConfigOptionStrings>("filament_colour");
@@ -12762,7 +12762,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
Vec3d old_plate_pos = old_plate->get_center_origin(); Vec3d old_plate_pos = old_plate->get_center_origin();
// BBS: Save the model in the current platelist // BBS: Save the model in the current platelist
std::vector<vector<int> > plate_object; std::vector<std::vector<int> > plate_object;
for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) { for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) {
PartPlate* plate = old_plate_list.get_plate(i); PartPlate* plate = old_plate_list.get_plate(i);
std::vector<int> obj_idxs; std::vector<int> obj_idxs;
@@ -17975,7 +17975,7 @@ bool Plater::load_files(const wxArrayString& filenames)
// Orca: Iters through given paths and imports files from zip then remove zip from paths // Orca: Iters through given paths and imports files from zip then remove zip from paths
// returns true if zip files were found // returns true if zip files were found
auto handle_zips = [this](vector<fs::path>& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly auto handle_zips = [this](std::vector<fs::path>& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly
bool res = false; bool res = false;
for (auto it = paths.begin(); it != paths.end();) { for (auto it = paths.begin(); it != paths.end();) {
if (boost::algorithm::iends_with(it->string(), ".zip")) { if (boost::algorithm::iends_with(it->string(), ".zip")) {
+2 -2
View File
@@ -378,7 +378,7 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto current_setting = app_config->get(param); auto current_setting = app_config->get(param);
if (!current_setting.empty()) { if (!current_setting.empty()) {
auto compare = [current_setting](string possible_setting) { return current_setting == possible_setting; }; auto compare = [current_setting](std::string possible_setting) { return current_setting == possible_setting; };
auto iterator = find_if(config_name_index.begin(), config_name_index.end(), compare); auto iterator = find_if(config_name_index.begin(), config_name_index.end(), compare);
if (iterator != config_name_index.end()) if (iterator != config_name_index.end())
current_index = static_cast<unsigned int>(iterator - config_name_index.begin()); current_index = static_cast<unsigned int>(iterator - config_name_index.begin());
@@ -1714,7 +1714,7 @@ void PreferencesDialog::create_items()
g_sizer->Add(create_item_title(_L("Project")), 1, wxEXPAND); g_sizer->Add(create_item_title(_L("Project")), 1, wxEXPAND);
std::vector<wxString> projectLoadSettingsBehaviourOptions = {_L("Load All"), _L("Ask When Relevant"), _L("Always Ask"), _L("Load Geometry Only")}; std::vector<wxString> projectLoadSettingsBehaviourOptions = {_L("Load All"), _L("Ask When Relevant"), _L("Always Ask"), _L("Load Geometry Only")};
std::vector<string> projectLoadSettingsConfigOptions = { OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL, OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT, OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY }; std::vector<std::string> projectLoadSettingsConfigOptions = { OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL, OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT, OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY };
auto item_project_load = create_item_combobox(_L("Load behavior"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions); auto item_project_load = create_item_combobox(_L("Load behavior"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions);
g_sizer->Add(item_project_load); g_sizer->Add(item_project_load);
+1 -1
View File
@@ -239,7 +239,7 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
} }
bool has_content = false; bool has_content = false;
for (const string& v : { for (const std::string& v : {
update_type, update_type,
license, license,
model_name, model_name,
+4 -4
View File
@@ -182,7 +182,7 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t
static int s_nozzle_mapping_last_request_time = 0; static int s_nozzle_mapping_last_request_time = 0;
std::vector<wxString> SelectMachineDialog::MACHINE_BED_TYPE_STRING; std::vector<wxString> SelectMachineDialog::MACHINE_BED_TYPE_STRING;
std::vector<string> SelectMachineDialog::MachineBedTypeString; std::vector<std::string> SelectMachineDialog::MachineBedTypeString;
void SelectMachineDialog::init_machine_bed_types() void SelectMachineDialog::init_machine_bed_types()
{ {
if (MACHINE_BED_TYPE_STRING.size() == 0) { if (MACHINE_BED_TYPE_STRING.size() == 0) {
@@ -2748,8 +2748,8 @@ void SelectMachineDialog::on_ok_btn(wxCommandEvent &event)
std::vector<ConfirmBeforeSendInfo> confirm_text; std::vector<ConfirmBeforeSendInfo> confirm_text;
// check more than one using in same external spool // check more than one using in same external spool
std::unordered_set<string> main_external_spool_filas; std::unordered_set<std::string> main_external_spool_filas;
std::unordered_set<string> deputy_external_spool_filas; std::unordered_set<std::string> deputy_external_spool_filas;
for (const auto& mapping_info : m_ams_mapping_result) { for (const auto& mapping_info : m_ams_mapping_result) {
if (mapping_info.ams_id == VIRTUAL_AMS_MAIN_ID_STR){ if (mapping_info.ams_id == VIRTUAL_AMS_MAIN_ID_STR){
main_external_spool_filas.insert(mapping_info.filament_id); main_external_spool_filas.insert(mapping_info.filament_id);
@@ -5239,7 +5239,7 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
/*Check high temperture slicing*/ /*Check high temperture slicing*/
if (m_print_type == PrintFromType::FROM_NORMAL) { if (m_print_type == PrintFromType::FROM_NORMAL) {
std::set<string> high_temp_filaments; std::set<std::string> high_temp_filaments;
std::unordered_set<int> known_fila_soften_extruders; std::unordered_set<int> known_fila_soften_extruders;
std::unordered_set<int> unknown_fila_soften_extruders; std::unordered_set<int> unknown_fila_soften_extruders;
auto preset_full_config = wxGetApp().preset_bundle->full_config(); auto preset_full_config = wxGetApp().preset_bundle->full_config();
+1 -1
View File
@@ -337,7 +337,7 @@ private:
wxColour m_colour_bold_color{wxColour(38, 46, 48)}; wxColour m_colour_bold_color{wxColour(38, 46, 48)};
StateColor m_btn_bg_enable; StateColor m_btn_bg_enable;
std::unordered_map<string, PrintOption*> m_checkbox_list; std::unordered_map<std::string, PrintOption*> m_checkbox_list;
std::list<PrintOption*> m_checkbox_list_order; std::list<PrintOption*> m_checkbox_list_order;
std::shared_ptr<int> m_token = std::make_shared<int>(0); std::shared_ptr<int> m_token = std::make_shared<int>(0);
+1 -1
View File
@@ -126,7 +126,7 @@ void SendToPrinterDialog::stripWhiteSpace(std::string& str)
{ {
if (str == "") { return; } if (str == "") { return; }
string::iterator cur_it; std::string::iterator cur_it;
cur_it = str.begin(); cur_it = str.begin();
while (cur_it != str.end()) { while (cur_it != str.end()) {
+2 -2
View File
@@ -171,7 +171,7 @@ public:
std::string current_nozzle_loc = ""; std::string current_nozzle_loc = "";
wxColour m_colour; wxColour m_colour;
string m_file_name; std::string m_file_name;
bool m_ams_loading{false}; bool m_ams_loading{false};
void doRender(wxDC &dc); void doRender(wxDC &dc);
ExtruderImage(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize); ExtruderImage(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
@@ -244,7 +244,7 @@ protected:
{ {
wxString local_image_url; //local image path wxString local_image_url; //local image path
std::string img_url_paths; // oss url path std::string img_url_paths; // oss url path
vector<wxPanel *> image_broad; std::vector<wxPanel *> image_broad;
bool is_selected; bool is_selected;
bool is_uploaded; // load bool is_uploaded; // load
wxBoxSizer * image_tb_broad = nullptr; wxBoxSizer * image_tb_broad = nullptr;
+2 -2
View File
@@ -1576,7 +1576,7 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
} }
} }
vector<int> map_extruders = {1, 0}; std::vector<int> map_extruders = {1, 0};
// The default two extruders are left, right, but the order of the extruders on the machine is right, left. // The default two extruders are left, right, but the order of the extruders on the machine is right, left.
std::vector<std::string> flow_type_of_machine; std::vector<std::string> flow_type_of_machine;
@@ -1676,7 +1676,7 @@ void SyncAmsInfoDialog::stripWhiteSpace(std::string &str)
{ {
if (str == "") { return; } if (str == "") { return; }
string::iterator cur_it; std::string::iterator cur_it;
cur_it = str.begin(); cur_it = str.begin();
while (cur_it != str.end()) { while (cur_it != str.end()) {
+1 -1
View File
@@ -2257,7 +2257,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
wxGetApp().plater()->update(); wxGetApp().plater()->update();
} }
string opt_key_without_idx = opt_key.substr(0, opt_key.find('#')); std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#'));
if (opt_key_without_idx == "long_retractions_when_cut") { if (opt_key_without_idx == "long_retractions_when_cut") {
unsigned char activate = boost::any_cast<unsigned char>(value); unsigned char activate = boost::any_cast<unsigned char>(value);
+5 -5
View File
@@ -1682,13 +1682,13 @@ int GuideFrame::SaveProfileData()
return 0; return 0;
} }
void StringReplace(string &strBase, string strSrc, string strDes) void StringReplace(std::string &strBase, std::string strSrc, std::string strDes)
{ {
string::size_type pos = 0; std::string::size_type pos = 0;
string::size_type srcLen = strSrc.size(); std::string::size_type srcLen = strSrc.size();
string::size_type desLen = strDes.size(); std::string::size_type desLen = strDes.size();
pos = strBase.find(strSrc, pos); pos = strBase.find(strSrc, pos);
while ((pos != string::npos)) { while ((pos != std::string::npos)) {
strBase.replace(pos, srcLen, strDes); strBase.replace(pos, srcLen, strDes);
pos = strBase.find(strSrc, (pos + desLen)); pos = strBase.find(strSrc, (pos + desLen));
} }
+1 -1
View File
@@ -1052,7 +1052,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
/*update humidity popup*/ /*update humidity popup*/
if (m_percent_humidity_dry_popup->IsShown()) if (m_percent_humidity_dry_popup->IsShown())
{ {
string target_id = m_percent_humidity_dry_popup->get_owner_ams_id(); std::string target_id = m_percent_humidity_dry_popup->get_owner_ams_id();
for (const auto& the_info : ams_info) for (const auto& the_info : ams_info)
{ {
if (target_id == the_info.ams_id) if (target_id == the_info.ams_id)
+2 -2
View File
@@ -63,7 +63,7 @@ protected:
std::string m_dev_id; std::string m_dev_id;
std::vector<std::vector<std::string>> m_item_ids{ {}, {} }; std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
std::vector<std::pair<string, string>> pair_id; std::vector<std::pair<std::string, std::string>> pair_id;
int m_total_ext_count = 1; int m_total_ext_count = 1;
AMSextruder *m_extruder{nullptr}; AMSextruder *m_extruder{nullptr};
@@ -181,7 +181,7 @@ public:
std::tuple<bool, bool> isFilaSwitchReady(); std::tuple<bool, bool> isFilaSwitchReady();
void show_switcher_status(bool show); void show_switcher_status(bool show);
void UpdatePassRoad(string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step); void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
void CreateAms(); void CreateAms();
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type); void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type); void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
+3 -3
View File
@@ -634,7 +634,7 @@ void AMSextruderImage::doRender(wxDC &dc)
} }
AMSextruderImage::AMSextruderImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos) AMSextruderImage::AMSextruderImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos)
{ {
wxWindow::Create(parent, id, pos, size); wxWindow::Create(parent, id, pos, size);
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
@@ -823,7 +823,7 @@ void SwitcherImage::doRender(wxDC &dc)
Layout(); Layout();
} }
SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos) SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos)
{ {
wxWindow::Create(parent, id, pos, size); wxWindow::Create(parent, id, pos, size);
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
@@ -899,7 +899,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
} }
/*return true if something is updated*/ /*return true if something is updated*/
bool AMSextruder::updateNozzleNum(int nozzle_num, const string& series_name) bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name)
{ {
if (m_nozzle_num == nozzle_num && m_series_name == series_name) return false; if (m_nozzle_num == nozzle_num && m_series_name == series_name) return false;
m_series_name = series_name; m_series_name = series_name;
+6 -6
View File
@@ -393,10 +393,10 @@ public:
bool m_show_state = {false}; bool m_show_state = {false};
wxColour m_colour; wxColour m_colour;
ScalableBitmap m_ams_extruder; ScalableBitmap m_ams_extruder;
string m_file_name; std::string m_file_name;
bool m_ams_loading{ false }; bool m_ams_loading{ false };
void doRender(wxDC &dc); void doRender(wxDC &dc);
AMSextruderImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition); AMSextruderImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
~AMSextruderImage(); ~AMSextruderImage();
}; };
@@ -444,10 +444,10 @@ public:
bool m_show_state = {false}; bool m_show_state = {false};
wxColour m_colour; wxColour m_colour;
ScalableBitmap m_switcher; ScalableBitmap m_switcher;
string m_file_name; std::string m_file_name;
// bool m_ams_loading{ false }; // bool m_ams_loading{ false };
void doRender(wxDC &dc); void doRender(wxDC &dc);
SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition); SwitcherImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
~SwitcherImage(); ~SwitcherImage();
}; };
@@ -456,7 +456,7 @@ class AMSextruder : public wxWindow
{ {
private: private:
int m_nozzle_num = -1; int m_nozzle_num = -1;
string m_series_name; std::string m_series_name;
public: public:
void TurnOn(wxColour col); void TurnOn(wxColour col);
@@ -466,7 +466,7 @@ public:
void msw_rescale(); void msw_rescale();
void has_ams(bool hams) {m_has_vams = hams; Refresh();}; void has_ams(bool hams) {m_has_vams = hams; Refresh();};
void no_ams_mode(bool mode) {m_none_ams_mode = mode; Refresh();}; void no_ams_mode(bool mode) {m_none_ams_mode = mode; Refresh();};
bool updateNozzleNum(int nozzle_num, const std::string& series_name = string()); bool updateNozzleNum(int nozzle_num, const std::string& series_name = std::string());
bool m_none_ams_mode{true}; bool m_none_ams_mode{true};
bool m_has_vams{false}; bool m_has_vams{false};
+2 -1
View File
@@ -596,7 +596,8 @@ PrintParams_Legacy BBLNetworkPlugin::as_legacy(PrintParams& param)
} }
// Every PrintParams field except the four the 02.08.01 series added // Every PrintParams field except the four the 02.08.01 series added
// (task_timelapse_use_internal, extruder_cali_manual_mode, svc_context, slicer_uid). // (task_timelapse_use_internal, extruder_cali_manual_mode, svc_context, slicer_uid) and the
// queue_plate_id 02.08.02 appended.
PrintParams_0203 BBLNetworkPlugin::as_0203(PrintParams& param) PrintParams_0203 BBLNetworkPlugin::as_0203(PrintParams& param)
{ {
PrintParams_0203 p; PrintParams_0203 p;
+22 -6
View File
@@ -204,8 +204,9 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
int dir = input_val > 0 ? 1 : -1; int dir = input_val > 0 ? 1 : -1;
// i3-arch printers move the bed for Y/Z, so the on-screen direction is // i3-arch printers move the bed for Y/Z, so the on-screen direction is
// reversed -- same negation the g-code fallback below applies. // reversed -- same negation the g-code fallback below applies.
if (!is_core_xy && (axis == "Y" || axis == "Z")) if (!is_core_xy && (axis == "Y" || axis == "Z")) {
dir = -dir; dir = -dir;
}
j["print"]["command"] = "xyz_ctrl"; j["print"]["command"] = "xyz_ctrl";
j["print"]["axis"] = axis; j["print"]["axis"] = axis;
@@ -215,8 +216,9 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
} }
double value = input_val; double value = input_val;
if (!is_core_xy && (axis == "Y" || axis == "Z")) if (!is_core_xy && (axis == "Y" || axis == "Z")) {
value = -input_val; value = -1.0 * input_val;
}
std::string value_str = (boost::format("%.1f") % (value * unit)).str(); std::string value_str = (boost::format("%.1f") % (value * unit)).str();
std::string gcode; std::string gcode;
@@ -237,10 +239,11 @@ int BBLPrinterAgent::command_axis_control(std::string dev_id, std::string axis,
int BBLPrinterAgent::publish(const std::string& dev_id, const nlohmann::json& j, bool 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); 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) if (rtn == 0) {
BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn; BOOST_LOG_TRIVIAL(info) << "publish_json: " << j.dump() << " code: " << rtn;
else } else {
BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn; BOOST_LOG_TRIVIAL(error) << "publish_json: " << j.dump() << " code: " << rtn;
}
return rtn; return rtn;
} }
@@ -586,8 +589,21 @@ int BBLPrinterAgent::start_local_print_with_record(PrintParams params, OnUpdateS
int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) int BBLPrinterAgent::start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn)
{ {
return dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>( // dispatch_start() moves out of `params`, so snapshot the diagnostic fields first;
// logging them after the call would print empty strings.
const bool try_emmc_print = params.try_emmc_print;
const std::string dev_ip = params.dev_ip;
const std::string dev_id = params.dev_id;
int result = dispatch_start<func_start_send_gcode_to_sdcard_legacy, func_start_send_gcode_to_sdcard_0203>(
BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn); BBLNetworkPlugin::instance().get_start_send_gcode_to_sdcard(), params, update_fn, cancel_fn, wait_fn);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << "start_send_gcode_to_sdcard failed: result=" << result
<< ", try_emmc_print=" << try_emmc_print
<< ", legacy_mode=" << BBLNetworkPlugin::instance().use_legacy_network()
<< ", dev_ip=" << dev_ip << ", dev_id=" << dev_id;
}
return result;
} }
int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) int BBLPrinterAgent::start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn)
+1
View File
@@ -110,6 +110,7 @@ public:
static std::string from_orca_payload(std::string json_text); static std::string from_orca_payload(std::string json_text);
private: 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); int publish(const std::string& dev_id, const nlohmann::json& j, bool lan_mode);
}; };
+9 -4
View File
@@ -288,9 +288,14 @@ bool CrealityPrintAgent::parse_cfs_response(const std::string& response,
return true; return true;
} }
bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
if (device_info.dev_ip.empty()) { if (sync_mode != get_filament_sync_mode())
return false;
const MoonrakerDeviceInfo info = snapshot_device_info();
if (info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning) BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent"; << "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id)); return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
@@ -299,11 +304,11 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
// Build a CrealityPrint helper so we can use its model detection + WS helpers // Build a CrealityPrint helper so we can use its model detection + WS helpers
// (added in upstream PR #13291). // (added in upstream PR #13291).
DynamicPrintConfig cfg; DynamicPrintConfig cfg;
cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip)); cfg.set_key_value("print_host", new ConfigOptionString("http://" + info.dev_ip));
cfg.set_key_value("print_host_webui", new ConfigOptionString("")); cfg.set_key_value("print_host_webui", new ConfigOptionString(""));
cfg.set_key_value("printhost_cafile", new ConfigOptionString("")); cfg.set_key_value("printhost_cafile", new ConfigOptionString(""));
cfg.set_key_value("printhost_port", new ConfigOptionString("")); cfg.set_key_value("printhost_port", new ConfigOptionString(""));
cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key)); cfg.set_key_value("printhost_apikey", new ConfigOptionString(info.api_key));
cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false)); cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false));
CrealityPrint host(&cfg); CrealityPrint host(&cfg);
+1 -1
View File
@@ -37,7 +37,7 @@ public:
}; };
explicit CrealityPrintAgent(std::string log_dir); explicit CrealityPrintAgent(std::string log_dir);
~CrealityPrintAgent() override = default; ~CrealityPrintAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
File diff suppressed because it is too large Load Diff
+172 -25
View File
@@ -12,6 +12,10 @@
#include <set> #include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <vector> #include <vector>
@@ -20,6 +24,56 @@ namespace Slic3r { class ICloudServiceAgent; }
namespace Slic3r { namespace Slic3r {
class Http;
bool moonraker_is_light_name(const std::string& name);
// Direction encoded by a light name: +1 turns on, -1 turns off, 0 is an ambiguous toggle.
// e.g. LIGHT_ON -> +1, LIGHT_OFF -> -1, LIGHT -> 0.
int moonraker_light_name_direction(const std::string& name);
struct MoonrakerWebcamSelection
{
std::string url;
CameraStreamMode mode = CameraStreamMode::none;
std::string name;
std::string error; // set when no selectable webcam was found
};
// Selects a webcam from a parsed /server/webcams/list response (the top-level JSON or its
// "result" value). Returns true and fills url/mode/name on success; on failure returns
// false and sets error.
bool moonraker_parse_webcam_list(const nlohmann::json& response, const std::string& base_url,
MoonrakerWebcamSelection& out);
class MoonrakerWebsocket
{
public:
enum class ReadResult
{
message,
timeout,
closed,
error,
};
MoonrakerWebsocket(bool secure, std::string api_key, std::string ca_file);
~MoonrakerWebsocket();
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
void tls_handshake(const std::string& host);
void handshake(const std::string& host, const std::string& target);
void text(bool enabled);
void write(const std::string& body);
ReadResult read(std::string& payload, std::string& error_message);
void close();
void expires_after(std::chrono::seconds timeout);
void abort();
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
class MoonrakerPrinterAgent : public IPrinterAgent class MoonrakerPrinterAgent : public IPrinterAgent
{ {
public: public:
@@ -65,12 +119,24 @@ public:
int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_connect_fn(OnLocalConnectedFn fn) override;
int set_on_local_message_fn(OnMessageFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override;
int set_queue_on_main_fn(QueueOnMainFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override;
// 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, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Called by the fetch-thread RAII guard when a background filament fetch finishes.
// Serialized with the reservation so shutdown()'s wait cannot miss the transition to 0.
void release_fetch_slot() noexcept;
protected: protected:
struct ConnectionSettings
{
std::string dev_id;
std::string base_url;
std::string api_key;
bool use_ssl = false;
std::string ca_file;
};
struct MoonrakerDeviceInfo struct MoonrakerDeviceInfo
{ {
std::string dev_id; std::string dev_id;
@@ -82,7 +148,9 @@ protected:
std::string dev_name; std::string dev_name;
std::string version; std::string version;
std::string klippy_state; std::string klippy_state;
float nozzle_diameter = 0.0f;
bool use_ssl = false; bool use_ssl = false;
std::string ca_file;
} device_info; } device_info;
// Tray data for AMS payload building // Tray data for AMS payload building
@@ -100,12 +168,29 @@ protected:
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays); void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
// Methods that derived classes may need to override or access // Methods that derived classes may need to override or access
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port); virtual bool init_device_info(const PrinterConnectionParams& params);
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const; virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const;
ConnectionSettings get_connection_settings() const;
// Copy of the mutable connection state, taken under connect_mutex. Background threads
// must use this instead of reading device_info directly.
MoonrakerDeviceInfo snapshot_device_info() const;
void configure_http(Http& http, const ConnectionSettings& connection) const;
static float parse_nozzle_diameter(const nlohmann::json& response);
// State access for derived classes // State access for derived classes
mutable std::recursive_mutex state_mutex; mutable std::recursive_mutex state_mutex;
// Detached fetch threads hold a raw `this`; shutdown() waits for this to reach 0.
std::atomic<int> filament_fetch_in_flight{0};
// Idempotent teardown; must be called from the most-derived destructor.
void shutdown();
std::atomic<bool> shutting_down{false};
// Serializes the shutting_down check with the in-flight reservation.
std::mutex fetch_lifecycle_mutex;
std::condition_variable fetch_done_cv; // notified when filament_fetch_in_flight reaches 0
// Helpers // Helpers
bool is_numeric(const std::string& value); bool is_numeric(const std::string& value);
std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port); std::string normalize_base_url(bool use_ssl, const std::string& host, const std::string& port);
@@ -118,13 +203,30 @@ protected:
// Map filament type to OrcaFilamentLibrary preset ID for AMS sync compatibility // 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); 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 ConnectionSettings& connection) const;
bool post_print_action(const std::string& action) const;
bool post_print_action(const std::string& action,
const ConnectionSettings& connection) const;
bool send_ws_rpc(const std::string& method, const nlohmann::json& params);
virtual void on_status_loop_tick(const std::string& dev_id) {}
// Queue work that may use agent state. The command worker is joined during
// destruction, so queued commands cannot outlive the agent.
void enqueue_command(std::function<void()> fn);
mutable std::recursive_mutex connect_mutex;
private: private:
int handle_request(const std::string& dev_id, const std::string& json_str); int handle_request(const std::string& dev_id, const std::string& json_str);
int send_version_info(const std::string& dev_id); int send_version_info(const std::string& dev_id);
int send_access_code(const std::string& dev_id); int send_access_code(const std::string& dev_id);
bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set<std::string>& objects, std::string& error) const; bool fetch_object_list(const ConnectionSettings& connection, 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 query_printer_status(const ConnectionSettings& connection, nlohmann::json& status, std::string& error) const;
bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const; bool send_gcode_sync(const std::string& dev_id, const std::string& gcode) const;
void send_gcode_async(const std::string& dev_id, const std::string& gcode, void send_gcode_async(const std::string& dev_id, const std::string& gcode,
std::function<void(bool)> on_result = {}) const; std::function<void(bool)> on_result = {}) const;
@@ -132,37 +234,48 @@ private:
void announce_printhost_device(); void announce_printhost_device();
void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg);
void dispatch_printer_connected(const std::string& dev_id); void dispatch_printer_connected(const std::string& dev_id);
// Self-contained snapshot of the message callbacks. Async completions capture this
// by value so they never dereference `this` after the agent may have been destroyed.
struct MessageRouter
{
OnMessageFn local_fn;
OnMessageFn cloud_fn;
QueueOnMainFn queue_fn;
std::string dev_id;
void deliver(std::string payload) const;
};
MessageRouter make_message_router(const std::string& dev_id) const;
void dispatch_message(const std::string& dev_id, const std::string& payload); void dispatch_message(const std::string& dev_id, const std::string& payload);
void start_status_stream(const std::string& dev_id, const std::string& base_url, const std::string& api_key); void start_status_stream(const std::string& dev_id, ConnectionSettings connection);
void stop_status_stream(); void stop_status_stream();
void run_status_stream(std::string dev_id, std::string base_url, std::string api_key); void run_status_stream(std::string dev_id, ConnectionSettings connection);
void handle_ws_message(const std::string& dev_id, const std::string& payload); void handle_ws_message(std::string dev_id, std::string payload, ConnectionSettings connection);
void refresh_thumbnail_url(const ConnectionSettings& connection);
void update_status_cache(const nlohmann::json& updates); void update_status_cache(const nlohmann::json& updates);
nlohmann::json build_print_payload_locked() const; nlohmann::json build_print_payload_locked() const;
// Print control helpers
int pause_print(const std::string& dev_id);
int resume_print(const std::string& dev_id);
int cancel_print(const std::string& dev_id);
// File upload // File upload
bool upload_gcode(const std::string& local_path, const std::string& filename, bool upload_gcode(const std::string& local_path, const std::string& filename,
const std::string& base_url, const std::string& api_key, const ConnectionSettings& connection,
OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn);
// 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 // Connection thread management
void perform_connection_async(const std::string& dev_id, void perform_connection_async(const std::string& dev_id,
const std::string& base_url, ConnectionSettings connection,
const std::string& api_key,
uint64_t generation); uint64_t generation);
// why: a printer with no /server/webcams/list entry can still name its stream directly;
// subclasses (e.g. printers with a fixed webcam path) can override this instead.
virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; }
void refresh_webcam_info() const;
bool fetch_webcam_info(const ConnectionSettings& connection, uint64_t generation) const;
// System-specific filament fetch methods // System-specific filament fetch methods
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_hh_filament_info(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index); bool fetch_moonraker_filament_data(const ConnectionSettings& connection, std::vector<AmsTrayData>& trays, int& max_lane_index);
// JSON helper methods // JSON helper methods
static std::string safe_json_string(const nlohmann::json& obj, const char* key); static std::string safe_json_string(const nlohmann::json& obj, const char* key);
@@ -188,15 +301,43 @@ private:
mutable std::recursive_mutex payload_mutex; mutable std::recursive_mutex payload_mutex;
nlohmann::json status_cache; 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;
mutable std::string webcam_stream_url;
mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none;
// Next time the status loop may look the webcam up again (steady_clock ms).
mutable std::atomic<uint64_t> webcam_info_next_attempt_ms{0};
unsigned thumbnail_lookup_attempts = 0;
static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 30000;
static constexpr uint64_t WEBCAM_INFO_FAILURE_BACKOFF_MS = 60000;
std::atomic<int> next_jsonrpc_id{1}; std::atomic<int> next_jsonrpc_id{1};
std::set<std::string> available_objects; // Track for feature detection 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_stop{false};
std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection std::atomic<bool> ws_reconnect_requested{false}; // Flag to trigger reconnection
std::atomic<uint64_t> ws_last_emit_ms{0}; std::atomic<uint64_t> ws_last_emit_ms{0};
std::thread ws_thread; std::thread ws_thread;
// stop_status_stream() invokes ws_abort_io to wake a blocked synchronous
// ws.read()/ws.write()/handshake in run_status_stream(): ws_stop is only
// observed between reads, and Beast's expires_after() does not bound
// synchronous operations.
std::mutex ws_abort_mutex;
std::function<void()> ws_abort_io; // guarded by ws_abort_mutex
// Interrupts the exponential reconnect backoff in run_status_stream() when stopping.
std::mutex ws_wait_mutex;
std::condition_variable ws_wait_cv;
// 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 // Throttling configuration for WebSocket updates
// Critical changes (state transitions) dispatch immediately; telemetry is throttled // Critical changes (state transitions) dispatch immediately; telemetry is throttled
static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry static constexpr uint64_t STATUS_UPDATE_INTERVAL_MS = 1000; // 1 update/sec for telemetry
@@ -206,7 +347,13 @@ private:
// Connection thread management // Connection thread management
std::atomic<uint64_t> connect_generation{0}; std::atomic<uint64_t> connect_generation{0};
std::thread connect_thread; std::thread connect_thread;
std::recursive_mutex connect_mutex;
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;
}; };
} // namespace Slic3r } // namespace Slic3r
+213 -56
View File
@@ -2,18 +2,24 @@
#include "Http.hpp" #include "Http.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <atomic>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/trim.hpp> #include <boost/algorithm/string/trim.hpp>
#include <boost/algorithm/string/case_conv.hpp> #include <boost/algorithm/string/case_conv.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype> #include <cctype>
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include <cstddef>
#include <exception>
#include <map> #include <map>
#include <mutex>
#include <sstream> #include <sstream>
#include <thread>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -34,6 +40,26 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
return false; return false;
} }
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively.
int read_int_or(const nlohmann::json& obj, const std::string& key, int fallback)
{
auto it = obj.find(key);
return (it != obj.end() && it->is_number_integer()) ? it->get<int>() : fallback;
}
} // anonymous namespace } // anonymous namespace
const std::string QidiPrinterAgent_VERSION = "0.0.1"; const std::string QidiPrinterAgent_VERSION = "0.0.1";
@@ -47,51 +73,166 @@ AgentInfo QidiPrinterAgent::get_agent_info_static()
return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"}; return AgentInfo{"qidi", "Qidi", QidiPrinterAgent_VERSION, "Qidi printer agent"};
} }
bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) FilamentSyncMode QidiPrinterAgent::get_filament_sync_mode() const
{ {
std::string error; if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
// 1. Fetch device info and infer series_id bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
std::string series_id; {
{ if (sync_mode != get_filament_sync_mode())
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; return false;
// Snapshot what the fetch needs; a reconnect can rewrite device_info meanwhile.
ConnectionSettings connection = get_connection_settings();
std::string model_id;
std::string model_name;
{
std::lock_guard<std::recursive_mutex> lock(connect_mutex);
model_id = device_info.model_id;
model_name = device_info.model_name;
} }
// 4. Build the AMS payload // Reserve under the same mutex shutdown() uses, so the flag and the count can't race.
build_ams_payload(box_count, box_count * 4 - 1, trays); {
std::lock_guard<std::mutex> lock(fetch_lifecycle_mutex);
if (shutting_down.load())
return false;
if (filament_fetch_in_flight.load() > 0)
return true; // a fetch is already running; don't pile on
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
}
InFlightGuard guard{*this};
std::thread([this, guard = std::move(guard), connection = std::move(connection), model_id, model_name]() mutable {
try {
std::string error;
// 1. Fetch device info and infer series_id
std::string series_id;
{
MoonrakerDeviceInfo info;
if (fetch_device_info(connection, 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(connection, 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(connection, 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);
} catch (const std::exception& e) {
// why: an exception escaping a detached thread is std::terminate, and firmware
// JSON is untrusted; mirror run_command_worker and swallow it here.
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception: " << e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::fetch_filament_info: unhandled exception";
}
}).detach();
return true; return true;
} }
bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url, bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
const std::string& api_key, {
// 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;
const std::string dev_id = get_connection_settings().dev_id;
// Build one gcode/script request instead of N blocking HTTP calls: apply_box_mapping
// runs on the caller's (GUI) thread before the print starts, so per-tool round trips
// would freeze the UI.
std::string script = "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable);
// 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) {
if (params.ams_mapping.empty()) {
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::apply_box_mapping: enable_box set but ams_mapping is empty";
} else {
// ams_mapping (v0) is a JSON array indexed by filament/tool; each value is the
// physical box slot (-1 = unmapped). Mirror it onto value_t<tool>.
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
script += "\nSAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
}
}
}
if (!send_gcode(dev_id, script)) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to send box mapping";
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)
{
// A failed box mapping is a send failure, not an upload failure.
if (!apply_box_mapping(params))
return BAMBU_NETWORK_ERR_PRINT_LP_PUBLISH_MSG_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 ConnectionSettings& connection,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error) std::string& error)
{ {
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables"); std::string url = join_url(connection.base_url, "/printer/objects/query?save_variables=variables");
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button"; url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
} }
@@ -101,8 +242,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
@@ -127,22 +269,12 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return false; return false;
} }
auto json = nlohmann::json::parse(response_body, nullptr, false, true); nlohmann::json status;
if (json.is_discarded()) { nlohmann::json variables;
error = "Invalid JSON response"; if (!parse_slot_response(response_body, status, variables, error))
return false; return false;
}
if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("save_variables") || box_count = read_int_or(variables, "box_count", 1);
!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) { if (box_count < 0) {
box_count = 0; box_count = 0;
} }
@@ -165,9 +297,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
tray.slot_index = i; tray.slot_index = i;
// Read slot variables // Read slot variables
const int color_index = variables.value("color_slot" + std::to_string(i), 1); const int color_index = read_int_or(variables, "color_slot" + std::to_string(i), 1);
const int filament_type = variables.value("filament_slot" + std::to_string(i), 1); const int filament_type = read_int_or(variables, "filament_slot" + std::to_string(i), 1);
const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0); const int vendor_type = read_int_or(variables, "vendor_slot" + std::to_string(i), 0);
// Check filament presence via runout sensor // Check filament presence via runout sensor
std::string box_stepper_key = "box_stepper slot" + std::to_string(i); std::string box_stepper_key = "box_stepper slot" + std::to_string(i);
@@ -175,7 +307,7 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
if (status.contains(box_stepper_key)) { if (status.contains(box_stepper_key)) {
auto& box_stepper = status[box_stepper_key]; auto& box_stepper = status[box_stepper_key];
if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) { if (box_stepper.contains("runout_button") && !box_stepper["runout_button"].is_null()) {
int runout_button = box_stepper["runout_button"].template get<int>(); const int runout_button = read_int_or(box_stepper, "runout_button", 0);
tray.has_filament = (runout_button == 0); tray.has_filament = (runout_button == 0);
} }
} }
@@ -215,20 +347,45 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
return true; return true;
} }
bool QidiPrinterAgent::fetch_filament_dict(const std::string& base_url, bool QidiPrinterAgent::parse_slot_response(const std::string& response_body,
const std::string& api_key, 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 ConnectionSettings& connection,
QidiFilamentDict& dict, QidiFilamentDict& dict,
std::string& error) const std::string& error) const
{ {
std::string url = join_url(base_url, "/server/files/config/officiall_filas_list.cfg"); std::string url = join_url(connection.base_url, "/server/files/config/officiall_filas_list.cfg");
std::string response_body; std::string response_body;
bool success = false; bool success = false;
std::string http_error; std::string http_error;
auto http = Http::get(url); auto http = Http::get(url);
if (!api_key.empty()) { configure_http(http, connection);
http.header("X-Api-Key", api_key); if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
http.timeout_connect(5) http.timeout_connect(5)
.timeout_max(10) .timeout_max(10)
+21 -4
View File
@@ -3,6 +3,8 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "nlohmann/json_fwd.hpp"
#include <map> #include <map>
#include <string> #include <string>
@@ -14,7 +16,7 @@ class QidiPrinterAgent final : public MoonrakerPrinterAgent
{ {
public: public:
explicit QidiPrinterAgent(std::string log_dir); explicit QidiPrinterAgent(std::string log_dir);
~QidiPrinterAgent() override = default; ~QidiPrinterAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return get_agent_info_static(); } AgentInfo get_agent_info() override { return get_agent_info_static(); }
@@ -22,7 +24,23 @@ public:
// Override filament sync (Qidi-specific implementation) // Override filament sync (Qidi-specific implementation)
bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; 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;
private: 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 struct QidiFilamentDict
{ {
std::map<int, std::string> colors; std::map<int, std::string> colors;
@@ -30,14 +48,13 @@ private:
}; };
// Qidi-specific methods // Qidi-specific methods
bool fetch_slot_info(const std::string& base_url, bool fetch_slot_info(const ConnectionSettings& connection,
const std::string& api_key,
const QidiFilamentDict& dict, const QidiFilamentDict& dict,
const std::string& series_id, const std::string& series_id,
std::vector<AmsTrayData>& trays, std::vector<AmsTrayData>& trays,
int& box_count, int& box_count,
std::string& error); std::string& error);
bool fetch_filament_dict(const std::string& base_url, const std::string& api_key, QidiFilamentDict& dict, std::string& error) const; bool fetch_filament_dict(const ConnectionSettings& connection, QidiFilamentDict& dict, std::string& error) const;
std::string normalize_filament_type(const std::string& filament_type); std::string normalize_filament_type(const std::string& filament_type);
std::string infer_series_id(const std::string& model_id, const std::string& dev_name); std::string infer_series_id(const std::string& model_id, const std::string& dev_name);
std::string normalize_model_key(std::string value); std::string normalize_model_key(std::string value);
+250 -101
View File
@@ -2,11 +2,19 @@
#include "Http.hpp" #include "Http.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "bambu_networking.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include <atomic>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <chrono>
#include <cstdint>
#include <exception>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector> #include <vector>
#include <string> #include <string>
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
@@ -18,6 +26,77 @@ namespace Slic3r {
namespace { namespace {
constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1"; constexpr const char* SNAPMAKER_AGENT_VERSION = "0.0.1";
constexpr int64_t CAMERA_REFRESH_INTERVAL_MS = 300'000;
int64_t now_ms()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
// null/wrong-typed value throws. Firmware JSON is untrusted, so read defensively.
int read_int_or(const nlohmann::json& obj, const char* key, int fallback)
{
auto it = obj.find(key);
return (it != obj.end() && it->is_number_integer()) ? it->get<int>() : fallback;
}
std::vector<std::string> read_string_array_or(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->is_array())
return {};
std::vector<std::string> out;
out.reserve(it->size());
for (const auto& v : *it)
out.push_back(v.is_string() ? v.get<std::string>() : std::string{});
return out;
}
std::vector<bool> read_bool_array_or(const nlohmann::json& obj, const char* key)
{
auto it = obj.find(key);
if (it == obj.end() || !it->is_array())
return {};
std::vector<bool> out;
out.reserve(it->size());
for (const auto& v : *it)
out.push_back(v.is_boolean() ? v.get<bool>() : false);
return out;
}
// Parse a hex colour, stopping at the first non-hex character. std::stoul throws on
// empty/non-hex input, which must not escape the detached fetch thread.
unsigned int parse_hex_color(const std::string& hex)
{
unsigned int value = 0;
for (char c : hex) {
int digit;
if (c >= '0' && c <= '9')
digit = c - '0';
else if (c >= 'a' && c <= 'f')
digit = c - 'a' + 10;
else if (c >= 'A' && c <= 'F')
digit = c - 'A' + 10;
else
break;
value = (value << 4) | static_cast<unsigned int>(digit);
}
return value;
}
// Safely access a parallel array by index, returning a fallback if out of bounds. // Safely access a parallel array by index, returning a fallback if out of bounds.
template<typename T> template<typename T>
@@ -40,14 +119,16 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name && filaments.get_preset_base(p) == &p && p.config.opt_string("filament_vendor", 0u) == vendor_name &&
p.config.opt_string("filament_type", 0u) == filament_type) { p.config.opt_string("filament_type", 0u) == filament_type) {
// The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB, // The printer returns RGBA in the format RRGGBBAA, but profiles store color as #RRGGBB,
// so we must remove # and ignore alpha channel for distance calculation // so we must remove # and ignore alpha channel for distance calculation. Firmware
unsigned int target_color_value = std::stoul(color_rgba.substr(0, color_rgba.length() - 2), nullptr, 16); // colours are untrusted; parse_hex_color tolerates empty/non-hex instead of throwing.
unsigned int target_color_value =
parse_hex_color(color_rgba.substr(0, color_rgba.size() >= 2 ? color_rgba.size() - 2 : 0));
std::string p_color = p.config.opt_string("default_filament_colour", 0u); std::string p_color = p.config.opt_string("default_filament_colour", 0u);
unsigned int p_color_value; unsigned int p_color_value = 0;
if (!p_color.empty()) { if (!p_color.empty()) {
size_t hash_pos = p_color.find("#"); size_t hash_pos = p_color.find("#");
p_color_value = std::stoul(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0), nullptr, 16); p_color_value = parse_hex_color(p_color.substr(hash_pos != std::string::npos ? hash_pos + 1 : 0));
} else { } else {
// Default to black if no color specified in profile. Assume other profiles might be a closer color match. // Default to black if no color specified in profile. Assume other profiles might be a closer color match.
// Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color // Could be a problem if the target color is also black and there exist a specific profile for that type, vendor and color
@@ -74,6 +155,31 @@ std::string find_closest_color_preset_by_vendor_and_type(const PresetCollection&
SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {} SnapmakerPrinterAgent::SnapmakerPrinterAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
void SnapmakerPrinterAgent::start_camera_monitor()
{
enqueue_command([this] {
send_ws_rpc("camera.start_monitor",
{{"domain", "lan"}, {"interval", 0}, {"expect_pw", false}});
});
m_camera_last_fire_ms.store(now_ms());
}
void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id)
{
(void) dev_id;
const int64_t last = m_camera_last_fire_ms.load();
if (last == 0 || now_ms() - last >= CAMERA_REFRESH_INTERVAL_MS) {
start_camera_monitor();
}
}
int SnapmakerPrinterAgent::command_start_camera(std::string dev_id)
{
(void) dev_id;
start_camera_monitor();
return BAMBU_NETWORK_SUCCESS;
}
AgentInfo SnapmakerPrinterAgent::get_agent_info_static() AgentInfo SnapmakerPrinterAgent::get_agent_info_static()
{ {
return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"}; return AgentInfo{"snapmaker", "Snapmaker", SNAPMAKER_AGENT_VERSION, "Snapmaker printer agent"};
@@ -109,127 +215,170 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type
return base; return base;
} }
bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode /*sync_mode*/) bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode)
{ {
std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); (void) dev_id;
if (sync_mode != get_filament_sync_mode())
return false;
std::string response_body; // Snapshot everything the fetch needs (URL, api key, TLS/CA): a reconnect can rewrite
bool success = false; // device_info meanwhile.
std::string http_error; const ConnectionSettings connection = get_connection_settings();
auto http = Http::get(url); // Reserve under the same mutex shutdown() uses, so the flag and the count can't race.
if (!device_info.api_key.empty()) { {
http.header("X-Api-Key", device_info.api_key); std::lock_guard<std::mutex> lock(fetch_lifecycle_mutex);
if (shutting_down.load())
return false;
if (filament_fetch_in_flight.load() > 0)
return true; // a fetch is already running; don't pile on
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
} }
http.timeout_connect(5)
.timeout_max(10) InFlightGuard guard{*this};
.on_complete([&](std::string body, unsigned status) { std::thread([this, guard = std::move(guard), connection]() {
if (status == 200) { try {
response_body = body; const std::string url = join_url(connection.base_url, "/printer/objects/query?print_task_config&filament_detect");
success = true;
} else { std::string response_body;
http_error = "HTTP error: " + std::to_string(status); bool success = false;
std::string http_error;
auto http = Http::get(url);
configure_http(http, connection);
if (!connection.api_key.empty()) {
http.header("X-Api-Key", connection.api_key);
} }
}) http.timeout_connect(5)
.on_error([&](std::string body, std::string err, unsigned status) { .timeout_max(10)
http_error = err; .on_complete([&](std::string body, unsigned status) {
if (status > 0) { if (status == 200) {
http_error += " (HTTP " + std::to_string(status) + ")"; response_body = body;
success = true;
} else {
http_error = "HTTP error: " + std::to_string(status);
}
})
.on_error([&](std::string body, std::string err, unsigned status) {
http_error = err;
if (status > 0) {
http_error += " (HTTP " + std::to_string(status) + ")";
}
})
.perform_sync();
if (!success) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error;
return;
} }
})
.perform_sync();
if (!success) { auto json = nlohmann::json::parse(response_body, nullptr, false, true);
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; if (json.is_discarded()) {
return false; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response";
} return;
}
auto json = nlohmann::json::parse(response_body, nullptr, false, true); // Navigate to result.status.print_task_config
if (json.is_discarded()) { if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return false; return;
} }
// Navigate to result.status.print_task_config auto& ptc = json["result"]["status"]["print_task_config"];
if (!json.contains("result") || !json["result"].contains("status") ||
!json["result"]["status"].contains("print_task_config")) {
BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response";
return false;
}
auto& ptc = json["result"]["status"]["print_task_config"]; // Read parallel arrays from print_task_config
auto filament_exist = read_bool_array_or(ptc, "filament_exist");
auto filament_type = read_string_array_or(ptc, "filament_type");
auto filament_sub_type = read_string_array_or(ptc, "filament_sub_type");
auto filament_color = read_string_array_or(ptc, "filament_color_rgba");
auto filament_vendor = read_string_array_or(ptc, "filament_vendor");
// Read parallel arrays from print_task_config const int slot_count = static_cast<int>(filament_exist.size());
auto filament_exist = ptc.value("filament_exist", std::vector<bool>{}); if (slot_count == 0) {
auto filament_type = ptc.value("filament_type", std::vector<std::string>{}); BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported";
auto filament_sub_type = ptc.value("filament_sub_type", std::vector<std::string>{}); return;
auto filament_color = ptc.value("filament_color_rgba", std::vector<std::string>{}); }
auto filament_vendor = ptc.value("filament_vendor", std::vector<std::string>{});
const int slot_count = static_cast<int>(filament_exist.size()); // Read NFC filament_detect data for temperature info (optional)
if (slot_count == 0) { nlohmann::json nfc_info;
BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) {
return false; nfc_info = json["result"]["status"]["filament_detect"]["info"];
} }
// Read NFC filament_detect data for temperature info (optional) static const std::string empty_str;
nlohmann::json nfc_info; static const std::string default_color = "FFFFFFFF";
if (json["result"]["status"].contains("filament_detect") &&
json["result"]["status"]["filament_detect"].contains("info")) {
nfc_info = json["result"]["status"]["filament_detect"]["info"];
}
static const std::string empty_str; std::vector<AmsTrayData> trays;
static const std::string default_color = "FFFFFFFF"; trays.reserve(slot_count);
std::vector<AmsTrayData> trays; for (int i = 0; i < slot_count; ++i) {
trays.reserve(slot_count); AmsTrayData tray;
tray.slot_index = i;
tray.has_filament = filament_exist[i];
for (int i = 0; i < slot_count; ++i) { if (tray.has_filament) {
AmsTrayData tray; tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str));
tray.slot_index = i; tray.tray_color = safe_at(filament_color, i, default_color);
tray.has_filament = filament_exist[i];
if (tray.has_filament) { auto* bundle = GUI::wxGetApp().preset_bundle;
tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), // Try to find a matching preset for this filament based on vendor, type and color.
safe_at(filament_sub_type, i, empty_str)); // If not found, default to traditional search by type only or generic type mapping.
tray.tray_color = safe_at(filament_color, i, default_color); if (bundle) {
std::string vendor = safe_at(filament_vendor, i, empty_str);
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type,
tray.tray_color);
auto* bundle = GUI::wxGetApp().preset_bundle; if (!filament_id.empty()) {
// Try to find a matching preset for this filament based on vendor, type and color. tray.tray_info_idx = filament_id;
// If not found, default to traditional search by type only or generic type mapping. BOOST_LOG_TRIVIAL(warning)
if (bundle) { << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id;
std::string vendor = safe_at(filament_vendor, i, empty_str); } else {
std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type);
tray.tray_color); }
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type);
}
if (!filament_id.empty()) { // Extract NFC temperature data if available
tray.tray_info_idx = filament_id; if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) {
BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " auto& nfc_slot = nfc_info[i];
<< filament_id; std::string vendor = "NONE";
} else { if (auto vendor_it = nfc_slot.find("VENDOR"); vendor_it != nfc_slot.end() && vendor_it->is_string()) {
tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); vendor = vendor_it->get<std::string>();
}
if (vendor != "NONE" && !vendor.empty()) {
tray.bed_temp = read_int_or(nfc_slot, "BED_TEMP", 0);
tray.nozzle_temp = read_int_or(nfc_slot, "FIRST_LAYER_TEMP", 0);
}
}
} }
} else {
tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); trays.emplace_back(std::move(tray));
} }
// Extract NFC temperature data if available build_ams_payload(1, slot_count - 1, trays);
if (nfc_info.is_array() && i < static_cast<int>(nfc_info.size()) && nfc_info[i].is_object()) { } catch (const std::exception& e) {
auto& nfc_slot = nfc_info[i]; // why: an exception escaping a detached thread is std::terminate, and firmware
std::string vendor = nfc_slot.value("VENDOR", "NONE"); // JSON is untrusted; mirror run_command_worker and swallow it here.
if (vendor != "NONE" && !vendor.empty()) { BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception: " << e.what();
tray.bed_temp = nfc_slot.value("BED_TEMP", 0); } catch (...) {
tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); BOOST_LOG_TRIVIAL(error) << "SnapmakerPrinterAgent::fetch_filament_info: unhandled exception";
}
}
} }
}).detach();
trays.emplace_back(std::move(tray));
}
build_ams_payload(1, slot_count - 1, trays);
return true; return true;
} }
std::string SnapmakerPrinterAgent::get_camera_url() const
{
return get_connection_settings().base_url + "/server/files/camera/monitor.jpg";
}
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
return FilamentSyncMode::subscription;
return FilamentSyncMode::pull;
}
} // namespace Slic3r } // namespace Slic3r
+12 -1
View File
@@ -3,6 +3,8 @@
#include "IPrinterAgent.hpp" #include "IPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp"
#include <atomic>
#include <cstdint>
#include <string> #include <string>
namespace Slic3r { namespace Slic3r {
@@ -11,16 +13,25 @@ class SnapmakerPrinterAgent final : public MoonrakerPrinterAgent
{ {
public: public:
explicit SnapmakerPrinterAgent(std::string log_dir); explicit SnapmakerPrinterAgent(std::string log_dir);
~SnapmakerPrinterAgent() override = default; ~SnapmakerPrinterAgent() override { shutdown(); }
static AgentInfo get_agent_info_static(); static AgentInfo get_agent_info_static();
AgentInfo get_agent_info() override { return 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, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override;
FilamentSyncMode get_filament_sync_mode() const override;
int command_start_camera(std::string dev_id) override;
CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; }
std::string get_camera_url() const override;
private: private:
// Combine filament_type + filament_sub_type into a unified type string // Combine filament_type + filament_sub_type into a unified type string
static std::string combine_filament_type(const std::string& type, const std::string& sub_type); static std::string combine_filament_type(const std::string& type, const std::string& sub_type);
void start_camera_monitor();
void on_status_loop_tick(const std::string& dev_id) override;
std::atomic<int64_t> m_camera_last_fire_ms{0};
}; };
} // namespace Slic3r } // namespace Slic3r
+7 -4
View File
@@ -336,6 +336,7 @@ struct PrintParams {
bool try_emmc_print; bool try_emmc_print;
std::string svc_context; std::string svc_context;
std::string slicer_uid; std::string slicer_uid;
std::string queue_plate_id;
}; };
struct TaskQueryParams struct TaskQueryParams
@@ -412,7 +413,7 @@ enum class NetworkAbi {
Unsupported, // no generation in this build can call it - never dispatch through it Unsupported, // no generation in this build can call it - never dispatch through it
Legacy, // 01.10.01: PrintParams_Legacy; send_message/send_message_to_printer take no flag Legacy, // 01.10.01: PrintParams_Legacy; send_message/send_message_to_printer take no flag
V0203, // 02.03.00: PrintParams_0203; bind takes no dev_model V0203, // 02.03.00: PrintParams_0203; bind takes no dev_model
Current, // 02.08.01: the layouts and signatures this build declares directly Current, // 02.08.04: the layouts and signatures this build declares directly
}; };
struct NetworkLibraryVersion { struct NetworkLibraryVersion {
@@ -425,10 +426,12 @@ struct NetworkLibraryVersion {
}; };
// Every row names the generation that can call it, so a series can never be offered without a // Every row names the generation that can call it, so a series can never be offered without a
// host-side ABI for it. Series with no generation - 02.01.01, 02.00.02 and older - must stay out; // host-side ABI for it. Series with no generation - 02.08.01 (whose PrintParams lacks the
// is_supported_network_version() is the gate that keeps them from loading. // queue_plate_id that 02.08.02 appended, and whose malformed bind table macOS 27 refuses to
// load), 02.01.01, 02.00.02 and older - must stay out; is_supported_network_version() is the
// gate that keeps them from loading.
static const NetworkLibraryVersion AVAILABLE_NETWORK_VERSIONS[] = { static const NetworkLibraryVersion AVAILABLE_NETWORK_VERSIONS[] = {
{"02.08.01", "02.08.01", nullptr, true, nullptr, NetworkAbi::Current}, {"02.08.04", "02.08.04", nullptr, true, nullptr, NetworkAbi::Current},
{"02.03.00", "02.03.00", nullptr, false, {"02.03.00", "02.03.00", nullptr, false,
"An older plug-in series. Features that need newer plug-in support, such as print-failure " "An older plug-in series. Features that need newer plug-in support, such as print-failure "
"snapshots in the device error dialog, are unavailable.", NetworkAbi::V0203}, "snapshots in the device error dialog, are unavailable.", NetworkAbi::V0203},
-3
View File
@@ -7,9 +7,6 @@
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
using namespace std;
class json_diff class json_diff
{ {
private: private:
+6 -25
View File
@@ -346,20 +346,8 @@ std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
// must never be able to reach a secret, a certificate, or a configuration file just because // must never be able to reach a secret, a certificate, or a configuration file just because
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at // it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is // resources_dir()/cert/..., which would become reachable the moment resources_dir() is
// granted as a read-only allowed root). Match as whole path components, not substrings, so // granted as a read-only allowed root).
// imports such as numpy/__config__.py and stdlib configparser.py remain usable. return {"secret", "cert", "conf"};
return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"};
}
static bool has_denied_config_extension(std::string name)
{
const size_t stream_pos = name.find(':');
if (stream_pos != std::string::npos)
name.erase(stream_pos);
const boost::filesystem::path path(name);
const std::string extension = path.extension().string();
return extension == ".conf" || extension == ".ini";
} }
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
@@ -384,7 +372,7 @@ bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& c
continue; continue;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
for (const auto& keyword : m_denied_path_keywords) { for (const auto& keyword : m_denied_path_keywords) {
if (name == keyword || (keyword == "conf" && has_denied_config_extension(name))) if (name.find(keyword) != std::string::npos)
return true; return true;
} }
} }
@@ -808,8 +796,7 @@ bool persist_permission(const std::string& plugin_key,
int report_denied(PluginAuditManager& mgr, int report_denied(PluginAuditManager& mgr,
const std::string& event_name, const std::string& event_name,
const AuditDecision& decision, const AuditDecision& decision)
const std::string& target = {})
{ {
AuditViolation violation; AuditViolation violation;
violation.plugin_key = mgr.current_plugin(); violation.plugin_key = mgr.current_plugin();
@@ -817,13 +804,7 @@ int report_denied(PluginAuditManager& mgr,
violation.reason = decision.reason; violation.reason = decision.reason;
mgr.report_violation(violation); mgr.report_violation(violation);
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission"; PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
if (!decision.reason.empty())
message += ": " + decision.reason;
if (!target.empty())
message += ": " + target;
PyErr_SetString(PyExc_PermissionError, message.c_str());
return -1; return -1;
} }
@@ -935,7 +916,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
if (fs_category) { if (fs_category) {
for (const auto& target : targets) { for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target))) if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}, target); return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
} }
} }
+11 -8
View File
@@ -4,6 +4,7 @@
// Via pybind11 so this file requests the same python3xx.lib as everything else. // Via pybind11 so this file requests the same python3xx.lib as everything else.
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <pybind11/conduit/wrap_include_python_h.h> #include <pybind11/conduit/wrap_include_python_h.h>
#include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -102,20 +103,22 @@ public:
bool is_denied_filename(const boost::filesystem::path& candidate) const; bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- denied-path-keyword registry --- // --- denied-path-keyword registry ---
// Keywords that categorically deny a path if ANY component matches one case-insensitively -- // Keywords that categorically deny a path if ANY of its components (directory or file
// e.g. a "secrets" subfolder, a "certificates" folder, a "conf"/"config" directory, or a // name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
// .conf/.ini file anywhere the plugin can otherwise reach, including inside an allowed root. // subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
// This is broader than the exact-name is_denied_filename registry, but it is not a substring // otherwise reach, including inside an allowed root. This is intentionally broader and
// match: importable modules such as numpy/__config__.py, configparser.py, sysconfig.py, or // fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
// user folders such as "Conference" and "Concert" are unrelated names and must stay promptable. // out whole classes of sensitive paths (secrets, certificates, config) rather than name
// specific known files, at the cost of over-blocking an unrelated name that happens to
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
void add_denied_path_keyword(const std::string& keyword); void add_denied_path_keyword(const std::string& keyword);
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact // The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
// same set without a live interpreter. // same set without a live interpreter.
static std::vector<std::string> default_denied_path_keywords(); static std::vector<std::string> default_denied_path_keywords();
// True when any component of candidate's (canonicalized) path matches a registered keyword, // True when any component of candidate's (canonicalized) path contains a registered
// case-insensitively. A registered "conf" keyword also blocks .conf/.ini file components. // keyword, case-insensitively.
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const; bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for // is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
+104
View File
@@ -538,6 +538,110 @@ SCENARIO("update_diff_values_to_child_config keeps a child's values on variants
} }
} }
SCENARIO("update_non_diff_values_to_base_config keeps a project's changed values on variants it does not list",
"[Config][Variant]") {
std::set<std::string> no_keys;
auto variants = [](std::initializer_list<std::string> names) { return new Slic3r::ConfigOptionStrings(names); };
GIVEN("A filament base with three variants") {
Slic3r::DynamicPrintConfig base;
base.set_key_value("filament_extruder_variant",
variants({"Direct Drive Standard", "Bowden Standard", "Direct Drive High Flow"}));
base.set_deserialize_strict("nozzle_temperature", "220,220,220");
WHEN("the project was saved when the base had only its first variant") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("filament_extruder_variant", variants({"Direct Drive Standard"}));
project.set_deserialize_strict("nozzle_temperature", "199");
AND_WHEN("the project lists the value as changed") {
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the project's value applies to every variant") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,199,199");
}
}
AND_WHEN("the project does not list the value as changed") {
project.update_non_diff_values_to_base_config(base, project.keys(), {}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the base's values replace it") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "220,220,220");
}
}
}
WHEN("the project lists every variant, in another order") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("filament_extruder_variant",
variants({"Bowden Standard", "Direct Drive High Flow", "Direct Drive Standard"}));
project.set_deserialize_strict("nozzle_temperature", "190,205,199");
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("each variant keeps its own value") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,190,205");
}
}
WHEN("the project lists no variants") {
Slic3r::DynamicPrintConfig project;
project.set_deserialize_strict("nozzle_temperature", "199");
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the project's value applies to every variant") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,199,199");
}
}
}
GIVEN("A two-extruder printer base with two variants per extruder") {
Slic3r::DynamicPrintConfig base;
base.set_key_value("printer_extruder_variant",
variants({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}));
base.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1, 2, 2}));
base.set_deserialize_strict("retraction_length", "0.8,0.8,0.8,0.8");
WHEN("the project lists only the Standard variant of each extruder") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("printer_extruder_variant", variants({"Direct Drive Standard", "Direct Drive Standard"}));
project.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 2}));
project.set_deserialize_strict("retraction_length", "1.1,2.2");
project.update_non_diff_values_to_base_config(base, project.keys(), {"retraction_length"}, "printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("each extruder's High Flow variant takes that extruder's value") {
REQUIRE(project.opt_serialize("retraction_length") == "1.1,1.1,2.2,2.2");
}
}
WHEN("the project lists only the Standard variant of each extruder, and the variant lists as changed") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("printer_extruder_variant", variants({"Direct Drive Standard", "Direct Drive Standard"}));
project.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 2}));
project.set_deserialize_strict("machine_max_speed_x", "300,100,400,150");
base.set_deserialize_strict("machine_max_speed_x", "500,200,500,200,500,200,500,200");
project.update_non_diff_values_to_base_config(base, project.keys(),
{"machine_max_speed_x", "printer_extruder_id", "printer_extruder_variant"},
"printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("the variant lists are the base's") {
REQUIRE(project.opt_serialize("printer_extruder_variant") == base.opt_serialize("printer_extruder_variant"));
REQUIRE(project.opt_serialize("printer_extruder_id") == "1,1,2,2");
}
THEN("each extruder's High Flow variant takes that extruder's pair of limits") {
REQUIRE(project.opt_serialize("machine_max_speed_x") == "300,100,300,100,400,150,400,150");
}
}
WHEN("the project lists no variants") {
Slic3r::DynamicPrintConfig project;
project.set_deserialize_strict("retraction_length", "1.1");
project.update_non_diff_values_to_base_config(base, project.keys(), {"retraction_length"}, "printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("only the first extruder's variants take the project's value") {
REQUIRE(project.opt_serialize("retraction_length") == "1.1,1.1,0.8,0.8");
}
}
}
}
// SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") { // SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") {
// WHEN("DynamicPrintConfig is serialized and deserialized") { // WHEN("DynamicPrintConfig is serialized and deserialized") {
// auto now = std::chrono::high_resolution_clock::now(); // auto now = std::chrono::high_resolution_clock::now();
@@ -799,3 +799,30 @@ TEST_CASE("A per-variant filament option read with a single value gives it to ev
config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable); config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(config.option<ConfigOptionFloats>("pressure_advance")->values == std::vector<double>({0.021, 0.021, 0.021})); REQUIRE(config.option<ConfigOptionFloats>("pressure_advance")->values == std::vector<double>({0.021, 0.021, 0.021}));
} }
// The device drying options hold several values per filament, as many as each filament preset gives.
TEST_CASE("The device drying options are rebuilt as each filament's values in slot order", "[Config]")
{
DynamicPrintConfig two_values, one_value, no_value;
two_values.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations", true)->values = {"1", "0"};
two_values.option<ConfigOptionFloats>("filament_dev_ams_drying_temperature", true)->values = {45., 45., 55., 55.};
one_value.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations", true)->values = {"1"};
one_value.option<ConfigOptionFloats>("filament_dev_ams_drying_temperature", true)->values = {65., 65., 75., 75.};
// values a project stored for three other filaments
DynamicPrintConfig config;
config.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations", true)->values = {"0", "0", "0"};
config.option<ConfigOptionFloats>("filament_dev_chamber_drying_time", true)->values = {12., 8., 12.};
set_filament_dev_options(config, {&two_values, &one_value, &two_values});
REQUIRE(config.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations")->values ==
std::vector<std::string>({"1", "0", "1", "1", "0"}));
REQUIRE(config.option<ConfigOptionFloats>("filament_dev_ams_drying_temperature")->values ==
std::vector<double>({45., 45., 55., 55., 65., 65., 75., 75., 45., 45., 55., 55.}));
// an option no filament defines keeps the stored values
REQUIRE(config.option<ConfigOptionFloats>("filament_dev_chamber_drying_time")->values == std::vector<double>({12., 8., 12.}));
// a filament without the option takes the option's default
set_filament_dev_options(config, {&two_values, &no_value});
REQUIRE(config.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations")->values == std::vector<std::string>({"1", "0", ""}));
}
@@ -6228,8 +6228,9 @@ TEST_CASE("A per-variant project value maps onto its base preset's variant layou
base_finder(&base, calls)); base_finder(&base, calls));
CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values == CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"}); std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"});
// The listed key keeps the project's Standard value and takes High Flow from the base. // The listed key keeps the project's Standard value, and High Flow, which the project does not
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 300.}); // list, takes it too, as a user preset's value does.
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 100.});
check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.}); check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.});
} }
+1
View File
@@ -25,6 +25,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_capabilities_in_use.cpp test_plugin_capabilities_in_use.cpp
test_plugin_status.cpp test_plugin_status.cpp
test_printer_agent.cpp test_printer_agent.cpp
test_qidi_printer_agent.cpp
test_plugin_install.cpp test_plugin_install.cpp
test_plugin_lifecycle.cpp test_plugin_lifecycle.cpp
test_plugin_printer_agent.cpp test_plugin_printer_agent.cpp
+36 -29
View File
@@ -78,31 +78,34 @@ TEST_CASE("Series and managed classification", "[NetworkVersions]")
TEST_CASE_METHOD(PluginFolderFixture, "Managed builds fold into the series; customs are surfaced", "[NetworkVersions]") TEST_CASE_METHOD(PluginFolderFixture, "Managed builds fold into the series; customs are surfaced", "[NetworkVersions]")
{ {
add_plugin("02.08.01.55"); // managed, same series -> folded into the 02.08.01 row add_plugin("02.08.04.60"); // managed, same series -> folded into the 02.08.04 row
add_plugin("02.09.00.10"); // managed, unknown series -> not listed add_plugin("02.09.00.10"); // managed, unknown series -> not listed
add_plugin("02.08.01.55"); // managed, series this build no longer has an ABI for -> not listed
add_plugin("02.03.00.62"); // managed, older whitelisted series -> folded into 02.03.00 add_plugin("02.03.00.62"); // managed, older whitelisted series -> folded into 02.03.00
add_plugin("02.01.01.52"); // managed, series with no ABI in this build -> not listed add_plugin("02.01.01.52"); // managed, series with no ABI in this build -> not listed
add_plugin("02.08.01_custom"); // custom, whitelisted series -> listed under it add_plugin("02.08.04_custom"); // custom, whitelisted series -> listed under it
add_plugin("02.08.01.52-dev"); // custom (dash-suffixed), whitelisted series -> listed add_plugin("02.08.04.52-dev"); // custom (dash-suffixed), whitelisted series -> listed
auto versions = get_all_available_versions(); auto versions = get_all_available_versions();
// The specific managed build never gets its own row - the series represents it. // The specific managed build never gets its own row - the series represents it.
REQUIRE(count_version(versions, "02.08.01.55") == 0); REQUIRE(count_version(versions, "02.08.04.60") == 0);
REQUIRE(count_version(versions, "02.08.01") == 1); REQUIRE(count_version(versions, "02.08.04") == 1);
REQUIRE(count_version(versions, "02.09.00.10") == 0); REQUIRE(count_version(versions, "02.09.00.10") == 0);
REQUIRE(count_version(versions, "02.08.01.55") == 0);
REQUIRE(count_version(versions, "02.08.01") == 0);
REQUIRE(count_version(versions, "02.03.00.62") == 0); REQUIRE(count_version(versions, "02.03.00.62") == 0);
REQUIRE(count_version(versions, "02.03.00") == 1); REQUIRE(count_version(versions, "02.03.00") == 1);
REQUIRE(count_version(versions, "02.01.01.52") == 0); REQUIRE(count_version(versions, "02.01.01.52") == 0);
// Custom-named builds are distinct files kept under their own name. // Custom-named builds are distinct files kept under their own name.
REQUIRE(count_version(versions, "02.08.01_custom") == 1); REQUIRE(count_version(versions, "02.08.04_custom") == 1);
REQUIRE(count_version(versions, "02.08.01.52-dev") == 1); REQUIRE(count_version(versions, "02.08.04.52-dev") == 1);
// Newest series first, its customs nested under it (suffix sort: "" < ".52-dev" < "_custom"), // Newest series first, its customs nested under it (suffix sort: "" < ".52-dev" < "_custom"),
// then older series, legacy last. // then older series, legacy last.
REQUIRE(versions[0].version == "02.08.01"); REQUIRE(versions[0].version == "02.08.04");
REQUIRE(versions[1].version == "02.08.01.52-dev"); REQUIRE(versions[1].version == "02.08.04.52-dev");
REQUIRE(versions[2].version == "02.08.01_custom"); REQUIRE(versions[2].version == "02.08.04_custom");
REQUIRE(versions[3].version == "02.03.00"); REQUIRE(versions[3].version == "02.03.00");
REQUIRE(versions.back().version == BAMBU_NETWORK_AGENT_VERSION_LEGACY); REQUIRE(versions.back().version == BAMBU_NETWORK_AGENT_VERSION_LEGACY);
@@ -111,9 +114,9 @@ TEST_CASE_METHOD(PluginFolderFixture, "Managed builds fold into the series; cust
REQUIRE_FALSE(versions[3].is_latest); REQUIRE_FALSE(versions[3].is_latest);
// Customs sort/render nested under their series (non-empty suffix, base = the series). // Customs sort/render nested under their series (non-empty suffix, base = the series).
REQUIRE(versions[1].base_version == "02.08.01"); REQUIRE(versions[1].base_version == "02.08.04");
REQUIRE_FALSE(versions[1].suffix.empty()); REQUIRE_FALSE(versions[1].suffix.empty());
REQUIRE(versions[2].base_version == "02.08.01"); REQUIRE(versions[2].base_version == "02.08.04");
REQUIRE_FALSE(versions[2].suffix.empty()); REQUIRE_FALSE(versions[2].suffix.empty());
// "(Latest)" is the series row, never a nested custom build. // "(Latest)" is the series row, never a nested custom build.
@@ -123,20 +126,20 @@ TEST_CASE_METHOD(PluginFolderFixture, "Managed builds fold into the series; cust
REQUIRE_FALSE(versions[2].is_latest); REQUIRE_FALSE(versions[2].is_latest);
// The stored default that drives download and update-check decisions is now the series. // The stored default that drives download and update-check decisions is now the series.
REQUIRE(std::string(get_latest_network_version()) == "02.08.01"); REQUIRE(std::string(get_latest_network_version()) == "02.08.04");
} }
TEST_CASE_METHOD(PluginFolderFixture, "Only the loaded series is marked installed", "[NetworkVersions]") TEST_CASE_METHOD(PluginFolderFixture, "Only the loaded series is marked installed", "[NetworkVersions]")
{ {
add_plugin("02.08.01.55"); add_plugin("02.08.04.60");
add_plugin("02.08.01_custom"); add_plugin("02.08.04_custom");
// The loaded plug-in reports its full build (02.08.01.55); the series row is what gets marked. // The loaded plug-in reports its full build (02.08.04.60); the series row is what gets marked.
{ {
auto versions = get_all_available_versions("02.08.01.55"); auto versions = get_all_available_versions("02.08.04.60");
int marked = 0; int marked = 0;
for (const auto& info : versions) for (const auto& info : versions)
if (info.is_loaded) { ++marked; REQUIRE(info.version == "02.08.01"); } if (info.is_loaded) { ++marked; REQUIRE(info.version == "02.08.04"); }
REQUIRE(marked == 1); REQUIRE(marked == 1);
} }
@@ -152,10 +155,10 @@ TEST_CASE_METHOD(PluginFolderFixture, "Only the loaded series is marked installe
// A loaded custom build matches its own row, never the bare series. // A loaded custom build matches its own row, never the bare series.
{ {
auto versions = get_all_available_versions("02.08.01_custom"); auto versions = get_all_available_versions("02.08.04_custom");
int marked = 0; int marked = 0;
for (const auto& info : versions) for (const auto& info : versions)
if (info.is_loaded) { ++marked; REQUIRE(info.version == "02.08.01_custom"); } if (info.is_loaded) { ++marked; REQUIRE(info.version == "02.08.04_custom"); }
REQUIRE(marked == 1); REQUIRE(marked == 1);
} }
@@ -167,11 +170,11 @@ TEST_CASE_METHOD(PluginFolderFixture, "Only the loaded series is marked installe
TEST_CASE("Only whitelisted series pass the load gate", "[NetworkVersions]") TEST_CASE("Only whitelisted series pass the load gate", "[NetworkVersions]")
{ {
// Each whitelisted series, its builds, and custom-named builds of that series. // Each whitelisted series, its builds, and custom-named builds of that series.
REQUIRE(is_supported_network_version("02.08.01")); REQUIRE(is_supported_network_version("02.08.04"));
REQUIRE(is_supported_network_version("02.08.01.52")); REQUIRE(is_supported_network_version("02.08.04.52"));
REQUIRE(is_supported_network_version("02.08.01.55")); REQUIRE(is_supported_network_version("02.08.04.60"));
REQUIRE(is_supported_network_version("02.08.01_custom")); REQUIRE(is_supported_network_version("02.08.04_custom"));
REQUIRE(is_supported_network_version("02.08.01.52-dev")); REQUIRE(is_supported_network_version("02.08.04.52-dev"));
REQUIRE(is_supported_network_version("02.03.00")); REQUIRE(is_supported_network_version("02.03.00"));
REQUIRE(is_supported_network_version("02.03.00.62")); REQUIRE(is_supported_network_version("02.03.00.62"));
REQUIRE(is_supported_network_version("02.03.00.70")); REQUIRE(is_supported_network_version("02.03.00.70"));
@@ -179,6 +182,9 @@ TEST_CASE("Only whitelisted series pass the load gate", "[NetworkVersions]")
REQUIRE(is_supported_network_version(BAMBU_NETWORK_AGENT_VERSION_LEGACY)); REQUIRE(is_supported_network_version(BAMBU_NETWORK_AGENT_VERSION_LEGACY));
// Series whitelisted by previous Orca releases that no generation here can call. // Series whitelisted by previous Orca releases that no generation here can call.
REQUIRE_FALSE(is_supported_network_version("02.08.01"));
REQUIRE_FALSE(is_supported_network_version("02.08.01.55"));
REQUIRE_FALSE(is_supported_network_version("02.08.01_custom"));
REQUIRE_FALSE(is_supported_network_version("02.01.01.52")); REQUIRE_FALSE(is_supported_network_version("02.01.01.52"));
REQUIRE_FALSE(is_supported_network_version("02.00.02.50")); REQUIRE_FALSE(is_supported_network_version("02.00.02.50"));
@@ -198,9 +204,9 @@ TEST_CASE("Each version resolves to the ABI generation that can call it", "[Netw
{ {
// The generation is keyed on the series, so every build of a series - including the // The generation is keyed on the series, so every build of a series - including the
// custom-named ones - resolves to the same one. // custom-named ones - resolves to the same one.
CHECK(network_plugin_abi("02.08.01") == NetworkAbi::Current); CHECK(network_plugin_abi("02.08.04") == NetworkAbi::Current);
CHECK(network_plugin_abi("02.08.01.55") == NetworkAbi::Current); CHECK(network_plugin_abi("02.08.04.60") == NetworkAbi::Current);
CHECK(network_plugin_abi("02.08.01.52-dev") == NetworkAbi::Current); CHECK(network_plugin_abi("02.08.04.52-dev") == NetworkAbi::Current);
CHECK(network_plugin_abi("02.03.00") == NetworkAbi::V0203); CHECK(network_plugin_abi("02.03.00") == NetworkAbi::V0203);
CHECK(network_plugin_abi("02.03.00.62") == NetworkAbi::V0203); CHECK(network_plugin_abi("02.03.00.62") == NetworkAbi::V0203);
CHECK(network_plugin_abi("02.03.00_custom") == NetworkAbi::V0203); CHECK(network_plugin_abi("02.03.00_custom") == NetworkAbi::V0203);
@@ -208,6 +214,7 @@ TEST_CASE("Each version resolves to the ABI generation that can call it", "[Netw
// Anything the load gate rejects must dispatch through nothing at all, rather than // Anything the load gate rejects must dispatch through nothing at all, rather than
// defaulting to a layout it does not share. // defaulting to a layout it does not share.
CHECK(network_plugin_abi("02.08.01.55") == NetworkAbi::Unsupported);
CHECK(network_plugin_abi("02.01.01.52") == NetworkAbi::Unsupported); CHECK(network_plugin_abi("02.01.01.52") == NetworkAbi::Unsupported);
CHECK(network_plugin_abi("02.00.02.50") == NetworkAbi::Unsupported); CHECK(network_plugin_abi("02.00.02.50") == NetworkAbi::Unsupported);
CHECK(network_plugin_abi("02.09.00.10") == NetworkAbi::Unsupported); CHECK(network_plugin_abi("02.09.00.10") == NetworkAbi::Unsupported);
@@ -234,7 +241,7 @@ TEST_CASE_METHOD(PluginFolderFixture, "Legacy series never adopts discovered bui
// With nothing else on disk, the series holds "(Latest)" even though its library is // With nothing else on disk, the series holds "(Latest)" even though its library is
// not installed. // not installed.
for (const auto& info : versions) { for (const auto& info : versions) {
if (info.version == "02.08.01") { if (info.version == "02.08.04") {
REQUIRE(info.is_latest); REQUIRE(info.is_latest);
REQUIRE_FALSE(info.is_loaded); REQUIRE_FALSE(info.is_loaded);
} }
+3 -12
View File
@@ -222,19 +222,18 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword",
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem"))); CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
} }
SECTION("a 'conf'/'config' directory or config file component is denied") SECTION("a 'conf'/'config' directory or file component is denied")
{ {
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.ini")));
} }
SECTION("matching is case-insensitive") SECTION("matching is case-insensitive")
{ {
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/Certificates/ca.pem"))); CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/PLUGIN.CONF"))); CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
} }
SECTION("matching is not limited to the base name -- any ancestor component counts") SECTION("matching is not limited to the base name -- any ancestor component counts")
@@ -246,14 +245,6 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword",
{ {
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode"))); CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json"))); CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/__config__.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/_core/_ufunc_config.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/configparser.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/sysconfig.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/logging/config.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/certifi/cacert.pem")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Conference/output.txt")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Concert/output.txt")));
} }
SECTION("an empty path is not denied") SECTION("an empty path is not denied")
+384
View File
@@ -1,18 +1,29 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <functional>
#include <slic3r/Utils/BBLPrinterAgent.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/MoonrakerPrinterAgent.hpp>
#include <memory> #include <memory>
#include <slic3r/Utils/NetworkAgentFactory.hpp> #include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
#include <catch2/catch_message.hpp> #include <catch2/catch_message.hpp>
#include "catch2/catch_approx.hpp"
#include "python_test_support.hpp" #include "python_test_support.hpp"
#include <pybind11/embed.h> #include <pybind11/embed.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <atomic>
#include <chrono>
#include <future>
#include <slic3r/Utils/bambu_networking.hpp>
#include <string> #include <string>
#include <thread>
#include <pybind11/cast.h> #include <pybind11/cast.h>
#include <utility>
namespace Slic3r { class ICloudServiceAgent; } namespace Slic3r { class ICloudServiceAgent; }
namespace Slic3r { class IPrinterAgent; } namespace Slic3r { class IPrinterAgent; }
@@ -20,6 +31,379 @@ namespace Slic3r { class IPrinterAgent; }
using namespace Slic3r; using namespace Slic3r;
namespace py = pybind11; namespace py = pybind11;
namespace {
// Releases a promise on scope exit, so a throwing REQUIRE cannot leave a parked detached
// thread (and any destructor that joins it) blocked forever.
class ScopedPromiseRelease
{
public:
explicit ScopedPromiseRelease(std::shared_ptr<std::promise<void>> p) : m_p(std::move(p)) {}
~ScopedPromiseRelease()
{
if (m_p) {
try {
m_p->set_value();
} catch (...) {
// promise already satisfied
}
}
}
ScopedPromiseRelease(const ScopedPromiseRelease&) = delete;
ScopedPromiseRelease& operator=(const ScopedPromiseRelease&) = delete;
private:
std::shared_ptr<std::promise<void>> m_p;
};
} // namespace
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: 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, 0, 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"));
}
TEST_CASE("Moonraker webcam selection skips disabled webcams and prefers the first enabled one",
"[unit][moonraker]")
{
const auto response = nlohmann::json::parse(R"({
"result": { "webcams": [
{ "name": "disabled", "enabled": false, "stream_url": "http://192.168.1.9:8080/stream" },
{ "name": "enabled", "enabled": true, "stream_url": "http://192.168.1.9:8080/stream" }
]}
})");
MoonrakerWebcamSelection selection;
REQUIRE(moonraker_parse_webcam_list(response, "http://192.168.1.9:7125", selection));
CHECK(selection.name == "enabled");
CHECK(selection.url == "http://192.168.1.9:8080/stream");
CHECK(selection.mode == CameraStreamMode::http);
CHECK(selection.error.empty());
}
TEST_CASE("Moonraker webcam selection resolves relative URLs, maps rtsp, and rejects other schemes",
"[unit][moonraker]")
{
const auto relative = nlohmann::json::parse(R"({
"result": { "webcams": [ { "name": "cam", "snapshot_url": "/webcam/?action=snapshot" } ] }
})");
MoonrakerWebcamSelection rel;
REQUIRE(moonraker_parse_webcam_list(relative, "http://192.168.1.9:7125", rel));
// Relative URLs use the printer web root, without the Moonraker API port.
CHECK(rel.url == "http://192.168.1.9/webcam/?action=snapshot");
CHECK(rel.mode == CameraStreamMode::http_snapshot);
const auto rtsp = nlohmann::json::parse(R"({
"result": { "webcams": [ { "name": "cam", "stream_url": "rtsp://192.168.1.9:554/live" } ] }
})");
MoonrakerWebcamSelection rt;
REQUIRE(moonraker_parse_webcam_list(rtsp, "http://192.168.1.9:7125", rt));
CHECK(rt.mode == CameraStreamMode::rtsp);
const auto unsupported = nlohmann::json::parse(R"({
"result": { "webcams": [ { "name": "cam", "stream_url": "weird://host/x" } ] }
})");
MoonrakerWebcamSelection bad;
CHECK_FALSE(moonraker_parse_webcam_list(unsupported, "http://192.168.1.9:7125", bad));
CHECK(bad.error == "Unsupported webcam URL");
}
TEST_CASE("Moonraker webcam selection reports no webcam and malformed structure", "[unit][moonraker]")
{
const auto empty = nlohmann::json::parse(R"({ "result": { "webcams": [] } })");
MoonrakerWebcamSelection none;
CHECK_FALSE(moonraker_parse_webcam_list(empty, "http://host:7125", none));
CHECK(none.error == "No enabled webcam");
const auto disabled_only = nlohmann::json::parse(R"({
"result": { "webcams": [ { "name": "disabled", "enabled": false, "stream_url": "http://host/stream" } ] }
})");
MoonrakerWebcamSelection off;
CHECK_FALSE(moonraker_parse_webcam_list(disabled_only, "http://host:7125", off));
const auto malformed = nlohmann::json::parse(R"({ "result": { "nope": 1 } })");
MoonrakerWebcamSelection shape;
CHECK_FALSE(moonraker_parse_webcam_list(malformed, "http://host:7125", shape));
CHECK(shape.error == "Unexpected JSON structure");
}
// ===========================================================================
// 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);
}
// why: IPrinterAgent::fetch_filament_info is the single virtual hook derived agents override
// (MoonrakerPrinterAgent's own override is synchronous, but QidiPrinterAgent's override is
// fire-and-forget: it spawns a detached thread and returns immediately). QidiPrinterAgent is
// `final`, so this probes the same contract with a controllable double instead.
TEST_CASE("unit: a fire-and-forget override of fetch_filament_info is not waited on by the caller",
"[unit][moonraker]")
{
class RecordingAgent : public Slic3r::MoonrakerPrinterAgent
{
public:
explicit RecordingAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
// Shared so the detached proxy fetch never touches `this`: a throwing REQUIRE
// then cannot leave it dereferencing a destroyed agent.
std::shared_ptr<std::atomic<bool>> invoked{std::make_shared<std::atomic<bool>>(false)};
std::shared_ptr<std::promise<void>> release_gate{std::make_shared<std::promise<void>>()};
std::shared_ptr<std::promise<void>> done_promise{std::make_shared<std::promise<void>>()};
bool fetch_filament_info(std::string /*dev_id*/, FilamentSyncMode /*sync_mode*/ = FilamentSyncMode::pull) override
{
auto invoked_p = invoked;
auto release_gate_p = release_gate;
auto done_promise_p = done_promise;
std::thread([invoked_p, release_gate_p, done_promise_p]() {
invoked_p->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_p->get_future().wait();
done_promise_p->set_value();
}).detach();
return true;
}
};
auto agent = std::make_shared<RecordingAgent>(std::string{});
auto done_future = agent->done_promise->get_future();
ScopedPromiseRelease release_gate_guard{agent->release_gate};
bool immediate_result = agent->fetch_filament_info("test-dev");
// fetch_filament_info must return before its background work 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);
}
namespace {
// Globals so a parked proxy fetch thread never dereferences a freed agent.
std::atomic<int> g_deferred_fetch_running{0};
std::atomic<bool> g_deferred_destroy_returned{false};
// Releases the given gates, then joins on scope exit: so a throwing REQUIRE cannot leave
// the thread blocked (deadlocking the join) or let it std::terminate.
class ScopedJoiner
{
public:
ScopedJoiner(std::thread& t, std::shared_ptr<std::promise<void>> gate1, std::shared_ptr<std::promise<void>> gate2)
: m_thread(t), m_gates{std::move(gate1), std::move(gate2)}
{}
~ScopedJoiner()
{
for (auto& gate : m_gates) {
if (gate) {
try {
gate->set_value();
} catch (...) {
// promise already satisfied
}
}
}
if (m_thread.joinable()) m_thread.join();
}
ScopedJoiner(const ScopedJoiner&) = delete;
ScopedJoiner& operator=(const ScopedJoiner&) = delete;
private:
std::thread& m_thread;
std::shared_ptr<std::promise<void>> m_gates[2];
};
// A fetch that parks before touching the in-flight counter, so teardown's wait can
// observe zero first.
class DeferredFetchAgent : public MoonrakerPrinterAgent
{
public:
explicit DeferredFetchAgent(std::string log_dir) : MoonrakerPrinterAgent(std::move(log_dir)) {}
// Shared so a parked proxy fetch can never outlive the stack that owns it.
std::shared_ptr<std::promise<void>> entered{std::make_shared<std::promise<void>>()};
std::shared_ptr<std::promise<void>> allow_fetch{std::make_shared<std::promise<void>>()};
std::shared_ptr<std::promise<void>> allow_finish{std::make_shared<std::promise<void>>()};
std::shared_ptr<std::promise<void>> running{std::make_shared<std::promise<void>>()};
// Runs the callable on the command worker, which teardown joins.
void post(std::function<void()> fn) { enqueue_command(std::move(fn)); }
bool fetch_filament_info(std::string /*dev_id*/, FilamentSyncMode /*sync_mode*/ = FilamentSyncMode::pull) override
{
// Resumes after ~DeferredFetchAgent destroyed these members; snapshot up front.
auto entered_p = entered;
auto allow_fetch_p = allow_fetch;
auto allow_finish_p = allow_finish;
auto running_p = running;
entered_p->set_value();
allow_fetch_p->get_future().wait();
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
std::thread([this, finish = std::move(allow_finish_p), running = std::move(running_p)] {
struct InFlightGuard
{
MoonrakerPrinterAgent& owner;
~InFlightGuard() { owner.release_fetch_slot(); }
} guard{*this};
g_deferred_fetch_running.fetch_add(1, std::memory_order_relaxed);
running->set_value();
finish->get_future().wait();
g_deferred_fetch_running.fetch_sub(1, std::memory_order_relaxed);
}).detach();
return true;
}
};
} // namespace
// REGRESSION - teardown must not return while a fetch it started is in flight.
// The command worker parks a fetch before it reserves the in-flight slot, forcing
// the "wait already observed zero" interleaving deterministically.
TEST_CASE("an agent's destruction waits for a fetch started by its worker during teardown",
"[unit][moonraker][Regression]")
{
g_deferred_fetch_running.store(0);
g_deferred_destroy_returned.store(false);
auto agent = std::make_shared<DeferredFetchAgent>(std::string{});
auto entered = agent->entered;
auto allow_fetch = agent->allow_fetch;
auto allow_finish = agent->allow_finish;
auto running = agent->running;
// Safety net for the pre-destroyer failure paths: release both gates before the
// agent is destroyed (declared after it, so destroyed before it).
ScopedPromiseRelease release_finish{allow_finish};
ScopedPromiseRelease release_fetch{allow_fetch};
// Park a fetch inside the command worker while the agent is still complete.
agent->post([ptr = agent.get()] { ptr->fetch_filament_info("dev", FilamentSyncMode::pull); });
REQUIRE(entered->get_future().wait_for(std::chrono::seconds(5)) == std::future_status::ready);
// Destroy on another thread so this one can drive the parked fetch.
std::thread destroyer([owned = std::move(agent)]() mutable {
owned.reset();
g_deferred_destroy_returned.store(true);
});
// Releases both gates before joining, so a failing REQUIRE cannot deadlock the join.
ScopedJoiner join_destroyer{destroyer, allow_fetch, allow_finish};
// Let the worker reserve the in-flight slot and spawn its fetch, then wait until it
// is genuinely parked (no polling).
allow_fetch->set_value();
REQUIRE(running->get_future().wait_for(std::chrono::seconds(5)) == std::future_status::ready);
REQUIRE(g_deferred_fetch_running.load() == 1);
// A correct teardown cannot return while the fetch is parked; give a buggy one time.
for (int i = 0; i < 200 && !g_deferred_destroy_returned.load(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (g_deferred_destroy_returned.load()) {
// Bug: teardown returned with a fetch still running. Don't release allow_finish.
CHECK(g_deferred_fetch_running.load() == 0);
return;
}
// Fixed order: destruction is still blocked on the in-flight fetch.
allow_finish->set_value();
destroyer.join();
CHECK(g_deferred_destroy_returned.load());
CHECK(g_deferred_fetch_running.load() == 0);
}
// =========================================================================== // ===========================================================================
// UNIT - printer-agent registry duplicate handling. // UNIT - printer-agent registry duplicate handling.
// Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in // Confirms a duplicate agent id is rejected so a plugin cannot shadow a built-in
@@ -0,0 +1,134 @@
#include "catch2/catch_test_macros.hpp"
#include "catch2/matchers/catch_matchers.hpp"
#include "catch2/matchers/catch_matchers_string.hpp"
#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");
}