Implement filament consumption dialog and undo functionality

This commit is contained in:
Ocraftyone 2024-11-27 12:31:49 -05:00
parent 1005067526
commit bf17e8e998
15 changed files with 241 additions and 49 deletions

View file

@ -407,6 +407,9 @@ void AppConfig::set_defaults()
set_str("print", "timelapse", "1");
}
if (get("spoolman", "consumption_type").empty())
set_str("spoolman", "consumption_type", "weight");
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");

View file

@ -1566,32 +1566,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
result->filename = path;
}
std::vector<int> filaments_with_spoolman_idxs;
for (int l = 0; l < print->config().filament_density.size(); ++l) {
if (print->config().filament_spoolman_enabled.get_at(l))
filaments_with_spoolman_idxs.push_back(l);
}
// Check the consumption of filament against the remaining filament as reported by Spoolman
for (const auto& est : print->get_spoolman_filament_consumption_estimates()) {
double remaining_length = print->config().filament_remaining_length.get_at(est.print_config_idx);
double remaining_weight = print->config().filament_remaining_weight.get_at(est.print_config_idx);
if (!filaments_with_spoolman_idxs.empty()) {
// get used filament (meters and grams) from used volume in respect to the active extruder
auto get_used_filament_from_volume = [&](int extruder_id) {
double volume = print->m_print_statistics.filament_stats[extruder_id];
std::pair<double, double> ret = { volume / (PI * sqr(0.5 * print->config().filament_diameter.get_at(extruder_id))),
volume * print->config().filament_density.get_at(extruder_id) * 0.001 };
return ret;
};
for (const auto& item : filaments_with_spoolman_idxs) {
auto [est_used_length, est_used_weight] = get_used_filament_from_volume(item);
double remaining_length = print->config().filament_remaining_length.get_at(item);
double remaining_weight = print->config().filament_remaining_weight.get_at(item);
if (est_used_length > remaining_length || est_used_weight > remaining_weight) {
std::string filament_name = print->config().filament_settings_id.get_at(item);
std::string msg = boost::str(boost::format(_("Filament %1% does not have enough material for the print. Used: %2$.2f m, %3$.2f g, Remaining: %4$.2f m, %5$.2f g")) %
filament_name % (est_used_length * 0.001) % est_used_weight % (remaining_length * 0.001) % remaining_weight);
print->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, msg, PrintStateBase::SlicingNotificationType::SlicingNotEnoughFilament);
}
if (est.est_used_length > remaining_length || est.est_used_weight > remaining_weight) {
std::string msg = boost::str(boost::format(_("Filament %1% does not have enough material for the print. Used: %2$.2f m, %3$.2f g, Remaining: %4$.2f m, %5$.2f g")) %
est.filament_name % (est.est_used_length * 0.001) % est.est_used_weight % (remaining_length * 0.001) % remaining_weight);
print->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, msg, PrintStateBase::SlicingNotificationType::SlicingNotEnoughFilament);
}
}

View file

