mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
feat(scheduler): preheat & heat-soak before queued prints with per-filament chamber targets + airduct flap control (#1468)
New scheduler stage that heats the bed (and the chamber, on supported
printers) and holds at temperature before each queued print starts —
the heat-soak engineering filaments need for adhesion and warp
control. Bambuddy waits between FTP upload and start_print, so the
soak runs while the printer is otherwise idle. M191 is silently
ignored by Bambu firmware, so doing this at the orchestration layer
is the only place it works.
Resolution order at dispatch:
1. PrintQueueItem.preheat_override ∈ {inherit, on, off}.
'off' skips entirely; 'inherit' falls back to the global
preheat_enabled toggle; 'on' forces the stage even when the
global is off.
2. chamber_target = item.preheat_chamber_target_override
?? max(filament_map[normalize(t.tray_type)] for loaded slots)
?? 0.
Mixed PA+PLA picks PA's 50 (max-across-slots — PA's chamber
requirement is binding, PLA doesn't suffer being warm). PLA-only
derives 0 and skips the chamber phase automatically.
3. Three hardware tiers for chamber heat:
- Active chamber heater (H2C/H2D/H2D Pro/H2S/X2D/X1E) → M141 +
chamber-sensor wait
- Chamber sensor only (X1C/P2S) → no M141, passive bed-radiation
wait with hard max-wait cap
- No chamber sensor (P1S/P1P/A1/A1 Mini) → bed + soak timer only
4. Airduct flap (H2C/H2D/H2D Pro/H2S/X2D/P2S) auto-switches to
match the chamber target — heating mode for engineering
filaments, cooling mode for PLA. Bambu firmware does NOT
auto-switch the flap with M141, so without this an ABS print
on a previously-cooling flap fights the open exhaust, and a
PLA print on a previously-hot flap recirculates ABS heat.
Idempotent: only fires set_airduct_mode when current ≠ desired.
Settings → Workflow → Queue & Dispatch → Preheat & Heat Soak card:
master enable toggle (default off — disabled installs see no change),
per-filament chamber-target editor (replaces a single global int that
shipped in the first cut and couldn't serve PA + PLA in the same
config), preheat_max_wait_seconds, preheat_soak_seconds. The Print
Options panel in PrintModal gets a Preheat sub-section with the
tri-state Inherit/On/Off control and an optional chamber-target
override input.
DB migration: PrintQueueItem gains preheat_override VARCHAR(10)
DEFAULT 'inherit' and preheat_chamber_target_override INTEGER NULL.
Idempotent via _safe_execute. Existing rows behave exactly as before
the migration.
Best-effort throughout: printer drops, refused M141 or set_airduct,
missing bed temp, lost MQTT state mid-wait all log and return cleanly.
Normal upload + start path runs after this returns regardless.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// Shared frontend helpers for the per-filament chamber-target map (#1468).
|
||||
// Mirrors backend `PrintScheduler.DEFAULT_PREHEAT_FILAMENT_TARGETS`. Values
|
||||
// are chamber-temperature recommendations from BambuStudio's bundled filament
|
||||
// profiles; users can override the whole map via the Settings → Workflow card.
|
||||
|
||||
export const DEFAULT_PREHEAT_FILAMENT_TARGETS: Record<string, number> = {
|
||||
PLA: 0,
|
||||
PETG: 0,
|
||||
'PETG-CF': 40,
|
||||
ABS: 45,
|
||||
ASA: 45,
|
||||
PA: 50,
|
||||
'PA-CF': 55,
|
||||
PC: 50,
|
||||
'PC-FR': 50,
|
||||
TPU: 0,
|
||||
PVA: 0,
|
||||
default: 0,
|
||||
};
|
||||
|
||||
// Stable display order for the editor and any preview UI. Matches the order
|
||||
// users will most likely tune (engineering filaments first, then commodity).
|
||||
export const PREHEAT_FILAMENT_ORDER: readonly string[] = [
|
||||
'PA-CF',
|
||||
'PA',
|
||||
'PC',
|
||||
'PC-FR',
|
||||
'ABS',
|
||||
'ASA',
|
||||
'PETG-CF',
|
||||
'PETG',
|
||||
'PLA',
|
||||
'TPU',
|
||||
'PVA',
|
||||
'default',
|
||||
];
|
||||
|
||||
export function parsePreheatFilamentTargets(raw: string): Record<string, number> {
|
||||
if (!raw) return { ...DEFAULT_PREHEAT_FILAMENT_TARGETS };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const out: Record<string, number> = {};
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
const num = typeof value === 'number' ? value : Number(value);
|
||||
if (Number.isFinite(num)) {
|
||||
out[key] = Math.max(0, Math.min(60, Math.round(num)));
|
||||
}
|
||||
}
|
||||
if (out.default === undefined) out.default = DEFAULT_PREHEAT_FILAMENT_TARGETS.default;
|
||||
return out;
|
||||
}
|
||||
} catch {
|
||||
// fall through to defaults
|
||||
}
|
||||
return { ...DEFAULT_PREHEAT_FILAMENT_TARGETS };
|
||||
}
|
||||
|
||||
export function serializePreheatFilamentTargets(map: Record<string, number>): string {
|
||||
// Re-encode in the canonical display order so the on-disk JSON stays
|
||||
// diff-stable. Strip 0 entries that match the default to keep the payload
|
||||
// small — except `default` itself, which we always keep for predictability.
|
||||
const out: Record<string, number> = {};
|
||||
for (const key of PREHEAT_FILAMENT_ORDER) {
|
||||
if (map[key] !== undefined) out[key] = map[key];
|
||||
}
|
||||
// Preserve any custom keys the user added that aren't in our canonical order.
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
if (!(key in out)) out[key] = value;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
|
||||
// Normalize a printer-reported tray_type to a map key. Mirrors the backend
|
||||
// `_normalize_filament_type` — split on space, uppercase.
|
||||
export function normalizePreheatFilamentType(trayType: string): string {
|
||||
if (!trayType) return '';
|
||||
return trayType.split(/\s+/)[0].toUpperCase();
|
||||
}
|
||||
|
||||
// Pick the max chamber target across a list of loaded tray types, falling
|
||||
// back to `default` when a type isn't in the map. Returns 0 when nothing
|
||||
// is loaded, which short-circuits the chamber phase at dispatch.
|
||||
export function deriveChamberTargetForTrays(
|
||||
trayTypes: readonly string[],
|
||||
map: Record<string, number>,
|
||||
): number {
|
||||
let best = 0;
|
||||
for (const raw of trayTypes) {
|
||||
const normalized = normalizePreheatFilamentType(raw);
|
||||
if (!normalized) continue;
|
||||
const target = map[normalized] ?? map.default ?? 0;
|
||||
if (target > best) best = target;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
Reference in New Issue
Block a user