A 3-gram PLA keychain took ~6 minutes longer than it should have, and the same root cause had already destroyed a nozzle assembly. _derive_chamber_target() in bambuddy’s print scheduler takes the max chamber target over every loaded AMS tray and never looks at the print’s own ams_mapping — so one PC spool parked in AMS slot 1 silently forced a 50 °C chamber and a 90 °C bed onto every PLA print. This note is the bug, the three wrong trails it sent us down, the nozzle-clog RCA it explains, the counter-intuitive fix, the material-sequencing rules bambuddy does not enforce, and a shelved Tapo P115 energy bridge. Builds directly on 2026-09-01-bambuddy-slicer-api-pipelines-mcp on telep-mainframe.

For Agents — quick facts

  • Bug: _derive_chamber_target(printer, targets)/app/backend/app/services/print_scheduler.py line ~4042 — maxes over all loaded AMS trays, ignores ams_mapping
  • Blast radius: PLA prints got chamber_target=50 → no bed_temperature in archive metadata → bed fell back to queue_keep_warm_bed_temp = 90 °C
  • Cost: 57 s ramp + 300 s soak = ~6 min per print; FTP upload (the usual suspect) is 0.4 s
  • Fix applied: inverted the default — PC: 0 and PC-FR: 0 in preheat_filament_targets; PC-FR jobs must now pass preheat_chamber_target_override: 60
  • ⚠️ ABS and ASA are still 45 in the map — the identical trap returns if either is loaded alongside PLA
  • Nozzle assembly (cold end + hot end) was replaced ~2026-09-01 — this bug is half the reason why
  • bambuddy has NO material-transition logic. PC → PLA is a temperature DROP and is what carbonised the hot end
  • Tapo P115 bridge at /opt/tapo-bridge/ is built, enabled, NOT started, and entirely UNTESTED (no plug on the network)

1. THE BUG — _derive_chamber_target ignores ams_mapping

/app/backend/app/services/print_scheduler.py, line ~4042. The docstring states the defect outright:

“Look up the chamber target for each loaded AMS tray and return the max.”

Each loaded tray. Not each tray the print actually uses. The function never consults the print’s ams_mapping.

The AMS state that triggered it

AMS 0:  T0 (PLA)   T1 (PC)   T2 (PLA)   T3 (PLA)
print ams_mapping: [0, 3]     ← both PLA

The print used two PLA trays. _derive_chamber_target looked at all four, found PC’s 50 °C, and returned it.

The cascade, measured live from the scheduler log

23:20:17  archive has no bed_temperature metadata - heating the bed to 90C to drive the chamber to 50C
23:20:17  preheat starting - bed=90C chamber_target=50C (source=filament-map ...) max_wait=900s soak=300s
23:21:14  preheat target reached (bed=91.0 chamber=48.0) - entering soak
23:21:14  preheat soak - holding for 300s
23:26:14  preheat complete - proceeding to upload
23:26:15  FTP upload complete (2236048 bytes in 0.4s, 5438 KB/s)
23:26:17  PRINT START detected

57 s ramp + 300 s soak ≈ 6 minutes of dead time on a 3-gram keychain. The FTP upload everyone instinctively blames is 0.4 seconds.

The second-order damage is worse than the delay

With no bed_temperature in the archive metadata and a non-zero chamber target, the code falls back to queue_keep_warm_bed_temp = 90 °C and uses the bed as a chamber heater.

So PLA was printing on a 90 °C bed, when the material guidance in 2026-09-01-bambuddy-slicer-api-pipelines-mcp calls for 35–45 °C.

Consequences of a 90 °C bed under PLA

  • Over-adhesion — parts fused to the plate, difficult release
  • Elephant’s foot — first layers squashed and flared
  • Long-term textured-PEI wear — repeated over-temperature cycling degrades the sheet
  • Heat-creep — see the clog RCA in §4; this is the cold-end half of it

2. THREE WRONG TRAILS — record these, they cost real time

awaiting_plate_clear=True in the log is a RED HERRING

The scheduler emits

Queue: printer %d not available - connected=%s, state=%s, awaiting_plate_clear=%s