@ -249,7 +249,7 @@ public:
// works for filament and printer profiles. All other profiles return false
bool spoolman_enabled() const {
if (type == TYPE_FILAMENT)
return config.opt_int("spoolman_spool_id") > 0;
return config.opt_int("spoolman_spool_id", 0) > 0;
if (type == TYPE_PRINTER)
return config.opt_bool("spoolman_enabled");
return false;

View file

@ -2127,7 +2127,6 @@ DynamicPrintConfig PresetBundle::full_fff_config() const
std::vector<std::string> print_compatible_printers;
//BBS: add logic for settings check between different system presets
std::vector<std::string> different_settings;
std::vector<unsigned char> filament_spoolman_enabled;
std::vector<double> filament_remaining_weight;
std::vector<double> filament_remaining_length;
std::string different_print_settings, different_printer_settings;
@ -2152,7 +2151,6 @@ DynamicPrintConfig PresetBundle::full_fff_config() const
out.apply(this->filaments.get_edited_preset().config);
compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition());
compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition());
filament_spoolman_enabled.emplace_back(this->filaments.get_edited_preset().spoolman_enabled());
filament_remaining_weight.emplace_back(this->filaments.get_edited_preset().spoolman_statistics->remaining_weight);
filament_remaining_length.emplace_back(this->filaments.get_edited_preset().spoolman_statistics->remaining_length);
//BBS: add logic for settings check between different system presets
@ -2196,7 +2194,6 @@ DynamicPrintConfig PresetBundle::full_fff_config() const
DynamicPrintConfig &cfg_rw = *const_cast<DynamicPrintConfig*>(cfg);
compatible_printers_condition.emplace_back(Preset::compatible_printers_condition(cfg_rw));
compatible_prints_condition .emplace_back(Preset::compatible_prints_condition(cfg_rw));
filament_spoolman_enabled.emplace_back(preset->spoolman_enabled());
filament_remaining_weight.emplace_back(preset->spoolman_statistics->remaining_weight);
filament_remaining_length.emplace_back(preset->spoolman_statistics->remaining_length);
@ -2306,7 +2303,6 @@ DynamicPrintConfig PresetBundle::full_fff_config() const
out.option<ConfigOptionStrings>("filament_settings_id", true)->values = this->filament_presets;
out.option<ConfigOptionString >("printer_settings_id", true)->value = this->printers.get_selected_preset_name();
out.option<ConfigOptionStrings>("filament_ids", true)->values = filament_ids;
out.option<ConfigOptionBools>("filament_spoolman_enabled", true)->values = filament_spoolman_enabled;
out.option<ConfigOptionFloats>("filament_remaining_weight", true)->values = filament_remaining_weight;
out.option<ConfigOptionFloats>("filament_remaining_length", true)->values = filament_remaining_length;

View file

@ -36,6 +36,7 @@
#include "nlohmann/json.hpp"
#include "GCode/ConflictChecker.hpp"
#include "slic3r/Utils/Spoolman.hpp"
#include <codecvt>
@ -3058,6 +3059,33 @@ Vec3d Print::shrinkage_compensation() const
return { xy_compensation, xy_compensation, z_compensation };
}
std::vector<SpoolmanFilamentConsumptionEstimate> Print::get_spoolman_filament_consumption_estimates() const
{
std::vector<SpoolmanFilamentConsumptionEstimate> spoolman_filament_consumption;
std::vector<int> filaments_with_spoolman_idxs;
for (int l = 0; l < m_config.spoolman_spool_id.size(); ++l) {
if (m_config.spoolman_spool_id.get_at(l) > 0)
filaments_with_spoolman_idxs.push_back(l);
}
// get used filament (meters and grams) from used volume in respect to the active extruder
auto get_used_filament_from_volume = [&](const int& extruder_id) {
// confirm the item exists in the stats map
if (m_print_statistics.filament_stats.count(extruder_id) <= 0)
return std::pair {0., 0.};
const double& volume = m_print_statistics.filament_stats.at(extruder_id);
return std::pair { volume / (PI * sqr(0.5 * m_config.filament_diameter.get_at(extruder_id))),
volume * m_config.filament_density.get_at(extruder_id) * 0.001 };
};
for (const auto& idx : filaments_with_spoolman_idxs)
spoolman_filament_consumption.emplace_back(idx, m_config, get_used_filament_from_volume(idx));
return spoolman_filament_consumption;
}
const std::string PrintStatistics::FilamentUsedG = "filament used [g]";
const std::string PrintStatistics::FilamentUsedGMask = "; filament used [g] =";

View file

