2021-10-28 02:53:12 -04:00
|
|
|
import * as WebSocket from 'ws';
|
|
|
|
import fetch from 'node-fetch';
|
2021-11-08 20:17:42 -05:00
|
|
|
import * as Mp4Frag from 'mp4frag';
|
2021-10-28 02:53:12 -04:00
|
|
|
|
2021-11-08 20:17:42 -05:00
|
|
|
import {make_mp4frag} from './camera-stream';
|
2021-11-04 23:28:21 -04:00
|
|
|
import {Message, StatusMessage, SettingsMessage} from '../../types/messages';
|
2021-11-03 18:55:39 -04:00
|
|
|
import * as octoprint from '../../types/octoprint';
|
2021-10-28 02:53:12 -04:00
|
|
|
|
|
|
|
const PING_TIME = 10000;
|
|
|
|
|
|
|
|
type Timeout = ReturnType<typeof setTimeout>;
|
|
|
|
|
|
|
|
export default class OctoprintConnection {
|
|
|
|
public name?: string;
|
2021-11-08 20:17:42 -05:00
|
|
|
public webcamStream?: Mp4Frag;
|
2021-11-04 23:28:21 -04:00
|
|
|
protected lastStatus?: StatusMessage;
|
|
|
|
protected settingsMessage?: SettingsMessage;
|
2021-10-28 02:53:12 -04:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
public slug: string,
|
|
|
|
public address: string,
|
|
|
|
protected apikey: string,
|
2021-11-04 23:28:21 -04:00
|
|
|
protected broadcast: (msg: Message) => void
|
2021-10-28 02:53:12 -04:00
|
|
|
) {
|
|
|
|
this.try_connect_websocket();
|
|
|
|
}
|
|
|
|
|
|
|
|
try_connect_websocket() {
|
|
|
|
this.connect_websocket().catch((e) => {
|
|
|
|
console.error(
|
|
|
|
`Failed to connect to "${this.slug}", attempting reconnection in 5 seconds`
|
|
|
|
);
|
|
|
|
console.error(e);
|
|
|
|
setTimeout(() => this.try_connect_websocket(), 5000);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async connect_websocket() {
|
|
|
|
const settings = await this.api_get('settings');
|
2021-11-03 19:36:25 -04:00
|
|
|
const webcamURL = new URL(settings.webcam.streamUrl, this.address);
|
|
|
|
// TODO: handle recreating proxy on URL change
|
2021-11-08 20:17:42 -05:00
|
|
|
if (this.webcamStream === undefined) {
|
|
|
|
this.webcamStream = make_mp4frag(this.slug, webcamURL);
|
2021-11-03 19:36:25 -04:00
|
|
|
}
|
2021-11-04 23:28:21 -04:00
|
|
|
this.settingsMessage = {
|
|
|
|
kind: "settings",
|
|
|
|
printer: this.slug,
|
|
|
|
name: settings.appearance.name,
|
|
|
|
color: settings.appearance.color,
|
|
|
|
webcam: {
|
|
|
|
flipH: settings.webcam.flipH,
|
|
|
|
flipV: settings.webcam.flipV,
|
|
|
|
rotate90: settings.webcam.rotate90,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.broadcast(this.settingsMessage);
|
2021-10-28 02:53:12 -04:00
|
|
|
|
|
|
|
// do passive login to get a session key from the API key
|
|
|
|
const login: octoprint.LoginResponse = await this.api_post('login', {
|
|
|
|
passive: 'true',
|
|
|
|
});
|
|
|
|
const session_key = login.name + ':' + login.session;
|
|
|
|
|
|
|
|
let pingSender: ReturnType<typeof setInterval>;
|
|
|
|
let pongTimeout: Timeout;
|
|
|
|
|
|
|
|
const url = new URL('/sockjs/websocket', this.address);
|
|
|
|
url.protocol = 'ws';
|
|
|
|
let websocket = new WebSocket(url.toString());
|
|
|
|
websocket
|
|
|
|
.on('open', () => {
|
|
|
|
pingSender = setInterval(() => websocket.ping(), PING_TIME);
|
|
|
|
pongTimeout = this.heartbeat(websocket, pongTimeout);
|
|
|
|
|
|
|
|
console.log(`Connected to "${this.slug}"`);
|
|
|
|
websocket.send(JSON.stringify({ auth: session_key }));
|
|
|
|
})
|
|
|
|
.on('message', (data: WebSocket.Data) => {
|
|
|
|
const event: octoprint.Message = JSON.parse(data as string);
|
|
|
|
|
2021-11-04 23:28:21 -04:00
|
|
|
let ext_event: StatusMessage = {
|
|
|
|
kind: "status",
|
2021-10-28 02:53:12 -04:00
|
|
|
printer: this.slug,
|
2021-11-04 23:28:21 -04:00
|
|
|
msg: event,
|
2021-10-28 02:53:12 -04:00
|
|
|
};
|
|
|
|
this.broadcast(ext_event);
|
|
|
|
|
|
|
|
if ('current' in event || 'history' in event) {
|
|
|
|
this.lastStatus = ext_event;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.on('pong', () => {
|
|
|
|
pongTimeout = this.heartbeat(websocket, pongTimeout);
|
|
|
|
})
|
|
|
|
.on('close', () => {
|
|
|
|
clearInterval(pingSender);
|
|
|
|
clearTimeout(pongTimeout);
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
`Lost connection to "${this.slug}", attempting reconnection in 5 seconds`
|
|
|
|
);
|
|
|
|
setTimeout(() => this.try_connect_websocket(), 5000);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
heartbeat(websocket: WebSocket, pongTimeout: Timeout): Timeout {
|
|
|
|
clearTimeout(pongTimeout);
|
|
|
|
return setTimeout(() => {
|
|
|
|
console.log(`Missed 2 heartbeats for "${this.slug}", disconnecting`);
|
|
|
|
websocket.terminate();
|
|
|
|
}, PING_TIME * 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
async api_get(endpoint: string): Promise<any> {
|
|
|
|
const r = await fetch(new URL('/api/' + endpoint, this.address), {
|
|
|
|
headers: { 'X-Api-Key': this.apikey },
|
|
|
|
});
|
|
|
|
return await r.json();
|
|
|
|
}
|
|
|
|
|
|
|
|
async api_post(endpoint: string, data: any): Promise<any> {
|
|
|
|
const r = await fetch(new URL('/api/' + endpoint, this.address), {
|
|
|
|
headers: {
|
|
|
|
'X-Api-Key': this.apikey,
|
|
|
|
Accept: 'application/json',
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
|
|
|
method: 'POST',
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
});
|
|
|
|
return await r.json();
|
|
|
|
}
|
|
|
|
|
|
|
|
send_init(ws: WebSocket) {
|
2021-11-04 23:28:21 -04:00
|
|
|
if (this.settingsMessage) {
|
|
|
|
ws.send(JSON.stringify(this.settingsMessage));
|
|
|
|
|
|
|
|
if (this.lastStatus) {
|
|
|
|
ws.send(JSON.stringify(this.lastStatus));
|
|
|
|
}
|
2021-10-28 02:53:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|