as a context dump, not as a diagnosis. The actual gate is if require_plate_clear and is_awaiting_plate_clear(...) at lines ~1991 and ~3320 — and require_plate_clear was false, so that gate could not possibly fire. The log line looks like an explanation and is not one.

Wrong trail 2 — state=FINISH is not a blocker. Line 3328:

idle = state.state in ("IDLE", "FINISH", "FAILED")

FINISH is explicitly idle.

Wrong trail 3 — a stale printing queue row was not it either. busy_printers is seeded from

SELECT printer_id FROM print_queue WHERE status='printing'

and there were no such rows.

The lesson, stated generally

In this scheduler a “not available” line is a context dump, not a diagnosis. The authoritative signal was the preheat log lines. Read those first.


3. THE FIX — counter-intuitive, and one option deliberately rejected

Rejected option: pull the PC spool out of the AMS

The first instinct — remove the offending spool — is wrong, and is recorded here so it is not re-proposed.

The AMS is the driest storage available (measured 41 % RH with desiccant). PC-FR is strongly hygroscopic. Removing it from the AMS worsens the moisture half of the clog while fixing the temperature half. Net negative.

Applied fix: invert the default

A per-item preheat_chamber_target_override beats the filament map:

if explicit_target is not None and explicit_target > 0:
    chamber_target = int(explicit_target)   # per-item override WINS
elif explicit_target == 0:
    chamber_target = 0                      # explicit zero also wins
else:
    chamber_target = self._derive_chamber_target(printer, targets)

So: set PC: 0 and PC-FR: 0 in preheat_filament_targets, and pass the override on the rare PC-FR job.

This puts the correct behaviour on the common case (~5 PLA prints to 2 PC in the observed history) with zero operator action, and moves the one manual step onto the rare and deliberate PC-FR jobs.

New map:

{"PA-CF":55,"PA":50,"PC":0,"PC-FR":0,"ABS":45,"ASA":45,
 "PETG-CF":40,"PETG":0,"PLA":0,"TPU":0,"PVA":0,"default":0}

ABS and ASA are still 45 — the trap is not gone

The exact same failure returns the moment an ABS or ASA spool is loaded alongside PLA. Either zero those too and rely on overrides, or remember to check before a PLA batch.

Sub-brand gotcha — PC-FR resolves as plain PC

The PC-FR spool reports tray_type: "PC" with tray_sub_brands: "PC FR". _derive_chamber_target reads tray_type, so it resolved to PC’s 50 °C, not PC-FR’s 60 °C — undershooting the documented target by 10 °C even when the preheat was wanted.


4. NOZZLE CLOG RCA — two independent causes, stacked

The nozzle assembly (cold end AND hot end) was replaced ~2026-09-01. Both halves failed, for different reasons.

Cold end — PLA heat-creep

Caused directly by §1: the chamber force-heated to 50 °C and the bed to 90 °C on PLA prints. The warning was already written down in 2026-09-01-bambuddy-slicer-api-pipelines-mcp:

“The printer runs the chamber at the highest requested chamber temp, so mixing PLA with a high-chamber material makes the PLA heat-creep.”

The note anticipated the failure mode for co-printing. What was not anticipated is that merely having the spool loaded was enough — no co-print required.

Hot end — PC residue carbonising

The print log shows relentless PC ↔ PLA alternation. Sep 2 alone:

TimeMaterial
05:47PC
08:59 – 17:55PLA ×5
18:22 – 20:49PC

PC needs 260–280 °C; PLA runs ~220 °C. PC residue left in the melt zone will not melt at PLA temperature, so it bakes, carbonises and blocks.

Contributing — PC-FR cannot be dried in the AMS

PC-FR is strongly hygroscopic. Its tray metadata declares drying_temp: 80. The AMS 2 Pro maxes at 65 °C. It therefore literally cannot dry this filament — external drying is mandatory (see §6).


5. MATERIAL SEQUENCING RULES — the operational core

bambuddy has NO material-transition logic — none, at all

Grepping the scheduler for material_change|filament_change|purge|previous_filament|last_filament returns only printer-state transitions. There is no purge step, no transition guard, no awareness of what was printed before. The queue will chain any two materials back-to-back.