@ -812,6 +812,28 @@ enum FilamentTempType {
HighLowCompatible,
Undefine
};
struct SpoolmanFilamentConsumptionEstimate
{
const unsigned int print_config_idx;
const unsigned int spoolman_spool_id;
const std::string filament_name;
const double est_used_length;
const double est_used_weight;
SpoolmanFilamentConsumptionEstimate(const unsigned int& print_config_idx, const PrintConfig& config, const double& est_used_length, const double& est_used_weight)
: print_config_idx(print_config_idx)
, spoolman_spool_id(config.spoolman_spool_id.get_at(print_config_idx))
, filament_name(config.filament_settings_id.get_at(print_config_idx))
, est_used_length(est_used_length)
, est_used_weight(est_used_weight)
{}
// Alternate that allows the estimates to be provided as a std::pair [est_used_length, est_used_weight]
SpoolmanFilamentConsumptionEstimate(const unsigned int& print_config_idx, const PrintConfig& config, const std::pair<double, double>& estimates) :
SpoolmanFilamentConsumptionEstimate(print_config_idx, config, estimates.first, estimates.second) {}
};
// The complete print tray with possibly multiple objects.
class Print : public PrintBaseWithState<PrintStep, psCount>
{
@ -990,6 +1012,8 @@ public:
std::tuple<float, float> object_skirt_offset(double margin_height = 0) const;
std::vector<SpoolmanFilamentConsumptionEstimate> get_spoolman_filament_consumption_estimates() const;
protected:
// Invalidates the step, and its depending steps in Print.
bool invalidate_step(PrintStep step);

View file

@ -2198,10 +2198,6 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionStrings());
def->cli = ConfigOptionDef::nocli;
def = this->add("filament_spoolman_enabled", coBools);
def->set_default_value(new ConfigOptionBools());
def->cli = ConfigOptionDef::nocli;
def = this->add("filament_remaining_weight", coFloats);
def->set_default_value(new ConfigOptionFloats());
def->cli = ConfigOptionDef::nocli;
@ -2217,13 +2213,13 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionStrings{L("(Undefined)")});
def->cli = ConfigOptionDef::nocli;
def = this->add("spoolman_spool_id", coInt);
def = this->add("spoolman_spool_id", coInts);
def->label = L("Spoolman ID");
def->tooltip = L("The spool ID of this filament profile within your Spoolman instance. This will allow automatic spool switching when "
"using moonraker to track spool usage and one touch updating of this filament profile from the Spoolman properties. "
"Setting this to a value of 0 disables its functionality.");
def->mode = comSimple;
def->set_default_value(new ConfigOptionInt());
def->set_default_value(new ConfigOptionInts({ 0 }));
def->cli = ConfigOptionDef::nocli;
def = this->add("infill_direction", coFloat);

View file

@ -1163,7 +1163,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
((ConfigOptionBool, has_scarf_joint_seam))
((ConfigOptionBools, filament_spoolman_enabled))
((ConfigOptionInts, spoolman_spool_id))
((ConfigOptionFloats, filament_remaining_weight))
((ConfigOptionFloats, filament_remaining_length))
)

View file

@ -250,9 +250,11 @@ void BackgroundSlicingProcess::process_fff()
finalize_gcode();
else
export_gcode();
wxQueueEvent(wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_finished_id));
} else if (! m_upload_job.empty()) {
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id));
prepare_upload();
wxQueueEvent(wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_finished_id));
} else {
m_print->set_status(100, _utf8(L("Slicing complete")));
}
@ -883,10 +885,10 @@ void BackgroundSlicingProcess::export_gcode()
}
// BBS
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
wxString output_gcode_str = wxString::FromUTF8(export_path.c_str(), export_path.length());
evt->SetString(output_gcode_str);
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt);
// auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
// wxString output_gcode_str = wxString::FromUTF8(export_path.c_str(), export_path.length());
// evt->SetString(output_gcode_str);
// wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, evt);
// BBS: to be checked. Whether use export_path or output_path.
gcode_add_line_number(export_path, m_fff_print->full_print_config());

View file

