Skip to content

Commit

Permalink
feat: WebSerial support
Browse files Browse the repository at this point in the history
Closes #63.
  • Loading branch information
nornagon committed Jul 14, 2021
1 parent 117161d commit 593c451
Show file tree
Hide file tree
Showing 9 changed files with 1,314 additions and 386 deletions.
1,217 changes: 904 additions & 313 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"prestart": "npm run build",
"start": "node cli.js",
"dev": "tsc && webpack --mode=development -w & node cli.js",
"deploy": "rimraf dist/ui && IS_WEB=1 webpack --mode=production && gh-pages -d dist/ui",
"test": "jest"
},
"author": "Jeremy Apthorp <[email protected]>",
Expand All @@ -39,6 +40,7 @@
"@types/react": "^16.7.20",
"@types/react-dom": "^16.0.11",
"@types/serialport": "^8.0.0",
"@types/w3c-web-serial": "^1.0.2",
"@types/ws": "^6.0.1",
"@types/yargs": "^12.0.8",
"@typescript-eslint/eslint-plugin": "^1.5.0",
Expand All @@ -49,6 +51,7 @@
"eslint": "^5.15.3",
"eslint-plugin-react": "^7.12.4",
"file-loader": "^3.0.1",
"gh-pages": "^3.2.3",
"html-webpack-plugin": "^3.2.0",
"jest": "^26.6.3",
"react": "^16.8.0-alpha.1",
Expand All @@ -68,9 +71,10 @@
"express": "^4.16.4",
"flatten-svg": "^0.2.1",
"optimize-paths": "^1.2.0",
"serialport": "^8.0.7",
"serialport": "^9.2.0",
"svgdom": "0.0.21",
"wake-lock": "^0.2.0",
"web-streams-polyfill": "^3.0.3",
"ws": "^7.4.6",
"yargs": "^15.4.1"
},
Expand Down
69 changes: 32 additions & 37 deletions src/ebb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SerialPort from "serialport";

import {Block, Motion, PenMotion, Plan, XYMotion} from "./planning";
import { RegexParser } from "./regex-transform-stream";
import {Vec2, vsub} from "./vec";

/** Split d into its fractional and integral parts */
Expand All @@ -10,20 +9,11 @@ function modf(d: number): [number, number] {
return [fracPart, intPart];
}

function isEBB(p: SerialPort.PortInfo): boolean {
return p.manufacturer === "SchmalzHaus" || p.manufacturer === "SchmalzHaus LLC" || (p.vendorId == "04D8" && p.productId == "FD92");
}