TransitionVerdictWhy
PLA → PCSAFETemperature goes up; PLA residue melts and purges at PC temps
PC → PCBESTNo transition at all; chamber still warm from the previous 90–110 °C bed. Batch these
PC → PLA🔴 DANGEROUSTemperature drops 280 → 220 °C; PC residue will not melt, it carbonises. This is what caused the clog

The rule, and how to enforce it

Batch by material. Never auto-chain a temperature DROP.

Enforcement mechanism: manual_start: true is a per-item field on a queue entry. Set it on the first PLA job following any PC job — the queue halts and waits for a human, and that pause is the purge window.


6. OPERATIONAL PROCEDURE — running a PC-FR print

Checklist

  1. Dry the filament externally at 80–100 °C. The AMS 2 Pro cannot do this (65 °C max) despite the tray metadata asking for 80.
  2. Close the chamber, glue the plate, ventilate (VOCs).
  3. Pass preheat_chamber_target_override: 60 when queueing. 🔴 Without it the preheat is skipped entirely — the log reads preheat skipped - archive has no bed_temperature metadata and no chamber target — and the print starts from a cold chamber. This is the deliberate cost of the §3 fix.
  4. Brim is NOT automatic. All four pipelines share 0.20mm Standard @BBL H2S; add it explicitly.
  5. Afterwards, purge or cold-pull at PC temps BEFORE any PLA/PETG job. See §5.
  6. Leave the spool IN the AMS. It stays dry there (41 % RH). Do not “fix” this by unloading it.

7. THREE UPSTREAM BUGS WORTH FILING

1. _derive_chamber_target should intersect with the print’s ams_mapping instead of maxing over all loaded trays. Repro: park a PC spool in any AMS slot, queue a pure-PLA print, watch it soak 5 minutes and heat the bed to 90 °C.

2. Pipelines cannot express preheat settings. print_queue has preheat_override and preheat_chamber_target_override; the slicer_pipelines table has neither. A pipeline run therefore cannot carry the override — so PC-FR run via its own pipeline gets NO chamber preheat at all. This is a real gap created by the §3 fix, not a theoretical one.

3. Zeroing a filament to dodge bug 1 silently disables keep-warm for it. From _apply_keep_warm’s docstring:

“Skips entirely for filaments that map to a 0 °C chamber target.”

Setting PC: 0 therefore also opts PC out of keep-warm. Accepted, but it is a hidden coupling between two unrelated features.


8. MAINTENANCE + CONFIG CHANGES MADE THIS SESSION

  • Clean Nozzle/Hotend counter reset. It read last_performed_at: null despite the nozzle having been replaced ~2026-09-01. The RCA from §4 was written into its history note.
  • Two new maintenance types created and assigned to printer 1, extending the 6 from 3. Maintenance tracker:
    • Cold End / Extruder Inspection — every 100 h
    • PC / PC-FR Dryness Check — every 14 days

    Why these two Nothing in the previous 6 tasks covered the cold end — which is half of what actually failed. And nothing tracked filament dryness, the contributing cause.

  • Local backups ENABLED — daily 03:00, 7-day retention. Previously off entirely, with 61 prints of history plus all pipeline / spool / maintenance data unbacked.
  • PLA and PC-FR pipeline descriptions rewritten to carry the purge procedure (§5) and the override requirement (§6 step 3).

9. CONFIG REVIEW — state as found

Good

  • Every AI detector on: spaghetti, first-layer, monitor, buildplate marker, nozzle clumping, pileup, airprint, auto_recovery_step_loss
  • Telegram alerts live and firing (provider from 4. Telegram notifications)
  • WiFi −33 dBm — excellent. Note the H2S is WiFi-only; the H2S has no Ethernet port (relevant to the cross-band isolation issue in 2026-08-26-router-reflash-mac-5ghz-dfs-and-bambu-offline — you cannot wire around it on this model)
  • AMS 40–41 % RH
  • 257 MB of 2.7 TB used

Gap — filament_tangle_detect is a dead capability

filament_tangle_detect: false, and it is not settable through bambuddy. Its REST endpoint rejects the name — the valid xcam modules are:

spaghetti_detector, first_layer_inspector, printing_monitor,
buildplate_marker_detector, allow_skip_parts, pileup_detector,
clump_detector, airprint_detector, auto_recovery_step_loss

