mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 20:31:14 +00:00
feat: update happy hare parsing
This commit is contained in:
@@ -947,6 +947,21 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give every live device extruder an external virtual tray slot (non-Bambu) so a plugin agent
|
||||
// that reaches the model only through this parser also gets the shared external-slot topology.
|
||||
if (obj)
|
||||
obj->ensure_external_virtual_tray_slots();
|
||||
|
||||
// Diagnostic: summarize the AMS state this production parse produced, so the agent boundary
|
||||
// (submit_ams_payload) box/tray counts can be correlated with what actually landed in the model.
|
||||
if (obj && system) {
|
||||
size_t parsed_trays = 0;
|
||||
for (const auto& kv : system->amsList)
|
||||
parsed_trays += kv.second->GetTrays().size();
|
||||
BOOST_LOG_TRIVIAL(info) << "DevFilaSystemParser::ParseV1_0: parsed units " << system->amsList.size()
|
||||
<< " trays " << parsed_trays;
|
||||
}
|
||||
}
|
||||
|
||||
static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
@@ -957,6 +972,23 @@ static DevAms::AmsType ams_type_from_string(const std::string& s)
|
||||
return DevAms::AMS; // default
|
||||
}
|
||||
|
||||
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id)
|
||||
{
|
||||
// Address a slot by its virtual tray id, never by a raw vector index: the slot for `tray_id`
|
||||
// lives at index VIRTUAL_TRAY_MAIN_ID - tray_id and placeholder entries carry
|
||||
// id = VIRTUAL_TRAY_MAIN_ID - index. Append-only, so a slot that already holds reported
|
||||
// metadata keeps it and a sparse/out-of-order report cannot shift the id <-> extruder mapping.
|
||||
if (tray_id > static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID))
|
||||
return;
|
||||
const std::size_t slot_index = static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID) - tray_id;
|
||||
while (slots.size() <= slot_index) {
|
||||
slots.emplace_back(std::to_string(VIRTUAL_TRAY_MAIN_ID - static_cast<int>(slots.size())));
|
||||
// A filler only makes the slot addressable; it is topology, not a reported spool. Callers
|
||||
// overwrite the target entry with reported metadata, which clears this marker.
|
||||
slots.back().is_slot_placeholder = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
|
||||
{
|
||||
if (!system || !data.is_object())
|
||||
@@ -1084,7 +1116,6 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
std::map<int, DevAmsTray> external_slots;
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
@@ -1105,9 +1136,23 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
}
|
||||
|
||||
for (auto& entry : external_slots)
|
||||
obj->vt_slot.push_back(std::move(entry.second));
|
||||
// Place each reported spool at its own index (VIRTUAL_TRAY_MAIN_ID - extruder_id) instead of
|
||||
// appending in report order: a sparse or out-of-order report must not compact the vector and
|
||||
// shift the slot -> extruder association.
|
||||
obj->vt_slot.clear();
|
||||
for (const auto& entry : external_slots)
|
||||
{
|
||||
// entry.first is the extruder id; its virtual tray id is VIRTUAL_TRAY_MAIN_ID - extruder_id
|
||||
// and the slot lives at vector index extruder_id (the id <-> index mapping grow applies).
|
||||
grow_virtual_tray_slots(obj->vt_slot, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - entry.first));
|
||||
obj->vt_slot[entry.first] = entry.second;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any extruder still missing a slot so the model always has one external slot per live
|
||||
// extruder, even when the agent reported no external data at all. Preserves reported metadata.
|
||||
if (obj)
|
||||
obj->ensure_external_virtual_tray_slots();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "DevFilaSwitch.h"
|
||||
#include "DevUtil.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -82,7 +83,7 @@ public:
|
||||
wxColour wx_color;
|
||||
bool is_bbl;
|
||||
bool is_exists = false;
|
||||
bool is_slot_placeholder = false; // Orca: True for empty tray slots from pull-mode agents
|
||||
bool is_slot_placeholder = false; // Orca: True for synthetic topology slots or empty reported trays
|
||||
int hold_count = 0;
|
||||
int remain = 0; // filament remain: 0 ~ 100
|
||||
|
||||
@@ -114,6 +115,16 @@ public:
|
||||
std::optional<DevFilamentDryingPreset> get_ams_drying_preset() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure the virtual-tray vector can address the slot for a virtual tray id. The slot's index is
|
||||
* `VIRTUAL_TRAY_MAIN_ID - tray_id` and placeholder entries carry id = VIRTUAL_TRAY_MAIN_ID - index.
|
||||
* Existing entries are left untouched, so a slot already populated with reported filament data
|
||||
* keeps it; callers then assign the reported tray at index `VIRTUAL_TRAY_MAIN_ID - tray_id`.
|
||||
* Appended placeholders are marked `is_slot_placeholder`, so consumers can tell a synthetic
|
||||
* topology filler from a spool the printer actually reported (overwriting the entry clears it).
|
||||
*/
|
||||
void grow_virtual_tray_slots(std::vector<DevAmsTray>& slots, std::size_t tray_id);
|
||||
|
||||
/**
|
||||
* DevAms - Represents a single AMS (Automatic Material System) unit.
|
||||
*
|
||||
|
||||
@@ -18,14 +18,80 @@
|
||||
|
||||
// TODO: remove this include
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/GuiColor.hpp"
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
// Bambu keeps its fixed external pair (255 main/right, 254 deputy/left) and its two
|
||||
// left/right flags. Every other printer addresses one external virtual tray per live
|
||||
// extruder, so ids 255, 254, 253, ... are extruders 0, 1, 2, ... .
|
||||
static bool _is_bbl_vendor_printer()
|
||||
{
|
||||
return wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
}
|
||||
|
||||
// Extruder a virtual (external spool) tray id feeds: VIRTUAL_TRAY_MAIN_ID - tray_id.
|
||||
// Returns -1 for an empty, non-numeric or out-of-range id.
|
||||
static int _virtual_tray_extruder_id(const std::string& tray_id)
|
||||
{
|
||||
if (tray_id.empty())
|
||||
return -1;
|
||||
for (char c : tray_id)
|
||||
{
|
||||
if (c < '0' || c > '9')
|
||||
return -1;
|
||||
}
|
||||
try
|
||||
{
|
||||
const int vt_id = std::stoi(tray_id);
|
||||
if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
return -1;
|
||||
return VIRTUAL_TRAY_MAIN_ID - vt_id;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// True when obj->vt_slot actually carries this virtual tray id as a reported spool. A synthetic
|
||||
// topology filler (is_slot_placeholder) only makes the slot addressable and does not count.
|
||||
static bool _has_virtual_tray_slot(const MachineObject* obj, const std::string& tray_id)
|
||||
{
|
||||
if (obj == nullptr || tray_id.empty())
|
||||
return false;
|
||||
for (const auto& tray : obj->vt_slot)
|
||||
{
|
||||
if (!tray.is_slot_placeholder && tray.id == tray_id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A generic printer may expose one AMS unit / external slot per extruder. When both legacy
|
||||
// flags of a category are enabled (the sidebar sync call uses both AMS and both external flags)
|
||||
// every extruder participates; when only one side is enabled (SelectMachine's left/right split)
|
||||
// keep to that side's legacy extruder (right = main = 0, left = deputy = 1), so the N slots do
|
||||
// not leak across sides.
|
||||
static bool _generic_extruder_in_selected_side(int extruder_id, bool use_right, bool use_left)
|
||||
{
|
||||
if (use_right && use_left)
|
||||
return true;
|
||||
if (use_right)
|
||||
return extruder_id == MAIN_EXTRUDER_ID;
|
||||
if (use_left)
|
||||
return extruder_id == DEPUTY_EXTRUDER_ID;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DevMappingUtil::is_valid_mapping_result(const MachineObject* obj, std::vector<FilamentInfo>& result, bool check_empty_slot)
|
||||
{
|
||||
if (result.empty()) return false;
|
||||
|
||||
const bool is_bbl_vendor = _is_bbl_vendor_printer();
|
||||
|
||||
for (int i = 0; i < result.size(); i++)
|
||||
{
|
||||
// invalid mapping result
|
||||
@@ -42,8 +108,13 @@ namespace Slic3r
|
||||
auto ams_item = obj->GetFilaSystem()->GetAmsById(result[i].ams_id);
|
||||
if (ams_item == nullptr)
|
||||
{
|
||||
if ((result[i].ams_id != std::to_string(VIRTUAL_TRAY_MAIN_ID)) &&
|
||||
(result[i].ams_id != std::to_string(VIRTUAL_TRAY_DEPUTY_ID)))
|
||||
// A virtual (external spool) tray is not an AMS unit. Bambu keeps its fixed pair;
|
||||
// a generic printer accepts whichever per-extruder slots it actually reported.
|
||||
const bool is_virtual_tray = is_bbl_vendor
|
||||
? (result[i].ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) ||
|
||||
result[i].ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
: _has_virtual_tray_slot(obj, result[i].ams_id);
|
||||
if (!is_virtual_tray)
|
||||
{
|
||||
result[i].tray_id = -1;
|
||||
return false;
|
||||
@@ -94,8 +165,11 @@ namespace Slic3r
|
||||
result.ams_id = std::to_string(ams_id);
|
||||
result.slot_id = std::to_string(slot_id);
|
||||
|
||||
if (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
if (type == DevAms::EXT_SPOOL || ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
// An external spool is addressed by its virtual tray id (255 -> extruder 0, 254 -> 1,
|
||||
// ...); the id is the tray's identity, so it must not fall through to the AMS
|
||||
// ams_id * 4 + slot_id indexing.
|
||||
result.tray_id = atoi(tray.id.c_str());
|
||||
result.id = atoi(tray.id.c_str());
|
||||
}
|
||||
@@ -126,11 +200,25 @@ namespace Slic3r
|
||||
std::map<int, FilamentInfo> tray_filaments; // tray_index : tray_color
|
||||
bool left_nozzle_has_ams = false, right_nozzle_has_ams = false;
|
||||
|
||||
const bool is_bbl_vendor = _is_bbl_vendor_printer();
|
||||
// Extruders whose AMS filament participates in this mapping. On a generic printer this
|
||||
// carries the per-extruder "nozzle already has AMS" state that the legacy right/left flags
|
||||
// express for Bambu, so the matching external spool can be suppressed.
|
||||
std::set<int> generic_extruders_with_ams;
|
||||
|
||||
const auto& ams_list = obj->GetFilaSystem()->GetAmsList();
|
||||
for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++)
|
||||
{
|
||||
std::string ams_id = ams->second->GetAmsId();
|
||||
auto ams_type = ams->second->GetAmsType();
|
||||
const int ams_id_int = atoi(ams->first.c_str());
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : ams_list) {
|
||||
if (atoi(other.first.c_str()) < ams_id_int)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
// GetAmsType() maps mixed -> AMS_LITE; recover the mixed type so N9 trays index at 24+slot.
|
||||
if (ams_type == DevAms::AMS_LITE && ams->second->IsAmsLiteMixed())
|
||||
{
|
||||
@@ -138,10 +226,16 @@ namespace Slic3r
|
||||
}
|
||||
for (auto tray = ams->second->GetTrays().begin(); tray != ams->second->GetTrays().end(); tray++)
|
||||
{
|
||||
int ams_id = atoi(ams->first.c_str());
|
||||
int ams_id = ams_id_int;
|
||||
int tray_id = atoi(tray->first.c_str());
|
||||
int tray_index = 0;
|
||||
if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
|
||||
if (!is_bbl_vendor)
|
||||
{
|
||||
// Generic units can have different lane counts; index all lanes cumulatively,
|
||||
// matching the shared tray-index map and sidebar list builder.
|
||||
tray_index = generic_tray_base + tray_id;
|
||||
}
|
||||
else if (ams_type == DevAms::AMS || ams_type == DevAms::AMS_LITE || ams_type == DevAms::N3F)
|
||||
{
|
||||
tray_index = ams_id * 4 + tray_id;
|
||||
}
|
||||
@@ -169,14 +263,18 @@ namespace Slic3r
|
||||
if (tray->second->is_tray_info_ready())
|
||||
{
|
||||
_parse_tray_info(ams_id, tray_id, ams_type, *(tray->second), info);
|
||||
// Generic unit lanes use one cumulative index across variable-width units;
|
||||
// ams_id * 4 + slot_id is only valid for Bambu's fixed four-slot units.
|
||||
if (!is_bbl_vendor)
|
||||
info.id = tray_index;
|
||||
}
|
||||
|
||||
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
|
||||
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
|
||||
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
|
||||
if (right_ams_valid || left_ams_valid)
|
||||
bool ams_valid = false;
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
tray_filaments.emplace(std::make_pair(tray_index, info));
|
||||
//first: left,nozzle=1,map=1 second: right,nozzle=0,map=2
|
||||
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
|
||||
bool left_ams_valid = (ams->second->GetBindedExtruderSet().count(DEPUTY_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_LEFT_AMS];
|
||||
if (right_ams_valid)
|
||||
{
|
||||
right_nozzle_has_ams = true;
|
||||
@@ -185,6 +283,25 @@ namespace Slic3r
|
||||
{
|
||||
left_nozzle_has_ams = true;
|
||||
}
|
||||
ams_valid = right_ams_valid || left_ams_valid;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is bound to its own extruder, so it takes part whenever the
|
||||
// request covers that extruder.
|
||||
for (int extruder_id : ams->second->GetBindedExtruderSet())
|
||||
{
|
||||
if (_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_AMS], map_opt[MappingOption::USE_LEFT_AMS]))
|
||||
{
|
||||
ams_valid = true;
|
||||
generic_extruders_with_ams.insert(extruder_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ams_valid)
|
||||
{
|
||||
tray_filaments.emplace(std::make_pair(tray_index, info));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,10 +310,21 @@ namespace Slic3r
|
||||
{
|
||||
for (auto tray : obj->vt_slot)
|
||||
{
|
||||
bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
|
||||
bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
|
||||
if (right_ext_valid || left_ext_valid)
|
||||
// A synthetic topology filler is not a reported external spool, so it is never a
|
||||
// mapping candidate. Bambu keeps its fixed reported pair.
|
||||
if (!is_bbl_vendor && tray.is_slot_placeholder)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
// Bambu keeps its fixed pair and the two left/right external flags.
|
||||
const bool right_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID) && map_opt[MappingOption::USE_RIGHT_EXT]);
|
||||
const bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
|
||||
if (!right_ext_valid && !left_ext_valid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (nozzle_has_ams_then_ignore_ext)
|
||||
{
|
||||
if (right_ext_valid && right_nozzle_has_ams)
|
||||
@@ -208,10 +336,31 @@ namespace Slic3r
|
||||
continue;
|
||||
}
|
||||
}
|
||||
FilamentInfo info;
|
||||
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
|
||||
tray_filaments.emplace(std::make_pair(info.tray_id, info));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic: every present virtual tray is the external spool of the extruder it
|
||||
// addresses (VIRTUAL_TRAY_MAIN_ID - tray.id): 255 -> 0, 254 -> 1, ... . Keep to
|
||||
// the selected side when only one legacy flag is enabled, and drop the spool when
|
||||
// its extruder already has AMS filament in this mapping and suppression is on.
|
||||
const int extruder_id = _virtual_tray_extruder_id(tray.id);
|
||||
if (extruder_id < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!_generic_extruder_in_selected_side(extruder_id, map_opt[MappingOption::USE_RIGHT_EXT], map_opt[MappingOption::USE_LEFT_EXT]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (nozzle_has_ams_then_ignore_ext && generic_extruders_with_ams.count(extruder_id) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
FilamentInfo info;
|
||||
_parse_tray_info(atoi(tray.id.c_str()), 0, DevAms::EXT_SPOOL, tray, info);
|
||||
tray_filaments.emplace(std::make_pair(info.tray_id, info));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,4 +561,4 @@ namespace Slic3r
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4134,11 +4134,8 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
continue;
|
||||
|
||||
auto vslot = parse_vt_tray(tray_json);
|
||||
grow_virtual_tray_slots(vt_slot, static_cast<std::size_t>(tray_id));
|
||||
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
|
||||
while (vt_slot.size() <= index) {
|
||||
const int placeholder_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(vt_slot.size());
|
||||
vt_slot.emplace_back(std::to_string(placeholder_id));
|
||||
}
|
||||
vt_slot[index] = vslot;
|
||||
}
|
||||
catch (...) {
|
||||
@@ -4173,6 +4170,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
try {
|
||||
parse_new_info(jj);
|
||||
} catch (...) {}
|
||||
|
||||
// Give every live device extruder an external virtual tray even when the payload
|
||||
// carried no vir_slot/vt_tray data (non-Bambu), so the UI always has a slot per
|
||||
// extruder. Runs after parse_new_info(), which refreshes the extruder count, and
|
||||
// after the vir_slot/vt_tray handling above, which may have cleared the support
|
||||
// flag and would otherwise hide these synthetic slots.
|
||||
ensure_external_virtual_tray_slots();
|
||||
#pragma endregion
|
||||
} else if (jj["command"].get<std::string>() == "gcode_line") {
|
||||
//ack of gcode_line
|
||||
@@ -5278,6 +5282,37 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
||||
return vt_tray;
|
||||
}
|
||||
|
||||
void MachineObject::ensure_external_virtual_tray_slots()
|
||||
{
|
||||
// Without a preset bundle the vendor layout is unknown, and Bambu owns its own fixed pair
|
||||
// (255, 254) reported through vir_slot/vt_tray; in both cases leave vt_slot untouched.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr ||
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int extruder_count = m_extder_system != nullptr ? m_extder_system->GetTotalExtderCount() : 0;
|
||||
if (extruder_count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One slot per live extruder, addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id. Grow with
|
||||
// placeholders and only fill slots that do not exist yet, so a slot already populated from a
|
||||
// reported tray keeps its metadata and the slot -> extruder mapping never shifts.
|
||||
for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
|
||||
const int tray_id = VIRTUAL_TRAY_MAIN_ID - extruder_id;
|
||||
grow_virtual_tray_slots(vt_slot, static_cast<std::vector<DevAmsTray>::size_type>(tray_id));
|
||||
auto& tray = vt_slot[extruder_id];
|
||||
// The constructor seeds id 255 without a placeholder flag. Mark any empty generic slot as
|
||||
// synthetic, but leave a slot carrying reported filament data eligible for sync/mapping.
|
||||
tray.is_slot_placeholder = !tray.is_exists && tray.setting_id.empty() && tray.m_fila_type.empty();
|
||||
}
|
||||
|
||||
// A printer with an external-slot layout supports virtual trays even when the payload carried
|
||||
// no vir_slot/vt_tray data, which would otherwise clear this flag and hide the synthetic slots.
|
||||
ams_support_virtual_tray = true;
|
||||
}
|
||||
|
||||
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
{
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
@@ -950,6 +950,16 @@ public:
|
||||
std::vector<DevAmsTray> vt_slot;
|
||||
DevAmsTray parse_vt_tray(nlohmann::json vtray);
|
||||
|
||||
/**
|
||||
* Give every live device extruder an external virtual tray slot (non-Bambu printers only).
|
||||
*
|
||||
* Slots are addressed by VIRTUAL_TRAY_MAIN_ID - extruder_id, so a slot already reported for an
|
||||
* extruder keeps its tray metadata and sparse/out-of-order reports never shift the slot ->
|
||||
* extruder association. Only missing topology slots are added, so the call is idempotent across
|
||||
* refreshes. Bambu keeps its existing fixed pair reported through vir_slot/vt_tray.
|
||||
*/
|
||||
void ensure_external_virtual_tray_slots();
|
||||
|
||||
/*get ams slot info*/
|
||||
bool contains_tray(const std::string &ams_id, const std::string &tray_id) const;
|
||||
DevAmsTray get_tray(const std::string &ams_id, const std::string &tray_id) const;/*use contains_tray() check first*/
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "../GUI/DeviceCore/DevStorage.h"
|
||||
#include "../GUI/DeviceCore/DevFirmware.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
@@ -583,8 +584,20 @@ void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector<AmsTrayD
|
||||
++ams_id;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: Parsed " << trays.size()
|
||||
<< " trays in " << boxes.size() << " extruder box(es)";
|
||||
size_t has_filament_count = 0;
|
||||
for (const auto& tray : trays) {
|
||||
if (tray.has_filament) ++has_filament_count;
|
||||
}
|
||||
std::ostringstream box_summary;
|
||||
bool first_box = true;
|
||||
for (const auto& [extruder_id, lanes] : boxes) {
|
||||
if (!first_box) box_summary << ", ";
|
||||
box_summary << "ext" << extruder_id << ":" << lanes.size();
|
||||
first_box = false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: input trays " << trays.size()
|
||||
<< " extruders " << boxes.size() << " has_filament " << has_filament_count
|
||||
<< " boxes [" << box_summary.str() << "]";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
@@ -643,9 +656,43 @@ void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array,
|
||||
nlohmann::json print_json = nlohmann::json::object();
|
||||
print_json["ams"] = ams_json;
|
||||
|
||||
// Diagnostic: log the AMS-box boundary input (box count and per-box tray counts) so what the
|
||||
// parser produces can be compared against what the agent actually submitted.
|
||||
std::ostringstream box_trays;
|
||||
for (size_t i = 0; i < ams_array.size(); ++i) {
|
||||
if (i > 0) box_trays << ", ";
|
||||
box_trays << "box" << ams_array[i].value("id", std::string()) << ":"
|
||||
<< ((ams_array[i].contains("tray") && ams_array[i]["tray"].is_array()) ? ams_array[i]["tray"].size() : 0);
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::submit_ams_payload: boundary boxes " << ams_array.size()
|
||||
<< " trays [" << box_trays.str() << "]";
|
||||
|
||||
// Call the parser to populate DevFilaSystem
|
||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
||||
|
||||
// ParseV1_0 handles only the AMS boxes, so grow the external-spool slots directly here too:
|
||||
// pull mode bypasses MachineObject::parse_json(), the only place the push-status vir_slot is
|
||||
// applied. Each Moonraker extruder gets one empty native virtual tray (id = 255 - extruder_id),
|
||||
// kept at its id-derived index so a non-contiguous extruder set does not shift slots. Slots that
|
||||
// already carry reported spool metadata are left untouched (only missing slots are added).
|
||||
const auto extruder_status = collect_extruder_status();
|
||||
for (const auto& entry : extruder_status) {
|
||||
const int extruder_id = entry.first;
|
||||
if (extruder_id < 0 || extruder_id > VIRTUAL_TRAY_MAIN_ID) {
|
||||
continue;
|
||||
}
|
||||
// Address the slot by its virtual tray id (VIRTUAL_TRAY_MAIN_ID - extruder_id), which places
|
||||
// it at vector index extruder_id. Append-only, so a reported spool and the extruder
|
||||
// association survive refreshes.
|
||||
grow_virtual_tray_slots(obj->vt_slot, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
|
||||
auto& tray = obj->vt_slot[extruder_id];
|
||||
tray.is_slot_placeholder = !tray.is_exists && tray.setting_id.empty() && tray.m_fila_type.empty();
|
||||
}
|
||||
if (!extruder_status.empty()) {
|
||||
// A native virtual tray implies external-spool support, matching parse_vt_tray().
|
||||
obj->ams_support_virtual_tray = true;
|
||||
}
|
||||
|
||||
// Set printer_type so update_sync_status() can match it against the preset's printer type.
|
||||
// Without this, the comparison fails and all sync badges are cleared.
|
||||
obj->printer_type = device_info.model_id;
|
||||
@@ -924,14 +971,30 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
const auto& value = json["result"]["value"];
|
||||
trays.clear();
|
||||
max_lane_index = 0;
|
||||
size_t skipped_records = 0;
|
||||
|
||||
// The lane_data namespace carries no extruder field; read the lane -> toolhead binding from the
|
||||
// AFC printer objects so lanes on different toolheads end up in different AMS boxes.
|
||||
std::map<std::string, int> lane_extruder;
|
||||
fetch_afc_lane_extruders(lane_extruder);
|
||||
|
||||
// Happy Hare also writes lane_data, where the lane number is the gate index. Its mmu_machine
|
||||
// status maps gates to units, and each unit's config maps to an extruder. Probe that mapping
|
||||
// only when a lane has no AFC/extruder field, preserving the existing AFC/generic behavior.
|
||||
std::map<int, int> hh_gate_extruder;
|
||||
bool hh_probed = false;
|
||||
auto hh_extruder_for_lane = [&](int lane) {
|
||||
if (!hh_probed) {
|
||||
hh_probed = true;
|
||||
fetch_hh_gate_extruders(hh_gate_extruder);
|
||||
}
|
||||
const auto it = hh_gate_extruder.find(lane);
|
||||
return it != hh_gate_extruder.end() ? it->second : 0;
|
||||
};
|
||||
|
||||
for (const auto& [lane_key, lane_obj] : value.items()) {
|
||||
if (!lane_obj.is_object()) {
|
||||
++skipped_records;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -943,6 +1006,7 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
if (lane_index < 0) {
|
||||
lane_index = lane_index_from_key(lane_key);
|
||||
if (lane_index < 0) {
|
||||
++skipped_records;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -950,13 +1014,16 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = lane_index;
|
||||
// AFC exposes the lane's extruder/toolhead as "extruder_index"; tolerate "extruder_id" as
|
||||
// an alias. lane_data has neither, so fall back to the AFC lane objects and only then to
|
||||
// the main extruder (best effort).
|
||||
tray.extruder_id = safe_json_int_flexible(lane_obj, "extruder_index",
|
||||
safe_json_int_flexible(lane_obj, "extruder_id", -1));
|
||||
// an alias. lane_data has neither, so fall back to the AFC lane objects, then to the Happy
|
||||
// Hare unit layout, and only then to the main extruder (best effort).
|
||||
const int source_extruder = safe_json_int_flexible(lane_obj, "extruder_index",
|
||||
safe_json_int_flexible(lane_obj, "extruder_id", -1));
|
||||
const char* source_extruder_field = lane_obj.contains("extruder_index") ? "extruder_index"
|
||||
: lane_obj.contains("extruder_id") ? "extruder_id" : "none";
|
||||
tray.extruder_id = source_extruder;
|
||||
if (tray.extruder_id < 0) {
|
||||
const auto it = lane_extruder.find(lane_key);
|
||||
tray.extruder_id = it != lane_extruder.end() ? it->second : 0;
|
||||
tray.extruder_id = it != lane_extruder.end() ? it->second : hh_extruder_for_lane(lane_index);
|
||||
}
|
||||
tray.tray_color = safe_json_string(lane_obj, "color");
|
||||
tray.tray_type = safe_json_string(lane_obj, "material");
|
||||
@@ -970,8 +1037,18 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
|
||||
|
||||
max_lane_index = std::max(max_lane_index, lane_index);
|
||||
trays.push_back(tray);
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane " << lane_index
|
||||
<< " src_extruder " << source_extruder_field << "=" << source_extruder
|
||||
<< " mapped_extruder " << tray.extruder_id
|
||||
<< " material " << tray.tray_type
|
||||
<< " color " << tray.tray_color
|
||||
<< " preset_resolved " << (!tray.tray_info_idx.empty());
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: lane_data records " << value.size()
|
||||
<< " parsed " << trays.size() << " skipped " << skipped_records
|
||||
<< " max_lane_index " << max_lane_index;
|
||||
|
||||
if (trays.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found";
|
||||
return false;
|
||||
@@ -1063,8 +1140,12 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
}
|
||||
|
||||
// Parse gate data. Every gate becomes a lane so empty/unknown gates surface
|
||||
// as empty AMS slots rather than vanishing. HH is single-extruder, so all
|
||||
// lanes belong to feed group 0.
|
||||
// as empty AMS slots rather than vanishing. Each gate is grouped by the toolhead its
|
||||
// mmu_machine unit feeds; when the unit layout is unavailable every gate keeps the previous
|
||||
// fallback of feed group 0.
|
||||
std::map<int, int> gate_extruder;
|
||||
fetch_hh_gate_extruders(gate_extruder);
|
||||
|
||||
trays.clear();
|
||||
|
||||
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
||||
@@ -1076,7 +1157,8 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = gate_idx;
|
||||
tray.extruder_id = 0;
|
||||
const auto extruder_it = gate_extruder.find(gate_idx);
|
||||
tray.extruder_id = extruder_it != gate_extruder.end() ? extruder_it->second : 0;
|
||||
|
||||
if (status > 0 && !material.empty()) {
|
||||
tray.tray_type = material;
|
||||
@@ -1101,6 +1183,30 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET a Moonraker path (relative to the device base URL) with the device's API key and parse the
|
||||
// response body. Returns false on a non-200 status or a body that is not valid JSON.
|
||||
bool MoonrakerPrinterAgent::http_get_json(const std::string& path, nlohmann::json& out) const
|
||||
{
|
||||
auto http = Http::get(join_url(device_info.base_url, path));
|
||||
if (!device_info.api_key.empty()) {
|
||||
http.header("X-Api-Key", device_info.api_key);
|
||||
}
|
||||
std::string body;
|
||||
unsigned status_code = 0;
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
.on_complete([&](std::string response, unsigned status) {
|
||||
body = std::move(response);
|
||||
status_code = status;
|
||||
})
|
||||
.perform_sync();
|
||||
if (status_code != 200) {
|
||||
return false;
|
||||
}
|
||||
out = nlohmann::json::parse(body, nullptr, false, true);
|
||||
return !out.is_discarded();
|
||||
}
|
||||
|
||||
// AFC writes its filament data into the lane_data database namespace, and the records of the
|
||||
// fixture/agent write path carry no extruder field, so the lane -> toolhead binding has to be read
|
||||
// from the printer objects: each AFC_lane <name>.extruder names the toolhead its lane feeds, and
|
||||
@@ -1108,29 +1214,8 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
// of the main extruder.
|
||||
bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder)
|
||||
{
|
||||
auto query_objects = [this](const std::string& query_string, nlohmann::json& out) {
|
||||
auto http = Http::get(join_url(device_info.base_url, "/printer/objects/query?" + query_string));
|
||||
if (!device_info.api_key.empty()) {
|
||||
http.header("X-Api-Key", device_info.api_key);
|
||||
}
|
||||
std::string body;
|
||||
unsigned status_code = 0;
|
||||
http.timeout_connect(5)
|
||||
.timeout_max(10)
|
||||
.on_complete([&](std::string response, unsigned status) {
|
||||
body = std::move(response);
|
||||
status_code = status;
|
||||
})
|
||||
.perform_sync();
|
||||
if (status_code != 200) {
|
||||
return false;
|
||||
}
|
||||
out = nlohmann::json::parse(body, nullptr, false, true);
|
||||
return !out.is_discarded();
|
||||
};
|
||||
|
||||
nlohmann::json afc_json;
|
||||
if (!query_objects("AFC", afc_json) || !afc_json.contains("result") ||
|
||||
if (!http_get_json("/printer/objects/query?AFC", afc_json) || !afc_json.contains("result") ||
|
||||
!afc_json["result"].contains("status") || !afc_json["result"]["status"].contains("AFC")) {
|
||||
return false;
|
||||
}
|
||||
@@ -1154,7 +1239,7 @@ bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>&
|
||||
}
|
||||
|
||||
nlohmann::json lanes_json;
|
||||
if (!query_objects(query_string, lanes_json) || !lanes_json.contains("result") ||
|
||||
if (!http_get_json("/printer/objects/query?" + query_string, lanes_json) || !lanes_json.contains("result") ||
|
||||
!lanes_json["result"].contains("status")) {
|
||||
return false;
|
||||
}
|
||||
@@ -1177,6 +1262,111 @@ bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>&
|
||||
return !lane_extruder.empty();
|
||||
}
|
||||
|
||||
// Happy Hare maps its gates to toolheads through mmu_machine units: each unit entry carries
|
||||
// {name, first_gate, num_gates}, and the matching [mmu_unit <name>] config section names the
|
||||
// extruder the unit is attached to. Only Happy Hare publishes mmu_machine, so a generic
|
||||
// lane_data / AFC printer is never reassigned from here. Best effort: returns false and leaves the
|
||||
// map empty when Happy Hare or its unit data is not available.
|
||||
bool MoonrakerPrinterAgent::fetch_hh_gate_extruders(std::map<int, int>& gate_extruder)
|
||||
{
|
||||
gate_extruder.clear();
|
||||
|
||||
nlohmann::json machine_json;
|
||||
if (!http_get_json("/printer/objects/query?mmu_machine", machine_json) ||
|
||||
!machine_json.contains("result") || !machine_json["result"].contains("status") ||
|
||||
!machine_json["result"]["status"].contains("mmu_machine") ||
|
||||
!machine_json["result"]["status"]["mmu_machine"].is_object()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& machine = machine_json["result"]["status"]["mmu_machine"];
|
||||
if (machine.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Happy Hare publishes its config through the configfile object; read every unit's section from
|
||||
// one fetch. An absent or unreadable configfile leaves every unit at the main extruder.
|
||||
nlohmann::json sections;
|
||||
nlohmann::json config_json;
|
||||
if (http_get_json("/printer/objects/query?configfile", config_json) &&
|
||||
config_json.contains("result") && config_json["result"].contains("status") &&
|
||||
config_json["result"]["status"].contains("configfile") &&
|
||||
config_json["result"]["status"]["configfile"].is_object()) {
|
||||
const auto& configfile = config_json["result"]["status"]["configfile"];
|
||||
if (configfile.contains("config") && configfile["config"].is_object()) {
|
||||
sections = configfile["config"];
|
||||
}
|
||||
}
|
||||
|
||||
// One gate range -> extruder resolution per unit, reused for every gate the unit carries.
|
||||
std::map<std::string, int> unit_extruder;
|
||||
|
||||
for (const auto& [unit_key, unit_obj] : machine.items()) {
|
||||
// A unit entry is the one carrying a gate range; skip scalars such as num_units.
|
||||
if (!unit_obj.is_object() || !unit_obj.contains("first_gate") || !unit_obj.contains("num_gates")) {
|
||||
continue;
|
||||
}
|
||||
const int first_gate = safe_json_int_flexible(unit_obj, "first_gate", -1);
|
||||
const int num_gates = safe_json_int_flexible(unit_obj, "num_gates", -1);
|
||||
if (first_gate < 0 || num_gates <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If status omits the unit name, use the unit key as a best-effort config section suffix.
|
||||
std::string unit_name = safe_json_string(unit_obj, "name");
|
||||
if (unit_name.empty() && unit_key.rfind("unit_", 0) == 0) {
|
||||
unit_name = unit_key;
|
||||
}
|
||||
if (unit_name.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = unit_extruder.find(unit_name);
|
||||
if (it == unit_extruder.end()) {
|
||||
it = unit_extruder.emplace(unit_name, hh_unit_extruder_from_config(sections, unit_name)).first;
|
||||
}
|
||||
for (int gate = first_gate; gate < first_gate + num_gates; ++gate) {
|
||||
gate_extruder[gate] = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (gate_extruder.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "MoonrakerPrinterAgent::fetch_hh_gate_extruders: Mapped "
|
||||
<< gate_extruder.size() << " Happy Hare gate(s) across "
|
||||
<< unit_extruder.size() << " unit(s)";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the [mmu_unit <name>] section of a configfile.config map and map its extruder_name to a
|
||||
// toolhead index ("extruder" -> 0, "extruderN" -> N). Happy Hare v4 defaults a missing
|
||||
// extruder_name to the main extruder, so an absent setting and an absent section both fall back to
|
||||
// 0 rather than dropping the unit.
|
||||
int MoonrakerPrinterAgent::hh_unit_extruder_from_config(const nlohmann::json& sections, const std::string& unit_name)
|
||||
{
|
||||
constexpr int default_extruder = 0;
|
||||
|
||||
if (!sections.is_object()) {
|
||||
return default_extruder;
|
||||
}
|
||||
|
||||
// Happy Hare names its unit sections "mmu_unit <name>" (e.g. "mmu_unit unit0").
|
||||
const auto it = sections.find("mmu_unit " + unit_name);
|
||||
if (it == sections.end() || !it->is_object()) {
|
||||
return default_extruder;
|
||||
}
|
||||
|
||||
const std::string extruder_name = safe_json_string(*it, "extruder_name");
|
||||
if (extruder_name.empty()) {
|
||||
return default_extruder;
|
||||
}
|
||||
|
||||
const int extruder_index = extruder_index_from_name(extruder_name);
|
||||
return extruder_index >= 0 ? extruder_index : default_extruder;
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
|
||||
{
|
||||
auto json = nlohmann::json::parse(json_str, nullptr, false);
|
||||
@@ -2017,6 +2207,31 @@ void MoonrakerPrinterAgent::update_status_cache(const nlohmann::json& updates)
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot Moonraker toolhead status objects ("extruder", "extruder1", ...) keyed by extruder id.
|
||||
// An id is the suffix of the object name, so a non-contiguous set keeps its real ids.
|
||||
std::map<int, nlohmann::json> MoonrakerPrinterAgent::collect_extruder_status() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(payload_mutex);
|
||||
std::map<int, nlohmann::json> extruder_status;
|
||||
for (const auto& item : status_cache.items()) {
|
||||
int extruder_id = -1;
|
||||
if (item.key() == "extruder") {
|
||||
extruder_id = 0;
|
||||
} else if (item.key().rfind("extruder", 0) == 0 && item.key().size() > 8 &&
|
||||
std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
extruder_id = std::stoi(item.key().substr(8));
|
||||
} catch (...) {
|
||||
extruder_id = -1;
|
||||
}
|
||||
}
|
||||
if (extruder_id >= 0 && item.value().is_object()) {
|
||||
extruder_status.emplace(extruder_id, item.value());
|
||||
}
|
||||
}
|
||||
return extruder_status;
|
||||
}
|
||||
|
||||
nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
{
|
||||
nlohmann::json payload;
|
||||
@@ -2102,23 +2317,7 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
// MachineObject::parse_new_info() only parses device.extruder when the four
|
||||
// legacy new-protocol fields are present. Moonraker has no equivalents, so
|
||||
// empty values act as compatibility markers without enabling any features.
|
||||
std::map<int, const nlohmann::json*> extruder_status;
|
||||
for (const auto& item : status_cache.items()) {
|
||||
int extruder_id = -1;
|
||||
if (item.key() == "extruder") {
|
||||
extruder_id = 0;
|
||||
} else if (item.key().rfind("extruder", 0) == 0 && item.key().size() > 8 &&
|
||||
std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
extruder_id = std::stoi(item.key().substr(8));
|
||||
} catch (...) {
|
||||
extruder_id = -1;
|
||||
}
|
||||
}
|
||||
if (extruder_id >= 0 && item.value().is_object()) {
|
||||
extruder_status.emplace(extruder_id, &item.value());
|
||||
}
|
||||
}
|
||||
const auto extruder_status = collect_extruder_status();
|
||||
|
||||
if (!extruder_status.empty()) {
|
||||
auto& device_extruder = payload["print"]["device"]["extruder"];
|
||||
@@ -2153,8 +2352,8 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
};
|
||||
|
||||
for (const auto& [extruder_id, status] : extruder_status) {
|
||||
const int current_temperature = encoded_temperature(*status, "temperature");
|
||||
const int target_temperature = encoded_temperature(*status, "target");
|
||||
const int current_temperature = encoded_temperature(status, "temperature");
|
||||
const int target_temperature = encoded_temperature(status, "target");
|
||||
device_extruder["info"].push_back({
|
||||
{"id", extruder_id},
|
||||
{"filam_bak", nlohmann::json::array()},
|
||||
@@ -2172,6 +2371,21 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||
payload["print"]["fun"] = "";
|
||||
payload["print"]["aux"] = "";
|
||||
payload["print"]["stat"] = "";
|
||||
|
||||
// External-spool placeholders. Moonraker reports the extruders but no external-spool
|
||||
// filament, so emit one empty native virtual tray per extruder (id = 255 - extruder_id).
|
||||
// Emitting vir_slot makes the parser grow vt_slot and set ams_support_virtual_tray, giving
|
||||
// every extruder a load slot instead of only the main one. Ids come from the extruder_status
|
||||
// map, so a non-contiguous extruder set keeps its id mapping (the parser fills the holes
|
||||
// with placeholders) rather than shifting every slot down.
|
||||
auto& vir_slot = payload["print"]["vir_slot"];
|
||||
vir_slot = nlohmann::json::array();
|
||||
for (const auto& entry : extruder_status) {
|
||||
if (entry.first < 0 || entry.first > VIRTUAL_TRAY_MAIN_ID) {
|
||||
continue;
|
||||
}
|
||||
vir_slot.push_back({{"id", std::to_string(VIRTUAL_TRAY_MAIN_ID - entry.first)}});
|
||||
}
|
||||
}
|
||||
|
||||
if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) {
|
||||
|
||||
@@ -145,6 +145,8 @@ private:
|
||||
void handle_ws_message(const std::string& dev_id, const std::string& payload);
|
||||
void update_status_cache(const nlohmann::json& updates);
|
||||
nlohmann::json build_print_payload_locked() const;
|
||||
// Snapshot Moonraker "extruder"/"extruderN" status objects from the cache, keyed by extruder id.
|
||||
std::map<int, nlohmann::json> collect_extruder_status() const;
|
||||
|
||||
// Print control helpers
|
||||
int pause_print(const std::string& dev_id);
|
||||
@@ -172,6 +174,15 @@ private:
|
||||
// Lane name -> toolhead index, read from the AFC printer objects (the lane_data database has no
|
||||
// extruder field). Returns false when AFC is not present or the objects cannot be read.
|
||||
bool fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder);
|
||||
// Gate -> toolhead index, read from the Happy Hare mmu_machine units (gate ranges) and each
|
||||
// unit's configured extruder_name. Returns false when Happy Hare is not present.
|
||||
bool fetch_hh_gate_extruders(std::map<int, int>& gate_extruder);
|
||||
// Read the extruder_name of the [mmu_unit <name>] section from a configfile.config map and map
|
||||
// it to a toolhead index. Missing section/setting falls back to the main extruder (Happy Hare's
|
||||
// own default).
|
||||
static int hh_unit_extruder_from_config(const nlohmann::json& sections, const std::string& unit_name);
|
||||
// GET a Moonraker path with the device's API key and parse the JSON body.
|
||||
bool http_get_json(const std::string& path, nlohmann::json& out) const;
|
||||
|
||||
// Build one BBL tray JSON object (placeholder when the tray has no filament).
|
||||
static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -141,6 +142,53 @@ TEST_CASE("Agent filament slots preserve every extruder and AMS binding", "[DevF
|
||||
CHECK(tray_index_map.at(252).second == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("External virtual tray slots cover each extruder without shifting existing data", "[DevFilaSystem]")
|
||||
{
|
||||
std::vector<DevAmsTray> slots;
|
||||
slots.emplace_back("255");
|
||||
slots[0].setting_id = "main-spool";
|
||||
|
||||
// Ensure slots for three extruders, preserving the main spool's reported metadata.
|
||||
for (int extruder_id = 0; extruder_id < 3; ++extruder_id) {
|
||||
grow_virtual_tray_slots(slots, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
|
||||
}
|
||||
|
||||
REQUIRE(slots.size() == 3);
|
||||
CHECK(slots[0].id == "255");
|
||||
CHECK(slots[1].id == "254");
|
||||
CHECK(slots[2].id == "253");
|
||||
CHECK(slots[0].setting_id == "main-spool");
|
||||
|
||||
// Repeating the topology update is idempotent and leaves existing tray data intact.
|
||||
slots[2].setting_id = "third-spool";
|
||||
for (int extruder_id = 0; extruder_id < 3; ++extruder_id) {
|
||||
grow_virtual_tray_slots(slots, static_cast<std::size_t>(VIRTUAL_TRAY_MAIN_ID - extruder_id));
|
||||
}
|
||||
REQUIRE(slots.size() == 3);
|
||||
CHECK(slots[2].setting_id == "third-spool");
|
||||
}
|
||||
|
||||
TEST_CASE("Sparse external spool reports retain their extruder slot IDs", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
const json filament = {
|
||||
{"units", json::array()},
|
||||
{"external", json::array({
|
||||
{{"extruder", 2}, {"loaded", true}, {"material", "PETG"}, {"preset_id", "third-spool"}},
|
||||
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "main-spool"}}
|
||||
})}
|
||||
};
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
REQUIRE(obj.vt_slot.size() == 3);
|
||||
CHECK(obj.vt_slot[0].id == "255");
|
||||
CHECK(obj.vt_slot[0].setting_id == "main-spool");
|
||||
CHECK(obj.vt_slot[1].id == "254");
|
||||
CHECK(obj.vt_slot[2].id == "253");
|
||||
CHECK(obj.vt_slot[2].setting_id == "third-spool");
|
||||
}
|
||||
|
||||
TEST_CASE("Generic AMS tray index map uses cumulative lane counts", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
@@ -392,3 +440,171 @@ TEST_CASE("Switch-bound AMS with an invalid track is excluded from mapping", "[D
|
||||
DevFilaSystemParser::ParseV1_0(print_push, &obj_no_switch, obj_no_switch.GetFilaSystem().get(), false);
|
||||
REQUIRE(obj_no_switch.GetFilaSystem()->GetAmsList().count("0") == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Generic external spools map by their virtual tray id", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// One external spool per extruder: 255 -> extruder 0, 254 -> 1, 253 -> 2, 252 -> 3.
|
||||
const json filament = {
|
||||
{"units", json::array()},
|
||||
{"external", json::array({
|
||||
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}, {"preset_id", "P0"}},
|
||||
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}, {"preset_id", "P1"}},
|
||||
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "8D33FFFF"}, {"preset_id", "P2"}},
|
||||
{{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}, {"preset_id", "P3"}}
|
||||
})}
|
||||
};
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
REQUIRE(obj.vt_slot.size() == 4);
|
||||
REQUIRE(obj.vt_slot[2].id == "253");
|
||||
|
||||
// Matches only extruder 2's external spool (ABS / 8D33FFFF).
|
||||
FilamentInfo fila;
|
||||
fila.id = 0;
|
||||
fila.type = "ABS";
|
||||
fila.color = "8D33FFFF";
|
||||
|
||||
std::vector<FilamentInfo> result;
|
||||
std::vector<bool> map_opt(4, false);
|
||||
map_opt[MappingOption::USE_LEFT_EXT] = true;
|
||||
map_opt[MappingOption::USE_RIGHT_EXT] = true;
|
||||
|
||||
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
|
||||
|
||||
REQUIRE(result.size() == 1);
|
||||
// The external spool keeps its virtual tray id (253), never the AMS ams_id * 4 + slot_id form.
|
||||
CHECK(result[0].tray_id == 253);
|
||||
CHECK(result[0].ams_id == "253");
|
||||
}
|
||||
|
||||
TEST_CASE("Mapping validation accepts only reported generic virtual slots", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
const json filament = {
|
||||
{"units", json::array()},
|
||||
{"external", json::array({
|
||||
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"preset_id", "P0"}},
|
||||
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"preset_id", "P1"}},
|
||||
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"preset_id", "P2"}}
|
||||
})}
|
||||
};
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
REQUIRE(obj.vt_slot.size() == 3);
|
||||
|
||||
FilamentInfo reported;
|
||||
reported.id = 0;
|
||||
reported.tray_id = 253;
|
||||
reported.ams_id = "253"; // extruder 2's external slot, present in vt_slot
|
||||
reported.slot_id = "0";
|
||||
{
|
||||
std::vector<FilamentInfo> result{reported};
|
||||
CHECK(DevMappingUtil::is_valid_mapping_result(&obj, result, false));
|
||||
}
|
||||
|
||||
// A virtual id the printer never reported is rejected and reset.
|
||||
FilamentInfo missing;
|
||||
missing.id = 1;
|
||||
missing.tray_id = 252;
|
||||
missing.ams_id = "252";
|
||||
missing.slot_id = "0";
|
||||
{
|
||||
std::vector<FilamentInfo> result{missing};
|
||||
CHECK_FALSE(DevMappingUtil::is_valid_mapping_result(&obj, result, false));
|
||||
CHECK(result[0].tray_id == -1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Generic AMS units on every extruder join the mapping", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
const std::vector<std::string> materials = {"PLA", "ASA", "ABS", "PETG", "TPU"};
|
||||
const std::vector<std::string> colors = {"FF5733FF", "33C1FFFF", "8D33FFFF", "44AA22FF", "FFAA00FF"};
|
||||
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
for (int extruder = 0; extruder < 5; ++extruder) {
|
||||
json unit = { {"id", std::to_string(extruder)}, {"extruder", extruder}, {"slots", json::array()} };
|
||||
unit["slots"].push_back({{"index", 0}, {"loaded", true}, {"material", materials[extruder]}, {"color", colors[extruder]}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 5);
|
||||
|
||||
// PETG / 44AA22FF is extruder 3's AMS slot, with cumulative generic tray index 3.
|
||||
FilamentInfo fila;
|
||||
fila.id = 0;
|
||||
fila.type = "PETG";
|
||||
fila.color = "44AA22FF";
|
||||
|
||||
// The sidebar sync enables both legacy AMS flags, so every extruder participates.
|
||||
std::vector<FilamentInfo> result;
|
||||
std::vector<bool> map_opt(4, false);
|
||||
map_opt[MappingOption::USE_LEFT_AMS] = true;
|
||||
map_opt[MappingOption::USE_RIGHT_AMS] = true;
|
||||
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
|
||||
REQUIRE(result.size() == 1);
|
||||
CHECK(result[0].ams_id == "3");
|
||||
CHECK(result[0].tray_id == 3);
|
||||
|
||||
// SelectMachine's right-side split passes only the right flag: extruder 3's unit stays out and
|
||||
// nothing in extruder 0's unit matches PETG, so the filament is left unmapped.
|
||||
std::vector<FilamentInfo> split_result;
|
||||
map_opt[MappingOption::USE_LEFT_AMS] = false;
|
||||
DevMappingUtil::ams_filament_mapping(&obj, {fila}, split_result, map_opt, {}, false);
|
||||
REQUIRE(split_result.size() == 1);
|
||||
CHECK(split_result[0].tray_id == -1);
|
||||
}
|
||||
|
||||
TEST_CASE("Generic external spool is suppressed when its extruder has AMS filament", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// One AMS unit on extruder 2 (red ABS) plus one external spool per extruder; extruder 2's
|
||||
// external spool is green ABS, which is what the sliced filament matches exactly.
|
||||
const json filament = {
|
||||
{"units", json::array({
|
||||
{{"id", "2"}, {"extruder", 2}, {"slots", json::array({
|
||||
{{"index", 0}, {"loaded", true}, {"material", "ABS"}, {"color", "FF0000FF"}}
|
||||
})}}
|
||||
})},
|
||||
{"external", json::array({
|
||||
{{"extruder", 0}, {"loaded", true}, {"material", "PLA"}, {"color", "FF5733FF"}},
|
||||
{{"extruder", 1}, {"loaded", true}, {"material", "ASA"}, {"color", "33C1FFFF"}},
|
||||
{{"extruder", 2}, {"loaded", true}, {"material", "ABS"}, {"color", "00FF00FF"}},
|
||||
{{"extruder", 3}, {"loaded", true}, {"material", "PETG"}, {"color", "44AA22FF"}},
|
||||
{{"extruder", 4}, {"loaded", true}, {"material", "TPU"}, {"color", "FFAA00FF"}}
|
||||
})}
|
||||
};
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
REQUIRE(obj.vt_slot.size() == 5);
|
||||
REQUIRE(obj.vt_slot[2].id == "253");
|
||||
|
||||
FilamentInfo fila;
|
||||
fila.id = 0;
|
||||
fila.type = "ABS";
|
||||
fila.color = "00FF00FF";
|
||||
|
||||
std::vector<bool> map_opt(4, false);
|
||||
map_opt[MappingOption::USE_LEFT_AMS] = true;
|
||||
map_opt[MappingOption::USE_RIGHT_AMS] = true;
|
||||
map_opt[MappingOption::USE_LEFT_EXT] = true;
|
||||
map_opt[MappingOption::USE_RIGHT_EXT] = true;
|
||||
|
||||
// With suppression off the exact-match external spool (253) wins over the red AMS tray.
|
||||
std::vector<FilamentInfo> result;
|
||||
DevMappingUtil::ams_filament_mapping(&obj, {fila}, result, map_opt, {}, false);
|
||||
REQUIRE(result.size() == 1);
|
||||
CHECK(result[0].tray_id == 253);
|
||||
CHECK(result[0].ams_id == "253");
|
||||
|
||||
// With suppression on, extruder 2 already has AMS filament in this mapping, so its external
|
||||
// spool is dropped and the AMS tray is the only candidate left. This fixture has only that
|
||||
// single generic unit, so its cumulative tray index is 0.
|
||||
std::vector<FilamentInfo> suppressed;
|
||||
DevMappingUtil::ams_filament_mapping(&obj, {fila}, suppressed, map_opt, {}, true);
|
||||
REQUIRE(suppressed.size() == 1);
|
||||
CHECK(suppressed[0].ams_id == "2");
|
||||
CHECK(suppressed[0].tray_id == 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user