mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 13:41:03 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53d7b87db5 | ||
|
|
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 |
No files matched your search
@@ -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;
|
<< "\", 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)
|
void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
|
||||||
{
|
{
|
||||||
combox_info.clear();
|
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)
|
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_presets;
|
||||||
std::vector<std::string> ams_filament_colors;
|
std::vector<std::string> ams_filament_colors;
|
||||||
std::vector<std::string> ams_filament_color_types;
|
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 valid{false};
|
||||||
bool is_map{false};
|
bool is_map{false};
|
||||||
bool is_placeholder{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 = "";
|
||||||
std::string filament_color_type = "";
|
std::string filament_color_type = "";
|
||||||
std::string filament_preset = "";
|
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_id = ams.opt_string("filament_id", 0u);
|
||||||
auto filament_color = ams.opt_string("filament_colour", 0u);
|
auto filament_color = ams.opt_string("filament_colour", 0u);
|
||||||
auto filament_color_type = ams.opt_string("filament_colour_type", 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_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed");
|
||||||
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
|
auto filament_multi_color = ams.opt<ConfigOptionStrings>("filament_multi_colour")->values;
|
||||||
auto ams_id = ams.opt_string("ams_id", 0u);
|
auto ams_id = ams.opt_string("ams_id", 0u);
|
||||||
auto slot_id = ams.opt_string("slot_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);
|
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};
|
AMSMapInfo temp = {ams_id, slot_id};
|
||||||
ams_array_maps.push_back(temp);
|
ams_array_maps.push_back(temp);
|
||||||
index++;
|
index++;
|
||||||
@@ -4607,20 +4680,55 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
maps.erase(j);
|
maps.erase(j);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
|
}
|
||||||
auto default_unknown_color = "#CECECE";
|
if (is_placeholder) {
|
||||||
ams_filament_colors.push_back(default_unknown_color);
|
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
|
||||||
ams_filament_color_types.push_back("1");
|
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
|
||||||
if (filament_multi_color.size() == 0) {
|
// drops it so no filament slot is created or retained for an empty tray.
|
||||||
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
|
|
||||||
ams_filament_presets.push_back("");
|
ams_filament_presets.push_back("");
|
||||||
ams_filament_colors.push_back("");
|
ams_filament_colors.push_back("");
|
||||||
ams_filament_color_types.push_back("");
|
ams_filament_color_types.push_back("");
|
||||||
ams_multi_color_filment.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;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4632,49 +4740,19 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bool has_type = false;
|
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) {
|
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;
|
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; });
|
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
|
||||||
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
warn_ambiguous_filament_id_match(filaments, iter, filament_id);
|
||||||
if (iter == filaments.end()) {
|
if (iter == filaments.end()) {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
|
||||||
if (!filament_type.empty()) {
|
std::string resolved_generic;
|
||||||
auto original_type = filament_type;
|
auto generic = find_generic_preset_for_type(filaments, filament_type, resolved_generic);
|
||||||
|
if (!filament_type.empty())
|
||||||
filament_type = "Generic " + filament_type;
|
filament_type = "Generic " + filament_type;
|
||||||
iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto &f) {
|
if (generic != filaments.end()) {
|
||||||
return f.is_compatible && f.is_system
|
iter = generic;
|
||||||
&& boost::algorithm::starts_with(f.name, filament_type);
|
filament_type = resolved_generic;
|
||||||
});
|
|
||||||
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 (iter == filaments.end()) {
|
if (iter == filaments.end()) {
|
||||||
// Prefer old selection
|
// 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;
|
filament_color->values = exist_colors;
|
||||||
ams_multi_color_filment = exist_multi_color_filment;
|
ams_multi_color_filment = exist_multi_color_filment;
|
||||||
merge_info.merges.clear();
|
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
|
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()) {
|
if (i >= ams_filament_presets.size()) {
|
||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)";
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ams_infos[i].filament_preset = ams_filament_presets[i];
|
ams_infos[i].filament_preset = ams_filament_presets[i];
|
||||||
ams_infos[i].mutli_filament_color = ams_multi_color_filment[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) {
|
if (!ams_infos[i].is_map) {
|
||||||
need_append_colors.emplace_back(ams_infos[i]);
|
need_append_colors.emplace_back(ams_infos[i]);
|
||||||
ams_filament_colors[i] = "";
|
ams_filament_colors[i] = "";
|
||||||
@@ -4909,6 +4994,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": entry %1% dropped (no valid id or reported data)") % i;
|
||||||
ams_filament_colors[i] = "";
|
ams_filament_colors[i] = "";
|
||||||
ams_filament_color_types[i] = "";
|
ams_filament_color_types[i] = "";
|
||||||
ams_filament_presets[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(); }),
|
[](std::vector<std::string> &value) { return value.empty(); }),
|
||||||
ams_multi_color_filment.end());
|
ams_multi_color_filment.end());
|
||||||
if (need_append_colors.size() > 0 && enable_append) {
|
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 {
|
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++) {
|
for (size_t i = 0; i < presets.size(); i++) {
|
||||||
if (presets[i] == preset && colors[i] == color) {
|
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++){
|
for (size_t i = 0; i < need_append_colors.size(); i++){
|
||||||
if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){
|
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;
|
break;
|
||||||
}
|
}
|
||||||
auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color);
|
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) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
exist_filament_presets.push_back(need_append_colors[i].filament_preset);
|
||||||
exist_colors.push_back(need_append_colors[i].filament_color);
|
exist_colors.push_back(need_append_colors[i].filament_color);
|
||||||
exist_color_types.push_back(need_append_colors[i].filament_color_type);
|
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);
|
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->values = exist_colors;
|
||||||
filament_color_type->values = exist_color_types;
|
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(
|
result_multi_colors.push_back(
|
||||||
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
|
||||||
: std::vector<std::string>{ams_filament_colors[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()) {
|
} else if (i < exist_presets.size()) {
|
||||||
// Empty tray or beyond tray count: keep existing filament
|
// Empty tray or beyond tray count: keep existing filament
|
||||||
result_colors.push_back(exist_colors[i]);
|
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;
|
filament_color_type->values = result_color_types;
|
||||||
this->filament_presets = result_presets;
|
this->filament_presets = result_presets;
|
||||||
ams_multi_color_filment = result_multi_colors;
|
ams_multi_color_filment = result_multi_colors;
|
||||||
filament_map->values.resize(total, 1);
|
filament_map->values.resize(result_presets.size(), 1);
|
||||||
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
|
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||||
} else {
|
} else {
|
||||||
// BBL: existing wholesale replace
|
// BBL: existing wholesale replace
|
||||||
filament_color->values = ams_filament_colors;
|
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 ams_multi_color_filment
|
||||||
update_filament_multi_color();
|
update_filament_multi_color();
|
||||||
update_multi_material_filament_presets();
|
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();
|
return this->filament_presets.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5138,6 +5233,20 @@ std::vector<std::vector<DynamicPrintConfig>> PresetBundle::get_extruder_filament
|
|||||||
if (extruder_nums > 1) {
|
if (extruder_nums > 1) {
|
||||||
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
|
filament_infos.resize(extruder_nums, std::vector<DynamicPrintConfig>());
|
||||||
for (auto ams_item : filament_ams_list) {
|
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
|
if (ams_item.first & 0x10000) { // right
|
||||||
filament_infos[1].push_back(ams_item.second);
|
filament_infos[1].push_back(ams_item.second);
|
||||||
} else { // left
|
} else { // left
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ public:
|
|||||||
~devPrinterUtil() = delete;
|
~devPrinterUtil() = delete;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
|
static bool IsVirtualSlot(int ams_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(const std::string& ams_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace GUI
|
namespace GUI
|
||||||
@@ -198,3 +198,6 @@ template<> struct std::hash<NozzleDef>
|
|||||||
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
|
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 <cstdlib>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/colour.h>
|
#include <wx/colour.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
@@ -33,6 +34,36 @@
|
|||||||
using namespace nlohmann;
|
using namespace nlohmann;
|
||||||
|
|
||||||
namespace Slic3r {
|
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; }
|
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)
|
wxColour DevAmsTray::decode_color(const std::string &color)
|
||||||
@@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const
|
|||||||
|
|
||||||
int DevAms::GetSlotCount() const
|
int DevAms::GetSlotCount() const
|
||||||
{
|
{
|
||||||
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
|
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
|
||||||
auto ams_type = GetAmsType();
|
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
|
||||||
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
|
if (GetAmsType() == N3S)
|
||||||
{
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
else if (ams_type == N3S)
|
|
||||||
{
|
|
||||||
return 1;
|
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
|
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();
|
return it->second->GetExtruderId();
|
||||||
}
|
}
|
||||||
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
|
|
||||||
{
|
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
return MAIN_EXTRUDER_ID;
|
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;
|
return DEPUTY_EXTRUDER_ID;
|
||||||
|
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
|
||||||
|
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(false && __FUNCTION__);
|
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> DevFilaSystem::GetTrayIndexMap()
|
||||||
{
|
{
|
||||||
std::map<int, DevAmsSlotId> tray_id_map;
|
std::map<int, DevAmsSlotId> tray_id_map;
|
||||||
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
|
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
|
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& [ams_id, ams_item] : GetAmsList()) {
|
||||||
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
|
||||||
if (ams_item && slot_item) {
|
if (ams_item && slot_item) {
|
||||||
@@ -335,9 +391,11 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
|
|||||||
int ams_id_int = stoi(ams_id);
|
int ams_id_int = stoi(ams_id);
|
||||||
int slot_id_int = stoi(slot_id);
|
int slot_id_int = stoi(slot_id);
|
||||||
int tray_index = -1;
|
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;
|
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;
|
tray_index = 24 + slot_id_int;
|
||||||
} else {
|
} else {
|
||||||
tray_index = (ams_id_int * 4 + slot_id_int);
|
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;
|
return tray_id_map;
|
||||||
@@ -456,6 +516,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
|||||||
ams_id_set.insert(it->first);
|
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++)
|
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
|
||||||
{
|
{
|
||||||
if (!it->contains("id")) continue;
|
if (!it->contains("id")) continue;
|
||||||
@@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
|
|||||||
|
|
||||||
if (type_id < 4)
|
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)
|
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
|
// remove not in amsList
|
||||||
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
|
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)
|
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
|
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)
|
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
|
||||||
{
|
{
|
||||||
if (!system || !data.is_object())
|
if (!system || !data.is_object())
|
||||||
@@ -913,6 +1026,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
|
|||||||
}
|
}
|
||||||
|
|
||||||
ams->m_exist = true;
|
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_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
|
||||||
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
ams->m_humidity_percent = u.value("humidity_percent", -1);
|
||||||
ams->m_left_dry_time = u.value("dry_time_min", 0);
|
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 ---
|
// --- 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())
|
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"])
|
for (const auto& e : data["external"])
|
||||||
{
|
{
|
||||||
if (!e.is_object())
|
if (!e.is_object())
|
||||||
continue;
|
continue;
|
||||||
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
|
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));
|
DevAmsTray tray(std::to_string(vt_id));
|
||||||
tray.is_exists = e.value("loaded", false);
|
tray.is_exists = e.value("loaded", false);
|
||||||
tray.m_fila_type = e.value("material", std::string());
|
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_min = std::to_string(e.value("nozzle_temp_min", 0));
|
||||||
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
|
||||||
tray.remain = e.value("remain_percent", -1);
|
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 "DevFilaSwitch.h"
|
||||||
#include "DevUtil.h"
|
#include "DevUtil.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -82,7 +83,7 @@ public:
|
|||||||
wxColour wx_color;
|
wxColour wx_color;
|
||||||
bool is_bbl;
|
bool is_bbl;
|
||||||
bool is_exists = false;
|
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 hold_count = 0;
|
||||||
int remain = 0; // filament remain: 0 ~ 100
|
int remain = 0; // filament remain: 0 ~ 100
|
||||||
|
|
||||||
@@ -114,6 +115,16 @@ public:
|
|||||||
std::optional<DevFilamentDryingPreset> get_ams_drying_preset() const;
|
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.
|
* 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;
|
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
|
||||||
void CollectAmsColors(std::vector<wxColour>& ams_colors) 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).
|
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
|
||||||
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
std::map<int, DevAmsSlotId> GetTrayIndexMap();
|
||||||
|
|
||||||
@@ -433,4 +444,4 @@ struct DevFilamentDryingPreset
|
|||||||
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
}// namespace Slic3r
|
}// namespace Slic3r
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/colour.h>
|
#include <wx/colour.h>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||||
@@ -18,14 +19,80 @@
|
|||||||
|
|
||||||
// TODO: remove this include
|
// TODO: remove this include
|
||||||
#include "slic3r/GUI/DeviceManager.hpp"
|
#include "slic3r/GUI/DeviceManager.hpp"
|
||||||
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/GUI/GuiColor.hpp"
|
#include "slic3r/GUI/GuiColor.hpp"
|
||||||
|
|
||||||
namespace Slic3r
|
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)
|
bool DevMappingUtil::is_valid_mapping_result(const MachineObject* obj, std::vector<FilamentInfo>& result, bool check_empty_slot)
|
||||||
{
|
{
|
||||||
if (result.empty()) return false;
|
if (result.empty()) return false;
|
||||||
|
|
||||||
|
const bool is_bbl_vendor = _is_bbl_vendor_printer();
|
||||||
|
|
||||||
for (int i = 0; i < result.size(); i++)
|
for (int i = 0; i < result.size(); i++)
|
||||||
{
|
{
|
||||||
// invalid mapping result
|
// invalid mapping result
|
||||||
@@ -42,8 +109,13 @@ namespace Slic3r
|
|||||||
auto ams_item = obj->GetFilaSystem()->GetAmsById(result[i].ams_id);
|
auto ams_item = obj->GetFilaSystem()->GetAmsById(result[i].ams_id);
|
||||||
if (ams_item == nullptr)
|
if (ams_item == nullptr)
|
||||||
{
|
{
|
||||||
if ((result[i].ams_id != std::to_string(VIRTUAL_TRAY_MAIN_ID)) &&
|
// A virtual (external spool) tray is not an AMS unit. Bambu keeps its fixed pair;
|
||||||
(result[i].ams_id != std::to_string(VIRTUAL_TRAY_DEPUTY_ID)))
|
// 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;
|
result[i].tray_id = -1;
|
||||||
return false;
|
return false;
|
||||||
@@ -94,8 +166,11 @@ namespace Slic3r
|
|||||||
result.ams_id = std::to_string(ams_id);
|
result.ams_id = std::to_string(ams_id);
|
||||||
result.slot_id = std::to_string(slot_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.tray_id = atoi(tray.id.c_str());
|
||||||
result.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
|
std::map<int, FilamentInfo> tray_filaments; // tray_index : tray_color
|
||||||
bool left_nozzle_has_ams = false, right_nozzle_has_ams = false;
|
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();
|
const auto& ams_list = obj->GetFilaSystem()->GetAmsList();
|
||||||
for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++)
|
for (auto ams = ams_list.begin(); ams != ams_list.end(); ams++)
|
||||||
{
|
{
|
||||||
std::string ams_id = ams->second->GetAmsId();
|
std::string ams_id = ams->second->GetAmsId();
|
||||||
auto ams_type = ams->second->GetAmsType();
|
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.
|
// 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())
|
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++)
|
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_id = atoi(tray->first.c_str());
|
||||||
int tray_index = 0;
|
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;
|
tray_index = ams_id * 4 + tray_id;
|
||||||
}
|
}
|
||||||
@@ -169,14 +264,18 @@ namespace Slic3r
|
|||||||
if (tray->second->is_tray_info_ready())
|
if (tray->second->is_tray_info_ready())
|
||||||
{
|
{
|
||||||
_parse_tray_info(ams_id, tray_id, ams_type, *(tray->second), info);
|
_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 ams_valid = false;
|
||||||
bool right_ams_valid = (ams->second->GetBindedExtruderSet().count(MAIN_EXTRUDER_ID) != 0) && map_opt[MappingOption::USE_RIGHT_AMS];
|
if (is_bbl_vendor)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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)
|
if (right_ams_valid)
|
||||||
{
|
{
|
||||||
right_nozzle_has_ams = true;
|
right_nozzle_has_ams = true;
|
||||||
@@ -185,6 +284,25 @@ namespace Slic3r
|
|||||||
{
|
{
|
||||||
left_nozzle_has_ams = true;
|
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)
|
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]);
|
// A synthetic topology filler is not a reported external spool, so it is never a
|
||||||
bool left_ext_valid = (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID) && map_opt[MappingOption::USE_LEFT_EXT]);
|
// mapping candidate. Bambu keeps its fixed reported pair.
|
||||||
if (right_ext_valid || left_ext_valid)
|
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 (nozzle_has_ams_then_ignore_ext)
|
||||||
{
|
{
|
||||||
if (right_ext_valid && right_nozzle_has_ams)
|
if (right_ext_valid && right_nozzle_has_ams)
|
||||||
@@ -208,10 +337,31 @@ namespace Slic3r
|
|||||||
continue;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||||
#include "libslic3r/PrintConfig.hpp"
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "libslic3r/CommonDefs.hpp"
|
#include "libslic3r/CommonDefs.hpp"
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -176,34 +178,52 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
|||||||
int DevNozzle::GetLogicExtruderId() const
|
int DevNozzle::GetLogicExtruderId() const
|
||||||
{
|
{
|
||||||
int total_ext_count = GetTotalExtruderCount();
|
int total_ext_count = GetTotalExtruderCount();
|
||||||
if (total_ext_count == 1) {
|
|
||||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||||
} else if (total_ext_count == 2) {
|
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
if (AtLeftExtruder()) {
|
if (is_bbl_vendor) {
|
||||||
return LOGIC_L_EXTRUDER_ID;
|
if (total_ext_count == 1) {
|
||||||
} else if (AtRightExtruder()) {
|
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||||
return LOGIC_R_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);
|
// For some reason, BBL's logical extruder ID is inverted:
|
||||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
// 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 DevNozzle::GetExtruderId() const
|
||||||
{
|
{
|
||||||
int total_ext_count = GetTotalExtruderCount();
|
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
|
||||||
if (total_ext_count == 1) {
|
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
return MAIN_EXTRUDER_ID;
|
if (is_bbl_vendor) {
|
||||||
} else if (total_ext_count == 2) {
|
int total_ext_count = GetTotalExtruderCount();
|
||||||
if (AtRightExtruder()) {
|
if (total_ext_count == 1) {
|
||||||
return MAIN_EXTRUDER_ID;
|
return MAIN_EXTRUDER_ID;
|
||||||
} else if (AtLeftExtruder()) {
|
} else if (total_ext_count == 2) {
|
||||||
return DEPUTY_EXTRUDER_ID;
|
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
|
bool DevNozzle::AtLeftExtruder() const
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/colour.h>
|
#include <wx/colour.h>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -806,8 +807,12 @@ bool MachineObject::is_extrusion_cali_finished()
|
|||||||
DevAmsTray *MachineObject::get_curr_tray()
|
DevAmsTray *MachineObject::get_curr_tray()
|
||||||
{
|
{
|
||||||
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
|
||||||
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
|
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
|
||||||
return &vt_slot[0];
|
for (auto& tray : vt_slot) {
|
||||||
|
if (tray.id == cur_ams_id)
|
||||||
|
return &tray;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
DevAms* curr_ams = get_curr_Ams();
|
DevAms* curr_ams = get_curr_Ams();
|
||||||
@@ -1273,19 +1278,24 @@ int MachineObject::get_bed_temperature_limit()
|
|||||||
|
|
||||||
bool MachineObject::is_filament_installed()
|
bool MachineObject::is_filament_installed()
|
||||||
{
|
{
|
||||||
if (m_extder_system->GetTotalExtderCount() > 0) {
|
// if (m_extder_system->GetTotalExtderCount() > 0) {
|
||||||
// right//or single
|
// // right//or single
|
||||||
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
|
||||||
if (ext.m_ext_has_filament) {
|
// 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;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2627,13 +2637,10 @@ void MachineObject::reset()
|
|||||||
json empty_j;
|
json empty_j;
|
||||||
print_json.diff2all_base_reset(empty_j);
|
print_json.diff2all_base_reset(empty_j);
|
||||||
|
|
||||||
for (auto i = 0; i < vt_slot.size(); i++) {
|
for (auto& tray : vt_slot)
|
||||||
vt_slot[i].reset();
|
tray.reset();
|
||||||
|
if (vt_slot.size() > 1)
|
||||||
if (i == 1) {
|
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
|
||||||
vt_slot.erase(vt_slot.begin() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// why: reset reuses MachineObject, so release its lazy subtask
|
// why: reset reuses MachineObject, so release its lazy subtask
|
||||||
// before dropping the pointer to prevent reconnect leaks.
|
// before dropping the pointer to prevent reconnect leaks.
|
||||||
if (subtask_) {
|
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()) {
|
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
|
||||||
|
|
||||||
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
|
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)) {
|
const auto tray_id_str = tray_json["id"].get<std::string>();
|
||||||
auto it = std::next(vt_slot.begin(), 0);
|
int tray_id = 0;
|
||||||
if (it != vt_slot.end()) {
|
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
|
||||||
vt_slot[0] = vslot;
|
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|
||||||
}
|
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
|
||||||
else {
|
continue;
|
||||||
vt_slot.push_back(vslot);
|
|
||||||
}
|
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)) {
|
catch (...) {
|
||||||
auto it = std::next(vt_slot.begin(), 1);
|
continue;
|
||||||
if (it != vt_slot.end()) {
|
|
||||||
vt_slot[1] = vslot;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
vt_slot.push_back(vslot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4163,6 +4170,13 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
|||||||
try {
|
try {
|
||||||
parse_new_info(jj);
|
parse_new_info(jj);
|
||||||
} catch (...) {}
|
} 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
|
#pragma endregion
|
||||||
} else if (jj["command"].get<std::string>() == "gcode_line") {
|
} else if (jj["command"].get<std::string>() == "gcode_line") {
|
||||||
//ack of gcode_line
|
//ack of gcode_line
|
||||||
@@ -5268,18 +5282,50 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
|
|||||||
return vt_tray;
|
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
|
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;
|
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) {
|
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
|
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);
|
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);
|
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) {
|
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;
|
std::vector<DevAmsTray> vt_slot;
|
||||||
DevAmsTray parse_vt_tray(nlohmann::json vtray);
|
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*/
|
/*get ams slot info*/
|
||||||
bool contains_tray(const std::string &ams_id, const std::string &tray_id) const;
|
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*/
|
DevAmsTray get_tray(const std::string &ams_id, const std::string &tray_id) const;/*use contains_tray() check first*/
|
||||||
|
|||||||
+528
-99
@@ -20,6 +20,7 @@
|
|||||||
#include <clocale>
|
#include <clocale>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <ios>
|
#include <ios>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -772,11 +773,13 @@ struct ExtruderGroup : StaticBox
|
|||||||
Label * prefix = nullptr;
|
Label * prefix = nullptr;
|
||||||
Label * diameter_label = nullptr; // read-only nozzle diameter of this extruder
|
Label * diameter_label = nullptr; // read-only nozzle diameter of this extruder
|
||||||
ComboBox * flow = nullptr;
|
ComboBox * flow = nullptr;
|
||||||
|
wxBoxSizer *row_sizer = nullptr; // the row's line, moved between the two columns as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
|
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
|
||||||
wxBoxSizer * sizer = nullptr;
|
wxBoxSizer * sizer = nullptr;
|
||||||
HoverLabel * hover_label = nullptr;
|
HoverLabel * hover_label = nullptr;
|
||||||
|
wxPanel * ams_panel = nullptr;
|
||||||
wxStaticText* ams_label{nullptr};
|
wxStaticText* ams_label{nullptr};
|
||||||
ScalableButton * btn_edit = nullptr;
|
ScalableButton * btn_edit = nullptr;
|
||||||
ComboBox * combo_diameter = nullptr;
|
ComboBox * combo_diameter = nullptr;
|
||||||
@@ -785,6 +788,8 @@ struct ExtruderGroup : StaticBox
|
|||||||
ScalableButton * btn_up{nullptr};
|
ScalableButton * btn_up{nullptr};
|
||||||
ScalableButton * btn_down{nullptr};
|
ScalableButton * btn_down{nullptr};
|
||||||
wxBoxSizer *hsizer_ams { nullptr };
|
wxBoxSizer *hsizer_ams { nullptr };
|
||||||
|
wxBoxSizer *ams_section { nullptr }; // AMS panel + its spacing, shown independently of the nozzle rows
|
||||||
|
bool ams_visible{false};
|
||||||
size_t page_cur{0};
|
size_t page_cur{0};
|
||||||
size_t page_num{3};
|
size_t page_num{3};
|
||||||
size_t ams_n4 = 0;
|
size_t ams_n4 = 0;
|
||||||
@@ -794,13 +799,30 @@ struct ExtruderGroup : StaticBox
|
|||||||
wxString diameter;
|
wxString diameter;
|
||||||
|
|
||||||
std::vector<NozzleRow> rows; // rows[0] always exists, one row per extruder when multi
|
std::vector<NozzleRow> rows; // rows[0] always exists, one row per extruder when multi
|
||||||
size_t nozzle_row_count = 0; // rows currently shown
|
size_t nozzle_row_count = 0; // rows currently configured
|
||||||
|
int visible_row = -1; // -1 shows every configured row, otherwise the only row shown
|
||||||
|
wxBoxSizer * rows_sizer = nullptr;
|
||||||
wxBoxSizer * row_columns[2] = {}; // row i is dealt into column i % 2
|
wxBoxSizer * row_columns[2] = {}; // row i is dealt into column i % 2
|
||||||
|
bool rows_single_column = false;
|
||||||
int group_index = -1; // ctor index, the extruder a single-row group edits
|
int group_index = -1; // ctor index, the extruder a single-row group edits
|
||||||
|
|
||||||
// Show `count` nozzle rows, one per extruder, each prefixed with T<index> when count > 1.
|
// Show `count` nozzle rows, one per extruder, each prefixed with T<index> when count > 1.
|
||||||
void SetNozzleRowCount(size_t count);
|
void SetNozzleRowCount(size_t count);
|
||||||
size_t NozzleRowCount() const { return nozzle_row_count; }
|
size_t NozzleRowCount() const { return nozzle_row_count; }
|
||||||
|
// Generic multi-tool cards list one tool at a time while keeping every configured row alive.
|
||||||
|
bool IsNozzleRowVisible(size_t row) const
|
||||||
|
{
|
||||||
|
return row < nozzle_row_count && (visible_row < 0 || row == size_t(visible_row));
|
||||||
|
}
|
||||||
|
void SetVisibleNozzleRow(int index);
|
||||||
|
// Re-apply row visibility, re-measure and lay the card out after the row selection changed.
|
||||||
|
void apply_nozzle_row_visibility();
|
||||||
|
// A single-tool card shows its one row across the whole width; a listing deals rows into columns.
|
||||||
|
void restack_nozzle_rows();
|
||||||
|
// The AMS section is hidden on the listing card used for a single nozzle or a plain multi-row list.
|
||||||
|
void SetAmsVisible(bool show);
|
||||||
|
// Restart paging when the card starts showing another tool.
|
||||||
|
void ResetAmsPage();
|
||||||
// Nozzle diameter readout of the shown rows, one value per extruder. A listing card cannot edit
|
// Nozzle diameter readout of the shown rows, one value per extruder. A listing card cannot edit
|
||||||
// it: the diameter belongs to the machine variant the printer preset selects, not to the extruder.
|
// it: the diameter belongs to the machine variant the printer preset selects, not to the extruder.
|
||||||
void SetRowDiameters(const std::vector<double> &diameters);
|
void SetRowDiameters(const std::vector<double> &diameters);
|
||||||
@@ -816,7 +838,7 @@ struct ExtruderGroup : StaticBox
|
|||||||
return;
|
return;
|
||||||
ams_n4 = n4;
|
ams_n4 = n4;
|
||||||
ams_n1 = n1;
|
ams_n1 = n1;
|
||||||
if (btn_edit) {
|
if (btn_edit || ams_visible) {
|
||||||
update_ams();
|
update_ams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,7 +849,7 @@ struct ExtruderGroup : StaticBox
|
|||||||
void SetEditEnabled(bool enable) { if (hover_label) hover_label->EnableEdit(enable); }
|
void SetEditEnabled(bool enable) { if (hover_label) hover_label->EnableEdit(enable); }
|
||||||
void SetOnHoverClick(std::function<void()> on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); }
|
void SetOnHoverClick(std::function<void()> on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); }
|
||||||
|
|
||||||
void sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1);
|
void sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1, bool live_count = false);
|
||||||
|
|
||||||
void Rescale()
|
void Rescale()
|
||||||
{
|
{
|
||||||
@@ -905,8 +927,17 @@ struct Sidebar::priv
|
|||||||
ExtruderGroup *right_extruder = nullptr;
|
ExtruderGroup *right_extruder = nullptr;
|
||||||
ExtruderGroup *single_extruder = nullptr;
|
ExtruderGroup *single_extruder = nullptr;
|
||||||
|
|
||||||
|
// Generic printers with more than two extruders show one tool at a time: a read-only selector
|
||||||
|
// above the card and the selected tool's nozzle row and AMS section inside it.
|
||||||
|
wxPanel * panel_extruder_tool = nullptr;
|
||||||
|
ComboBox * combo_extruder_tool = nullptr;
|
||||||
|
int selected_tool = 0;
|
||||||
|
bool generic_multi_tool_layout = false;
|
||||||
|
|
||||||
int FromDIP(int n) { return plater->FromDIP(n); }
|
int FromDIP(int n) { return plater->FromDIP(n); }
|
||||||
void layout_printer(bool isBBL, bool isDual);
|
void layout_printer(bool isBBL, bool isDual);
|
||||||
|
// Refresh the tool choices and keep `selected_tool` valid when the extruder count changes.
|
||||||
|
void update_tool_selector(int extruder_count, bool generic_multi);
|
||||||
|
|
||||||
void flush_printer_sync(bool restart = false);
|
void flush_printer_sync(bool restart = false);
|
||||||
|
|
||||||
@@ -1080,11 +1111,42 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tool selector for a generic printer with more than two extruders. The card below shows only
|
||||||
|
// the selected tool; every other layout keeps the selector hidden.
|
||||||
|
panel_extruder_tool = new wxPanel(m_panel_printer_content, wxID_ANY);
|
||||||
|
panel_extruder_tool->SetBackgroundColour(*wxWHITE);
|
||||||
|
combo_extruder_tool = new ComboBox(panel_extruder_tool, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
|
||||||
|
combo_extruder_tool->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) {
|
||||||
|
const int selection = combo_extruder_tool->GetSelection();
|
||||||
|
if (selection < 0 || !generic_multi_tool_layout)
|
||||||
|
return;
|
||||||
|
selected_tool = selection;
|
||||||
|
single_extruder->SetVisibleNozzleRow(selection);
|
||||||
|
single_extruder->SetTitle(wxString::Format(_L("Tool %d"), selected_tool + 1));
|
||||||
|
if (auto *plater = GUI::wxGetApp().plater())
|
||||||
|
plater->update_machine_sync_status();
|
||||||
|
single_extruder->ResetAmsPage();
|
||||||
|
if (m_panel_printer_content)
|
||||||
|
m_panel_printer_content->Layout();
|
||||||
|
});
|
||||||
|
use_nozzle_row_font(combo_extruder_tool);
|
||||||
|
auto *tool_label = new Label(panel_extruder_tool, _L("Tool"));
|
||||||
|
tool_label->SetFont(Label::Body_13);
|
||||||
|
auto *tool_row = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
tool_row->Add(tool_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||||
|
tool_row->Add(combo_extruder_tool, 1, wxALIGN_CENTER_VERTICAL);
|
||||||
|
auto *tool_panel_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
tool_panel_sizer->Add(tool_row, 0, wxEXPAND);
|
||||||
|
tool_panel_sizer->AddSpacer(FromDIP(4)); // gap above the card, only present while the selector is shown
|
||||||
|
panel_extruder_tool->SetSizer(tool_panel_sizer);
|
||||||
|
panel_extruder_tool->Hide();
|
||||||
|
|
||||||
// single
|
// single
|
||||||
extruder_single_sizer = new wxBoxSizer(wxHORIZONTAL);
|
extruder_single_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
extruder_single_sizer->Add(single_extruder, 1, wxEXPAND, 0);
|
extruder_single_sizer->Add(single_extruder, 1, wxEXPAND, 0);
|
||||||
|
|
||||||
wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
extruder_sizer->Add(panel_extruder_tool, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
||||||
extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
||||||
extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
|
||||||
|
|
||||||
@@ -1114,19 +1176,37 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
|||||||
int extruder_count = 0;
|
int extruder_count = 0;
|
||||||
const bool has_flow_variant = cfg.support_different_extruders(extruder_count);
|
const bool has_flow_variant = cfg.support_different_extruders(extruder_count);
|
||||||
|
|
||||||
// ORCA: a non-Bambu printer with several extruders lists one nozzle row per tool; Bambu's own
|
// ORCA: a non-Bambu printer with several extruders lists nozzle rows per tool; Bambu's own
|
||||||
// multi-extruder printers keep the left/right pair above.
|
// multi-extruder printers keep the left/right pair above. Beyond two tools the card shows one
|
||||||
|
// tool at a time, chosen with the selector, so the AMS section has room to stay visible.
|
||||||
const bool multi_extruder_rows = !preset_bundle.is_bbl_vendor() && extruder_count > 1;
|
const bool multi_extruder_rows = !preset_bundle.is_bbl_vendor() && extruder_count > 1;
|
||||||
|
const bool generic_multi_tool = multi_extruder_rows && extruder_count > 2;
|
||||||
|
update_tool_selector(extruder_count, generic_multi_tool);
|
||||||
|
|
||||||
// The variant box switches the machine variant. The card below lists the nozzles when there
|
// The variant box switches the machine variant. The card below lists the nozzles when there
|
||||||
// are flow variants to choose or several extruders to show; otherwise the box says it all.
|
// are flow variants to choose or several extruders to show; otherwise the box says it all.
|
||||||
panel_nozzle_dia->Show(true);
|
panel_nozzle_dia->Show(true);
|
||||||
extruder_single_sizer->Show(has_flow_variant || multi_extruder_rows);
|
extruder_single_sizer->Show(has_flow_variant || multi_extruder_rows);
|
||||||
single_extruder->SetNozzleRowCount(multi_extruder_rows ? size_t(extruder_count) : 1);
|
if (generic_multi_tool) {
|
||||||
|
// Keep every tool row configured (see update_all_presets) but display only the selected
|
||||||
|
// one, together with that tool's AMS previews.
|
||||||
|
single_extruder->SetNozzleRowCount(size_t(extruder_count));
|
||||||
|
single_extruder->SetVisibleNozzleRow(selected_tool);
|
||||||
|
// The card may be carrying another printer's AMS until the device sync refills it.
|
||||||
|
single_extruder->sync_ams(nullptr, {}, {}, true);
|
||||||
|
single_extruder->SetAmsVisible(true);
|
||||||
|
} else {
|
||||||
|
single_extruder->SetNozzleRowCount(multi_extruder_rows ? size_t(extruder_count) : 1);
|
||||||
|
single_extruder->SetVisibleNozzleRow(-1);
|
||||||
|
single_extruder->SetAmsVisible(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
update_tool_selector(0, false);
|
||||||
panel_nozzle_dia->Show(false);
|
panel_nozzle_dia->Show(false);
|
||||||
extruder_single_sizer->Show(false);
|
extruder_single_sizer->Show(false);
|
||||||
single_extruder->SetNozzleRowCount(1);
|
single_extruder->SetNozzleRowCount(1);
|
||||||
|
single_extruder->SetVisibleNozzleRow(-1);
|
||||||
|
single_extruder->SetAmsVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ORCA ensure printer section is visible after changing printer from printer selection dialog
|
// ORCA ensure printer section is visible after changing printer from printer selection dialog
|
||||||
@@ -1140,6 +1220,44 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Sidebar::priv::update_tool_selector(int extruder_count, bool generic_multi)
|
||||||
|
{
|
||||||
|
generic_multi_tool_layout = generic_multi;
|
||||||
|
if (!combo_extruder_tool)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!generic_multi) {
|
||||||
|
selected_tool = 0;
|
||||||
|
single_extruder->SetTitle(_L("Nozzle"));
|
||||||
|
combo_extruder_tool->Hide();
|
||||||
|
if (panel_extruder_tool)
|
||||||
|
panel_extruder_tool->Hide();
|
||||||
|
if (m_panel_printer_content)
|
||||||
|
m_panel_printer_content->Layout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int count = std::min(std::max(extruder_count, 1), int(MAXIMUM_EXTRUDER_NUMBER));
|
||||||
|
if (combo_extruder_tool->GetCount() != count) {
|
||||||
|
combo_extruder_tool->Clear();
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
combo_extruder_tool->Append(wxString::Format("T%d", i + 1));
|
||||||
|
}
|
||||||
|
// A device refresh keeps the chosen tool; a preset change that removes it falls back to the last
|
||||||
|
// remaining tool rather than leaving a stale selection.
|
||||||
|
const int previous_tool = selected_tool;
|
||||||
|
selected_tool = std::clamp(selected_tool, 0, count - 1);
|
||||||
|
combo_extruder_tool->SetSelection(selected_tool);
|
||||||
|
single_extruder->SetTitle(wxString::Format(_L("Tool %d"), selected_tool + 1));
|
||||||
|
if (selected_tool != previous_tool)
|
||||||
|
single_extruder->ResetAmsPage();
|
||||||
|
if (panel_extruder_tool)
|
||||||
|
panel_extruder_tool->Show();
|
||||||
|
combo_extruder_tool->Show();
|
||||||
|
if (m_panel_printer_content)
|
||||||
|
m_panel_printer_content->Layout();
|
||||||
|
}
|
||||||
|
|
||||||
void Sidebar::priv::flush_printer_sync(bool restart)
|
void Sidebar::priv::flush_printer_sync(bool restart)
|
||||||
{
|
{
|
||||||
if (restart) {
|
if (restart) {
|
||||||
@@ -1545,7 +1663,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
|||||||
use_nozzle_row_font(combo_diameter);
|
use_nozzle_row_font(combo_diameter);
|
||||||
|
|
||||||
// AMS
|
// AMS
|
||||||
auto ams_panel = new wxPanel(this, wxID_ANY);
|
ams_panel = new wxPanel(this, wxID_ANY);
|
||||||
ams_panel->SetBackgroundColour(*wxWHITE);
|
ams_panel->SetBackgroundColour(*wxWHITE);
|
||||||
|
|
||||||
ams_label = new wxStaticText(ams_panel, wxID_ANY, _L("AMS"));
|
ams_label = new wxStaticText(ams_panel, wxID_ANY, _L("AMS"));
|
||||||
@@ -1630,18 +1748,19 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
|||||||
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
|
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
|
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
|
||||||
if (index < 0) {
|
ams_section = new wxBoxSizer(wxVERTICAL);
|
||||||
ams_panel->Hide();
|
ams_section->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
|
||||||
} else {
|
ams_section->AddSpacer(FromDIP(2));
|
||||||
vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
|
vsizer->Add(ams_section, 0, wxEXPAND);
|
||||||
vsizer->AddSpacer(FromDIP(2));
|
ams_visible = index >= 0;
|
||||||
}
|
ams_panel->Show(ams_visible);
|
||||||
|
vsizer->Show(ams_section, ams_visible);
|
||||||
|
|
||||||
// Nozzle rows. The first one is the only one for a printer whose nozzles are not listed
|
// Nozzle rows. The first one is the only one for a printer whose nozzles are not listed
|
||||||
// individually; a multi-extruder printer grows one row per tool (see SetNozzleRowCount).
|
// individually; a multi-extruder printer grows one row per tool (see SetNozzleRowCount).
|
||||||
// The rows are dealt into two columns, which halves the height of a long tool list; the second
|
// The rows are dealt into two columns, which halves the height of a long tool list; the second
|
||||||
// column empties itself when there is a single row, so a one-row card still spans the width.
|
// column empties itself when there is a single row, so a one-row card still spans the width.
|
||||||
auto rows_sizer = new wxBoxSizer(wxHORIZONTAL);
|
rows_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
for (wxBoxSizer *&column : row_columns) {
|
for (wxBoxSizer *&column : row_columns) {
|
||||||
column = new wxBoxSizer(wxVERTICAL);
|
column = new wxBoxSizer(wxVERTICAL);
|
||||||
rows_sizer->Add(column, 1, wxEXPAND | wxLEFT , FromDIP(5));
|
rows_sizer->Add(column, 1, wxEXPAND | wxLEFT , FromDIP(5));
|
||||||
@@ -1653,7 +1772,11 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
|
|||||||
|
|
||||||
Layout();
|
Layout();
|
||||||
|
|
||||||
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
|
// The indexed cards also maintain the two-entry AMS-count model used by the edit popup. The
|
||||||
|
// single/nozzle card is a listing card: its AMS previews come from the live unit list bound to
|
||||||
|
// the chosen tool instead, so it must not seed that model.
|
||||||
|
if (index >= 0)
|
||||||
|
AMSCountPopupWindow::UpdateAMSCount(index, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
|
ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
|
||||||
@@ -1697,8 +1820,11 @@ ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
|
|||||||
row_sizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
|
row_sizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
|
||||||
row_sizer->Add(row.diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
row_sizer->Add(row.diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||||
row_sizer->Add(row.flow, 1);
|
row_sizer->Add(row.flow, 1);
|
||||||
// Row-major: T1 T2 on the first line, T3 T4 on the second, and so on.
|
row.row_sizer = row_sizer;
|
||||||
row_columns[row_index % 2]->Add(row_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(5));
|
// Row-major: T1 T2 on the first line, T3 T4 on the second, and so on. A single-tool card keeps
|
||||||
|
// its one row on the first column so it spans the card width.
|
||||||
|
wxBoxSizer *column = rows_single_column ? row_columns[0] : row_columns[row_index % 2];
|
||||||
|
column->Add(row_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(5));
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -1739,22 +1865,56 @@ void ExtruderGroup::SetNozzleRowCount(size_t count)
|
|||||||
if (row_count == nozzle_row_count)
|
if (row_count == nozzle_row_count)
|
||||||
return;
|
return;
|
||||||
nozzle_row_count = row_count;
|
nozzle_row_count = row_count;
|
||||||
|
if (visible_row >= int(row_count))
|
||||||
|
visible_row = int(row_count) - 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < row_count; ++i) {
|
||||||
|
if (i >= rows.size())
|
||||||
|
rows.push_back(create_nozzle_row());
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_nozzle_row_visibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtruderGroup::SetVisibleNozzleRow(int index)
|
||||||
|
{
|
||||||
|
const size_t shown_rows = std::min(rows.size(), nozzle_row_count);
|
||||||
|
visible_row = shown_rows == 0 || index < 0 ? -1 : std::min<int>(index, int(shown_rows) - 1);
|
||||||
|
apply_nozzle_row_visibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtruderGroup::restack_nozzle_rows()
|
||||||
|
{
|
||||||
|
const bool single = visible_row >= 0;
|
||||||
|
if (rows_single_column == single || !rows_sizer || !row_columns[0] || !row_columns[1])
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (const NozzleRow &row : rows) {
|
||||||
|
row_columns[0]->Detach(row.row_sizer);
|
||||||
|
row_columns[1]->Detach(row.row_sizer);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < rows.size(); ++i) {
|
||||||
|
wxBoxSizer *column = single ? row_columns[0] : row_columns[i % 2];
|
||||||
|
column->Add(rows[i].row_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(5));
|
||||||
|
}
|
||||||
|
rows_sizer->Show(row_columns[1], !single);
|
||||||
|
rows_single_column = single;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtruderGroup::apply_nozzle_row_visibility()
|
||||||
|
{
|
||||||
|
restack_nozzle_rows();
|
||||||
|
|
||||||
// Every row names its tool as soon as there is more than one, so a single-nozzle card keeps the
|
// Every row names its tool as soon as there is more than one, so a single-nozzle card keeps the
|
||||||
// plain "Nozzle" look. A row is shown by showing its widgets: a sizer item holding a sub-sizer
|
// plain "Nozzle" look. A row is shown by showing its widgets: a sizer item holding a sub-sizer
|
||||||
// reports itself as shown while any of its windows is, so hiding the sub-sizer alone would
|
// reports itself as shown while any of its windows is, so hiding the sub-sizer alone would
|
||||||
// leave the row in the layout.
|
// leave the row in the layout.
|
||||||
for (size_t i = 0; i < row_count; ++i) {
|
for (size_t i = 0; i < rows.size(); ++i) {
|
||||||
if (i >= rows.size())
|
const bool show = IsNozzleRowVisible(i);
|
||||||
rows.push_back(create_nozzle_row());
|
rows[i].prefix->Show(show && nozzle_row_count > 1);
|
||||||
rows[i].prefix->Show(row_count > 1);
|
rows[i].diameter_label->Show(show && group_index < 0); // the dual left/right cards show combo_diameter
|
||||||
rows[i].diameter_label->Show(group_index < 0); // the dual left/right cards show combo_diameter
|
// The flow combo's own visibility follows its choices and the selected row, see update_extruder_variant.
|
||||||
// The flow combo's own visibility is decided with its choices, see update_extruder_variant.
|
rows[i].flow->Show(show && rows[i].flow->GetCount() > 0);
|
||||||
}
|
|
||||||
for (size_t i = row_count; i < rows.size(); ++i) {
|
|
||||||
rows[i].prefix->Hide();
|
|
||||||
rows[i].diameter_label->Hide();
|
|
||||||
rows[i].flow->Hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rows just changed: re-measure them, then let the card and its containers give it the new room.
|
// The rows just changed: re-measure them, then let the card and its containers give it the new room.
|
||||||
@@ -1767,6 +1927,33 @@ void ExtruderGroup::SetNozzleRowCount(size_t count)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ExtruderGroup::SetAmsVisible(bool show)
|
||||||
|
{
|
||||||
|
if (ams_visible == show)
|
||||||
|
return;
|
||||||
|
ams_visible = show;
|
||||||
|
if (ams_panel)
|
||||||
|
ams_panel->Show(show);
|
||||||
|
if (wxSizer *vsizer = GetSizer(); vsizer && ams_section)
|
||||||
|
vsizer->Show(ams_section, show);
|
||||||
|
if (show)
|
||||||
|
update_ams();
|
||||||
|
|
||||||
|
Layout();
|
||||||
|
if (wxWindow *parent = GetParent()) {
|
||||||
|
parent->Layout();
|
||||||
|
if (wxWindow *grand_parent = parent->GetParent())
|
||||||
|
grand_parent->Layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtruderGroup::ResetAmsPage()
|
||||||
|
{
|
||||||
|
page_cur = 0;
|
||||||
|
if (ams_visible)
|
||||||
|
update_ams();
|
||||||
|
}
|
||||||
|
|
||||||
void ExtruderGroup::update_ams()
|
void ExtruderGroup::update_ams()
|
||||||
{
|
{
|
||||||
static AMSinfo info4;
|
static AMSinfo info4;
|
||||||
@@ -1777,10 +1964,16 @@ void ExtruderGroup::update_ams()
|
|||||||
info1.cans.push_back({});
|
info1.cans.push_back({});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btn_edit == nullptr)
|
if (!ams_visible)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
page_num = (ams_n4 * 2 + ams_n1 + 3) / 4;
|
page_num = (ams_n4 * 2 + ams_n1 + 3) / 4;
|
||||||
|
// The AMS set can shrink when another tool is selected or the device changes; never leave the
|
||||||
|
// page pointer behind the last page.
|
||||||
|
if (page_num == 0)
|
||||||
|
page_cur = 0;
|
||||||
|
else if (page_cur >= page_num)
|
||||||
|
page_cur = page_num - 1;
|
||||||
size_t i4 = page_cur * 2;
|
size_t i4 = page_cur * 2;
|
||||||
size_t i1 = 0;
|
size_t i1 = 0;
|
||||||
if (i4 > ams_n4) {
|
if (i4 > ams_n4) {
|
||||||
@@ -1807,8 +2000,12 @@ void ExtruderGroup::update_ams()
|
|||||||
btn_up->Show(page_cur > 0);
|
btn_up->Show(page_cur > 0);
|
||||||
btn_down->Show(page_cur + 1 < page_num);
|
btn_down->Show(page_cur + 1 < page_num);
|
||||||
|
|
||||||
while (hsizer_ams->GetItemCount() > 2)
|
// Rebuild the row instead of trimming a fixed prefix: the listing card has no edit button, and
|
||||||
hsizer_ams->Remove(2);
|
// the installed/not-installed message is reused across updates.
|
||||||
|
hsizer_ams->Clear(false);
|
||||||
|
hsizer_ams->Add(ams_label, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
|
||||||
|
if (btn_edit)
|
||||||
|
hsizer_ams->Add(btn_edit, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
|
||||||
if (ams_not_installed_msg->IsShown()) {
|
if (ams_not_installed_msg->IsShown()) {
|
||||||
hsizer_ams->AddStretchSpacer(1);
|
hsizer_ams->AddStretchSpacer(1);
|
||||||
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER);
|
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER);
|
||||||
@@ -1834,27 +2031,41 @@ void ExtruderGroup::update_ams()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The row was rebuilt: lay its contents out before the card sizes itself around them.
|
||||||
|
ams_panel->Layout();
|
||||||
Layout();
|
Layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1)
|
void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1, bool live_count)
|
||||||
{
|
{
|
||||||
if (ams_4.empty() && ams4.empty()
|
bool changed = false;
|
||||||
&& ams_1.empty() && ams1.empty())
|
if (!ams_4.empty() || !ams4.empty()
|
||||||
return;
|
|| !ams_1.empty() || !ams1.empty()) {
|
||||||
auto sync = [obj](std::vector<AMSinfo> &infos, std::vector<DevAms *> const &ams) -> bool {
|
auto sync = [obj](std::vector<AMSinfo> &infos, std::vector<DevAms *> const &ams) -> bool {
|
||||||
std::vector<AMSinfo> infos2;
|
std::vector<AMSinfo> infos2;
|
||||||
for (auto a : ams) {
|
for (auto a : ams) {
|
||||||
AMSinfo ams_info;
|
AMSinfo ams_info;
|
||||||
ams_info.parse_ams_info(const_cast<MachineObject*>(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity);
|
ams_info.parse_ams_info(const_cast<MachineObject*>(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity);
|
||||||
infos2.push_back(ams_info);
|
infos2.push_back(ams_info);
|
||||||
}
|
}
|
||||||
if (infos == infos2)
|
if (infos == infos2)
|
||||||
return false;
|
return false;
|
||||||
infos.swap(infos2);
|
infos.swap(infos2);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
if (sync(ams_4, ams4) || sync(ams_1, ams1))
|
const bool ams4_changed = sync(ams_4, ams4);
|
||||||
|
const bool ams1_changed = sync(ams_1, ams1);
|
||||||
|
changed = ams4_changed || ams1_changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A card fed from its own extruder binding derives its preview count from that list instead of
|
||||||
|
// the preset's AMS count.
|
||||||
|
if (live_count && (ams_n4 != ams4.size() || ams_n1 != ams1.size())) {
|
||||||
|
ams_n4 = ams4.size();
|
||||||
|
ams_n1 = ams1.size();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
update_ams();
|
update_ams();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2361,17 +2572,24 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->plater)
|
const Preset &selected_preset = preset_bundle->printers.get_selected_preset();
|
||||||
return false;
|
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]() {
|
this->plater->update_objects_position_when_select_preset([machine_preset]() {
|
||||||
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
|
Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER);
|
||||||
printer_tab->select_preset(machine_preset->name);
|
printer_tab->select_preset(machine_preset->name);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list";
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list";
|
||||||
const Preset &cur_preset = preset_bundle->printers.get_selected_preset();
|
const Preset &cur_preset = preset_bundle->printers.get_selected_preset();
|
||||||
int extruder_nums = preset_bundle->get_printer_extruder_count();
|
int extruder_nums = preset_bundle->get_printer_extruder_count();
|
||||||
|
// The left/right cards and their two-entry AMS-count model do not describe an N-tool printer.
|
||||||
|
const bool generic_multi_tool = !preset_bundle->is_bbl_vendor() && extruder_nums > 2;
|
||||||
std::vector<int> extruder_map(extruder_nums);
|
std::vector<int> extruder_map(extruder_nums);
|
||||||
std::iota(extruder_map.begin(), extruder_map.end(), 0);
|
std::iota(extruder_map.begin(), extruder_map.end(), 0);
|
||||||
const ConfigOptionInts *physical_extruder_map = cur_preset.config.option<ConfigOptionInts>("physical_extruder_map");
|
const ConfigOptionInts *physical_extruder_map = cur_preset.config.option<ConfigOptionInts>("physical_extruder_map");
|
||||||
@@ -2379,7 +2597,19 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man
|
|||||||
assert(physical_extruder_map->values.size() == extruder_nums);
|
assert(physical_extruder_map->values.size() == extruder_nums);
|
||||||
extruder_map = physical_extruder_map->values;
|
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
|
// 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
|
// existing printer — support_multi_nozzle is false unless some extruder_max_nozzle_count entry is a real
|
||||||
@@ -2458,26 +2688,62 @@ 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 main_index = obj->is_main_extruder_on_left() ? 0 : 1;
|
||||||
int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0;
|
int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0;
|
||||||
|
|
||||||
if (extruder_nums > 1) {
|
// Nozzle diameters are not changed by device sync. If the device reports a different or unavailable
|
||||||
int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
// diameter, warn and leave the preset and controls alone while syncing AMS and other printer info.
|
||||||
int right_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[1]));
|
const ConfigOptionFloats *preset_nozzle_diameter =
|
||||||
assert(left_index != -1 && right_index != -1);
|
preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||||
left_extruder->combo_diameter->SetSelection(left_index);
|
bool nozzle_diameters_match = false;
|
||||||
right_extruder->combo_diameter->SetSelection(right_index);
|
if (preset_nozzle_diameter != nullptr && preset_nozzle_diameter->values.size() == nozzle_diameters.size()) {
|
||||||
is_switching_diameter = true;
|
nozzle_diameters_match = true;
|
||||||
switch_diameter(false);
|
for (size_t index = 0; index < nozzle_diameters.size(); ++index) {
|
||||||
is_switching_diameter = false;
|
// A diameter the device does not report (missing, zero, negative or not a finite number) never matches.
|
||||||
AMSCountPopupWindow::SetAMSCount(deputy_index, deputy_4, deputy_1);
|
const double device_diameter = nozzle_diameters[index];
|
||||||
AMSCountPopupWindow::SetAMSCount(main_index, main_4, main_1);
|
const double preset_diameter = preset_nozzle_diameter->values[index];
|
||||||
AMSCountPopupWindow::UpdateAMSCount(0, left_extruder);
|
if (!std::isfinite(device_diameter) || device_diameter <= 0.0 ||
|
||||||
AMSCountPopupWindow::UpdateAMSCount(1, right_extruder);
|
!std::isfinite(preset_diameter) || preset_diameter <= 0.0 ||
|
||||||
} else {
|
std::fabs(device_diameter - preset_diameter) > EPSILON) {
|
||||||
int index = single_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0]));
|
nozzle_diameters_match = false;
|
||||||
assert(index != -1);
|
break;
|
||||||
single_extruder->combo_diameter->SetSelection(index);
|
}
|
||||||
is_switching_diameter = true;
|
}
|
||||||
switch_diameter(true);
|
}
|
||||||
is_switching_diameter = false;
|
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 (!generic_multi_tool) {
|
||||||
|
if (extruder_nums > 1) {
|
||||||
|
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 {
|
||||||
|
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
|
// set nozzle volume type after switching prset, so this value can override the old value stored in conf
|
||||||
@@ -2518,7 +2784,7 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
right_extruder->ShowBadge(false);
|
right_extruder->ShowBadge(false);
|
||||||
right_extruder->sync_ams(nullptr, {}, {});
|
right_extruder->sync_ams(nullptr, {}, {});
|
||||||
single_extruder->ShowBadge(false);
|
single_extruder->ShowBadge(false);
|
||||||
single_extruder->sync_ams(nullptr, {}, {});
|
single_extruder->sync_ams(nullptr, {}, {}, true);
|
||||||
update_extruder_separator_icon(false, false);
|
update_extruder_separator_icon(false, false);
|
||||||
//btn_sync_printer->SetBorderColor(not_synced_colour);
|
//btn_sync_printer->SetBorderColor(not_synced_colour);
|
||||||
//btn_sync_printer->SetIcon("printer_sync");
|
//btn_sync_printer->SetIcon("printer_sync");
|
||||||
@@ -2593,8 +2859,19 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
|
|
||||||
// 2. update extruder status
|
// 2. update extruder status
|
||||||
int extruder_nums = preset_bundle->get_printer_extruder_count();
|
int extruder_nums = preset_bundle->get_printer_extruder_count();
|
||||||
if (extruder_nums != obj->GetExtderSystem()->GetTotalExtderCount())
|
// A generic printer with more than two tools shows one tool at a time. Its AMS units are
|
||||||
|
// attributed by their own extruder bindings; the Bambu left/right conventions do not apply.
|
||||||
|
const bool generic_multi_tool = !preset_bundle->is_bbl_vendor() && extruder_nums > 2;
|
||||||
|
if (extruder_nums != obj->GetExtderSystem()->GetTotalExtderCount()) {
|
||||||
|
// A count mismatch means the card cannot be trusted. Clear the previous device's previews
|
||||||
|
// and make sure the selected tool still exists instead of keeping a stale card.
|
||||||
|
clear_all_sync_status();
|
||||||
|
if (generic_multi_tool) {
|
||||||
|
update_tool_selector(extruder_nums, true);
|
||||||
|
single_extruder->SetVisibleNozzleRow(selected_tool);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Filament Track Switch: only ever surfaced on multi-extruder machines. When installed, tip/warn
|
// Filament Track Switch: only ever surfaced on multi-extruder machines. When installed, tip/warn
|
||||||
// and show the status icon (green ready / red not-calibrated); when absent the icon stays hidden.
|
// and show the status icon (green ready / red not-calibrated); when absent the icon stays hidden.
|
||||||
@@ -2643,6 +2920,13 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
right_extruder->diameter.ToDouble(&value);
|
right_extruder->diameter.ToDouble(&value);
|
||||||
extruder_infos[1].diameter = float(value);
|
extruder_infos[1].diameter = float(value);
|
||||||
}
|
}
|
||||||
|
else if (generic_multi_tool) {
|
||||||
|
// The selected card reads the nozzle diameter from the preset, one value per tool.
|
||||||
|
if (const auto *nozzle_diameter = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")) {
|
||||||
|
for (size_t i = 0; i < extruder_infos.size() && i < nozzle_diameter->values.size(); ++i)
|
||||||
|
extruder_infos[i].diameter = float(nozzle_diameter->values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ExtruderInfo> machine_extruder_infos(obj->GetExtderSystem()->GetTotalExtderCount());
|
std::vector<ExtruderInfo> machine_extruder_infos(obj->GetExtderSystem()->GetTotalExtderCount());
|
||||||
|
|
||||||
@@ -2652,6 +2936,31 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter();
|
machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter();
|
||||||
}
|
}
|
||||||
for (auto &item : obj->GetFilaSystem()->GetAmsList()) {
|
for (auto &item : obj->GetFilaSystem()->GetAmsList()) {
|
||||||
|
auto add_to_extruder = [&machine_extruder_infos](DevAms *ams, int extruder_id) {
|
||||||
|
if (extruder_id < 0 || extruder_id >= int(machine_extruder_infos.size()))
|
||||||
|
return;
|
||||||
|
if (ams->GetAmsType() == DevAms::N3S) { // N3S
|
||||||
|
machine_extruder_infos[extruder_id].ams_1++;
|
||||||
|
machine_extruder_infos[extruder_id].ams_v1.push_back(ams);
|
||||||
|
} else {
|
||||||
|
machine_extruder_infos[extruder_id].ams_4++;
|
||||||
|
machine_extruder_infos[extruder_id].ams_v4.push_back(ams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (generic_multi_tool) {
|
||||||
|
// Generic agents report the bound extruder on every AMS unit. Honour the whole binding
|
||||||
|
// set so a unit the device binds to several tools appears on each of them. Bindings are
|
||||||
|
// 0-based tool indices, matching selected_tool (T1 == 0).
|
||||||
|
const std::set<int> &bindings = item.second->GetBindedExtruderSet();
|
||||||
|
if (bindings.empty())
|
||||||
|
add_to_extruder(item.second, item.second->GetExtruderId());
|
||||||
|
else
|
||||||
|
for (int extruder_id : bindings)
|
||||||
|
add_to_extruder(item.second, extruder_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int extruder_id;
|
int extruder_id;
|
||||||
// With the switch ready every AMS feeds both extruders, so attribute each to the extruder its
|
// With the switch ready every AMS feeds both extruders, so attribute each to the extruder its
|
||||||
// input track feeds. Without a switch the binding is a single extruder
|
// input track feeds. Without a switch the binding is a single extruder
|
||||||
@@ -2670,20 +2979,13 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
extruder_id = uniq_extruder_id.value();
|
extruder_id = uniq_extruder_id.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extruder_id >= machine_extruder_infos.size())
|
add_to_extruder(item.second, extruder_id);
|
||||||
continue;
|
|
||||||
|
|
||||||
if (item.second->GetAmsType() == DevAms::N3S)
|
|
||||||
{ // N3S
|
|
||||||
machine_extruder_infos[extruder_id].ams_1++;
|
|
||||||
machine_extruder_infos[extruder_id].ams_v1.push_back(item.second);
|
|
||||||
} else {
|
|
||||||
machine_extruder_infos[extruder_id].ams_4++;
|
|
||||||
machine_extruder_infos[extruder_id].ams_v4.push_back(item.second);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::reverse(machine_extruder_infos.begin(), machine_extruder_infos.end());
|
// UI left is the deputy extruder on Bambu's dual-tool cards, hence the reversal. Generic
|
||||||
|
// multi-tool cards keep the tool order the printer reports.
|
||||||
|
if (!generic_multi_tool)
|
||||||
|
std::reverse(machine_extruder_infos.begin(), machine_extruder_infos.end());
|
||||||
|
|
||||||
std::vector<bool> extruder_synced(extruder_nums, false);
|
std::vector<bool> extruder_synced(extruder_nums, false);
|
||||||
if (extruder_nums == 1) {
|
if (extruder_nums == 1) {
|
||||||
@@ -2720,6 +3022,16 @@ void Sidebar::priv::update_sync_status(const MachineObject *obj)
|
|||||||
right_extruder->sync_ams(obj, {}, {});
|
right_extruder->sync_ams(obj, {}, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (generic_multi_tool) {
|
||||||
|
// Show the selected tool's AMS previews and badge. The other tools keep their sync state for
|
||||||
|
// the status icon but have no card of their own.
|
||||||
|
for (size_t i = 0; i < extruder_infos.size(); ++i)
|
||||||
|
extruder_synced[i] = is_same_nozzle_info(extruder_infos[i], machine_extruder_infos[i]);
|
||||||
|
|
||||||
|
const size_t tool = size_t(std::clamp(selected_tool, 0, extruder_nums - 1));
|
||||||
|
single_extruder->ShowBadge(extruder_synced[tool]);
|
||||||
|
single_extruder->sync_ams(obj, machine_extruder_infos[tool].ams_v4, machine_extruder_infos[tool].ams_v1, true);
|
||||||
|
}
|
||||||
|
|
||||||
StateColor synced_colour(std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Normal));
|
StateColor synced_colour(std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Normal));
|
||||||
bool all_extruder_synced = std::all_of(extruder_synced.begin(), extruder_synced.end(), [](bool value) { return value; });
|
bool all_extruder_synced = std::all_of(extruder_synced.begin(), extruder_synced.end(), [](bool value) { return value; });
|
||||||
@@ -4048,7 +4360,7 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
|||||||
select = combo_flow->GetCount() - 1;
|
select = combo_flow->GetCount() - 1;
|
||||||
combo_flow->SetSelection(select);
|
combo_flow->SetSelection(select);
|
||||||
// No flow variant for this extruder: the diameter keeps the whole row.
|
// No flow variant for this extruder: the diameter keeps the whole row.
|
||||||
combo_flow->Show(combo_flow->GetCount() > 0);
|
combo_flow->Show(combo_flow->GetCount() > 0 && extruder.IsNozzleRowVisible(row));
|
||||||
};
|
};
|
||||||
|
|
||||||
auto update_extruder_diameter = [&diameters, &nozzle_diameter, &diameter](int extruder_index,ExtruderGroup & extruder) {
|
auto update_extruder_diameter = [&diameters, &nozzle_diameter, &diameter](int extruder_index,ExtruderGroup & extruder) {
|
||||||
@@ -4095,7 +4407,10 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
|||||||
//}
|
//}
|
||||||
p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight()));
|
p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight()));
|
||||||
} else {
|
} else {
|
||||||
AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder);
|
// The generic multi-tool card is populated from the live AMS bindings, not the two-entry
|
||||||
|
// AMS-count model used by the editable left/right cards.
|
||||||
|
if (!p->generic_multi_tool_layout)
|
||||||
|
AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder);
|
||||||
for (size_t row = 0; row < p->single_extruder->NozzleRowCount(); ++row)
|
for (size_t row = 0; row < p->single_extruder->NozzleRowCount(); ++row)
|
||||||
update_extruder_variant(*p->single_extruder, row, int(row));
|
update_extruder_variant(*p->single_extruder, row, int(row));
|
||||||
//if (!p->is_switching_diameter)
|
//if (!p->is_switching_diameter)
|
||||||
@@ -6069,8 +6384,12 @@ void Sidebar::on_bed_type_change(BedType bed_type)
|
|||||||
*
|
*
|
||||||
* Return Value:
|
* Return Value:
|
||||||
* - Map key encoding:
|
* - 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)
|
* - 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
|
* - The 0x10000 flag indicates the main/right extruder
|
||||||
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
|
||||||
*
|
*
|
||||||
@@ -6090,7 +6409,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%")
|
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;
|
% name % tray.setting_id % tray.m_fila_type % tray.color;
|
||||||
DynamicPrintConfig tray_config;
|
DynamicPrintConfig tray_config;
|
||||||
@@ -6105,6 +6430,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_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_exist", new ConfigOptionBools{tray.is_exists});
|
||||||
tray_config.set_key_value("filament_slot_placeholder", new ConfigOptionBools{tray.is_slot_placeholder});
|
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;
|
std::optional<FilamentBaseInfo> info;
|
||||||
if (wxGetApp().preset_bundle) {
|
if (wxGetApp().preset_bundle) {
|
||||||
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id);
|
||||||
@@ -6117,10 +6448,44 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (obj->ams_support_virtual_tray) {
|
if (obj->ams_support_virtual_tray) {
|
||||||
int extruder = 0x10000; // Main (first) extruder at right
|
for (size_t index = 0; index < obj->vt_slot.size(); ++index) {
|
||||||
for (auto & vt_tray : obj->vt_slot) {
|
auto & vt_tray = obj->vt_slot[index];
|
||||||
filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255
|
int vt_id = 0;
|
||||||
extruder = 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 +6505,61 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
|
|||||||
for (auto ams : list) {
|
for (auto ams : list) {
|
||||||
int ams_id = std::stoi(ams.first);
|
int ams_id = std::stoi(ams.first);
|
||||||
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
|
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()) {
|
for (auto tray : ams.second->GetTrays()) {
|
||||||
int slot_id = std::stoi(tray.first);
|
int slot_id = std::stoi(tray.first);
|
||||||
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
|
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + 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 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;
|
return filament_ams_list;
|
||||||
}
|
}
|
||||||
@@ -6381,6 +6796,20 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
|
|||||||
std::vector<std::pair<DynamicPrintConfig *,std::string>> unknowns;
|
std::vector<std::pair<DynamicPrintConfig *,std::string>> unknowns;
|
||||||
auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams");
|
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";
|
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);
|
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;
|
wxString detail;
|
||||||
for (auto & uk : unknowns) {
|
for (auto & uk : unknowns) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
|
#include "BackgroundSlicingProcess.hpp" // complete type for background_process().get_current_gcode_result()
|
||||||
#include "DeviceCore/DevStorage.h"
|
#include "DeviceCore/DevStorage.h"
|
||||||
|
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "libslic3r/PrintConfig.hpp"
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
@@ -1155,7 +1156,7 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
|
|||||||
wxString ams_id;
|
wxString ams_id;
|
||||||
wxColour ams_col;
|
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");
|
ams_id = _L("Ext");
|
||||||
}else if (f->tray_id >= 0) {
|
}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())
|
if (m_ams_mapping_result.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
|
|
||||||
bool valid_mapping_result = true;
|
bool valid_mapping_result = true;
|
||||||
int invalid_count = 0;
|
int invalid_count = 0;
|
||||||
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
|
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
|
// convert #RRGGBB to RRGGBBAA
|
||||||
mapping_item["sourceColor"] = m_filaments[k].color;
|
mapping_item["sourceColor"] = m_filaments[k].color;
|
||||||
mapping_item["targetColor"] = m_ams_mapping_result[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*/
|
/*new ams mapping data*/
|
||||||
try {
|
try {
|
||||||
|
|||||||
+831
-255
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,8 @@
|
|||||||
#ifndef slic3r_StatusPanel_hpp_
|
#ifndef slic3r_StatusPanel_hpp_
|
||||||
#define slic3r_StatusPanel_hpp_
|
#define slic3r_StatusPanel_hpp_
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "libslic3r/ProjectTask.hpp"
|
#include "libslic3r/ProjectTask.hpp"
|
||||||
#include "DeviceManager.hpp"
|
#include "DeviceManager.hpp"
|
||||||
#include "MonitorPage.hpp"
|
#include "MonitorPage.hpp"
|
||||||
@@ -66,6 +68,7 @@
|
|||||||
#include "StagedBuild.hpp"
|
#include "StagedBuild.hpp"
|
||||||
|
|
||||||
class StepIndicator;
|
class StepIndicator;
|
||||||
|
class wxChoice;
|
||||||
|
|
||||||
#define COMMAND_TIMEOUT 5
|
#define COMMAND_TIMEOUT 5
|
||||||
|
|
||||||
@@ -140,28 +143,36 @@ class ExtruderImage : public wxWindow
|
|||||||
ScalableBitmap *m_left_extruder_active_empty;
|
ScalableBitmap *m_left_extruder_active_empty;
|
||||||
ScalableBitmap *m_left_extruder_unactive_filled;
|
ScalableBitmap *m_left_extruder_unactive_filled;
|
||||||
ScalableBitmap *m_left_extruder_unactive_empty;
|
ScalableBitmap *m_left_extruder_unactive_empty;
|
||||||
|
|
||||||
ScalableBitmap *m_right_extruder_active_filled;
|
ScalableBitmap *m_right_extruder_active_filled;
|
||||||
ScalableBitmap *m_right_extruder_active_empty;
|
ScalableBitmap *m_right_extruder_active_empty;
|
||||||
ScalableBitmap *m_right_extruder_unactive_filled;
|
ScalableBitmap *m_right_extruder_unactive_filled;
|
||||||
ScalableBitmap *m_right_extruder_unactive_empty;
|
ScalableBitmap *m_right_extruder_unactive_empty;
|
||||||
|
|
||||||
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
ScalableBitmap *m_extruder_single_nozzle_empty_load;
|
||||||
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
|
||||||
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
ScalableBitmap *m_extruder_single_nozzle_filled_load;
|
||||||
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
|
||||||
|
|
||||||
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||||
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
|
||||||
ExtruderState m_single_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:
|
public:
|
||||||
void update(int nozzle_num, int nozzle_id);
|
void update(int nozzle_num, int nozzle_id);
|
||||||
void update(ExtruderState single_state);
|
void update(ExtruderState single_state);
|
||||||
void update(ExtruderState right_state, ExtruderState left_state);
|
void update(ExtruderState right_state, ExtruderState left_state);
|
||||||
|
|
||||||
|
void update(ExtruderState state, int idx);
|
||||||
|
|
||||||
void msw_rescale();
|
void msw_rescale();
|
||||||
|
void updateGenericSize();
|
||||||
void setExtruderCount(int nozzle_num);
|
void setExtruderCount(int nozzle_num);
|
||||||
|
void setGenericNozzleDisplay(bool enabled);
|
||||||
void setExtruderUsed(std::string loc);
|
void setExtruderUsed(std::string loc);
|
||||||
|
void setExtruderUsed(int nozzle_idx);
|
||||||
void paintEvent(wxPaintEvent &evt);
|
void paintEvent(wxPaintEvent &evt);
|
||||||
|
|
||||||
void render(wxDC &dc);
|
void render(wxDC &dc);
|
||||||
@@ -495,6 +506,8 @@ protected:
|
|||||||
std::vector<ExtruderImage *> m_extruderImage;
|
std::vector<ExtruderImage *> m_extruderImage;
|
||||||
|
|
||||||
SwitchBoard * m_nozzle_btn_panel;
|
SwitchBoard * m_nozzle_btn_panel;
|
||||||
|
wxChoice* m_generic_nozzle_selector{nullptr};
|
||||||
|
int m_generic_nozzle_selector_count{0};
|
||||||
|
|
||||||
wxStaticText * m_text_tasklist_caption;
|
wxStaticText * m_text_tasklist_caption;
|
||||||
|
|
||||||
@@ -507,10 +520,23 @@ protected:
|
|||||||
wxBoxSizer * m_misc_ctrl_sizer;
|
wxBoxSizer * m_misc_ctrl_sizer;
|
||||||
StaticBox* m_fan_panel;
|
StaticBox* m_fan_panel;
|
||||||
StaticLine * m_line_nozzle;
|
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 };
|
int m_temp_nozzle_timeout{ 0 };
|
||||||
TempInput* m_tempCtrl_nozzle_deputy;
|
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
|
||||||
int m_temp_nozzle_deputy_timeout{ 0 };
|
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;
|
TempInput * m_tempCtrl_bed;
|
||||||
int m_temp_bed_timeout {0};
|
int m_temp_bed_timeout {0};
|
||||||
TempInput * m_tempCtrl_chamber;
|
TempInput * m_tempCtrl_chamber;
|
||||||
@@ -550,6 +576,7 @@ protected:
|
|||||||
|
|
||||||
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
|
||||||
SwitchBoard* m_ams_rack_switch{ nullptr };
|
SwitchBoard* m_ams_rack_switch{ nullptr };
|
||||||
|
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
|
||||||
|
|
||||||
AMSControl* m_ams_control;
|
AMSControl* m_ams_control;
|
||||||
StaticBox* m_ams_control_box;
|
StaticBox* m_ams_control_box;
|
||||||
@@ -624,6 +651,9 @@ public:
|
|||||||
|
|
||||||
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
|
||||||
wxBoxSizer *create_temp_control(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_misc_control(wxWindow *parent);
|
||||||
wxBoxSizer *create_axis_control(wxWindow *parent);
|
wxBoxSizer *create_axis_control(wxWindow *parent);
|
||||||
wxPanel *create_bed_control(wxWindow *parent);
|
wxPanel *create_bed_control(wxWindow *parent);
|
||||||
@@ -654,6 +684,7 @@ private:
|
|||||||
friend class MonitorPanel;
|
friend class MonitorPanel;
|
||||||
void wire_controls();
|
void wire_controls();
|
||||||
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
|
||||||
|
void sync_nozzle_temp_controls(size_t count);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
|
||||||
@@ -801,6 +832,7 @@ protected:
|
|||||||
void update_cloud_subtask(MachineObject *obj);
|
void update_cloud_subtask(MachineObject *obj);
|
||||||
void update_sdcard_subtask(MachineObject *obj);
|
void update_sdcard_subtask(MachineObject *obj);
|
||||||
void update_temp_ctrl(MachineObject *obj);
|
void update_temp_ctrl(MachineObject *obj);
|
||||||
|
void check_extruder_count_mismatch(MachineObject* obj);
|
||||||
void update_misc_ctrl(MachineObject *obj);
|
void update_misc_ctrl(MachineObject *obj);
|
||||||
void update_ams(MachineObject* obj);
|
void update_ams(MachineObject* obj);
|
||||||
void update_rack(MachineObject* obj);
|
void update_rack(MachineObject* obj);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <wx/anybutton.h>
|
#include <wx/anybutton.h>
|
||||||
#include <wx/animate.h>
|
#include <wx/animate.h>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/busycursor.h>
|
#include <wx/busycursor.h>
|
||||||
#include <wx/chartype.h>
|
#include <wx/chartype.h>
|
||||||
#include <wx/arrstr.h>
|
#include <wx/arrstr.h>
|
||||||
@@ -1245,7 +1246,7 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
|
|||||||
wxString ams_id;
|
wxString ams_id;
|
||||||
wxColour ams_col;
|
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");
|
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())
|
if (m_ams_mapping_result.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
|
|
||||||
bool valid_mapping_result = true;
|
bool valid_mapping_result = true;
|
||||||
int invalid_count = 0;
|
int invalid_count = 0;
|
||||||
for (int i = 0; i < m_ams_mapping_result.size(); i++) {
|
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
|
// convert #RRGGBB to RRGGBBAA
|
||||||
mapping_item["sourceColor"] = m_filaments[k].color;
|
mapping_item["sourceColor"] = m_filaments[k].color;
|
||||||
mapping_item["targetColor"] = m_ams_mapping_result[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*/
|
/*new ams mapping data*/
|
||||||
try {
|
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_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(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_ams_body, 0, wxALIGN_CENTER, 0);
|
||||||
m_sizer_body->Add(m_sizer_down_road, 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);
|
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
|
||||||
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else if (m_total_ext_count == 1){
|
||||||
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
|
||||||
if (id == ams_id){
|
if (id == ams_id){
|
||||||
return true;
|
return true;
|
||||||
@@ -379,6 +379,10 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Generic N Nozzles
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
|
||||||
@@ -519,7 +523,7 @@ void AMSControl::msw_rescale()
|
|||||||
m_button_ams_setting_press.msw_rescale();
|
m_button_ams_setting_press.msw_rescale();
|
||||||
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
|
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_feed) m_button_extruder_feed->Rescale(); // ORCA
|
||||||
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
|
||||||
@@ -603,6 +607,8 @@ void AMSControl::CreateAms()
|
|||||||
|
|
||||||
|
|
||||||
void AMSControl::ClearAms() {
|
void AMSControl::ClearAms() {
|
||||||
|
m_sizer_ams_items->Clear(false);
|
||||||
|
|
||||||
m_simplebook_ams_right->DeleteAllPages();
|
m_simplebook_ams_right->DeleteAllPages();
|
||||||
m_simplebook_ams_left->DeleteAllPages();
|
m_simplebook_ams_left->DeleteAllPages();
|
||||||
m_simplebook_ams_right->DestroyChildren();
|
m_simplebook_ams_right->DestroyChildren();
|
||||||
@@ -613,7 +619,7 @@ void AMSControl::ClearAms() {
|
|||||||
m_simplebook_ams_left->Refresh();
|
m_simplebook_ams_left->Refresh();
|
||||||
|
|
||||||
for (auto it : m_ams_preview_list) {
|
for (auto it : m_ams_preview_list) {
|
||||||
delete it.second;
|
if (it.second) it.second->Destroy();
|
||||||
}
|
}
|
||||||
m_ams_preview_list.clear();
|
m_ams_preview_list.clear();
|
||||||
m_ext_image_list.clear();
|
m_ext_image_list.clear();
|
||||||
@@ -624,12 +630,181 @@ void AMSControl::ClearAms() {
|
|||||||
m_ams_item_list.clear();
|
m_ams_item_list.clear();
|
||||||
m_sizer_prv_right->Clear();
|
m_sizer_prv_right->Clear();
|
||||||
m_sizer_prv_left->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();
|
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)
|
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_left;
|
||||||
std::vector<AMSinfo> single_info_right;
|
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)
|
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<int>m_item_nums{0,0};
|
||||||
std::vector<AMSinfo> single_info;
|
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()) {
|
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
|
||||||
fresh = true;
|
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{
|
else{
|
||||||
@@ -993,11 +1181,16 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
|||||||
m_dev_id = dev_id;
|
m_dev_id = dev_id;
|
||||||
if (fresh){
|
if (fresh){
|
||||||
ClearAms();
|
ClearAms();
|
||||||
if (m_total_ext_count >= 2){
|
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
|
||||||
CreateAmsDoubleNozzle(series_name, printer_type);
|
if (is_bbl) {
|
||||||
}else{
|
if (m_total_ext_count == 1)
|
||||||
CreateAmsSingleNozzle(series_name, printer_type);
|
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));
|
SetSize(wxSize(FromDIP(578), -1));
|
||||||
SetMinSize(wxSize(FromDIP(578), -1));
|
SetMinSize(wxSize(FromDIP(578), -1));
|
||||||
Layout();
|
Layout();
|
||||||
@@ -1070,7 +1263,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*update ams extruder*/
|
/*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();
|
m_amswin->Layout();
|
||||||
}
|
}
|
||||||
@@ -1106,7 +1299,15 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
|
|||||||
{
|
{
|
||||||
AMSPreview *ams_prv = nullptr;
|
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);
|
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));
|
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) {
|
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
|
||||||
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
|
||||||
parent->InsertPage(idx, ams_item, wxEmptyString, true);
|
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);
|
ams_item->set_selection(idx);
|
||||||
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;
|
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;
|
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){
|
for (auto ids : pair_id){
|
||||||
if (ids.first == ams_id || ids.second == ams_id){
|
if (ids.first == ams_id || ids.second == ams_id){
|
||||||
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
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);
|
wxPanel* book_panel = new wxPanel(parent);
|
||||||
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
|
||||||
book_panel->SetSize(AMS_PANEL_SIZE);
|
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
|
||||||
book_panel->SetMinSize(AMS_PANEL_SIZE);
|
// 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;
|
AmsItem* ams1 = nullptr, * ams2 = nullptr;
|
||||||
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
|
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, wxALIGN_CENTER_HORIZONTAL, 0);
|
||||||
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
|
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{
|
else{
|
||||||
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
|
||||||
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
|
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)
|
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()){
|
if (info.routes_to_main_extruder()){
|
||||||
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
|
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){
|
else if (m_total_ext_count == 1){
|
||||||
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
|
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_left->Layout();
|
||||||
m_simplebook_ams_right->Layout();
|
m_simplebook_ams_right->Layout();
|
||||||
m_simplebook_ams_left->Refresh();
|
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){
|
if (single_info.size() <= 0){
|
||||||
return;
|
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()) {
|
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);
|
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);
|
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_left->Layout();
|
||||||
m_simplebook_ams_right->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) {
|
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
|
||||||
if (single_info.size() <= 0) return;
|
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_prv = nullptr;
|
||||||
AMSPreview* ams_prv2 = nullptr;
|
AMSPreview* ams_prv2 = nullptr;
|
||||||
if (pos == AMSPanelPos::RIGHT_PANEL){
|
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)
|
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;}
|
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
|
||||||
|
|
||||||
bool is_in_right = IsAmsInRightPanel(ams_id);
|
bool is_in_right = IsAmsInRightPanel(ams_id);
|
||||||
@@ -1432,13 +1832,17 @@ void AMSControl::SwitchAms(std::string ams_id)
|
|||||||
else {
|
else {
|
||||||
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
|
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;
|
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
|
||||||
|
|
||||||
for (auto it : pair_id) {
|
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
|
||||||
if (it.first == ams_id || it.second == ams_id) {
|
for (auto it : pair_id) {
|
||||||
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
if (it.first == ams_id || it.second == ams_id) {
|
||||||
break;
|
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
|
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;
|
AmsItem *item = nullptr;
|
||||||
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
|
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) {
|
if (on_off && item) {
|
||||||
auto col = item->GetTagColr(slot_id);
|
auto col = item->GetTagColr(slot_id);
|
||||||
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
m_extruder->OnAmsLoading(true, nozzle_id, col);
|
||||||
@@ -1624,6 +2042,69 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
|
|||||||
else{
|
else{
|
||||||
return;
|
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())
|
if (can_index >= 0 && can_index < info.cans.size())
|
||||||
{
|
{
|
||||||
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ public:
|
|||||||
void on_retry();
|
void on_retry();
|
||||||
|
|
||||||
protected:
|
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_ams;
|
||||||
std::string m_current_slot_left;
|
std::string m_current_slot_left;
|
||||||
std::string m_current_slot_right;
|
std::string m_current_slot_right;
|
||||||
@@ -63,6 +77,9 @@ protected:
|
|||||||
|
|
||||||
std::string m_dev_id;
|
std::string m_dev_id;
|
||||||
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
|
||||||
|
std::vector<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;
|
std::vector<std::pair<std::string, std::string>> pair_id;
|
||||||
|
|
||||||
int m_total_ext_count = 1;
|
int m_total_ext_count = 1;
|
||||||
@@ -103,6 +120,7 @@ protected:
|
|||||||
wxSimplebook *m_simplebook_ams_left{nullptr};
|
wxSimplebook *m_simplebook_ams_left{nullptr};
|
||||||
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
wxSimplebook *m_simplebook_ams_right{ nullptr };
|
||||||
wxSimplebook *m_simplebook_bottom{nullptr};
|
wxSimplebook *m_simplebook_bottom{nullptr};
|
||||||
|
wxSizerItem *m_sizer_ams_gap{nullptr};
|
||||||
wxPanel *m_panel_down_road{ nullptr };
|
wxPanel *m_panel_down_road{ nullptr };
|
||||||
int m_left_page_index = 0;
|
int m_left_page_index = 0;
|
||||||
int m_right_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 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);
|
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);
|
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
|
||||||
|
void restore_legacy_ams_layout();
|
||||||
|
|
||||||
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
AMSModel m_ams_model{AMSModel::EXT_AMS};
|
||||||
AMSModel m_ext_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 UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||||
void CreateAms();
|
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 CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
|
||||||
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
|
||||||
void ClearAms();
|
void ClearAms();
|
||||||
@@ -210,6 +230,8 @@ public:
|
|||||||
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
|
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 SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
|
||||||
void SwitchAms(std::string ams_id);
|
void SwitchAms(std::string ams_id);
|
||||||
|
void SelectNozzle(int nozzle_id);
|
||||||
|
int GetActiveNozzleId() const { return m_active_nozzle_id; }
|
||||||
|
|
||||||
void msw_rescale();
|
void msw_rescale();
|
||||||
void on_filament_load(wxCommandEvent &event);
|
void on_filament_load(wxCommandEvent &event);
|
||||||
@@ -234,6 +256,11 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
|
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:
|
public:
|
||||||
std::string m_current_select;
|
std::string m_current_select;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <wx/timer.h>
|
#include <wx/timer.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
|
#include <wx/scrolwin.h>
|
||||||
|
|
||||||
#include "CalibUtils.hpp"
|
#include "CalibUtils.hpp"
|
||||||
#include "slic3r/GUI/wxExtensions.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_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
|
||||||
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
|
||||||
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 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)
|
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
|
||||||
{
|
{
|
||||||
if (!ams) return false;
|
if (!ams) return false;
|
||||||
@@ -170,10 +213,14 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
|
|||||||
info.can_id = std::to_string(0);
|
info.can_id = std::to_string(0);
|
||||||
this->cans.clear();
|
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;
|
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()) {
|
if (tray.is_tray_info_ready()) {
|
||||||
info.ctype = tray.ctype;
|
info.ctype = tray.ctype;
|
||||||
@@ -856,7 +903,11 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
|
|||||||
|
|
||||||
void AMSextruder::TurnOff()
|
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)
|
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);
|
m_left_extruder->OnAmsLoading(load, col);
|
||||||
if (load) m_current_colur_deputy = 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) {
|
if (nozzle_id == MAIN_EXTRUDER_ID) {
|
||||||
m_right_extruder->OnAmsLoading(load, col);
|
m_right_extruder->OnAmsLoading(load, col);
|
||||||
if (m_current_colur != 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*/
|
/*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_series_name = series_name;
|
||||||
m_nozzle_num = nozzle_num;
|
m_nozzle_num = nozzle_num;
|
||||||
this->DestroyChildren();
|
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);
|
m_left_extruder->setShowState(true);
|
||||||
if (m_nozzle_num >= 2)
|
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 = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
|
||||||
m_left_extruder->setShowState(true);
|
m_left_extruder->setShowState(true);
|
||||||
m_right_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->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
|
||||||
m_bitmap_sizer->AddSpacer(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
|
else
|
||||||
{
|
{
|
||||||
if (MachineObject::is_series_n(m_series_name))
|
// Generic N Nozzles
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SetSizer(m_bitmap_sizer);
|
SetSizer(m_bitmap_sizer);
|
||||||
@@ -951,6 +1017,9 @@ void AMSextruder::msw_rescale()
|
|||||||
//m_amsSextruder->msw_rescale();
|
//m_amsSextruder->msw_rescale();
|
||||||
if (m_left_extruder) m_left_extruder->msw_rescale();
|
if (m_left_extruder) m_left_extruder->msw_rescale();
|
||||||
if (m_right_extruder) m_right_extruder->msw_rescale();
|
if (m_right_extruder) m_right_extruder->msw_rescale();
|
||||||
|
for (auto *extruder : m_nozzle_extruders) {
|
||||||
|
if (extruder) extruder->msw_rescale();
|
||||||
|
}
|
||||||
Layout();
|
Layout();
|
||||||
Update();
|
Update();
|
||||||
Refresh();
|
Refresh();
|
||||||
@@ -2123,7 +2192,10 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
|
|||||||
m_ams_model = model;
|
m_ams_model = model;
|
||||||
|
|
||||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
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{
|
else{
|
||||||
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
|
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.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||||
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
|
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){
|
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));
|
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){
|
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();
|
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*/)
|
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
|
||||||
{
|
{
|
||||||
/*m_vams_loading = load;
|
/*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)); }
|
/*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)); }*/
|
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
|
||||||
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
|
||||||
|
|
||||||
auto xpos = left_nozzle_pos.x;
|
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;
|
auto length = 50;
|
||||||
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
|
||||||
length = -13;
|
length = -13;
|
||||||
@@ -2465,6 +2594,9 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
|||||||
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
|
||||||
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
|
||||||
break;
|
break;
|
||||||
|
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||||
|
// Generic N-lane geometry is intentionally left unimplemented.
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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, (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));
|
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
|
||||||
break;
|
break;
|
||||||
|
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
|
||||||
|
// Generic N-lane geometry is intentionally left unimplemented.
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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_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(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(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((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));
|
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
|
// Generic N Nozzles
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2533,10 +2683,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
|||||||
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
|
||||||
int x = left_nozzle_pos.x;
|
int x = left_nozzle_pos.x;
|
||||||
int len = m_right_road_length;
|
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;
|
x = right_nozzle_pos.x;
|
||||||
len = len - 14;
|
len = len - 14;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Generic N Nozzles
|
||||||
|
}
|
||||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
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 + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
||||||
@@ -2544,10 +2700,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
|
|||||||
else{
|
else{
|
||||||
int x = left_nozzle_pos.x;
|
int x = left_nozzle_pos.x;
|
||||||
int len = m_right_road_length;
|
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;
|
x = right_nozzle_pos.x;
|
||||||
len = len - 14;
|
len = len - 14;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Generic N Nozzles
|
||||||
|
}
|
||||||
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
|
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 + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
|
||||||
dc.DrawLine((x), (size.y / 2), (x), (size.y));
|
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) {
|
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 (pos == AMSPanelPos::LEFT_PANEL){
|
||||||
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
|
||||||
m_left_road_length = len;;
|
m_left_road_length = len;;
|
||||||
@@ -2583,7 +2745,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
|||||||
m_pass_road_right_step = step;
|
m_pass_road_right_step = step;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else{
|
else if (m_nozzle_num == 1){
|
||||||
if (pos == AMSPanelPos::LEFT_PANEL) {
|
if (pos == AMSPanelPos::LEFT_PANEL) {
|
||||||
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
|
||||||
m_left_road_length = len;
|
m_left_road_length = len;
|
||||||
@@ -2597,6 +2759,9 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
|
|||||||
m_pass_road_right_step = step;
|
m_pass_road_right_step = step;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Generic N Nozzles
|
||||||
|
}
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2615,7 +2780,10 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
|
|||||||
{
|
{
|
||||||
wxWindow::Create(parent, id, pos);
|
wxWindow::Create(parent, id, pos);
|
||||||
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
|
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 {
|
else {
|
||||||
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
|
||||||
@@ -2650,7 +2818,13 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
|
|||||||
m_amsinfo = amsinfo;
|
m_amsinfo = amsinfo;
|
||||||
m_ams_item_type = amsinfo.ams_type;
|
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);
|
SetMinSize(AMS_PREV_FOUR_SIZE);
|
||||||
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
SetMaxSize(AMS_PREV_FOUR_SIZE);
|
||||||
} else {
|
} else {
|
||||||
@@ -2758,8 +2932,41 @@ void AMSPreview::doRender(wxDC &dc)
|
|||||||
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
|
||||||
|
|
||||||
auto left = 0;
|
auto left = 0;
|
||||||
//four slot
|
// Generic AMS previews use the same normalized lane centers as the card row and connector.
|
||||||
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
|
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);
|
left = FromDIP(8);
|
||||||
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
|
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;
|
m_panel_pos = pos;
|
||||||
|
|
||||||
if (m_ams_model == AMSModel::GENERIC_AMS){
|
if (m_ams_model == AMSModel::GENERIC_AMS){
|
||||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
|
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||||
SetSize(AMS_CANS_WINDOW_SIZE);
|
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
|
||||||
SetMinSize(AMS_CANS_WINDOW_SIZE);
|
? generic_ams_layout_width(this, lane_count)
|
||||||
SetMaxSize(AMS_CANS_WINDOW_SIZE);
|
: 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{
|
else{
|
||||||
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
|
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) {
|
if (m_ams_model != AMSModel::AMS_LITE) {
|
||||||
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
sizer_can = new wxBoxSizer(wxHORIZONTAL);
|
||||||
sizer_item = new wxBoxSizer(wxVERTICAL);
|
sizer_item = new wxBoxSizer(wxVERTICAL);
|
||||||
auto it = m_info.cans.begin();
|
const bool generic_layout = use_generic_ams_layout(m_ams_model);
|
||||||
for (; it != m_info.cans.end(); it++) {
|
const int lane_count = static_cast<int>(m_info.cans.size());
|
||||||
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
|
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++;
|
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 wxWindow(this, wxID_ANY);
|
||||||
//auto road_panel = new wxPanel(this, wxID_ANY);
|
//auto road_panel = new wxPanel(this, wxID_ANY);
|
||||||
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
|
//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);
|
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);
|
if (scroll_lanes) {
|
||||||
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
|
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
|
||||||
sizer_item->Add(m_panel_road, 1, wxEXPAND);
|
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);
|
SetSizer(sizer_item);
|
||||||
}
|
}
|
||||||
@@ -3235,13 +3489,17 @@ void AmsItem::create(wxWindow *parent)
|
|||||||
|
|
||||||
Layout();
|
Layout();
|
||||||
Fit();
|
Fit();
|
||||||
|
if (m_can_scroll) {
|
||||||
|
m_can_scroll->Layout();
|
||||||
|
m_can_scroll->FitInside();
|
||||||
|
}
|
||||||
Thaw();
|
Thaw();
|
||||||
//Refresh();
|
//Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
|
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->SetSize(wxSize(FromDIP(52), FromDIP(109)));
|
||||||
amscan->SetMinSize(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->Layout();
|
||||||
amscan->Fit();
|
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_lib_list[caninfo.can_id] = m_panel_lib;
|
||||||
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
//m_can_road_list[caninfo.can_id] = m_panel_road;
|
||||||
@@ -3447,6 +3706,10 @@ void AmsItem::UpdateInfo(AMSinfo info)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Layout();
|
Layout();
|
||||||
|
if (m_can_scroll) {
|
||||||
|
m_can_scroll->Layout();
|
||||||
|
m_can_scroll->FitInside();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AmsItem::SetDefSelectCan()
|
void AmsItem::SetDefSelectCan()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/timer.h>
|
#include <wx/timer.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
|
#include <wx/scrolwin.h>
|
||||||
|
|
||||||
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
|
#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,
|
||||||
AMS_ROAD_MODE_SINGLE_N3S,
|
AMS_ROAD_MODE_SINGLE_N3S,
|
||||||
AMS_ROAD_MODE_AMS_LITE,
|
AMS_ROAD_MODE_AMS_LITE,
|
||||||
|
AMS_ROAD_MODE_GENERIC,
|
||||||
AMS_ROAD_MODE_NONE
|
AMS_ROAD_MODE_NONE
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -480,6 +482,7 @@ public:
|
|||||||
//AMSextruderImage *m_amsSextruder{nullptr};
|
//AMSextruderImage *m_amsSextruder{nullptr};
|
||||||
AMSextruderImage* m_left_extruder = nullptr;
|
AMSextruderImage* m_left_extruder = nullptr;
|
||||||
AMSextruderImage* m_right_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(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
|
||||||
~AMSextruder();
|
~AMSextruder();
|
||||||
|
|
||||||
@@ -681,6 +684,8 @@ public:
|
|||||||
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
|
||||||
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
|
||||||
void UpdateRight(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 OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
|
||||||
void SetPassRoadColour(bool left, wxColour col);
|
void SetPassRoadColour(bool left, wxColour col);
|
||||||
@@ -698,6 +703,8 @@ private:
|
|||||||
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||||
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
|
||||||
bool m_selected = {false};
|
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_left_road_length = {-1};
|
||||||
int m_right_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, AMSLib*> m_can_lib_list;
|
||||||
//std::map<std::string, AMSRoad*> m_can_road_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 };
|
AMSRoadUpPart* m_panel_road = { nullptr };
|
||||||
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
std::map<std::string, AMSrefresh*> m_can_refresh_list;
|
||||||
AMSHumidity* m_humidity = { nullptr };
|
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)
|
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_diameter_list[extruder_id] = diameter;
|
||||||
m_volume_type_list[extruder_id] = volume_type;
|
m_volume_type_list[extruder_id] = volume_type;
|
||||||
|
|
||||||
@@ -867,14 +875,21 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
|
|||||||
bool has_right = false;
|
bool has_right = false;
|
||||||
for (auto& elem : nozzles_in_extruder) {
|
for (auto& elem : nozzles_in_extruder) {
|
||||||
auto& nozzle = elem.second;
|
auto& nozzle = elem.second;
|
||||||
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
|
||||||
if (nozzle.AtRightExtruder())
|
int extruder_id{};
|
||||||
has_right = true;
|
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);
|
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
|
||||||
|
|
||||||
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
|
||||||
}
|
}
|
||||||
|
// TODO: Update for N extruders
|
||||||
m_badge->SetExtruderValid(has_right);
|
m_badge->SetExtruderValid(has_right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,9 @@ void TempInput::SetFinish()
|
|||||||
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
|
||||||
event.SetInt(temp_type);
|
event.SetInt(temp_type);
|
||||||
event.SetString(wxString::Format("%d", m_input_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);
|
wxPostEvent(this->GetParent(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "slic3r/GUI/DeviceManager.hpp"
|
#include "slic3r/GUI/DeviceManager.hpp"
|
||||||
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
#include "slic3r/GUI/DeviceCore/DevFilaSystem.h"
|
||||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||||
|
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||||
#include "../GUI/DeviceCore/DevStorage.h"
|
#include "../GUI/DeviceCore/DevStorage.h"
|
||||||
#include "../GUI/DeviceCore/DevFirmware.h"
|
#include "../GUI/DeviceCore/DevFirmware.h"
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <ios>
|
#include <ios>
|
||||||
@@ -61,6 +63,42 @@ namespace websocket = beast::websocket;
|
|||||||
namespace net = boost::asio;
|
namespace net = boost::asio;
|
||||||
using tcp = net::ip::tcp;
|
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
|
struct WsEndpoint
|
||||||
{
|
{
|
||||||
std::string host;
|
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)
|
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
|
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
|
||||||
nlohmann::json ams_json = nlohmann::json::object();
|
|
||||||
nlohmann::json ams_array = nlohmann::json::array();
|
nlohmann::json ams_array = nlohmann::json::array();
|
||||||
|
|
||||||
// Calculate ams_exist_bits and tray_exist_bits
|
// 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;
|
unsigned long tray_exist_bits = 0;
|
||||||
|
|
||||||
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
|
||||||
ams_exist_bits |= (1 << ams_id);
|
if (ams_id < 32) {
|
||||||
|
ams_exist_bits |= (1UL << 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
|
|
||||||
|
|
||||||
nlohmann::json tray_array = nlohmann::json::array();
|
nlohmann::json tray_array = nlohmann::json::array();
|
||||||
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
|
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();
|
if (tray && tray->has_filament && slot_index < 32) {
|
||||||
tray_json["id"] = std::to_string(slot_id);
|
tray_exist_bits |= (1UL << slot_index);
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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_unit["tray"] = tray_array;
|
||||||
ams_array.push_back(ams_unit);
|
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)
|
// Format as hex strings (matching BBL protocol)
|
||||||
std::ostringstream ams_exist_ss;
|
std::ostringstream ams_exist_ss;
|
||||||
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
|
||||||
std::ostringstream tray_exist_ss;
|
std::ostringstream tray_exist_ss;
|
||||||
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
|
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"] = ams_array;
|
||||||
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
ams_json["ams_exist_bits"] = ams_exist_ss.str();
|
||||||
ams_json["tray_exist_bits"] = tray_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();
|
nlohmann::json print_json = nlohmann::json::object();
|
||||||
print_json["ams"] = ams_json;
|
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
|
// Call the parser to populate DevFilaSystem
|
||||||
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
|
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.
|
// 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.
|
// 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)) {
|
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
|
||||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
|
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
|
||||||
<< (max_lane_index + 1) << " lanes";
|
<< (max_lane_index + 1) << " lanes";
|
||||||
int ams_count = (max_lane_index + 4) / 4;
|
build_ams_payload_grouped(trays);
|
||||||
build_ams_payload(ams_count, max_lane_index, trays);
|
|
||||||
return true;
|
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)) {
|
if (fetch_hh_filament_info(trays, max_lane_index)) {
|
||||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
|
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
|
||||||
<< (max_lane_index + 1) << " gates";
|
<< (max_lane_index + 1) << " gates";
|
||||||
int ams_count = (max_lane_index + 4) / 4;
|
build_ams_payload_grouped(trays);
|
||||||
build_ams_payload(ams_count, max_lane_index, trays);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +845,30 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char*
|
|||||||
return 0;
|
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)
|
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())
|
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"];
|
const auto& value = json["result"]["value"];
|
||||||
trays.clear();
|
trays.clear();
|
||||||
max_lane_index = 0;
|
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()) {
|
for (const auto& [lane_key, lane_obj] : value.items()) {
|
||||||
if (!lane_obj.is_object()) {
|
if (!lane_obj.is_object()) {
|
||||||
|
++skipped_records;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract lane index from the "lane" field (tool number, 0-based)
|
// Lane tool number (0-based). AFC reports it as a string in the documented sample but it
|
||||||
std::string lane_str = safe_json_string(lane_obj, "lane");
|
// can also be a number; accept either and skip lanes without a usable index. Databases
|
||||||
int lane_index = -1;
|
// written without a "lane" field fall back to the key, which names the lane either as
|
||||||
if (!lane_str.empty()) {
|
// "T<N>" (0-based) or as "lane<N>" (1-based).
|
||||||
try {
|
int lane_index = safe_json_int_flexible(lane_obj, "lane", -1);
|
||||||
lane_index = std::stoi(lane_str);
|
|
||||||
} catch (...) {
|
|
||||||
lane_index = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lane_index < 0) {
|
if (lane_index < 0) {
|
||||||
continue;
|
lane_index = lane_index_from_key(lane_key);
|
||||||
|
if (lane_index < 0) {
|
||||||
|
++skipped_records;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AmsTrayData tray;
|
AmsTrayData tray;
|
||||||
tray.slot_index = lane_index;
|
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_color = safe_json_string(lane_obj, "color");
|
||||||
tray.tray_type = safe_json_string(lane_obj, "material");
|
tray.tray_type = safe_json_string(lane_obj, "material");
|
||||||
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
|
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);
|
max_lane_index = std::max(max_lane_index, lane_index);
|
||||||
trays.push_back(tray);
|
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()) {
|
if (trays.empty()) {
|
||||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found";
|
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_moonraker_filament_data: No lanes found";
|
||||||
return false;
|
return false;
|
||||||
@@ -915,52 +1139,234 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
|
|||||||
return false;
|
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();
|
trays.clear();
|
||||||
max_lane_index = 0;
|
|
||||||
|
|
||||||
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
|
||||||
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
|
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
|
||||||
int status = safe_array_int(gate_status, gate_idx);
|
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 material = safe_array_string(gate_material, gate_idx);
|
||||||
std::string color = safe_array_string(gate_color, gate_idx);
|
std::string color = safe_array_string(gate_color, gate_idx);
|
||||||
int nozzle_temp = safe_array_int(gate_temperature, 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;
|
AmsTrayData tray;
|
||||||
tray.slot_index = gate_idx;
|
tray.slot_index = gate_idx;
|
||||||
tray.tray_type = material;
|
const auto extruder_it = gate_extruder.find(gate_idx);
|
||||||
tray.tray_color = color;
|
tray.extruder_id = extruder_it != gate_extruder.end() ? extruder_it->second : 0;
|
||||||
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;
|
if (status > 0 && !material.empty()) {
|
||||||
tray.tray_info_idx = bundle
|
tray.tray_type = material;
|
||||||
? bundle->filaments.filament_id_by_type(tray.tray_type)
|
tray.tray_color = color;
|
||||||
: map_filament_type_to_generic_id(tray.tray_type);
|
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);
|
trays.push_back(tray);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trays.empty()) {
|
max_lane_index = num_gates - 1;
|
||||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found";
|
|
||||||
|
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;
|
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;
|
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)
|
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
|
||||||
{
|
{
|
||||||
auto json = nlohmann::json::parse(json_str, nullptr, false);
|
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") {
|
if (cmd == "home") {
|
||||||
send_gcode_async(dev_id, "G28");
|
send_gcode_async(dev_id, "G28");
|
||||||
return BAMBU_NETWORK_SUCCESS;
|
return BAMBU_NETWORK_SUCCESS;
|
||||||
@@ -1155,7 +1580,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
|
|||||||
nlohmann::json& status,
|
nlohmann::json& status,
|
||||||
std::string& error) const
|
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;
|
std::string response_body;
|
||||||
bool success = false;
|
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) {
|
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);
|
subscribe_objects.insert(name);
|
||||||
if (name == "extruder") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 MoonrakerPrinterAgent::build_print_payload_locked() const
|
||||||
{
|
{
|
||||||
nlohmann::json payload;
|
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()) {
|
if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) {
|
||||||
const auto& bed = status_cache["heater_bed"];
|
const auto& bed = status_cache["heater_bed"];
|
||||||
if (bed.contains("temperature") && bed["temperature"].is_number()) {
|
if (bed.contains("temperature") && bed["temperature"].is_number()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -88,6 +89,7 @@ protected:
|
|||||||
// Tray data for AMS payload building
|
// Tray data for AMS payload building
|
||||||
struct AmsTrayData {
|
struct AmsTrayData {
|
||||||
int slot_index = 0; // 0-based slot index
|
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;
|
bool has_filament = false;
|
||||||
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
std::string tray_type; // Material type (e.g., "PLA", "ASA")
|
||||||
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
|
||||||
@@ -99,6 +101,10 @@ protected:
|
|||||||
// Build ams JSON and call parser
|
// Build ams JSON and call parser
|
||||||
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
|
||||||
|
|
||||||
|
// 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
|
// Methods that derived classes may need to override or access
|
||||||
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
|
virtual bool init_device_info(const 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;
|
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 handle_ws_message(const std::string& dev_id, const std::string& payload);
|
||||||
void update_status_cache(const nlohmann::json& updates);
|
void update_status_cache(const nlohmann::json& updates);
|
||||||
nlohmann::json build_print_payload_locked() const;
|
nlohmann::json build_print_payload_locked() const;
|
||||||
|
// 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
|
// Print control helpers
|
||||||
int pause_print(const std::string& dev_id);
|
int pause_print(const std::string& dev_id);
|
||||||
@@ -163,10 +171,31 @@ private:
|
|||||||
// System-specific filament fetch methods
|
// System-specific filament fetch methods
|
||||||
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
||||||
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
|
bool fetch_moonraker_filament_data(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
|
// JSON helper methods
|
||||||
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
|
||||||
static int safe_json_int(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 std::string safe_array_string(const nlohmann::json& arr, int idx);
|
||||||
static int safe_array_int(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);
|
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
|
// 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
|
// 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
|
// 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_config_value_formatter.cpp
|
||||||
test_creality_cfs_match.cpp
|
test_creality_cfs_match.cpp
|
||||||
test_dev_mapping.cpp
|
test_dev_mapping.cpp
|
||||||
|
test_dev_extder_system.cpp
|
||||||
test_device_access.cpp
|
test_device_access.cpp
|
||||||
test_moonraker_discovery.cpp
|
test_moonraker_discovery.cpp
|
||||||
test_network_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>
|
#include <Windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||||
#include "libslic3r/ProjectTask.hpp"
|
#include "libslic3r/ProjectTask.hpp"
|
||||||
#include <vector>
|
|
||||||
#include <catch2/catch_message.hpp>
|
#include <catch2/catch_message.hpp>
|
||||||
|
|
||||||
#include <catch2/catch_all.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);
|
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]")
|
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
|
||||||
{
|
{
|
||||||
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
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);
|
DevFilaSystemParser::ParseV1_0(print_push, &obj_no_switch, obj_no_switch.GetFilaSystem().get(), false);
|
||||||
REQUIRE(obj_no_switch.GetFilaSystem()->GetAmsList().count("0") == 0);
|
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