bambuddy’s MQTT client has _set_print_option documented as handling filament_tangle_detect via the print.print_option command — but nothing anywhere calls it with that name. Dead code path.

The false reading may not even be true

filament_tangle_detect is commented “tracked locally only” — so the value bambuddy reports is a local placeholder and may not reflect the printer’s actual setting. Do not trust it either way.

Gap — plate detection is off and uncalibrated

plate_detection_enabled: false
calibration:            "Not calibrated - please calibrate with empty plate"
require_plate_clear:    false

Nothing stops the queue starting a print onto an occupied plate. (This is also why the awaiting_plate_clear red herring in §2 could not have been the blocker.)

Calibration is a remote call — no physical presence needed — once two preconditions hold: empty plate and chamber light on.


10. TAPO P115 ENERGY BRIDGE — built, shelved, UNTESTED

Everything in this section is UNTESTED

No Tapo plug was on the network during this session. The service is enabled but NOT started. Treat every claim below as “read from source / written to spec”, not “verified in operation”.

bambuddy has no native Tapo backend — only tasmota, homeassistant, mqtt, and rest. So a shim adapts a P115 to the generic rest backend.

ItemValue
Binary/opt/tapo-bridge/tapo-bridge
Venv/opt/tapo-bridge/venv (python-kasa 0.10.2)
Bind127.0.0.2:8117
Unittapo-bridge.serviceenabled, NOT started
Env file/etc/tapo-bridge.env, 0600 root-only, currently placeholders
State/opt/tapo-bridge/state.json
  • Binds 127.0.0.2, never 127.0.0.1 — house rule from 2026-08-31-tailnet-plaintext-port-hardening.
  • bambuddy runs NetworkMode: host, so 127.0.0.2:8117 is reachable from inside it — verified.
  • Units, read from source rather than guessed: power in W, energy in kWh, all multipliers 1.0.

Critical P115 limitation — no lifetime energy counter exists

consumption_total returns None for every Tapo device. This is hard-coded in python-kasa’s kasa/smart/modules/energy.py — the P115 simply exposes no lifetime counter over the local API.

The bridge therefore synthesizes one by accumulating daily values into /opt/tapo-bridge/state.json. It is monotonic and restart-safe, but it starts at zero and under-counts if the bridge is down across midnight. Any figure it reports is “energy since the bridge was installed”, not “energy since the plug was made”.

python-kasa 0.10.2 API notes (both cost time to find)

  • Device.connect() has NO credentials= parameter. Credentials go inside DeviceConfig(...), with
    DeviceConnectionParameters.from_values(
        "SMART.TAPOPLUG", "KLAP", login_version=2, https=False
    )
  • rest_method must be set to GET on the bambuddy side — bambuddy defaults to POST, which the bridge does not serve.

rest_* field mapping — for when a plug is actually connected

bambuddy settingValue
backendrest
rest_url (base)http://127.0.0.2:8117
rest_methodGET (not the default POST)
power fieldpower, W, multiplier 1.0
energy fieldenergy, kWh, multiplier 1.0
total fieldsynthesized from state.jsonstarts at 0, see the warning above

Before starting it: fill in real credentials in /etc/tapo-bridge.env (keep it 0600), put the plug on the LAN, then systemctl start tapo-bridge and verify against the plug’s own app reading before trusting any number.


11. IF THIS HAPPENS AGAIN — triage order

Three checks, in this order

  1. Read the preheat log lines FIRST — not the “not available” line. preheat starting - bed=… chamber_target=… (source=…) names the decision and its source. The Queue: printer N not available - connected=…, state=…, awaiting_plate_clear=… line is a context dump and will point you at the wrong thing (§2).
  2. Verify _derive_chamber_target’s answer against the print’s actual ams_mapping. If the derived target corresponds to a filament the print does not use, you are looking at §1. Dump the AMS tray table and the mapping side by side.
  3. Check per-input connected state before blaming hardware. A disconnected input reads as a fault at every layer above it; confirm the link before pulling anything apart.

Corollary for delays specifically: the FTP upload is never the problem — it moved 2.2 MB in 0.4 s. Look at ramp + soak.