Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for brushless servo upgrade #197

Closed
wants to merge 9 commits into from
Prev Previous commit
Next Next commit
Add brushless hardware support
Jonathan Dahan authored and alexrudd2 committed Oct 26, 2023
commit 109649cbc4dd226daf525a951b25ecd3b8ab27c8
11 changes: 5 additions & 6 deletions src/__tests__/planning.test.ts
Original file line number Diff line number Diff line change
@@ -2,11 +2,10 @@ import {Plan, plan, Device, AxidrawFast, XYMotion, PenMotion, defaultPlanOptions
import {Vec2} from '../vec';

describe("plan", () => {
const device = Device.Axidraw
const device = Device()
const positions = {
up: AxidrawFast.penUpPos,
down: AxidrawFast.penDownPos,
zero: device.penPctToPos(0)
down: AxidrawFast.penDownPos
}
it.skip("handles an empty input", () => {
expect(plan([], AxidrawFast)).toEqual(new Plan([]))
@@ -31,7 +30,7 @@ describe("plan", () => {
expect(xyMotions(p)).toEqual([
{from: {x: 0, y: 0}, to: {x: 10, y: 10}, penPos: 0},
{from: {x: 10, y: 10}, to: {x: 10, y: 10}, penPos: positions.down},
{from: {x: 10, y: 10}, to: {x: 0, y: 0}, penPos: positions.zero},
{from: {x: 10, y: 10}, to: {x: 0, y: 0}, penPos: positions.up},
]);
});

@@ -41,7 +40,7 @@ describe("plan", () => {
expect(xyMotions(p)).toEqual([
{from: {x: 0, y: 0}, to: {x: 10, y: 10}, penPos: 0},
{from: {x: 10, y: 10}, to: {x: 20, y: 10}, penPos: positions.down},
{from: {x: 20, y: 10}, to: {x: 0, y: 0}, penPos: positions.zero},
{from: {x: 20, y: 10}, to: {x: 0, y: 0}, penPos: positions.up},
]);
});

@@ -56,7 +55,7 @@ describe("plan", () => {
{from: {x: 10, y: 10}, to: {x: 20, y: 10}, penPos: positions.down},
{from: {x: 20, y: 10}, to: {x: 10, y: 20}, penPos: positions.up},
{from: {x: 10, y: 20}, to: {x: 20, y: 20}, penPos: positions.down},
{from: {x: 20, y: 20}, to: {x: 0, y: 0}, penPos: positions.zero},
{from: {x: 20, y: 20}, to: {x: 0, y: 0}, penPos: positions.up},
]);
});

86 changes: 40 additions & 46 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import { Vec2 } from "./vec";
import { formatDuration } from "./util";
import { Device, PlanOptions, defaultPlanOptions } from "./planning";
import { PaperSize } from "./paper-size";
import { Hardware } from "./ebb";

function parseSvg(svg: string) {
const window = new Window
@@ -16,49 +17,19 @@ function parseSvg(svg: string) {
}

export function cli(argv: string[]): void {
yargs.strict()
.option("device", {
alias: "d",
describe: "device to connect to",
type: "string"
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 <file>', '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)
@@ -251,17 +223,39 @@ export function cli(argv: string[]): void {
.check(args => args.percent >= 0 && args.percent <= 100),
async args => {
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)
}
await ebb.setPenHeight(Device.Axidraw.penPctToPos(args.percent), 1000)
const device = Device(ebb.hardware)
await ebb.setPenHeight(device.penPctToPos(args.percent), 1000)

console.log(`moving to ${args.percent}%...`)
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.port, args.hardware, args.device, args['enable-cors'], args['max-payload-size'])
}
)
.parse(argv)
}

function linesToVecs(lines: any[]): Vec2[][] {
12 changes: 9 additions & 3 deletions src/ebb.ts
Original file line number Diff line number Diff line change
@@ -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<any, any, Buffer>[];
private writer: WritableStreamDefaultWriter<Uint8Array>;
private readableClosed: Promise<void>
public hardware: Hardware

private microsteppingMode = 0;

@@ -22,7 +25,8 @@ export class EBB {

private cachedFirmwareVersion: [number, number, number] | undefined = undefined;

public constructor(port: SerialPort) {
public constructor (port: SerialPort, hardware: Hardware = 'v3') {
this.hardware = hardware
this.port = port;
this.writer = this.port.writable.getWriter();
this.commandQueue = [];
@@ -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<void> {
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<void> {
const output_pin = this.hardware === 'v3' ? 4 : 5
return await this.command(`S2,${height},${output_pin},${rate},${delay}`)
}

public lowlevelMove(
28 changes: 14 additions & 14 deletions src/massager.ts
Original file line number Diff line number Diff line change
@@ -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";

@@ -12,6 +11,7 @@ const mmPerSvgUnit = mmPerInch / svgUnitsPerInch

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
}
Loading