printer.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. export function getPrinterImage(model: string | null | undefined): string {
  2. if (!model) return '/img/printers/default.png';
  3. const m = model.toLowerCase().replace(/\s+/g, '');
  4. if (m.includes('x1e')) return '/img/printers/x1e.png';
  5. if (m.includes('x1c') || m.includes('x1carbon')) return '/img/printers/x1c.png';
  6. if (m.includes('x1')) return '/img/printers/x1c.png';
  7. if (m.includes('x2d') || m === 'n6') return '/img/printers/x2d.png';
  8. if (m.includes('h2dpro') || m.includes('h2d-pro')) return '/img/printers/h2dpro.png';
  9. if (m.includes('h2d')) return '/img/printers/h2d.png';
  10. if (m.includes('h2c')) return '/img/printers/h2c.png';
  11. if (m.includes('h2s')) return '/img/printers/h2d.png';
  12. if (m.includes('p2s')) return '/img/printers/p1s.png';
  13. if (m.includes('p1s')) return '/img/printers/p1s.png';
  14. if (m.includes('p1p')) return '/img/printers/p1p.png';
  15. if (m.includes('a2l') || m === 'n9') return '/img/printers/a2l.png';
  16. if (m.includes('a1mini')) return '/img/printers/a1mini.png';
  17. if (m.includes('a1')) return '/img/printers/a1.png';
  18. return '/img/printers/default.png';
  19. }
  20. // Ceiling for every chamber-temperature target the UI accepts (manual set,
  21. // preheat filament map, per-item preheat override, chamber quick-select
  22. // presets). Mirrors backend MAX_CHAMBER_TEMP_C in
  23. // backend/app/utils/printer_models.py — keep the two in sync. The H2 series
  24. // (H2C / H2D / H2D Pro / H2S) and X2D heat the chamber to 65 °C; X1E tops out
  25. // at 60 and its firmware clamps anything higher.
  26. export const MAX_CHAMBER_TEMP_C = 65;
  27. // G-code interchange families (#2578). Mirrors backend GCODE_COMPAT_FAMILIES
  28. // in backend/app/utils/printer_models.py — keep the two in sync. A sliced 3MF
  29. // may target a different model ONLY within its family; everything else is
  30. // exact-match only.
  31. const GCODE_COMPAT_FAMILIES: ReadonlyArray<ReadonlySet<string>> = [
  32. new Set(['X1', 'X1C', 'X1E', 'P1P', 'P1S']),
  33. ];
  34. /** True when G-code sliced for one model may be dispatched to the other.
  35. * Unknown/missing metadata on either side returns true (can't validate). */
  36. export function isGcodeCompatible(
  37. slicedForModel: string | null | undefined,
  38. targetModel: string | null | undefined,
  39. ): boolean {
  40. if (!slicedForModel || !targetModel) return true;
  41. const norm = (m: string) => m.trim().toUpperCase().replace(/[\s-]/g, '');
  42. const a = norm(slicedForModel);
  43. const b = norm(targetModel);
  44. if (a === b) return true;
  45. return GCODE_COMPAT_FAMILIES.some((family) => family.has(a) && family.has(b));
  46. }
  47. export function getWifiStrength(rssi: number): { labelKey: string; color: string; bars: number } {
  48. if (rssi >= -50) return { labelKey: 'printers.wifiSignal.excellent', color: 'text-bambu-green', bars: 4 };
  49. if (rssi >= -60) return { labelKey: 'printers.wifiSignal.good', color: 'text-bambu-green', bars: 3 };
  50. if (rssi >= -70) return { labelKey: 'printers.wifiSignal.fair', color: 'text-yellow-400', bars: 2 };
  51. if (rssi >= -80) return { labelKey: 'printers.wifiSignal.weak', color: 'text-orange-400', bars: 1 };
  52. return { labelKey: 'printers.wifiSignal.veryWeak', color: 'text-red-400', bars: 1 };
  53. }
  54. import type { PrinterStatus, PrintQueueItem } from '../api/client';
  55. /**
  56. * True when a queue item aimed at this printer would start now rather than wait.
  57. *
  58. * Every print Bambuddy sends goes through the queue, so this is not "can we
  59. * print at all" — it is "will ASAP mean now". The PrintModal uses it to promise
  60. * a later start, and the printer card uses it to say whether a dropped file
  61. * prints or queues. Both must agree, or the card promises one thing and the
  62. * modal immediately says another.
  63. */
  64. export function isPrinterCurrentlyDispatchable(status: PrinterStatus | undefined): boolean {
  65. if (!status?.connected) return false;
  66. if (status.awaiting_plate_clear) return false;
  67. if (status.ams?.some((ams) => ams.dry_time > 0)) return false;
  68. return ['IDLE', 'FINISH', 'FAILED'].includes(status.state ?? '');
  69. }
  70. /**
  71. * Filters queue items based on printer compatibility (filament types and colors).
  72. * Mirrors backend _find_idle_printer_for_model() logic.
  73. * @param items - Array of queue items to filter
  74. * @param loadedFilamentTypes - Set of loaded filament types (e.g., "PLA", "PETG")
  75. * @param loadedFilaments - Set of loaded filament type+color pairs (e.g., "PLA:ffffff", "PETG:ff0000")
  76. * @param loadedVariants - Set of loaded type+color+tray_info_idx triples
  77. * (e.g., "PLA:ffffff:GFA01"; the idx is "" for custom/third-party spools). Used to
  78. * distinguish Bambu PLA sub-variants (Basic GFA00 / Matte GFA01 / Silk GFA06) that
  79. * share a base type+colour, mirroring the backend _get_missing_force_color_slots (#2650).
  80. * When omitted, force matching falls back to type+colour so the hint is never stricter
  81. * than the data available.
  82. * @returns Array of compatible queue items
  83. */
  84. export function filterCompatibleQueueItems(
  85. items: PrintQueueItem[],
  86. loadedFilamentTypes?: Set<string>,
  87. loadedFilaments?: Set<string>,
  88. loadedVariants?: Set<string>
  89. ): PrintQueueItem[] {
  90. return items.filter(item => {
  91. // A job bound to one printer has no printer left to choose, and the backend
  92. // never gates it on filament: the scheduler maps its trays at dispatch. Such
  93. // a job can carry overrides — one moved from "Any P2S" to a specific P2S
  94. // keeps its colour (#3133) — and filtering on them would hide a job that is
  95. // going to run from the card of the printer it is going to run on.
  96. if (item.printer_id != null) return true;
  97. // Type check: all required filament types must be loaded
  98. if (item.required_filament_types && item.required_filament_types.length > 0 && loadedFilamentTypes !== undefined) {
  99. if (!item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()))) {
  100. return false;
  101. }
  102. }
  103. // Color check: evaluate force_color_match per slot
  104. // Only apply when loadedFilaments is provided (not undefined).
  105. // An empty Set means no filaments are loaded — force-matched slots cannot match.
  106. if (item.filament_overrides && item.filament_overrides.length > 0 && loadedFilaments !== undefined) {
  107. const forceOverrides = item.filament_overrides.filter(o => o.force_color_match === true);
  108. const prefOverrides = item.filament_overrides.filter(o => o.force_color_match !== true);
  109. // All force-matched slots must have an exact type+color match — and, when the
  110. // override carries a tray_info_idx, the same variant too (a loaded tray with a
  111. // blank idx still satisfies it, matching the backend's type+colour fallback).
  112. if (forceOverrides.length > 0) {
  113. const allForceMatch = forceOverrides.every(o => {
  114. const oType = (o.type || '').toUpperCase();
  115. const oColor = (o.color || '').replace('#', '').toLowerCase().slice(0, 6);
  116. const oIdx = o.tray_info_idx || '';
  117. // No variant on the override, or no variant data supplied → type+colour only.
  118. if (!oIdx || loadedVariants === undefined) {
  119. return loadedFilaments.has(`${oType}:${oColor}`);
  120. }
  121. // Variant-specific: same idx, or a same-colour tray that reports no idx.
  122. return loadedVariants.has(`${oType}:${oColor}:${oIdx}`) || loadedVariants.has(`${oType}:${oColor}:`);
  123. });
  124. if (!allForceMatch) return false;
  125. }
  126. // Preference-only overrides: at least one color must match (existing behaviour)
  127. if (prefOverrides.length > 0 && forceOverrides.length === 0) {
  128. const hasColorMatch = prefOverrides.some(o => {
  129. const oType = (o.type || '').toUpperCase();
  130. const oColor = (o.color || '').replace('#', '').toLowerCase().slice(0, 6);
  131. return loadedFilaments.has(`${oType}:${oColor}`);
  132. });
  133. if (!hasColorMatch) return false;
  134. }
  135. }
  136. return true;
  137. });
  138. }