| 1 | // Pen-lift servo handling and XYZ position bookkeeping (port of
|
| 2 | // nextdrawcore/pen_handling.py).
|
| 3 |
|
| 4 | import { exhaustQueue, QG_PEN_UP, readStatusByte } from './control.js';
|
| 5 | import { pyInt, pyRound } from './calc.js';
|
| 6 |
|
| 7 | export function newPenPosition() {
|
| 8 | return { xpos: 0, ypos: 0, zUp: null, accum1: 0, accum2: 0, homed: false };
|
| 9 | }
|
| 10 |
|
| 11 | /**
|
| 12 | * Servo travel time (ms) between pen-up and pen-down heights: a 4th-power
|
| 13 | * blend of the fast-servo transit time and the rate-limited sweep time.
|
| 14 | */
|
| 15 | export function penLiftTimes(nd, penDownPos) {
|
| 16 | const p = nd.params;
|
| 17 | const o = nd.options;
|
| 18 | const vDist = Math.abs(o.penPosUp - penDownPos);
|
| 19 | const timeFor = (rate, extraDelay) => {
|
| 20 | let t = pyInt(((p.servoMoveSlope * vDist + p.servoMoveMin) ** 4 +
|
| 21 | (p.servoSweepTime * vDist / rate) ** 4) ** 0.25);
|
| 22 | if (vDist < 0.9) t = 0; // Same up and down positions: no delay
|
| 23 | return Math.max(0, t + extraDelay);
|
| 24 | };
|
| 25 | return { raiseTime: timeFor(o.penRateRaise, p.penDelayUp), lowerTime: timeFor(o.penRateLower, p.penDelayDown) };
|
| 26 | }
|
| 27 |
|
| 28 | /** Servo PWM values and rates sent to the EBB for the current settings. */
|
| 29 | export function servoSettings(nd, penPosDown) {
|
| 30 | const p = nd.params;
|
| 31 | const o = nd.options;
|
| 32 | // Units of "ms / 100", since the rates are percentages.
|
| 33 | const pwmPeriod = p.zMotor ? 0.03 : 0.24; // 1 channel vs 8 channels at 3 ms
|
| 34 | const range = p.servoMax - p.servoMin;
|
| 35 | const slope = range / 100.0;
|
| 36 | const rateScale = range * pwmPeriod / p.servoSweepTime;
|
| 37 | return {
|
| 38 | rateUp: pyRound(rateScale * o.penRateRaise),
|
| 39 | rateDown: pyRound(rateScale * o.penRateLower),
|
| 40 | posUp: pyRound(p.servoMin + slope * o.penPosUp),
|
| 41 | posDown: pyRound(p.servoMin + slope * penPosDown),
|
| 42 | };
|
| 43 | }
|
| 44 |
|
| 45 | export class PenHandler {
|
| 46 | constructor() {
|
| 47 | this.penPosDown = null; // Active pen-down height (may be a temporary override)
|
| 48 | this.useTempHeight = false;
|
| 49 | this.times = { raiseTime: null, lowerTime: null };
|
| 50 | this.lifts = 0;
|
| 51 | // [penPosUp, penPosDown, zMotor, penUp] as read from / desired on the EBB.
|
| 52 | this.initState = [-1, -1, -1, null];
|
| 53 | this.initGoal = [-1, -1, -1, null];
|
| 54 | this.phys = newPenPosition(); // Physical pen position
|
| 55 | this.turtle = newPenPosition(); // Commanded ("turtle") position, interactive API
|
| 56 | }
|
| 57 |
|
| 58 | reset() {
|
| 59 | this.lifts = 0;
|
| 60 | this.useTempHeight = false;
|
| 61 | }
|
| 62 |
|
| 63 | updateHeights(nd) {
|
| 64 | if (!this.useTempHeight) this.penPosDown = nd.options.penPosDown;
|
| 65 | this.times = penLiftTimes(nd, this.penPosDown);
|
| 66 | this.initGoal[0] = nd.options.penPosUp;
|
| 67 | this.initGoal[1] = this.penPosDown;
|
| 68 | }
|
| 69 |
|
| 70 | async raise(nd) {
|
| 71 | if (this.phys.zUp) return;
|
| 72 | this.lifts += 1;
|
| 73 | const vTime = this.times.raiseTime;
|
| 74 | if (nd.options.preview) {
|
| 75 | nd.plotStatus.stats.ptEstimate += vTime;
|
| 76 | } else {
|
| 77 | await nd.machine.penRaise(vTime, nd.params.servoPin);
|
| 78 | if (vTime > 50 && !['utility', 'align', 'cycle'].includes(nd.options.mode)) {
|
| 79 | await nd.clock.sleep(vTime - 30);
|
| 80 | }
|
| 81 | if (nd.params.useB3Out) {
|
| 82 | if (nd.params.syncB3) await exhaustQueue(nd);
|
| 83 | await nd.machine.dioBSet(3, 0);
|
| 84 | }
|
| 85 | }
|
| 86 | this.phys.zUp = true;
|
| 87 | }
|
| 88 |
|
| 89 | async lower(nd) {
|
| 90 | if (this.phys.zUp != null && !this.phys.zUp) return; // Known to be down already
|
| 91 | if (nd.plotStatus.stopped) return;
|
| 92 | const vTime = this.times.lowerTime;
|
| 93 | if (nd.options.preview) {
|
| 94 | nd.plotStatus.stats.ptEstimate += vTime;
|
| 95 | } else {
|
| 96 | await nd.machine.penLower(vTime, nd.params.servoPin);
|
| 97 | if (vTime > 50 && !['utility', 'align', 'cycle'].includes(nd.options.mode)) {
|
| 98 | await nd.clock.sleep(vTime - 30);
|
| 99 | }
|
| 100 | if (nd.params.useB3Out) {
|
| 101 | if (nd.params.syncB3) await exhaustQueue(nd);
|
| 102 | await nd.machine.dioBSet(3, 1);
|
| 103 | }
|
| 104 | }
|
| 105 | this.phys.zUp = false;
|
| 106 | }
|
| 107 |
|
| 108 | /** Lower, pause 500 ms, raise. Setup utility only. */
|
| 109 | async cycle(nd) {
|
| 110 | await this.lower(nd);
|
| 111 | await nd.machine.timedPause(500);
|
| 112 | await this.raise(nd);
|
| 113 | }
|
| 114 |
|
| 115 | /**
|
| 116 | * Read back the pen configuration cached in EBB variables 10 and 11 (and
|
| 117 | * the QG pen bit) to tell whether the servo is already set up as we want.
|
| 118 | */
|
| 119 | async findPenState(nd) {
|
| 120 | const codeUp = await nd.machine.varRead(10);
|
| 121 | const codeDown = await nd.machine.varRead(11);
|
| 122 | if (codeUp == null || codeDown == null) {
|
| 123 | this.initState[0] = -1;
|
| 124 | this.initState[1] = -1;
|
| 125 | return;
|
| 126 | }
|
| 127 | if (nd.params.zMotor) {
|
| 128 | if (codeUp > 101 && codeDown > 101) this.initState[2] = 1;
|
| 129 | } else if (codeUp < 102 && codeDown < 102) {
|
| 130 | this.initState[2] = 0;
|
| 131 | }
|
| 132 | const decode = (code) => (code === 0 ? -1 : code > 101 ? code - 102 : code - 1);
|
| 133 | this.initState[0] = decode(codeUp);
|
| 134 | this.initState[1] = decode(codeDown);
|
| 135 | const qg = await readStatusByte(nd);
|
| 136 | this.initState[3] = qg == null ? true : Boolean(qg & QG_PEN_UP);
|
| 137 | }
|
| 138 |
|
| 139 | /**
|
| 140 | * Configure servo positions/rates on the EBB and put the pen into a known
|
| 141 | * state (normally up). Skips the physical move if the EBB reports it is
|
| 142 | * already configured identically.
|
| 143 | */
|
| 144 | async servoInit(nd) {
|
| 145 | this.updateHeights(nd);
|
| 146 | if (nd.options.preview) this.phys.zUp = true;
|
| 147 | if (nd.options.preview || !nd.machine.connected) return;
|
| 148 |
|
| 149 | let codeUp = 1 + nd.options.penPosUp; // 1-101
|
| 150 | let codeDown = 1 + this.penPosDown;
|
| 151 | this.initGoal[0] = nd.options.penPosUp;
|
| 152 | this.initGoal[1] = this.penPosDown;
|
| 153 | if (nd.params.zMotor) {
|
| 154 | this.initGoal[2] = 1;
|
| 155 | codeUp += 101; // 102-202 marks the brushless motor
|
| 156 | codeDown += 101;
|
| 157 | } else {
|
| 158 | this.initGoal[2] = 0;
|
| 159 | }
|
| 160 |
|
| 161 | await this.findPenState(nd);
|
| 162 | const initialized = this.initState[0] === this.initGoal[0] &&
|
| 163 | this.initState[1] === this.initGoal[1] && this.initState[2] === this.initGoal[2];
|
| 164 |
|
| 165 | if (initialized) {
|
| 166 | this.phys.zUp = this.initState[3];
|
| 167 | this.initGoal[3] = this.initState[3];
|
| 168 | }
|
| 169 |
|
| 170 | const o = nd.options;
|
| 171 | if ((o.mode === 'utility' && o.utilityCmd === 'lower_pen') ||
|
| 172 | (o.mode === 'toggle' && Boolean(this.phys.zUp)) || o.mode === 'cycle') {
|
| 173 | this.initGoal[3] = false;
|
| 174 | } else if (!initialized || o.mode === 'toggle' ||
|
| 175 | (o.mode === 'utility' && o.utilityCmd === 'raise_pen')) {
|
| 176 | this.initGoal[3] = true;
|
| 177 | }
|
| 178 |
|
| 179 | const s = servoSettings(nd, this.penPosDown);
|
| 180 | const pin = nd.params.servoPin;
|
| 181 | await nd.machine.penRateUp(s.rateUp);
|
| 182 | await nd.machine.penRateDown(s.rateDown);
|
| 183 | if (nd.params.useB3Out) await nd.machine.dioBConfig(3, 0, 0);
|
| 184 | await nd.machine.penPosUp(s.posUp);
|
| 185 | await nd.machine.penPosDown(s.posDown);
|
| 186 |
|
| 187 | if (initialized && this.phys.zUp != null && this.phys.zUp === this.initGoal[3]) return;
|
| 188 |
|
| 189 | if (!this.initGoal[3]) {
|
| 190 | await nd.machine.penLower(this.times.lowerTime, pin);
|
| 191 | if (nd.params.useB3Out) await nd.machine.dioBSet(3, 1);
|
| 192 | this.phys.zUp = false;
|
| 193 | }
|
| 194 | if (this.initGoal[3]) {
|
| 195 | await nd.machine.penRaise(this.times.raiseTime, pin);
|
| 196 | if (nd.params.useB3Out) await nd.machine.dioBSet(3, 0);
|
| 197 | this.phys.zUp = true;
|
| 198 | }
|
| 199 | if (nd.params.zMotor) {
|
| 200 | await nd.machine.command('SC,8,1'); // 1 channel of servo PWM
|
| 201 | } else {
|
| 202 | await nd.machine.command('SC,8,8');
|
| 203 | await nd.machine.servoTimeout(nd.params.servoTimeout, null);
|
| 204 | }
|
| 205 | this.initState = [...this.initGoal];
|
| 206 | await nd.machine.varWrite(codeUp, 10);
|
| 207 | await nd.machine.varWrite(codeDown, 11);
|
| 208 | }
|
| 209 | }
|