export class EBB {
/** List connected EBBs */
public static async list(): Promise<string[]> {
const ports = await SerialPort.list();
return ports.filter(isEBB).map((p) => p.path);
}

public port: SerialPort;
public parser: SerialPort.parsers.Delimiter;
private commandQueue: Iterator<any, any, Buffer>[];
private writer: WritableStreamDefaultWriter<Uint8Array>;
private readableClosed: Promise<void>

private microsteppingMode: number = 0;

Expand All @@ -34,25 +24,33 @@ export class EBB {

public constructor(port: SerialPort) {
this.port = port;
this.parser = this.port.pipe(new SerialPort.parsers.Regex({ regex: /[\r\n]+/ }));
this.writer = this.port.writable.getWriter()
this.commandQueue = [];
this.parser.on("data", (chunk: Buffer) => {
if (this.commandQueue.length) {
if (chunk[0] === "!".charCodeAt(0)) {
return (this.commandQueue.shift() as any).reject(new Error(chunk.toString("ascii")));
}
try {
const d = this.commandQueue[0].next(chunk);
if (d.done) {
return (this.commandQueue.shift() as any).resolve(d.value);
this.readableClosed = port.readable
.pipeThrough(new RegexParser({ regex: /[\r\n]+/ }))
.pipeTo(new WritableStream({
write: (chunk) => {
if (/^[\r\n]*$/.test(chunk)) return
if (this.commandQueue.length) {
if (chunk[0] === "!".charCodeAt(0)) {
(this.commandQueue.shift() as any).reject(new Error(chunk.toString("ascii")));
return;
}
try {
const d = this.commandQueue[0].next(chunk);
if (d.done) {
(this.commandQueue.shift() as any).resolve(d.value);
return;
}
} catch (e) {
(this.commandQueue.shift() as any).reject(e);
return;
}
} else {
console.log(`unexpected data: ${chunk}`);
}
} catch (e) {
return (this.commandQueue.shift() as any).reject(e);
}
} else {
console.log(`unexpected data: ${chunk}`);
}
});
}))
}

private get stepMultiplier() {
Expand All @@ -67,19 +65,16 @@ export class EBB {
}
}

public close(): Promise<void> {
return new Promise((resolve, reject) => {
this.port.close((err) => {
if (err) { reject(err); } else { resolve(); }
});
});
public async close(): Promise<void> {
throw new Error("TODO")
}

private write(str: string): boolean {
private write(str: string): Promise<void> {
if (process.env.DEBUG_SAXI_COMMANDS) {
console.log(`writing: ${str}`)
}
return this.port.write(str);
const encoder = new TextEncoder()
return this.writer.write(encoder.encode(str))
}

/** Send a raw command to the EBB and expect a single line in return, without an "OK" line to terminate. */
Expand Down
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ declare module 'flatten-svg' {
import main = require('flatten-svg/index');
export = main;
}

declare const IS_WEB: boolean
29 changes: 29 additions & 0 deletions src/regex-transform-stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class RegexParser extends TransformStream {
public constructor(opts: { regex: RegExp }) {
if (opts.regex === undefined) {
throw new TypeError('"options.regex" must be a regular expression pattern or object')
}

if (!(opts.regex instanceof RegExp)) {
opts.regex = new RegExp(opts.regex)
}

const regex = opts.regex
let data = ''
const decoder = new TextDecoder()
super({
transform(chunk, controller) {
const newData = data + decoder.decode(chunk)
const parts = newData.split(regex)
data = parts.pop()
parts.forEach(part => {
controller.enqueue(part)
})
},
flush(controller) {
controller.enqueue(data)
data = ''
}
})
}
}
125 changes: 125 additions & 0 deletions src/serialport-serialport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { EventEmitter } from "events";
import { default as NodeSerialPort } from "serialport";

function readableStreamFromAsyncIterable<T>(iterable: AsyncIterable<T>) {
const it = iterable[Symbol.asyncIterator]();
return new ReadableStream({
async pull(controller) {
const { done, value } = await it.next();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
async cancel(reason) {
await it.throw(reason);
}
}, { highWaterMark: 0 });
}

export class SerialPortSerialPort extends EventEmitter implements SerialPort {
private _path: string;
private _port: NodeSerialPort

public constructor(path: string) {
super()
this._path = path
}

public onconnect: (this: this, ev: Event) => any;
public ondisconnect: (this: this, ev: Event) => any;
public readable: ReadableStream<Uint8Array>;
public writable: WritableStream<Uint8Array>;

public open(options: SerialOptions): Promise<void> {
const opts: NodeSerialPort.OpenOptions = {
baudRate: options.baudRate,
}
if (options.dataBits != null)
opts.dataBits = options.dataBits as any
if (options.stopBits != null)
opts.stopBits = options.stopBits as any
if (options.parity != null)
opts.parity = options.parity

/*
TODO:
bufferSize?: number | undefined;
flowControl?: FlowControlType | undefined;
*/
return new Promise((resolve, reject) => {
this._port = new NodeSerialPort(this._path, opts, (err) => {
this._port.once('close', () => this.emit('disconnect'))
if (err) reject(err)
else {
// Drain the port
while (this._port.read() != null) { /* do nothing */ }
resolve()
}
})
this.readable = readableStreamFromAsyncIterable(this._port)
this.writable = new WritableStream({
write: (chunk) => {
return new Promise((resolve, reject) => {
this._port.write(Buffer.from(chunk), (err, _bytesWritten) => {
if (err) reject(err)
else resolve()
// TODO: check bytesWritten?
})
})
}
})
})
}
public setSignals(signals: SerialOutputSignals): Promise<void> {
return new Promise((resolve, reject) => {
this._port.set({
dtr: signals.dataTerminalReady,
rts: signals.requestToSend,
brk: signals.break
}, (err) => {
if (err) reject(err)
else resolve()
})
})
}
public getSignals(): Promise<SerialInputSignals> {
throw new Error("Method not implemented.");
}
public getInfo(): SerialPortInfo {
throw new Error("Method not implemented.");
}
public close(): Promise<void> {
return new Promise((resolve, reject) => {
this._port.close((err) => {
if (err) reject(err)
else resolve()
})
})
}

public addEventListener(type: "connect" | "disconnect", listener: (this: this, ev: Event) => any, useCapture?: boolean): void;
public addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
public addEventListener(type: any, listener: any, options?: any): void {
if (typeof options === 'object' && options.once) {
this.once(type, listener)
} else {
this.on(type, listener)
}
}

public removeEventListener(type: "connect" | "disconnect", callback: (this: this, ev: Event) => any, useCapture?: boolean): void;
public removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
public removeEventListener(type: any, callback: any, options?: any): void {
if (typeof options === 'object' && options.once) {
this.off(type, callback)
} else {
this.off(type, callback)
}
}

public dispatchEvent(event: Event): boolean {
return this.emit(event.type)
}
}
Loading

0 comments on commit 593c451

Please sign in to comment.