nomos system nomos system

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

  1. Den P2S in den LAN-Modus schalten und IP-Adresse, Seriennummer und Zugangscode notieren (Druckerdisplay)
  2. In nomos das Node-RED-Panel öffnen
  3. 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":""}]
    
  4. 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
  5. In der nomos Konfiguration unter Skills → Add-ons das Bambu Lab P2S-Profil importieren
  6. Gerät anlegen
  7. 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