grid-apps-cmms/mods/bambu/init.js

443 lines
15 KiB
JavaScript
Raw Normal View History

2025-01-29 11:54:52 -05:00
const { Client } = require('@gridspace/basic-ftp');
2025-01-25 18:03:56 -05:00
const { Readable } = require('stream');
const { FrameStream } = require('./frames');
2025-02-15 14:05:02 -05:00
const { bblCA } = require('./certs');
2025-01-25 18:03:56 -05:00
module.exports = async (server) => {
const { api, env, util } = server;
const confdir = util.confdir();
const mqtt = require("mqtt");
const mcache = {};
const wsopen = [];
const found = {};
const video = {};
const useCA = false;
const debug = false;
class MQTT {
#timer;
2025-02-02 00:09:19 -05:00
#timer2;
#client;
#serial;
2025-02-03 16:37:02 -05:00
#frames;
#topic_report;
#topic_request;
#options = Object.assign({}, {
protocol: 'mqtts',
port: 8883,
username: 'bblp',
}, useCA ? {
2025-02-04 05:50:28 +01:00
ca: bblCA
} : {
rejectUnauthorized: false
});
2025-01-25 18:03:56 -05:00
constructor(host, code, serial, onready, onerror, onmessage) {
this.#options.host = host;
this.#options.password = code;
2025-02-04 05:50:28 +01:00
this.#options.servername = serial;
this.#serial = serial;
let client = this.#client = mqtt.connect(this.#options);
client.on("connect", () => {
let report = this.#topic_report = `device/${serial}/report`;
let request = this.#topic_request = `device/${serial}/request`;
// util.log({ report, request });
client.subscribe(report, (err) => {
debug && util.log('mqtt sub', this.#serial, err || "ok");
onready(this);
this.keepalive();
2025-02-02 14:07:59 -05:00
// this.keepconn();
});
});
2025-01-25 18:03:56 -05:00
client.on("message", (topic, message) => {
message = JSON.parse(message.toString());
if (onmessage) {
onmessage(message);
} else {
debug && util.log('mqtt_recv', this.#serial, message);
}
});
client.on("error", error => onerror(error));
}
2025-02-03 16:37:02 -05:00
set_frames(bool) {
video[this.#serial] = bool;
2025-02-03 16:37:02 -05:00
if (this.#frames && !bool) {
this.#frames.end();
this.#frames = undefined;
} else if (!this.#frames && bool) {
2025-02-12 13:15:07 -05:00
let { host, password } = this.#options;
this.#frames = new FrameStream(host, password, this.#serial)
2025-02-03 16:37:02 -05:00
.on("frame", jpg => {
wsend({ serial: this.#serial, frame: jpg.toString('base64') });
})
}
}
keepalive() {
clearTimeout(this.#timer);
this.#timer = setTimeout(() => {
util.log('keepalive expired', this.#serial);
this.end();
}, 300000);
}
2025-01-25 18:03:56 -05:00
2025-02-02 00:09:19 -05:00
keepconn() {
clearTimeout(this.#timer2);
this.#timer2 = setTimeout(() => { this.keepconn() }, 120000);
2025-02-02 00:09:19 -05:00
if_mqtt(this.#serial, {
print: {
2025-02-02 00:09:19 -05:00
sequence_id: "0",
command: "push_status",
msg: 1
2025-02-02 00:09:19 -05:00
}
});
}
2025-01-25 18:03:56 -05:00
async send(msg) {
if (this.#client) {
debug && util.log('mqtt send', this.#serial, msg);
this.#client.publish(this.#topic_request, JSON.stringify(msg));
this.keepalive();
2025-01-25 18:03:56 -05:00
return true;
} else {
return false;
}
}
2025-01-25 18:03:56 -05:00
end() {
if (this.#client) {
util.log('mqtt end', this.#serial);
this.#client.end();
this.#client = undefined;
this.set_frames(false);
2025-01-25 18:03:56 -05:00
}
this.#topic_report = undefined;
this.#topic_request = undefined;
delete mcache[this.#serial];
2025-01-25 18:03:56 -05:00
}
}
2025-01-25 18:03:56 -05:00
2025-02-01 23:10:20 -05:00
function if_mqtt(serial, msg) {
mcache[serial]?.send(msg);
}
function get_mqtt(host, code, serial, onmsg, onconn) {
2025-01-25 18:03:56 -05:00
const fns = {};
const promise = new Promise((resolve, reject) => {
Object.assign(fns, { resolve, reject });
});
let mqtt = mcache[serial];
if (mqtt) {
fns.resolve(mqtt);
} else {
mqtt = new MQTT(host, code, serial, obj => {
mcache[serial] = obj;
fns.resolve(obj);
if (onconn) {
onconn(mqtt);
}
mcache[serial]?.set_frames(video[serial]);
}, error => fns.reject(error), onmsg);
2025-01-25 18:03:56 -05:00
}
return promise;
}
2025-01-29 00:35:56 -05:00
async function ftp_open(args = {}) {
2025-01-25 18:03:56 -05:00
const client = new Client();
const port = parseInt(args.port || 990);
const host = args.host || "localhost";
const user = args.user || "bblp";
2025-01-29 00:35:56 -05:00
const password = args.password || args.code || '';
client.ftp.verbose = debug;
2025-01-25 18:03:56 -05:00
try {
await client.access({
port,
host,
user,
password,
secure: "implicit",
secureOptions: useCA ? {
// this appears to break some
// operations like file deletion
ca: bblCA,
servername: args.serial
} : {
rejectUnauthorized: false
}
2025-01-25 18:03:56 -05:00
});
} catch (error) {
2025-01-29 00:35:56 -05:00
util.log({ ftp_error: error });
throw error;
2025-01-29 00:35:56 -05:00
}
return client;
}
async function ftp_send(args = {}) {
const client = await ftp_open(args);
const filename = args.filename || "test.3mf";
const data = args.data || undefined;
try {
2025-01-25 18:03:56 -05:00
const readableStream = new Readable();
readableStream._read = () => {};
readableStream.push(data);
readableStream.push(null);
await client.uploadFrom(readableStream, filename);
} finally {
client.close();
}
}
2025-01-29 00:35:56 -05:00
async function ftp_list(args = {}) {
const client = await ftp_open(args);
const list = [];
try {
let files = await client.list();
2025-02-01 23:10:20 -05:00
files.forEach(file => file.root = "");
list.push(...files);
} catch (e) { }
try {
let files = await client.list("/cache");
2025-02-01 23:10:20 -05:00
files.forEach(file => file.root = "cache/");
list.push(...files);
} catch (e) { }
client.close();
2025-01-29 00:35:56 -05:00
return list;
}
2025-02-01 23:10:20 -05:00
async function ftp_delete(args = {}) {
const client = await ftp_open(args);
try {
await client.remove(args.path);
} catch (error) {
util.log({ ftp_delete_error: error });
}
client.close();
}
function file_print(opts = {}) {
const { host, code, serial, filename, amsmap } = opts;
const cmd = {
print: {
command: "project_file",
url: `file:///sdcard/${filename}`,
param: "Metadata/plate_1.gcode",
subtask_id: "0",
use_ams: amsmap ? true : false,
timelapse: false,
flow_cali: false,
bed_leveling: false,
layer_inspect: false,
vibration_cali: false
}
};
if (amsmap && amsmap !== 'auto') {
2025-02-10 17:00:25 -05:00
cmd.print.ams_mapping = amsmap.split(',').map(v => parseInt(v));
2025-02-01 23:10:20 -05:00
}
util.log({ file_print: cmd });
2025-02-01 23:10:20 -05:00
get_mqtt(host, code, serial, message => {
debug && util.log('mqtt_recv', message);
2025-02-01 23:10:20 -05:00
wsend({ serial, message });
})
.then(mqtt => mqtt.send(cmd))
.catch(err => {
util.log({ mqtt_err: err });
});
}
2025-01-25 18:03:56 -05:00
function decode_post(req, res, next) {
if (req.method === 'POST') {
let chunks = [];
req
.on('data', data => chunks.push(data) )
.on('end', () => {
req.app.post = Buffer.concat(chunks);
next();
});
} else {
next();
}
}
// insert scripts before all others in kiri client
2025-01-25 18:03:56 -05:00
server.inject("kiri", "bambu.js");
2025-04-13 21:03:49 -04:00
server.inject("kiri", "errors.js");
server.inject("kiri", "filament.js");
2025-01-25 18:03:56 -05:00
function o2s(obj) {
return JSON.stringify(obj);
}
function wsend(msg) {
wsopen.forEach(ws => ws.send(JSON.stringify(msg)));
}
2025-01-25 18:03:56 -05:00
if (!(env.debug || env.electron)) {
2025-02-04 16:59:06 -05:00
util.log('not a valid context for bambu');
2025-01-25 18:03:56 -05:00
return;
}
// start SSDP listener for local Bambu printer broadcasts
{
const dgram = require("dgram");
const SSDP_ADDRESS = "239.255.255.250";
const SSDP_PORT = 1990;
const socket = dgram.createSocket("udp4");
socket.on("message", msg => {
msg = msg.toString();
if (msg.indexOf('.bambu.com') > 0) {
let rec = {};
map = msg.split('\n')
.filter(l => l.indexOf(': ') > 0)
.map(l => l.trim().replace('.bambu.com','').split(': '));
map.forEach(line => rec[line[0]] = line[1]);
// console.log({ ssdp: rec, map })
if (rec.DevName && rec.Location) {
let nurec = {
host: rec.Location,
name: rec.DevName,
type: rec.DevModel,
firm: rec.DevVersion,
srno: rec.USN
}
if (!found[rec.DevName]) {
found[rec.DevName] = nurec;
util.log(`found Bambu ${nurec.name} ${nurec.srno} @ ${nurec.host}`);
}
wsend({ found });
}
}
});
socket.bind(SSDP_PORT, () => {
socket.addMembership(SSDP_ADDRESS);
});
}
2025-01-25 18:03:56 -05:00
api.bambu_send = (req, res, next) => {
const { app, url, headers } = req;
const { host } = headers;
const { query } = app;
server.handler.addCORS(req, res);
decode_post(req, res, async () => {
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader('Cache-Control', 'no-cache, no-store, private');
const data = req.app.post;
2025-02-01 23:10:20 -05:00
const { host, code, filename, serial, ams, start } = query;
2025-02-04 05:50:28 +01:00
ftp_send({ host, code, filename, data, serial })
2025-01-25 18:03:56 -05:00
.then(() => {
2025-02-01 23:10:20 -05:00
if (serial && start ==='true') {
2025-02-10 17:00:25 -05:00
file_print({ host, code, serial, filename, amsmap: ams });
2025-01-25 18:03:56 -05:00
}
res.end(o2s({ sent: true }));
})
.catch(error => {
util.log({ ftp_send_error: error });
res.end(o2s({ sent: false, error }));
});
});
};
server.ws.register("/bambu", function(ws, req) {
wsopen.push(ws);
debug && util.log('ws open', req.url, wsopen.length);
wsend({ found });
ws.on('message', msg => {
msg = JSON.parse(msg);
2025-02-03 16:37:02 -05:00
let { cmd, host, code, serial, path, amsmap, direct, frames } = msg;
switch (cmd) {
case "monitor":
get_mqtt(host, code, serial, message => {
// util.log({ mqtt_msg: serial });
wsend({ serial, message });
}, mqtt => {
// on open only
}).then(mqtt => {
// util.log({ mqtt_mon: mqtt });
// request all printer state info
mqtt.send({
pushing: {
sequence_id: "0",
command: "pushall"
}
});
// request system info
mqtt.send({
info: {
command: "get_version"
}
});
// announce all current monitor hosts
wsend({ monitoring: Object.keys(mcache) });
}).catch(error => {
util.log({ mqtt_err: error });
wsend({ serial, error: error.message || error.toString() });
});
break;
2025-01-29 00:35:56 -05:00
case "files":
2025-02-04 05:50:28 +01:00
ftp_list({ host, code, serial }).then(files => {
debug && util.log({ ftp_files: files.length });
// console.log(JSON.stringify(files,undefined,4));
2025-01-29 00:35:56 -05:00
files = files
.filter(file => file.name.toLowerCase().endsWith(".3mf"))
.map(file => {
return {
2025-02-01 23:10:20 -05:00
root: file.root,
2025-01-29 00:35:56 -05:00
name: file.name,
2025-02-01 23:10:20 -05:00
path: file.root + file.name,
size: file.size,
date: file.rawModifiedAt
2025-01-29 00:35:56 -05:00
};
});
2025-02-01 23:10:20 -05:00
wsend({ serial, files });
}).catch(error => {
util.log({ ftp_error: error });
wsend({ serial, error: error.message || error.toString() });
2025-01-29 00:35:56 -05:00
});
break;
2025-02-01 23:10:20 -05:00
case "file-delete":
2025-02-04 05:50:28 +01:00
ftp_delete({ host, code, path, serial }).then(() => {
2025-02-01 23:10:20 -05:00
util.log({ ftp_delete: path });
wsend({ serial, deleted: path });
});
break;
case "file-print":
file_print({ host, code, serial, filename: path, amsmap });
break;
case "pause":
2025-02-01 23:10:20 -05:00
if_mqtt(serial, { print: { command: "pause", sequence_id: "0" } });
break;
case "resume":
2025-02-01 23:10:20 -05:00
if_mqtt(serial, { print: { command: "resume", sequence_id: "0" } });
break;
case "cancel":
2025-02-01 23:10:20 -05:00
if_mqtt(serial, { print: { command: "stop", sequence_id: "0", param: "" } });
break;
2025-02-02 14:07:59 -05:00
case "direct":
if_mqtt(serial, direct);
break;
2025-02-03 16:37:02 -05:00
case "frames":
debug && util.log('request frames', serial, frames);
2025-02-03 16:37:02 -05:00
mcache[serial]?.set_frames(frames);
video[serial] = frames;
2025-02-03 16:37:02 -05:00
break;
case "keepalive":
// util.log({ keepalive: serial });
mcache[serial]?.keepalive();
break;
}
});
ws.on('close', () => {
let io = wsopen.indexOf(ws);
if (io >= 0) wsopen.splice(io, 1);
debug && util.log('ws close', wsopen.length);
});
});
2025-01-25 18:03:56 -05:00
};