| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- """Bambu Studio's global tray numbering (utils.tray_ids), for every AMS kind.
- Studio (DevFilaSystem::GetTrayIndexMap, DevAmsTray::get_nozzle_addr_id):
- regular AMS ams_id*4+slot, A2L AMS Lite 24+slot, AMS 3H 28+slot, AMS-B
- 37+slot, AMS-HT its unit id, external inputs their own vir_slot id -- 254/255,
- and 247-255 on a printer with a direct-connected nozzle rack, where input v
- feeds hotend 255-v.
- """
- import pytest
- from backend.app.utils.tray_ids import (
- AmsType,
- TrayLayout,
- ams_type_from_info,
- decode_wire_slot,
- external_hotend,
- external_slot_index,
- external_tray_id,
- global_tray_id,
- is_external_tray,
- is_rack_ams,
- legacy_tray_index,
- mapped_trays_with_slot,
- tray_flag_bit,
- unit_ams_type,
- unit_label,
- )
- # The H3C's AMS 3H as the printer reports it (info captured 2026-11-06).
- H3C_UNITS = [{"id": "32", "info": "240842026", "tray": [{"id": str(k)} for k in range(9)]}]
- H3C_VT = [{"id": str(v)} for v in range(247, 256)]
- H3C = TrayLayout.from_units(H3C_UNITS, H3C_VT)
- class TestAmsType:
- @pytest.mark.parametrize(
- ("info", "kind"),
- [
- ("10001001", AmsType.AMS),
- ("2002", AmsType.AMS_LITE),
- ("10003", AmsType.N3F),
- ("1104", AmsType.N3S),
- ("5", AmsType.AMS_LITE_MIXED),
- ("240842026", AmsType.ZMULTI_A),
- ("240002006", AmsType.ZMULTI_A),
- ("7", AmsType.ZMULTI_B),
- ],
- )
- def test_low_four_bits_of_the_hex_info(self, info, kind):
- assert ams_type_from_info(info) == kind
- @pytest.mark.parametrize("info", [None, "", "zz", True, 3.5])
- def test_unreadable_info_is_none(self, info):
- assert ams_type_from_info(info) is None
- def test_type_told_from_the_id_without_info(self):
- assert unit_ams_type(0) == AmsType.AMS
- assert unit_ams_type(3) == AmsType.AMS
- assert unit_ams_type(128) == AmsType.N3S
- assert unit_ams_type(135) == AmsType.N3S
- assert unit_ams_type(32) == AmsType.ZMULTI_A
- def test_info_outranks_the_id_except_for_an_ams_ht(self):
- assert unit_ams_type(32, "7") == AmsType.ZMULTI_B
- assert unit_ams_type(128, "1") == AmsType.N3S
- def test_rack_types(self):
- assert is_rack_ams(AmsType.ZMULTI_A) and is_rack_ams(AmsType.ZMULTI_B)
- assert not any(is_rack_ams(k) for k in (AmsType.AMS, AmsType.N3F, AmsType.N3S, AmsType.AMS_LITE_MIXED))
- class TestGlobalTrayId:
- @pytest.mark.parametrize(
- ("ams_id", "slot", "kind", "expected"),
- [
- (0, 0, AmsType.AMS, 0),
- (1, 2, AmsType.AMS, 6),
- (3, 3, AmsType.N3F, 15),
- (6, 1, AmsType.AMS_LITE, 25), # the A2L's AMS Lite, stored as unit 6
- (16, 1, AmsType.AMS_LITE_MIXED, 25), # ...and Studio's own numbering of its unit 16
- (32, 0, AmsType.ZMULTI_A, 28),
- (32, 8, AmsType.ZMULTI_A, 36),
- (33, 0, AmsType.ZMULTI_B, 37),
- (33, 4, AmsType.ZMULTI_B, 41),
- (128, 0, AmsType.N3S, 128),
- (135, 0, AmsType.N3S, 135),
- ],
- )
- def test_every_ams_kind(self, ams_id, slot, kind, expected):
- assert global_tray_id(ams_id, slot, kind) == expected
- def test_without_a_type_regular_ht_and_the_ams3h_by_their_ids(self):
- assert [global_tray_id(1, s) for s in range(4)] == [4, 5, 6, 7]
- assert global_tray_id(129, 0) == 129
- assert [global_tray_id(32, s) for s in range(9)] == list(range(28, 37))
- def test_the_ams3h_no_longer_lands_in_the_ams_ht_range(self):
- ams3h = {global_tray_id(32, s) for s in range(9)}
- assert ams3h.isdisjoint(range(128, 136))
- assert ams3h.isdisjoint(range(0, 16))
- class TestLayout:
- def test_h3c_is_a_rack_printer(self):
- assert H3C.rack
- assert H3C.rack_units == ((32, AmsType.ZMULTI_A),)
- assert H3C.ams_type(32) == AmsType.ZMULTI_A
- def test_both_directions_on_the_h3c(self):
- for slot in range(9):
- tray = H3C.global_tray_id(32, slot)
- assert tray == 28 + slot
- assert H3C.slot_key(tray) == (32, slot)
- assert H3C.tray_from_slot_key(32, slot) == tray
- def test_both_directions_on_a_regular_printer(self):
- layout = TrayLayout.from_units([{"id": "0", "info": "1001"}, {"id": "1", "info": "1001"}, {"id": "128"}])
- assert not layout.rack
- for ams_id in (0, 1):
- for slot in range(4):
- tray = layout.global_tray_id(ams_id, slot)
- assert tray == ams_id * 4 + slot
- assert layout.slot_key(tray) == (ams_id, slot)
- assert layout.tray_from_slot_key(ams_id, slot) == tray
- assert layout.global_tray_id(128, 0) == 128
- assert layout.slot_key(128) == (128, 0)
- def test_without_live_data_both_directions_agree(self):
- # No AMS data at all: 28-36 are the AMS 3H's (unit 32) and 247-253 the
- # per-hotend inputs, the same way round as tray_from_slot_key numbers them.
- empty = TrayLayout()
- for slot in range(9):
- assert empty.tray_from_slot_key(32, slot) == 28 + slot
- assert empty.slot_key(28 + slot) == (32, slot)
- for index in range(9):
- assert empty.slot_key(empty.tray_from_slot_key(255, index)) == (255, index)
- def test_a_listed_regular_unit_7_keeps_28_to_31(self):
- layout = TrayLayout.from_units([{"id": "7", "info": "1001"}])
- assert layout.slot_key(28) == (7, 0)
- assert layout.slot_key(31) == (7, 3)
- assert layout.tray_from_slot_key(7, 2) == 30
- def test_ams_b_by_its_info(self):
- layout = TrayLayout.from_units([{"id": "40", "info": "7"}])
- assert layout.rack
- assert layout.global_tray_id(40, 2) == 39
- assert layout.slot_key(39) == (40, 2)
- def test_api_units_with_an_explicit_type(self):
- layout = TrayLayout.from_units([{"id": 32, "ams_type": 6}])
- assert layout.global_tray_id(32, 4) == 32
- assert layout.slot_key(32) == (32, 4)
- def test_raw_tray_now_of_the_rack(self):
- assert [H3C.raw_slot_to_global(k) for k in range(9)] == list(range(28, 37))
- assert H3C.raw_slot_to_global(255) is None
- assert TrayLayout.from_units([{"id": "0"}]).raw_slot_to_global(2) is None
- def test_loaded_trays(self):
- regular = TrayLayout.from_units([{"id": "0"}])
- for tray in (0, 15, 24, 27, 128, 135, 254):
- assert regular.is_loaded_tray(tray)
- for tray in (16, 28, 250, 255):
- assert not regular.is_loaded_tray(tray)
- assert H3C.is_loaded_tray(28) and H3C.is_loaded_tray(36) and H3C.is_loaded_tray(247)
- assert not H3C.is_loaded_tray(255)
- def test_layout_from_a_state(self):
- class _State:
- raw_data = {"ams": H3C_UNITS, "vt_tray": H3C_VT}
- assert TrayLayout.from_state(_State()) == H3C
- assert TrayLayout.from_state(None) == TrayLayout()
- def test_an_ams3h_model_without_ams_data_gets_its_ams3h(self):
- class _NoAms:
- raw_data: dict = {}
- assert TrayLayout.from_state(None, ams3h_model=True) == TrayLayout.ams3h()
- assert TrayLayout.from_state(_NoAms(), ams3h_model=True).slot_key(250) == (255, 5)
- assert TrayLayout.ams3h().is_external(250)
- # Live units always win over the model.
- class _Regular:
- raw_data = {"ams": [{"id": "0", "info": "1001"}]}
- assert TrayLayout.from_state(_Regular(), ams3h_model=True).rack_units == ()
- class TestWireSlot:
- """(ams_id << 8) | slot, as print.mapping and the snow fields carry a slot."""
- def test_regular_ams_ht_and_external(self):
- assert decode_wire_slot((1 << 8) | 2) == 6
- assert decode_wire_slot((0 << 8) | 0x13) == 3 # slot bits & 3
- assert decode_wire_slot(129 << 8) == 129
- assert decode_wire_slot((254 << 8) | 0) == 254
- assert decode_wire_slot((255 << 8) | 0) == 254
- assert decode_wire_slot((254 << 8) | 255) == 255
- assert decode_wire_slot(0xFFFF) is None
- assert decode_wire_slot(-1) is None
- assert decode_wire_slot((16 << 8) | 1) is None
- assert decode_wire_slot("258") == 6
- def test_ams3h_slots_with_and_without_a_layout(self):
- assert [decode_wire_slot((32 << 8) | k, H3C) for k in range(9)] == list(range(28, 37))
- assert decode_wire_slot((32 << 8) | 6) == 34
- assert decode_wire_slot((32 << 8) | 9, H3C) is None
- def test_a_rack_printer_keeps_its_external_inputs(self):
- # On a rack printer every input 247-255 is its own hotend's.
- assert decode_wire_slot((250 << 8) | 0, H3C) == 250
- assert decode_wire_slot((255 << 8) | 0, H3C) == 255
- assert decode_wire_slot((254 << 8) | 0, H3C) == 254
- assert decode_wire_slot((250 << 8) | 0) is None
- def test_trays_with_a_local_slot(self):
- mapping = [(0 << 8) | 1, (1 << 8) | 1, (1 << 8) | 2, 128 << 8, 254 << 8, 0xFFFF]
- assert mapped_trays_with_slot(mapping, 1) == {1: 0, 5: 1}
- assert mapped_trays_with_slot(mapping, 0) == {128: 128}
- assert mapped_trays_with_slot(None, 0) == {}
- def test_legacy_index_is_the_flat_four_per_unit_number(self):
- assert legacy_tray_index(1, 2) == 6
- assert legacy_tray_index(16, 3) == 67 # the A2L AMS Lite's physical tray
- assert legacy_tray_index(255, 1) == 1021
- class TestExternals:
- def test_254_255_are_external_everywhere(self):
- for tray in (254, 255):
- assert is_external_tray(tray)
- assert is_external_tray(tray, rack=True)
- def test_247_to_253_only_on_a_rack_printer(self):
- for tray in range(247, 254):
- assert not is_external_tray(tray)
- assert is_external_tray(tray, rack=True)
- assert H3C.is_external(tray)
- assert not TrayLayout.from_units([{"id": "0"}], [{"id": "254"}, {"id": "255"}]).is_external(250)
- def test_ams_slots_are_not_external(self):
- for tray in (0, 15, 28, 36, 128):
- assert not is_external_tray(tray, rack=True)
- def test_an_input_feeds_hotend_255_minus_its_id(self):
- assert [external_hotend(v) for v in range(255, 246, -1)] == list(range(9))
- def test_stored_under_ams_255(self):
- assert external_slot_index(254) == 0 and external_slot_index(255) == 1
- assert [external_slot_index(v) for v in range(247, 254)] == list(range(2, 9))
- for v in range(247, 256):
- assert external_tray_id(external_slot_index(v)) == v
- assert H3C.slot_key(v) == (255, external_slot_index(v))
- assert H3C.tray_from_slot_key(255, external_slot_index(v)) == v
- def test_a_reported_input_247_to_253_makes_a_rack_printer(self):
- assert TrayLayout.from_units([], H3C_VT).rack
- assert not TrayLayout.from_units([], [{"id": "254"}, {"id": "255"}]).rack
- def test_a_regular_printer_keeps_its_external_slot_keys(self):
- layout = TrayLayout.from_units([{"id": "0"}], [{"id": "254"}])
- assert layout.slot_key(254) == (255, 0)
- assert layout.slot_key(255) == (255, 1)
- # 247-253 are only ever external inputs, so they keep their stored key everywhere.
- assert layout.slot_key(250) == (255, 5)
- assert not layout.is_external(250)
- class TestTrayBitsAndLabels:
- def test_tray_flag_bits(self):
- assert tray_flag_bit(1, 2) == 6
- assert tray_flag_bit(128, 0) == 16
- assert tray_flag_bit(130, 0) == 18
- assert tray_flag_bit(6, 1, AmsType.AMS_LITE_MIXED) == 25
- assert [tray_flag_bit(32, k, AmsType.ZMULTI_A) for k in range(9)] == list(range(28, 37))
- assert tray_flag_bit(40, 0, AmsType.AMS) is None
- def test_unit_labels(self):
- assert unit_label(0) == "AMS-A"
- assert unit_label(129) == "HT-B"
- assert unit_label(32) == "AMS 3H"
- def test_a_unit_listed_twice_counts_once():
- # A push's units are combined with the ones already known: unit 32 twice is one AMS 3H.
- layout = TrayLayout.from_units([{"id": "32", "info": "240002006"}, {"id": "32", "info": "240002006"}])
- assert layout.rack_units == ((32, AmsType.ZMULTI_A),)
- assert layout.raw_slot_to_global(4) == 32
|