self.kiri.load(api => { if (api.electron || api.const.LOCAL) { console.log('BAMBU MODULE RUNNING'); } else { return; } const { kiri, moto } = self; const { ui } = api; const h = moto.webui; const defhost = ";; DEFINE BAMBU-HOST "; const defams = ";; DEFINE BAMBU-AMS "; let init = false; let status = {}; let bound, device, printers, select, selected; let btn_del, in_host, in_code, in_serial; let host, password, serial, amsmap, socket = { open: false, q: [], start() { if (socket.ws) { return; } let ws = socket.ws = new WebSocket("/bambu"); ws.onopen = () => { socket.open = true; socket.drain(); }; ws.onclose = () => { socket.open = false; }; ws.onmessage = msg => { let data = JSON.parse(msg.data); let { serial, message, error } = data; if (error) { console.log({ serial, error }); } else if (serial) { let rec = status[serial] = deepMerge(status[serial] || {}, message); if (selected?.rec.serial === serial) { printer_render(rec); } else { console.log('update', serial, rec); } } else { console.log('ignored', serial, data); } }; }, stop() { if (socket.ws) { socket.ws.close(); } }, drain() { while (socket.open && socket.q.length) { socket.ws.send(JSON.stringify(socket.q.shift())); } }, send(msg) { socket.start(); socket.q.push(msg); socket.drain(); } }; function deepMerge(target, source) { const result = structuredClone(target); Object.keys(source).forEach((key) => { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = deepMerge(result[key] || {}, source[key]); } else { result[key] = source[key]; } }); return result; } function deepSortObject(obj) { if (Array.isArray(obj)) { obj = obj.map(v => deepSortObject(v)); } else if (obj && typeof obj === "object" && !Array.isArray(obj)) { return Object.keys(obj) .sort() .reduce((sorted, key) => { sorted[key] = deepSortObject(obj[key]); return sorted; }, {}); } return obj; } function printer_add() { let name = prompt('printer name'); if (!name) { return; } printers[name] = printers[name] || { host:'', code:'', serial:'' }; render_list(); } function printer_del() { if (!selected.name) { return; } delete printers[selected.name]; printer_select(); render_list(); } function printer_update() { Object.assign(selected.rec, { host: in_host.value, code: in_code.value, serial: in_serial.value, modified: true }); } function printer_select(name) { btn_del.disabled = false; let rec = printers[name] || {}; selected = { name, rec }; in_host.value = rec.host || ''; in_code.value = rec.code || ''; in_serial.value = rec.serial || ''; in_host.onkeypress = in_host.onblur = printer_update; in_code.onkeypress = in_code.onblur = printer_update; in_serial.onkeypress = in_serial.onblur = printer_update; monitor_start(rec); $('bbl_name').innerText = name; } function printer_render(rec) { $('bbl_rec').value = JSON.stringify(deepSortObject(rec), undefined, 2); let { info, print } = rec; let { ams_status, bet_target_temper, bed_temper, big_fan1_speed, big_fan2_speed, chamber_temper, cooling_fan_speed, gcode_file, heatbreak_fan_speed, layer_num, mc_percent, mc_remaining_time, nozzle_diameter, nozzle_target_temper, nozzle_temper, print_error, print_type, sdcard, total_layer_num, upload } = print || {}; if (nozzle_diameter) { $('bbl_noz').value = nozzle_diameter; $('bbl_noz_temp').value = nozzle_temper; $('bbl_noz_target').value = nozzle_target_temper; } } function render_list() { h.bind(select, Object.keys(printers).map(name => { return h.option({ _: name, value: name }) })); } function monitor_start(rec) { socket.send({ cmd: "monitor", ...rec }); } function monitor_keepalive() { // console.log({ keepalive: selected }); if (selected?.rec?.serial) { socket.send({ cmd: "keepalive", serial: selected.rec.serial }); } } function monitor_stop() { socket.stop(); } api.event.on("init-done", function() { if (init) { return; } init = true; bound = h.bind($('device-save'), h.button({ _: 'Manage', id: "bblman", onclick() { api.modal.show('bambu'); } }), { before: true }); let modal = h.bind($('mod-help'), h.div({ id: "mod-bambu", class: "mdialog fcol gap4" }, [ h.div({ class: "f-row a-center gap4" }, [ h.label({ class: "set-header dev-sel" }, [ h.a('bambu manager') ]), h.select({ id: "bbl_sel", class: "dev-list" }, []), h.button({ _: 'monitor', title: "monitor printer", class: "grid", onclick() { printer_select(select.value) } }), h.div({ class: "grow gap3 j-end" }, [ h.button({ _: 'new', title: "add printer", class: "grid", onclick: printer_add }), h.button({ _: 'rename', title: "rename printer", class: "grid", onclick: printer_add }), h.button({ _: 'delete', id: 'bbl_pdel', title: "remove printer", class: "grid", onclick: printer_del }) ]) ]), h.div({ class: "set-sep "}), h.div({ class: "frow gap3" }, [ h.div({ class: "grow fcol gap3" }, [ h.div({ class: "t-body t-inset frow gap4 pad4 a-center" }, [ h.label({ class: "set-header dev-sel" }, [ h.a({ _: 'printer', id: "bbl_name" }) ]), h.label('host'), h.input({ id: "bbl_host", size: 15, class: "t-left mono" }), h.label('code'), h.input({ id: "bbl_code", size: 10, class: "t-left mono" }), h.label('serial'), h.input({ id: "bbl_serial", size: 20, class: "t-left mono" }), h.button({ id: "bbl_hide", _: '', onclick(ev) { if (ev.target.hide === true) { ev.target.hide = false; $('bbl_code').type = 'text'; $('bbl_serial').type = 'text'; $('bbl_hide').innerHTML = ''; } else { ev.target.hide = true; $('bbl_code').type = 'password'; $('bbl_serial').type = 'password'; $('bbl_hide').innerHTML = ''; } }}) ]), h.div({ class: "t-body t-inset fcol gap4 pad3 grow" }, [ h.div({ class: "frow gap4 pad4" }, [ h.label('nozzle'), h.input({ id: "bbl_noz", size: 5, class: "t-left mono" }), h.label('temp'), h.input({ id: "bbl_noz_temp", size: 8, class: "t-left mono" }), h.label('target'), h.input({ id: "bbl_noz_target", size: 8, class: "t-left mono" }), ]), h.textarea({ id: "bbl_rec", style: "width: 100%; resize: none; box-sizing: border-box", rows: 15, cols: 65 }) ]) ]), h.div({ class: "t-body t-inset fcol gap3 pad4" }, [ h.div({ class: "set-header" }, [ h.a('files') ]), h.select({ id: "bbl_sel", style: "height: 100%", size: 5 }, []), h.button({ _: "refresh", onclick() { console.log('file list refresh'); }}) ]) ]) ]), { before: true }); select = modal.bbl_sel; btn_del = modal.bbl_pdel; in_host = modal.bbl_host; in_code = modal.bbl_code; in_serial = modal.bbl_serial; api.ui.modals['bambu'] = modal['mod-bambu']; btn_del.disabled = true; }); api.event.on("modal.show", which => { if (which !== 'bambu' || !device) { return; } render_list(); }); api.event.on("modal.hide", which => { if (selected?.rec.modified) { api.conf.save(); selected = undefined; } }); api.event.on("device.selected", devsel => { if (!bound) { return; } if (devsel.extras?.bbl) { device = devsel; printers = devsel.extras.bbl; bound.bblman.classList.remove('hide'); } else { device = undefined; printers = undefined; bound.bblman.classList.add('hide'); } }); function sendok(params = {}) { host = password = serial = amsmap = undefined; const { settings } = params; const ams = settings.device?.gcodePre.filter(line => line.indexOf(defams) === 0)[0]; if (ams) { try { amsmap = ams.substring(defams.length).trim().replaceAll(' ',''); } catch (e) { console.log({ invalid_ams_map: ams }); } } const feature = settings.device?.gcodePre.filter(line => line.indexOf(defhost) === 0)[0]; if (feature) { [ host, password, serial ] = feature.substring(defhost.length).split(' ').map(v => v.trim()); console.log('BAMBU', { host , password, serial }); if (host && password) { return true; } } return false; } function send(filename, gcode) { const baseUrl = '/api/bambu_send'; const url = new URL(baseUrl, window.location.origin); url.searchParams.append('host', host); url.searchParams.append('password', password); url.searchParams.append('filename', filename); url.searchParams.append('serial', serial); url.searchParams.append('ams', amsmap); const alert = api.alerts.show('Sending to Bambu Printer'); fetch(url.toString(), { headers: { 'Content-Type': 'text/plain' }, method: 'POST', body: gcode }).then((response) => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } api.alerts.hide(alert); return response.json(); }).then(res => { console.log('Bambu Send', res); if (res.sent) { api.alerts.show('File Sent', 3); } else { api.alerts.show('File Send Error', 3); } }).catch(error => { console.error('Bambu Send Error', error); api.alerts.show('File Send Error', 3); }); }; setInterval(monitor_keepalive, 5000); api.bambu = { send, sendok, amsmap }; });