mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 20:31:14 +00:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f38f2bddec | ||
|
|
1d291f6f04 | ||
|
|
5390b789f1 | ||
|
|
f68fbfed58 | ||
|
|
7ed4095b20 | ||
|
|
ee1cc1b227 | ||
|
|
46096416cd | ||
|
|
74fe2b7996 | ||
|
|
38f649fc00 | ||
|
|
23f6771dcb | ||
|
|
aebc0a2979 | ||
|
|
6f0ec60d21 | ||
|
|
8e43a2ac13 | ||
|
|
6b366ad8a9 | ||
|
|
7d8261668e | ||
|
|
00127beffa | ||
|
|
927eb9922c | ||
|
|
3d62769664 | ||
|
|
e5e30ee0e8 | ||
|
|
7515fb480f | ||
|
|
acc1d6fd21 | ||
|
|
6ca0fc5ef1 | ||
|
|
bcf7d078cd | ||
|
|
15fc96fca7 | ||
|
|
1fffda0533 | ||
|
|
33e26a127e | ||
|
|
6dd0cb9eba | ||
|
|
9bf0fc2a8e | ||
|
|
5bf6db95b3 | ||
|
|
0feb87b417 |
@@ -0,0 +1,233 @@
|
||||
# Multi-extruder and multi-AMS device control
|
||||
|
||||
`DeviceManager`/`MachineObject` model the toolheads, AMS units, and filament slots of a connected
|
||||
printer and drive the device-control UI from that model. The model represents the counts as
|
||||
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
|
||||
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
|
||||
code paths. The printer agent is responsible for translating its printer's state into the shared
|
||||
JSON dialect this model consumes; the agent boundary is described in
|
||||
[printer-agent.md](printer-agent.md), and this document describes the dialect and the model.
|
||||
|
||||
## Model layout
|
||||
|
||||
`MachineObject` owns the device subsystems that this document covers:
|
||||
|
||||
- `DevExtderSystem` — the toolheads (extruders) and their per-toolhead filament/temperature state.
|
||||
- `DevFilaSystem` — the AMS units (`DevAms`) and their trays (`DevAmsTray`).
|
||||
- `DevNozzleSystem` — the nozzles and, on rack printers, `DevNozzleRack`.
|
||||
- `DevFilaSwitch` — a filament switch that can feed several AMS units to more than one extruder.
|
||||
|
||||
Each subsystem exposes its own count — `DevExtderSystem::GetTotalExtderCount()`,
|
||||
`DevFilaSystem::GetAmsCount()`, `DevAms::GetSlotCount()` — and the rest of the application derives
|
||||
its layout from those values rather than from fixed assumptions.
|
||||
|
||||
## Wire dialect
|
||||
|
||||
The dialect is the Bambu-shaped JSON that `DeviceManager` and `MachineObject` already consume. An
|
||||
agent whose printer speaks something else is expected to translate into it; the agent is not free
|
||||
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
|
||||
|
||||
### Extruders
|
||||
|
||||
The generic representation is `print.device.extruder`, parsed by
|
||||
`ExtderSystemParser::ParseV2_0` (`DevExtruderSystem.cpp`). It has a packed `state` integer and an
|
||||
`info` array with one object per toolhead:
|
||||
|
||||
```text
|
||||
device.extruder.state packed integer
|
||||
bits 0..3 total extruder count
|
||||
bits 4..7 current extruder id
|
||||
bits 8..11 target extruder id
|
||||
bits 12..14 switch state
|
||||
bits 15..18 currently loading extruder id
|
||||
bit 19 busy for loading
|
||||
|
||||
device.extruder.info[] one entry per toolhead
|
||||
id extruder id
|
||||
filam_bak array of backup-group bits
|
||||
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
|
||||
temp bits 0..15 current temperature, bits 16..31 target temperature
|
||||
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
|
||||
snow same packing (current)
|
||||
star same packing (target)
|
||||
stat bit 0..15 AMS status, bits 16..31 RFID status
|
||||
hnow current nozzle id
|
||||
```
|
||||
|
||||
The legacy single/dual representation is the pair of scalar fields `nozzle_temper` /
|
||||
`nozzle_target_temper`, plus the `print.ams.tray_tar` / `tray_now` tray pointer, parsed by
|
||||
`ExtderSystemParser::ParseV1_0`. That parser is a fallback and only fills the main extruder when the
|
||||
reported toolhead count is one.
|
||||
|
||||
Physical extruder ids are fixed (`MAIN_EXTRUDER_ID = 0`, `DEPUTY_EXTRUDER_ID = 1`). Bambu printers
|
||||
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
|
||||
printers use the toolhead id itself as the logical id. `DevNozzle::GetLogicExtruderId()` /
|
||||
`GetExtruderId()` centralise that distinction.
|
||||
|
||||
### AMS units and slots
|
||||
|
||||
AMS units arrive as `print.ams.ams[]` and are parsed by `DevFilaSystemParser::ParseV1_0`. Each unit
|
||||
carries an `id` and an `info` bitfield:
|
||||
|
||||
```text
|
||||
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
|
||||
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
|
||||
info bits 24..27 switch inlet when the unit is switch-bound
|
||||
```
|
||||
|
||||
`DevAmsType` (`DevDefs.h`) enumerates the resulting unit kinds: `EXT_SPOOL`, `AMS`, `AMS_LITE`,
|
||||
`N3F`, `N3S`, and the `AMS_LITE_MIXED` N9 variant. `AMS_LITE_MIXED` reports itself as `AMS_LITE` to
|
||||
generic callers; code that needs the distinction uses `IsAmsLiteMixed()`.
|
||||
|
||||
A unit whose bound-extruder nibble is `0xE` is switch-bound: it is kept only when a `DevFilaSwitch`
|
||||
is installed, in which case its extruder id is pinned to the main extruder and its binding set
|
||||
becomes `{0, 1}`; otherwise the unit is dropped from the AMS list.
|
||||
|
||||
Each unit owns a map of `DevAmsTray` keyed by the tray id from the payload. The slot count is a
|
||||
property of the unit kind and the reported trays:
|
||||
|
||||
- `AMS`, `AMS_LITE`, and `N3F` report four slots on Bambu printers, matching the firmware contract;
|
||||
- `N3S` reports one slot;
|
||||
- `EXT_SPOOL` and any non-Bambu unit report the number of trays actually present.
|
||||
|
||||
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous
|
||||
within a unit, because the generic tray-index mapping below is cumulative.
|
||||
|
||||
### External spool slots
|
||||
|
||||
External/manual spools are virtual trays in `MachineObject::vt_slot`, separate from the trays owned
|
||||
by `DevFilaSystem`. Their stable id is `vt_id = VIRTUAL_TRAY_MAIN_ID - extruder_id`: 255 for
|
||||
extruder 0, 254 for extruder 1, then 253, 252, and so on. The index in `vt_slot` is therefore
|
||||
`VIRTUAL_TRAY_MAIN_ID - vt_id`, not the order in which a printer happens to report trays.
|
||||
|
||||
For non-Bambu printers, slot topology comes from the live `DevExtderSystem` extruder count, not from
|
||||
external-spool telemetry. `MachineObject::ensure_external_virtual_tray_slots()` grows `vt_slot` to
|
||||
one entry per extruder and enables virtual-tray support even when the agent reports no `vir_slot`,
|
||||
`vt_tray`, or external-spool data. It only adds missing entries, so reported tray metadata is kept.
|
||||
The shared ensure runs after push-status parsing has updated the extruder count and after the generic
|
||||
AMS/agent-filament parsers. Thus agents may report external-spool metadata, but do not have to invent
|
||||
empty slots just to make each toolhead's external-spool row visible.
|
||||
|
||||
When a payload does report virtual trays, `vir_slot` entries are placed by their id and missing
|
||||
indices are filled with placeholders. Generic agent `external` entries are likewise placed by
|
||||
extruder id, so sparse or out-of-order reports do not shift a spool onto another toolhead. An empty
|
||||
placeholder represents the slot's existence, not a claim that the printer reported a loaded spool.
|
||||
Bambu keeps its fixed 255/254 virtual-tray pair and does not use generic slot synthesis.
|
||||
|
||||
## Mapping
|
||||
|
||||
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
|
||||
index. Bambu mapping computes `ams_id * 4 + slot_id` for its fixed-width units; generic mapping in
|
||||
`DevFilaSystem` and the sidebar matcher uses the count of lanes in preceding units plus the slot id.
|
||||
The two agree for Bambu units and differ for a non-Bambu unit whose slot count is not four.
|
||||
|
||||
### AMS id to extruder id
|
||||
|
||||
`DevFilaSystem::GetExtruderIdByAmsId()` resolves an AMS id to the extruder it feeds:
|
||||
|
||||
- an AMS unit returns its bound extruder (`DevAms::GetExtruderId()`);
|
||||
- a virtual tray returns `VIRTUAL_TRAY_MAIN_ID - vt_id`;
|
||||
- Bambu's virtual AMS ids map to the main/deputy extruder.
|
||||
|
||||
A unit may be bound to more than one extruder when a `DevFilaSwitch` feeds it; the binding set
|
||||
(`DevAms::m_binded_extruder_set`) and switch inlet are read by switch-aware code while the
|
||||
single-extruder id stays pinned for legacy callers.
|
||||
|
||||
### Tray index
|
||||
|
||||
`DevFilaSystem::GetTrayIndexMap()` maps a global tray index to an `(ams_id, slot_id)` pair and is
|
||||
used for backup-slot translation, calibration addressing, and the mapping popups.
|
||||
|
||||
- Bambu AMS: `ams_id * 4 + slot_id`; N3S: the AMS id; AMS-Lite-mixed (N9): `24 + slot_id`.
|
||||
- Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot
|
||||
id. This depends on each unit's tray ids being 0-based and contiguous.
|
||||
|
||||
The sidebar sync matcher uses the same identities. Bambu retains its fixed left/right AMS and
|
||||
external-spool choices. For a generic printer, a full sync considers AMS units according to their
|
||||
bound extruder ids and considers every external slot by `255 - vt_id`; a split left/right mapping
|
||||
still restricts itself to the corresponding legacy extruders 0 and 1. Generic AMS lanes use the
|
||||
cumulative tray index above, while their `ams_id`/`slot_id` pair remains the address used to update
|
||||
the matching sidebar entry. The runtime filament-list configs also carry a transient device-extruder
|
||||
association so generic entries are grouped under the correct toolhead after sync; that association
|
||||
is removed before the configs enter the slicing pipeline. Bambu keeps the existing key-based
|
||||
left/right grouping.
|
||||
|
||||
### Virtual-slot recognition
|
||||
|
||||
`devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it
|
||||
is the fixed pair `{255, 254}`. On generic printers it is the descending range
|
||||
`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. Slot
|
||||
topology is grown from the live device extruder count, while this recognition range still uses the
|
||||
preset count; the preset is expected to match the connected device. A mismatch can therefore make
|
||||
the recognized virtual-id range disagree with `vt_slot`, and is reported by the existing count
|
||||
warning rather than silently accepted.
|
||||
|
||||
## Device-control UI
|
||||
|
||||
The device-control widgets follow the same device-reported counts.
|
||||
|
||||
- `AMSModel` (`AMSItem.hpp`) mirrors `DevAmsType` numerically: `EXT_AMS`, `GENERIC_AMS`, `AMS_LITE`,
|
||||
`N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`.
|
||||
- On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane
|
||||
rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering.
|
||||
- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one
|
||||
toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS
|
||||
simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the
|
||||
legacy left/right presentation; that is deliberate.
|
||||
- The status panel builds external-spool entries from `MachineObject::vt_slot`; generic slot
|
||||
synthesis keeps one empty entry available for each live toolhead even when the printer does not
|
||||
report external-spool state.
|
||||
- The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to
|
||||
`GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through
|
||||
`m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right
|
||||
`m_nozzle_btn_panel`. Nozzle temperature controls are created on demand per toolhead.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Single dialect.** The agent translates its protocol into this JSON; the model does not learn
|
||||
vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
|
||||
- **Runtime counts.** The agent reports toolhead count and AMS unit/tray data. The shared model derives
|
||||
generic external-spool slot count from the parsed live toolhead count; nothing caps the number of
|
||||
toolheads or generic slots except the id range and the Bambu contract described above.
|
||||
- **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of
|
||||
`info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the
|
||||
parser places toolheads by array position while `GetExtderById()` indexes by id.
|
||||
- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an
|
||||
independent per-toolhead road state. A generic scaffold that allowed the latter was removed as
|
||||
unused; the one-extruder-per-toolhead road is the intended model.
|
||||
- **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the
|
||||
selected printer preset. A printer preset must therefore declare the same number of
|
||||
`nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning
|
||||
(`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and
|
||||
extruder logic would otherwise silently disagree.
|
||||
- **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed
|
||||
virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do
|
||||
not alter it.
|
||||
- **Tray-index invariant.** The generic tray-index map is cumulative, so payload tray ids must be
|
||||
unique, 0-based, and contiguous per unit. A malformed id is a producer error.
|
||||
- **Protocol markers.** The dialect has no neutral equivalent for some Bambu state, so agents that
|
||||
lack it send empty `print.cfg`, `print.fun`, `print.aux`, and `print.stat`. Their presence selects
|
||||
the new-protocol parse path; they are compatibility markers, not feature switches.
|
||||
|
||||
## Main implementation locations
|
||||
|
||||
- [`DevExtruderSystem`](../../src/slic3r/GUI/DeviceCore/DevExtruderSystem.h) — toolhead model and the
|
||||
`device.extruder` parser
|
||||
- [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser,
|
||||
and the mapping helpers
|
||||
- [`DevMapping.cpp`](../../src/slic3r/GUI/DeviceCore/DevMapping.cpp) — filament-to-device tray matching
|
||||
for Bambu and generic external/AMS slots
|
||||
- [`PresetBundle.cpp`](../../src/libslic3r/PresetBundle.cpp) — per-extruder grouping of the transient
|
||||
device filament list
|
||||
- [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual
|
||||
tray ids
|
||||
- [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot`
|
||||
parsing, external-slot topology, and device state assembly
|
||||
- [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes
|
||||
(`CreateAmsMultiNozzle`) and the generic layout
|
||||
- [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS
|
||||
rendering
|
||||
- [`StatusPanel.cpp`](../../src/slic3r/GUI/StatusPanel.cpp) — `ExtruderImage`, the toolhead selector,
|
||||
and per-toolhead temperature controls
|
||||
- [`MoonrakerPrinterAgent.cpp`](../../src/slic3r/Utils/MoonrakerPrinterAgent.cpp) — producer example
|
||||
(AMS payload and the `device.extruder` encoding)
|
||||
+162
-53
@@ -4511,6 +4511,53 @@ static void warn_ambiguous_filament_id_match(const PresetCollection &filaments,
|
||||
<< "\", also matches " << others;
|
||||
}
|
||||
|
||||
// Orca: resolve a compatible system preset from a printer-reported filament type. Prefers an exact
|
||||
// "Generic <type>" match, then the longest generic whose filament_type appears as a whole word in
|
||||
// the reported type (e.g. "ASA" in "ASA Sparkle"). On success `generic_name_out` holds the
|
||||
// "Generic <...>" name used for compatibility messages. Shared by the known-filament_id lookup and
|
||||
// the data-bearing-but-unmapped tray path so both pick the same preset.
|
||||
static PresetCollection::Iterator find_generic_preset_for_type(PresetCollection &filaments, const std::string &filament_type, std::string &generic_name_out)
|
||||
{
|
||||
if (filament_type.empty())
|
||||
return filaments.end();
|
||||
const std::string generic_name = "Generic " + filament_type;
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [&generic_name](const Preset &f) {
|
||||
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, generic_name);
|
||||
});
|
||||
if (iter != filaments.end()) {
|
||||
generic_name_out = generic_name;
|
||||
return iter;
|
||||
}
|
||||
// Similarity fallback: find a generic preset whose filament_type
|
||||
// appears as a whole word in the reported type (e.g. "ASA" in "ASA Sparkle").
|
||||
auto upper_type = boost::to_upper_copy(filament_type);
|
||||
auto contains_word = [](const std::string& haystack, const std::string& needle) {
|
||||
auto pos = haystack.find(needle);
|
||||
while (pos != std::string::npos) {
|
||||
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
|
||||
bool end_ok = (pos + needle.size() >= haystack.size() ||
|
||||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
|
||||
if (start_ok && end_ok)
|
||||
return true;
|
||||
pos = haystack.find(needle, pos + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
|
||||
size_t best_len = 0;
|
||||
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
|
||||
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
|
||||
continue;
|
||||
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
|
||||
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
|
||||
iter = it;
|
||||
best_len = preset_type.size();
|
||||
generic_name_out = "Generic " + it->config.opt_string("filament_type", 0u);
|
||||
}
|
||||
}
|
||||
return iter;
|
||||
}
|
||||
|
||||
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
{
|
||||
combox_info.clear();
|
||||
@@ -4566,7 +4613,9 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||
|
||||
unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfig *,std::string>> &unknowns, bool use_map, std::map<int, AMSMapInfo> &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": use_map %1% enable_append %2% color_only %3% start_presets %4% start_colors %5%")
|
||||
% use_map % enable_append % color_only % this->filament_presets.size()
|
||||
% project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
|
||||
std::vector<std::string> ams_filament_presets;
|
||||
std::vector<std::string> ams_filament_colors;
|
||||
std::vector<std::string> ams_filament_color_types;
|
||||
@@ -4578,6 +4627,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
bool valid{false};
|
||||
bool is_map{false};
|
||||
bool is_placeholder{false};
|
||||
// Orca: set for a tray that reports filament data (type and/or loaded flag) but carries
|
||||
// no preset id. It is not `valid` (no id to map through), yet it still holds a real spool
|
||||
// and is eligible for the same append step as a mapped tray.
|
||||
bool has_reported_data{false};
|
||||
std::string filament_color = "";
|
||||
std::string filament_color_type = "";
|
||||
std::string filament_preset = "";
|
||||
@@ -4591,12 +4644,32 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
auto filament_id = ams.opt_string("filament_id", 0u);
|
||||
auto filament_color = ams.opt_string("filament_colour", 0u);
|
||||
auto filament_color_type = ams.opt_string("filament_colour_type", 0u);
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto filament_exist = ams.has("filament_exist") && ams.opt_bool("filament_exist", 0u);
|
||||
auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
|
||||
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
|
||||
auto ams_id = ams.opt_string("ams_id", 0u);
|
||||
auto slot_id = ams.opt_string("slot_id", 0u);
|
||||
auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
|
||||
ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color});
|
||||
// Real reported filament data: the device named a type or flagged the slot loaded. A
|
||||
// topology-only synthetic virtual tray carries neither, so it must not become a filament.
|
||||
bool has_reported_data = !is_placeholder && filament_id.empty() && (!filament_type.empty() || filament_exist);
|
||||
AmsInfo ams_info;
|
||||
ams_info.valid = !filament_id.empty();
|
||||
ams_info.is_placeholder = is_placeholder;
|
||||
ams_info.has_reported_data = has_reported_data;
|
||||
ams_info.filament_color = filament_color;
|
||||
// Only the append path of a data-bearing, id-less tray reads these; the known-id path keeps
|
||||
// taking its color and color type from ams_filament_colors/ams_filament_color_types.
|
||||
if (has_reported_data) {
|
||||
if (ams_info.filament_color.empty())
|
||||
ams_info.filament_color = "#CECECE";
|
||||
ams_info.filament_color_type = filament_color_type.empty() ? "1" : filament_color_type;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% valid %7% placeholder %8% reported_data %9% changed %10%")
|
||||
% index % ams_id % slot_id % filament_id % filament_type % filament_color
|
||||
% (!filament_id.empty()) % is_placeholder % has_reported_data % filament_changed;
|
||||
ams_infos.push_back(std::move(ams_info));
|
||||
AMSMapInfo temp = {ams_id, slot_id};
|
||||
ams_array_maps.push_back(temp);
|
||||
index++;
|
||||
@@ -4607,20 +4680,55 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
maps.erase(j);
|
||||
}
|
||||
}
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
} else if (is_placeholder) {
|
||||
// Orca: push placeholders to keep index alignment with ams_infos
|
||||
}
|
||||
if (is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
|
||||
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
|
||||
// drops it so no filament slot is created or retained for an empty tray.
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else if (use_map) {
|
||||
if (has_reported_data) {
|
||||
// Orca: the tray reports a filament but no preset id, so it never reaches the
|
||||
// filament_id lookup. Resolve the same generic preset the id path would pick
|
||||
// from the reported type and keep the reported color; the append step below (if
|
||||
// enabled) then turns it into a sidebar filament. Without a generic match the
|
||||
// slot is dropped rather than invented, exactly like an empty tray.
|
||||
std::string resolved_generic;
|
||||
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
|
||||
if (generic == filaments.end()) {
|
||||
// Reported loaded but no usable type: fall back to the first compatible
|
||||
// generic, matching the id path's fallback for an unknown model.
|
||||
generic = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
|
||||
return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, "Generic ");
|
||||
});
|
||||
}
|
||||
if (generic != filaments.end()) {
|
||||
auto append_color = filament_color.empty() ? std::string("#CECECE") : filament_color;
|
||||
ams_filament_presets.push_back(generic->name);
|
||||
ams_filament_colors.push_back(append_color);
|
||||
ams_filament_color_types.push_back(filament_color_type.empty() ? "1" : filament_color_type);
|
||||
if (filament_multi_color.size() == 0)
|
||||
filament_multi_color.push_back(append_color);
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
continue;
|
||||
}
|
||||
ams_filament_presets.push_back("");
|
||||
ams_filament_colors.push_back("");
|
||||
ams_filament_color_types.push_back("");
|
||||
ams_multi_color_filment.push_back({});
|
||||
} else {
|
||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
||||
auto default_unknown_color = "#CECECE";
|
||||
ams_filament_colors.push_back(default_unknown_color);
|
||||
ams_filament_color_types.push_back("1");
|
||||
if (filament_multi_color.size() == 0) {
|
||||
filament_multi_color.push_back(default_unknown_color);
|
||||
}
|
||||
ams_multi_color_filment.push_back(filament_multi_color);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -4632,49 +4740,19 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
continue;
|
||||
}
|
||||
bool has_type = false;
|
||||
auto filament_type = ams.opt_string("filament_type", 0u);
|
||||
auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id, &has_type, filament_type](auto &f) {
|
||||
has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
|
||||
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||
if (iter == filaments.end()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||
if (!filament_type.empty()) {
|
||||
auto original_type = filament_type;
|
||||
std::string resolved_generic;
|
||||
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
|
||||
if (!filament_type.empty())
|
||||
filament_type = "Generic " + filament_type;
|
||||
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
|
||||
return f.is_compatible && f.is_system
|
||||
&& boost::algorithm::starts_with(f.name, filament_type);
|
||||
});
|
||||
if (iter == filaments.end()) {
|
||||
// Similarity fallback: find a generic preset whose filament_type
|
||||
// appears as a whole word in the AMS type (e.g. "ASA" in "ASA Sparkle").
|
||||
auto upper_type = boost::to_upper_copy(original_type);
|
||||
auto contains_word = [](const std::string& haystack, const std::string& needle) {
|
||||
auto pos = haystack.find(needle);
|
||||
while (pos != std::string::npos) {
|
||||
bool start_ok = (pos == 0 || !std::isalnum(static_cast<unsigned char>(haystack[pos - 1])));
|
||||
bool end_ok = (pos + needle.size() >= haystack.size() ||
|
||||
!std::isalnum(static_cast<unsigned char>(haystack[pos + needle.size()])));
|
||||
if (start_ok && end_ok)
|
||||
return true;
|
||||
pos = haystack.find(needle, pos + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
|
||||
size_t best_len = 0;
|
||||
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
|
||||
if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
|
||||
continue;
|
||||
auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
|
||||
if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
|
||||
iter = it;
|
||||
best_len = preset_type.size();
|
||||
filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generic != filaments.end()) {
|
||||
iter = generic;
|
||||
filament_type = resolved_generic;
|
||||
}
|
||||
if (iter == filaments.end()) {
|
||||
// Prefer old selection
|
||||
@@ -4834,6 +4912,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": color_only mapped_any %1% maps %2% entries %3%")
|
||||
% mapped_any % maps.size() % ams_infos.size();
|
||||
filament_color->values = exist_colors;
|
||||
ams_multi_color_filment = exist_multi_color_filment;
|
||||
merge_info.merges.clear();
|
||||
@@ -4893,13 +4973,18 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < ams_infos.size(); i++) {// check append
|
||||
if (ams_infos[i].valid) {
|
||||
// A mapped tray (valid id) and a data-bearing tray without an id both append the same
|
||||
// way; only a tray with neither contributes nothing.
|
||||
if (ams_infos[i].valid || ams_infos[i].has_reported_data) {
|
||||
if (i >= ams_filament_presets.size()) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)";
|
||||
continue;
|
||||
}
|
||||
ams_infos[i].filament_preset = ams_filament_presets[i];
|
||||
ams_infos[i].mutli_filament_color = ams_multi_color_filment[i];
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% mapped %2% preset %3% color %4% valid %5% reported_data %6%")
|
||||
% i % ams_infos[i].is_map % ams_infos[i].filament_preset % ams_infos[i].filament_color
|
||||
% ams_infos[i].valid % ams_infos[i].has_reported_data;
|
||||
if (!ams_infos[i].is_map) {
|
||||
need_append_colors.emplace_back(ams_infos[i]);
|
||||
ams_filament_colors[i] = "";
|
||||
@@ -4909,6 +4994,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% dropped (no valid id or reported data)") % i;
|
||||
ams_filament_colors[i] = "";
|
||||
ams_filament_color_types[i] = "";
|
||||
ams_filament_presets[i] = "";
|
||||
@@ -4926,7 +5012,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
[](std::vector<std::string> &value) { return value.empty(); }),
|
||||
ams_multi_color_filment.end());
|
||||
if (need_append_colors.size() > 0 && enable_append) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append";
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append candidates %1% (start existing %2%)") % need_append_colors.size() % exist_filament_presets.size();
|
||||
auto get_idx_in_array = [](std::vector<std::string> &presets, std::vector<std::string> &colors, const std::string &preset, const std::string &color) -> int {
|
||||
for (size_t i = 0; i < presets.size(); i++) {
|
||||
if (presets[i] == preset && colors[i] == color) {
|
||||
@@ -4937,17 +5023,22 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
};
|
||||
for (size_t i = 0; i < need_append_colors.size(); i++){
|
||||
if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append capped at %1%, %2% candidate(s) left") % MAXIMUM_AMS_SYNC_FILAMENT_NUMBER % (need_append_colors.size() - i);
|
||||
break;
|
||||
}
|
||||
auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color);
|
||||
if (idx >= 0) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append dedup preset %1% color %2% already at %3%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % idx;
|
||||
continue;
|
||||
}
|
||||
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
||||
exist_colors.push_back(need_append_colors[i].filament_color);
|
||||
exist_color_types.push_back(need_append_colors[i].filament_color_type);
|
||||
exist_multi_color_filment.push_back(need_append_colors[i].mutli_filament_color);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": appended preset %1% color %2% type %3% -> slot %4%") % need_append_colors[i].filament_preset % need_append_colors[i].filament_color % need_append_colors[i].filament_color_type % (exist_filament_presets.size() - 1);
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": append skipped candidates %1% enable_append %2%") % need_append_colors.size() % enable_append;
|
||||
}
|
||||
filament_color->values = exist_colors;
|
||||
filament_color_type->values = exist_color_types;
|
||||
@@ -4984,6 +5075,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
result_multi_colors.push_back(
|
||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||
: std::vector<std::string>{ams_filament_colors[i]});
|
||||
} else if (i < ams_infos.size() && ams_infos[i].is_placeholder) {
|
||||
// Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of
|
||||
// keeping or filling a slot for it. The device AMS panel still shows the empty tray.
|
||||
continue;
|
||||
} else if (i < exist_presets.size()) {
|
||||
// Empty tray or beyond tray count: keep existing filament
|
||||
result_colors.push_back(exist_colors[i]);
|
||||
@@ -5008,8 +5103,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
filament_color_type->values = result_color_types;
|
||||
this->filament_presets = result_presets;
|
||||
ams_multi_color_filment = result_multi_colors;
|
||||
filament_map->values.resize(total, 1);
|
||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
filament_map->values.resize(result_presets.size(), 1);
|
||||
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
} else {
|
||||
// BBL: existing wholesale replace
|
||||
filament_color->values = ams_filament_colors;
|
||||
@@ -5059,7 +5154,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
||||
// Update ams_multi_color_filment
|
||||
update_filament_multi_color();
|
||||
update_multi_material_filament_presets();
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish sync ams list";
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finish sync ams list, end_presets %1% end_colors %2%") % this->filament_presets.size() % filament_color->values.size();
|
||||
return this->filament_presets.size();
|
||||
}
|
||||
|
||||
@@ -5138,6 +5233,20 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
|
||||
if (extruder_nums > 1) {
|
||||
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
|
||||
for (auto ams_item : filament_ams_list) {
|
||||
// A generic slot carries the extruder it actually belongs to, so it lands in that
|
||||
// extruder's bucket instead of the legacy left/right pair. Bambu entries (and anything
|
||||
// without the field) keep the existing 0x10000 key convention.
|
||||
const bool has_extruder_id = ams_item.second.has("device_tray_extruder_id");
|
||||
const int entry_extruder = has_extruder_id ? ams_item.second.opt_int("device_tray_extruder_id") : -1;
|
||||
if (has_extruder_id) {
|
||||
if (entry_extruder >= 0 && entry_extruder < extruder_nums) {
|
||||
// Drop the transient association before the config reaches the slicing pipeline.
|
||||
DynamicPrintConfig entry = ams_item.second;
|
||||
entry.erase("device_tray_extruder_id");
|
||||
filament_infos[entry_extruder].push_back(std::move(entry));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ams_item.first & 0x10000) { // right
|
||||
filament_infos[1].push_back(ams_item.second);
|
||||
} else { // left
|
||||
|
||||
@@ -163,8 +163,8 @@ public:
|
||||
~devPrinterUtil() = delete;
|
||||
|
||||
public:
|
||||
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
|
||||
static bool IsVirtualSlot(const std::string& ams_id) { return (ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR); }
|
||||
static bool IsVirtualSlot(int ams_id);
|
||||
static bool IsVirtualSlot(const std::string& ams_id);
|
||||
};
|
||||
|
||||
namespace GUI
|
||||
@@ -198,3 +198,6 @@ template<> struct std::hash<NozzleDef>
|
||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
||||
};
|
||||
};
|
||||
|
||||
// key(extruder_id) -> { key1(nozzle type info), val1( number of the nozzle type)}
|
||||
using ExtruderNozzleInfos = std::unordered_map<int, std::unordered_map<NozzleDef, int>>;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
@@ -33,6 +34,36 @@
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(int ams_id)
|
||||
{
|
||||
// When the app or preset bundle is unavailable we cannot know the vendor's
|
||||
// extruder count, so fall back deterministically to the fixed Bambu pair
|
||||
// {255, 254} rather than guessing from an unknown generic layout.
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor())
|
||||
return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID;
|
||||
|
||||
const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count();
|
||||
return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID;
|
||||
}
|
||||
|
||||
bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id)
|
||||
{
|
||||
// Strict: only a fully numeric string is a valid slot id. std::stoi alone
|
||||
// would accept trailing garbage (e.g. "255abc" -> 255).
|
||||
if (ams_id.empty())
|
||||
return false;
|
||||
for (char c : ams_id) {
|
||||
if (c < '0' || c > '9')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return IsVirtualSlot(std::stoi(ams_id));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; }
|
||||
|
||||
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||
@@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const
|
||||
|
||||
int DevAms::GetSlotCount() const
|
||||
{
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
else if (ams_type == N3S)
|
||||
{
|
||||
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
|
||||
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
|
||||
if (GetAmsType() == N3S)
|
||||
return 1;
|
||||
|
||||
if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
||||
auto ams_type = GetAmsType();
|
||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
return static_cast<int>(m_trays.size());
|
||||
}
|
||||
|
||||
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
|
||||
@@ -302,13 +337,21 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
|
||||
{
|
||||
return it->second->GetExtruderId();
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
||||
{
|
||||
return MAIN_EXTRUDER_ID;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (!is_bbl_vendor && GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
if (tray.id == ams_id)
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
}
|
||||
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
{
|
||||
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
|
||||
return MAIN_EXTRUDER_ID;
|
||||
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||
}
|
||||
|
||||
assert(false && __FUNCTION__);
|
||||
@@ -325,9 +368,22 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
|
||||
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
{
|
||||
std::map<int, DevAmsSlotId> tray_id_map;
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
||||
} else if (GetOwner()) {
|
||||
for (const auto& tray : GetOwner()->vt_slot) {
|
||||
try {
|
||||
const int tray_id = std::stoi(tray.id);
|
||||
tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0};
|
||||
} catch (...) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int generic_tray_index = 0;
|
||||
for (auto& [ams_id, ams_item] : GetAmsList()) {
|
||||
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
||||
if (ams_item && slot_item) {
|
||||
@@ -335,9 +391,11 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
int ams_id_int = stoi(ams_id);
|
||||
int slot_id_int = stoi(slot_id);
|
||||
int tray_index = -1;
|
||||
if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
if (!is_bbl_vendor) {
|
||||
tray_index = generic_tray_index + slot_id_int;
|
||||
} else if (ams_item->GetAmsType() == DevAms::N3S) {
|
||||
tray_index = ams_id_int;
|
||||
} else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
} else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
|
||||
tray_index = 24 + slot_id_int;
|
||||
} else {
|
||||
tray_index = (ams_id_int * 4 + slot_id_int);
|
||||
@@ -348,6 +406,8 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor)
|
||||
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
|
||||
}
|
||||
|
||||
return tray_id_map;
|
||||
@@ -456,6 +516,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
ams_id_set.insert(it->first);
|
||||
}
|
||||
|
||||
// Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit
|
||||
// reports its own lane count, so its trays carry a cumulative lane index instead.
|
||||
// Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with.
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
int generic_tray_base = 0;
|
||||
|
||||
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
|
||||
{
|
||||
if (!it->contains("id")) continue;
|
||||
@@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
|
||||
if (type_id < 4)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
if (is_bbl_vendor)
|
||||
{
|
||||
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A generic unit is not four slots wide, so its trays carry a
|
||||
// cumulative lane index across units instead of ams_id*4+tray_id.
|
||||
const int tray_bit = generic_tray_base + tray_id_int;
|
||||
curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 &&
|
||||
(obj->tray_exist_bits & (1UL << tray_bit)) != 0;
|
||||
}
|
||||
}
|
||||
else if (type_id == DevAms::AMS_LITE_MIXED)
|
||||
{
|
||||
@@ -851,6 +929,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) {
|
||||
generic_tray_base += static_cast<int>((*it)["tray"].size());
|
||||
}
|
||||
}
|
||||
// remove not in amsList
|
||||
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
|
||||
@@ -866,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)
|
||||
@@ -876,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())
|
||||
@@ -913,6 +1026,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
ams->m_exist = true;
|
||||
ams->m_binded_extruder_set = {ext_id};
|
||||
ams->m_binded_switcher_pos.reset();
|
||||
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
||||
@@ -995,16 +1110,21 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
}
|
||||
|
||||
// --- external / direct spools -> obj->vt_slot ---
|
||||
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
|
||||
// Keep one virtual slot per extruder. The legacy two-extruder mapping uses
|
||||
// 255 for extruder 0 and 254 for extruder 1; continue that sequence for
|
||||
// additional extruders (253, 252, ...).
|
||||
if (obj && data.contains("external") && data["external"].is_array())
|
||||
{
|
||||
obj->vt_slot.clear();
|
||||
std::map<int, DevAmsTray> external_slots;
|
||||
for (const auto& e : data["external"])
|
||||
{
|
||||
if (!e.is_object())
|
||||
continue;
|
||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
||||
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
|
||||
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
|
||||
continue;
|
||||
|
||||
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
|
||||
DevAmsTray tray(std::to_string(vt_id));
|
||||
tray.is_exists = e.value("loaded", false);
|
||||
tray.m_fila_type = e.value("material", std::string());
|
||||
@@ -1013,9 +1133,26 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
||||
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||
tray.remain = e.value("remain_percent", -1);
|
||||
obj->vt_slot.push_back(tray);
|
||||
external_slots.insert_or_assign(ext, std::move(tray));
|
||||
}
|
||||
|
||||
// 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.
|
||||
*
|
||||
@@ -348,7 +359,7 @@ public:
|
||||
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
|
||||
void CollectAmsColors(std::vector<wxColour>& ams_colors) const;
|
||||
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes the two virtual (external-spool) trays,
|
||||
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
|
||||
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||
|
||||
@@ -433,4 +444,4 @@ struct DevFilamentDryingPreset
|
||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||
};
|
||||
|
||||
}// namespace Slic3r
|
||||
}// namespace Slic3r
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <set>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
@@ -18,14 +19,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 +109,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 +166,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 +201,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 +227,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 +264,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 +284,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 +311,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 +337,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 +562,4 @@ namespace Slic3r
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include <string>
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
@@ -27,6 +28,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -176,34 +178,52 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
||||
int DevNozzle::GetLogicExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
if (total_ext_count == 1) {
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtLeftExtruder()) {
|
||||
return LOGIC_L_EXTRUDER_ID;
|
||||
} else if (AtRightExtruder()) {
|
||||
return LOGIC_R_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
assert(0);
|
||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||
// For some reason, BBL's logical extruder ID is inverted:
|
||||
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID)
|
||||
// Likely because logical id reads from left to right (left = 0, right = 1)
|
||||
// For generic N extruders, this inversion does not apply.
|
||||
if (IsOnRack()) return INVALID_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
}
|
||||
|
||||
int DevNozzle::GetExtruderId() const
|
||||
{
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor) {
|
||||
int total_ext_count = GetTotalExtruderCount();
|
||||
if (total_ext_count == 1) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
} else if (total_ext_count == 2) {
|
||||
if (AtRightExtruder()) {
|
||||
return MAIN_EXTRUDER_ID;
|
||||
} else if (AtLeftExtruder()) {
|
||||
return DEPUTY_EXTRUDER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
}
|
||||
|
||||
return MAIN_EXTRUDER_ID;
|
||||
return m_nozzle_id;
|
||||
}
|
||||
|
||||
bool DevNozzle::AtLeftExtruder() const
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
@@ -806,8 +807,12 @@ bool MachineObject::is_extrusion_cali_finished()
|
||||
DevAmsTray *MachineObject::get_curr_tray()
|
||||
{
|
||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
||||
return &vt_slot[0];
|
||||
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||
for (auto& tray : vt_slot) {
|
||||
if (tray.id == cur_ams_id)
|
||||
return &tray;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DevAms* curr_ams = get_curr_Ams();
|
||||
@@ -1273,19 +1278,24 @@ int MachineObject::get_bed_temperature_limit()
|
||||
|
||||
bool MachineObject::is_filament_installed()
|
||||
{
|
||||
if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// right//or single
|
||||
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
// if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||
// // right//or single
|
||||
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// /*left*/
|
||||
// if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
// if (ext.m_ext_has_filament) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (auto& ext : m_extder_system->m_extders) {
|
||||
if (ext.m_ext_has_filament)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*left*/
|
||||
if (m_extder_system->GetTotalExtderCount() > 1) {
|
||||
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
|
||||
if (ext.m_ext_has_filament) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2627,13 +2637,10 @@ void MachineObject::reset()
|
||||
json empty_j;
|
||||
print_json.diff2all_base_reset(empty_j);
|
||||
|
||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
||||
vt_slot[i].reset();
|
||||
|
||||
if (i == 1) {
|
||||
vt_slot.erase(vt_slot.begin() + 1);
|
||||
}
|
||||
}
|
||||
for (auto& tray : vt_slot)
|
||||
tray.reset();
|
||||
if (vt_slot.size() > 1)
|
||||
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||
// why: reset reuses MachineObject, so release its lazy subtask
|
||||
// before dropping the pointer to prevent reconnect leaks.
|
||||
if (subtask_) {
|
||||
@@ -4114,25 +4121,25 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||
|
||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
||||
auto vslot = parse_vt_tray(it.value().get<json>());
|
||||
try {
|
||||
const auto tray_json = it.value().get<json>();
|
||||
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
|
||||
continue;
|
||||
|
||||
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 0);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[0] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
const auto tray_id_str = tray_json["id"].get<std::string>();
|
||||
int tray_id = 0;
|
||||
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
|
||||
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|
||||
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
|
||||
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);
|
||||
vt_slot[index] = vslot;
|
||||
}
|
||||
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
|
||||
auto it = std::next(vt_slot.begin(), 1);
|
||||
if (it != vt_slot.end()) {
|
||||
vt_slot[1] = vslot;
|
||||
}
|
||||
else {
|
||||
vt_slot.push_back(vslot);
|
||||
}
|
||||
catch (...) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4163,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
|
||||
@@ -5268,18 +5282,50 @@ 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
|
||||
{
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR)
|
||||
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
} else {
|
||||
|
||||
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto& tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return true; }
|
||||
if (tray.id == ams_id) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
|
||||
}
|
||||
|
||||
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
|
||||
@@ -5289,13 +5335,19 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
|
||||
return DevAmsTray(tray_id);
|
||||
}
|
||||
|
||||
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) { return *tray;};
|
||||
if (tray) return *tray;
|
||||
}
|
||||
else {
|
||||
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
|
||||
for (const auto &tray : vt_slot) {
|
||||
if (tray.id == ams_id) { return tray; }
|
||||
if (tray.id == ams_id) return tray;
|
||||
}
|
||||
|
||||
if (!is_bbl_vendor) {
|
||||
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
|
||||
if (tray) return *tray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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*/
|
||||
|
||||
+195
-29
@@ -20,6 +20,7 @@
|
||||
#include <clocale>
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
#include <cstring>
|
||||
@@ -2361,13 +2362,18 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->plater)
|
||||
return false;
|
||||
const Preset &selected_preset = preset_bundle->printers.get_selected_preset();
|
||||
const Preset *parent_preset = preset_bundle->printers.get_preset_parent(selected_preset);
|
||||
const bool inherits_system_preset = selected_preset.is_user() && parent_preset && parent_preset->is_system;
|
||||
if (!inherits_system_preset) {
|
||||
if (!this->plater)
|
||||
return false;
|
||||
|
||||
this->plater->update_objects_position_when_select_preset([machine_preset]() {
|
||||
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
|
||||
printer_tab->select_preset(machine_preset->name);
|
||||
});
|
||||
this->plater->update_objects_position_when_select_preset([machine_preset]() {
|
||||
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
|
||||
printer_tab->select_preset(machine_preset->name);
|
||||
});
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list";
|
||||
const Preset &cur_preset = preset_bundle->printers.get_selected_preset();
|
||||
@@ -2379,7 +2385,19 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
|
||||
assert(physical_extruder_map->values.size() == extruder_nums);
|
||||
extruder_map = physical_extruder_map->values;
|
||||
}
|
||||
assert(obj->GetExtderSystem()->GetTotalExtderCount() == extruder_nums);
|
||||
const int device_extruder_count = obj->GetExtderSystem()->GetTotalExtderCount();
|
||||
if (device_extruder_count != extruder_nums) {
|
||||
MessageDialog dlg(
|
||||
this->plater,
|
||||
format_wxstr(
|
||||
_L("The connected printer reports %1% extruder(s), but the selected printer preset has %2%. "
|
||||
"Update the preset to match the printer before syncing."),
|
||||
device_extruder_count, extruder_nums),
|
||||
_L("Sync printer information"),
|
||||
wxOK | wxICON_WARNING);
|
||||
dlg.ShowModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multi-nozzle: resolve the nozzle option (pops MultiNozzleSyncDialog when is_manual). No-op for every
|
||||
// existing printer — support_multi_nozzle is false unless some extruder_max_nozzle_count entry is a real
|
||||
@@ -2458,26 +2476,60 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
|
||||
int main_index = obj->is_main_extruder_on_left() ? 0 : 1;
|
||||
int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0;
|
||||
|
||||
// Nozzle diameters are not changed by device sync. If the device reports a different or unavailable
|
||||
// diameter, warn and leave the preset and controls alone while syncing AMS and other printer info.
|
||||
const ConfigOptionFloats *preset_nozzle_diameter =
|
||||
preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
bool nozzle_diameters_match = false;
|
||||
if (preset_nozzle_diameter != nullptr && preset_nozzle_diameter->values.size() == nozzle_diameters.size()) {
|
||||
nozzle_diameters_match = true;
|
||||
for (size_t index = 0; index < nozzle_diameters.size(); ++index) {
|
||||
// A diameter the device does not report (missing, zero, negative or not a finite number) never matches.
|
||||
const double device_diameter = nozzle_diameters[index];
|
||||
const double preset_diameter = preset_nozzle_diameter->values[index];
|
||||
if (!std::isfinite(device_diameter) || device_diameter <= 0.0 ||
|
||||
!std::isfinite(preset_diameter) || preset_diameter <= 0.0 ||
|
||||
std::fabs(device_diameter - preset_diameter) > EPSILON) {
|
||||
nozzle_diameters_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!nozzle_diameters_match) {
|
||||
if (is_manual) {
|
||||
MessageDialog dlg(
|
||||
this->plater,
|
||||
_L("The nozzle diameter reported by the connected printer does not match the current printer preset. "
|
||||
"The nozzle diameter and the printer preset will not be synchronized. "
|
||||
"Select the printer preset that matches the nozzle installed on the printer, then sync again."),
|
||||
_L("Sync printer information"), wxICON_WARNING | wxOK);
|
||||
dlg.ShowModal();
|
||||
} else {
|
||||
// An automatic sync runs on every device update, so log the mismatch instead of popping a dialog.
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << __LINE__
|
||||
<< " nozzle diameter mismatch between connected printer and printer preset,"
|
||||
" skip syncing the nozzle diameter";
|
||||
}
|
||||
}
|
||||
|
||||
if (extruder_nums > 1) {
|
||||
int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
||||
int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1]));
|
||||
assert(left_index != -1 && right_index != -1);
|
||||
left_extruder->combo_diameter->SetSelection(left_index);
|
||||
right_extruder->combo_diameter->SetSelection(right_index);
|
||||
is_switching_diameter = true;
|
||||
switch_diameter(false);
|
||||
is_switching_diameter = false;
|
||||
if (nozzle_diameters_match) {
|
||||
int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
||||
int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1]));
|
||||
assert(left_index != -1 && right_index != -1);
|
||||
left_extruder->combo_diameter->SetSelection(left_index);
|
||||
right_extruder->combo_diameter->SetSelection(right_index);
|
||||
}
|
||||
AMSCountPopupWindow::SetAMSCount(deputy_index, deputy_4, deputy_1);
|
||||
AMSCountPopupWindow::SetAMSCount(main_index, main_4, main_1);
|
||||
AMSCountPopupWindow::UpdateAMSCount(0, left_extruder);
|
||||
AMSCountPopupWindow::UpdateAMSCount(1, right_extruder);
|
||||
} else {
|
||||
int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
||||
assert(index != -1);
|
||||
single_extruder->combo_diameter->SetSelection(index);
|
||||
is_switching_diameter = true;
|
||||
switch_diameter(true);
|
||||
is_switching_diameter = false;
|
||||
if (nozzle_diameters_match) {
|
||||
int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
||||
assert(index != -1);
|
||||
single_extruder->combo_diameter->SetSelection(index);
|
||||
}
|
||||
}
|
||||
|
||||
// set nozzle volume type after switching prset, so this value can override the old value stored in conf
|
||||
@@ -6069,8 +6121,12 @@ void Sidebar::on_bed_type_change(BedType bed_type)
|
||||
*
|
||||
* Return Value:
|
||||
* - Map key encoding:
|
||||
* - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary)
|
||||
* - Virtual trays: 0x10000 + vt_tray.id for the main extruder's slot, or just vt_tray.id for the
|
||||
* others. Bambu's fixed pair is 255 (main) / 254 (secondary); a generic printer identifies each
|
||||
* slot by its own id (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...)
|
||||
* - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary)
|
||||
* - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane
|
||||
* count across units instead of ams_id * 4 + slot_id
|
||||
* - The 0x10000 flag indicates the main/right extruder
|
||||
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
||||
*
|
||||
@@ -6090,7 +6146,13 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
}
|
||||
}
|
||||
|
||||
auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) {
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
// Diagnostic: keys produced from virtual (external) trays, so the final summary can label each
|
||||
// output entry's origin as virtual vs AMS.
|
||||
std::set<int> virtual_slot_keys;
|
||||
|
||||
auto build_tray_config = [is_bbl_vendor](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id, int extruder_id) {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%")
|
||||
% name % tray.setting_id % tray.m_fila_type % tray.color;
|
||||
DynamicPrintConfig tray_config;
|
||||
@@ -6105,6 +6167,12 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)});
|
||||
tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists});
|
||||
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
|
||||
// Transient (never persisted) per-extruder association of a generic slot, so the sync and
|
||||
// the slicing per-extruder grouping keep the real extruder instead of the legacy left/right
|
||||
// buckets. Bambu entries omit it and keep the legacy key-only convention.
|
||||
if (!is_bbl_vendor) {
|
||||
tray_config.set_key_value("device_tray_extruder_id", new ConfigOptionInt(extruder_id));
|
||||
}
|
||||
std::optional<FilamentBaseInfo> info;
|
||||
if (wxGetApp().preset_bundle) {
|
||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
||||
@@ -6117,10 +6185,44 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
};
|
||||
|
||||
if (obj->ams_support_virtual_tray) {
|
||||
int extruder = 0x10000; // Main (first) extruder at right
|
||||
for (auto & vt_tray : obj->vt_slot) {
|
||||
filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255
|
||||
extruder = 0;
|
||||
for (size_t index = 0; index < obj->vt_slot.size(); ++index) {
|
||||
auto & vt_tray = obj->vt_slot[index];
|
||||
int vt_id = 0;
|
||||
if (vt_tray.id.empty()) continue;
|
||||
if (!std::all_of(vt_tray.id.begin(), vt_tray.id.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) continue;
|
||||
try { vt_id = std::stoi(vt_tray.id); } catch (...) { continue; }
|
||||
if (vt_id < 0 || vt_id > VIRTUAL_TRAY_MAIN_ID) continue;
|
||||
// Enumerate the extruder ordinal (the vt_slot index) and validate the expected tray id
|
||||
// instead of trusting the vector size: a stray filler or a stale id for a non-live
|
||||
// extruder must never surface as a real tray. Bambu keeps its fixed pair untouched.
|
||||
if (!is_bbl_vendor) {
|
||||
const int expected_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(index);
|
||||
if (vt_id != expected_id || !devPrinterUtil::IsVirtualSlot(vt_id)) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip vt_slot index %1% id %2% expected %3%") % index % vt_id % expected_id;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Bambu keeps its fixed pair with the leading (main/right) slot marked 0x10000. On a
|
||||
// generic printer every external slot is identified by its own virtual tray id
|
||||
// (extruder = VIRTUAL_TRAY_MAIN_ID - id: 255 -> 0, 254 -> 1, ...), so mark the main
|
||||
// extruder's slot by identity rather than by position in vt_slot. The map key then
|
||||
// keeps each slot bound to its extruder across refreshes and sparse reports.
|
||||
const bool is_main_slot = is_bbl_vendor ? (&vt_tray == &obj->vt_slot.front())
|
||||
: (vt_id == VIRTUAL_TRAY_MAIN_ID);
|
||||
const int key = (is_main_slot ? 0x10000 : 0) + vt_id;
|
||||
const auto inserted = filament_ams_list.emplace(key,
|
||||
build_tray_config(vt_tray, "Ext", vt_tray.id, "0", VIRTUAL_TRAY_MAIN_ID - vt_id));
|
||||
virtual_slot_keys.insert(key);
|
||||
if (!inserted.second) {
|
||||
const auto &existing = inserted.first->second;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": virtual insert collision key %1% incoming_id %2% existing_origin %3% existing_ams %4% existing_slot %5% existing_type %6% existing_id %7%")
|
||||
% key % vt_id
|
||||
% (virtual_slot_keys.count(key) ? "virtual" : "ams")
|
||||
% existing.opt_string("ams_id", 0u)
|
||||
% existing.opt_string("slot_id", 0u)
|
||||
% existing.opt_string("filament_type", 0u)
|
||||
% existing.opt_string("filament_id", 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6140,11 +6242,61 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
||||
for (auto ams : list) {
|
||||
int ams_id = std::stoi(ams.first);
|
||||
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% extruder %2% trays %3%")
|
||||
% ams_id % ams.second->GetExtruderId() % ams.second->GetTrays().size();
|
||||
// Bambu units are four slots wide; a generic unit reports its own lane count and its trays
|
||||
// carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units
|
||||
// numerically to keep this the same index the parser and the agent use.
|
||||
int generic_tray_base = 0;
|
||||
if (!is_bbl_vendor) {
|
||||
for (const auto& other : list) {
|
||||
if (std::stoi(other.first) < ams_id)
|
||||
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
|
||||
}
|
||||
}
|
||||
size_t inserted_trays = 0;
|
||||
size_t rejected_trays = 0;
|
||||
for (auto tray : ams.second->GetTrays()) {
|
||||
int slot_id = std::stoi(tray.first);
|
||||
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
|
||||
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id)));
|
||||
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id;
|
||||
const int key = extruder + tray_index;
|
||||
auto inserted = filament_ams_list.emplace(key,
|
||||
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id),
|
||||
ams.second->GetExtruderId()));
|
||||
if (inserted.second) {
|
||||
++inserted_trays;
|
||||
} else {
|
||||
++rejected_trays;
|
||||
const auto &existing = inserted.first->second;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS insert collision key %1% incoming_ams %2% incoming_slot %3% incoming_extruder %4% existing_origin %5% existing_ams %6% existing_slot %7% existing_extruder %8% existing_type %9% existing_id %10%")
|
||||
% key % ams_id % slot_id % ams.second->GetExtruderId()
|
||||
% (virtual_slot_keys.count(key) ? "virtual" : "ams")
|
||||
% existing.opt_string("ams_id", 0u)
|
||||
% existing.opt_string("slot_id", 0u)
|
||||
% (existing.has("device_tray_extruder_id") ? existing.opt_int("device_tray_extruder_id") : -1)
|
||||
% existing.opt_string("filament_type", 0u)
|
||||
% existing.opt_string("filament_id", 0u);
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": AMS unit %1% tray inserts accepted %2% rejected %3%")
|
||||
% ams_id % inserted_trays % rejected_trays;
|
||||
}
|
||||
|
||||
// Diagnostic: dump the final output map (origin, key, identity/data flags) so parser output can
|
||||
// be correlated with the entries logged at the sync boundary.
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": output entries %1%") % filament_ams_list.size();
|
||||
for (const auto &entry : filament_ams_list) {
|
||||
const auto &cfg = entry.second;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": out %1% key %2% ams_id %3% slot_id %4% extruder %5% type %6% id %7% exist %8% placeholder %9%")
|
||||
% (virtual_slot_keys.count(entry.first) ? "virtual" : "ams")
|
||||
% entry.first
|
||||
% cfg.opt_string("ams_id", 0u)
|
||||
% cfg.opt_string("slot_id", 0u)
|
||||
% (cfg.has("device_tray_extruder_id") ? cfg.opt_int("device_tray_extruder_id") : -1)
|
||||
% cfg.opt_string("filament_type", 0u)
|
||||
% cfg.opt_string("filament_id", 0u)
|
||||
% cfg.opt_bool("filament_exist", 0u)
|
||||
% cfg.opt_bool("filament_slot_placeholder", 0u);
|
||||
}
|
||||
return filament_ams_list;
|
||||
}
|
||||
@@ -6381,6 +6533,20 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
||||
std::vector<std::pair<DynamicPrintConfig *,std::string>> unknowns;
|
||||
auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams");
|
||||
auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1";
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": sync_color_only %1% enable_append %2% direct_sync %3% effective_use_map %4% filament_ams_list %5%")
|
||||
% sync_color_only % enable_append % sync_result.direct_sync % (!sync_result.direct_sync) % list.size();
|
||||
for (const auto &entry : list) {
|
||||
const auto &tray = entry.second;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": tray %1% ams_id %2% slot_id %3% filament_id %4% type %5% color %6% filament_exist %7% placeholder %8%")
|
||||
% entry.first
|
||||
% tray.opt_string("ams_id", 0u)
|
||||
% tray.opt_string("slot_id", 0u)
|
||||
% tray.opt_string("filament_id", 0u)
|
||||
% tray.opt_string("filament_type", 0u)
|
||||
% tray.opt_string("filament_colour", 0u)
|
||||
% (tray.has("filament_exist") && tray.opt_bool("filament_exist", 0u))
|
||||
% (tray.has("filament_slot_placeholder") && tray.opt_bool("filament_slot_placeholder", 0u));
|
||||
}
|
||||
auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only);
|
||||
wxString detail;
|
||||
for (auto & uk : unknowns) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/event.h>
|
||||
#include <string>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
@@ -1155,7 +1156,7 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
|
||||
wxString ams_id;
|
||||
wxColour ams_col;
|
||||
|
||||
if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID)
|
||||
if (devPrinterUtil::IsVirtualSlot(f->ams_id))
|
||||
{
|
||||
ams_id = _L("Ext");
|
||||
}else if (f->tray_id >= 0) {
|
||||
@@ -1347,6 +1348,8 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str,
|
||||
if (m_ams_mapping_result.empty())
|
||||
return false;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
bool valid_mapping_result = true;
|
||||
int invalid_count = 0;
|
||||
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
|
||||
@@ -1402,7 +1405,10 @@ bool SelectMachineDialog::get_ams_mapping_result(std::string &mapping_array_str,
|
||||
// convert #RRGGBB to RRGGBBAA
|
||||
mapping_item["sourceColor"] = m_filaments[k].color;
|
||||
mapping_item["targetColor"] = m_ams_mapping_result[k].color;
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; }
|
||||
// The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware
|
||||
// contract. A generic printer keeps the virtual tray id (255 - extruder) so
|
||||
// the payload still says which extruder's external slot was mapped.
|
||||
if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; }
|
||||
|
||||
/*new ams mapping data*/
|
||||
try {
|
||||
|
||||
+831
-255
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
#ifndef slic3r_StatusPanel_hpp_
|
||||
#define slic3r_StatusPanel_hpp_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "MonitorPage.hpp"
|
||||
@@ -66,6 +68,7 @@
|
||||
#include "StagedBuild.hpp"
|
||||
|
||||
class StepIndicator;
|
||||
class wxChoice;
|
||||
|
||||
#define COMMAND_TIMEOUT 5
|
||||
|
||||
@@ -140,28 +143,36 @@ class ExtruderImage : public wxWindow
|
||||
ScalableBitmap *m_left_extruder_active_empty;
|
||||
ScalableBitmap *m_left_extruder_unactive_filled;
|
||||
ScalableBitmap *m_left_extruder_unactive_empty;
|
||||
|
||||
ScalableBitmap *m_right_extruder_active_filled;
|
||||
ScalableBitmap *m_right_extruder_active_empty;
|
||||
ScalableBitmap *m_right_extruder_unactive_filled;
|
||||
ScalableBitmap *m_right_extruder_unactive_empty;
|
||||
|
||||
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
||||
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
||||
|
||||
|
||||
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
ExtruderState m_single_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||
std::vector<ExtruderState> m_multi_extruder_states;
|
||||
bool m_generic_nozzle_display{false};
|
||||
|
||||
public:
|
||||
void update(int nozzle_num, int nozzle_id);
|
||||
void update(ExtruderState single_state);
|
||||
void update(ExtruderState right_state, ExtruderState left_state);
|
||||
|
||||
void update(ExtruderState state, int idx);
|
||||
|
||||
void msw_rescale();
|
||||
void updateGenericSize();
|
||||
void setExtruderCount(int nozzle_num);
|
||||
void setGenericNozzleDisplay(bool enabled);
|
||||
void setExtruderUsed(std::string loc);
|
||||
void setExtruderUsed(int nozzle_idx);
|
||||
void paintEvent(wxPaintEvent &evt);
|
||||
|
||||
void render(wxDC &dc);
|
||||
@@ -495,6 +506,8 @@ protected:
|
||||
std::vector<ExtruderImage *> m_extruderImage;
|
||||
|
||||
SwitchBoard * m_nozzle_btn_panel;
|
||||
wxChoice* m_generic_nozzle_selector{nullptr};
|
||||
int m_generic_nozzle_selector_count{0};
|
||||
|
||||
wxStaticText * m_text_tasklist_caption;
|
||||
|
||||
@@ -507,10 +520,23 @@ protected:
|
||||
wxBoxSizer * m_misc_ctrl_sizer;
|
||||
StaticBox* m_fan_panel;
|
||||
StaticLine * m_line_nozzle;
|
||||
TempInput* m_tempCtrl_nozzle;
|
||||
wxWindowID m_nozzle_temp_control_id{wxID_ANY};
|
||||
wxWindow* m_temp_nozzle_parent{nullptr};
|
||||
wxBoxSizer* m_temp_nozzle_sizer{nullptr};
|
||||
size_t m_temp_nozzle_active_count{0};
|
||||
TempInput* m_tempCtrl_nozzle{nullptr};
|
||||
int m_temp_nozzle_timeout{ 0 };
|
||||
TempInput* m_tempCtrl_nozzle_deputy;
|
||||
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
|
||||
int m_temp_nozzle_deputy_timeout{ 0 };
|
||||
|
||||
std::vector<TempInput*> m_tempCtrl_nozzles;
|
||||
std::vector<int> m_temp_nozzle_timeouts;
|
||||
|
||||
// Last (dev_id, device toolhead count, preset nozzle count) we warned about, to avoid log spam.
|
||||
std::string m_last_mismatch_dev_id;
|
||||
int m_last_mismatch_device_count{-1};
|
||||
int m_last_mismatch_preset_count{-1};
|
||||
|
||||
TempInput * m_tempCtrl_bed;
|
||||
int m_temp_bed_timeout {0};
|
||||
TempInput * m_tempCtrl_chamber;
|
||||
@@ -550,6 +576,7 @@ protected:
|
||||
|
||||
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
||||
SwitchBoard* m_ams_rack_switch{ nullptr };
|
||||
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
|
||||
|
||||
AMSControl* m_ams_control;
|
||||
StaticBox* m_ams_control_box;
|
||||
@@ -624,6 +651,9 @@ public:
|
||||
|
||||
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
||||
wxBoxSizer *create_temp_control(wxWindow *parent);
|
||||
TempInput* create_nozzle_temp_control(wxWindow *parent, wxWindowID id);
|
||||
void set_temp_input_colors(TempInput* temp_ctrl);
|
||||
void ensure_nozzle_temp_controls(size_t count);
|
||||
wxBoxSizer *create_misc_control(wxWindow *parent);
|
||||
wxBoxSizer *create_axis_control(wxWindow *parent);
|
||||
wxPanel *create_bed_control(wxWindow *parent);
|
||||
@@ -654,6 +684,7 @@ private:
|
||||
friend class MonitorPanel;
|
||||
void wire_controls();
|
||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||
void sync_nozzle_temp_controls(size_t count);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||
@@ -801,6 +832,7 @@ protected:
|
||||
void update_cloud_subtask(MachineObject *obj);
|
||||
void update_sdcard_subtask(MachineObject *obj);
|
||||
void update_temp_ctrl(MachineObject *obj);
|
||||
void check_extruder_count_mismatch(MachineObject* obj);
|
||||
void update_misc_ctrl(MachineObject *obj);
|
||||
void update_ams(MachineObject* obj);
|
||||
void update_rack(MachineObject* obj);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <vector>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/animate.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/busycursor.h>
|
||||
#include <wx/chartype.h>
|
||||
#include <wx/arrstr.h>
|
||||
@@ -1245,7 +1246,7 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
|
||||
wxString ams_id;
|
||||
wxColour ams_col;
|
||||
|
||||
if (f->tray_id == VIRTUAL_TRAY_MAIN_ID || f->tray_id == VIRTUAL_TRAY_DEPUTY_ID) {
|
||||
if (devPrinterUtil::IsVirtualSlot(f->ams_id)) {
|
||||
ams_id = _L("Ext");
|
||||
}
|
||||
|
||||
@@ -1393,6 +1394,8 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
|
||||
if (m_ams_mapping_result.empty())
|
||||
return false;
|
||||
|
||||
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
|
||||
bool valid_mapping_result = true;
|
||||
int invalid_count = 0;
|
||||
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
|
||||
@@ -1449,7 +1452,10 @@ bool SyncAmsInfoDialog::get_ams_mapping_result(std::string &mapping_array_str, s
|
||||
// convert #RRGGBB to RRGGBBAA
|
||||
mapping_item["sourceColor"] = m_filaments[k].color;
|
||||
mapping_item["targetColor"] = m_ams_mapping_result[k].color;
|
||||
if (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID) { tray_id = -1; }
|
||||
// The legacy v0 mapping array encodes an external spool as -1, a BBL-firmware
|
||||
// contract. A generic printer keeps the virtual tray id (255 - extruder) so the
|
||||
// payload still says which extruder's external slot was mapped.
|
||||
if (is_bbl_vendor && (tray_id == VIRTUAL_TRAY_MAIN_ID || tray_id == VIRTUAL_TRAY_DEPUTY_ID)) { tray_id = -1; }
|
||||
|
||||
/*new ams mapping data*/
|
||||
try {
|
||||
|
||||
@@ -244,7 +244,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
|
||||
m_sizer_ams_body->Add(m_sizer_ams_area_right, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_body->Add(m_sizer_ams_items, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_ams_gap = m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
|
||||
m_sizer_body->Add(m_sizer_ams_body, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_down_road, 0, wxALIGN_CENTER, 0);
|
||||
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
|
||||
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_total_ext_count == 1){
|
||||
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
||||
if (id == ams_id){
|
||||
return true;
|
||||
@@ -379,6 +379,10 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
||||
@@ -519,7 +523,7 @@ void AMSControl::msw_rescale()
|
||||
m_button_ams_setting_press.msw_rescale();
|
||||
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
|
||||
|
||||
m_extruder->msw_rescale();
|
||||
if (m_extruder) m_extruder->msw_rescale();
|
||||
|
||||
if (m_button_extruder_feed) m_button_extruder_feed->Rescale(); // ORCA
|
||||
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
||||
@@ -603,6 +607,8 @@ void AMSControl::CreateAms()
|
||||
|
||||
|
||||
void AMSControl::ClearAms() {
|
||||
m_sizer_ams_items->Clear(false);
|
||||
|
||||
m_simplebook_ams_right->DeleteAllPages();
|
||||
m_simplebook_ams_left->DeleteAllPages();
|
||||
m_simplebook_ams_right->DestroyChildren();
|
||||
@@ -613,7 +619,7 @@ void AMSControl::ClearAms() {
|
||||
m_simplebook_ams_left->Refresh();
|
||||
|
||||
for (auto it : m_ams_preview_list) {
|
||||
delete it.second;
|
||||
if (it.second) it.second->Destroy();
|
||||
}
|
||||
m_ams_preview_list.clear();
|
||||
m_ext_image_list.clear();
|
||||
@@ -624,12 +630,181 @@ void AMSControl::ClearAms() {
|
||||
m_ams_item_list.clear();
|
||||
m_sizer_prv_right->Clear();
|
||||
m_sizer_prv_left->Clear();
|
||||
m_item_ids = { {}, {} };
|
||||
|
||||
if (m_nozzle_book) {
|
||||
m_nozzle_book->Destroy();
|
||||
m_nozzle_book = nullptr;
|
||||
} else {
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.preview_panel) pane.preview_panel->Destroy();
|
||||
if (pane.ams_book) pane.ams_book->Destroy();
|
||||
if (pane.page) pane.page->Destroy();
|
||||
}
|
||||
}
|
||||
m_item_ids.assign(std::max(m_total_ext_count, 2), {});
|
||||
m_nozzle_panes.clear();
|
||||
m_active_nozzle_id = 0;
|
||||
m_current_ams.clear();
|
||||
m_current_show_ams_left.clear();
|
||||
m_current_show_ams_right.clear();
|
||||
pair_id.clear();
|
||||
}
|
||||
|
||||
void AMSControl::restore_legacy_ams_layout()
|
||||
{
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxHORIZONTAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_ams_items->Add(m_panel_prv_left, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
m_sizer_ams_items->Add(m_panel_prv_right, 0, wxLEFT | wxRIGHT, FromDIP(5));
|
||||
|
||||
m_panel_prv_left->Show();
|
||||
m_panel_prv_right->Show();
|
||||
m_simplebook_ams_left->Show();
|
||||
m_simplebook_ams_right->Show();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_body->Show(m_sizer_ams_body, true);
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_down_road->SetSingleSideLayout(false);
|
||||
m_sizer_ams_gap->SetProportion(1);
|
||||
m_sizer_ams_gap->SetBorder(FromDIP(10));
|
||||
m_down_road->SetNozzleCount(m_total_ext_count);
|
||||
}
|
||||
|
||||
bool AMSControl::use_multi_nozzle_layout() const
|
||||
{
|
||||
const bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
return is_bbl ? m_total_ext_count > 2 : m_nozzle_book != nullptr;
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsMultiNozzle(const std::string &series_name, const std::string &printer_type) {
|
||||
const size_t pane_count = static_cast<size_t>(std::max(m_total_ext_count, 0));
|
||||
m_nozzle_panes.resize(pane_count);
|
||||
m_active_nozzle_id = 0;
|
||||
|
||||
m_sizer_ams_items->Clear(false);
|
||||
m_sizer_ams_items->SetOrientation(wxVERTICAL);
|
||||
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
|
||||
m_sizer_body->Show(m_sizer_ams_body, false);
|
||||
m_sizer_ams_gap->SetProportion(0);
|
||||
m_sizer_ams_gap->SetBorder(0);
|
||||
|
||||
m_panel_prv_left->Hide();
|
||||
m_panel_prv_right->Hide();
|
||||
m_simplebook_ams_left->Hide();
|
||||
m_simplebook_ams_right->Hide();
|
||||
m_panel_down_road->Show();
|
||||
m_sizer_option_mid->Show(m_extruder, true);
|
||||
m_switcher->Hide();
|
||||
m_down_road->SetNozzleCount(1);
|
||||
m_down_road->SetSingleSideLayout(true, AMSPanelPos::LEFT_PANEL);
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_SINGLE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_extruder->updateNozzleNum(1, series_name);
|
||||
|
||||
m_nozzle_book = new wxSimplebook(m_amswin, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_nozzle_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
m_nozzle_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
m_sizer_ams_items->Add(m_nozzle_book, 0, wxALIGN_CENTER);
|
||||
|
||||
for (size_t nozzle_id = 0; nozzle_id < pane_count; ++nozzle_id) {
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
// A generic page is one H2C side. Keep the left-side geometry as the
|
||||
// canonical single-nozzle presentation; the road and AMS item code
|
||||
// still use the same side-specific paths as the legacy layout.
|
||||
pane.panel_pos = AMSPanelPos::LEFT_PANEL;
|
||||
pane.page = new wxPanel(m_nozzle_book, wxID_ANY);
|
||||
pane.page->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.page->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
pane.page_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
pane.page->SetSizer(pane.page_sizer);
|
||||
|
||||
pane.preview_panel = new wxScrolledWindow(pane.page, wxID_ANY);
|
||||
pane.preview_panel->SetScrollRate(10, 0);
|
||||
pane.preview_panel->SetSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
|
||||
pane.preview_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
|
||||
pane.preview_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.preview_panel->SetSizer(pane.preview_sizer);
|
||||
|
||||
// Generic lane cards grow to their calculated width for 1-8 lanes. Keep the
|
||||
// book at the legacy minimum, but allow the page to expand to that width.
|
||||
pane.ams_book = new wxSimplebook(pane.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
|
||||
pane.ams_book->SetMinSize(wxSize(FromDIP(264), -1));
|
||||
pane.ams_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
|
||||
// wxALIGN_CENTER does not centre horizontally inside a HORIZONTAL sizer, so a book wider
|
||||
// than pane.ams_area's own minimum would sit at ams_area's left edge and overflow to the
|
||||
// right, shifting the whole card column off the shared down-road connector axis. A pair of
|
||||
// stretch spacers keeps the book centred on the pane (and thus on the connector) at any
|
||||
// book width; ams_area is expanded below so the spacers have room to work with.
|
||||
pane.ams_area = new wxBoxSizer(wxHORIZONTAL);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
pane.ams_area->Add(pane.ams_book, 0, wxALIGN_CENTER, 0);
|
||||
pane.ams_area->AddStretchSpacer(1);
|
||||
|
||||
pane.page_sizer->Add(pane.preview_panel, 0, wxALIGN_CENTER | wxTOP, FromDIP(5));
|
||||
pane.page_sizer->Add(pane.ams_area, 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
|
||||
m_nozzle_book->AddPage(pane.page, wxEmptyString, nozzle_id == 0);
|
||||
}
|
||||
|
||||
auto add_info = [this, series_name, printer_type, pane_count](AMSinfo info) {
|
||||
if (pane_count == 0) return;
|
||||
|
||||
info.nozzle_id = std::clamp(info.nozzle_id, 0, static_cast<int>(pane_count) - 1);
|
||||
AddAmsPreview(info, info.ams_type);
|
||||
if (info.ams_type == AMSModel::EXT_AMS && info.cans.size() == 1)
|
||||
AddAms({info}, series_name, printer_type, m_nozzle_panes[info.nozzle_id].panel_pos);
|
||||
else
|
||||
AddAms(info);
|
||||
};
|
||||
|
||||
for (const auto &info : m_ams_info)
|
||||
add_info(info);
|
||||
|
||||
for (auto &info : m_ext_info) {
|
||||
const int virtual_tray_id = std::atoi(info.ams_id.c_str());
|
||||
if (virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID &&
|
||||
virtual_tray_id >= VIRTUAL_TRAY_MAIN_ID - static_cast<int>(pane_count) + 1)
|
||||
info.nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
add_info(info);
|
||||
}
|
||||
|
||||
for (auto &pane : m_nozzle_panes) {
|
||||
if (pane.ams_book && pane.ams_book->GetPageCount() > 0)
|
||||
pane.ams_book->SetSelection(0);
|
||||
if (pane.preview_sizer) pane.preview_sizer->Layout();
|
||||
if (pane.preview_panel) pane.preview_panel->FitInside();
|
||||
if (pane.ams_book) pane.ams_book->Layout();
|
||||
if (pane.page) pane.page->Layout();
|
||||
|
||||
if (!pane.current_ams.empty()) {
|
||||
auto preview = m_ams_preview_list.find(pane.current_ams);
|
||||
if (preview != m_ams_preview_list.end() && preview->second)
|
||||
preview->second->OnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nozzle_book && pane_count > 0) {
|
||||
m_nozzle_book->SetSelection(0);
|
||||
m_current_ams = m_nozzle_panes[0].current_ams;
|
||||
if (!m_current_ams.empty())
|
||||
SwitchAms(m_current_ams);
|
||||
else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
}
|
||||
m_amswin->Layout();
|
||||
m_amswin->Fit();
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<AMSinfo> single_info_left;
|
||||
std::vector<AMSinfo> single_info_right;
|
||||
|
||||
@@ -751,6 +926,8 @@ void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std
|
||||
|
||||
void AMSControl::CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type)
|
||||
{
|
||||
restore_legacy_ams_layout();
|
||||
|
||||
std::vector<int>m_item_nums{0,0};
|
||||
std::vector<AMSinfo> single_info;
|
||||
|
||||
@@ -979,6 +1156,17 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
|
||||
fresh = true;
|
||||
}
|
||||
if (m_ams_info[i].nozzle_id != ams_info[i].nozzle_id) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_ext_info.size()); ++i) {
|
||||
if (m_ext_info[i].ams_id != ext_info[i].ams_id ||
|
||||
m_ext_info[i].nozzle_id != ext_info[i].nozzle_id ||
|
||||
m_ext_info[i].ext_type != ext_info[i].ext_type) {
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
@@ -993,11 +1181,16 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
m_dev_id = dev_id;
|
||||
if (fresh){
|
||||
ClearAms();
|
||||
if (m_total_ext_count >= 2){
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}else{
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (is_bbl) {
|
||||
if (m_total_ext_count == 1)
|
||||
CreateAmsSingleNozzle(series_name, printer_type);
|
||||
else if (m_total_ext_count == 2)
|
||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
||||
}
|
||||
else
|
||||
CreateAmsMultiNozzle(series_name, printer_type);
|
||||
|
||||
SetSize(wxSize(FromDIP(578), -1));
|
||||
SetMinSize(wxSize(FromDIP(578), -1));
|
||||
Layout();
|
||||
@@ -1070,7 +1263,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
||||
}
|
||||
|
||||
/*update ams extruder*/
|
||||
if (m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
if (m_total_ext_count <= 2 && m_extruder->updateNozzleNum(m_total_ext_count, series_name))
|
||||
{
|
||||
m_amswin->Layout();
|
||||
}
|
||||
@@ -1106,7 +1299,15 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
{
|
||||
AMSPreview *ams_prv = nullptr;
|
||||
|
||||
if (info.routes_to_main_extruder())
|
||||
// Match AddAms: the generic multi-nozzle layout's preview panel is the visible one for
|
||||
// non-Bambu vendors at any extruder count, not only above two.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
}
|
||||
else if (info.routes_to_main_extruder())
|
||||
{
|
||||
ams_prv = new AMSPreview(m_panel_prv_right, wxID_ANY, info, type);
|
||||
m_sizer_prv_right->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
@@ -1129,6 +1330,54 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
||||
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
|
||||
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
||||
parent->InsertPage(idx, ams_item, wxEmptyString, true);
|
||||
|
||||
// Generic AMS cards may be wider than the legacy 264 DIP page. Propagate
|
||||
// that calculated width through the page and simplebook so 5-8 lanes are
|
||||
// laid out fully instead of being clipped by the old minimum width.
|
||||
if (info.ams_type == AMSModel::GENERIC_AMS) {
|
||||
const int required_width = ams_item->GetMinSize().x;
|
||||
if (required_width > 0) {
|
||||
const int page_width = std::max(parent->GetMinSize().x, required_width);
|
||||
parent->SetMinSize(wxSize(page_width, -1));
|
||||
if (auto *page = parent->GetParent()) {
|
||||
page->SetMinSize(wxSize(std::max(page->GetMinSize().x, page_width), -1));
|
||||
if (auto *nozzle_book = page->GetParent())
|
||||
nozzle_book->SetMinSize(wxSize(std::max(nozzle_book->GetMinSize().x, page_width), -1));
|
||||
}
|
||||
}
|
||||
// Above eight lanes, AmsItem is a fixed-minimum viewport over a wider
|
||||
// lane strip. Expand the viewport through its page's sizers rather
|
||||
// than propagating the strip's virtual width into the card/book.
|
||||
// Reset back to the centered fixed layout for eight or fewer lanes so a
|
||||
// later small unit is not left in expand mode.
|
||||
const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor();
|
||||
if (generic_lane_layout) {
|
||||
if (auto *page = parent->GetParent()) {
|
||||
auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(),
|
||||
[page](const NozzleAmsPane &candidate) { return candidate.page == page; });
|
||||
if (pane != m_nozzle_panes.end() && m_nozzle_book) {
|
||||
const bool expand_viewport = info.cans.size() > 8;
|
||||
if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) {
|
||||
book_item->SetProportion(expand_viewport ? 1 : 0);
|
||||
book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
}
|
||||
// Keep the centring spacers (items 0 and 2) elastic only in the centred layout;
|
||||
// in the >8-lane viewport they must collapse so the book can fill ams_area.
|
||||
if (auto *lead = pane->ams_area->GetItem(static_cast<size_t>(0)))
|
||||
lead->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *trail = pane->ams_area->GetItem(static_cast<size_t>(2)))
|
||||
trail->SetProportion(expand_viewport ? 0 : 1);
|
||||
if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area))
|
||||
area_item->SetFlag(wxEXPAND | wxTOP);
|
||||
if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book))
|
||||
nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items))
|
||||
items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
|
||||
m_amswin->Layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ams_item->set_selection(idx);
|
||||
idx++;
|
||||
|
||||
@@ -1153,7 +1402,10 @@ AMSRoadShowMode AMSControl::findFirstMode(AMSPanelPos pos) {
|
||||
if (item->second->get_ams_model() == AMSModel::EXT_AMS && item->second->get_ext_type() == AMSModelOriginType::LITE_EXT) return AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
}
|
||||
else{
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM) {
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
}
|
||||
else {
|
||||
for (auto ids : pair_id){
|
||||
if (ids.first == ams_id || ids.second == ams_id){
|
||||
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
@@ -1172,8 +1424,13 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
wxPanel* book_panel = new wxPanel(parent);
|
||||
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
book_panel->SetSize(AMS_PANEL_SIZE);
|
||||
book_panel->SetMinSize(AMS_PANEL_SIZE);
|
||||
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
|
||||
// added directly as the book page (so it always spans the book), while a single external
|
||||
// spool is wrapped here. Matching the book keeps the Ext card centred on the same axis as
|
||||
// the AMS cards and the shared down-road connector anchored below them.
|
||||
const wxSize book_panel_size(std::max(parent->GetMinSize().x, AMS_PANEL_SIZE.x), AMS_PANEL_SIZE.y);
|
||||
book_panel->SetSize(book_panel_size);
|
||||
book_panel->SetMinSize(book_panel_size);
|
||||
|
||||
AmsItem* ams1 = nullptr, * ams2 = nullptr;
|
||||
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
|
||||
@@ -1200,6 +1457,14 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
//book_sizer->Add(ams1, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
|
||||
}
|
||||
else if (pos == AMSPanelPos::SINGLE_PANEL) {
|
||||
// Centre the single external-spool card explicitly, using the same fixed-margin
|
||||
// technique as the Lite-EXT branch above. Two stretch spacers centred the card
|
||||
// against the wrapper's own width rather than the AMS book, which left the card
|
||||
// (and the connector drawn below it) off the book/connector axis.
|
||||
book_sizer->Add(ams1, 0, wxLEFT,
|
||||
std::max(0, (book_panel_size.x - ams1->GetSize().x) / 2));
|
||||
}
|
||||
else{
|
||||
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
||||
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
|
||||
@@ -1228,7 +1493,20 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
|
||||
|
||||
void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
{
|
||||
if (m_total_ext_count > 1){
|
||||
// The generic multi-nozzle layout (non-Bambu vendors) owns the only visible book; a unit must
|
||||
// land there for any extruder count. Keying off m_total_ext_count instead sent 1- and 2-extruder
|
||||
// generic printers into the legacy simplebooks that CreateAmsMultiNozzle hides.
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
if (pane.current_ams.empty()) pane.current_ams = info.ams_id;
|
||||
createAms(pane.ams_book, pane.page_index, info, AMSPanelPos::SINGLE_PANEL);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2){
|
||||
if (info.routes_to_main_extruder()){
|
||||
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
|
||||
}
|
||||
@@ -1239,6 +1517,9 @@ void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
|
||||
else if (m_total_ext_count == 1){
|
||||
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
m_simplebook_ams_left->Refresh();
|
||||
@@ -1269,7 +1550,19 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
if (single_info.size() <= 0){
|
||||
return;
|
||||
}
|
||||
if (m_total_ext_count == 2) {
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
pane.item_ids.push_back(info.ams_id);
|
||||
m_item_ids[nozzle_id].push_back(info.ams_id);
|
||||
}
|
||||
if (pane.current_ams.empty()) pane.current_ams = single_info.front().ams_id;
|
||||
createAmsPanel(pane.ams_book, pane.page_index, single_info, series_name, printer_type, AMSPanelPos::SINGLE_PANEL, m_total_ext_count);
|
||||
pane.ams_book->Fit();
|
||||
pane.ams_book->Layout();
|
||||
}
|
||||
else if (m_total_ext_count == 2) {
|
||||
if (single_info[0].routes_to_main_extruder()) {
|
||||
createAmsPanel(m_simplebook_ams_right, m_right_page_index, single_info, series_name, printer_type, AMSPanelPos::RIGHT_PANEL, m_total_ext_count);
|
||||
}
|
||||
@@ -1285,6 +1578,9 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
createAmsPanel(m_simplebook_ams_left, m_left_page_index, single_info, series_name, printer_type, AMSPanelPos::LEFT_PANEL, m_total_ext_count);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
m_simplebook_ams_left->Layout();
|
||||
m_simplebook_ams_right->Layout();
|
||||
@@ -1322,6 +1618,21 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
|
||||
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
|
||||
if (single_info.size() <= 0) return;
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
for (const auto &info : single_info) {
|
||||
auto *ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, info.ams_type);
|
||||
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
|
||||
ams_prv->Bind(wxEVT_LEFT_DOWN, [this, ams_prv](wxMouseEvent &e) {
|
||||
SwitchAms(ams_prv->get_ams_id());
|
||||
e.Skip();
|
||||
});
|
||||
m_ams_preview_list[info.ams_id] = ams_prv;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AMSPreview* ams_prv = nullptr;
|
||||
AMSPreview* ams_prv2 = nullptr;
|
||||
if (pos == AMSPanelPos::RIGHT_PANEL){
|
||||
@@ -1360,8 +1671,97 @@ void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos)
|
||||
}
|
||||
}
|
||||
|
||||
void AMSControl::SelectNozzle(int nozzle_id)
|
||||
{
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (m_active_nozzle_id == nozzle_id &&
|
||||
(!m_nozzle_book || m_nozzle_book->GetSelection() == nozzle_id))
|
||||
return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
m_current_ams = m_nozzle_panes[nozzle_id].current_ams;
|
||||
if (!m_current_ams.empty()) {
|
||||
SwitchAms(m_current_ams);
|
||||
return;
|
||||
}
|
||||
if (m_down_road) {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
}
|
||||
|
||||
void AMSControl::SwitchAms(std::string ams_id)
|
||||
{
|
||||
if (use_multi_nozzle_layout()) {
|
||||
NozzleAmsPane *pane = nullptr;
|
||||
int nozzle_id = -1;
|
||||
for (int index = 0; index < static_cast<int>(m_nozzle_panes.size()); ++index) {
|
||||
auto &candidate = m_nozzle_panes[index];
|
||||
if (std::find(candidate.item_ids.begin(), candidate.item_ids.end(), ams_id) != candidate.item_ids.end()) {
|
||||
pane = &candidate;
|
||||
nozzle_id = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pane) return;
|
||||
|
||||
m_active_nozzle_id = nozzle_id;
|
||||
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
|
||||
m_nozzle_book->SetSelection(nozzle_id);
|
||||
|
||||
pane->current_ams = ams_id;
|
||||
m_current_ams = ams_id;
|
||||
m_current_select = ams_id;
|
||||
|
||||
for (const auto &id : pane->item_ids) {
|
||||
auto preview = m_ams_preview_list.find(id);
|
||||
if (preview == m_ams_preview_list.end() || !preview->second) continue;
|
||||
if (id == ams_id) preview->second->OnSelected();
|
||||
else preview->second->UnSelected();
|
||||
}
|
||||
|
||||
auto item = m_ams_item_list.find(ams_id);
|
||||
if (item != m_ams_item_list.end() && item->second) {
|
||||
if (pane->ams_book)
|
||||
pane->ams_book->SetSelection(item->second->get_selection());
|
||||
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (item->second->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->second->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (item->second->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(item->second->get_ams_model() == AMSModel::EXT_AMS &&
|
||||
item->second->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (m_down_road) {
|
||||
if (pane->panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
m_down_road->UpdatePassRoad(pane->panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
}
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
|
||||
post_event(SimpleEvent(EVT_AMS_SWITCH));
|
||||
return;
|
||||
}
|
||||
|
||||
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
|
||||
|
||||
bool is_in_right = IsAmsInRightPanel(ams_id);
|
||||
@@ -1432,13 +1832,17 @@ void AMSControl::SwitchAms(std::string ams_id)
|
||||
else {
|
||||
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
|
||||
if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
if (item->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (item->get_ams_model() == AMSModel::N3S_AMS)
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
|
||||
for (auto it : pair_id) {
|
||||
if (it.first == ams_id || it.second == ams_id) {
|
||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
|
||||
@@ -1500,6 +1904,20 @@ void AMSControl::SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std
|
||||
AmsItem *item = nullptr;
|
||||
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
|
||||
if (!m_extruder)
|
||||
return;
|
||||
|
||||
if (on_off && item)
|
||||
m_extruder->OnAmsLoading(true, 0, item->GetTagColr(slot_id));
|
||||
else
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (on_off && item) {
|
||||
auto col = item->GetTagColr(slot_id);
|
||||
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
||||
@@ -1624,6 +2042,69 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
|
||||
else{
|
||||
return;
|
||||
}
|
||||
|
||||
if (use_multi_nozzle_layout()) {
|
||||
const int nozzle_id = std::clamp(ams->get_nozzle_id(), 0, m_total_ext_count - 1);
|
||||
if (nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
|
||||
return;
|
||||
auto &pane = m_nozzle_panes[nozzle_id];
|
||||
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
||||
if (ams->get_can_count() == GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
|
||||
else if (ams->get_can_count() > GENERIC_AMS_SLOT_NUM)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
|
||||
else if (ams->get_ams_model() == AMSModel::N3S_AMS)
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||
else if (ams->get_ams_model() == AMSModel::AMS_LITE ||
|
||||
(ams->get_ams_model() == AMSModel::EXT_AMS && ams->get_ext_type() == AMSModelOriginType::LITE_EXT))
|
||||
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
|
||||
|
||||
if (can_index >= 0 && can_index < static_cast<int>(info.cans.size()) && m_down_road)
|
||||
m_down_road->SetPassRoadColour(pane.panel_pos == AMSPanelPos::LEFT_PANEL,
|
||||
info.cans[can_index].material_colour);
|
||||
if (m_down_road) {
|
||||
if (pane.panel_pos == AMSPanelPos::LEFT_PANEL) {
|
||||
m_down_road->UpdateLeft(1, road_mode);
|
||||
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
} else {
|
||||
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
|
||||
m_down_road->UpdateRight(1, road_mode);
|
||||
}
|
||||
}
|
||||
|
||||
int road_length = 0;
|
||||
|
||||
if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_NONE) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP1) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(false, 0);
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3) {
|
||||
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_down_road)
|
||||
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
|
||||
if (m_extruder)
|
||||
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (can_index >= 0 && can_index < info.cans.size())
|
||||
{
|
||||
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
||||
|
||||
@@ -47,6 +47,20 @@ public:
|
||||
void on_retry();
|
||||
|
||||
protected:
|
||||
struct NozzleAmsPane
|
||||
{
|
||||
wxPanel* page{nullptr};
|
||||
wxBoxSizer* page_sizer{nullptr};
|
||||
wxScrolledWindow* preview_panel{nullptr};
|
||||
wxBoxSizer* preview_sizer{nullptr};
|
||||
wxSimplebook* ams_book{nullptr};
|
||||
wxBoxSizer* ams_area{nullptr};
|
||||
AMSPanelPos panel_pos{AMSPanelPos::LEFT_PANEL};
|
||||
std::vector<std::string> item_ids;
|
||||
std::string current_ams;
|
||||
int page_index{0};
|
||||
};
|
||||
|
||||
std::string m_current_ams;
|
||||
std::string m_current_slot_left;
|
||||
std::string m_current_slot_right;
|
||||
@@ -63,6 +77,9 @@ protected:
|
||||
|
||||
std::string m_dev_id;
|
||||
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
||||
std::vector<NozzleAmsPane> m_nozzle_panes;
|
||||
wxSimplebook* m_nozzle_book{nullptr};
|
||||
int m_active_nozzle_id{0};
|
||||
std::vector<std::pair<std::string, std::string>> pair_id;
|
||||
|
||||
int m_total_ext_count = 1;
|
||||
@@ -103,6 +120,7 @@ protected:
|
||||
wxSimplebook *m_simplebook_ams_left{nullptr};
|
||||
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
||||
wxSimplebook *m_simplebook_bottom{nullptr};
|
||||
wxSizerItem *m_sizer_ams_gap{nullptr};
|
||||
wxPanel *m_panel_down_road{ nullptr };
|
||||
int m_left_page_index = 0;
|
||||
int m_right_page_index = 0;
|
||||
@@ -156,6 +174,7 @@ public:
|
||||
void createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos);
|
||||
void createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSinfo> infos, const std::string &series_name, const std::string &printer_type, AMSPanelPos pos, int total_ext_num);
|
||||
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
|
||||
void restore_legacy_ams_layout();
|
||||
|
||||
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
||||
AMSModel m_ext_model{AMSModel::EXT_AMS};
|
||||
@@ -183,6 +202,7 @@ public:
|
||||
|
||||
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void CreateAms();
|
||||
void CreateAmsMultiNozzle(const std::string& series_name, const std::string& printer_type);
|
||||
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
|
||||
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
||||
void ClearAms();
|
||||
@@ -210,6 +230,8 @@ public:
|
||||
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
|
||||
void SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||
void SwitchAms(std::string ams_id);
|
||||
void SelectNozzle(int nozzle_id);
|
||||
int GetActiveNozzleId() const { return m_active_nozzle_id; }
|
||||
|
||||
void msw_rescale();
|
||||
void on_filament_load(wxCommandEvent &event);
|
||||
@@ -234,6 +256,11 @@ public:
|
||||
private:
|
||||
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
|
||||
|
||||
// True when AMS units are laid out on the generic multi-nozzle book. Bambu
|
||||
// printers keep the legacy count-based single/double-nozzle layout at any
|
||||
// extruder count; other vendors use the book created for them.
|
||||
bool use_multi_nozzle_layout() const;
|
||||
|
||||
public:
|
||||
std::string m_current_select;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "CalibUtils.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -76,6 +77,48 @@ namespace Slic3r { namespace GUI {
|
||||
//#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
|
||||
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
||||
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), FromDIP(174))
|
||||
|
||||
int generic_ams_layout_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(264);
|
||||
const int lane_width = window->FromDIP(52);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 4)
|
||||
return base_width;
|
||||
|
||||
// Lane centers are distributed as width * (index + 1) / (count + 1).
|
||||
// Keep at least one card width plus the requested gap between centers.
|
||||
return std::max(base_width, (lane_count + 1) * (lane_width + lane_gap));
|
||||
}
|
||||
|
||||
std::vector<int> generic_ams_lane_centers(int lane_count, int width)
|
||||
{
|
||||
std::vector<int> centers;
|
||||
if (lane_count <= 0)
|
||||
return centers;
|
||||
|
||||
centers.reserve(lane_count);
|
||||
for (int lane = 0; lane < lane_count; ++lane)
|
||||
centers.push_back(width * (lane + 1) / (lane_count + 1));
|
||||
return centers;
|
||||
}
|
||||
|
||||
int generic_ams_preview_width(wxWindow *window, int lane_count)
|
||||
{
|
||||
const int base_width = window->FromDIP(52);
|
||||
const int lane_width = window->FromDIP(9);
|
||||
const int lane_gap = window->FromDIP(4);
|
||||
if (lane_count <= 0)
|
||||
return base_width;
|
||||
|
||||
return std::max(base_width, lane_count * lane_width + (lane_count + 1) * lane_gap);
|
||||
}
|
||||
|
||||
bool use_generic_ams_layout(AMSModel model)
|
||||
{
|
||||
return model == AMSModel::GENERIC_AMS &&
|
||||
(!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor());
|
||||
}
|
||||
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
|
||||
{
|
||||
if (!ams) return false;
|
||||
@@ -170,10 +213,14 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
||||
info.can_id = std::to_string(0);
|
||||
this->cans.clear();
|
||||
|
||||
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
|
||||
try {
|
||||
const int virtual_tray_id = std::stoi(tray.id);
|
||||
if (virtual_tray_id >= 0 && virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID)
|
||||
this->nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
|
||||
}
|
||||
catch (...) {
|
||||
this->nozzle_id = 0;
|
||||
else if (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
|
||||
this->nozzle_id = 1;
|
||||
}
|
||||
|
||||
if (tray.is_tray_info_ready()) {
|
||||
info.ctype = tray.ctype;
|
||||
@@ -856,7 +903,11 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
|
||||
|
||||
void AMSextruder::TurnOff()
|
||||
{
|
||||
m_left_extruder->TurnOff();
|
||||
if (m_left_extruder) m_left_extruder->TurnOff();
|
||||
if (m_right_extruder) m_right_extruder->TurnOff();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->TurnOff();
|
||||
}
|
||||
}
|
||||
|
||||
void AMSextruder::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, int nozzle_num)
|
||||
@@ -882,7 +933,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
m_left_extruder->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur_deputy = col;
|
||||
}
|
||||
else if (m_nozzle_num > 1){
|
||||
else if (m_nozzle_num == 2){
|
||||
if (nozzle_id == MAIN_EXTRUDER_ID) {
|
||||
m_right_extruder->OnAmsLoading(load, col);
|
||||
if (m_current_colur != col){
|
||||
@@ -896,6 +947,15 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(m_nozzle_extruders.size())) {
|
||||
m_nozzle_extruders[nozzle_id]->OnAmsLoading(load, col);
|
||||
if (load) m_current_colur = col;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
/*return true if something is updated*/
|
||||
@@ -905,12 +965,26 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_series_name = series_name;
|
||||
m_nozzle_num = nozzle_num;
|
||||
this->DestroyChildren();
|
||||
m_left_extruder = nullptr;
|
||||
m_right_extruder = nullptr;
|
||||
m_nozzle_extruders.clear();
|
||||
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num == 1) {
|
||||
if (MachineObject::is_series_n(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
} else if (MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name)) {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
} else {
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
wxBoxSizer* m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
if (m_nozzle_num >= 2)
|
||||
m_left_extruder->setShowState(true);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
else if (m_nozzle_num == 2)
|
||||
{
|
||||
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(true);
|
||||
@@ -918,25 +992,17 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
|
||||
m_bitmap_sizer->AddSpacer(2);
|
||||
}
|
||||
else if (m_nozzle_num > 2) {
|
||||
for (int nozzle_id = 0; nozzle_id < m_nozzle_num; ++nozzle_id) {
|
||||
auto *extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
extruder->setShowState(true);
|
||||
m_nozzle_extruders.push_back(extruder);
|
||||
m_bitmap_sizer->Add(extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MachineObject::is_series_n(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
|
||||
}
|
||||
else if(MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name))
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
|
||||
}
|
||||
|
||||
m_left_extruder->setShowState(true);
|
||||
m_right_extruder->setShowState(false);
|
||||
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
|
||||
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(3));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
|
||||
SetSizer(m_bitmap_sizer);
|
||||
@@ -951,6 +1017,9 @@ void AMSextruder::msw_rescale()
|
||||
//m_amsSextruder->msw_rescale();
|
||||
if (m_left_extruder) m_left_extruder->msw_rescale();
|
||||
if (m_right_extruder) m_right_extruder->msw_rescale();
|
||||
for (auto *extruder : m_nozzle_extruders) {
|
||||
if (extruder) extruder->msw_rescale();
|
||||
}
|
||||
Layout();
|
||||
Update();
|
||||
Refresh();
|
||||
@@ -2123,7 +2192,10 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
|
||||
m_ams_model = model;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
create(parent, id, pos, wxSize(FromDIP(264), FromDIP(34)));
|
||||
const int width = use_generic_ams_layout(m_ams_model)
|
||||
? generic_ams_layout_width(this, static_cast<int>(m_amsinfo.cans.size()))
|
||||
: FromDIP(264);
|
||||
create(parent, id, pos, wxSize(width, FromDIP(34)));
|
||||
}
|
||||
else{
|
||||
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
|
||||
@@ -2235,6 +2307,38 @@ void AMSRoadUpPart::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
|
||||
|
||||
if (use_generic_ams_layout(m_ams_model)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
if (lane_count <= 0)
|
||||
return;
|
||||
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int height = FromDIP(21);
|
||||
for (const int lane_center : lane_centers)
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
|
||||
if (lane_centers.size() > 1)
|
||||
dc.DrawLine(lane_centers.front(), height, lane_centers.back(), height);
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
|
||||
if (m_load_step != AMSPassRoadSTEP::AMS_ROAD_STEP_NONE &&
|
||||
m_load_slot_index >= 0 && m_load_slot_index < lane_count) {
|
||||
const int lane_center = lane_centers[m_load_slot_index];
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_amsinfo.cans[m_load_slot_index].material_colour), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(lane_center, 0, lane_center, height);
|
||||
dc.DrawLine(std::min(lane_center, size.x / 2), height,
|
||||
std::max(lane_center, size.x / 2), height);
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3)
|
||||
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
|
||||
}
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(wxColour(194, 194, 194)));
|
||||
dc.DrawRectangle(size.x / 2 - FromDIP(14), height - FromDIP(5), FromDIP(28), FromDIP(10));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((m_ams_model == N3S_AMS || m_ams_model == EXT_AMS) && m_amsinfo.cans.size() != 4){
|
||||
dc.DrawLine(((float)size.x / 2), (0), ((float)size.x / 2), (size.y));
|
||||
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 || m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3){
|
||||
@@ -2360,6 +2464,25 @@ void AMSRoadDownPart::UpdateRight(int nozzle_num, AMSRoadShowMode mode)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetNozzleCount(int nozzle_num)
|
||||
{
|
||||
const int new_nozzle_num = std::max(1, nozzle_num);
|
||||
if (m_nozzle_num == new_nozzle_num) return;
|
||||
|
||||
m_nozzle_num = new_nozzle_num;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::SetSingleSideLayout(bool enabled, AMSPanelPos pos)
|
||||
{
|
||||
if (m_single_side_layout == enabled && m_single_side_pos == pos)
|
||||
return;
|
||||
|
||||
m_single_side_layout = enabled;
|
||||
m_single_side_pos = pos;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
|
||||
{
|
||||
/*m_vams_loading = load;
|
||||
@@ -2433,8 +2556,14 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
/*if (m_road_color.Alpha() == 0) { dc.SetPen(wxPen(*wxWHITE, m_passroad_width, wxPENSTYLE_SOLID)); }
|
||||
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
|
||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||
|
||||
auto xpos = left_nozzle_pos.x;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE){
|
||||
if (m_single_side_layout) {
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
else if (
|
||||
(m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE ||
|
||||
m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE)){
|
||||
auto length = 50;
|
||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
||||
length = -13;
|
||||
@@ -2465,6 +2594,9 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
||||
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -2492,23 +2624,41 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(left_nozzle_pos.x, (size.y / 2), left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||
break;
|
||||
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||
// Generic N-lane geometry is intentionally left unimplemented.
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_single_side_layout) {
|
||||
if (m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
|
||||
m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3) {
|
||||
dc.SetPen(wxPen(_get_diff_clr(this, m_road_color[1]), 4, wxPENSTYLE_SOLID));
|
||||
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE){
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
const auto &nozzle_pos = m_single_side_layout && m_single_side_pos == AMSPanelPos::RIGHT_PANEL
|
||||
? right_nozzle_pos
|
||||
: left_nozzle_pos;
|
||||
dc.DrawLine(nozzle_pos.x, (size.y / 2), nozzle_pos.x, (size.y));
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
/*dc.DrawLine(FromDIP(left_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(left_nozzle_pos.x), FromDIP(size.y));
|
||||
dc.DrawLine(FromDIP(right_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(right_nozzle_pos.x), FromDIP(size.y));*/
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
|
||||
}
|
||||
else {
|
||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
|
||||
}
|
||||
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
|
||||
// Generic N Nozzles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2533,10 +2683,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2544,10 +2700,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
else{
|
||||
int x = left_nozzle_pos.x;
|
||||
int len = m_right_road_length;
|
||||
if (m_nozzle_num == 2) {
|
||||
if (m_nozzle_num == 1) {
|
||||
// Keep the single-nozzle path anchored at the left nozzle.
|
||||
}
|
||||
else if (m_nozzle_num == 2) {
|
||||
x = right_nozzle_pos.x;
|
||||
len = len - 14;
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||
@@ -2571,7 +2733,7 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
||||
}
|
||||
|
||||
void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP step) {
|
||||
if (m_nozzle_num >= 2){
|
||||
if (m_nozzle_num == 2){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL){
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
||||
m_left_road_length = len;;
|
||||
@@ -2583,7 +2745,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else{
|
||||
else if (m_nozzle_num == 1){
|
||||
if (pos == AMSPanelPos::LEFT_PANEL) {
|
||||
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
||||
m_left_road_length = len;
|
||||
@@ -2597,6 +2759,9 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
||||
m_pass_road_right_step = step;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Generic N Nozzles
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -2615,7 +2780,10 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
|
||||
{
|
||||
wxWindow::Create(parent, id, pos);
|
||||
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
if (itemType == AMSModel::GENERIC_AMS && use_generic_ams_layout(itemType))
|
||||
create(parent, id, pos, wxSize(generic_ams_preview_width(this, static_cast<int>(amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y));
|
||||
else
|
||||
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
|
||||
}
|
||||
else {
|
||||
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
||||
@@ -2650,7 +2818,13 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
|
||||
m_amsinfo = amsinfo;
|
||||
m_ams_item_type = amsinfo.ams_type;
|
||||
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const wxSize generic_size(generic_ams_preview_width(this, static_cast<int>(m_amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
|
||||
SetMinSize(AMS_PREV_FOUR_SIZE);
|
||||
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
||||
} else {
|
||||
@@ -2758,8 +2932,41 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
||||
|
||||
auto left = 0;
|
||||
//four slot
|
||||
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
// Generic AMS previews use the same normalized lane centers as the card row and connector.
|
||||
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
|
||||
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
|
||||
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
|
||||
const int cube_width = FromDIP(9);
|
||||
const int cube_height = FromDIP(14);
|
||||
|
||||
for (int lane = 0; lane < lane_count; ++lane) {
|
||||
const Caninfo &can = m_amsinfo.cans[lane];
|
||||
const int cube_left = lane_centers[lane] - cube_width / 2;
|
||||
const int cube_top = (size.y - cube_height) / 2;
|
||||
wxRect rect(cube_left, cube_top, cube_width, cube_height);
|
||||
|
||||
if (can.material_colour.Alpha() == 0) {
|
||||
if (wxGetApp().dark_mode())
|
||||
dc.DrawBitmap(m_ts_bitmap_cube_dark.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube_dark.GetBmpHeight()) / 2);
|
||||
else
|
||||
dc.DrawBitmap(m_ts_bitmap_cube.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube.GetBmpHeight()) / 2);
|
||||
}
|
||||
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
|
||||
dc.SetPen(wxPen(wxColor(0, 0, 0)));
|
||||
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1),
|
||||
rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
|
||||
}
|
||||
else {
|
||||
wxColour color = can.material_colour;
|
||||
change_the_opacity(color);
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(color));
|
||||
dc.DrawRoundedRectangle(rect, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// four slot
|
||||
else if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
||||
left = FromDIP(8);
|
||||
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
|
||||
|
||||
@@ -3158,10 +3365,19 @@ AmsItem::AmsItem(wxWindow *parent,AMSinfo info, AMSModel model, AMSPanelPos pos
|
||||
m_panel_pos = pos;
|
||||
|
||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
|
||||
SetSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMinSize(AMS_CANS_WINDOW_SIZE);
|
||||
SetMaxSize(AMS_CANS_WINDOW_SIZE);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
|
||||
? generic_ams_layout_width(this, lane_count)
|
||||
: FromDIP(264);
|
||||
const wxSize generic_size(width, FromDIP(174));
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, generic_size);
|
||||
SetSize(generic_size);
|
||||
SetMinSize(generic_size);
|
||||
// For large lane counts the card is a viewport, not the lane strip. Let
|
||||
// its parent allocate the available width; the scrolled child retains
|
||||
// the wider virtual content size.
|
||||
if (!(use_generic_ams_layout(m_ams_model) && lane_count > 8))
|
||||
SetMaxSize(generic_size);
|
||||
}
|
||||
else{
|
||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
|
||||
@@ -3187,12 +3403,43 @@ void AmsItem::create(wxWindow *parent)
|
||||
if (m_ams_model != AMSModel::AMS_LITE) {
|
||||
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer_item = new wxBoxSizer(wxVERTICAL);
|
||||
auto it = m_info.cans.begin();
|
||||
for (; it != m_info.cans.end(); it++) {
|
||||
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
|
||||
const bool generic_layout = use_generic_ams_layout(m_ams_model);
|
||||
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||
const int content_width = generic_layout ? generic_ams_layout_width(this, lane_count) : 0;
|
||||
const bool scroll_lanes = generic_layout && lane_count > 8;
|
||||
const auto lane_centers = generic_layout
|
||||
? generic_ams_lane_centers(lane_count, content_width)
|
||||
: std::vector<int>();
|
||||
|
||||
m_can_parent = this;
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll = new wxScrolledWindow(this, wxID_ANY);
|
||||
m_can_scroll->SetScrollRate(FromDIP(10), 0);
|
||||
m_can_scroll->SetMinSize(wxSize(FromDIP(264), FromDIP(143)));
|
||||
m_can_scroll->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||
m_can_scroll_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
m_can_scroll->SetSizer(m_can_scroll_sizer);
|
||||
m_can_parent = m_can_scroll;
|
||||
}
|
||||
|
||||
int previous_right = 0;
|
||||
for (int can_index = 0; can_index < lane_count; ++can_index) {
|
||||
if (generic_layout) {
|
||||
const int left = lane_centers[can_index] - FromDIP(26);
|
||||
sizer_can->AddSpacer(std::max(0, left - previous_right));
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
previous_right = left + FromDIP(52);
|
||||
}
|
||||
else {
|
||||
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
|
||||
}
|
||||
m_can_count++;
|
||||
}
|
||||
it = m_info.cans.begin();
|
||||
if (generic_layout)
|
||||
sizer_can->AddSpacer(std::max(0, content_width - previous_right));
|
||||
if (generic_layout)
|
||||
sizer_can->SetMinSize(wxSize(content_width, -1));
|
||||
|
||||
//auto road_panel = new wxWindow(this, wxID_ANY);
|
||||
//auto road_panel = new wxPanel(this, wxID_ANY);
|
||||
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
|
||||
@@ -3207,11 +3454,18 @@ void AmsItem::create(wxWindow *parent)
|
||||
sizer_item->Add(m_ext_image, 0, wxALIGN_CENTER, 0);
|
||||
}
|
||||
}
|
||||
m_panel_road = new AMSRoadUpPart(this, wxID_ANY, m_info, m_ams_model);
|
||||
m_panel_road = new AMSRoadUpPart(m_can_parent, wxID_ANY, m_info, m_ams_model);
|
||||
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
if (scroll_lanes) {
|
||||
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
|
||||
m_can_scroll_sizer->Add(m_panel_road, 0, wxALIGN_LEFT, 0);
|
||||
sizer_item->Add(m_can_scroll, 1, wxEXPAND);
|
||||
}
|
||||
else {
|
||||
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
||||
}
|
||||
|
||||
SetSizer(sizer_item);
|
||||
}
|
||||
@@ -3235,13 +3489,17 @@ void AmsItem::create(wxWindow *parent)
|
||||
|
||||
Layout();
|
||||
Fit();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
Thaw();
|
||||
//Refresh();
|
||||
}
|
||||
|
||||
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
|
||||
{
|
||||
auto amscan = new wxWindow(this, wxID_ANY);
|
||||
auto amscan = new wxWindow(m_can_parent ? m_can_parent : this, wxID_ANY);
|
||||
|
||||
amscan->SetSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
amscan->SetMinSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||
@@ -3322,7 +3580,8 @@ void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* size
|
||||
amscan->Layout();
|
||||
amscan->Fit();
|
||||
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, FromDIP(5));
|
||||
const int margin = use_generic_ams_layout(m_ams_model) ? 0 : FromDIP(5);
|
||||
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, margin);
|
||||
|
||||
m_can_lib_list[caninfo.can_id] = m_panel_lib;
|
||||
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
||||
@@ -3447,6 +3706,10 @@ void AmsItem::UpdateInfo(AMSinfo info)
|
||||
}
|
||||
|
||||
Layout();
|
||||
if (m_can_scroll) {
|
||||
m_can_scroll->Layout();
|
||||
m_can_scroll->FitInside();
|
||||
}
|
||||
}
|
||||
|
||||
void AmsItem::SetDefSelectCan()
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <wx/string.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/scrolwin.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
|
||||
|
||||
@@ -96,6 +97,7 @@ enum class AMSRoadShowMode : int {
|
||||
AMS_ROAD_MODE_SINGLE,
|
||||
AMS_ROAD_MODE_SINGLE_N3S,
|
||||
AMS_ROAD_MODE_AMS_LITE,
|
||||
AMS_ROAD_MODE_GENERIC,
|
||||
AMS_ROAD_MODE_NONE
|
||||
};
|
||||
|
||||
@@ -480,6 +482,7 @@ public:
|
||||
//AMSextruderImage *m_amsSextruder{nullptr};
|
||||
AMSextruderImage* m_left_extruder = nullptr;
|
||||
AMSextruderImage* m_right_extruder = nullptr;
|
||||
std::vector<AMSextruderImage*> m_nozzle_extruders;
|
||||
AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
|
||||
~AMSextruder();
|
||||
|
||||
@@ -681,6 +684,8 @@ public:
|
||||
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
||||
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
||||
void UpdateRight(int nozzle_num, AMSRoadShowMode mode);
|
||||
void SetNozzleCount(int nozzle_num);
|
||||
void SetSingleSideLayout(bool enabled, AMSPanelPos pos = AMSPanelPos::LEFT_PANEL);
|
||||
|
||||
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
|
||||
void SetPassRoadColour(bool left, wxColour col);
|
||||
@@ -698,6 +703,8 @@ private:
|
||||
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||
bool m_selected = {false};
|
||||
bool m_single_side_layout = {false};
|
||||
AMSPanelPos m_single_side_pos = {AMSPanelPos::LEFT_PANEL};
|
||||
|
||||
int m_left_road_length = {-1};
|
||||
int m_right_road_length = {-1};
|
||||
@@ -864,6 +871,9 @@ private:
|
||||
|
||||
std::map<std::string, AMSLib*> m_can_lib_list;
|
||||
//std::map<std::string, AMSRoad*> m_can_road_list;
|
||||
wxScrolledWindow* m_can_scroll = { nullptr };
|
||||
wxBoxSizer* m_can_scroll_sizer = { nullptr };
|
||||
wxWindow* m_can_parent = { nullptr };
|
||||
AMSRoadUpPart* m_panel_road = { nullptr };
|
||||
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
||||
AMSHumidity* m_humidity = { nullptr };
|
||||
|
||||
@@ -420,6 +420,14 @@ ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent)
|
||||
|
||||
void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type)
|
||||
{
|
||||
// The badge renders exactly two extruders (left/right). A generic non-BBL printer with
|
||||
// N > 2 can report an out-of-range id here (see MultiNozzleStatusTable::UpdateRackInfo);
|
||||
// reject it rather than writing past the two-element lists.
|
||||
if (extruder_id < 0 || extruder_id >= static_cast<int>(m_diameter_list.size())) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": out-of-range extruder_id " << extruder_id;
|
||||
return;
|
||||
}
|
||||
|
||||
m_diameter_list[extruder_id] = diameter;
|
||||
m_volume_type_list[extruder_id] = volume_type;
|
||||
|
||||
@@ -867,14 +875,21 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
|
||||
bool has_right = false;
|
||||
for (auto& elem : nozzles_in_extruder) {
|
||||
auto& nozzle = elem.second;
|
||||
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
|
||||
int extruder_id{};
|
||||
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) {
|
||||
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
||||
if (nozzle.AtRightExtruder())
|
||||
has_right = true;
|
||||
}
|
||||
else
|
||||
extruder_id = nozzle.GetExtruderId();
|
||||
|
||||
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
|
||||
|
||||
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
||||
}
|
||||
// TODO: Update for N extruders
|
||||
m_badge->SetExtruderValid(has_right);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +184,9 @@ void TempInput::SetFinish()
|
||||
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
||||
event.SetInt(temp_type);
|
||||
event.SetString(wxString::Format("%d", m_input_type));
|
||||
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
|
||||
// tell them apart; carry widget identity so the listener can find which one fired.
|
||||
event.SetEventObject(this);
|
||||
wxPostEvent(this->GetParent(), event);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -38,6 +39,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cctype>
|
||||
#include <ios>
|
||||
@@ -61,6 +63,42 @@ namespace websocket = beast::websocket;
|
||||
namespace net = boost::asio;
|
||||
using tcp = net::ip::tcp;
|
||||
|
||||
// Decimal value of a string that is nothing but digits, or -1 when it is empty or not a number.
|
||||
int decimal_number(const std::string& digits)
|
||||
{
|
||||
if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](unsigned char c) { return std::isdigit(c) != 0; }))
|
||||
return -1;
|
||||
try {
|
||||
return std::stoi(digits);
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Lane database keys address a lane either as "T<N>" (0-based tool index) or as "lane<N>"
|
||||
// (1-based, the AFC/Happy Hare style). Returns -1 when the key carries no usable lane number.
|
||||
int lane_index_from_key(const std::string& lane_key)
|
||||
{
|
||||
if (lane_key.size() > 1 && (lane_key[0] == 'T' || lane_key[0] == 't'))
|
||||
return decimal_number(lane_key.substr(1));
|
||||
if (lane_key.size() > 4 && lane_key.compare(0, 4, "lane") == 0) {
|
||||
const int lane_number = decimal_number(lane_key.substr(4));
|
||||
return lane_number > 0 ? lane_number - 1 : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// AFC names toolheads "extruder", "extruder1", ... and derives the toolhead index from the name
|
||||
// itself (AFC_lane.lane_extruder_index): no suffix is 0, otherwise the suffix is the index.
|
||||
int extruder_index_from_name(const std::string& toolhead)
|
||||
{
|
||||
if (toolhead == "extruder")
|
||||
return 0;
|
||||
if (toolhead.size() > 8 && toolhead.compare(0, 8, "extruder") == 0)
|
||||
return decimal_number(toolhead.substr(8));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct WsEndpoint
|
||||
{
|
||||
std::string host;
|
||||
@@ -443,19 +481,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
|
||||
// Calculate ams_exist_bits and tray_exist_bits
|
||||
@@ -463,11 +489,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
||||
ams_exist_bits |= (1 << ams_id);
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
ams_unit["info"] = "0002"; // treat as AMS_LITE
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
|
||||
@@ -483,43 +507,147 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = std::to_string(slot_id);
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray && tray->has_filament) {
|
||||
tray_exist_bits |= (1 << slot_index);
|
||||
|
||||
tray_json["tray_info_idx"] = tray->tray_info_idx;
|
||||
tray_json["tray_type"] = tray->tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray->tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray->bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
|
||||
}
|
||||
if (tray->nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
if (tray && tray->has_filament && slot_index < 32) {
|
||||
tray_exist_bits |= (1UL << slot_index);
|
||||
}
|
||||
|
||||
tray_array.push_back(tray_json);
|
||||
static const AmsTrayData empty_tray;
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot_id), tray ? *tray : empty_tray));
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// Generic unit: type 1 (AMS) maps to AMSModel::GENERIC_AMS, which drives the
|
||||
// variable-lane rendering. This is not a real Bambu AMS-Lite.
|
||||
ams_unit["info"] = "0001";
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector<AmsTrayData>& trays)
|
||||
{
|
||||
// One AMS box per extruder, best effort. Lane-data changers (AFC, recent Happy Hare) report an
|
||||
// extruder per lane but no physical box layout, so a box here is a logical feed group, not a
|
||||
// discovered unit. Lanes without an extruder fall back to the main extruder.
|
||||
std::map<int, std::vector<const AmsTrayData*>> boxes;
|
||||
for (const auto& tray : trays) {
|
||||
boxes[tray.extruder_id].push_back(&tray);
|
||||
}
|
||||
|
||||
nlohmann::json ams_array = nlohmann::json::array();
|
||||
unsigned long ams_exist_bits = 0;
|
||||
unsigned long tray_exist_bits = 0;
|
||||
|
||||
int ams_id = 0;
|
||||
// Cumulative lane index across all boxes, in box then slot order. A lane maps to exactly one
|
||||
// extruder, so this is the lane's identity; the parser derives the same index for generic units.
|
||||
int lane_index = 0;
|
||||
for (auto& [extruder_id, lanes] : boxes) {
|
||||
if (ams_id < 32) {
|
||||
ams_exist_bits |= (1UL << ams_id);
|
||||
}
|
||||
|
||||
// Keep a deterministic slot order within the box.
|
||||
std::stable_sort(lanes.begin(), lanes.end(),
|
||||
[](const AmsTrayData* a, const AmsTrayData* b) { return a->slot_index < b->slot_index; });
|
||||
|
||||
nlohmann::json tray_array = nlohmann::json::array();
|
||||
for (size_t slot = 0; slot < lanes.size(); ++slot) {
|
||||
const AmsTrayData& tray = *lanes[slot];
|
||||
// A logical feed group is not a four-slot Bambu AMS: the bit is the lane's cumulative
|
||||
// index, not ams_id*4+slot. DevFilaSystemParser::ParseV1_0 reads the same index.
|
||||
const int bit = lane_index++;
|
||||
if (tray.has_filament && bit < 32) {
|
||||
tray_exist_bits |= (1UL << bit);
|
||||
}
|
||||
tray_array.push_back(make_ams_tray_json(std::to_string(slot), tray));
|
||||
}
|
||||
|
||||
nlohmann::json ams_unit = nlohmann::json::object();
|
||||
ams_unit["id"] = std::to_string(ams_id);
|
||||
// info: bits 0-3 = unit type (1 = AMS, i.e. AMSModel::GENERIC_AMS), bits 8-11 = bound
|
||||
// extruder id. A logical feed group is a generic unit, not a Bambu AMS-Lite.
|
||||
const int info_val = 1 | ((extruder_id & 0x0F) << 8);
|
||||
const char* hex_digits = "0123456789ABCDEF";
|
||||
std::string info = "0000";
|
||||
info[3] = hex_digits[info_val & 0x0F];
|
||||
info[2] = hex_digits[(info_val >> 4) & 0x0F];
|
||||
info[1] = hex_digits[(info_val >> 8) & 0x0F];
|
||||
info[0] = hex_digits[(info_val >> 12) & 0x0F];
|
||||
ams_unit["info"] = info;
|
||||
ams_unit["tray"] = tray_array;
|
||||
ams_array.push_back(ams_unit);
|
||||
++ams_id;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
nlohmann::json MoonrakerPrinterAgent::make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray)
|
||||
{
|
||||
nlohmann::json tray_json = nlohmann::json::object();
|
||||
tray_json["id"] = tray_id;
|
||||
tray_json["tag_uid"] = "0000000000000000";
|
||||
|
||||
if (tray.has_filament) {
|
||||
tray_json["tray_info_idx"] = tray.tray_info_idx;
|
||||
tray_json["tray_type"] = tray.tray_type;
|
||||
tray_json["tray_color"] = normalize_color_value(tray.tray_color);
|
||||
|
||||
// Add temperature data if provided
|
||||
if (tray.bed_temp > 0) {
|
||||
tray_json["bed_temp"] = std::to_string(tray.bed_temp);
|
||||
}
|
||||
if (tray.nozzle_temp > 0) {
|
||||
tray_json["nozzle_temp_max"] = std::to_string(tray.nozzle_temp);
|
||||
}
|
||||
} else {
|
||||
tray_json["tray_info_idx"] = "";
|
||||
tray_json["tray_type"] = "";
|
||||
tray_json["tray_color"] = "00000000";
|
||||
tray_json["tray_slot_placeholder"] = "1";
|
||||
}
|
||||
|
||||
return tray_json;
|
||||
}
|
||||
|
||||
void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits)
|
||||
{
|
||||
// Look up MachineObject via DeviceManager
|
||||
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
|
||||
if (!dev_manager) {
|
||||
return;
|
||||
}
|
||||
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format as hex strings (matching BBL protocol)
|
||||
std::ostringstream ams_exist_ss;
|
||||
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
||||
std::ostringstream tray_exist_ss;
|
||||
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
|
||||
|
||||
nlohmann::json ams_json = nlohmann::json::object();
|
||||
ams_json["ams"] = ams_array;
|
||||
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
||||
ams_json["tray_exist_bits"] = tray_exist_ss.str();
|
||||
@@ -528,9 +656,42 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
|
||||
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);
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
|
||||
|
||||
// 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.
|
||||
@@ -570,17 +731,15 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
|
||||
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
|
||||
<< (max_lane_index + 1) << " lanes";
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
build_ams_payload_grouped(trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt Happy Hare first (more widely adopted, supports more filament changers)
|
||||
// Fall back to the Happy Hare `mmu` object for HH builds that do not publish lane_data
|
||||
if (fetch_hh_filament_info(trays, max_lane_index)) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
|
||||
<< (max_lane_index + 1) << " gates";
|
||||
int ams_count = (max_lane_index + 4) / 4;
|
||||
build_ams_payload(ams_count, max_lane_index, trays);
|
||||
build_ams_payload_grouped(trays);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -686,6 +845,30 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char*
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MoonrakerPrinterAgent::safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value)
|
||||
{
|
||||
auto it = obj.find(key);
|
||||
if (it == obj.end())
|
||||
return default_value;
|
||||
if (it->is_number_integer())
|
||||
return it->get<int>();
|
||||
if (it->is_string()) {
|
||||
const std::string s = it->get<std::string>();
|
||||
if (s.empty())
|
||||
return default_value;
|
||||
try {
|
||||
std::size_t consumed = 0;
|
||||
const int value = std::stoi(s, &consumed);
|
||||
if (consumed != s.size()) // reject partial parses such as "2oops"
|
||||
return default_value;
|
||||
return value;
|
||||
} catch (...) {
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx)
|
||||
{
|
||||
if (arr.is_array() && idx >= 0 && idx < static_cast<int>(arr.size()) && arr[idx].is_string())
|
||||
@@ -788,29 +971,60 @@ 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;
|
||||
}
|
||||
|
||||
// Extract lane index from the "lane" field (tool number, 0-based)
|
||||
std::string lane_str = safe_json_string(lane_obj, "lane");
|
||||
int lane_index = -1;
|
||||
if (!lane_str.empty()) {
|
||||
try {
|
||||
lane_index = std::stoi(lane_str);
|
||||
} catch (...) {
|
||||
lane_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Lane tool number (0-based). AFC reports it as a string in the documented sample but it
|
||||
// can also be a number; accept either and skip lanes without a usable index. Databases
|
||||
// written without a "lane" field fall back to the key, which names the lane either as
|
||||
// "T<N>" (0-based) or as "lane<N>" (1-based).
|
||||
int lane_index = safe_json_int_flexible(lane_obj, "lane", -1);
|
||||
if (lane_index < 0) {
|
||||
continue;
|
||||
lane_index = lane_index_from_key(lane_key);
|
||||
if (lane_index < 0) {
|
||||
++skipped_records;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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, 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 : hh_extruder_for_lane(lane_index);
|
||||
}
|
||||
tray.tray_color = safe_json_string(lane_obj, "color");
|
||||
tray.tray_type = safe_json_string(lane_obj, "material");
|
||||
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
|
||||
@@ -823,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;
|
||||
@@ -915,52 +1139,234 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse gate data
|
||||
// Parse gate data. Every gate becomes a lane so empty/unknown gates surface
|
||||
// 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();
|
||||
max_lane_index = 0;
|
||||
|
||||
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
||||
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
|
||||
int status = safe_array_int(gate_status, gate_idx);
|
||||
if (status <= 0) {
|
||||
continue; // Skip unknown or empty gates
|
||||
}
|
||||
|
||||
// Extract gate data
|
||||
std::string material = safe_array_string(gate_material, gate_idx);
|
||||
std::string color = safe_array_string(gate_color, gate_idx);
|
||||
int nozzle_temp = safe_array_int(gate_temperature, gate_idx);
|
||||
|
||||
// Skip if no material type (empty gate)
|
||||
if (material.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = gate_idx;
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
const auto extruder_it = gate_extruder.find(gate_idx);
|
||||
tray.extruder_id = extruder_it != gate_extruder.end() ? extruder_it->second : 0;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
if (status > 0 && !material.empty()) {
|
||||
tray.tray_type = material;
|
||||
tray.tray_color = color;
|
||||
tray.nozzle_temp = nozzle_temp;
|
||||
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
|
||||
tray.has_filament = true;
|
||||
|
||||
auto* bundle = GUI::wxGetApp().preset_bundle;
|
||||
tray.tray_info_idx = bundle
|
||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
||||
: map_filament_type_to_generic_id(tray.tray_type);
|
||||
} else {
|
||||
tray.has_filament = false;
|
||||
}
|
||||
|
||||
max_lane_index = std::max(max_lane_index, gate_idx);
|
||||
trays.push_back(tray);
|
||||
}
|
||||
|
||||
if (trays.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found";
|
||||
max_lane_index = num_gates - 1;
|
||||
|
||||
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
|
||||
// the toolhead's name carries its own index. Best effort: on failure every lane keeps the fallback
|
||||
// of the main extruder.
|
||||
bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder)
|
||||
{
|
||||
nlohmann::json afc_json;
|
||||
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;
|
||||
}
|
||||
|
||||
const auto& afc = afc_json["result"]["status"]["AFC"];
|
||||
const auto& lanes = afc.contains("lanes") ? afc["lanes"] : nlohmann::json::array();
|
||||
if (!lanes.is_array() || lanes.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Query every lane object in one request; the object names are "AFC_lane <lane name>".
|
||||
std::string query_string;
|
||||
for (const auto& lane : lanes) {
|
||||
if (!lane.is_string()) {
|
||||
return false;
|
||||
}
|
||||
if (!query_string.empty()) {
|
||||
query_string += "&";
|
||||
}
|
||||
query_string += "AFC_lane%20" + lane.get<std::string>();
|
||||
}
|
||||
|
||||
nlohmann::json lanes_json;
|
||||
if (!http_get_json("/printer/objects/query?" + query_string, lanes_json) || !lanes_json.contains("result") ||
|
||||
!lanes_json["result"].contains("status")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& status = lanes_json["result"]["status"];
|
||||
for (const auto& lane : lanes) {
|
||||
const std::string name = lane.get<std::string>();
|
||||
const std::string key = "AFC_lane " + name;
|
||||
if (!status.contains(key) || !status[key].is_object() || !status[key].contains("extruder") ||
|
||||
!status[key]["extruder"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
const std::string toolhead = status[key]["extruder"].get<std::string>();
|
||||
const int toolhead_index = extruder_index_from_name(toolhead);
|
||||
if (toolhead_index >= 0) {
|
||||
lane_extruder[name] = toolhead_index;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1059,6 +1465,25 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
|
||||
}
|
||||
}
|
||||
|
||||
// Tool selection is printer-specific G-code. Moonraker forwards the
|
||||
// conventional T<index> command to Klipper, where the printer can
|
||||
// implement logical selection or a physical toolhead attach macro.
|
||||
if (cmd == "select_extruder") {
|
||||
if (!json["print"].contains("extruder_index") || !json["print"]["extruder_index"].is_number_integer()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder missing integer extruder_index, full json: " << json_str;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
const int extruder_idx = json["print"]["extruder_index"].get<int>();
|
||||
if (extruder_idx < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder received negative extruder_index: " << extruder_idx;
|
||||
return BAMBU_NETWORK_ERR_INVALID_RESULT;
|
||||
}
|
||||
|
||||
send_gcode_async(dev_id, "T" + std::to_string(extruder_idx));
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
if (cmd == "home") {
|
||||
send_gcode_async(dev_id, "G28");
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
@@ -1155,7 +1580,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
|
||||
nlohmann::json& status,
|
||||
std::string& error) const
|
||||
{
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&toolhead&extruder&heater_bed&fan");
|
||||
|
||||
std::string response_body;
|
||||
bool success = false;
|
||||
@@ -1563,11 +1988,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
||||
}
|
||||
|
||||
for (const auto& name : this->available_objects) {
|
||||
if (name == "extruder" || name.rfind("extruder", 0) == 0) {
|
||||
const bool is_main_extruder = name == "extruder";
|
||||
const bool is_additional_extruder =
|
||||
name.rfind("extruder", 0) == 0 && name.size() > 8 &&
|
||||
std::all_of(name.begin() + 8, name.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
|
||||
if (is_main_extruder || is_additional_extruder) {
|
||||
subscribe_objects.insert(name);
|
||||
if (name == "extruder") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1781,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;
|
||||
@@ -1863,6 +2314,80 @@ 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.
|
||||
const auto extruder_status = collect_extruder_status();
|
||||
|
||||
if (!extruder_status.empty()) {
|
||||
auto& device_extruder = payload["print"]["device"]["extruder"];
|
||||
|
||||
int current_extruder_id = 0;
|
||||
if (status_cache.contains("toolhead") && status_cache["toolhead"].is_object() &&
|
||||
status_cache["toolhead"].contains("extruder") && status_cache["toolhead"]["extruder"].is_string()) {
|
||||
const std::string active_extruder = status_cache["toolhead"]["extruder"].get<std::string>();
|
||||
if (active_extruder != "extruder" && active_extruder.rfind("extruder", 0) == 0 && active_extruder.size() > 8 &&
|
||||
std::all_of(active_extruder.begin() + 8, active_extruder.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
|
||||
try {
|
||||
const int parsed_id = std::stoi(active_extruder.substr(8));
|
||||
if (parsed_id >= 0 && parsed_id < 16 && extruder_status.count(parsed_id) != 0) {
|
||||
current_extruder_id = parsed_id;
|
||||
}
|
||||
} catch (...) {
|
||||
// Keep the main extruder as the fallback for malformed status data.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int extruder_count = std::min<int>(static_cast<int>(extruder_status.size()), 0xF);
|
||||
device_extruder["state"] = extruder_count | (current_extruder_id << 4);
|
||||
device_extruder["info"] = nlohmann::json::array();
|
||||
|
||||
auto encoded_temperature = [](const nlohmann::json& status, const char* key) {
|
||||
if (!status.contains(key) || !status[key].is_number()) {
|
||||
return 0;
|
||||
}
|
||||
const double temperature = status[key].get<double>();
|
||||
return std::clamp(static_cast<int>(temperature + 0.5), 0, 65535);
|
||||
};
|
||||
|
||||
for (const auto& [extruder_id, status] : extruder_status) {
|
||||
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()},
|
||||
{"info", 1 << 3}, // The configured Moonraker extruder has a toolhead.
|
||||
{"temp", current_temperature | (target_temperature << 16)},
|
||||
{"spre", 0},
|
||||
{"snow", 0},
|
||||
{"star", 0},
|
||||
{"stat", 0},
|
||||
{"hnow", 0},
|
||||
});
|
||||
}
|
||||
|
||||
payload["print"]["cfg"] = "";
|
||||
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()) {
|
||||
const auto& bed = status_cache["heater_bed"];
|
||||
if (bed.contains("temperature") && bed["temperature"].is_number()) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
@@ -88,6 +89,7 @@ protected:
|
||||
// Tray data for AMS payload building
|
||||
struct AmsTrayData {
|
||||
int slot_index = 0; // 0-based slot index
|
||||
int extruder_id = 0; // Extruder this lane feeds (AFC extruder_index); 0 when unknown
|
||||
bool has_filament = false;
|
||||
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||
@@ -99,6 +101,10 @@ protected:
|
||||
// Build ams JSON and call parser
|
||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Build ams JSON with one box per extruder (best effort, for lane_data-based filament
|
||||
// changers such as AFC) and call the parser. Boxes are ordered by extruder id.
|
||||
void build_ams_payload_grouped(const std::vector<AmsTrayData>& trays);
|
||||
|
||||
// Methods that derived classes may need to override or access
|
||||
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
|
||||
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
|
||||
@@ -139,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);
|
||||
@@ -163,10 +171,31 @@ private:
|
||||
// System-specific filament fetch methods
|
||||
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||
// 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);
|
||||
|
||||
// Wrap the AMS boxes into the payload ParseV1_0 understands and apply the pull-mode
|
||||
// readiness state (printer_type, push counters, storage, module versions).
|
||||
void submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits);
|
||||
|
||||
// JSON helper methods
|
||||
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
||||
static int safe_json_int(const nlohmann::json& obj, const char* key);
|
||||
// Accepts either a JSON number or a numeric string (AFC reports some fields as strings).
|
||||
static int safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value);
|
||||
static std::string safe_array_string(const nlohmann::json& arr, int idx);
|
||||
static int safe_array_int(const nlohmann::json& arr, int idx);
|
||||
static std::string normalize_color_value(const std::string& color);
|
||||
|
||||
@@ -2304,6 +2304,105 @@ TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// One system "Generic PLA" the sync can resolve a reported "PLA" tray to, with a real
|
||||
// filament_id so a tray that does report one still maps through the known-id path.
|
||||
void add_generic_pla(PresetBundle &bundle)
|
||||
{
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "Generic PLA");
|
||||
pla.is_system = true;
|
||||
pla.is_compatible = true;
|
||||
pla.filament_id = "GFL99";
|
||||
}
|
||||
|
||||
// A device tray as build_filament_ams_list builds it: a reported type, color and loaded flag,
|
||||
// an optional preset id, and the placeholder flag pull-mode agents set on empty slots.
|
||||
DynamicPrintConfig ams_tray(const std::string &filament_id, const std::string &filament_type, bool exist, bool placeholder)
|
||||
{
|
||||
DynamicPrintConfig tray;
|
||||
tray.set_key_value("filament_id", new ConfigOptionStrings{filament_id});
|
||||
tray.set_key_value("filament_type", new ConfigOptionStrings{filament_type});
|
||||
tray.set_key_value("filament_colour", new ConfigOptionStrings{"#FF0000"});
|
||||
tray.set_key_value("filament_colour_type", new ConfigOptionStrings{"1"});
|
||||
tray.set_key_value("filament_multi_colour", new ConfigOptionStrings{});
|
||||
tray.set_key_value("filament_exist", new ConfigOptionBools{exist});
|
||||
tray.set_key_value("filament_slot_placeholder", new ConfigOptionBools{placeholder});
|
||||
tray.set_key_value("ams_id", new ConfigOptionStrings{"0"});
|
||||
tray.set_key_value("slot_id", new ConfigOptionStrings{"0"});
|
||||
return tray;
|
||||
}
|
||||
|
||||
// Run the mapped sync over the bundle's single tray and return the filament count it reports,
|
||||
// which is what the sidebar uses to rebuild the Prepare filament list.
|
||||
unsigned int sync_one_tray(PresetBundle &bundle, bool enable_append)
|
||||
{
|
||||
std::vector<std::pair<DynamicPrintConfig *, std::string>> unknowns;
|
||||
std::map<int, AMSMapInfo> maps;
|
||||
MergeFilamentInfo merge;
|
||||
return bundle.sync_ams_list(unknowns, /*use_map=*/true, maps, enable_append, merge, /*color_only=*/false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// A generic device reports type/color/loaded for a spool but no preset id. The Device tab shows
|
||||
// that spool, so the Prepare sidebar has to count it: the sync appends the generic preset its
|
||||
// reported type resolves to. A tray that only carried topology (no type, not loaded) must not.
|
||||
TEST_CASE("A data-bearing AMS tray without a preset id appends a generic filament on sync", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_generic_pla(bundle);
|
||||
bundle.set_num_filaments(1, std::string("#00FF00"));
|
||||
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false);
|
||||
|
||||
const size_t before = bundle.filament_presets.size();
|
||||
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
|
||||
|
||||
CHECK(count == before + 1);
|
||||
CHECK(bundle.filament_presets.back() == "Generic PLA");
|
||||
}
|
||||
|
||||
TEST_CASE("An empty synthetic AMS tray does not append a filament on sync", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_generic_pla(bundle);
|
||||
bundle.set_num_filaments(1, std::string("#00FF00"));
|
||||
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/false);
|
||||
|
||||
const size_t before = bundle.filament_presets.size();
|
||||
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
|
||||
|
||||
CHECK(count == before);
|
||||
}
|
||||
|
||||
TEST_CASE("An empty AMS placeholder does not append a filament on sync", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_generic_pla(bundle);
|
||||
bundle.set_num_filaments(1, std::string("#00FF00"));
|
||||
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, /*filament_type=*/{}, /*exist=*/false, /*placeholder=*/true);
|
||||
|
||||
const size_t before = bundle.filament_presets.size();
|
||||
const auto count = sync_one_tray(bundle, /*enable_append=*/true);
|
||||
|
||||
CHECK(count == before);
|
||||
}
|
||||
|
||||
// The append preference still governs a data-bearing tray: with append off the sidebar count is
|
||||
// unchanged, exactly as for a mapped-but-unlisted tray.
|
||||
TEST_CASE("The append preference gates a data-bearing AMS tray without a preset id", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_generic_pla(bundle);
|
||||
bundle.set_num_filaments(1, std::string("#00FF00"));
|
||||
bundle.filament_ams_list[0] = ams_tray(/*filament_id=*/{}, "PLA", /*exist=*/true, /*placeholder=*/false);
|
||||
|
||||
const size_t before = bundle.filament_presets.size();
|
||||
const auto count = sync_one_tray(bundle, /*enable_append=*/false);
|
||||
|
||||
CHECK(count == before);
|
||||
}
|
||||
|
||||
// A mix is described by 1-based indices into the project's filament list, which Orca rebuilds
|
||||
// from the selected printer's snapshot (filament_%02u / filament_colors) at startup and on every
|
||||
// printer selection. Held anywhere but that same per-printer snapshot, the mixed arrays end up
|
||||
|
||||
@@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_config_value_formatter.cpp
|
||||
test_creality_cfs_match.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_dev_extder_system.cpp
|
||||
test_device_access.cpp
|
||||
test_moonraker_discovery.cpp
|
||||
test_network_discovery.cpp
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Match the include environment that libslic3r_gui TUs get from pchheader.hpp: Windows.h with
|
||||
// WIN32_LEAN_AND_MEAN/NOMINMAX must come first so rpcndr.h's `byte` is processed before <cstddef>
|
||||
// makes std::byte a competing candidate (otherwise the Windows COM headers pulled in via
|
||||
// DeviceManager.hpp error with an ambiguous `byte`). wx/timer.h must precede DeviceManager.hpp,
|
||||
// which includes DeviceErrorDialog.hpp (uses wxTimerEvent) before its own wx/timer.h include.
|
||||
#include "catch2/catch_test_macros.hpp"
|
||||
#include "catch2/catch_message.hpp"
|
||||
#include <slic3r/GUI/DeviceCore/DevDefs.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <wx/timer.h>
|
||||
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Extruder state decodes a generic toolhead set", "[DevExtderSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// device.extruder.state packing: count 0..3, current 4..7, target 8..11,
|
||||
// switch 12..14, loading 15..18, busy 19.
|
||||
const int extruder_count = 4;
|
||||
const int current_extruder = 2;
|
||||
const int switch_state = ES_BUSY;
|
||||
const int loading_extruder = 1;
|
||||
const int state = extruder_count | (current_extruder << 4) | (3 << 8) |
|
||||
(switch_state << 12) | (loading_extruder << 15) | (1 << 19);
|
||||
|
||||
json extruder = { {"state", state}, {"info", json::array()} };
|
||||
for (int id = 0; id < extruder_count; ++id) {
|
||||
// info: bit 1 has filament, bit 3 nozzle present.
|
||||
// temp: current 0..15, target 16..31. spre/snow/star: slot 0..7, ams 8..15.
|
||||
extruder["info"].push_back({
|
||||
{"id", id},
|
||||
{"filam_bak", json::array({id})},
|
||||
{"info", (id % 2 == 0 ? 2 : 0) | 8},
|
||||
{"temp", (200 + id) | ((250 + id) << 16)},
|
||||
{"spre", 10 << 8},
|
||||
{"snow", id | ((20 + id) << 8)},
|
||||
{"star", 30 << 8},
|
||||
{"stat", 0},
|
||||
{"hnow", id},
|
||||
});
|
||||
}
|
||||
|
||||
ExtderSystemParser::ParseV2_0(extruder, obj.GetExtderSystem());
|
||||
|
||||
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == extruder_count);
|
||||
CHECK(obj.GetExtderSystem()->GetCurrentExtderId() == current_extruder);
|
||||
CHECK(static_cast<int>(obj.GetExtderSystem()->GetSwitchState()) == switch_state);
|
||||
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == loading_extruder);
|
||||
CHECK(obj.GetExtderSystem()->IsBusyLoading());
|
||||
|
||||
for (int id = 0; id < extruder_count; ++id) {
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
|
||||
REQUIRE(ext.has_value());
|
||||
INFO("extruder " << id);
|
||||
CHECK(ext->GetExtId() == id);
|
||||
CHECK(ext->GetCurrentTemp() == 200 + id);
|
||||
CHECK(ext->GetTargetTemp() == 250 + id);
|
||||
CHECK(ext->HasFilamentInExt() == (id % 2 == 0));
|
||||
CHECK(ext->GetFilamBackup() == std::vector<int>{id});
|
||||
CHECK(ext->GetSlotPre().ams_id == "10");
|
||||
CHECK(ext->GetSlotNow().ams_id == std::to_string(20 + id));
|
||||
CHECK(ext->GetSlotNow().slot_id == std::to_string(id));
|
||||
CHECK(ext->GetSlotTarget().ams_id == "30");
|
||||
CHECK(ext->GetNozzleId() == id);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Legacy single-toolhead parser fills only the main extruder", "[DevExtderSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == 1);
|
||||
|
||||
// ParseV1_0 is the legacy single/dual fallback. It returns unless the system reports exactly
|
||||
// one toolhead, so it only ever writes MAIN_EXTRUDER_ID's fields.
|
||||
const json legacy = {
|
||||
{"nozzle_temper", 210},
|
||||
{"nozzle_target_temper", 245},
|
||||
{"ams", {{"tray_now", "2"}, {"tray_tar", "3"}}}
|
||||
};
|
||||
ExtderSystemParser::ParseV1_0(legacy, obj.GetExtderSystem());
|
||||
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
|
||||
REQUIRE(ext.has_value());
|
||||
CHECK(ext->GetCurrentTemp() == 210);
|
||||
CHECK(ext->GetTargetTemp() == 245);
|
||||
|
||||
// Below 0x80 the tray pointer encodes ams_id = value / 4, slot_id = value % 4.
|
||||
CHECK(ext->GetSlotNow().ams_id == "0");
|
||||
CHECK(ext->GetSlotNow().slot_id == "2");
|
||||
CHECK(ext->GetSlotTarget().ams_id == "0");
|
||||
CHECK(ext->GetSlotTarget().slot_id == "3");
|
||||
CHECK(obj.GetExtderSystem()->IsBusyLoading());
|
||||
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == MAIN_EXTRUDER_ID);
|
||||
}
|
||||
|
||||
// ParseV2_0 does not enforce the documented extruder dialect invariants: the count in `state` and
|
||||
// the id ordering of `info[]` are trusted, not checked. This test pins the resulting behavior.
|
||||
TEST_CASE("Malformed extruder payload is stored by info array position", "[DevExtderSystem]")
|
||||
{
|
||||
const auto extder_info = [](int id) {
|
||||
// Every key ParseV2_0 reads must be present.
|
||||
return json{ {"id", id}, {"filam_bak", json::array()}, {"info", 0}, {"temp", 0},
|
||||
{"spre", 0}, {"snow", 0}, {"star", 0}, {"stat", 0}, {"hnow", 0} };
|
||||
};
|
||||
|
||||
SECTION("state count disagrees with info length") {
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// `state` reports 2 toolheads but info[] carries 3.
|
||||
json info = json::array();
|
||||
for (int id = 0; id < 3; ++id)
|
||||
info.push_back(extder_info(id));
|
||||
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
|
||||
|
||||
// The info array wins: three toolheads are stored. m_total_extder_count still holds 2, so
|
||||
// GetTotalExtderCount() would trip its debug assert; only the stored vector is inspected.
|
||||
CHECK(obj.GetExtderSystem()->GetTotalExtderSize() == 3);
|
||||
for (int id = 0; id < 3; ++id) {
|
||||
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
|
||||
REQUIRE(ext.has_value());
|
||||
CHECK(ext->GetExtId() == id);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("info id differs from the array index") {
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// ids 1 and 2 sit at array positions 0 and 1.
|
||||
json info = json::array({extder_info(1), extder_info(2)});
|
||||
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
|
||||
|
||||
// Toolheads are stored by array position; the payload id is kept but is not the lookup key.
|
||||
const auto first = obj.GetExtderSystem()->GetExtderById(0);
|
||||
REQUIRE(first.has_value());
|
||||
CHECK(first->GetExtId() == 1);
|
||||
const auto second = obj.GetExtderSystem()->GetExtderById(1);
|
||||
REQUIRE(second.has_value());
|
||||
CHECK(second->GetExtId() == 2);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,15 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <cstddef>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include <vector>
|
||||
#include <catch2/catch_message.hpp>
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
@@ -65,6 +70,209 @@ TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSyste
|
||||
CHECK_FALSE(tray->is_slot_placeholder);
|
||||
}
|
||||
|
||||
TEST_CASE("Agent filament slots preserve every extruder and AMS binding", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
const json filament = json::parse(R"({
|
||||
"units": [
|
||||
{ "id": "0", "extruder": 0, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "PLA", "color": "FF5733FF" },
|
||||
{ "index": 1, "loaded": true, "material": "PLA", "color": "33C1FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "PLA", "color": "8D33FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "1", "extruder": 1, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "ASA", "color": "33C1FFFF" },
|
||||
{ "index": 1, "loaded": true, "material": "ASA", "color": "8D33FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "ASA", "color": "FF5733FF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "2", "extruder": 2, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "ABS", "color": "8D33FFFF" },
|
||||
{ "index": 1, "loaded": true, "material": "ABS", "color": "FF5733FF" },
|
||||
{ "index": 2, "loaded": true, "material": "ABS", "color": "33C1FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] },
|
||||
{ "id": "3", "extruder": 3, "slots": [
|
||||
{ "index": 0, "loaded": true, "material": "PETG", "color": "FF5733FF" },
|
||||
{ "index": 1, "loaded": true, "material": "PETG", "color": "33C1FFFF" },
|
||||
{ "index": 2, "loaded": true, "material": "PETG", "color": "8D33FFFF" },
|
||||
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
|
||||
] }
|
||||
],
|
||||
"external": [
|
||||
{ "extruder": 3, "loaded": true, "material": "PETG", "preset_id": "P3" },
|
||||
{ "extruder": 0, "loaded": true, "material": "PLA", "preset_id": "P0" },
|
||||
{ "extruder": 2, "loaded": false, "material": "ABS", "preset_id": "P2" },
|
||||
{ "extruder": 1, "loaded": true, "material": "ASA", "preset_id": "P1" }
|
||||
]
|
||||
})");
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
REQUIRE(obj.vt_slot.size() == 4);
|
||||
CHECK(obj.vt_slot[0].id == "255");
|
||||
CHECK(obj.vt_slot[1].id == "254");
|
||||
CHECK(obj.vt_slot[2].id == "253");
|
||||
CHECK(obj.vt_slot[3].id == "252");
|
||||
CHECK(obj.vt_slot[0].setting_id == "P0");
|
||||
CHECK(obj.vt_slot[1].setting_id == "P1");
|
||||
CHECK(obj.vt_slot[2].setting_id == "P2");
|
||||
CHECK(obj.vt_slot[3].setting_id == "P3");
|
||||
|
||||
REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 4);
|
||||
for (int extruder = 0; extruder < 4; ++extruder) {
|
||||
const auto& ams = obj.GetFilaSystem()->GetAmsList().at(std::to_string(extruder));
|
||||
CHECK(ams->GetExtruderId() == extruder);
|
||||
CHECK(ams->GetBindedExtruderSet() == std::set<int>{extruder});
|
||||
REQUIRE(ams->GetTrays().size() == 4);
|
||||
CHECK(ams->GetTrays().at("0")->color != ams->GetTrays().at("1")->color);
|
||||
CHECK(ams->GetTrays().at("1")->color != ams->GetTrays().at("2")->color);
|
||||
CHECK_FALSE(ams->GetTrays().at("3")->is_exists);
|
||||
}
|
||||
|
||||
CHECK(obj.contains_tray("253", "0"));
|
||||
CHECK(obj.get_tray("253", "0").setting_id == "P2");
|
||||
CHECK(obj.GetFilaSystem()->GetExtruderIdByAmsId("253") == 2);
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.count(252) == 1);
|
||||
CHECK(tray_index_map.at(252).first == 252);
|
||||
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");
|
||||
|
||||
// A well-formed generic payload pairs "units" with "external". An empty external array resets
|
||||
// vt_slot, clearing the default virtual tray (255) MachineObject's constructor seeds; otherwise
|
||||
// that phantom external tray lands in GetTrayIndexMap and inflates the map size.
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
for (int ams_id = 0; ams_id < 2; ++ams_id) {
|
||||
json unit = {
|
||||
{"id", std::to_string(ams_id)},
|
||||
{"extruder", ams_id},
|
||||
{"slots", json::array()}
|
||||
};
|
||||
for (int slot_id = 0; slot_id < 5; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.size() == 10);
|
||||
CHECK(tray_index_map.at(0).first == 0);
|
||||
CHECK(tray_index_map.at(0).second == 0);
|
||||
CHECK(tray_index_map.at(4).first == 0);
|
||||
CHECK(tray_index_map.at(4).second == 4);
|
||||
CHECK(tray_index_map.at(5).first == 1);
|
||||
CHECK(tray_index_map.at(5).second == 0);
|
||||
CHECK(tray_index_map.at(9).first == 1);
|
||||
CHECK(tray_index_map.at(9).second == 4);
|
||||
}
|
||||
|
||||
TEST_CASE("Generic tray index map handles unequal per-unit slot counts", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
// An "external" array must accompany "units" so vt_slot is reset (see the cumulative test above).
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
const std::vector<int> slot_counts = {4, 12};
|
||||
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
|
||||
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
|
||||
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
|
||||
REQUIRE(tray_index_map.size() == 16);
|
||||
// Unit 0 occupies global lanes 0..3; unit 1 continues at 4..15 because the counter advances
|
||||
// by the reported tray count, not by a fixed four-slot stride.
|
||||
CHECK(tray_index_map.at(0).first == 0);
|
||||
CHECK(tray_index_map.at(0).second == 0);
|
||||
CHECK(tray_index_map.at(3).first == 0);
|
||||
CHECK(tray_index_map.at(3).second == 3);
|
||||
CHECK(tray_index_map.at(4).first == 1);
|
||||
CHECK(tray_index_map.at(4).second == 0);
|
||||
CHECK(tray_index_map.at(15).first == 1);
|
||||
CHECK(tray_index_map.at(15).second == 11);
|
||||
}
|
||||
|
||||
TEST_CASE("A generic AMS unit reports its reported tray count", "[DevFilaSystem]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
|
||||
json filament = { {"units", json::array()}, {"external", json::array()} };
|
||||
const std::vector<int> slot_counts = {8, 1};
|
||||
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
|
||||
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
|
||||
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
|
||||
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
|
||||
filament["units"].push_back(std::move(unit));
|
||||
}
|
||||
|
||||
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
|
||||
|
||||
// Non-Bambu units are not capped at the four-slot Bambu contract; the reported tray count wins.
|
||||
const auto& ams_list = obj.GetFilaSystem()->GetAmsList();
|
||||
REQUIRE(ams_list.at("0")->GetSlotCount() == 8);
|
||||
REQUIRE(ams_list.at("1")->GetSlotCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
|
||||
{
|
||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
@@ -232,3 +440,174 @@ 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";
|
||||
// FilamentInfo.color carries the "#RRGGBBAA" form the GUI builds for a sliced filament
|
||||
// (SelectMachine/SyncAmsInfoDialog); tray colors are the bare RRGGBBAA DevAmsTray::decode_color
|
||||
// consumes. A bare string here is not parsed by wxColour's hex branch and reads as black.
|
||||
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