@ -26,6 +26,8 @@
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "Spoolman.hpp"
#include <imgui/imgui_internal.h>
static constexpr float GAP_WIDTH = 10.0f;
@ -2087,6 +2089,19 @@ void NotificationManager::push_import_finished_notification(const std::string& p
set_slicing_progress_hidden();
}
void NotificationManager::push_spoolman_consumption_finished_notification()
{
close_notification_of_type(NotificationType::SpoolmanConsumptionFinished);
auto callback = [](wxEvtHandler*) {
if (Spoolman::get_instance()->undo_use_spoolman_spools())
return true;
show_error(nullptr, _L("Failed to undo Spoolman filament consumption"));
return false;
};
NotificationData data {NotificationType::SpoolmanConsumptionFinished, NotificationLevel::RegularNotificationLevel, 0, _u8L("Spoolman consumption finished successfully.") + " ", _u8L("Undo"), callback };
push_notification_data(data, 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists

View file

@ -126,6 +126,8 @@ enum class NotificationType
NetfabbFinished,
// Short meesage to fill space between start and finish of export
ExportOngoing,
// A message showing that Spoolman filament consumption finished and allow a rollback of the action
SpoolmanConsumptionFinished,
// Progressbar of download from prusaslicer://url
URLDownload,
// BBS: Short meesage to fill space between start and finish of arranging
@ -250,6 +252,8 @@ public:
void push_exporting_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable);
void push_import_finished_notification(const std::string& path, const std::string& dir_path, bool on_removable);
void push_spoolman_consumption_finished_notification();
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
void set_download_URL_progress(size_t id, float percentage);

View file

@ -2564,6 +2564,10 @@ struct Plater::priv
// Returns true if current_warnings vector is empty without showning the dialog
bool warnings_dialog();
// If Spoolman is active, the server is valid, and at least one Spoolman spool is used,
// a dialog will be show asking if the user would like to consume the estimated filament usage
void spoolman_consumption_dialog(const bool& all_plates);
void on_action_add(SimpleEvent&);
void on_action_add_plate(SimpleEvent&);
void on_action_del_plate(SimpleEvent&);
@ -6760,8 +6764,12 @@ void Plater::priv::on_export_began(wxCommandEvent& evt)
void Plater::priv::on_export_finished(wxCommandEvent& evt)
{
if (!m_export_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1)))
spoolman_consumption_dialog(m_export_all);
#if 0
//BBS: also export 3mf to the same directory for debugging
if (evt.GetString().empty())
return;
std::string gcode_path_str(evt.GetString().ToUTF8().data());
fs::path gcode_path(gcode_path_str);
@ -6866,6 +6874,60 @@ bool Plater::priv::warnings_dialog()
return res == wxID_YES;
}
void Plater::priv::spoolman_consumption_dialog(const bool& all_plates)
{
if (!wxGetApp().preset_bundle->printers.get_edited_preset().spoolman_enabled())
return;
if (!Spoolman::is_server_valid())
return;
auto spoolman = Spoolman::get_instance();
const auto& consumption_type = wxGetApp().app_config->get("spoolman", "consumption_type");
std::string unit = consumption_type == "weight" ? "g" : consumption_type == "length" ? "mm" : "";
if (unit.empty()) {
BOOST_LOG_TRIVIAL(error) << "The specified consumption type is not valid";
return;
}
std::map<unsigned, double> estimates;
std::map<unsigned, wxString> messages;
auto apply_estimates_from_plate = [&] (PartPlate* plate) {
for (const auto& est : plate->fff_print()->get_spoolman_filament_consumption_estimates()) {
auto& id = est.spoolman_spool_id;
if (consumption_type == "weight") {
estimates[id] += est.est_used_weight;
} else if (consumption_type == "length") {
estimates[id] += est.est_used_length;
} else return;
messages[id] = wxString::FromUTF8((boost::format("%1%: %2% %3%") % est.filament_name % double_to_string(estimates[id], 2) % unit).str());
}
};
if (all_plates)
for (const auto& plate : partplate_list.get_plate_list())
apply_estimates_from_plate(plate);
else
apply_estimates_from_plate(partplate_list.get_curr_plate());
if (estimates.empty()) return;
auto msg = _L("Would you like to consume the used filaments registered in Spoolman?") + "\n\n";
for (const auto& [id, message] : messages)
msg += message + "\n";
auto dlg = MessageDialog(nullptr, msg, _L("Spoolman Filament Consumption"), wxYES_NO);
if (dlg.ShowModal() == wxID_YES) {
if (spoolman->use_spoolman_spools(estimates, consumption_type)) {
notification_manager->push_spoolman_consumption_finished_notification();
} else {
BOOST_LOG_TRIVIAL(error) << "Failed to consume filament from Spoolman";
show_error(nullptr, _L("Failed to consume filament from Spoolman"));
}
}
}
//BBS: add project slice logic
void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
{

View file

@ -3712,7 +3712,7 @@ void TabFilament::toggle_options()
}
if (m_active_page->title() == L("Spoolman")) {
toggle_line("spoolman_update", m_config->opt_int("spoolman_spool_id") > 0);
toggle_line("spoolman_update", m_config->opt_int("spoolman_spool_id", 0) > 0);
update_spoolman_statistics();
}
}

