Map the P-series model codes to the right printers

C11 is the P1P, C12 the P1S, C13 the X1E and N7 the P2S, as the virtual
    printer and a real P1P 3MF already say. The frontend map had them shifted,
    so discovery pre-filled a P1S as a P1P and an X1E as a P2S. The backend
    map read C11/C12 as X1C/X1 and lacked N7, the firmware check sent C13 to
    the P2S line, and the capability lists never matched BL-P001 because
    their lookup strips the dash.

    -----

    Post work PR #3134
This commit is contained in:
maziggy
2026-09-30 16:24:42 +02:00
8 changed files with 138 additions and 18 deletions
+2 -1
View File
@@ -15,7 +15,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Ambient drying can wait until the humidity stays high, so opening the AMS lid no longer buys a drying cycle (#2518, requested by @ryansouza, contributed by @M2ABRAMSTANK in #2895)** — Ambient auto-drying started on the first humidity reading above the threshold. Opening the AMS lid to swap a spool lets room air reach the sensor for a few minutes, and that brief spike was enough to start a cycle of up to 12 hours. A new **Require sustained humidity** switch in the **Queue Auto-Drying** card under Settings → Workflow, shown while **Ambient drying** is on, sets how long the humidity has to stay above the threshold before a cycle starts: 5 to 240 minutes, 15 when first switched on. It is off by default, so nothing changes until it is turned on. The 15 minutes come from a measured trace on an H2D: after the lid had been open for one to five minutes, every unit was back at or below 25% within about 12 minutes of opening, and the wait only starts counting at the first high reading. One reading at or below the threshold starts the wait over, and so does a gap of more than two minutes without a reading (a disconnect, or a print on a printer that isn't drying while printing), which is logged with the measured gap. A missing reading changes nothing. A printer with a scheduled print pending still starts at once, for every AMS unit on it, because that drying has a deadline. The wait also applies to a printer that is printing with **Continue drying while printing** on, but not while ambient drying itself is off. It runs alongside the cooldown after a cycle instead of being added to it, never interrupts a running or manual cycle, and does not log a wait for a unit the printer refuses to dry. Translated in all locales.
- **Other applications can send messages through your notification channels** — A new switch per notification provider, **Messages from connected apps** (off by default), lets an application such as Bambuddy Orders send its own messages ("3 orders need you") to Telegram, ntfy, email and the other channels. The app calls `POST /api/v1/notifications/app-message` with an API key that has the new **Send Notifications** permission (off by default; the key's owner also needs `notifications:update`). Messages go through quiet hours, the daily digest and the notification log like Bambuddy's own. Text is plain, links must be http(s), and each key is limited to 20 messages a minute. `GET /api/v1/notifications/app-message/channels` tells the app which channels take its messages. The check behind the electricity-price door and this one is now one shared helper.
- **A link can open one batch order** — `/queue?batch=<id>` opens the Batches tab with that order highlighted and scrolled into view, whatever its status.
- **The streaming overlay can show the printer model (#3080, requested and contributed by @adamspicedev in #3099)** — Settings → API Keys → Streaming Overlay has a new **Printer model** checkbox, which adds `model` to the overlay URL's `show=` list. It can be picked together with the printer name or on its own. With both, the overlay shows `Big Mumma · H2D`; with only the model, `H2D`. A printer with no model stored shows the name alone, without a stray separator. The model is off by default, so every overlay URL already set up in OBS looks the same after upgrading. The token-authenticated feed that OBS reads (`/printers/{id}/overlay-status`) now returns `model` for connected and disconnected printers; it exposes nothing else new. A long name plus model in a narrow OBS source is now cut off with an ellipsis instead of running past the edge of the panel.
- **The streaming overlay can show the printer model (#3080, requested and contributed by @adamspicedev in #3099 and #3134)** — Settings → API Keys → Streaming Overlay has a new **Printer model** checkbox, which adds `model` to the overlay URL's `show=` list. It can be picked together with the printer name or on its own. With both, the overlay shows `Big Mumma · H2D`; with only the model, `H2D`. A printer with no model stored shows the name alone, without a stray separator. The model is off by default, so every overlay URL already set up in OBS looks the same after upgrading. The token-authenticated feed that OBS reads (`/printers/{id}/overlay-status`) now returns `model` for connected and disconnected printers; it exposes nothing else new. A long name plus model in a narrow OBS source is now cut off with an ellipsis instead of running past the edge of the panel. A printer saved by an older version under a discovery code such as `BL-P001` or `O1D` shows its model name (`X1C`, `H2D`), as on the Printers page, instead of the code.
- **An app shown in the sidebar can match Bambuddy's theme** — An external link that opens inside Bambuddy now tells the framed page which theme is showing: light or dark, background, style and accent. It sends this when the page loads, whenever the theme changes, and when the page asks with a `bambuddy:theme-request` message. The message is `{type: "bambuddy:theme", mode, style, background, accent}` and goes only to the link's own origin. It carries nothing but those names. Pages that don't listen are unaffected.
- **Post-print outcome confirmation: say whether a completed print is actually a good part (#1898, requested by @FedericoPuntelli, contributed by @Thomansky in #3047)** — The printer can only report that a print *completed*; a warped, out-of-tolerance or wrong-colour part counted as a success anyway. A print can now ask for a verdict: turn on **Ask for Outcome** in the print dialog, or set it as the default under Settings → Workflow → Queue & Dispatch → Default Print Options, and when the print finishes Bambuddy asks "How did your print come out?" with the finish photo and **Good** / **Reject**. It can be answered in the dialog that opens in the web UI, from the printer card next to the plate-clear button, or from the phone: the new **Outcome Confirmation** notification event puts Good/Reject buttons into ntfy and Telegram notifications and a link to the dialog into every other channel. A reject takes an optional reason from the failure-reason list and offers **Print again**. *Ask me later* leaves an amber **outcome?** badge on the archive card, and the Archives page gets an **Unconfirmed** filter; the verdict stays editable from the card's menu or Edit Archive, and says where it came from (in the app, a link, the printer card, or a plate release). Two further options, both off by default: **Also ask for prints started outside Bambuddy** gives prints started at the printer, in Bambu Studio or in the Handy app the same prompt, and **Count unanswered outcomes as good on plate release** (Plate-Clear Confirmation card) records a still-open prompt as good when the plate is released. Rejected prints don't touch the machine failure rate; Failure Analysis shows them as their own line with a yield figure, and project part counts only count parts that were not rejected. A one-tap link shows a confirmation page and records nothing on a plain open, so a link preview, a mail scanner or a browser prefetch can't answer the question for you; only the notification buttons record in one tap. Nothing changes for anyone who doesn't turn it on: the option is off by default, and the notification event only fires for prints that asked.
- **Connected apps: other applications can sign people in with their Bambuddy account** — Settings → API Keys → Connected Apps registers an app with one exact callback URL and gives it a client ID and a secret, shown once. The app sends the browser to `/connect/authorize`; the user confirms once ("Allow"), and after that sign-in is automatic, so an app opened from the sidebar needs no second login. The app's server then swaps a single-use code for the user's name, email, groups and permissions at `POST /api/v1/connect/token`. It is the OAuth 2.0 authorization-code flow with PKCE (S256 only), kept to what an app on the same network needs: a code lives 60 seconds, works once, is bound to its app, callback URL and PKCE challenge, and is redeemable only with the app's secret; codes and secrets are stored hashed, failed exchanges are rate-limited per app and per IP, and the page never redirects to a callback URL the backend has not matched against the registration. The app never receives the user's password or Bambuddy session, and an API key cannot sign its owner in to an app. Connected apps require authentication to be enabled; with it off, both endpoints answer `auth_disabled` so an app falls back to its own login instead of letting everyone in. The consent page may be shown inside Bambuddy's own frames (`frame-ancestors 'self'`), so an app opened from the sidebar can sign in there; every other page still refuses all framing. See the wiki page *Connected Apps*.
@@ -52,6 +52,7 @@ All notable changes to Bambuddy will be documented in this file.
- **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
### Fixed
- **A P1P, P1S or X1E added by discovery is saved as the right printer** — Bambu printers announce themselves with an internal code, and the table that turns it into a model name had the P-series codes shifted: a discovered P1S was saved as a P1P, a P1P as a P1S, and an X1E as a P2S. The same mix-up sat in the backend: a file sliced for a P1P or P1S could say it was sliced for an X1C or X1, the X1E's code checked the P2S firmware line, and the P2S's code was missing. All of them now agree with what the printers and 3MF files actually report. Printers saved under a raw code by an older version also get the right ethernet, rod-type and storage handling. A printer that was already saved with the wrong model keeps it: pick the right one under **Model** in **Edit** from the printer card's menu.
- **Generate Thumbnails works for users who can edit only their own files, and external folder scans no longer wait on PDF thumbnails** — The toolbar's **Generate Thumbnails** button and the file menu's **Generate Thumbnail** entry were offered to users who may edit only their own library files, but the server refused them. For those users both now cover the files they uploaded themselves; users who may edit all files still get everyone's. Scanning an external folder rendered each new PDF's thumbnail before the scan finished, which on a Raspberry Pi with a share full of PDFs kept the scan waiting for a long time. PDF thumbnails are now made in the background after the scan, like STL thumbnails already were.
- **The Low Filament notification now fires (#2913, reported and contributed by @ojimpo in #2940)** — The event had a toggle on every notification provider, but nothing in Bambuddy ever sent it, so switching it on did nothing. It now alerts when a spool assigned to an AMS, AMS-HT or external slot drops below its low-stock threshold. That is the same global percentage (default 20 %) and per-spool override that drive the Low Stock count on the Inventory page, so the alert and the count agree. Remaining filament comes from the spool's weight, not the AMS remain percentage, which can be far off. It works with the built-in inventory and with Spoolman; with Spoolman only the global threshold applies. Each spool alerts once per slot and can alert again after it goes back above the threshold, for example when a fresh one is loaded. Archived spools and slots with no assigned spool never alert. The check runs every 30 seconds and does nothing unless a provider has the event switched on. `{color}` is now filled in for custom templates. If you switched Low Filament on in the past, expect one alert for each assigned spool that is already low after updating, and again after each restart while it stays low.
- **A live camera view no longer stops for good after about half an hour on X1, H2 and P2 printers** — These printers' camera streams come over RTSP, and the printer ends each session after a while; a stock X1 Carbon ends every one after exactly a minute. Bambuddy reconnects straight away, so viewers never notice, but every reconnect counted against a limit of 30 for the life of the stream. Half an hour into a print the camera stopped and did not come back until the page was reopened. The limit now counts failed attempts in a row, and a session that delivered video resets it. A printer that refuses the camera for a while, for example because another app is on it, used to be given up on after 30 attempts in nine seconds. Bambuddy now waits longer between failed attempts, up to five seconds each, so it gets about two minutes. External cameras had the same problem with a limit of three: a camera server that ends its sessions now and then stopped the stream on the fourth drop. External streams now always reconnect after a session that delivered video. Found while investigating #3189.
+1 -1
View File
@@ -84,7 +84,7 @@ MODEL_TO_API_KEY = {
"BL-P003": "x1e",
"C11": "p1",
"C12": "p1",
"C13": "p2s",
"C13": "x1e",
"N2S": "a1",
"N1": "a1-mini",
"N6": "x2d",
+21 -9
View File
@@ -27,16 +27,22 @@ PRINTER_MODEL_MAP = {
}
# Map from printer_model_id (internal codes in slice_info.config) to short names
# These are the codes Bambu Studio uses internally
# These are the codes Bambu Studio uses internally, and the same codes the
# printers announce over SSDP (DevModel), so Printer.model can hold them too.
# A real P1P 3MF carries printer_model_id "C11" next to "Bambu Lab P1P"; the
# X1 Carbon's code is "BL-P001", not a C-code.
PRINTER_MODEL_ID_MAP = {
# X1 series
"C11": "X1C",
"C12": "X1",
"BL-P001": "X1C",
"BL-P002": "X1",
"C13": "X1E",
# P1 series
"C11": "P1P",
"C12": "P1S",
"P1P": "P1P",
"P1S": "P1S",
# P2 series
"N7": "P2S",
"P2S": "P2S",
# X2 series
"N6": "X2D",
@@ -71,10 +77,12 @@ CARBON_ROD_MODELS = frozenset(
"X1E",
"P1P",
"P1S",
# Internal codes
"C11", # X1C
"C12", # X1
# Internal codes (dashes stripped, as the lookup strips them)
"BLP001", # X1C (BL-P001)
"BLP002", # X1 (BL-P002)
"C13", # X1E
"C11", # P1P
"C12", # P1S
]
)
@@ -176,6 +184,9 @@ NO_REMOTE_STORAGE_TOGGLE_MODELS = frozenset(
# Display names (uppercase, no spaces)
"P1S",
"P1P",
# Internal codes
"C11", # P1P
"C12", # P1S
]
)
@@ -194,11 +205,12 @@ ETHERNET_MODELS = frozenset(
"H2DPRO",
"H2C",
"H2S",
# Internal codes
"C11", # X1C
# Internal codes (dashes stripped, as the lookup strips them)
"BLP001", # X1C (BL-P001)
"C13", # X1E
"N6", # X2D
"P1S", # P1S
"C12", # P1S
"N7", # P2S
"O1D", # H2D
"O1E", # H2D Pro
"O2D", # H2D Pro (alternate)
@@ -1424,7 +1424,7 @@ class TestSliceArchiveResliceModel:
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=200,
content=_make_sliced_3mf("C11"), # X1C — same model as source
content=_make_sliced_3mf("BL-P001"), # X1C — same model as source
headers={
"x-print-time-seconds": "600",
"x-filament-used-g": "5.0",
@@ -0,0 +1,65 @@
"""Bambu internal model codes resolve to the right printer everywhere.
The codes are shared by the SSDP DevModel header (so ``Printer.model`` of an
older install can hold them) and slice_info's ``printer_model_id``. A real P1P
3MF carries ``C11`` next to "Bambu Lab P1P", and the X1 Carbon's code is
``BL-P001``. Several maps had the C-codes shifted onto the wrong printers:
C11/C12 read as X1C/X1, the X1E's C13 checked P2S firmware, and the P2S's N7
was missing, which left a P2S file's ``sliced_for_model`` as the raw code.
"""
import pytest
from backend.app.services.firmware_check import FirmwareCheckService
from backend.app.utils.printer_models import (
get_rod_type,
has_ethernet,
has_remote_storage_toggle,
is_gcode_compatible,
normalize_printer_model_id,
)
CODES = [
("BL-P001", "X1C"),
("BL-P002", "X1"),
("C13", "X1E"),
("C11", "P1P"),
("C12", "P1S"),
("N7", "P2S"),
("N6", "X2D"),
("O1D", "H2D"),
("N2S", "A1"),
("N1", "A1 Mini"),
]
@pytest.mark.parametrize(("code", "name"), CODES)
def test_code_resolves_to_its_printer(code, name):
assert normalize_printer_model_id(code) == name
@pytest.mark.parametrize(("code", "name"), CODES)
def test_capabilities_agree_between_code_and_name(code, name):
assert has_ethernet(code) == has_ethernet(name)
assert get_rod_type(code) == get_rod_type(name)
assert has_remote_storage_toggle(code) == has_remote_storage_toggle(name)
def test_the_p1p_has_no_ethernet_and_the_x1c_has():
assert not has_ethernet("C11")
assert has_ethernet("BL-P001")
def test_a_p2s_file_can_go_to_a_p2s():
# A 3MF without printer_model in project_settings keeps the resolved
# printer_model_id as sliced_for_model; for N7 that used to be "N7",
# which the dispatch gate compared against "P2S" and refused.
assert is_gcode_compatible(normalize_printer_model_id("N7"), "P2S")
@pytest.mark.parametrize(
("model", "key"),
[("C13", "x1e"), ("X1E", "x1e"), ("C11", "p1"), ("C12", "p1"), ("N7", "p2s"), ("BL-P001", "x1")],
)
def test_firmware_line_for_code(model, key):
assert FirmwareCheckService()._resolve_api_key(model) == key
@@ -72,9 +72,11 @@ def test_normalization_spaces_dashes_case():
def test_internal_codes_resolve_to_short_names():
# slice_info printer_model_id codes compare equal to their short names.
assert is_gcode_compatible("C11", "X1C")
assert is_gcode_compatible("BL-P001", "X1C")
assert is_gcode_compatible("C11", "P1P")
assert is_gcode_compatible("N7", "P2S")
assert is_gcode_compatible("O1D", "H2D")
assert is_gcode_compatible("C11", "P1S") # X1C → family with P1S
assert is_gcode_compatible("C11", "X1C") # P1P → family with X1C
assert not is_gcode_compatible("C11", "H2D")
@@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { mapModelCode } from '../../utils/printerModel';
describe('mapModelCode', () => {
// A real P1P 3MF carries C11 next to "Bambu Lab P1P"; the X1 Carbon's code
// is BL-P001. The C-codes used to be shifted onto the wrong printers, and a
// discovered printer is saved under the mapped name.
it.each([
['BL-P001', 'X1C'],
['BL-P002', 'X1'],
['C13', 'X1E'],
['C11', 'P1P'],
['C12', 'P1S'],
['N7', 'P2S'],
['N6', 'X2D'],
['O1D', 'H2D'],
['N2S', 'A1'],
['N1', 'A1 Mini'],
['A11', 'A1'],
['A12', 'A1 Mini'],
['A04', 'A1 Mini'],
])('maps %s to %s', (code, name) => {
expect(mapModelCode(code)).toBe(name);
});
it('keeps display names and unknown values as they are', () => {
expect(mapModelCode('P1S')).toBe('P1S');
expect(mapModelCode('Future printer')).toBe('Future printer');
});
it('returns an empty string for a missing model', () => {
expect(mapModelCode(null)).toBe('');
expect(mapModelCode('')).toBe('');
});
});
+9 -4
View File
@@ -13,17 +13,22 @@ export function mapModelCode(ssdpModel: string | null): string {
'BL-P001': 'X1C',
'BL-P002': 'X1',
'BL-P003': 'X1E',
'C13': 'X1E',
// X2 Series
'N6': 'X2D',
// A2 Series
'N9': 'A2L',
// P Series
'C11': 'P1S',
'C12': 'P1P',
'C13': 'P2S',
// P Series. A real P1P 3MF carries C11 next to "Bambu Lab P1P"; the
// backend's PRINTER_MODEL_ID_MAP and the virtual printer use the same codes.
'C11': 'P1P',
'C12': 'P1S',
'N7': 'P2S',
// A1 Series
'N2S': 'A1',
'N1': 'A1 Mini',
'A11': 'A1',
'A12': 'A1 Mini',
'A04': 'A1 Mini',
// Direct matches
'X1C': 'X1C',
'X1': 'X1',