UNPKG

@chrisheninger/saxi

Version:

Drive the AxiDraw pen plotter

459 lines 18.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const serialport_1 = __importDefault(require("serialport")); const planning_1 = require("./planning"); const vec_1 = require("./vec"); /** Split d into its fractional and integral parts */ function modf(d) { const intPart = Math.floor(d); const fracPart = d - intPart; return [fracPart, intPart]; } function isEBB(p) { return p.manufacturer === "SchmalzHaus" || p.manufacturer === "SchmalzHaus LLC" || (p.vendorId == "04D8" && p.productId == "FD92"); } class EBB { constructor(port) { this.microsteppingMode = 0; /** Accumulated XY error, used to correct for movements with sub-step resolution */ this.error = { x: 0, y: 0 }; this.cachedFirmwareVersion = undefined; this.port = port; this.parser = this.port.pipe(new serialport_1.default.parsers.Regex({ regex: /[\r\n]+/ })); this.commandQueue = []; this.parser.on("data", (chunk) => { if (this.commandQueue.length) { if (chunk[0] === "!".charCodeAt(0)) { return this.commandQueue.shift().reject(new Error(chunk.toString("ascii"))); } try { const d = this.commandQueue[0].next(chunk); if (d.done) { return this.commandQueue.shift().resolve(d.value); } } catch (e) { return this.commandQueue.shift().reject(e); } } else { console.log(`unexpected data: ${chunk}`); } }); } /** List connected EBBs */ static list() { return __awaiter(this, void 0, void 0, function* () { const ports = yield serialport_1.default.list(); return ports.filter(isEBB).map((p) => p.path); }); } get stepMultiplier() { switch (this.microsteppingMode) { case 5: return 1; case 4: return 2; case 3: return 4; case 2: return 8; case 1: return 16; default: throw new Error(`Invalid microstepping mode: ${this.microsteppingMode}`); } } close() { return new Promise((resolve, reject) => { this.port.close((err) => { if (err) { reject(err); } else { resolve(); } }); }); } write(str) { if (process.env.DEBUG_SAXI_COMMANDS) { console.log(`writing: ${str}`); } return this.port.write(str); } /** Send a raw command to the EBB and expect a single line in return, without an "OK" line to terminate. */ query(cmd) { return __awaiter(this, void 0, void 0, function* () { try { return yield this.run(function* () { this.write(`${cmd}\r`); const result = (yield).toString("ascii"); return result; }); } catch (err) { throw new Error(`Error in response to query '${cmd}': ${err.message}`); } }); } /** Send a raw command to the EBB and expect multiple lines in return, with an "OK" line to terminate. */ queryM(cmd) { return __awaiter(this, void 0, void 0, function* () { try { return yield this.run(function* () { this.write(`${cmd}\r`); const result = []; while (true) { const line = (yield).toString("ascii"); if (line === "OK") { break; } result.push(line); } return result; }); } catch (err) { throw new Error(`Error in response to queryM '${cmd}': ${err.message}`); } }); } /** Send a raw command to the EBB and expect a single "OK" line in return. */ command(cmd) { return __awaiter(this, void 0, void 0, function* () { try { return yield this.run(function* () { this.write(`${cmd}\r`); const ok = (yield).toString("ascii"); if (ok !== "OK") { throw new Error(`Expected OK, got ${ok}`); } }); } catch (err) { throw new Error(`Error in response to command '${cmd}': ${err.message}`); } }); } enableMotors(microsteppingMode) { return __awaiter(this, void 0, void 0, function* () { if (!(1 <= microsteppingMode && microsteppingMode <= 5)) { throw new Error(`Microstepping mode must be between 1 and 5, but was ${microsteppingMode}`); } this.microsteppingMode = microsteppingMode; yield this.command(`EM,${microsteppingMode},${microsteppingMode}`); // if the board supports SR, we should also enable the servo motors. if (yield this.supportsSR()) yield this.setServoPowerTimeout(0, true); }); } disableMotors() { return __awaiter(this, void 0, void 0, function* () { yield this.command("EM,0,0"); // if the board supports SR, we should also disable the servo motors. if (yield this.supportsSR()) // 60 seconds is the default boot-time servo power timeout. yield this.setServoPowerTimeout(60000, false); }); } /** * Set the servo power timeout, in seconds. If a second parameter is * supplied, the servo will be immediately commanded into the given state (on * or off) depending on its value, in addition to setting the power-off * timeout duration. * * NB. this command is only avaliable on firmware v2.6.0 and hardware of at * least version 2.5.0. */ setServoPowerTimeout(timeout, power) { return __awaiter(this, void 0, void 0, function* () { yield this.command(`SR,${(timeout * 1000) | 0}${power != null ? `,${power ? 1 : 0}` : ''}`); }); } setPenHeight(height, rate, delay = 0) { return this.command(`S2,${height},4,${rate},${delay}`); } lowlevelMove(stepsAxis1, initialStepsPerSecAxis1, finalStepsPerSecAxis1, stepsAxis2, initialStepsPerSecAxis2, finalStepsPerSecAxis2) { const [initialRate1, deltaR1] = this.axisRate(stepsAxis1, initialStepsPerSecAxis1, finalStepsPerSecAxis1); const [initialRate2, deltaR2] = this.axisRate(stepsAxis2, initialStepsPerSecAxis2, finalStepsPerSecAxis2); return this.command(`LM,${initialRate1},${stepsAxis1},${deltaR1},${initialRate2},${stepsAxis2},${deltaR2}`); } /** * Use the low-level move command "LM" to perform a constant-acceleration stepper move. * * Available with EBB firmware 2.5.3 and higher. * * @param xSteps Number of steps to move in the X direction * @param ySteps Number of steps to move in the Y direction * @param initialRate Initial step rate, in steps per second * @param finalRate Final step rate, in steps per second */ moveWithAcceleration(xSteps, ySteps, initialRate, finalRate) { if (!(xSteps !== 0 || ySteps !== 0)) { throw new Error("Must move on at least one axis"); } if (!(initialRate >= 0 && finalRate >= 0)) { throw new Error(`Rates must be positive, were ${initialRate},${finalRate}`); } if (!(initialRate > 0 || finalRate > 0)) { throw new Error("Must have non-zero velocity during motion"); } const stepsAxis1 = xSteps + ySteps; const stepsAxis2 = xSteps - ySteps; const norm = Math.sqrt(Math.pow(xSteps, 2) + Math.pow(ySteps, 2)); const normX = xSteps / norm; const normY = ySteps / norm; const initialRateX = initialRate * normX; const initialRateY = initialRate * normY; const finalRateX = finalRate * normX; const finalRateY = finalRate * normY; const initialRateAxis1 = Math.abs(initialRateX + initialRateY); const initialRateAxis2 = Math.abs(initialRateX - initialRateY); const finalRateAxis1 = Math.abs(finalRateX + finalRateY); const finalRateAxis2 = Math.abs(finalRateX - finalRateY); return this.lowlevelMove(stepsAxis1, initialRateAxis1, finalRateAxis1, stepsAxis2, initialRateAxis2, finalRateAxis2); } /** * Use the high-level move command "XM" to perform a constant-velocity stepper move. * * @param duration Duration of the move, in seconds * @param x Number of microsteps to move in the X direction * @param y Number of microsteps to move in the Y direction */ moveAtConstantRate(duration, x, y) { return this.command(`XM,${Math.floor(duration * 1000)},${x},${y}`); } waitUntilMotorsIdle() { return __awaiter(this, void 0, void 0, function* () { while (true) { const [, commandStatus, _motor1Status, _motor2Status, fifoStatus] = (yield this.query("QM")).split(","); if (commandStatus === "0" && fifoStatus === "0") { break; } } }); } executeBlockWithLM(block) { return __awaiter(this, void 0, void 0, function* () { const [errX, stepsX] = modf((block.p2.x - block.p1.x) * this.stepMultiplier + this.error.x); const [errY, stepsY] = modf((block.p2.y - block.p1.y) * this.stepMultiplier + this.error.y); this.error.x = errX; this.error.y = errY; if (stepsX !== 0 || stepsY !== 0) { yield this.moveWithAcceleration(stepsX, stepsY, block.vInitial * this.stepMultiplier, block.vFinal * this.stepMultiplier); } }); } /** * Execute a constant-acceleration motion plan using the low-level LM command. * * Note that the LM command is only available starting from EBB firmware version 2.5.3. */ executeXYMotionWithLM(plan) { return __awaiter(this, void 0, void 0, function* () { for (const block of plan.blocks) { yield this.executeBlockWithLM(block); } }); } /** * Execute a constant-acceleration motion plan using the high-level XM command. * * This is less accurate than using LM, since acceleration will only be adjusted every timestepMs milliseconds, * where LM can adjust the acceleration at a much higher rate, as it executes on-board the EBB. */ executeXYMotionWithXM(plan, timestepMs = 15) { return __awaiter(this, void 0, void 0, function* () { const timestepSec = timestepMs / 1000; let t = 0; while (t < plan.duration()) { const i1 = plan.instant(t); const i2 = plan.instant(t + timestepSec); const d = vec_1.vsub(i2.p, i1.p); const [ex, sx] = modf(d.x * this.stepMultiplier + this.error.x); const [ey, sy] = modf(d.y * this.stepMultiplier + this.error.y); this.error.x = ex; this.error.y = ey; yield this.moveAtConstantRate(timestepSec, sx, sy); t += timestepSec; } }); } /** Execute a constant-acceleration motion plan, starting and ending with zero velocity. */ executeXYMotion(plan) { return __awaiter(this, void 0, void 0, function* () { if (yield this.supportsLM()) { yield this.executeXYMotionWithLM(plan); } else { yield this.executeXYMotionWithXM(plan); } }); } executePenMotion(pm) { // rate is in units of clocks per 24ms. // so to fit the entire motion in |pm.duration|, // dur = diff / rate // [time] = [clocks] / ([clocks]/[time]) // [time] = [clocks] * [clocks]^-1 * [time] // [time] = [time] // ✔ // so rate = diff / dur // dur is in [sec] // but rate needs to be in [clocks] / [24ms] // duration in units of 24ms is duration * [24ms] / [1s] return this.setPenHeight(pm.finalPos, 0, Math.round(pm.duration() * 1000 + 0)); } executeMotion(m) { if (m instanceof planning_1.XYMotion) { return this.executeXYMotion(m); } else if (m instanceof planning_1.PenMotion) { return this.executePenMotion(m); } else { throw new Error(`Unknown motion type: ${m.constructor.name}`); } } executePlan(plan, microsteppingMode = 2) { return __awaiter(this, void 0, void 0, function* () { yield this.enableMotors(microsteppingMode); for (const m of plan.motions) { yield this.executeMotion(m); } yield this.waitUntilMotorsIdle(); yield this.disableMotors(); }); } /** * Query voltages for board & steppers. Useful to check whether stepper power is plugged in. * * @return Tuple of (RA0_VOLTAGE, V+_VOLTAGE, VIN_VOLTAGE) */ queryVoltages() { return __awaiter(this, void 0, void 0, function* () { const [ra0Voltage, vPlusVoltage] = (yield this.queryM("QC"))[0].split(/,/).map(Number); return [ ra0Voltage / 1023.0 * 3.3, vPlusVoltage / 1023.0 * 3.3, vPlusVoltage / 1023.0 * 3.3 * 9.2 + 0.3 ]; }); } /** * Query the firmware version running on the EBB. * * @return The version string, e.g. "EBBv13_and_above EB Firmware Version 2.5.3" */ firmwareVersion() { return __awaiter(this, void 0, void 0, function* () { return yield this.query("V"); }); } /** * @return The firmware version as a parsed version triple, e.g. [2, 5, 3] */ firmwareVersionNumber() { return __awaiter(this, void 0, void 0, function* () { if (this.cachedFirmwareVersion === undefined) { const versionString = yield this.firmwareVersion(); const versionWords = versionString.split(" "); const [major, minor, patch] = versionWords[versionWords.length - 1].split(".").map(Number); this.cachedFirmwareVersion = [major, minor, patch]; } return this.cachedFirmwareVersion; }); } /** * Compare the firmware version of the EBB with the given version. * * @return -1 if the firmware is older than the given version, 0 if it's * identical, and 1 if it's newer. */ firmwareVersionCompare(major, minor, patch) { return __awaiter(this, void 0, void 0, function* () { const [fwMajor, fwMinor, fwPatch] = yield this.firmwareVersionNumber(); if (fwMajor < major) return -1; if (fwMajor > major) return 1; if (fwMinor < minor) return -1; if (fwMinor > minor) return 1; if (fwPatch < patch) return -1; if (fwPatch > patch) return 1; return 0; }); } areSteppersPowered() { return __awaiter(this, void 0, void 0, function* () { const [, , vInVoltage] = yield this.queryVoltages(); return vInVoltage > 6; }); } queryButton() { return __awaiter(this, void 0, void 0, function* () { return (yield this.queryM("QB"))[0] === "1"; }); } /** * @return true iff the EBB firmware supports the LM command. */ supportsLM() { return __awaiter(this, void 0, void 0, function* () { return (yield this.firmwareVersionCompare(2, 5, 3)) >= 0; }); } /** * @return true iff the EBB firmware supports the SR command. */ supportsSR() { return __awaiter(this, void 0, void 0, function* () { return (yield this.firmwareVersionCompare(2, 6, 0)) >= 0; }); } /** * Helper method for computing axis rates for the LM command. * * See http://evil-mad.github.io/EggBot/ebb.html#LM * * @param steps Number of steps being taken * @param initialStepsPerSec Initial movement rate, in steps per second * @param finalStepsPerSec Final movement rate, in steps per second * @return A tuple of (initialAxisRate, deltaR) that can be passed to the LM command */ axisRate(steps, initialStepsPerSec, finalStepsPerSec) { if (steps === 0) return [0, 0]; const initialRate = Math.round(initialStepsPerSec * (0x80000000 / 25000)); const finalRate = Math.round(finalStepsPerSec * (0x80000000 / 25000)); const moveTime = 2 * Math.abs(steps) / (initialStepsPerSec + finalStepsPerSec); const deltaR = Math.round((finalRate - initialRate) / (moveTime * 25000)); return [initialRate, deltaR]; } run(g) { const cmd = g.call(this); const d = cmd.next(); if (d.done) { return Promise.resolve(d.value); } this.commandQueue.push(cmd); return new Promise((resolve, reject) => { cmd.resolve = resolve; cmd.reject = reject; }); } } exports.EBB = EBB; //# sourceMappingURL=ebb.js.map