View file

@ -152,10 +152,10 @@ bool Spoolman::pull_spoolman_spools()
return true;
}
bool Spoolman::use_spoolman_spool(const unsigned int& spool_id, const double& weight_used)
bool Spoolman::use_spoolman_spool(const unsigned int& spool_id, const double& usage, const std::string& usage_type)
{
pt::ptree tree;
tree.put("use_weight", weight_used);
tree.put("use_" + usage_type, usage);
std::string endpoint = (boost::format("spool/%1%/use") % spool_id).str();
tree = put_spoolman_json(endpoint, tree);
@ -166,6 +166,46 @@ bool Spoolman::use_spoolman_spool(const unsigned int& spool_id, const double& we
return true;
}
bool Spoolman::use_spoolman_spools(const std::map<unsigned int, double>& data, const std::string& usage_type)
{
if (!(usage_type == "length" || usage_type == "weight"))
return false;
std::vector<unsigned int> spool_ids;
for (auto& [spool_id, usage] : data) {
if (!use_spoolman_spool(spool_id, usage, usage_type))
return false;
spool_ids.emplace_back(spool_id);
}
update_specific_spool_statistics(spool_ids);
m_use_undo_buffer = data;
m_last_usage_type = usage_type;
return true;
}
bool Spoolman::undo_use_spoolman_spools()
{
if (m_use_undo_buffer.empty() || m_last_usage_type.empty())
return false;
std::vector<unsigned int> spool_ids;
for (auto& [spool_id, usage] : m_use_undo_buffer) {
if (!use_spoolman_spool(spool_id, usage * -1, m_last_usage_type))
return false;
spool_ids.emplace_back(spool_id);
}
update_specific_spool_statistics(spool_ids);
m_use_undo_buffer.clear();
m_last_usage_type.clear();
return true;
}
SpoolmanResult Spoolman::create_filament_preset_from_spool(const SpoolmanSpoolShrPtr& spool,
const Preset* base_profile,
bool detach,
@ -192,7 +232,7 @@ SpoolmanResult Spoolman::create_filament_preset_from_spool(const SpoolmanSpoolSh
// Check for presets with the same spool ID
int visible(0), invisible(0);
for (const auto& item : filaments()) { // count num of visible and invisible
if (item.config.opt_int("spoolman_spool_id") == spool->id) {
if (item.config.opt_int("spoolman_spool_id", 0) == spool->id) {
if (item.is_visible)
visible++;
else
@ -241,7 +281,7 @@ SpoolmanResult Spoolman::update_filament_preset_from_spool(Preset* filament_pres
result.messages.emplace_back("Preset is not a filament preset");
return result;
}
const int& spool_id = filament_preset->config.opt_int("spoolman_spool_id");
const int& spool_id = filament_preset->config.opt_int("spoolman_spool_id", 0);
if (spool_id < 1) {
result.messages.emplace_back(
"Preset provided does not have a valid Spoolman spool ID"); // IDs below 1 are not used by spoolman and should be ignored
@ -271,12 +311,34 @@ void Spoolman::update_visible_spool_statistics(bool clear_cache)
if (item->is_user() && item->spoolman_enabled()) {
if (auto res = update_filament_preset_from_spool(item, true, true); res.has_failed())
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Failed to update spoolman statistics with the following error: "
<< res.build_single_line_message() << "Spool ID: " << item->config.opt_int("spoolman_spool_id");
<< res.build_single_line_message() << "Spool ID: " << item->config.opt_int("spoolman_spool_id", 0);
}
}
}
}
void Spoolman::update_specific_spool_statistics(const std::vector<unsigned int>& spool_ids)
{
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
PresetCollection& printers = preset_bundle->printers;
PresetCollection& filaments = preset_bundle->filaments;
std::set spool_ids_set(spool_ids.begin(), spool_ids.end());
// make sure '0' is not a value
spool_ids_set.erase(0);
if (printers.get_edited_preset().spoolman_enabled() && is_server_valid()) {
for (auto item : filaments.get_visible()) {
if (item->is_user() && spool_ids_set.count(item->config.opt_int("spoolman_spool_id", 0)) > 0) {
if (auto res = update_filament_preset_from_spool(item, true, true); res.has_failed())
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Failed to update spoolman statistics with the following error: "
<< res.build_single_line_message() << "Spool ID: " << item->config.opt_int("spoolman_spool_id", 0);
}
}
}
}
bool Spoolman::is_server_valid()
{
bool res = false;

View file

@ -50,6 +50,9 @@ class Spoolman
bool m_initialized{false};
std::map<unsigned int, double> m_use_undo_buffer{};
std::string m_last_usage_type{};
std::map<unsigned int, SpoolmanVendorShrPtr> m_vendors{};
std::map<unsigned int, SpoolmanFilamentShrPtr> m_filaments{};
std::map<unsigned int, SpoolmanSpoolShrPtr> m_spools{};
@ -71,10 +74,21 @@ class Spoolman
/// get all the spools from the api and store them
/// \returns if succeeded
bool pull_spoolman_spools();
public:
/// uses/consumes filament from the specified spool then updates the spool
/// \param usage_type The consumption metric to be used. Should be "length" or "weight". This will NOT be checked.
/// \returns if succeeded
bool use_spoolman_spool(const unsigned int& spool_id, const double& weight_used);
bool use_spoolman_spool(const unsigned int& spool_id, const double& usage, const std::string& usage_type);
public:
/// uses/consumes filament from multiple specified spools then updates them
/// \param data a map with the spool ID as the key and the amount to be consumed as the value
/// \param usage_type The consumption metric to be used. Should be "length" or "weight". This will be checked.
/// \returns if succeeded
bool use_spoolman_spools(const std::map<unsigned int, double>& data, const std::string& usage_type);
/// undo the previous use/consumption
/// \returns if succeeded
bool undo_use_spoolman_spools();
static SpoolmanResult create_filament_preset_from_spool(const SpoolmanSpoolShrPtr& spool,
const Preset* base_profile,
@ -88,6 +102,9 @@ public:
/// clear_cache should be set true if the update is due to a change in printer profile or other change that requires it
static void update_visible_spool_statistics(bool clear_cache = false);
/// Update the statistics values for the filament profiles tied to the specified spool IDs
static void update_specific_spool_statistics(const std::vector<unsigned int>& spool_ids);
static bool is_server_valid();
const std::map<unsigned int, SpoolmanSpoolShrPtr>& get_spoolman_spools(bool update = false)