Wired air condition, speed and light buttons

This commit is contained in:
Martin Ziegler
2025-12-03 17:29:46 +01:00
parent 9e638e2d0f
commit e8a11d273a
4 changed files with 149 additions and 20 deletions
+134 -14
View File
@@ -705,16 +705,65 @@ class BambuMQTTClient:
if "left_nozzle_target_temper" in data and "nozzle_target" not in temps:
temps["nozzle_target"] = float(data["left_nozzle_target_temper"])
if "chamber_temper" in data:
temps["chamber"] = float(data["chamber_temper"])
chamber_val = float(data["chamber_temper"])
logger.debug(f"[{self.serial_number}] chamber_temper raw value: {chamber_val}")
# Check if we recently set the target locally (within 5 seconds)
local_set_time = self.state.temperatures.get("_chamber_target_set_time", 0)
respect_local = (time.time() - local_set_time) < 5.0
# H2D protocol: chamber_temper encoding indicates heater state
# - When > 500: encoded as (target * 65536 + current) - heater is ON
# - When < 500: direct Celsius current temp only - heater is OFF
if -50 < chamber_val < 100:
# Direct value = heater is OFF
temps["chamber"] = chamber_val
if not respect_local:
temps["chamber_target"] = 0.0 # Heater off means target = 0
logger.debug(f"[{self.serial_number}] chamber_temper direct value: {chamber_val}°C (heater OFF)")
else:
logger.debug(f"[{self.serial_number}] chamber_temper {chamber_val} out of direct range")
# Try to decode if it looks like an encoded value
if chamber_val > 500:
mqtt_target = int(chamber_val) // 65536
current = int(chamber_val) % 65536
logger.debug(f"[{self.serial_number}] chamber_temper decoded: mqtt_target={mqtt_target}, current={current}, respect_local={respect_local}")
if -50 < current < 100:
temps["chamber"] = float(current)
# Store decoded target for later use, but DON'T set chamber_heating here!
# Heating state will be calculated later after parsing ctc.info.target (explicit target)
# which is the authoritative source the slicer uses.
if not respect_local:
if 0 <= mqtt_target <= 60:
# Store as "decoded" target - may be overridden by explicit target fields
temps["_chamber_decoded_target"] = float(mqtt_target)
# Chamber target temperature (set by print file or display)
if "mc_target_cham" in data:
temps["chamber_target"] = float(data["mc_target_cham"])
# H2D series: Chamber temp is in info.temp (directly in °C)
mc_target = float(data["mc_target_cham"])
logger.debug(f"[{self.serial_number}] mc_target_cham raw value: {mc_target}")
# Filter out encoded/invalid values - valid chamber target is 0-60°C
if 0 <= mc_target <= 60:
temps["chamber_target"] = mc_target
# H2D series: Chamber temp is in info.temp (may be encoded or direct °C)
# NOTE: Don't set chamber_heating here - let ctc.info.target or fallback logic handle it
# The encoded target in info.temp may be stale (slicer uses ctc.info.target as source of truth)
try:
if "info" in data and isinstance(data["info"], dict):
info_temp = data["info"].get("temp")
if info_temp is not None and "chamber" not in temps:
temps["chamber"] = float(info_temp)
# Check for encoded value (target * 65536 + current)
if info_temp > 500:
# Decode: extract current temperature and target
target = info_temp // 65536
current = info_temp % 65536
temps["chamber"] = float(current)
# Store decoded target as fallback (may be overridden by ctc.info.target)
if "_chamber_decoded_target" not in temps:
temps["_chamber_decoded_target"] = float(target)
logger.debug(f"[{self.serial_number}] info.temp encoded: {info_temp} -> current={current}, decoded_target={target}")
elif -50 < info_temp < 100:
# Valid direct temperature - heater is OFF
temps["chamber"] = float(info_temp)
temps["chamber_target"] = 0.0 # Direct value means heater off
logger.debug(f"[{self.serial_number}] info.temp direct: {info_temp}°C (heater OFF)")
# H2D series: Dual extruder temps are in device.extruder.info array
# Temperature values are encoded as fixed-point (value / 65536 = °C)
if "device" in data and isinstance(data["device"], dict):
@@ -794,28 +843,90 @@ class BambuMQTTClient:
if new_mode != self.state.airduct_mode:
logger.info(f"[{self.serial_number}] airduct_mode changed: {self.state.airduct_mode} -> {new_mode}")
self.state.airduct_mode = new_mode
# Parse chamber temp - may be encoded as (target*65536+current) when > 500
# Check if we recently set the target locally (within 5 seconds)
local_set_time = self.state.temperatures.get("_chamber_target_set_time", 0)
respect_local_target = (time.time() - local_set_time) < 5.0
# Log ctc_info contents for debugging
if ctc_info:
logger.debug(f"[{self.serial_number}] ctc_info keys: {list(ctc_info.keys())}")
# FIRST: Parse explicit ctc.info.target if available - this is the authoritative target
# (what the slicer shows). This OVERRIDES any previously decoded target.
explicit_target = None
if "target" in ctc_info:
target_val = ctc_info["target"]
logger.debug(f"[{self.serial_number}] ctc_info.target explicit value: {target_val}, respect_local={respect_local_target}")
# Filter out invalid values (valid chamber target is 0-60°C)
if 0 <= target_val <= 60 and not respect_local_target:
explicit_target = float(target_val)
temps["chamber_target"] = explicit_target # Override any previous value
logger.debug(f"[{self.serial_number}] Setting chamber_target from ctc_info.target: {explicit_target}")
# Parse chamber temp from ctc.info.temp - may be encoded
if "temp" in ctc_info and "chamber" not in temps:
temps["chamber"] = float(ctc_info["temp"])
# Parse chamber target from ctc.info.target if available
if "target" in ctc_info and "chamber_target" not in temps:
temps["chamber_target"] = float(ctc_info["target"])
# Parse chamber heating state from temp encoding
# temp > 500 means encoded (target*65536+current), heating = target > 0 AND current < target
if "temp" in ctc_info:
temp_val = ctc_info["temp"]
logger.debug(f"[{self.serial_number}] ctc_info.temp raw value: {temp_val}")
if temp_val > 500:
target = temp_val // 65536
# Encoded value: decode target and current
decoded_target = temp_val // 65536
current = temp_val % 65536
temps["chamber_heating"] = target > 0 and current < target
temps["chamber"] = float(current)
logger.debug(f"[{self.serial_number}] ctc_info.temp decoded: target={decoded_target}, current={current}, explicit_target={explicit_target}")
# Determine which target to use for heating state:
# Priority: local target > explicit target > decoded target
if respect_local_target:
local_target = self.state.temperatures.get("chamber_target", 0)
temps["chamber_heating"] = local_target > 0 and current < local_target
elif explicit_target is not None:
# Use explicit ctc.info.target - this is what slicer sees
temps["chamber_heating"] = explicit_target > 0 and current < explicit_target
else:
# Fallback to decoded target only if no explicit target available
if not respect_local_target and "chamber_target" not in temps:
temps["chamber_target"] = float(decoded_target)
temps["chamber_heating"] = decoded_target > 0 and current < decoded_target
else:
# Direct value (not encoded) - heater is OFF
temps["chamber"] = float(temp_val)
temps["chamber_heating"] = False
except Exception as e:
logger.warning(f"[{self.serial_number}] Error parsing H2D temperatures: {e}")
if temps:
# Handle chamber_target: prefer explicit over decoded
if "_chamber_decoded_target" in temps and "chamber_target" not in temps:
# No explicit target available, use decoded target from chamber_temper
temps["chamber_target"] = temps["_chamber_decoded_target"]
# Remove internal temp key before merging
temps.pop("_chamber_decoded_target", None)
# Merge new temps into existing, preserving valid values when new ones are filtered out
for key, value in temps.items():
self.state.temperatures[key] = value
# Calculate chamber_heating after all targets are known
# Priority: local target (if recent) > explicit target (chamber_target) > 0
if "chamber" in temps and "chamber_heating" not in temps:
current = self.state.temperatures.get("chamber", 0)
local_set_time = self.state.temperatures.get("_chamber_target_set_time", 0)
respect_local = (time.time() - local_set_time) < 5.0
if respect_local:
# Use locally-set target
target = self.state.temperatures.get("chamber_target", 0)
else:
# Use explicit/decoded target from MQTT
target = self.state.temperatures.get("chamber_target", 0)
self.state.temperatures["chamber_heating"] = target > 0 and current < target
logger.debug(f"[{self.serial_number}] Chamber heating calculated: target={target}, current={current}, heating={self.state.temperatures['chamber_heating']}, respect_local={respect_local}")
# Debug: log chamber value if it was updated
if "chamber" in temps:
logger.debug(f"[{self.serial_number}] Chamber temp updated to: {self.state.temperatures.get('chamber')}, target: {self.state.temperatures.get('chamber_target')}, heating: {self.state.temperatures.get('chamber_heating')}")
# Parse HMS (Health Management System) errors
if "hms" in data:
hms_list = data["hms"]
@@ -1815,7 +1926,16 @@ class BambuMQTTClient:
True if command was sent, False otherwise
"""
# M141 sets chamber temperature
return self.send_gcode(f"M141 S{target}")
result = self.send_gcode(f"M141 S{target}")
# Track chamber target locally (MQTT reports encoded values that need filtering)
if result:
self.state.temperatures["chamber_target"] = float(target)
self.state.temperatures["_chamber_target_set_time"] = time.time()
# Update heating state immediately based on new target
current_temp = self.state.temperatures.get("chamber", 0)
self.state.temperatures["chamber_heating"] = target > 0 and current_temp < target
logger.info(f"[{self.serial_number}] Tracking chamber target locally: {target}°C (heating={self.state.temperatures['chamber_heating']})")
return result
def set_print_speed(self, mode: int) -> bool:
"""Set the print speed mode.
@@ -203,14 +203,23 @@ export function TemperatureColumn({ printer, status, disabled = false }: Tempera
{renderTargetTemp('chamber', temps.chamber_target ?? 0)}
</div>
{/* Air Condition - full width button */}
{/* Air Condition - full width button with mode indicator */}
<button
onClick={() => setShowAirConditionModal(true)}
disabled={isDisabled}
className="flex items-center justify-center gap-2 w-full py-2 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center justify-between w-full px-3 py-4 rounded-md bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary disabled:opacity-50 disabled:cursor-not-allowed"
>
<img src="/icons/ventilation.svg" alt="" className="w-5 h-5 icon-theme" />
<span className="text-xs text-bambu-gray">Air Condition</span>
<div className="flex items-center gap-2">
<img src="/icons/ventilation.svg" alt="" className="w-5 h-5 icon-theme" />
<span className="text-xs text-bambu-gray">A/C</span>
</div>
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
status?.airduct_mode === 1
? 'bg-orange-500/20 text-orange-400'
: 'bg-blue-500/20 text-blue-400'
}`}>
{status?.airduct_mode === 1 ? 'Heat' : 'Cool'}
</span>
</button>
{/* Speed & Lamp - half width each */}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
<script type="module" crossorigin src="/assets/index-DzdPY90c.js"></script>
<script type="module" crossorigin src="/assets/index-BGXbFbC2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DyjgQh1m.css">
</head>
<body>