Geräteprofil · Verschiedenes
Bambu Lab P2S
Der Drucker wird nicht direkt angesprochen. Ein Node-RED-Flow spricht mit dem Drucker und veröffentlicht dessen Werte retained auf dem MQTT-Broker von nomos. Dieses Profil liest diese Werte und schickt Befehle an den Flow zurück.
Einrichtung
- Den P2S in den LAN-Modus schalten und IP-Adresse, Seriennummer und Zugangscode notieren (Druckerdisplay)
- In nomos das Node-RED-Panel öffnen
- Den folgenden Flow importieren:
JSON zum Einfügen
[{"id":"tab_bambu","type":"tab","label":"Bambu P2S → nomos","disabled":false,"info":"Relay: liest den lokalen MQTT-Report des Bambu Lab P2S (TLS, Port 8883) und publiziert flache, retained Werte unter bambu/p2s/... auf den nomos-Broker.\n\nVor dem Start:\n1. In 'Bambu P2S' (Broker-Konfiguration) IP des Druckers statt PRINTER_IP eintragen, Benutzer bblp, Passwort = Zugangscode vom Display.\n2. In 'nomos Controller' IP des Controllers und die MQTT-Zugangsdaten aus den nomos-MQTT-Einstellungen eintragen.\n3. Seriennummer des Druckers statt BAMBUSERIAL in den beiden Topic-Feldern eintragen (device/<SERIAL>/report bzw. /request).\n4. Am Drucker ggf. Entwicklermodus einschalten, falls die Verbindung abgewiesen wird."},{"id":"in_report","type":"mqtt in","z":"tab_bambu","name":"P2S report","topic":"device/BAMBUSERIAL/report","qos":"0","datatype":"json","broker":"bambu_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":150,"y":120,"wires":[["fn_map","dbg_raw"]]},{"id":"fn_map","type":"function","z":"tab_bambu","name":"Status zusammenführen → bambu/p2s/*","func":"// Bambu schickt nach pushall den vollen Status, danach nur noch Teil-Updates.\n// Wir halten den letzten bekannten Stand im Flow-Kontext und publizieren nur Änderungen.\n\nconst p = msg.payload && msg.payload.print;\nif (!p) return null;\n\nconst state = flow.get('bambu_state', 'memory') || {};\nfor (const k of Object.keys(p)) state[k] = p[k];\nflow.set('bambu_state', state, 'memory');\n\nconst last = flow.get('bambu_last', 'memory') || {};\nconst base = 'bambu/p2s/';\nconst out = [];\n\nfunction pub(sub, val) {\n if (val === undefined || val === null || Number.isNaN(val)) return;\n const s = typeof val === 'object' ? JSON.stringify(val) : String(val);\n if (last[sub] === s) return;\n last[sub] = s;\n out.push({ topic: base + sub, payload: s, retain: true, qos: 0 });\n}\n\nfunction num(v, digits) {\n const n = Number(v);\n if (Number.isNaN(n)) return undefined;\n return digits === undefined ? n : Number(n.toFixed(digits));\n}\n\n// Druckstatus\nconst gstate = state.gcode_state; // IDLE, RUNNING, PAUSE, FINISH, FAILED, PREPARE, SLICING\nconst printing = gstate === 'RUNNING' || gstate === 'PREPARE' || gstate === 'SLICING';\nconst stateDe = { IDLE: 'Bereit', RUNNING: 'Druckt', PAUSE: 'Pausiert', FINISH: 'Fertig', FAILED: 'Fehler', PREPARE: 'Vorbereitung', SLICING: 'Vorbereitung' }[gstate] || gstate;\n\npub('state', gstate);\npub('state_text', stateDe);\npub('printing', printing);\n\nconst pct = num(state.mc_percent);\nconst remain = num(state.mc_remaining_time); // Minuten\npub('progress', printing || gstate === 'PAUSE' ? pct : (gstate === 'FINISH' ? 100 : 0));\npub('remaining_min', printing || gstate === 'PAUSE' ? remain : 0);\n\nif (remain !== undefined && (printing || gstate === 'PAUSE')) {\n const h = Math.floor(remain / 60), m = remain % 60;\n pub('remaining_text', h > 0 ? `${h} h ${m} min` : `${m} min`);\n const eta = new Date(Date.now() + remain * 60000);\n pub('eta', eta.toLocaleTimeString('de-AT', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Vienna' }));\n} else {\n pub('remaining_text', '');\n pub('eta', '');\n}\n\nconst ln = num(state.layer_num), lt = num(state.total_layer_num);\npub('layer', ln);\npub('layer_total', lt);\nif (ln !== undefined && lt !== undefined) pub('layer_text', `${ln} / ${lt}`);\n\npub('job', state.subtask_name || '');\n\n// Temperaturen\npub('nozzle_temp', num(state.nozzle_temper, 1));\npub('nozzle_target', num(state.nozzle_target_temper, 0));\npub('bed_temp', num(state.bed_temper, 1));\npub('bed_target', num(state.bed_target_temper, 0));\npub('chamber_temp', num(state.chamber_temper, 1));\n\n// Licht, WLAN, Fehler\nif (Array.isArray(state.lights_report)) {\n const l = state.lights_report.find(x => x.node === 'chamber_light');\n if (l) pub('light', l.mode === 'on');\n}\nif (state.wifi_signal !== undefined) {\n pub('wifi', String(state.wifi_signal).replace('dBm', ''));\n}\nconst err = num(state.print_error);\nif (err !== undefined) {\n pub('error', err !== 0);\n pub('error_code', err);\n}\n\n// AMS (erstes Gerät, 4 Fächer)\nif (state.ams && Array.isArray(state.ams.ams) && state.ams.ams[0]) {\n const a = state.ams.ams[0];\n pub('ams/humidity', num(a.humidity));\n pub('ams/temp', num(a.temp, 1));\n const now = state.ams.tray_now; // \"0\"-\"3\" = AMS-Fach, \"254\" = externe Spule, \"255\" = nichts geladen\n if (now !== undefined) pub('ams/active_tray', now === '254' ? 'extern' : (now === '255' ? 'keins' : String(Number(now) + 1))); // Fach 1-4 wie am Drucker\n (a.tray || []).forEach((t, i) => {\n const empty = !t || !t.tray_type;\n pub(`ams/tray/${i}/type`, empty ? '' : t.tray_type);\n pub(`ams/tray/${i}/color`, empty || !t.tray_color ? '' : '#' + String(t.tray_color).slice(0, 6));\n pub(`ams/tray/${i}/remain`, empty ? 0 : num(t.remain));\n pub(`ams/tray/${i}/name`, empty ? '' : (t.tray_sub_brands || t.tray_type));\n });\n}\n\n// Kompaktes JSON für eine einzelne Anzeige\npub('summary', {\n state: gstate, printing, progress: pct, remaining_min: remain,\n layer: ln, layer_total: lt, job: state.subtask_name || '',\n nozzle: num(state.nozzle_temper, 1), bed: num(state.bed_temper, 1), chamber: num(state.chamber_temper, 1)\n});\n\nflow.set('bambu_last', last, 'memory');\nreturn [out];","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":120,"wires":[["out_nomos"]]},{"id":"out_nomos","type":"mqtt out","z":"tab_bambu","name":"→ nomos Broker","topic":"","qos":"0","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"64880cc0.94fac4","x":800,"y":120,"wires":[]},{"id":"inj_pushall","type":"inject","z":"tab_bambu","name":"pushall (Start + alle 10 min)","props":[{"p":"payload"}],"repeat":"600","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"{\"pushing\":{\"sequence_id\":\"0\",\"command\":\"pushall\"}}","payloadType":"json","x":190,"y":220,"wires":[["out_request"]]},{"id":"out_request","type":"mqtt out","z":"tab_bambu","name":"→ P2S request","topic":"device/BAMBUSERIAL/request","qos":"0","retain":"false","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"bambu_broker","x":800,"y":220,"wires":[]},{"id":"status_printer","type":"status","z":"tab_bambu","name":"Verbindung P2S","scope":["in_report"],"x":170,"y":320,"wires":[["fn_online"]]},{"id":"fn_online","type":"function","z":"tab_bambu","name":"online/offline","func":"const txt = (msg.status && msg.status.text) || '';\nconst online = /connected/.test(txt) && !/disconnected/.test(txt);\nif (flow.get('bambu_online', 'memory') === online) return null;\nflow.set('bambu_online', online, 'memory');\nif (!online) { flow.set('bambu_last', {}, 'memory'); } // nach Reconnect alles neu publizieren\nreturn { topic: 'bambu/p2s/online', payload: String(online), retain: true, qos: 0 };","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":320,"wires":[["out_nomos"]]},{"id":"in_cmd","type":"mqtt in","z":"tab_bambu","name":"bambu/p2s/cmd/# (optional)","topic":"bambu/p2s/cmd/#","qos":"0","datatype":"auto-detect","broker":"64880cc0.94fac4","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":420,"wires":[["fn_cmd"]]},{"id":"fn_cmd","type":"function","z":"tab_bambu","name":"Befehl → Bambu JSON","func":"// Topics: bambu/p2s/cmd/light (true/false|on/off), /pause, /resume, /stop\nconst cmd = msg.topic.split('/').pop();\nconst v = String(msg.payload).toLowerCase();\nconst on = v === 'true' || v === 'on' || v === '1';\n\nswitch (cmd) {\n case 'light':\n return { payload: { system: { sequence_id: '0', command: 'ledctrl', led_node: 'chamber_light', led_mode: on ? 'on' : 'off', led_on_time: 500, led_off_time: 500, loop_times: 0, interval_time: 0 } } };\n case 'pause':\n return { payload: { print: { sequence_id: '0', command: 'pause' } } };\n case 'resume':\n return { payload: { print: { sequence_id: '0', command: 'resume' } } };\n case 'stop':\n if (!on) return null; // Stop nur bei true, damit ein Fehlklick nichts abbricht\n return { payload: { print: { sequence_id: '0', command: 'stop' } } };\n default:\n node.warn('Unbekannter Befehl: ' + cmd);\n return null;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":420,"wires":[["out_request"]]},{"id":"dbg_raw","type":"debug","z":"tab_bambu","name":"Rohdaten P2S (zum Nachsehen)","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":480,"y":60,"wires":[]},{"id":"bambu_broker","type":"mqtt-broker","name":"Bambu P2S","broker":"PRINTER_IP","port":"8883","tls":"bambu_tls","clientid":"nodered-nomos-relay","autoConnect":true,"usetls":true,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"64880cc0.94fac4","type":"mqtt-broker","name":"nomos Controller","broker":"127.0.0.1","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"bambu_tls","type":"tls-config","name":"Bambu P2S (selbstsigniert)","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"","verifyservercert":false,"alpnprotocol":""}] - Im Flow die Platzhalter ersetzen:
- Broker-Konfiguration Bambu P2S: PRINTER_IP durch die IP des Druckers, Benutzer bblp, Passwort = Zugangscode
- Broker-Konfiguration nomos Controller: die MQTT-Zugangsdaten des Controllers, falls der Broker welche verlangt
- Knoten P2S report und → P2S request: BAMBUSERIAL durch die Seriennummer des Druckers
- Weist der Drucker die Verbindung ab, am Drucker den Entwicklermodus einschalten
- In der nomos Konfiguration unter Skills → Add-ons das Bambu Lab P2S-Profil importieren
- Gerät anlegen
- Einen Namen vergeben und bambu/p2s als MQTT-Topic eintragen (die Topic-Wurzel, die der Flow verwendet)
Werte, die der Flow veröffentlicht
Alle Topics sind relativ zum MQTT-Topic des Geräts (also z. B. bambu/p2s/state).
| Topic | Inhalt |
|---|---|
online |
true / false |
state |
IDLE, RUNNING, PAUSE, FINISH, FAILED |
state_text |
Zustand im Klartext |
printing |
true / false |
progress |
0–100 |
remaining_min |
Restzeit in Minuten |
remaining_text |
Restzeit im Klartext |
eta |
voraussichtliches Ende, HH:MM |
layer, layer_total |
aktuelle und gesamte Schicht |
layer_text |
Schichten im Klartext |
job |
Name des Druckauftrags |
nozzle_temp, nozzle_target |
Düsentemperatur und Soll, °C |
bed_temp, bed_target |
Betttemperatur und Soll, °C |
chamber_temp |
Kammertemperatur, °C |
light |
Kammerlicht, true / false |
error |
true / false |
error_code |
Fehlercode |
wifi |
WLAN-Signal, dBm |
ams/humidity, ams/temp |
Feuchteindex und Temperatur des AMS |
ams/active_tray |
aktives AMS-Fach: 1–4, extern oder keins |
ams/tray/0..3/type, color, remain, name |
Filament je AMS-Fach (Fach 0 ist Schacht 1) |
summary (JSON) wird vom Profil nicht gelesen.
Befehle, die der Flow entgegennimmt
| Topic | Nutzlast |
|---|---|
cmd/light |
true / false |
cmd/pause |
true |
cmd/resume |
true |
cmd/stop |
true |