@ralphwetzel/node-red-mcu-plugin
Version:
Plugin to integrate Node-RED MCU Edition into the Node-RED Editor
406 lines (341 loc) • 12.2 kB
JavaScript
class XsbugMessageEx {
constructor(xml) {
xml = xml.documentElement;
if ("xsbug" !== xml.nodeName)
throw new Error("not xsbug xml");
for (let node = xml.firstChild; node; node = node.nextSibling) {
if (XsbugMessageEx[node.nodeName]) {
XsbugMessageEx[node.nodeName](this, node);
} else {
XsbugMessage[node.nodeName](this, node);
}
}
}
static bubble(message, node) {
message.bubble = {
node: node.getAttribute('name'),
data: node.textContent
};
}
}
class XsbugSerial extends XsbugConnection {
#active;
#reader;
#readLoop;
constructor(options = {}) {
super();
this.baud = options.baudRate || 921600;
this.dst = new Uint8Array(32768);
// this.connect();
this.port = options.port
}
reset() {
this.binary = false;
this.dstIndex = 0;
this.currentMachine = undefined;
this.#active = false;
}
async connect(device) {
// https://developer.chrome.com/articles/serial/
let xs = this;
await xs.closeDevice();
async function readUntilClosed(port) {
while (port.readable && xs.#active) {
xs.#reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await xs.#reader.read();
if (done) {
// reader.cancel() has been called.
break;
}
// value is a Uint8Array.
// console.log(value);
xs.usbReceive(value);
}
} catch (error) {
console.log(error.toString());
// Handle error...
} finally {
// Allow the serial port to be closed later.
xs.#reader.releaseLock();
}
}
await port.close();
}
try {
xs.reset();
if (device) {
xs.port = device;
}
if (!xs.port) {
await xs.getDevice();
}
await xs.openDevice();
xs.#active = true;
xs.#readLoop = readUntilClosed(xs.port);
}
catch (e) {
console.log("Connect error: ", e.toString());
}
}
async getDevice() {
this.port = await navigator.serial.requestPort({});
}
async openDevice(baud) {
baud ??= this.baud;
await this.port.open({ baudRate: baud });
}
async closeDevice() {
if (this.#readLoop) {
this.#active = false;
this.#reader.cancel();
await this.#readLoop;
} else {
return Promise.resolve();
}
}
usbReceive(src) {
const mxTagSize = 17;
let dst = this.dst;
let dstIndex = this.dstIndex;
let srcIndex = 0, machine;
while (srcIndex < src.length) {
if (dstIndex === dst.length) { // grow buffer
dst = new Uint8Array(dst.length + 32768);
dst.set(this.dst);
this.dst = dst;
}
dst[dstIndex++] = src[srcIndex++];
if (this.binary) {
if (dstIndex < 2)
this.binaryLength = dst[0] << 8;
else if (2 === dstIndex)
this.binaryLength |= dst[1];
if ((2 + this.binaryLength) === dstIndex) {
this.onReceive(dst.slice(2, 2 + this.binaryLength).buffer);
dstIndex = 0;
this.binary = false;
delete this.binaryLength;
}
}
else if ((dstIndex >= 2) && (dst[dstIndex - 2] == 13) && (dst[dstIndex - 1] == 10)) {
if ((dstIndex >= mxTagSize) && (machine = XsbugSerial.matchProcessingInstruction(dst.subarray(dstIndex - mxTagSize, dstIndex)))) {
if (machine.flag)
this.currentMachine = machine.value;
else
this.currentMachine = undefined;
this.binary = machine.binary;
}
else if ((dstIndex >= 10) && (dst[dstIndex - 10] == '<'.charCodeAt()) &&
(dst[dstIndex - 9] == '/'.charCodeAt()) && (dst[dstIndex - 8] == 'x'.charCodeAt()) &&
(dst[dstIndex - 7] == 's'.charCodeAt()) && (dst[dstIndex - 6] == 'b'.charCodeAt()) &&
(dst[dstIndex - 5] == 'u'.charCodeAt()) && (dst[dstIndex - 4] == 'g'.charCodeAt()) &&
(dst[dstIndex - 3] == '>'.charCodeAt())) {
const message = new TextDecoder().decode(dst.subarray(0, dstIndex));
// console.log(message);
this.onReceive(message);
}
else {
dst[dstIndex - 2] = 0;
//@@ if (offset > 2) fprintf(stderr, "%s\n", self->buffer);
}
dstIndex = 0;
}
}
this.dstIndex = dstIndex;
}
onReceive(data) {
if ("string" === typeof data) {
const msg = new XsbugMessageEx((new DOMParser).parseFromString(data, "application/xml"));
if (msg.break)
this.onBreak(msg);
else if (msg.login)
this.onLogin(msg);
else if (msg.instruments)
this.onInstrumentationConfigure(msg);
else if (msg.local)
this.onLocal(msg);
else if (msg.log)
this.onLog(msg);
else if (msg.samples)
this.onInstrumentationSamples(msg);
else if (msg.bubble)
this.onBubble(msg);
else
debugger; // unhandled
}
else {
const view = new DataView(data);
switch (view.getUint8(0)) {
case 5:
const id = view.getUint16(1), code = view.getInt16(3);
const index = this.pending.findIndex(pending => id === pending.id)
if (index >= 0) {
const pending = this.pending[index];
this.pending.splice(index, 1);
(pending.callback)(code, data.slice(5));
}
break;
default:
debugger;
break;
}
}
}
static matchProcessingInstruction(dst) {
let flag, binary = false, value = 0;
if (dst[0] != '<'.charCodeAt())
return;
if (dst[1] != '?'.charCodeAt())
return;
if (dst[2] != 'x'.charCodeAt())
return;
if (dst[3] != 's'.charCodeAt())
return;
let c = dst[4];
if (c == '.'.charCodeAt())
flag = true;
else if (c == '-'.charCodeAt())
flag = false;
else if (c == '#'.charCodeAt()) {
flag = true;
binary = true;
}
else
return;
for (let i = 0; i < 8; i++) {
c = dst[5 + i]
if (('0'.charCodeAt() <= c) && (c <= '9'.charCodeAt()))
value = (value * 16) + (c - '0'.charCodeAt());
else if (('a'.charCodeAt() <= c) && (c <= 'f'.charCodeAt()))
value = (value * 16) + (10 + c - 'a'.charCodeAt());
else if (('A'.charCodeAt() <= c) && (c <= 'F'.charCodeAt()))
value = (value * 16) + (10 + c - 'A'.charCodeAt());
else
return;
}
if (dst[13] != '?'.charCodeAt())
return;
if (dst[14] != '>'.charCodeAt())
return;
return { value: value.toString(16).padStart(8, "0"), flag, binary };
}
async send(data) {
if ("string" == typeof data) {
const preamble = XsbugConnection.crlf + `<?xs.${this.currentMachine}?>` + XsbugConnection.crlf;
data = new TextEncoder().encode(preamble + data);
// tracePacket("<", data);
await this.#write(data);
}
else {
let preamble = XsbugConnection.crlf + `<?xs#${this.currentMachine}?>`;
preamble = new TextEncoder().encode(preamble);
let payload = new Uint8Array(data);
let buffer = new Uint8Array(preamble.length + 2 + payload.length);
buffer.set(preamble, 0);
buffer[preamble.length] = (payload.length >> 8) & 0xff;
buffer[preamble.length + 1] = payload.length & 0xff;
buffer.set(payload, preamble.length + 2);
// tracePacket("< ", buffer);
await this.#write(buffer);
}
}
async #write(data) {
let port = this.port;
let writer;
let pass;
if (!port)
return Promise.reject(Error("Port is not defined!"));
while (!pass) {
if (!port.writable.locked) {
try {
console.log("#write: @get")
writer = port.writable.getWriter();
console.log("#write: locked")
pass = true;
} catch (err) {
// TypeError: Failed to execute 'getWriter' on 'WritableStream': Cannot create writer when WritableStream is locked
if (err instanceof TypeError == false) {
// sth else failed.
throw(err);
}
}
}
if (!pass)
await this.timeout(100);
}
writer.write(data);
writer.releaseLock();
console.log("#write: released")
}
// async write(data) {
// console.log("@#write")
// let port = this.port;
// if (port) {
// let writer;
// let x = new Promise((resolve, reject) => {
// // let writer;
// let pass;
// while (!pass) {
// if (!port.writable.locked) {
// try {
// console.log("#write: @get")
// // despite .locked == false, getWriter could fail!
// writer = port.writable.getWriter();
// pass = true;
// } catch (err) {
// // TypeError: Failed to execute 'getWriter' on 'WritableStream': Cannot create writer when WritableStream is locked
// if (err instanceof TypeError == false) {
// // sth else failed.
// reject(err);
// }
// }
// } else {
// await this.timeout(100);
// console.log("#write: locked")
// }
// }
// console.log("resolving");
// resolve();
// }).then(() => {
// console.log("#write: writing")
// // data shall be an Uint8Array
// return writer.write(data);
// }).then(() => {
// // Allow the serial port to be closed later.
// writer.releaseLock();
// console.log("#write: released")
// return;
// })
// // return x;
// } else {
// // should we better resolve here?
// return Promise.reject(Error("Port is not defined!"));
// }
// }
async timeout(ms) {
return new Promise((resolve) =>
setTimeout(resolve, ms)
);
}
doSetTime(time, timezoneoffset, dstoffset) {
const payload = new DataView(new ArrayBuffer(12));
payload.setUint32(0, time, false); // big endian
if (!timezoneoffset) {
timezoneoffset = new Date('November 1, 2020 00:00:00').getTimezoneOffset();
timezoneoffset *= -60; // seconds
console.log(timezoneoffset);
}
// this needs to be verified at DST times...
if (!dstoffset) {
let todayoffset = new Date().getTimezoneOffset();
todayoffset *= -60;
dstoffset = timezoneoffset - todayoffset;
console.log(dstoffset);
}
payload.setUint32(4, timezoneoffset, false);
payload.setUint32(8, dstoffset, false);
this.sendBinaryCommand(9, payload);
}
}