mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
fix(filament): a unique preset match no longer counts as a colour match (#2687)
The Filament Mapping panel reported "(Ready)" with a green tick for a slot
where the slice wanted dark red and the auto-matched tray held dark green.
Manually picking that same tray reported the mismatch correctly, which is
what made it obvious something was inconsistent.
Auto-match ranks candidates by tray_info_idx first, and a uniquely-matching
preset was accepted as definitive on the premise "same preset = same spool =
same colour". The preset names the variant, not the spool: GFA00 is PLA
Basic, GFA01 PLA Matte, GFA17 PLA Translucent, in every colour Bambu sells.
The reporter's own bundle has eight GFA00 trays in eight colours. With one
Matte spool loaded, every Matte requirement idx-matched it and the colour
comparison was never reached - which is why this surfaced on PLA Matte and
not on Basic, where several spools are usually loaded and the match falls
through to the branch that does compare colours.
The verdict now comes from the tray that was selected rather than from which
rule selected it, and both branches share one comparison so they cannot
drift apart again. Selection is unchanged - the right variant still wins per
mismatch and the slot stays selected.
A requirement with no colour at all is treated as satisfied rather than
mismatched; 3MFs that omit it parse to "" and there is nothing to disagree
with. That also affects the manual branch, which used to flag it.
No dispatch change: _get_missing_force_color_slots already required an exact
colour, so force colour match was gated correctly throughout.
This commit is contained in:
@@ -30,6 +30,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Debug logs now record what the printer reports between the last layer and the end of a print (#2547, reporter @anthonyma94)** — The finish photo wants a moment that Bambu firmware does not obviously announce: printing done, toolhead parked, filament unload not yet started. Bambuddy has been driving that capture from `stg_cur=22` ("Filament unloading"), which turns out to fire on no model at all — across 247 support bundles there is not a single stage-22 capture, including the window in which it was the only trigger in the code, where all 104 captures on A1, A1 Mini, H2C, H2D, P1S, P2S, X1C and X2D fell through to the after-the-fact fallback. Choosing a replacement was not possible from the bundles we had, because outside `stg_cur` and `mc_print_sub_stage` every stage and action field the printers send is dropped unread, and the most promising candidates (`print_real_action`, `mc_action`, `mc_stage`) are absent from A1, A1 Mini and P1S payloads entirely. With debug logging enabled, Bambuddy now dumps those raw fields for the window between the last object layer and the end of the print — opening on the first end-of-print signal (last layer reached, progress at 99+, or no remaining time), logging only what changed frame to frame, and closing on the state transition — so a single debug bundle per model can show whether any firmware marks that moment. Diagnostics only: nothing reads these values, they are printer telemetry with nothing identifying in them, and at normal log levels the probe does no work at all. Covered by tests for the window boundaries, the frame budget and the guarantee that the probe cannot break status ingest.
|
||||
|
||||
### Fixed
|
||||
- **Auto-matched filament showed a green tick when the colour was plainly wrong (#2687, reporter @pchulpjoost)** — The Filament Mapping panel reported a slot as matched, with the header reading **(Ready)**, while the swatch beside it showed the slice wanted dark red and the tray it had picked held Dark Green. Manually selecting that very same tray from the dropdown correctly reported the colour mismatch, which is what made the disagreement so visible. **Root cause.** Auto-match ranks candidate trays by filament preset ID (`tray_info_idx`) first, and when exactly one loaded tray carried the preset the slice asked for, that tray was accepted as a *definitive* match on the assumption "same preset means same spool, so the colour must agree too". The preset ID names the **variant**, not the spool — `GFA00` is PLA Basic, `GFA01` PLA Matte, `GFA17` PLA Translucent, in every colour Bambu sells it. So a user with one Matte spool loaded matched every Matte requirement regardless of colour, and the colour comparison was never reached. This is why the report came in for PLA Matte in particular: generic PLA Basic is usually loaded several times over, which sent the match down a different path that did compare colours correctly. **Fix.** The colour verdict is now taken from the tray that was actually selected, never from which rule selected it, and the automatic and manual paths share one comparison so they cannot drift apart again. The preset still decides *selection*, because the Basic/Matte/Silk distinction matters ([#2650](https://github.com/maziggy/bambuddy/issues/2650)) — a wrong-coloured tray of the right variant is still chosen, but it is now reported as an amber **Color mismatch** instead of a green tick, and you can print anyway or pick another slot. A near-enough shade still counts as a match, and a 3MF that specifies no colour for a slot is satisfied by any colour rather than being flagged. Dispatch behaviour is unchanged: **Force color match** already required an exact colour before sending a job, so nothing was ever printed in the wrong colour because of this — the panel was simply telling you it was fine when it wasn't. Frontend-only. Wiki updated. Covered by tests for the unique-preset wrong-colour case, agreement between the auto and manual verdicts, the near-shade and colourless-requirement cases, and the multi-preset path that already worked.
|
||||
- **P1-series archives kept the worse finish photo when the timelapse arrived late (#2704 follow-up)** — When a print records a timelapse, Bambuddy prefers the video's last frame as the finish photo: the firmware stops recording after the toolhead parks but before the end G-code drops the bed, so it frames the finished print properly, where a live camera grab at that moment catches an already-lowered plate. Bambuddy waited 60 seconds for the video and then gave up, because the print-complete notification is waiting on that photo and holding a notification for minutes is worse than sending it with the live grab. On P1-series printers the video usually arrives later than that — they write MJPEG AVI instead of H.264 MP4 and serve it slowly, so across the support bundles their median was 33 seconds but the 90th percentile was 167 and the slowest observed was 546; every other model finished inside 26 seconds. The result was that the printers most in need of the better photo were the ones that never got it. **Fix.** The notification still goes out on the same 60-second bound with the live grab, so nothing gets slower. If the video was still on its way when that bound expired, Bambuddy now keeps waiting in the background and adds the extracted frame to the archive when it lands, at the front of the photo list so opening the gallery shows it first. The live grab is kept rather than replaced — the notification that already went out links to that exact file, and removing it would leave a broken image in Discord or Telegram. Covered by tests for the ordering, the longer budget, idempotency and the cases where the video never arrives.
|
||||
- **Timelapses that never got attached, and a Scan button that could not find them (#2704)** — Timelapse was on for the print, the video never arrived in the archive, and pressing **Scan for Timelapse** afterwards turned up nothing. Measured across 247 support bundles, this was not rare: of 457 automatic scans only 262 ever attached a video. **Root cause, part one.** The scan looked four times, at 5, 10, 20 and 30 seconds, then stopped. The printer writes the video only after the print ends and a long print makes a large file, so it often arrived after the last look — the attempt that found the video was the first one 272 times and then 17 / 13 / 13, a flat tail against the cutoff rather than a decaying one. What ran after those four attempts was a fallback that searched for the print's name inside the video filename; Bambu firmware only ever writes `video_<timestamp>`, so in 247 bundles it fired 159 times and matched exactly zero. **Root cause, part two.** The manual Scan button had no such snapshot to work from and matched by filename timestamp, by FTP modification time, or by there being exactly one video on the printer — all of which read a clock the printer cannot set, because a printer in LAN Only mode never reaches Bambu's time server. The reporter's P1S was six and a half days out, which defeats every one of those. **Fix.** The automatic scan now polls for several minutes instead of giving up after about a minute, and the name-match fallback is gone. The list of videos present when the print started is saved with the archive, so the comparison survives a Bambuddy restart mid-print and the manual Scan button can use it too — same clock-independent comparison, no timestamps anywhere. When a previous print's video lands late and two files look new, the one already attached to another archive is ruled out by name rather than by picking whichever the printer listed first, which could attach the wrong video. **Bambuddy now deletes a timelapse from the printer once it has been archived**, which keeps the printer's folder down to unclaimed videos and stops P1-series cards filling up with AVIs; your copy is in the archive, where you can watch, edit, download or remove it. That delete only happens after the transfer has been checked against the size the printer reported — which also fixes a silent truncation: an FTPS transfer that ended early produced a partial video that was attached as though it were complete. Because the first look happens seconds after the print ends — while the printer may still be writing the video — the file is also re-checked afterwards and only accepted once it has stopped growing, so a partial video is never mistaken for a finished one and the printer's copy is never removed on the strength of one. Wiki updated. Covered by tests for candidate selection, the download check gating the delete, the poll bounds, baseline persistence and the manual scan.
|
||||
- **A printer that refuses Bambuddy's access code now says so, instead of reconnecting silently forever (#2698, reporter @djepsylon)** — A printer whose access code or serial was wrong produced no explanation anywhere: the connection attempt was refused, and the only trace was a warning every 30 seconds reading `MQTT disconnected: rc=Unspecified error` — the same line you get from a printer that is simply switched off. The failure branch of the MQTT connect callback set "not connected" and discarded the reason code the printer had just sent, so the one piece of evidence that would have named the cause never reached the log, the support bundle, or the UI. In the report behind this fix, one of three printers had been in that loop for the entire capture and nothing said why. **Fix.** A refused connection is now logged with the printer's own reason ("Not authorized", "Bad user name or password") and, for those two, the remedy — the access code is regenerated every time LAN Only or Developer Mode is toggled, so it has to be re-read from the printer's screen. The reason is kept on the connection, so the **Connection Diagnostic**'s *Printer credentials* check now states plainly that the printer refused the credentials when that is what happened, and falls back to hedged wording when all Bambuddy knows is that there is no session — previously it asserted "the access code is most likely wrong" even for a printer that was merely rebooting or already at its connection limit. The same reason is returned by the pre-add connection test. The access code itself is never written to the log. Translated in all locales; wiki updated. Covered by tests for both refusal codes, the clearing of the reason on a successful reconnect, the diagnostic's reason plumbing, and the two UI variants.
|
||||
|
||||
@@ -1287,3 +1287,113 @@ describe('useFilamentMapping — no [-1] mapping during a status-load race (#258
|
||||
expect(result.current.hasTypeMismatch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('colour verdict is independent of how the tray was found (#2687)', () => {
|
||||
// tray_info_idx names the filament *variant*, not an individual spool:
|
||||
// GFA00 = PLA Basic, GFA01 = PLA Matte, GFA17 = PLA Translucent. A user with
|
||||
// exactly one Matte spool loaded therefore idx-matches every Matte
|
||||
// requirement no matter what colour it is.
|
||||
const MATTE_DARK_GREEN = createPrinterStatus([
|
||||
{ id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA01' }] },
|
||||
]);
|
||||
const wantRedMatte = {
|
||||
filaments: [
|
||||
{ slot_id: 1, type: 'PLA', color: '#9D432C', used_grams: 31, tray_info_idx: 'GFA01' },
|
||||
],
|
||||
};
|
||||
|
||||
it('reports a unique-idx tray of the wrong colour as type_only, not match', () => {
|
||||
const [item] = buildFilamentComparison(
|
||||
wantRedMatte,
|
||||
buildLoadedFilaments(MATTE_DARK_GREEN),
|
||||
{},
|
||||
);
|
||||
|
||||
// The tray is still selected — it is the right variant (#2650) ...
|
||||
expect(item.loaded?.globalTrayId).toBe(0);
|
||||
// ... but red-on-dark-green is not a colour match.
|
||||
expect(item.colorMatch).toBe(false);
|
||||
expect(item.status).toBe('type_only');
|
||||
});
|
||||
|
||||
it('auto and manual agree on the same tray', () => {
|
||||
const loaded = buildLoadedFilaments(MATTE_DARK_GREEN);
|
||||
const auto = buildFilamentComparison(wantRedMatte, loaded, {})[0];
|
||||
const manual = buildFilamentComparison(wantRedMatte, loaded, { 1: 0 })[0];
|
||||
|
||||
// The original report: auto said "match", manually picking that very tray
|
||||
// said "mismatch". Both paths must now reach the same verdict.
|
||||
expect(manual.isManual).toBe(true);
|
||||
expect(auto.status).toBe(manual.status);
|
||||
expect(auto.colorMatch).toBe(manual.colorMatch);
|
||||
});
|
||||
|
||||
it('surfaces the mismatch through the hook so the panel stops saying Ready', () => {
|
||||
const { result } = renderHook(() => useFilamentMapping(wantRedMatte, MATTE_DARK_GREEN, {}));
|
||||
// hasColorMismatch drives the yellow "(Color mismatch)" header; the tray is
|
||||
// still mapped, so this is not a type mismatch.
|
||||
expect(result.current.hasColorMismatch).toBe(true);
|
||||
expect(result.current.hasTypeMismatch).toBe(false);
|
||||
expect(result.current.amsMapping).toEqual([0]);
|
||||
});
|
||||
|
||||
it('still reports a match when the unique-idx tray does carry the right colour', () => {
|
||||
const [item] = buildFilamentComparison(
|
||||
{ filaments: [{ slot_id: 1, type: 'PLA', color: '#004225', used_grams: 31, tray_info_idx: 'GFA01' }] },
|
||||
buildLoadedFilaments(MATTE_DARK_GREEN),
|
||||
{},
|
||||
);
|
||||
expect(item.status).toBe('match');
|
||||
expect(item.colorMatch).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a near-enough shade on the idx path', () => {
|
||||
// Within colorsAreSimilar's per-channel tolerance — the printer reporting a
|
||||
// spool a shade off must not become a mismatch.
|
||||
const [item] = buildFilamentComparison(
|
||||
{ filaments: [{ slot_id: 1, type: 'PLA', color: '#0A4A2A', used_grams: 31, tray_info_idx: 'GFA01' }] },
|
||||
buildLoadedFilaments(MATTE_DARK_GREEN),
|
||||
{},
|
||||
);
|
||||
expect(item.status).toBe('match');
|
||||
});
|
||||
|
||||
it('treats a colourless requirement as satisfied by any colour', () => {
|
||||
// 3MFs that omit the colour parse to "" (filament_requirements.py); there is
|
||||
// nothing to disagree with, so this must not read as a colour mismatch.
|
||||
const [item] = buildFilamentComparison(
|
||||
{ filaments: [{ slot_id: 1, type: 'PLA', color: '', used_grams: 31, tray_info_idx: 'GFA01' }] },
|
||||
buildLoadedFilaments(MATTE_DARK_GREEN),
|
||||
{},
|
||||
);
|
||||
expect(item.status).toBe('match');
|
||||
expect(item.colorMatch).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the multi-idx path intact — same idx, several colours picks the right one', () => {
|
||||
// Two Matte spools: the branch that already compared colours must be
|
||||
// unaffected, and the exact-colour tray still wins.
|
||||
const twoMatte = createPrinterStatus([
|
||||
{
|
||||
id: 0,
|
||||
tray: [
|
||||
{ id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA01' },
|
||||
{ id: 1, tray_type: 'PLA', tray_color: '9D432C', tray_info_idx: 'GFA01' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const [item] = buildFilamentComparison(wantRedMatte, buildLoadedFilaments(twoMatte), {});
|
||||
expect(item.loaded?.globalTrayId).toBe(1);
|
||||
expect(item.status).toBe('match');
|
||||
});
|
||||
|
||||
it('a type-only fallback with no idx candidate is still type_only', () => {
|
||||
// Regression guard: the pre-existing "type matches, colour does not" path.
|
||||
const basicOnly = createPrinterStatus([
|
||||
{ id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA00' }] },
|
||||
]);
|
||||
const [item] = buildFilamentComparison(wantRedMatte, buildLoadedFilaments(basicOnly), {});
|
||||
expect(item.status).toBe('type_only');
|
||||
expect(item.colorMatch).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,6 +203,27 @@ export function useLoadedFilaments(
|
||||
}, [printerStatus]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the tray we picked actually carry the colour the slice asked for?
|
||||
*
|
||||
* Shared by the manual and auto branches below so the two can never disagree
|
||||
* about the same tray again (#2687). Exact hex first, then the perceptual
|
||||
* tolerance, so a spool the printer reports one shade off still reads as a
|
||||
* match.
|
||||
*
|
||||
* A requirement with no colour at all is not a mismatch — the 3MF simply
|
||||
* didn't ask for one (`filament_requirements.py` defaults it to `""`), so any
|
||||
* loaded colour satisfies it. Loaded trays always have a colour: buildLoaded-
|
||||
* Filaments falls back to grey when MQTT reports none.
|
||||
*/
|
||||
function coloursMatch(loadedColor: string | undefined, requiredColor: string | undefined): boolean {
|
||||
const required = normalizeColorForCompare(requiredColor);
|
||||
if (!required) return true;
|
||||
return (
|
||||
normalizeColorForCompare(loadedColor) === required || colorsAreSimilar(loadedColor, requiredColor)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare required filaments with loaded filaments (non-hook version).
|
||||
*
|
||||
@@ -236,9 +257,7 @@ export function buildFilamentComparison(
|
||||
|
||||
if (manualLoaded) {
|
||||
const typeMatch = manualLoaded.type?.toUpperCase() === req.type?.toUpperCase();
|
||||
const colorMatch =
|
||||
normalizeColorForCompare(manualLoaded.color) === normalizeColorForCompare(req.color) ||
|
||||
colorsAreSimilar(manualLoaded.color, req.color);
|
||||
const colorMatch = coloursMatch(manualLoaded.color, req.color);
|
||||
|
||||
let status: FilamentStatus;
|
||||
if (typeMatch && colorMatch) {
|
||||
@@ -358,17 +377,25 @@ export function buildFilamentComparison(
|
||||
|
||||
const hasFilament = !!loaded;
|
||||
const typeMatch = hasFilament;
|
||||
// idxMatch is always considered a color match (same spool = same color)
|
||||
const colorMatch = !!idxMatch || !!exactMatch || !!similarMatch;
|
||||
// #2687: judge the colour on the tray we actually picked, never on which
|
||||
// branch found it. tray_info_idx identifies the filament *variant* — GFA00
|
||||
// is PLA Basic, GFA01 PLA Matte, GFA17 PLA Translucent — not an individual
|
||||
// spool, so one Matte spool idx-matches every Matte requirement whatever
|
||||
// colour it is. The old rule ("same spool = same color") therefore reported
|
||||
// red-required-on-green-loaded as a match, while manually picking that same
|
||||
// tray reported the mismatch honestly. Variant still decides *selection*
|
||||
// (#2650: Basic is not Matte) — it just no longer decides the verdict.
|
||||
const colorMatch = hasFilament && coloursMatch(loaded.color, req.color);
|
||||
|
||||
// Status: match (tray_info_idx, type+color, or similar color), type_only (type ok, color very different), mismatch (type not found)
|
||||
// No tray of the required type at all is a type mismatch; otherwise the
|
||||
// colour decides between a full match and type-only.
|
||||
let status: FilamentStatus;
|
||||
if (idxMatch || exactMatch || similarMatch) {
|
||||
status = 'match';
|
||||
} else if (typeOnlyMatch) {
|
||||
status = 'type_only';
|
||||
} else {
|
||||
if (!hasFilament) {
|
||||
status = 'mismatch';
|
||||
} else if (colorMatch) {
|
||||
status = 'match';
|
||||
} else {
|
||||
status = 'type_only';
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DAvf4vFq.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-B-Fe9tPc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D4bpNaiw.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user