fix(spool-assign): defer MQTT for empty AMS slot, replay on physical insert

The SpoolBuddy "weigh-then-assign" workflow tried to configure an empty AMS
  slot at assign time, but Bambu firmware silently drops ams_filament_setting
  and extrusion_cali_sel for unloaded slots — the MQTT calls completed and the
  modal closed, yet BambuStudio kept showing the slot as default-PLA forever.

  assign_spool now detects an empty target slot (fingerprint_type empty) and
  persists the SpoolAssignment without publishing MQTT, returning a new
  pending_config flag so the frontend can swap "Assigned!" for "Slot will
  configure when you insert the spool." on_ams_change watches for the slot
  to load (state == 11, which fires for 3rd-party tags too even when
  tray_type stays empty) and replays the deferred ams_filament_setting +
  extrusion_cali_sel — including the printer-kp realignment that converts
  PFUS-prefix cloud user presets to the P-prefix local-preset filament_id
  the slicer actually accepts.

  The full assign-time MQTT block was extracted into
  apply_spool_to_slot_via_mqtt so both the assign endpoint and the
  on_ams_change replay path use the same resolution logic; the helper takes
  ~270 lines of duplication out of assign_spool.
This commit is contained in:
maziggy
2026-05-04 16:02:18 +02:00
parent 8986343999
commit b42aaca521
17 changed files with 659 additions and 304 deletions
@@ -167,27 +167,37 @@ export function AssignToAmsModal({ isOpen, onClose, spool, printerId }: AssignTo
}, [isDualNozzle, amsExtruderMap]);
// Assign spool to AMS slot — single API call, backend handles both
// DB record AND MQTT auto-configuration (same as SpoolStation).
// DB record AND MQTT auto-configuration (same as SpoolStation). When the
// target slot is currently empty, the backend persists the assignment and
// skips the MQTT publish (firmware drops it anyway); on_ams_change re-fires
// the full configuration when filament is later inserted. The response's
// `pending_config` flag distinguishes that from the immediate-apply path
// so we can adjust the success toast.
const configureMutation = useMutation({
mutationFn: async ({ amsId, trayId }: { amsId: number; trayId: number }) => {
if (!printerId) throw new Error('No printer selected');
await api.assignSpool({
return await api.assignSpool({
spool_id: spool.id,
printer_id: printerId,
ams_id: amsId,
tray_id: trayId,
});
// Slot preset mapping is now saved by the backend in assign_spool()
// after successful MQTT configuration, using the authoritative
// slicer_filament_name from the spool record.
},
onSuccess: () => {
onSuccess: (assignment) => {
setStatusType('success');
setStatusMessage(t('spoolbuddy.modal.assignSuccess', 'Assigned!'));
if (assignment?.pending_config) {
setStatusMessage(
t(
'spoolbuddy.modal.assignPendingInsert',
'Assigned. Slot will configure when you insert the spool.',
),
);
} else {
setStatusMessage(t('spoolbuddy.modal.assignSuccess', 'Assigned!'));
}
queryClient.invalidateQueries({ queryKey: ['slotPresets'] });
setTimeout(() => onClose(), 1500);
setTimeout(() => onClose(), assignment?.pending_config ? 2500 : 1500);
},
onError: (err) => {
setStatusType('error');