From c6e58f77465b334cc1f81e12a5f4b7056c723105 Mon Sep 17 00:00:00 2001 From: Jonathan Dahan Date: Wed, 4 Oct 2023 16:59:15 -0400 Subject: [PATCH] Add brushless hardware support --- src/cli.ts | 87 ++++++++++++------------- src/ebb.ts | 18 +++-- src/massager.ts | 32 ++++----- src/planning.ts | 170 ++++++++++++++++++++++++++++++------------------ src/server.ts | 87 +++++++++++++------------ src/ui.tsx | 102 ++++++++++++++++++++--------- 6 files changed, 292 insertions(+), 204 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 52d1c656..20dc989e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,57 +8,28 @@ import { Vec2 } from "./vec"; import { formatDuration } from "./util"; import { PlanOptions, defaultPlanOptions } from "./planning"; import { PaperSize } from "./paper-size"; +import { Hardware } from "./ebb"; -function parseSvg(svg: string) { - const window = new Window +function parseSvg (svg: string) { + const window = new Window() window.document.documentElement.innerHTML = svg return window.document.documentElement } -export function cli(argv: string[]): void { - yargs.strict() - .option("device", { - alias: "d", - describe: "device to connect to", - type: "string" +export function cli (argv: string[]): void { + yargs + .strict() + .option('hardware', { + describe: 'select hardware type', + choices: ['v3', 'brushless'] as const, + default: 'v3', + coerce: value => value as Hardware + }) + .option('device', { + alias: 'd', + describe: 'device to connect to', + type: 'string' }) - .command('$0', 'run the saxi web server', - yargs => yargs - .option("port", { - alias: "p", - default: Number(process.env.PORT || 9080), - describe: "TCP port on which to listen", - type: "number" - }) - .option("enable-cors", { - describe: "enable cross-origin resource sharing (CORS)", - type: "boolean" - }) - .option("max-payload-size", { - describe: "maximum payload size to accept", - default: "200 mb", - type: "string" - }) - .option("firmware-version", { - describe: "print the device's firmware version and exit", - type: "boolean" - }), - args => { - if (args["firmware-version"]) { - connectEBB(args.device).then(async (ebb) => { - if (!ebb) { - console.error(`No EBB connected`); - return process.exit(1); - } - const fwv = await ebb.firmwareVersion(); - console.log(fwv); - await ebb.close(); - }); - } else { - startServer(args.port, args.device, args["enable-cors"], args["max-payload-size"]); - } - } - ) .command('plot ', 'plot an svg, then exit', yargs => yargs .positional("file", { @@ -205,6 +176,7 @@ export function cli(argv: string[]): void { const planOptions: PlanOptions = { paperSize, marginMm: args.margin, + hardware: args.hardware, selectedGroupLayers: new Set([]), // TODO selectedStrokeLayers: new Set([]), // TODO @@ -234,7 +206,7 @@ export function cli(argv: string[]): void { const p = replan(linesToVecs(lines), planOptions) console.log(`${p.motions.length} motions, estimated duration: ${formatDuration(p.duration())}`) console.log("connecting to plotter...") - const ebb = await connectEBB(args.device) + const ebb = await connectEBB(args.hardware, args.device) if (!ebb) { console.error("Couldn't connect to device!") process.exit(1) @@ -246,7 +218,28 @@ export function cli(argv: string[]): void { await ebb.close() } ) - .parse(argv); + .command('$0', 'run the saxi web server', + args => args + .option('port', { + alias: 'p', + describe: 'TCP port on which to listen', + default: 9080, + type: 'number', + }) + .option('enable-cors', { + describe: 'enable cross-origin resource sharing (CORS)', + default: false, + type: 'boolean', + }) + .option('max-payload-size', { + describe: 'maximum payload size to accept', + default: '200mb', + }), + args => { + startServer(args.hardware, args.device, args.port, args['enable-cors'], args['max-payload-size']) + } + ) + .parse(argv) } function linesToVecs(lines: any[]): Vec2[][] { diff --git a/src/ebb.ts b/src/ebb.ts index f138d5b6..5f62074c 100644 --- a/src/ebb.ts +++ b/src/ebb.ts @@ -9,11 +9,14 @@ function modf(d: number): [number, number] { return [fracPart, intPart]; } +export type Hardware = 'v3' | 'brushless' + export class EBB { public port: SerialPort; private commandQueue: Iterator[]; private writer: WritableStreamDefaultWriter; private readableClosed: Promise + public hardware: Hardware private microsteppingMode = 0; @@ -22,10 +25,11 @@ export class EBB { private cachedFirmwareVersion: [number, number, number] | undefined = undefined; - public constructor(port: SerialPort) { - this.port = port; - this.writer = this.port.writable.getWriter(); - this.commandQueue = []; + public constructor (port: SerialPort, hardware: Hardware = 'v3') { + this.hardware = hardware + this.port = port + this.writer = this.port.writable.getWriter() + this.commandQueue = [] this.readableClosed = port.readable .pipeThrough(new RegexParser({ regex: /[\r\n]+/ })) .pipeTo(new WritableStream({ @@ -155,8 +159,10 @@ export class EBB { await this.command(`SR,${(timeout * 1000) | 0}${power != null ? `,${power ? 1 : 0}` : ''}`) } - public setPenHeight(height: number, rate: number, delay = 0): Promise { - return this.command(`S2,${height},4,${rate},${delay}`); + // https://evil-mad.github.io/EggBot/ebb.html#S2 General RC Servo Output + public async setPenHeight (height: number, rate: number, delay = 0): Promise { + const output_pin = this.hardware === 'v3' ? 4 : 5 + return await this.command(`S2,${height},${output_pin},${rate},${delay}`) } public lowlevelMove( diff --git a/src/massager.ts b/src/massager.ts index 3cb5f727..c7e382f3 100644 --- a/src/massager.ts +++ b/src/massager.ts @@ -1,6 +1,5 @@ import * as Optimization from "optimize-paths"; -import * as Planning from "./planning"; -import {Device, Plan, PlanOptions} from "./planning"; +import {Device, Plan, PlanOptions, plan} from "./planning"; import {dedupPoints, scaleToPaper, cropToMargins} from "./util"; import {Vec2, vmul, vrot} from "./vec"; @@ -10,8 +9,9 @@ const svgUnitsPerInch = 96 const mmPerInch = 25.4 const mmPerSvgUnit = mmPerInch / svgUnitsPerInch -export function replan(inPaths: Vec2[][], planOptions: PlanOptions): Plan { - let paths = inPaths; +export function replan (inPaths: Vec2[][], planOptions: PlanOptions): Plan { + let paths = inPaths + const device = Device(planOptions.hardware) // Rotate drawing around center of paper to handle plotting portrait drawings // along y-axis of plotter @@ -71,27 +71,27 @@ export function replan(inPaths: Vec2[][], planOptions: PlanOptions): Plan { } // Convert the paths to units of "steps". - paths = paths.map((ps) => ps.map((p) => vmul(p, Device.Axidraw.stepsPerMm))); + paths = paths.map((ps) => ps.map((p) => vmul(p, device.stepsPerMm))) // And finally, motion planning. - console.time("planning pen motions"); - const plan = Planning.plan(paths, { - penUpPos: Device.Axidraw.penPctToPos(planOptions.penUpHeight), - penDownPos: Device.Axidraw.penPctToPos(planOptions.penDownHeight), + console.time('planning pen motions') + const theplan = plan(paths, { + penUpPos: device.penPctToPos(planOptions.penUpHeight), + penDownPos: device.penPctToPos(planOptions.penDownHeight), penDownProfile: { - acceleration: planOptions.penDownAcceleration * Device.Axidraw.stepsPerMm, - maximumVelocity: planOptions.penDownMaxVelocity * Device.Axidraw.stepsPerMm, - corneringFactor: planOptions.penDownCorneringFactor * Device.Axidraw.stepsPerMm, + acceleration: planOptions.penDownAcceleration * device.stepsPerMm, + maximumVelocity: planOptions.penDownMaxVelocity * device.stepsPerMm, + corneringFactor: planOptions.penDownCorneringFactor * device.stepsPerMm }, penUpProfile: { - acceleration: planOptions.penUpAcceleration * Device.Axidraw.stepsPerMm, - maximumVelocity: planOptions.penUpMaxVelocity * Device.Axidraw.stepsPerMm, - corneringFactor: 0, + acceleration: planOptions.penUpAcceleration * device.stepsPerMm, + maximumVelocity: planOptions.penUpMaxVelocity * device.stepsPerMm, + corneringFactor: 0 }, penDropDuration: planOptions.penDropDuration, penLiftDuration: planOptions.penLiftDuration, }); console.timeEnd("planning pen motions"); - return plan; + return theplan } diff --git a/src/planning.ts b/src/planning.ts index 47cdfbc9..12bef29e 100644 --- a/src/planning.ts +++ b/src/planning.ts @@ -1,9 +1,8 @@ -/** - * Cribbed from https://github.com/fogleman/axi/blob/master/axi/planner.py - */ -const epsilon = 1e-9; -import {PaperSize} from "./paper-size"; -import {vadd, vdot, Vec2, vlen, vmul, vnorm, vsub} from "./vec"; +// Cribbed from https://github.com/fogleman/axi/blob/master/axi/planner.py +import { Hardware } from './ebb' +import { PaperSize } from './paper-size' +import { vadd, vdot, Vec2, vlen, vmul, vnorm, vsub } from './vec' +const epsilon = 1e-9 export interface PlanOptions { paperSize: PaperSize; @@ -33,6 +32,7 @@ export interface PlanOptions { cropToMargins: boolean; minimumPathLength: number; + hardware: Hardware } export const defaultPlanOptions: PlanOptions = { @@ -62,7 +62,9 @@ export const defaultPlanOptions: PlanOptions = { cropToMargins: true, minimumPathLength: 0, -}; + + hardware: 'v3' +} interface Instant { t: number; @@ -87,39 +89,79 @@ interface ToolingProfile { penDropDuration: number; } -export const Device = { - Axidraw: { - stepsPerMm: 5, +export const Device = (hardware = 'v3'): Device => { + if (hardware === 'brushless') return AxidrawBrushless + return Axidraw +} - // Practical min/max that you might ever want the pen servo to go on the AxiDraw (v2) - // Units: 83ns resolution pwm output. - // Defaults: penup at 12000 (1ms), pendown at 16000 (1.33ms). - penServoMin: 7500, // pen down - penServoMax: 28000, // pen up +export interface Device { + stepsPerMm: number + // Practical min/max that you might ever want the pen servo to go on the AxiDraw + // Units: 83ns resolution pwm output. + penServoMin: number // pen down + penServoMax: number // pen up + penPctToPos: (pct: number) => number +} - penPctToPos(pct: number): number { - const t = pct / 100.0; - return Math.round(this.penServoMin * t + this.penServoMax * (1 - t)); - } +// Defaults: penup at 12000 (1ms), pendown at 16000 (1.33ms). +const Axidraw: Device = { + stepsPerMm: 5, + + penServoMin: 7500, // pen down + penServoMax: 28000, // pen up + + penPctToPos (pct: number): number { + const t = pct / 100.0 + return Math.round(this.penServoMin * t + this.penServoMax * (1 - t)) } -}; +} + +// brushless servo (https://shop.evilmadscientist.com/productsmenu/else?id=56) +const AxidrawBrushless: Device = { + stepsPerMm: 5, + + penServoMin: 5400, // pen down + penServoMax: 12600, // pen up + + penPctToPos (pct: number): number { + const t = pct / 100.0 + return Math.round(this.penServoMin * t + this.penServoMax * (1 - t)) + } +} export const AxidrawFast: ToolingProfile = { penDownProfile: { - acceleration: 200 * Device.Axidraw.stepsPerMm, - maximumVelocity: 50 * Device.Axidraw.stepsPerMm, - corneringFactor: 0.127 * Device.Axidraw.stepsPerMm + acceleration: 200 * Axidraw.stepsPerMm, + maximumVelocity: 50 * Axidraw.stepsPerMm, + corneringFactor: 0.127 * Axidraw.stepsPerMm }, penUpProfile: { - acceleration: 400 * Device.Axidraw.stepsPerMm, - maximumVelocity: 200 * Device.Axidraw.stepsPerMm, + acceleration: 400 * Axidraw.stepsPerMm, + maximumVelocity: 200 * Axidraw.stepsPerMm, corneringFactor: 0 }, - penUpPos: Device.Axidraw.penPctToPos(50), - penDownPos: Device.Axidraw.penPctToPos(60), + penUpPos: Axidraw.penPctToPos(50), + penDownPos: Axidraw.penPctToPos(60), penDropDuration: 0.12, - penLiftDuration: 0.12, -}; + penLiftDuration: 0.12 +} + +export const AxidrawBrushlessFast: ToolingProfile = { + penDownProfile: { + acceleration: 200 * AxidrawBrushless.stepsPerMm, + maximumVelocity: 50 * AxidrawBrushless.stepsPerMm, + corneringFactor: 0.127 * AxidrawBrushless.stepsPerMm + }, + penUpProfile: { + acceleration: 400 * AxidrawBrushless.stepsPerMm, + maximumVelocity: 200 * AxidrawBrushless.stepsPerMm, + corneringFactor: 0 + }, + penUpPos: AxidrawBrushless.penPctToPos(50), + penDownPos: AxidrawBrushless.penPctToPos(60), + penDropDuration: 0.08, + penLiftDuration: 0.08 +} export class Block { public static deserialize(o: any): Block { @@ -271,11 +313,14 @@ export class Plan { case "XYMotion": return XYMotion.deserialize(m); case "PenMotion": return PenMotion.deserialize(m); } - })); + }), o.minPenPosition) } - public motions: Motion[]; - public constructor(motions: Motion[]) { - this.motions = motions; + + private readonly minPenPosition: number + public motions: Motion[] + public constructor (motions: Motion[], minPenPosition: number) { + this.motions = motions + this.minPenPosition = minPenPosition } public duration(start = 0): number { return this.motions.slice(start).map((m) => m.duration()).reduce((a, b) => a + b, 0); @@ -288,25 +333,24 @@ export class Plan { if (motion instanceof XYMotion) { return motion; } else if (motion instanceof PenMotion) { - // Uuuugh this is really hacky. We should instead store the - // pen-up/pen-down heights in a single place and reference them from - // the PenMotions. Then we can change them in just one place. - if (j === this.motions.length - 3) { - return new PenMotion(penDownHeight, Device.Axidraw.penPctToPos(0), motion.duration()); - } else if (j === this.motions.length - 1) { - return new PenMotion(Device.Axidraw.penPctToPos(0), penUpHeight, motion.duration()); + // TODO: Remove this hack by storing the pen-up/pen-down heights + // in a single place, and reference them from the PenMotions. + if (j === this.motions.length - 1) { + return new PenMotion(this.minPenPosition, penUpHeight, motion.duration()) } return (penMotionIndex++ % 2 === 0 ? new PenMotion(penUpHeight, penDownHeight, motion.duration()) : new PenMotion(penDownHeight, penUpHeight, motion.duration())); } - })); + // TODO: CHECK THAT Plan() doesn't overwrite the above motions with this.minPenPosition + }), this.minPenPosition) } public serialize(): any { return { - motions: this.motions.map((m) => m.serialize()) - }; + motions: this.motions.map((m) => m.serialize()), + minPenPosition: this.minPenPosition + } } } @@ -561,24 +605,24 @@ export function plan( paths: Vec2[][], profile: ToolingProfile ): Plan { - const motions: Motion[] = []; - let curPos = { x: 0, y: 0 }; - const penMaxUpPos = profile.penUpPos < profile.penDownPos ? 100 : 0 - // for each path: move to the initial point, put the pen down, draw the path, - // then pick the pen up. - paths.forEach((p, i) => { - const m = constantAccelerationPlan(p, profile.penDownProfile); - const penUpPos = i === paths.length - 1 ? Device.Axidraw.penPctToPos(penMaxUpPos) : profile.penUpPos; - motions.push( - constantAccelerationPlan([curPos, m.p1], profile.penUpProfile), - new PenMotion(profile.penUpPos, profile.penDownPos, profile.penDropDuration), - m, - new PenMotion(profile.penDownPos, penUpPos, profile.penLiftDuration) - ); - curPos = m.p2; - }); - // finally, move back to (0, 0). - motions.push(constantAccelerationPlan([curPos, {x: 0, y: 0}], profile.penUpProfile)); - motions.push(new PenMotion(Device.Axidraw.penPctToPos(penMaxUpPos), profile.penUpPos, profile.penDropDuration)); - return new Plan(motions); + const motions: Motion[] = [] + let curPos = { x: 0, y: 0 } + + const penMotions = { + up: new PenMotion(profile.penDownPos, profile.penUpPos, profile.penLiftDuration), + down: new PenMotion(profile.penUpPos, profile.penDownPos, profile.penDropDuration) + } + + // For each path - move to the initial position, put the pen down, draw the path, bring pen up + paths.forEach(path => { + const motion = constantAccelerationPlan(path, profile.penDownProfile) + const position = constantAccelerationPlan([curPos, motion.p1], profile.penUpProfile) + + motions.push(position, penMotions.down, motion, penMotions.up) + curPos = motion.p2 + }) + + // Move to {x: 0, y: 0} + motions.push(constantAccelerationPlan([curPos, { x: 0, y: 0 }], profile.penUpProfile)) + return new Plan(motions, profile.penDownPos) } diff --git a/src/server.ts b/src/server.ts index 0d8029bb..099a22a4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,17 +7,23 @@ import { PortInfo } from "@serialport/bindings-interface" import { WakeLock } from "wake-lock"; import WebSocket from "ws"; import { SerialPortSerialPort } from "./serialport-serialport"; -import { EBB } from "./ebb"; import { Device, PenMotion, Motion, Plan } from "./planning"; import { formatDuration } from "./util"; import { autoDetect } from '@serialport/bindings-cpp'; -import * as self from './server' // for mocking +import * as _self from './server' // use self-import for test mocking -export function startServer(port: number, device: string | null = null, enableCors = false, maxPayloadSize = "200mb"): Promise { - const app = express(); +import { EBB, Hardware } from './ebb' - app.use("/", express.static(path.join(__dirname, "..", "ui"))); - app.use(express.json({limit: maxPayloadSize})); +type Com = string + +const getDeviceInfo = (ebb: EBB | null, com: Com) => { + return { com: ebb ? com : null, hardware: ebb?.hardware } +} + +export async function startServer (hardware: Hardware, com: Com, port: number, enableCors = false, maxPayloadSize = '200mb') { + const app = express() + app.use('/', express.static(path.join(__dirname, '..', 'ui'))) + app.use(express.json({ limit: maxPayloadSize })) if (enableCors) { app.use(cors()); } @@ -58,8 +64,9 @@ export function startServer(port: number, device: string | null = null, enableCo } }); - ws.send(JSON.stringify({c: "dev", p: {path: ebb ? /*ebb.port.path*/"/dev/XXX" : null}})); - ws.send(JSON.stringify({c: "pause", p: {paused: !!unpaused}})); + ws.send(JSON.stringify({ c: 'dev', p: getDeviceInfo(ebb, com) })) + + ws.send(JSON.stringify({ c: 'pause', p: { paused: !!unpaused } })) if (motionIdx != null) { ws.send(JSON.stringify({c: "progress", p: {motionIdx}})); } @@ -160,13 +167,14 @@ export function startServer(port: number, device: string | null = null, enableCo await ebb.executeMotion(motion); }, async postCancel(): Promise { - await ebb.setPenHeight(Device.Axidraw.penPctToPos(0), 1000); + const device = Device(ebb.hardware) + await ebb.setPenHeight(device.penPctToPos(0), 1000); }, async postPlot(): Promise { await ebb.waitUntilMotorsIdle(); await ebb.disableMotors(); - }, - }; + } + } const simPlotter: Plotter = { // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -222,10 +230,11 @@ export function startServer(port: number, device: string | null = null, enableCo return new Promise((resolve) => { server.listen(port, () => { - async function connect() { - for await (const d of ebbs(device)) { - ebb = d; - broadcast({c: "dev", p: {path: ebb ? /*ebb.port.path*/"/dev/XXX" : null}}); + async function connect () { + const devices = ebbs(com, hardware) + for await (const device of devices) { + ebb = device + broadcast({ c: 'dev', p: getDeviceInfo(ebb, com) }) } } connect(); @@ -237,9 +246,9 @@ export function startServer(port: number, device: string | null = null, enableCo }); } -async function tryOpen(path: string): Promise { - const port = new SerialPortSerialPort(path); - await port.open({baudRate: 9600}) +async function tryOpen (com: Com) { + const port = new SerialPortSerialPort(com) + await port.open({ baudRate: 9600 }) return port } @@ -257,8 +266,8 @@ async function listEBBs() { return ports.filter(isEBB).map((p: { path: any; }) => p.path); } -export async function waitForEbb() { - // eslint-disable-next-line no-constant-condition +export async function waitForEbb (): Promise { +// eslint-disable-next-line no-constant-condition while (true) { const ebbs = await listEBBs(); if (ebbs.length) { @@ -268,19 +277,19 @@ export async function waitForEbb() { } } -async function* ebbs(path?: string) { +async function * ebbs (path?: string, hardware: Hardware = 'v3') { while (true) { try { - const com = path || (await self.waitForEbb()); // use self-import for mocking - console.log(`Found EBB at ${com}`); - const port = await tryOpen(com); + const com: Com = path || (await _self.waitForEbb()) // use self-import for test mocking + console.log(`Found EBB at ${com}`) + const port = await tryOpen(com) const closed = new Promise((resolve) => { port.addEventListener('disconnect', resolve, { once: true }) - }); - yield new EBB(port); - await closed; - yield null; - console.error(`Lost connection to EBB, reconnecting...`); + }) + yield new EBB(port, hardware) + await closed + yield null + console.error('Lost connection to EBB, reconnecting...') } catch (e) { console.error(`Error connecting to EBB: ${e.message}`); console.error(`Retrying in 5 seconds...`); @@ -289,17 +298,13 @@ async function* ebbs(path?: string) { } } -export async function connectEBB(path: string | undefined): Promise { - if (path) { - const port = await tryOpen(path); - return new EBB(port); - } else { - const ebbs = await listEBBs(); - if (ebbs.length) { - const port = await tryOpen(ebbs[0]); - return new EBB(port); - } else { - return null; - } +export async function connectEBB (hardware: Hardware = 'v3', device: string | undefined): Promise { + if (!device) { + const ebbs = await listEBBs() + if (ebbs.length === 0) return null + device = ebbs[0] } + + const port = await tryOpen(device) + return new EBB(port, hardware) } diff --git a/src/ui.tsx b/src/ui.tsx index 2eb20a40..ee83d59f 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -17,7 +17,7 @@ import "./style.css"; import pathJoinRadiusIcon from "./icons/path-joining radius.svg"; import pointJoinRadiusIcon from "./icons/point-joining radius.svg"; import rotateDrawingIcon from "./icons/rotate-drawing.svg"; -import { EBB } from "./ebb"; +import { EBB, Hardware } from "./ebb"; const defaultVisualizationOptions = { penStrokeWidth: 0.5, @@ -48,13 +48,16 @@ const initialState = { }; // Update the initial state with previously persisted settings (if present) -const persistedPlanOptions = JSON.parse(window.localStorage.getItem("planOptions")) || {}; -initialState.planOptions = {...initialState.planOptions, ...persistedPlanOptions}; + +const persistedPlanOptions = JSON.parse(window.localStorage.getItem("planOptions") ?? "{}"); +initialState.planOptions = { ...initialState.planOptions, ...persistedPlanOptions }; initialState.planOptions.paperSize = new PaperSize(initialState.planOptions.paperSize.size); type State = typeof initialState; -const DispatchContext = React.createContext(null); +type Dispatcher = React.Dispatch<{ type: string; value: Record }>; +const nullDispatch: Dispatcher = () => null; +const DispatchContext = React.createContext(nullDispatch); function reducer(state: State, action: any): State { switch (action.type) { @@ -82,6 +85,7 @@ function reducer(state: State, action: any): State { interface DeviceInfo { path: string; + hardware: Hardware; } interface Driver { @@ -118,7 +122,7 @@ class WebSerialDriver implements Driver { private _signalUnpause: () => void = null; private _cancelRequested = false; - public static async connect(port?: SerialPort) { + public static async connect(port?: SerialPort, hardware: Hardware = 'v3') { if (!port) port = await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x04D8, usbProductId: 0xFD92 }] }) // baudRate ref: https://github.com/evil-mad/plotink/blob/a45739b7d41b74d35c1e933c18949ed44c72de0e/plotink/ebb_serial.py#L281 @@ -127,7 +131,13 @@ class WebSerialDriver implements Driver { // (pyserial defaults to 9600) await port.open({ baudRate: 9600 }) const { usbVendorId, usbProductId } = port.getInfo() - return new WebSerialDriver(new EBB(port), `${usbVendorId.toString(16).padStart(4, '0')}:${usbProductId.toString(16).padStart(4, '0')}`) + const ebb = new EBB(port, hardware) + + const vendorId = usbVendorId?.toString(16).padStart(4, '0') + const productId = usbProductId?.toString(16).padStart(4, '0') + const name = `${vendorId}:${productId}` + + return new WebSerialDriver(ebb, name) } private _name: string @@ -168,7 +178,8 @@ class WebSerialDriver implements Driver { } if (this._cancelRequested) { - await this.ebb.setPenHeight(Device.Axidraw.penPctToPos(0), 1000); + const device = Device(this.ebb.hardware); + await this.ebb.setPenHeight(device.penPctToPos(0), 1000); if (this.oncancelled) this.oncancelled() } else { if (this.onfinished) this.onfinished() @@ -354,10 +365,11 @@ const usePlan = (paths: Vec2[][] | null, planOptions: PlanOptions) => { penDownHeight: previousOptions.penDownHeight, }; if (serialize(previousOptions) === serialize(newOptionsWithOldPenHeights)) { + const device = Device(newOptions.hardware); // The existing plan should be the same except for penup/pendown heights. return previousPlan.withPenHeights( - Device.Axidraw.penPctToPos(newOptions.penUpHeight), - Device.Axidraw.penPctToPos(newOptions.penDownHeight) + device.penPctToPos(newOptions.penUpHeight), + device.penPctToPos(newOptions.penDownHeight) ); } } @@ -371,7 +383,7 @@ const usePlan = (paths: Vec2[][] | null, planOptions: PlanOptions) => { return; } if (lastPlan.current != null && lastPaths.current === paths) { - const rejiggered = attemptRejigger(lastPlanOptions.current, planOptions, lastPlan.current); + const rejiggered = attemptRejigger(lastPlanOptions.current ?? defaultPlanOptions, planOptions, lastPlan.current); if (rejiggered) { setPlan(rejiggered); lastPlan.current = rejiggered; @@ -402,7 +414,7 @@ const usePlan = (paths: Vec2[][] | null, planOptions: PlanOptions) => { }; }, [paths, serialize(planOptions)]); - return [isPlanning, latestPlan, setPlan]; + return { isPlanning, plan: latestPlan, setPlan }; }; const setPaths = (paths: Vec2[][]) => { @@ -418,17 +430,19 @@ const setPaths = (paths: Vec2[][]) => { return {type: "SET_PATHS", paths, groupLayers, strokeLayers, selectedGroupLayers: new Set(groupLayers), selectedStrokeLayers: new Set(strokeLayers), layerMode}; }; -function PenHeight({state, driver}: {state: State; driver: Driver}) { - const {penUpHeight, penDownHeight} = state.planOptions; +function PenHeight({ state, driver }: { state: State; driver: Driver }) { + const { penUpHeight, penDownHeight, hardware } = state.planOptions; const dispatch = useContext(DispatchContext); const setPenUpHeight = (x: number) => dispatch({type: "SET_PLAN_OPTION", value: {penUpHeight: x}}); const setPenDownHeight = (x: number) => dispatch({type: "SET_PLAN_OPTION", value: {penDownHeight: x}}); + const device = Device(hardware); + const penUp = () => { - const height = Device.Axidraw.penPctToPos(penUpHeight); + const height = device.penPctToPos(penUpHeight); driver.setPenHeight(height, 1000); }; const penDown = () => { - const height = Device.Axidraw.penPctToPos(penDownHeight); + const height = device.penPctToPos(penDownHeight); driver.setPenHeight(height, 1000); }; return @@ -455,7 +469,25 @@ function PenHeight({state, driver}: {state: State; driver: Driver}) { ; } -function VisualizationOptions({state}: {state: State}) { +function HardwareOptions({ state }: { state: State }) { + const dispatch = useContext(DispatchContext); + const setHardware = (hardware: string) => dispatch({ + type: "SET_PLAN_OPTION", + value: { hardware } + }); + return
+ +
; +} + +function VisualizationOptions({ state }: { state: State }) { const dispatch = useContext(DispatchContext); return <> @@ -632,7 +664,8 @@ function PlanPreview( } ) { const ps = state.planOptions.paperSize; - const strokeWidth = state.visualizationOptions.penStrokeWidth * Device.Axidraw.stepsPerMm + const device = Device(state.planOptions.hardware); + const strokeWidth = state.visualizationOptions.penStrokeWidth * device.stepsPerMm const colorPathsByStrokeOrder = state.visualizationOptions.colorPathsByStrokeOrder const memoizedPlanPreview = useMemo(() => { if (plan) { @@ -644,7 +677,7 @@ function PlanPreview( return m.blocks.map((b) => b.p1).concat([m.p2]); } else { return []; } }).filter((m) => m.length); - return + return {lines.map((line, i) => ; if (state.progress != null && plan != null) { const motion = plan.motion(state.progress); const pos = motion instanceof XYMotion ? motion.instant(Math.min(microprogress / 1000, motion.duration())).p : (plan.motion(state.progress - 1) as XYMotion).p2; - const {stepsPerMm} = Device.Axidraw; - const posXMm = pos.x / stepsPerMm; - const posYMm = pos.y / stepsPerMm; + const posXMm = pos.x / device.stepsPerMm; + const posYMm = pos.y / device.stepsPerMm; progressIndicator = ; } -function PortSelector({driver, setDriver}: {driver: Driver; setDriver: (d: Driver) => void}) { +type PortSelectorProps = { + driver: Driver | null; + setDriver: (driver: Driver) => void; + hardware: Hardware; +} + +function PortSelector({ driver, setDriver, hardware }: PortSelectorProps) { const [initializing, setInitializing] = useState(false) useEffect(() => { (async () => { try { const ports = await navigator.serial.getPorts() - if (ports.length > 0) { - console.log('connecting to', ports[0]) + const port = ports[0] + if (port) { + console.log('connecting to', port) // get the first - setDriver(await WebSerialDriver.connect(ports[0])) + setDriver(await WebSerialDriver.connect(port, hardware)) } } finally { setInitializing(false) @@ -1026,9 +1065,9 @@ function PortSelector({driver, setDriver}: {driver: Driver; setDriver: (d: Drive onClick={async () => { try { const port = await navigator.serial.requestPort({ filters: [{ usbVendorId: 0x04D8, usbProductId: 0xFD92 }] }) - if (driver) - await driver.close() - setDriver(await WebSerialDriver.connect(port)) + // TODO: describe why we close if we already checked that driver is null + // await driver?.close() + setDriver(await WebSerialDriver.connect(port, hardware)) } catch (e) { alert(`Failed to connect to serial device: ${e.message}`) console.error(e) @@ -1047,7 +1086,7 @@ function Root() { IS_WEB ? null as Driver | null : SaxiDriver.connect() ) const [state, dispatch] = useReducer(reducer, initialState); - const [isPlanning, plan, setPlan] = usePlan(state.paths, state.planOptions); + const { isPlanning, plan, setPlan } = usePlan(state.paths, state.planOptions); const [isLoadingFile, setIsLoadingFile] = useState(false); useEffect(() => { @@ -1134,12 +1173,13 @@ function Root() {
saxi
- {IS_WEB ? : null} + {IS_WEB ? : null} {!state.connected ?
disconnected
: null}
pen
+
paper