mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Fix H2D AMS units displayed on wrong nozzle (#659)
Three interrelated bugs in AMS info field parsing caused AMS units
to appear on the wrong nozzle on H2D dual-nozzle printers:
1. Info field parsed as decimal instead of hex (BambuStudio uses
std::stoull with base 16)
2. Extruder ID extracted as 1-bit instead of 4-bit field (bits 8-11)
3. Partial MQTT updates replaced the full extruder map instead of
merging into it
This commit is contained in:
@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Ethernet Badge Always Shown on Printer Cards** — The printer card network badge always showed "Ethernet" instead of the WiFi signal indicator, even on printers without an ethernet port. The `home_flag` bit 18 was incorrectly interpreted as indicating a wired connection. Removed the faulty ethernet detection; the WiFi signal badge now displays correctly when the printer reports signal strength.
|
||||
- **GitHub Backup Required Cloud Login** ([#655](https://github.com/maziggy/bambuddy/issues/655)) — The GitHub backup settings card was completely blocked behind Bambu Cloud authentication, showing "Bambu Cloud login required" even though the backup feature works without it (K-profiles and app settings don't need cloud). Removed the cloud auth gate so GitHub backup can be configured and used without Bambu Cloud. The "Cloud Profiles" checkbox is disabled with a hint when not logged in. Reported by @TravisWilder.
|
||||
- **GitHub Backup Log Timestamps Off by 1 Hour** — Backup log timestamps in the history table were displayed in UTC instead of the user's local timezone. The local `formatDateTime` function didn't use `parseUTCDate`, so timezone-less timestamps from SQLite were interpreted as local time. Now uses the shared `parseUTCDate` utility for correct UTC-to-local conversion.
|
||||
- **H2D AMS Units Shown on Wrong Nozzle** ([#659](https://github.com/maziggy/bambuddy/issues/659)) — On the H2D dual-nozzle printer, AMS units were displayed on the wrong nozzle (e.g. both AMS-HT and AMS2 Pro shown on the left nozzle instead of their correct assignments). Three interrelated bugs in the AMS `info` field parsing: (1) the field was parsed as decimal instead of hexadecimal (BambuStudio uses `std::stoull(str, nullptr, 16)`), (2) the extruder ID was extracted as a single bit instead of a 4-bit field, and (3) partial MQTT updates overwrote the full extruder map instead of merging. Now correctly hex-parses the `info` field, extracts the 4-bit extruder ID from bits 8-11, skips uninitialized AMS units (`0xE`), and merges partial updates into the existing map. Reported by @cadtoolbox.
|
||||
|
||||
## [0.2.2b3] - Unreleased
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class PrinterState:
|
||||
mc_print_sub_stage: int = 0
|
||||
# AMS mapping for dual nozzle: which slot is active (from ams.ams_exist_bits/tray_exist_bits)
|
||||
ams_mapping: list = field(default_factory=list)
|
||||
# Per-AMS extruder map: {ams_id: extruder_id} where 0=right, 1=left
|
||||
# Per-AMS extruder map: {ams_id: extruder_id} where 0=right/main, 1=left/deputy
|
||||
ams_extruder_map: dict = field(default_factory=dict)
|
||||
# H2D per-extruder tray_now from snow field: {extruder_id: normalized_global_tray_id}
|
||||
# snow encodes AMS ID in high byte: ams_id = snow >> 8, slot = snow & 0xFF
|
||||
@@ -1431,29 +1431,35 @@ class BambuMQTTClient:
|
||||
logger.debug("[%s] Merged AMS data: %s new units, %s total", self.serial_number, len(ams_list), len(merged_ams))
|
||||
|
||||
# Extract ams_extruder_map from each AMS unit's info field
|
||||
# According to OpenBambuAPI: info field bit 8 indicates which extruder (0=right, 1=left)
|
||||
# BambuStudio DevFilaSystem.cpp parses info as hex string:
|
||||
# type_id = get_flag_bits(info, 0, 4) // bits 0-3: AMS type
|
||||
# extruder_id = get_flag_bits(info, 8, 4) // bits 8-11: extruder assignment
|
||||
# where get_flag_bits uses std::stoull(str, nullptr, 16) — hex parsing.
|
||||
# extruder_id: 0=right/main, 1=left/deputy, 0xE=uninitialized (skip)
|
||||
#
|
||||
# Use merged_ams (not ams_list) to avoid partial MQTT updates overwriting
|
||||
# the full map. Merge into existing map to preserve entries from prior updates.
|
||||
|
||||
ams_extruder_map = {}
|
||||
for ams_unit in ams_list:
|
||||
ams_extruder_map = dict(self.state.ams_extruder_map) if self.state.ams_extruder_map else {}
|
||||
for ams_unit in merged_ams:
|
||||
ams_id = ams_unit.get("id")
|
||||
info = ams_unit.get("info")
|
||||
if ams_id is not None and info is not None:
|
||||
try:
|
||||
info_val = int(info) if isinstance(info, str) else info
|
||||
# Extract bit 8 for extruder assignment
|
||||
# Bit 8 = 0 means LEFT extruder (id 1), bit 8 = 1 means RIGHT extruder (id 0)
|
||||
# So we invert: extruder_id = 1 - bit8
|
||||
bit8 = (info_val >> 8) & 0x1
|
||||
extruder_id = 1 - bit8 # 0=right, 1=left
|
||||
# info is a hex-encoded string in MQTT JSON (e.g. "10001003")
|
||||
info_val = int(str(info), 16)
|
||||
# Extract 4 bits starting at bit 8 for extruder assignment
|
||||
extruder_id = (info_val >> 8) & 0xF
|
||||
if extruder_id == 0xE:
|
||||
# 0xE = uninitialized AMS, skip
|
||||
continue
|
||||
ams_extruder_map[str(ams_id)] = extruder_id
|
||||
logger.debug(
|
||||
f"[{self.serial_number}] AMS {ams_id} info={info_val} (bit8={bit8}) -> extruder {extruder_id}"
|
||||
)
|
||||
logger.debug(f"[{self.serial_number}] AMS {ams_id} info=0x{info} -> extruder {extruder_id}")
|
||||
except (ValueError, TypeError):
|
||||
pass # Skip AMS units with unparseable info bitmask values
|
||||
if ams_extruder_map:
|
||||
self.state.raw_data["ams_extruder_map"] = ams_extruder_map
|
||||
self.state.ams_extruder_map = ams_extruder_map # Also set on state for inference logic
|
||||
self.state.ams_extruder_map = ams_extruder_map
|
||||
logger.debug("[%s] ams_extruder_map: %s", self.serial_number, ams_extruder_map)
|
||||
|
||||
# Create a hash of relevant AMS data to detect changes
|
||||
|
||||
@@ -1550,10 +1550,11 @@ class TestTrayNowDualNozzleH2DSetup:
|
||||
assert mqtt_client._is_dual_nozzle is True
|
||||
|
||||
def test_ams_extruder_map_parsed_from_info_field(self, mqtt_client):
|
||||
"""AMS 0 info=2003 → right (ext 0), AMS 128 info=2104 → left (ext 1)."""
|
||||
"""AMS info field is hex: 0x2003 → ext 0 (right), 0x2104 → ext 1 (left)."""
|
||||
# MQTT sends info as string; BambuStudio parses as hex via stoull(str, 16)
|
||||
ams_units = [
|
||||
{"id": 0, "info": 2003, "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": 2104, "tray": [{"id": 0}]},
|
||||
{"id": 0, "info": "2003", "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": "2104", "tray": [{"id": 0}]},
|
||||
]
|
||||
payload = {
|
||||
"print": {
|
||||
@@ -1566,8 +1567,80 @@ class TestTrayNowDualNozzleH2DSetup:
|
||||
}
|
||||
mqtt_client._process_message(payload)
|
||||
|
||||
# info=2003: bit8 = (2003>>8)&1 = 7&1 = 1 → extruder = 1-1 = 0 (right)
|
||||
# info=2104: bit8 = (2104>>8)&1 = 8&1 = 0 → extruder = 1-0 = 1 (left)
|
||||
# 0x2003: bits 8-11 = (0x2003 >> 8) & 0xF = 0x20 & 0xF = 0 → extruder 0 (right)
|
||||
# 0x2104: bits 8-11 = (0x2104 >> 8) & 0xF = 0x21 & 0xF = 1 → extruder 1 (left)
|
||||
assert mqtt_client.state.ams_extruder_map == {"0": 0, "128": 1}
|
||||
|
||||
def test_ams_extruder_map_real_h2d_values(self, mqtt_client):
|
||||
"""Real H2D MQTT values: AMS2 Pro on right, AMS-HT on left."""
|
||||
ams_units = [
|
||||
{"id": 0, "info": "10001003", "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": "10002104", "tray": [{"id": 0}]},
|
||||
]
|
||||
payload = {
|
||||
"print": {
|
||||
"ams": {
|
||||
"ams": ams_units,
|
||||
"tray_now": "255",
|
||||
"tray_exist_bits": "1000a",
|
||||
},
|
||||
}
|
||||
}
|
||||
mqtt_client._process_message(payload)
|
||||
|
||||
# 0x10001003: bits 8-11 = (0x10001003 >> 8) & 0xF = 0x10 & 0xF = 0 → right
|
||||
# 0x10002104: bits 8-11 = (0x10002104 >> 8) & 0xF = 0x21 & 0xF = 1 → left
|
||||
assert mqtt_client.state.ams_extruder_map == {"0": 0, "128": 1}
|
||||
|
||||
def test_ams_extruder_map_skips_uninitialized(self, mqtt_client):
|
||||
"""extruder_id 0xE means uninitialized AMS — should be skipped."""
|
||||
ams_units = [
|
||||
{"id": 0, "info": "e03", "tray": [{"id": i} for i in range(4)]},
|
||||
]
|
||||
payload = {
|
||||
"print": {
|
||||
"ams": {
|
||||
"ams": ams_units,
|
||||
"tray_now": "255",
|
||||
"tray_exist_bits": "f",
|
||||
},
|
||||
}
|
||||
}
|
||||
mqtt_client._process_message(payload)
|
||||
assert mqtt_client.state.ams_extruder_map == {}
|
||||
|
||||
def test_ams_extruder_map_partial_update_preserves_entries(self, mqtt_client):
|
||||
"""Partial MQTT update with one AMS should not overwrite other entries."""
|
||||
# First: full update with both AMS units
|
||||
full_payload = {
|
||||
"print": {
|
||||
"ams": {
|
||||
"ams": [
|
||||
{"id": 0, "info": "2003", "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": "2104", "tray": [{"id": 0}]},
|
||||
],
|
||||
"tray_now": "255",
|
||||
"tray_exist_bits": "1000f",
|
||||
},
|
||||
}
|
||||
}
|
||||
mqtt_client._process_message(full_payload)
|
||||
assert mqtt_client.state.ams_extruder_map == {"0": 0, "128": 1}
|
||||
|
||||
# Then: partial update with only AMS 0 (no info field this time)
|
||||
partial_payload = {
|
||||
"print": {
|
||||
"ams": {
|
||||
"ams": [
|
||||
{"id": 0, "tray": [{"id": 0, "remain": 50}]},
|
||||
],
|
||||
"tray_now": "0",
|
||||
"tray_exist_bits": "1000f",
|
||||
},
|
||||
}
|
||||
}
|
||||
mqtt_client._process_message(partial_payload)
|
||||
# Both entries should still be present
|
||||
assert mqtt_client.state.ams_extruder_map == {"0": 0, "128": 1}
|
||||
|
||||
def test_dual_nozzle_detection_before_ams_in_same_message(self, mqtt_client):
|
||||
@@ -1588,7 +1661,7 @@ class TestTrayNowDualNozzleH2DSetup:
|
||||
},
|
||||
"ams": {
|
||||
"ams": [
|
||||
{"id": 0, "info": 2003, "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 0, "info": "2003", "tray": [{"id": i} for i in range(4)]},
|
||||
],
|
||||
"tray_now": "2",
|
||||
"tray_exist_bits": "f",
|
||||
@@ -1639,8 +1712,8 @@ class _H2DFixtureMixin:
|
||||
},
|
||||
"ams": {
|
||||
"ams": [
|
||||
{"id": 0, "info": 2003, "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": 2104, "tray": [{"id": 0}]},
|
||||
{"id": 0, "info": "2003", "tray": [{"id": i} for i in range(4)]},
|
||||
{"id": 128, "info": "2104", "tray": [{"id": 0}]},
|
||||
],
|
||||
"tray_now": "255",
|
||||
"tray_exist_bits": "1000f",
|
||||
|
||||
Reference in New Issue
Block a user