| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- /**
- * AMS (Automatic Material System) helper utilities for Bambu Lab printers.
- * These functions handle color normalization, slot labeling, and tray ID calculations
- * for AMS, AMS-HT, and external spool configurations.
- */
- import { parseUTCDate } from './date';
- /**
- * Normalize color format from various sources.
- * API returns "RRGGBBAA" (8-char), 3MF uses "#RRGGBB" (7-char with hash).
- * This normalizes to "#RRGGBB" format.
- */
- export function normalizeColor(color: string | null | undefined): string {
- if (!color) return '#808080';
- // Remove alpha channel if present (8-char hex to 6-char)
- const hex = color.replace('#', '').substring(0, 6);
- return `#${hex}`;
- }
- /**
- * Normalize color for comparison (case-insensitive, strip hash and alpha).
- */
- export function normalizeColorForCompare(color: string | undefined): string {
- if (!color) return '';
- 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.
- * @param color1 - First hex color
- * @param color2 - Second hex color
- * @param threshold - Maximum difference per RGB component (default: 40)
- */
- export function colorsAreSimilar(
- color1: string | undefined,
- color2: string | undefined,
- threshold = 40
- ): boolean {
- const hex1 = normalizeColorForCompare(color1);
- const hex2 = normalizeColorForCompare(color2);
- if (!hex1 || !hex2 || hex1.length < 6 || hex2.length < 6) return false;
- const r1 = parseInt(hex1.substring(0, 2), 16);
- const g1 = parseInt(hex1.substring(2, 4), 16);
- const b1 = parseInt(hex1.substring(4, 6), 16);
- const r2 = parseInt(hex2.substring(0, 2), 16);
- const g2 = parseInt(hex2.substring(2, 4), 16);
- const b2 = parseInt(hex2.substring(4, 6), 16);
- return (
- Math.abs(r1 - r2) <= threshold &&
- Math.abs(g1 - g2) <= threshold &&
- Math.abs(b1 - b2) <= threshold
- );
- }
- /**
- * Format slot label for display in the UI.
- * @param amsId - AMS unit ID (0-3 for regular AMS, 128+ for AMS-HT)
- * @param trayId - Tray/slot ID within the AMS unit (0-3)
- * @param isHt - Whether this is an AMS-HT unit (single tray)
- * @param isExternal - Whether this is the external spool holder
- */
- export function formatSlotLabel(
- amsId: number,
- trayId: number,
- isHt: boolean,
- isExternal: boolean
- ): string {
- if (isExternal) return 'Ext';
- // Convert AMS ID to letter (A, B, C, D)
- // AMS-HT uses IDs starting at 128
- const letter = String.fromCharCode(65 + (amsId >= 128 ? amsId - 128 : amsId));
- if (isHt) return `HT-${letter}`;
- return `${letter}${trayId + 1}`;
- }
- /**
- * Calculate global tray ID for MQTT command.
- * Used in the ams_mapping array sent to the printer.
- * @param amsId - AMS unit ID (0-3 for regular AMS, 128+ for AMS-HT)
- * @param trayId - Tray/slot ID within the AMS unit
- * @param isExternal - Whether this is the external spool holder
- * @returns Global tray ID (0-15 for AMS, 128+ for AMS-HT, 254 for external)
- */
- export function getGlobalTrayId(
- amsId: number,
- trayId: number,
- isExternal: boolean
- ): number {
- if (isExternal) return 254 + trayId;
- // AMS-HT units have IDs starting at 128 with a single tray — use ID directly
- if (amsId >= 128) return amsId;
- return amsId * 4 + trayId;
- }
- /**
- * Get fill bar color based on spool fill level.
- * Matches PrintersPage thresholds and Bambu Lab brand green.
- */
- export function getFillBarColor(fillLevel: number): string {
- if (fillLevel > 50) return '#00ae42'; // Green - good
- if (fillLevel >= 15) return '#f59e0b'; // Amber - warning (<= 50%)
- return '#ef4444'; // Red - critical (< 15%)
- }
- /**
- * Calculate fill level from Spoolman weight data.
- * Used as the first source in the Spoolman → Inventory → AMS fill chain.
- */
- export function getSpoolmanFillLevel(
- linkedSpool: { remaining_weight: number | null; filament_weight: number | null } | undefined
- ): number | null {
- if (!linkedSpool?.remaining_weight || !linkedSpool?.filament_weight
- || linkedSpool.filament_weight <= 0) return null;
- return Math.min(100, Math.round(
- (linkedSpool.remaining_weight / linkedSpool.filament_weight) * 100
- ));
- }
- function toFixedHex(value: number, width: number): string {
- const safe = Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
- return safe.toString(16).toUpperCase().padStart(width, '0').slice(-width);
- }
- // 32-bit FNV-1a hash -> 8-char hex (stable for alphanumeric serials)
- function hashSerialToHex32(serial: string): string {
- const input = (serial || '').trim().toUpperCase();
- let hash = 0x811c9dc5;
- for (let i = 0; i < input.length; i++) {
- hash ^= input.charCodeAt(i);
- hash = Math.imul(hash, 0x01000193);
- }
- return (hash >>> 0).toString(16).toUpperCase().padStart(8, '0');
- }
- /**
- * Generate a stable fallback spool tag for slots without RFID identifiers.
- * Returns a 16-char hex string derived from the printer serial + slot position.
- */
- export function getFallbackSpoolTag(printerSerial: string, amsId: number, trayId: number): string {
- return `${hashSerialToHex32(printerSerial)}${toFixedHex(amsId, 4)}${toFixedHex(trayId, 4)}`;
- }
- /**
- * Get minimum datetime for scheduling (now + 1 minute).
- * Returns ISO string format for datetime-local input.
- */
- export function getMinDateTime(): string {
- const now = new Date();
- now.setMinutes(now.getMinutes() + 1);
- return now.toISOString().slice(0, 16);
- }
- /**
- * Check if a scheduled time is a placeholder far-future date.
- * Placeholder dates (more than 6 months out) are treated as ASAP.
- */
- export function isPlaceholderDate(scheduledTime: string | null | undefined): boolean {
- if (!scheduledTime) return false;
- const sixMonthsFromNow = Date.now() + 180 * 24 * 60 * 60 * 1000;
- return (parseUTCDate(scheduledTime)?.getTime() ?? 0) > sixMonthsFromNow;
- }
- /**
- * Auto-match a filament requirement to a loaded filament, respecting nozzle constraints.
- * Used by both single-printer (FilamentMapping) and multi-printer (InlineMappingEditor) paths.
- */
- export function autoMatchFilament(
- req: { type?: string; color?: string; nozzle_id?: number | null },
- loadedFilaments: { globalTrayId: number; type?: string; color?: string; extruderId?: number }[],
- usedTrayIds: Set<number>,
- ): typeof loadedFilaments[number] | undefined {
- const nozzleFilaments = filterFilamentsByNozzle(loadedFilaments, req.nozzle_id);
- const exactMatch = nozzleFilaments.find(
- (f) =>
- !usedTrayIds.has(f.globalTrayId) &&
- filamentTypesCompatible(f.type, req.type) &&
- normalizeColorForCompare(f.color) === normalizeColorForCompare(req.color)
- );
- const similarMatch = exactMatch
- ? undefined
- : nozzleFilaments.find(
- (f) =>
- !usedTrayIds.has(f.globalTrayId) &&
- filamentTypesCompatible(f.type, req.type) &&
- colorsAreSimilar(f.color, req.color)
- );
- const typeOnlyMatch =
- exactMatch || similarMatch
- ? undefined
- : nozzleFilaments.find(
- (f) => !usedTrayIds.has(f.globalTrayId) && filamentTypesCompatible(f.type, req.type)
- );
- return exactMatch ?? similarMatch ?? typeOnlyMatch;
- }
- /**
- * Filter loaded filaments to those valid for a given nozzle requirement.
- * For single-nozzle printers (nozzle_id is null/undefined), returns all filaments.
- */
- export function filterFilamentsByNozzle<T extends { extruderId?: number }>(
- loadedFilaments: T[],
- nozzleId: number | undefined | null,
- ): T[] {
- return loadedFilaments.filter(
- (f) => nozzleId == null || f.extruderId === nozzleId
- );
- }
|