mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
Load and Unload in the AMS slot menu did nothing on an H2C with the switch fitted. The ams_change_filament command carries an optional extruder_id and Bambuddy never sent it. That is correct on every printer without the switch, and is what BambuStudio does there too -- each AMS is wired to one hotend, so the firmware works the target out for itself and an explicit value would only be a guess at something it already knows. Fit the switch and every AMS is bound to one of its two inlets instead, either hotend is reachable from any slot, and a command naming neither leaves the firmware nothing to act on. It was discarded in silence. Load now asks which hotend to feed, on the same terms as Bambu Studio: no preselection, so a stray Enter cannot feed the wrong one, and the hotend already fed from that very slot greyed out. Printers without a switch send a byte-identical command and still load in one click. A switch fitted but not yet set up -- any AMS still unassigned to an inlet -- refuses the load up front rather than publishing one the firmware will drop, mirroring DevFilaSwitch::IsReady, which likewise demands a switcher position on every AMS. Unload was addressed at the same time. It was aimed with tray_now, a single value for the whole printer, so on any dual-nozzle machine with both hotends loaded it unloaded whichever that field happened to name regardless of which slot's menu was used. It now names the slot and resolves the holding hotend from device.extruder.info, previously read for temperatures only. That resolution is gated on the printer having reported two extruders: single-nozzle machines do send the block, but nobody has read a single-nozzle snow value off the wire, and staking every X1C, P1S and A1 unload on an unverified encoding buys nothing where tray_now is already unambiguous. Both new state fields ride the WebSocket and are in the broadcast key, and both are computed in the REST status route as well -- that response is what the page has before any push arrives, and leaving them at their defaults would have told a correctly set-up machine that its switch was not set up. Verified on H2C-1, AMS-A slot 3: loaded and unloaded from each hotend in turn, all four correct. Covered by 18 MQTT unit tests, 4 status-dict tests, 7 integration tests and 6 component tests. Two known stragglers, both deliberately left alone. Load on an AMS-HT slot has never worked -- an HT unit is addressed by its unit id rather than ams*4+slot, which these endpoints do not accept -- so unload there keeps the printer-wide form it always used instead of gaining a slot it cannot name. And a slot's K-profile still follows the AMS's plumbing rather than the nozzle just loaded, so loading to the far hotend leaves the other one's calibration bound; that is the same per-nozzle problem the filament and K-profile redesign is scoped to fix.
140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Loader2 } from 'lucide-react';
|
|
import { Card, CardContent } from './Card';
|
|
import { Button } from './Button';
|
|
import type { ExtruderSlot } from '../api/client';
|
|
|
|
// Hotend ids as the firmware numbers them. Mirrors BambuStudio's
|
|
// MAIN_EXTRUDER_ID / DEPUTY_EXTRUDER_ID and `fts_routing.py` on the backend.
|
|
const RIGHT_EXTRUDER = 0;
|
|
const LEFT_EXTRUDER = 1;
|
|
|
|
interface FeedDirectionModalProps {
|
|
// Human-readable name of the slot being loaded, e.g. "AMS-A 3".
|
|
slotLabel: string;
|
|
// The slot's own coordinates, used to spot the hotend already holding it.
|
|
amsId: number;
|
|
slotId: number;
|
|
extruderSlots: Record<string, ExtruderSlot>;
|
|
isLoading?: boolean;
|
|
onConfirm: (extruderId: number) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
/**
|
|
* Asks which hotend to feed a slot into, for printers with a Filament Track
|
|
* Switch fitted.
|
|
*
|
|
* Without a switch each AMS is wired to one hotend and the firmware works the
|
|
* target out for itself, so the load command carries no hotend at all. With one
|
|
* fitted, every AMS is bound to a switch *inlet* instead and either hotend is
|
|
* reachable — the firmware then has nothing to infer from and drops a command
|
|
* that does not name one. BambuStudio asks the same question in the same place
|
|
* (`FeedDirectionDialog`), including leaving Confirm disabled until a side is
|
|
* picked, so there is no default to accidentally act on.
|
|
*/
|
|
export function FeedDirectionModal({
|
|
slotLabel,
|
|
amsId,
|
|
slotId,
|
|
extruderSlots,
|
|
isLoading = false,
|
|
onConfirm,
|
|
onCancel,
|
|
}: FeedDirectionModalProps) {
|
|
const { t } = useTranslation();
|
|
const [selected, setSelected] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && !isLoading) onCancel();
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [onCancel, isLoading]);
|
|
|
|
// A hotend already fed from this exact slot cannot be loaded from it again.
|
|
const holdsThisSlot = (extruderId: number) => {
|
|
const slot = extruderSlots[String(extruderId)];
|
|
return slot?.ams_id === amsId && slot?.slot_id === slotId;
|
|
};
|
|
|
|
const options = [
|
|
{ extruderId: LEFT_EXTRUDER, label: t('printers.ams.feedLeft'), taken: holdsThisSlot(LEFT_EXTRUDER) },
|
|
{ extruderId: RIGHT_EXTRUDER, label: t('printers.ams.feedRight'), taken: holdsThisSlot(RIGHT_EXTRUDER) },
|
|
];
|
|
const selectedIsTaken = options.some(o => o.extruderId === selected && o.taken);
|
|
|
|
// Status keeps arriving while the dialog is open, so a hotend can become the
|
|
// one holding this slot after it was picked — someone loading it from the
|
|
// printer's own screen. Drop the selection rather than leave Confirm armed on
|
|
// an option that has since been disabled.
|
|
useEffect(() => {
|
|
if (selectedIsTaken) setSelected(null);
|
|
}, [selectedIsTaken]);
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
|
|
onClick={isLoading ? undefined : onCancel}
|
|
>
|
|
<Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
|
<CardContent className="p-6">
|
|
<h3 className="text-lg font-semibold text-white mb-2">
|
|
{t('printers.ams.feedTitle', { slot: slotLabel })}
|
|
</h3>
|
|
<p className="text-bambu-gray text-sm">{t('printers.ams.feedPrompt')}</p>
|
|
|
|
<div className="grid grid-cols-2 gap-3 mt-4">
|
|
{options.map(({ extruderId, label, taken }) => {
|
|
const isSelected = selected === extruderId;
|
|
return (
|
|
<button
|
|
key={extruderId}
|
|
type="button"
|
|
onClick={() => setSelected(extruderId)}
|
|
disabled={taken || isLoading}
|
|
title={taken ? t('printers.ams.feedAlreadyLoaded') : undefined}
|
|
className={`p-3 rounded-lg border text-sm transition-colors ${
|
|
taken
|
|
? 'border-transparent bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
|
|
: isSelected
|
|
? 'border-bambu-green/50 bg-bambu-green/10 text-white'
|
|
: 'border-transparent bg-bambu-dark text-white hover:bg-bambu-dark-tertiary'
|
|
}`}
|
|
>
|
|
<div className="font-medium">{label}</div>
|
|
{taken && (
|
|
<div className="text-xs mt-1">{t('printers.ams.feedAlreadyLoaded')}</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex gap-3 mt-6">
|
|
<Button variant="secondary" onClick={onCancel} className="flex-1" disabled={isLoading}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button
|
|
onClick={() => selected !== null && onConfirm(selected)}
|
|
className="flex-1"
|
|
disabled={selected === null || isLoading}
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
{t('common.loading')}
|
|
</>
|
|
) : (
|
|
t('common.confirm')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|