Compare commits
No commits in common. "a8fd1122579c41c7b3fae0d463f79dfbe20dbdd7" and "f7f7b21d36d0c6b3b77e121b7c1c957c7c0a9980" have entirely different histories.
a8fd112257
...
f7f7b21d36
3 changed files with 0 additions and 159 deletions
|
|
@ -1,123 +0,0 @@
|
|||
// The chamber camera on a P1 or A1 series printer.
|
||||
//
|
||||
// The printer serves still frames on port 6000 over TLS: an 80-byte hello with
|
||||
// the same "bblp" username and access code as the status connection, then a
|
||||
// stream of JPEGs, each behind a 16-byte header giving its size. Roughly one
|
||||
// frame a second — stills, not video. Read-only, like everything else here.
|
||||
//
|
||||
// X1 printers don't do this; they serve RTSP on port 322, which a browser
|
||||
// can't show without transcoding.
|
||||
const tls = require('tls');
|
||||
|
||||
const PORT = 6000;
|
||||
const HEADER = 16;
|
||||
const MAX_FRAME = 12 * 1024 * 1024; // a frame is well under this; a bigger one means we've lost our place
|
||||
|
||||
function hello(accessCode) {
|
||||
const buf = Buffer.alloc(80);
|
||||
buf.writeUInt32LE(0x40, 0); // payload length
|
||||
buf.writeUInt32LE(0x3000, 4); // protocol marker
|
||||
buf.write('bblp', 16, 32, 'ascii');
|
||||
buf.write(String(accessCode).slice(0, 31), 48, 32, 'ascii');
|
||||
return buf;
|
||||
}
|
||||
|
||||
// The picture's size, from the JPEG's own frame header — a cheap sanity check
|
||||
// that what arrived really is an image.
|
||||
function jpegSize(buf) {
|
||||
let i = 2;
|
||||
while (i + 9 <= buf.length) {
|
||||
if (buf[i] !== 0xff) { i += 1; continue; }
|
||||
const marker = buf[i + 1];
|
||||
// SOF0…SOF3, SOF5…SOF7, SOF9…SOF11, SOF13…SOF15 carry the dimensions.
|
||||
if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
|
||||
return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
|
||||
}
|
||||
i += 2 + buf.readUInt16BE(i + 2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Connects and calls onFrame for each JPEG until stop() is called.
|
||||
// onError is called once, with a plain-language reason.
|
||||
function watch({ host, accessCode, onFrame, onError = () => {}, connectTimeoutMs = 8000 }) {
|
||||
let closed = false;
|
||||
let pending = Buffer.alloc(0);
|
||||
let expecting = null; // size of the frame being gathered
|
||||
|
||||
const socket = tls.connect({
|
||||
host,
|
||||
port: PORT,
|
||||
rejectUnauthorized: false, // the printer's certificate is self-signed
|
||||
timeout: connectTimeoutMs,
|
||||
});
|
||||
|
||||
const fail = (message) => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
socket.destroy();
|
||||
onError(message);
|
||||
};
|
||||
|
||||
socket.on('secureConnect', () => {
|
||||
socket.setTimeout(0);
|
||||
socket.write(hello(accessCode));
|
||||
});
|
||||
socket.on('timeout', () => fail('The printer didn’t answer on its camera port.'));
|
||||
socket.on('error', (err) => fail(
|
||||
/ECONNREFUSED/.test(err.message)
|
||||
? 'The printer refused the camera connection — “LAN Mode Liveview” may be off.'
|
||||
: err.message));
|
||||
socket.on('close', () => fail('The printer closed the camera connection — the access code may be wrong, or “LAN Mode Liveview” is off.'));
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
||||
for (;;) {
|
||||
if (expecting === null) {
|
||||
if (pending.length < HEADER) return;
|
||||
expecting = pending.readUInt32LE(0);
|
||||
pending = pending.subarray(HEADER);
|
||||
if (!expecting || expecting > MAX_FRAME) { fail('The printer sent something Printhive couldn’t read as a picture.'); return; }
|
||||
}
|
||||
if (pending.length < expecting) return;
|
||||
const frame = pending.subarray(0, expecting);
|
||||
pending = pending.subarray(expecting);
|
||||
expecting = null;
|
||||
// A frame always starts with the JPEG marker; anything else means trouble.
|
||||
if (frame[0] !== 0xff || frame[1] !== 0xd8) { fail('The printer sent something Printhive couldn’t read as a picture.'); return; }
|
||||
onFrame(frame);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
socket.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
// Connects, waits for one frame, and says what came back. Used to check a
|
||||
// printer's camera without opening a viewer.
|
||||
function testCamera({ host, accessCode, waitMs = 10000 }) {
|
||||
return new Promise((resolve) => {
|
||||
const started = Date.now();
|
||||
let stop = () => {};
|
||||
let done = false;
|
||||
const finish = (result) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
stop();
|
||||
resolve({ ...result, ms: Date.now() - started });
|
||||
};
|
||||
const timer = setTimeout(() => finish({ ok: false, error: 'No picture arrived within ten seconds.' }), waitMs);
|
||||
stop = watch({
|
||||
host,
|
||||
accessCode,
|
||||
onFrame: (frame) => finish({ ok: true, bytes: frame.length, ...(jpegSize(frame) || {}) }),
|
||||
onError: (error) => finish({ ok: false, error }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { watch, testCamera, jpegSize, hello, PORT };
|
||||
12
server.js
12
server.js
|
|
@ -35,7 +35,6 @@ const printers = require('./lib/printers');
|
|||
const spoolSync = require('./lib/spoolSync');
|
||||
|
||||
const printerLive = require('./lib/printerLive');
|
||||
const printerCamera = require('./lib/printerCamera');
|
||||
|
||||
// What a printer has loaded → its card and the Filament page.
|
||||
async function syncTrays(printerId, trays) {
|
||||
|
|
@ -377,17 +376,6 @@ app.delete('/api/printers/:id', async (req, res, next) => {
|
|||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// Tries the printer's chamber camera and says whether a picture arrived.
|
||||
// P1 and A1 series only, and the printer needs "LAN Mode Liveview" switched on.
|
||||
app.post('/api/printers/:id/camera/test', async (req, res, next) => {
|
||||
try {
|
||||
const p = await printers.get(Number(req.params.id), { withCode: true });
|
||||
if (!p) return res.status(404).json({ error: 'No such printer' });
|
||||
if (!p.host || !p.access_code) return res.json({ ok: false, error: 'Fill in the printer’s address and access code first.' });
|
||||
res.json(await printerCamera.testCamera({ host: p.host, accessCode: p.access_code }));
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// What every printer is doing right now, from the live connections.
|
||||
app.get('/api/printers/live', (req, res) => {
|
||||
res.set('Cache-Control', 'no-store');
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { hello, jpegSize } = require('../lib/printerCamera');
|
||||
|
||||
test('the camera hello carries the username and code in the right places', () => {
|
||||
const buf = hello('12345678');
|
||||
assert.equal(buf.length, 80);
|
||||
assert.equal(buf.readUInt32LE(0), 0x40);
|
||||
assert.equal(buf.readUInt32LE(4), 0x3000);
|
||||
assert.equal(buf.subarray(16, 20).toString('ascii'), 'bblp');
|
||||
assert.equal(buf.subarray(16, 48).toString('ascii').replace(/\0+$/, ''), 'bblp');
|
||||
assert.equal(buf.subarray(48, 80).toString('ascii').replace(/\0+$/, ''), '12345678');
|
||||
});
|
||||
|
||||
test('a frame’s size is read from the JPEG itself', () => {
|
||||
// A JPEG header: start marker, a comment segment, then the frame header.
|
||||
const jpeg = Buffer.concat([
|
||||
Buffer.from([0xff, 0xd8]),
|
||||
Buffer.from([0xff, 0xfe, 0x00, 0x04, 0x61, 0x62]), // comment, skipped
|
||||
Buffer.from([0xff, 0xc0, 0x00, 0x11, 0x08, 0x04, 0x38, 0x07, 0x80]), // 1080 × 1920
|
||||
]);
|
||||
assert.deepEqual(jpegSize(jpeg), { height: 1080, width: 1920 });
|
||||
assert.equal(jpegSize(Buffer.from([0xff, 0xd8, 0x00])), null);
|
||||
});
|
||||
Loading…
Reference in a new issue