Przeglądaj źródła

Fix PA-CF/PA12-CF/PAHT-CF type matching and force color match click target (#688)

  Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable,
  but the print scheduler and filament override UI used exact string matching.
  A 3MF requiring PA-CF wouldn't match PA12-CF in the AMS, leaving the
  override dropdown disabled and preventing scheduler assignment.

  Add a filament type equivalence system (both backend and frontend) so
  these PA variants are treated as compatible in scheduler assignment,
  AMS slot matching, force color match validation, and the override dropdown.

  Also fix the "Force color match" checkbox click target being too wide
  (entire row instead of just the checkbox/label) by changing flex to
  inline-flex on the label element.
maziggy 2 miesięcy temu
rodzic
commit
5a4a2260b1

+ 2 - 0
CHANGELOG.md

@@ -33,6 +33,8 @@ All notable changes to Bambuddy will be documented in this file.
 - **Interface Aliases Not Shown in Virtual Printer Interface Select** — Interface aliases (e.g. `eth0:1`) added for multi-virtual-printer setups were invisible in the bind IP dropdown. The Docker image didn't include `iproute2`, so the `ip` command wasn't available and the code fell back to ioctl-based enumeration which can only return one IP per interface. Added `iproute2` to the Docker image.
 - **P2S Camera Stream Disconnects After a Few Seconds** ([#661](https://github.com/maziggy/bambuddy/issues/661)) — The P2S firmware drops RTSP sessions after a few seconds with an I/O error. The backend treated this as a fatal failure, ending the MJPEG stream and forcing the frontend through a full reconnection cycle (stop → start → brief connection → fail → repeat). Added transparent auto-reconnection: when ffmpeg's RTSP connection dies, it respawns immediately and continues streaming MJPEG frames to the browser without interruption. Reported by @ddetton, confirmed by @DMoenning.
 - **iOS/iPadOS Cannot Reposition Floating Camera** ([#687](https://github.com/maziggy/bambuddy/issues/687)) — The floating camera viewer (embedded camera window on the dashboard) could not be dragged or resized on iOS/iPadOS because it only handled mouse events. Touch input scrolled the page underneath instead of moving the camera window. Added touch event support (`touchstart`/`touchmove`/`touchend`) to both the header drag handle and the resize handle, with `preventDefault` to stop page scrolling during drag. Reported by @dsmitty166.
+- **PA-CF / PA12-CF / PAHT-CF Not Treated as Compatible** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable, but the print scheduler and filament override UI used exact string matching. If a 3MF required PA-CF but the AMS had PA12-CF loaded, the scheduler wouldn't assign the job and the filament override dropdown was empty/disabled. Added a filament type equivalence system so these PA variants are treated as compatible in scheduler assignment, AMS slot matching, force color match validation, and the filament override dropdown. Reported by @aneopsy.
+- **Force Color Match Toggle Click Target Too Large** ([#688](https://github.com/maziggy/bambuddy/issues/688)) — In the Schedule Print modal, clicking anywhere on the "Force color match" row toggled the checkbox, not just the checkbox and its label. The click target now covers only the checkbox, icon, and label text. Reported by @aneopsy.
 - **Debug Logging Endpoint 500 Error** — The `GET /api/v1/support/debug-logging` endpoint returned a 500 Internal Server Error when the database contained a timezone-aware timestamp written by a previous version. The duration calculation subtracted a timezone-aware datetime from a naive `datetime.now()`, raising `TypeError`. Now strips timezone info when reading the stored timestamp.
 - **Bed Cooled Notification Never Fires** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — The bed cooldown monitor always timed out after 30 minutes without sending a notification. After print completion, P1S (and likely other models) sends partial MQTT status updates that don't include `bed_temper`, so the cached bed temperature stayed frozen at the end-of-print value and never dropped below the threshold. The monitor now sends periodic `pushall` commands to the printer to force fresh temperature data. Also added debug logging to the polling loop for future diagnostics.
 - **Notification Provider Missing Event Toggles on Create** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — When creating a new notification provider, the `on_bed_cooled` toggle and all 7 queue event toggles (`on_queue_job_added`, `on_queue_job_assigned`, `on_queue_job_started`, `on_queue_job_waiting`, `on_queue_job_skipped`, `on_queue_job_failed`, `on_queue_completed`) were silently discarded. The create endpoint manually listed each field but omitted these 8 toggles, so they always defaulted to `false` regardless of user selection. Editing an existing provider worked correctly.

+ 26 - 8
backend/app/services/print_scheduler.py

@@ -27,6 +27,23 @@ from backend.app.utils.threemf_tools import extract_nozzle_mapping_from_3mf
 
 logger = logging.getLogger(__name__)
 
+# Filament type equivalence groups — types within the same group are
+# interchangeable on the printer side (Bambu Lab firmware treats them as compatible).
+_FILAMENT_TYPE_GROUPS: list[list[str]] = [
+    ["PA-CF", "PA12-CF", "PAHT-CF"],
+]
+_FILAMENT_EQUIV_MAP: dict[str, str] = {}
+for _group in _FILAMENT_TYPE_GROUPS:
+    _canonical = _group[0].upper()
+    for _t in _group:
+        _FILAMENT_EQUIV_MAP[_t.upper()] = _canonical
+
+
+def _canonical_filament_type(ftype: str) -> str:
+    """Return canonical type for equivalence matching."""
+    upper = ftype.upper()
+    return _FILAMENT_EQUIV_MAP.get(upper, upper)
+
 
 class PrintScheduler:
     """Background scheduler that processes the print queue."""
@@ -484,16 +501,16 @@ class PrintScheduler:
                 tray_color = tray.get("tray_color", "")
                 if tray_type:
                     color_norm = tray_color.replace("#", "").lower()[:6]
-                    loaded.add((tray_type.upper(), color_norm))
+                    loaded.add((_canonical_filament_type(tray_type), color_norm))
         for vt in status.raw_data.get("vt_tray") or []:
             vt_type = vt.get("tray_type")
             if vt_type:
                 color_norm = (vt.get("tray_color", "") or "").replace("#", "").lower()[:6]
-                loaded.add((vt_type.upper(), color_norm))
+                loaded.add((_canonical_filament_type(vt_type), color_norm))
 
         missing = []
         for o in force_overrides:
-            o_type = (o.get("type") or "").upper()
+            o_type = _canonical_filament_type(o.get("type") or "")
             o_color = (o.get("color") or "").replace("#", "").lower()[:6]
             if (o_type, o_color) not in loaded:
                 color_label = o.get("color_name") or o.get("color", "?")
@@ -515,6 +532,7 @@ class PrintScheduler:
             return required_types  # Can't determine, assume all missing
 
         # Collect all filament types loaded on this printer (AMS units + external spool)
+        # Use canonical types so equivalence groups (e.g. PA-CF/PA12-CF/PAHT-CF) match.
         loaded_types: set[str] = set()
 
         # Check AMS units (stored in raw_data["ams"])
@@ -524,18 +542,18 @@ class PrintScheduler:
                 for tray in ams_unit.get("tray", []):
                     tray_type = tray.get("tray_type")
                     if tray_type:
-                        loaded_types.add(tray_type.upper())
+                        loaded_types.add(_canonical_filament_type(tray_type))
 
         # Check external spool(s) (virtual tray, stored in raw_data["vt_tray"] as list)
         for vt in status.raw_data.get("vt_tray") or []:
             vt_type = vt.get("tray_type")
             if vt_type:
-                loaded_types.add(vt_type.upper())
+                loaded_types.add(_canonical_filament_type(vt_type))
 
-        # Find which required types are missing (case-insensitive comparison)
+        # Find which required types are missing (using canonical type for equivalence)
         missing = []
         for req_type in required_types:
-            if req_type.upper() not in loaded_types:
+            if _canonical_filament_type(req_type) not in loaded_types:
                 missing.append(req_type)
 
         return missing
@@ -915,7 +933,7 @@ class PrintScheduler:
             if not idx_match and not exact_match and not similar_match and not type_only_match:
                 for f in available:
                     f_type = (f.get("type") or "").upper()
-                    if f_type != req_type:
+                    if _canonical_filament_type(f_type) != _canonical_filament_type(req_type):
                         continue
 
                     # Type matches - check color

+ 6 - 4
frontend/src/components/PrintModal/FilamentOverride.tsx

@@ -2,6 +2,7 @@ import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Circle, RotateCcw, Palette } from 'lucide-react';
 import { getColorName } from '../../utils/colors';
+import { canonicalFilamentType } from '../../utils/amsHelpers';
 import type { FilamentReqsData } from './types';
 
 interface FilamentOverrideProps {
@@ -31,11 +32,12 @@ export function FilamentOverride({
 }: FilamentOverrideProps) {
   const { t } = useTranslation();
 
-  // Index available filaments by type (uppercased) for per-slot filtering
+  // Index available filaments by canonical type for per-slot filtering.
+  // Types in the same equivalence group (e.g. PA-CF / PA12-CF / PAHT-CF) share one bucket.
   const filamentsByType = useMemo(() => {
     const map: Record<string, Array<{ type: string; color: string; tray_info_idx: string; tray_sub_brands: string; extruder_id: number | null }>> = {};
     for (const f of availableFilaments) {
-      const key = f.type.toUpperCase();
+      const key = canonicalFilamentType(f.type);
       if (!map[key]) map[key] = [];
       map[key].push(f);
     }
@@ -71,7 +73,7 @@ export function FilamentOverride({
           const override = overrides[req.slot_id];
           const isOverridden = !!override;
           // Only show filaments of the same type AND compatible nozzle/extruder
-          const sameType = filamentsByType[req.type.toUpperCase()] || [];
+          const sameType = filamentsByType[canonicalFilamentType(req.type)] || [];
           // On dual-nozzle printers (H2D), filter to filaments on the correct extruder.
           // nozzle_id from 3MF maps to extruder_id from AMS. If nozzle_id is undefined
           // (single-nozzle) or extruder_id is null, no nozzle filtering is needed.
@@ -134,7 +136,7 @@ export function FilamentOverride({
                 )}
               </div>
               {/* Force Color Match checkbox — shown below each filament row */}
-              <label className="flex items-center gap-1.5 text-xs text-bambu-gray cursor-pointer select-none pl-5">
+              <label className="inline-flex items-center gap-1.5 text-xs text-bambu-gray cursor-pointer select-none pl-5">
                 <input
                   type="checkbox"
                   checked={forceColorMatch?.[req.slot_id] ?? false}

+ 37 - 3
frontend/src/utils/amsHelpers.ts

@@ -25,6 +25,40 @@ export function normalizeColorForCompare(color: string | undefined): string {
   return color.replace('#', '').toLowerCase().substring(0, 6);
 }
 
+/**
+ * Filament type equivalence groups.
+ * Types within the same group are interchangeable on the printer side
+ * (e.g., Bambu Lab firmware treats PA-CF and PA12-CF as compatible).
+ */
+const FILAMENT_TYPE_GROUPS: string[][] = [
+  ['PA-CF', 'PA12-CF', 'PAHT-CF'],
+];
+
+const _equivalenceMap: Record<string, string> = {};
+for (const group of FILAMENT_TYPE_GROUPS) {
+  const canonical = group[0];
+  for (const t of group) {
+    _equivalenceMap[t.toUpperCase()] = canonical.toUpperCase();
+  }
+}
+
+/**
+ * Get the canonical filament type for equivalence matching.
+ * Types in the same group (e.g., PA-CF / PA12-CF / PAHT-CF) return the same canonical type.
+ */
+export function canonicalFilamentType(type: string | undefined): string {
+  if (!type) return '';
+  const upper = type.toUpperCase();
+  return _equivalenceMap[upper] ?? upper;
+}
+
+/**
+ * Check if two filament types are compatible (same type or same equivalence group).
+ */
+export function filamentTypesCompatible(a: string | undefined, b: string | undefined): boolean {
+  return canonicalFilamentType(a) === canonicalFilamentType(b);
+}
+
 /**
  * Check if two colors are visually similar within a threshold.
  * Uses RGB component comparison with configurable tolerance.
@@ -129,7 +163,7 @@ export function autoMatchFilament(
   const exactMatch = nozzleFilaments.find(
     (f) =>
       !usedTrayIds.has(f.globalTrayId) &&
-      f.type?.toUpperCase() === req.type?.toUpperCase() &&
+      filamentTypesCompatible(f.type, req.type) &&
       normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
   );
   const similarMatch = exactMatch
@@ -137,14 +171,14 @@ export function autoMatchFilament(
     : nozzleFilaments.find(
         (f) =>
           !usedTrayIds.has(f.globalTrayId) &&
-          f.type?.toUpperCase() === req.type?.toUpperCase() &&
+          filamentTypesCompatible(f.type, req.type) &&
           colorsAreSimilar(f.color, req.color)
       );
   const typeOnlyMatch =
     exactMatch || similarMatch
       ? undefined
       : nozzleFilaments.find(
-          (f) => !usedTrayIds.has(f.globalTrayId) && f.type?.toUpperCase() === req.type?.toUpperCase()
+          (f) => !usedTrayIds.has(f.globalTrayId) && filamentTypesCompatible(f.type, req.type)
         );
   return exactMatch ?? similarMatch ?? typeOnlyMatch;
 }

Plik diff jest za duży
+ 0 - 0
static/assets/index-Bl2AZAaE.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BwyddZpl.js"></script>
+    <script type="module" crossorigin src="/assets/index-Bl2AZAaE.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index--YKaUCwD.css">
   </head>
   <body>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików