diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 4dc838172c..2d0c6a5d8d 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -856,6 +856,10 @@ std::string AppConfig::load() local_machine.dev_ip = p["dev_ip"].get(); if (p.contains("printer_type")) local_machine.printer_type = p["printer_type"].get(); + if (p.contains("printer_agent_id")) + local_machine.printer_agent_id = p["printer_agent_id"].get(); + if (p.contains("access_code")) + local_machine.access_code = p["access_code"].get(); m_local_machines[local_machine.dev_id] = local_machine; } } else { @@ -1068,6 +1072,8 @@ void AppConfig::save() m_json["dev_name"] = local_machine.second.dev_name; m_json["dev_ip"] = local_machine.second.dev_ip; m_json["printer_type"] = local_machine.second.printer_type; + m_json["printer_agent_id"] = local_machine.second.printer_agent_id; + m_json["access_code"] = local_machine.second.access_code; j["local_machines"][local_machine.first] = m_json; } diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index 65c57cdb30..0a278f4f1f 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -66,10 +66,19 @@ struct BBLocalMachine std::string dev_ip; std::string dev_id; /* serial number */ std::string printer_type; /* model_id */ + std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */ + // Access code, scoped to printer_agent_id above - so a code saved while bound under one + // printer agent isn't treated as valid for a different, independent agent talking to the + // same physical dev_id. Empty for entries persisted before this field existed; those fall + // back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only, + // since BBL was the only agent when they were saved) - see + // get_access_code_with_legacy_fallback() in DevManager.cpp. + std::string access_code; bool operator==(const BBLocalMachine& other) const { - return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type; + return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type && + printer_agent_id == other.printer_agent_id && access_code == other.access_code; } bool operator!=(const BBLocalMachine& other) const { return !operator==(other); } }; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index cebfe486cb..b13273d696 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset() //BBS enter_direction = { 0.0f, 0.0f, 0.0f }; exit_direction = { 0.0f, 0.0f, 0.0f }; + jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f }; } void GCodeProcessor::TimeMachine::CustomGCodeTime::reset() @@ -5036,6 +5037,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5118,22 +5123,32 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -5400,6 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5480,22 +5499,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) block.acceleration = acceleration; - // calculates block exit feedrate - curr.safe_feedrate = block.feedrate_profile.cruise; + static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; + const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; - for (unsigned char a = X; a <= E; ++a) { - float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); - if (curr.abs_axis_feedrate[a] > axis_max_jerk) - curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + // Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J). + // Negative leaves the classic jerk path below unchanged. + const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move, + static_cast(i)); + const bool use_junction_deviation = vmax_junction_jd >= 0.0f; + + // calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is + // free to start from rest. + curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise; + + if (!use_junction_deviation) { + for (unsigned char a = X; a <= E; ++a) { + float axis_max_jerk = get_axis_max_jerk(static_cast(i), static_cast(a)); + if (curr.abs_axis_feedrate[a] > axis_max_jerk) + curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk); + } } block.feedrate_profile.exit = curr.safe_feedrate; - static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; - // calculates block entry feedrate - float vmax_junction = curr.safe_feedrate; - if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) { + float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate; + if (!use_junction_deviation && has_prev_move) { bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise; float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise); // Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting. @@ -7168,6 +7197,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode)); } +float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const +{ + const size_t id = static_cast(mode); + + // Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel + // (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel + // in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner. + if (m_flavor == gcfKlipper) { + // machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it. + const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id); + if (scv <= 0.0f || acceleration <= 0.0f) + return 0.0f; + return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration; + } + + // Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0. + if (m_flavor == gcfMarlinFirmware) + return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id); + + return 0.0f; +} + +float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const +{ + float junction_acceleration = block.acceleration; + for (unsigned char a = X; a <= E; ++a) { + if (junction_unit_vec[a] == 0.0f) + continue; + const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast(a), m_machine_config_idx); + if (axis_max_acceleration > 0.0f) + junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a])); + } + return junction_acceleration; +} + +// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp). +float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const +{ + const float junction_deviation = get_junction_deviation(mode, block.acceleration); + if (junction_deviation <= 0.0f) + return -1.0f; // classic jerk machine, the caller keeps its own computation + if (!has_prev_move) + return 0.0f; // starts from rest, the planner raises this on the reverse pass + + // -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin(). + // Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance + // instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter + // than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0) + // and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree + // that the corner is planned by its geometry, and normalizing matches them to within 1e-5. + float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); + if (junction_cos_theta > 0.999999f) + return 0.0f; // the path doubles back, the machine has to stop + junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below + + const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive + const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec; + const float junction_vec_norm = junction_vec.norm(); + const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm) + : Vec4f(0.0f, 0.0f, 0.0f, 0.0f); + const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode); + + float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2); + + // Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and + // capped by the centripetal acceleration it needs. Klipper has no equivalent. + if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) { + // Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin: + // https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html + const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f; + const float t = neg * junction_cos_theta; + const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f + + t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f)))))); + const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033 + vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta); + } + + // Never faster than either of the two moves the junction joins. + vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate))); + return std::sqrt(vmax_junction_sqr); +} + float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const { const size_t id = static_cast(mode); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f5bec9e826..505f7c06a0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -637,6 +637,9 @@ class Print; //For line move, there are same. For arc move, there are different. Vec3f enter_direction; Vec3f exit_direction; + // Orca: move direction over all four axes, unit length. Used by + // calc_vmax_junction_deviation(); see there for why E is normalized in. + Vec4f jd_unit_vec; void reset(); }; @@ -1488,6 +1491,16 @@ class Print; float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const; float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; + // Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine. + float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const; + // Orca: acceleration along the junction direction, clamped by the per axis limits. + float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec, + PrintEstimatedStatistics::ETimeMode mode) const; + // Orca: entry speed from the junction deviation model, which limits a corner by its angle alone + // and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead. + float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev, + const TimeMachine::State& curr, bool has_prev_move, + PrintEstimatedStatistics::ETimeMode mode) const; float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const; Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const; float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 1af28255ee..509744abe2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -5827,7 +5827,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const { Polygon PrintInstance::get_convex_hull_2d() { Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix()); - poly.douglas_peucker(0.1); + poly.douglas_peucker(scale_(0.1)); return poly; } diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index edc958ec53..8844303793 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,20 +10,36 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "libslic3r/Time.hpp" using namespace nlohmann; namespace { - // Orca: access_code and user_access_code used to be separate AppConfig keys before the two - // fields were merged; fall back to the legacy key so existing users' saved codes aren't lost. - std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id) + // Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via + // get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a + // printer under one agent doesn't silently appear as already-bound under a different, + // independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code + // and user_access_code used to be the only, flat dev_id-only AppConfig keys before + // BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no + // agent association at all). Since BBL was the only agent that existed at the time, honor + // those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't + // leaked to other agents that never bound the device themselves. + std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id) { - std::string code = config->get("access_code", dev_id); - if (code.empty()) - code = config->get("user_access_code", dev_id); - return code; + const auto& machines = config->get_local_machines(); + auto it = machines.find(dev_id); + if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty()) + return it->second.access_code; + + if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) { + std::string code = config->get("access_code", dev_id); + if (code.empty()) + code = config->get("user_access_code", dev_id); + return code; + } + return ""; } } @@ -55,12 +71,13 @@ namespace Slic3r continue; MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip); obj->printer_type = m.printer_type; + obj->printer_agent_id = m.printer_agent_id; obj->dev_connection_type = "lan"; obj->bind_state = "free"; obj->bind_sec_link = "secure"; obj->m_is_online = true; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false); if (obj->has_access_right()) { localMachineList.insert(std::make_pair(m.dev_id, obj)); } else { @@ -77,10 +94,12 @@ namespace Slic3r if (m.is_lan_mode_printer()) { if (m.has_access_right()) { BBLocalMachine local_machine; - local_machine.dev_id = m.get_dev_id(); - local_machine.dev_name = m.get_dev_name(); - local_machine.dev_ip = m.get_dev_ip(); - local_machine.printer_type = m.printer_type; + local_machine.dev_id = m.get_dev_id(); + local_machine.dev_name = m.get_dev_name(); + local_machine.dev_ip = m.get_dev_ip(); + local_machine.printer_type = m.printer_type; + local_machine.printer_agent_id = m.printer_agent_id; + local_machine.access_code = m.get_access_code(); config->update_local_machine(local_machine); } } else { @@ -143,6 +162,14 @@ namespace Slic3r } } + std::string DeviceManager::get_current_printer_agent_id() const + { + if (!m_agent) + return ""; + auto printer_agent = m_agent->get_printer_agent(); + return printer_agent ? printer_agent->get_agent_info().id : ""; + } + void DeviceManager::EnableMultiMachine(bool enable) { m_agent->enable_multi_machine(enable); @@ -339,6 +366,7 @@ namespace Slic3r /* insert a new machine */ obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip); obj->printer_type = _parse_printer_type(printer_type_str); + obj->printer_agent_id = get_current_printer_agent_id(); obj->wifi_signal = printer_signal; obj->dev_connection_type = connect_type; obj->bind_state = bind_state; @@ -350,7 +378,7 @@ namespace Slic3r //load access code AppConfig* config = Slic3r::GUI::wxGetApp().app_config; if (config) { - obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id), false); + obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false); } localMachineList.insert(std::make_pair(dev_id, obj)); @@ -379,6 +407,7 @@ namespace Slic3r obj = it->second; } else { obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip); + obj->printer_agent_id = get_current_printer_agent_id(); localMachineList.insert(std::make_pair(machine.dev_id, obj)); } if (machine.printer_type.empty()) @@ -505,16 +534,26 @@ namespace Slic3r OnSelectedMachineChanged(previous_selected_machine, selected_machine); } - void DeviceManager::clear_other_devices() + void DeviceManager::clear_other_devices(const std::string& target_agent_id) { // why: on agent swap, keep "My Devices" but drop the transient "Other Devices" // Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own. + // + // Also drop "My Devices" stamped by a different agent than the one we're swapping to + // (target_agent_id, passed by the caller since the live agent hasn't been repointed yet + // at this point): otherwise a device first discovered under agent A survives every swap + // with a stale printer_agent_id, stays hidden from every agent's filtered list, and only + // gets re-tagged if something happens to delete and re-create it (e.g. account logout). + // Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it + // like any other fresh device. const auto my = get_my_machine_list(); for (auto it = localMachineList.begin(); it != localMachineList.end();) { - if (my.find(it->first) == my.end()) + const bool is_my_device = my.find(it->first) != my.end(); + const bool agent_mismatch = !target_agent_id.empty() && it->second && + it->second->printer_agent_id != target_agent_id; + if (!is_my_device || agent_mismatch) { - // not a "My Device" -> an "Other Device" delete it->second; it = localMachineList.erase(it); } @@ -697,13 +736,16 @@ namespace Slic3r m_agent->add_subscribe(subscribe_list_cache); } - std::map DeviceManager::get_my_machine_list() + std::map DeviceManager::get_my_machine_list(const std::string& agent_id) { std::map result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.insert(std::make_pair(it->first, it->second)); } @@ -711,7 +753,10 @@ namespace Slic3r for (auto it = localMachineList.begin(); it != localMachineList.end(); it++) { - if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer()) { // remove redundant in userMachineList if (result.find(it->first) == result.end()) @@ -723,12 +768,15 @@ namespace Slic3r return result; } - std::map DeviceManager::get_my_cloud_machine_list() + std::map DeviceManager::get_my_cloud_machine_list(const std::string& agent_id) { std::map result; for (auto it = userMachineList.begin(); it != userMachineList.end(); it++) { - if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); } + if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id)) + continue; + + if (!it->second->is_lan_mode_printer()) { result.emplace(*it); } } return result; } @@ -801,6 +849,7 @@ namespace Slic3r else { obj = new MachineObject(this, m_agent, "", "", ""); + obj->printer_agent_id = get_current_printer_agent_id(); if (m_agent) { obj->set_bind_status(m_agent->get_user_name(provider)); diff --git a/src/slic3r/GUI/DeviceCore/DevManager.h b/src/slic3r/GUI/DeviceCore/DevManager.h index e3ac0064b9..1f48baba98 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.h +++ b/src/slic3r/GUI/DeviceCore/DevManager.h @@ -74,7 +74,10 @@ public: void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); } void clean_user_info(bool keep_local_selection = false); - void clear_other_devices(); + // target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check, + // just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the + // live one - this runs before the live agent is repointed. + void clear_other_devices(const std::string& target_agent_id = ""); void load_last_machine(); void update_user_machine_list_info(const std::string& provider); @@ -90,10 +93,15 @@ public: /* my machine*/ MachineObject* get_my_machine(std::string dev_id); - std::map get_my_machine_list(); - std::map get_my_cloud_machine_list(); + std::map get_my_machine_list(const std::string& agent_id = ""); + std::map get_my_cloud_machine_list(const std::string& agent_id = ""); void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider); + // id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if + // m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list() + // to scope results to the active agent. + std::string get_current_printer_agent_id() const; + /* create machine or update machine properties */ void on_machine_alive(std::string json_str); int query_bind_status(std::string& msg, const std::string& provider); diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index f4befe78c1..d8487f4660 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/Utils/NetworkAgentFactory.hpp" #include "GuiColor.hpp" #include "GUI_App.hpp" @@ -458,11 +459,41 @@ void MachineObject::set_access_code(std::string code, bool only_refresh) if (only_refresh) { AppConfig* config = GUI::wxGetApp().app_config; if (config) { - if (!code.empty()) { - GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code); - DeviceManager::update_local_machine(*this); + if (is_lan_mode_printer()) { + // why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and + // scoped by that record's own printer_agent_id field - see the matching comment + // on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this + // device under one printer agent doesn't silently read as already-bound under a + // different, independent one. Cloud devices (the else branch below) aren't + // scoped this way: they're never recalled from a stale local cache across a + // session boundary, since parse_user_print_info() always overwrites their code + // fresh from the cloud API's current response, so there's no cross-agent leakage + // risk to guard against there. + if (!code.empty()) { + DeviceManager::update_local_machine(*this); + } else { + // Only patch an existing record's code - don't persist a brand-new + // never-bound entry just because set_access_code("") was called on it. + const auto& machines = config->get_local_machines(); + auto it = machines.find(get_dev_id()); + if (it != machines.end()) { + BBLocalMachine local_machine = it->second; + local_machine.access_code = ""; + config->update_local_machine(local_machine); + } + // Also clear the pre-scoping flat legacy key when unbinding under BBL, so an + // old BBL-era code can't silently "re-bind" this device again via + // get_access_code_with_legacy_fallback()'s legacy fallback. + if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) { + config->erase("access_code", get_dev_id()); + config->erase("user_access_code", get_dev_id()); + } + } } else { - GUI::wxGetApp().app_config->erase("access_code", get_dev_id()); + if (!code.empty()) + config->set_str("access_code", get_dev_id(), code); + else + config->erase("access_code", get_dev_id()); } } } diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 33635fbe6e..914c8f7868 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -229,6 +229,16 @@ public: //PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN; std::string printer_type; /* model_id */ + + // id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id, + // e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single + // process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap + // (see DeviceManager::set_agent()), so it can't tell which agent originally found this device. + // We persist this as well so that when the printer agent is swapped, we don't show unrelated devices, + // e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent + // under local machines. + std::string printer_agent_id; + std::string get_show_printer_type() const; PrinterSeries get_printer_series() const; PrinterArch get_printer_arch() const; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index ffde09dbca..3a85dc1928 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3951,7 +3951,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr agent) m_agent->set_user_selected_machine(""); // note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer) dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS - dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices + // why: drop stale LAN discoveries; keep My Devices, but only those belonging to the + // agent we're about to swap to, so a device stamped by the outgoing agent doesn't + // linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh. + // agent is null when clearing the live agent entirely (e.g. plugin unload); there's no + // target to filter against then, so fall back to the original "keep all My Devices" + // behavior rather than guessing. + dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string()); } m_agent->set_printer_agent(agent); diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 6238849ebc..7d724ae273 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3912,7 +3912,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager, }; // collect from user machine list - const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list + const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list for (const auto& elem : user_machine_list) { MachineObject* mobj = elem.second; diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 96324fb4d8..df0a566917 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices() DeviceManager* dev = wxGetApp().getDeviceManager(); if (!dev) return; m_free_machine_list = dev->get_local_machinelist(); + const std::string current_agent_id = dev->get_current_printer_agent_id(); BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start"; this->Freeze(); @@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices() /* do not show printer bind state is empty */ if (!mobj->is_avaliable()) continue; + /* do not show devices discovered/bound by a different printer agent */ + if (mobj->printer_agent_id != current_agent_id) + continue; + if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer()) continue; @@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices() } m_bind_machine_list.clear(); - m_bind_machine_list = dev->get_my_machine_list(); + m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id()); //sort list std::vector> user_machine_list; diff --git a/tests/fff_print/test_gcode_timing.cpp b/tests/fff_print/test_gcode_timing.cpp index 4570f253bb..8c08fc4f03 100644 --- a/tests/fff_print/test_gcode_timing.cpp +++ b/tests/fff_print/test_gcode_timing.cpp @@ -7,9 +7,14 @@ #include "test_utils.hpp" +#include #include +#include #include #include +#include +#include +#include using namespace Slic3r; using Catch::Matchers::WithinAbs; @@ -418,3 +423,194 @@ TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][Mul REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10)); } } + +// Junction planning decides the speeds the "actual speed" / "actual flow" preview shows. Per-axis +// jerk limits a corner by the largest single-axis component of the velocity change, allowing sqrt(2) +// more speed on a diagonal than on an axis -- a four-lobed ripple around every circle. Klipper and +// Marlin 2 with M205 J plan with junction deviation instead, which sees only the corner angle. +namespace { + +// One acceleration everywhere and axis limits far above it, so only the junction model under test +// can slow a corner down. +FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, double junction_deviation) +{ + FullPrintConfig config; + config.gcode_flavor.value = flavor; + config.filament_diameter.values = {1.75}; + config.filament_map.values = {1}; + + const std::vector accel = {1000.0, 1000.0}; + const std::vector axis = {20000.0, 20000.0}; + const std::vector speed = {500.0, 500.0}; + config.machine_max_acceleration_extruding.values = accel; + config.machine_max_acceleration_travel.values = accel; + config.machine_max_acceleration_retracting.values = accel; + config.machine_max_acceleration_x.values = axis; + config.machine_max_acceleration_y.values = axis; + config.machine_max_acceleration_z.values = axis; + config.machine_max_acceleration_e.values = axis; + config.machine_max_speed_x.values = speed; + config.machine_max_speed_y.values = speed; + config.machine_max_speed_z.values = speed; + config.machine_max_speed_e.values = speed; + // Klipper reads this as the square corner velocity, Marlin as classic jerk. + config.machine_max_jerk_x.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_y.values = {corner_velocity, corner_velocity}; + config.machine_max_jerk_z.values = {corner_velocity, corner_velocity}; + // Kept out of the way so it never binds in the classic-jerk comparisons. + config.machine_max_jerk_e.values = {100.0, 100.0}; + config.machine_max_junction_deviation.values = {junction_deviation, junction_deviation}; + config.machine_min_extruding_rate.values = {0.0, 0.0}; + config.machine_min_travel_rate.values = {0.0, 0.0}; + return config; +} + +constexpr double junction_x = 60.0; +constexpr double junction_y = 60.0; + +// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. +// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests +// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric +// as the formulas below assume. +std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0) +{ + const double len = 40.0; + const double a_in = orientation_deg * M_PI / 180.0; + const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0; + std::ostringstream extrude; + if (e_per_mm > 0.0) + extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm; + + std::ostringstream os; + os << std::fixed << std::setprecision(4) + << "M83\n" + << "G1 Z0.2 F1200\n" + << "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n" + << "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n" + << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) + << extrude.str() << " F9000\n"; + return os.str(); +} + +// Speed allowed through the corner: the vertex ending the incoming move carries that block's exit +// speed, and the vertices the actual-speed pass inserts are all strictly interior. +double corner_speed(const GCodeProcessorResult& r) +{ + for (const auto& mv : r.moves) + if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) && + std::abs(mv.position.x() - junction_x) < 1e-3 && + std::abs(mv.position.y() - junction_y) < 1e-3) + return mv.actual_feedrate; + return -1.0; +} + +double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation, + double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0) +{ + GCodeProcessor proc; + run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation), + corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str()); + return corner_speed(proc.get_result()); +} + +} // namespace + +TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity", + "[GCodeTiming][JunctionDeviation]") +{ + // jd = scv^2 * (sqrt(2) - 1) / max_accel, then v^2 = jd * accel * sin(t/2) / (1 - sin(t/2)). + // The acceleration cancels: the corner speed depends only on the scv and the angle. + const double scv = 5.0; + + SECTION("a right angle is taken at exactly the square corner velocity") { + // sin(t/2) = sqrt(0.5) at 90 degrees, so v == scv -- the definition of the square corner + // velocity, and what makes the mapping above the right one. + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, 90.0), Catch::Matchers::WithinRel(scv, 0.02)); + } + + SECTION("a shallow corner is taken far faster than the per-axis jerk model allows") { + // 6 degrees: sin(t/2) = cos(3 deg), so v = 5 * sqrt((sqrt(2) - 1) * 728.68) = 86.9mm/s. Per-axis + // jerk ignores the angle and caps the velocity *change* (2v*sin(3 deg)), giving 47.8mm/s. + const double jd_speed = planned_corner_speed(gcfKlipper, scv, 0.0, 6.0); + const double jerk_speed = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, 6.0); + REQUIRE_THAT(jd_speed, Catch::Matchers::WithinRel(86.87, 0.02)); + REQUIRE_THAT(jerk_speed, Catch::Matchers::WithinRel(47.75, 0.02)); + } +} + +TEST_CASE("Junction deviation limits a corner by its angle alone, not by its orientation", + "[GCodeTiming][JunctionDeviation]") +{ + // The four-lobed ripple on circular walls is per-axis jerk being anisotropic: a velocity change + // lying on an axis gets sqrt(2) less headroom than the same change on the diagonal. + const double scv = 5.0; + const double turn = 6.0; + + SECTION("Klipper plans both orientations identically") { + const double on_axis = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE_THAT(diagonal, Catch::Matchers::WithinRel(on_axis, 0.02)); + } + + SECTION("the classic jerk model keeps its orientation dependence") { + const double on_axis = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 0.0); + const double diagonal = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 45.0); + REQUIRE(on_axis > 0.0); + REQUIRE(diagonal / on_axis > 1.2); + } +} + +TEST_CASE("Junction deviation is only used where the firmware actually plans with it", + "[GCodeTiming][JunctionDeviation]") +{ + const double jerk = 5.0; + + SECTION("Marlin 2 with M205 J disabled keeps the classic jerk planning") { + // machine_max_junction_deviation == 0 is how a Marlin 2 printer says it runs classic jerk. + const double classic = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE(classic > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.0, 90.0), + Catch::Matchers::WithinRel(classic, 1e-4)); + } + + SECTION("Marlin 2 with M205 J enabled switches to junction deviation") { + // sqrt(1000 * 0.05 * 2.4142136) = 11.0mm/s, independent of the jerk values it no longer reads. + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(10.99, 0.02)); + } + + SECTION("machines without junction deviation are untouched by the jerk values it would ignore") { + // A flavor that never enters the junction deviation path must ignore the setting entirely. + const double without = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinLegacy, jerk, 0.05, 90.0), + Catch::Matchers::WithinRel(without, 1e-4)); + } +} + +TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it", + "[GCodeTiming][JunctionDeviation]") +{ + // The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the + // E term makes the two paths look more parallel than they are and the corner comes out too fast, + // the more so the higher the flow. Marlin normalizes over XYZE on any extruding move + // (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether + // (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone. + const double scv = 5.0; + const double turn = 6.0; + const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn); + REQUIRE(geometric > 0.0); + + // 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one. + // Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9. + for (double e_per_mm : {0.029, 0.1}) + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm), + Catch::Matchers::WithinRel(geometric, 0.02)); + + SECTION("and the same holds on Marlin 2") { + const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn); + REQUIRE(marlin > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029), + Catch::Matchers::WithinRel(marlin, 0.02)); + } +}