Log in
src/pen.js 209 lines · 7.2 KB · javascript
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
}