Log in
src/homing.js 407 lines · 13.3 KB · javascript
1
// Automatic homing against the limit switch on EBB pin B1, and origin-offset
2
// bookkeeping (port of nextdrawcore/homing.py).
3
//
4
// The switch is normally closed: B1 reads 0 when present and not pressed,
5
// 1 when pressed (or absent).
6
7
import { absMoveWrapper, exhaustQueue, QG_MOTION, readStatusByte, readStepOffsets, readStepPosition,
8
  writeStepOffsets } from './control.js';
9
import { distance, pyInt, pyRound } from './calc.js';
10
11
/** XY inches -> absolute (A, B) motor step position. */
12
export function xyToStepPos(nd, x, y) {
13
  if (nd.params.resolution === 2) {
14
    x /= 2;
15
    y /= 2;
16
  }
17
  const k = 2 * nd.params.nativeResFactor;
18
  return [pyRound(k * (x + y)), pyRound(k * (x - y))];
19
}
20
21
/** Absolute (A, B) motor step position -> XY inches. */
22
export function stepsToXyPos(nd, a, b) {
23
  let x = (a + b) / (4 * nd.params.nativeResFactor);
24
  let y = (a - b) / (4 * nd.params.nativeResFactor);
25
  if (nd.params.resolution === 2) {
26
    x *= 2;
27
    y *= 2;
28
  }
29
  return [x, y];
30
}
31
32
const SPEED_FAST = 2; // in/s, coarse
33
const SPEED_FINE = 0.25; // in/s
34
const SIDE_DIST = 6; // mm
35
36
export class Homing {
37
  constructor(nd) {
38
    this.nd = nd;
39
    this.failed = false;
40
    this.paused = false;
41
    this.stepScale = null;
42
    this.res = 0;
43
  }
44
45
  message(text) {
46
    this.nd.log(text);
47
  }
48
49
  /**
50
   * Home the machine (if supported and not already homed) and set the
51
   * step counter origin. Returns true on success.
52
   */
53
  async findHome() {
54
    const nd = this.nd;
55
    this.failed = false;
56
    this.paused = false;
57
    if (nd.options.preview) return true;
58
    const autoHoming = nd.options.homing && nd.params.autoHome;
59
    if (!nd.machine.connected || nd.machine.err != null) {
60
      await this.markFailed();
61
      return false;
62
    }
63
    if (await nd.machine.varRead(12)) { // Already homed
64
      await this.readPosition();
65
      return true;
66
    }
67
    if (autoHoming) {
68
      if (nd.warnings.has('voltage')) {
69
        nd.warn('homing_voltage');
70
        await this.markFailed();
71
        return false;
72
      }
73
      if (nd.params.skipVoltageCheck && !(await nd.machine.queryVoltage(200))) {
74
        nd.warn('homing_voltage');
75
        await this.markFailed();
76
        return false;
77
      }
78
      this.stepScale = nd.stepScale;
79
      this.res = nd.params.resolution === 1 ? 1 : 2;
80
      await this.rhmHoming();
81
      if (this.failed) {
82
        await this.markFailed();
83
        return false;
84
      }
85
      await nd.machine.clearSteps();
86
      await this.lhmHoming();
87
    }
88
    if (this.failed) {
89
      await this.markFailed();
90
      return false;
91
    }
92
    await this.setHome();
93
    return true;
94
  }
95
96
  async markFailed() {
97
    const nd = this.nd;
98
    this.failed = true;
99
    if (nd.plotStatus.stopped === 0) {
100
      nd.plotStatus.stopped = nd.warnings.has('voltage') ? 105 : 106;
101
    }
102
    if (!nd.machine.connected || nd.machine.err != null) return;
103
    await nd.machine.varWrite(0, 12);
104
  }
105
106
  /** Wait for motion to finish; E-stop on button press. */
107
  async block(timeoutMs = null) {
108
    const nd = this.nd;
109
    if (this.failed) return;
110
    let timeLeft = timeoutMs == null ? 1000 : timeoutMs;
111
    for (;;) {
112
      const qg = await readStatusByte(nd);
113
      if (nd.plotStatus.button) {
114
        this.message('Homing interrupted by button press.');
115
        this.paused = true;
116
        this.failed = true;
117
        await nd.machine.query('ES');
118
        return;
119
      }
120
      if (qg == null) {
121
        this.failed = true;
122
        this.message('Error reading status byte during homing.');
123
        return;
124
      }
125
      if ((qg & QG_MOTION) === 0) return;
126
      if (timeLeft <= 0) {
127
        this.failed = true;
128
        this.message('Timeout at block function in homing routine.');
129
        return;
130
      }
131
      if (timeLeft < 10) {
132
        await nd.clock.sleep(timeLeft);
133
        timeLeft = 0;
134
      } else {
135
        await nd.clock.sleep(2);
136
        if (timeoutMs != null) timeLeft -= 2;
137
      }
138
    }
139
  }
140
141
  /** 1 if switch pressed (or absent), 0 if present and open, 2 on error. */
142
  async queryLimitSwitch() {
143
    const nd = this.nd;
144
    if (!nd.machine.connected || nd.machine.err != null) return 2;
145
    await this.block();
146
    const response = await nd.machine.dioBRead(1);
147
    if (response == null) {
148
      this.message('Error reading serial data in Homing process.');
149
      this.failed = true;
150
      return 2;
151
    }
152
    return response ? 1 : 0;
153
  }
154
155
  async moveXyInch(x, y, timeMs) {
156
    if (this.failed || this.paused) return;
157
    const steps1 = pyRound(this.stepScale * (x + y));
158
    const steps2 = pyRound(this.stepScale * (x - y));
159
    await this.nd.machine.xyMove(steps2, steps1, timeMs);
160
  }
161
162
  /**
163
   * Move left with only the right-hand motor until the limit switch trips.
164
   * Returns the apparent distance moved (inches), or -1 on failure.
165
   */
166
  async leftUntilBump(speed, maxDist) {
167
    const nd = this.nd;
168
    if (this.failed || this.paused) {
169
      this.failed = true;
170
      return -1;
171
    }
172
    if ((await this.queryLimitSwitch()) === 1) {
173
      this.failed = true;
174
      this.message('Automatic homing failed. (Error: Limit switch not ready.)');
175
      return -1;
176
    }
177
    if (nd.warnings.has('voltage')) {
178
      nd.warn('homing_voltage');
179
      return -1;
180
    }
181
    await this.block();
182
    await nd.machine.motorsEnable(this.res, 0); // Freewheel the left-hand motor
183
    await this.block();
184
    await this.enableLimitDetection();
185
    await this.moveXyInch(-maxDist, 0, Math.abs(pyInt(1000 * maxDist / speed)));
186
    // Until Y = 0 the carriage actually moves diagonally, at half the distance.
187
    await this.block();
188
    const limitOccurred = nd.plotStatus.limit;
189
    await nd.machine.command('CU,51,0'); // Disable limit switch detection
190
    if (!limitOccurred && !this.paused) {
191
      this.failed = true;
192
      this.message('Automatic homing failed with a "limit not found" error. ' +
193
        `This could indicate the wrong plotter model is selected (currently ${nd.params.modelName}).`);
194
      return -1;
195
    }
196
    // EM cleared the step counter before the move, so this is this move's count.
197
    const steps = await nd.machine.querySteps();
198
    if (steps == null) {
199
      this.failed = true;
200
      return -1;
201
    }
202
    return Math.abs(steps[1] / this.stepScale);
203
  }
204
205
  /** Back off right 6 mm, left 3.5 mm, then fine-approach the switch (<= 7 mm). */
206
  async rhmHomingFine() {
207
    const nd = this.nd;
208
    if (this.failed || this.paused) return -1;
209
    const leftCoarseDist = 7; // mm
210
    await nd.machine.motorsEnable(this.res, this.res);
211
    let step = 6 / 25.4;
212
    await this.moveXyInch(step, 0, pyInt(1000 * Math.abs(step / SPEED_FAST)));
213
    if ((await this.queryLimitSwitch()) === 1) {
214
      this.failed = true;
215
      this.message('Automatic homing failed. (Error: Limit switch not detected.)');
216
      return -1;
217
    }
218
    step = (leftCoarseDist / 2) / 25.4;
219
    await this.moveXyInch(-step, 0, pyInt(1000 * Math.abs(step / SPEED_FAST)));
220
    if ((await this.queryLimitSwitch()) === 1) {
221
      this.failed = true;
222
      this.message('Automatic homing failed. (Error: Limit switch position error.)');
223
    }
224
    if (this.failed) return -1;
225
    return this.leftUntilBump(SPEED_FINE, leftCoarseDist / 25.4);
226
  }
227
228
  /** Right-hand-motor homing: find X = 0 with Y = 0 (see homing.py for stages). */
229
  async rhmHoming() {
230
    const nd = this.nd;
231
    const [xMax, yMax] = nd.bounds[1];
232
    await nd.machine.dioBConfig(1, 1, 1); // B1 as input, initially high
233
    await nd.machine.command('CU,50,0');
234
235
    let firstCoarseDist;
236
    if ((await this.queryLimitSwitch()) === 1) {
237
      firstCoarseDist = 0.05; // Switch already pressed; skip the first coarse move.
238
    } else {
239
      // Twice the diagonal (one motor moves at half distance) plus any extra X travel.
240
      const maxDist = 2 * yMax + (xMax - yMax) + 0.5;
241
      const apparent = await this.leftUntilBump(SPEED_FAST, maxDist);
242
      if (this.failed) return;
243
      firstCoarseDist = apparent / 2;
244
    }
245
246
    let dist = await this.rhmHomingFine(); // ~2.5 mm (0.0984 in) if Y = 0
247
    if (this.failed) return;
248
    const firstDistClose = dist < 0.15;
249
250
    dist = await this.rhmHomingFine();
251
    if (dist < 0) {
252
      this.failed = true;
253
      return;
254
    }
255
    if (dist <= 0.0984) return;
256
    if (firstDistClose && dist < 0.12) return;
257
    if (dist < 0.15) {
258
      const dist2 = await this.rhmHomingFine();
259
      if (dist2 > 0.12) {
260
        this.failed = true;
261
        this.message('Homing failed');
262
      }
263
      return;
264
    }
265
266
    // Not yet at Y = 0: big move right, then coarse and fine approach again.
267
    let remaining = yMax - firstCoarseDist - 2.5 / 25.4;
268
    remaining = Math.max(remaining, 0);
269
    await nd.machine.motorsEnable(this.res, this.res);
270
    await this.moveXyInch(remaining, 0, pyInt(1000 * remaining / SPEED_FAST));
271
    const coarse = await this.leftUntilBump(SPEED_FAST, 2 * (remaining + 0.1));
272
    if (coarse < 0) {
273
      this.message('Error: Homing failed at second coarse move; no limit found.');
274
      this.failed = true;
275
      return;
276
    }
277
    dist = await this.rhmHomingFine();
278
    if (dist < 0) {
279
      this.message('Error; limit not found');
280
      return;
281
    }
282
    if (dist <= 0.0984) return;
283
    if (dist < 0.15) {
284
      const dist2 = await this.rhmHomingFine();
285
      if (dist2 > 0.12) {
286
        this.message('Error: Homing failed (not found; secondary)');
287
        this.failed = true;
288
      }
289
    }
290
  }
291
292
  /** Left-hand-motor precision homing: step out diagonally, approach, return. */
293
  async lhmHoming() {
294
    const nd = this.nd;
295
    await nd.machine.motorsEnable(this.res, this.res);
296
    let step = SIDE_DIST / 25.4;
297
    const motorSteps1 = pyRound(this.stepScale * (step + step));
298
    let timeMs = pyInt(1000 * Math.abs(step / SPEED_FAST));
299
    await nd.machine.xyMove(0, motorSteps1, timeMs);
300
301
    step = (SIDE_DIST / 2) / 25.4; // First half of the leftward move, fast
302
    const timeMs1 = pyInt(1000 * Math.abs(step / SPEED_FAST));
303
    let motorSteps2 = pyRound(this.stepScale * (-2 * step));
304
    await nd.machine.xyMove(motorSteps2, 0, timeMs1);
305
306
    step = SIDE_DIST / 25.4; // Second half, slow, until the switch
307
    timeMs = pyInt(1000 * Math.abs(step / SPEED_FINE));
308
    motorSteps2 = pyRound(this.stepScale * (-2 * step));
309
    if ((await this.queryLimitSwitch()) === 1) {
310
      this.failed = true;
311
      this.message('Automatic homing failed. (Limit switch not ready; Precision stage)');
312
      return;
313
    }
314
    await this.enableLimitDetection();
315
    await nd.machine.xyMove(motorSteps2, 0, timeMs);
316
    await this.block();
317
    const limitOccurred = nd.plotStatus.limit;
318
    await nd.machine.command('CU,51,0');
319
    if (!limitOccurred) {
320
      this.failed = true;
321
      this.message('Automatic homing failed. (Limit not found; precision stage)');
322
      return;
323
    }
324
    await nd.machine.xyMove(motorSteps1, -motorSteps1, 4 * timeMs1);
325
    await this.block();
326
  }
327
328
  async enableLimitDetection() {
329
    await this.nd.machine.command('CU,52,2'); // Target: B1 high
330
    await this.nd.machine.command('CU,51,2'); // Mask: B1
331
    this.nd.plotStatus.limit = false;
332
  }
333
334
  /** Set nd.pen.phys from the EBB step counter, minus the origin offset. */
335
  async readPosition() {
336
    const nd = this.nd;
337
    if (nd.options.preview) {
338
      nd.pen.phys.accum1 = 0;
339
      nd.pen.phys.accum2 = 0;
340
    }
341
    if (!nd.machine.connected || nd.machine.err != null) return;
342
    await exhaustQueue(nd);
343
    const pos = await nd.machine.querySteps();
344
    const offset = await readStepOffsets(nd);
345
    if (pos == null || offset == null) {
346
      this.message('Error reading step positions.');
347
      return;
348
    }
349
    const offsetXy = stepsToXyPos(nd, offset[0] / 1000, offset[1] / 1000);
350
    const posXy = stepsToXyPos(nd, pos[0], pos[1]);
351
    nd.pen.phys.xpos = posXy[0] - offsetXy[0];
352
    nd.pen.phys.ypos = posXy[1] - offsetXy[1];
353
    nd.pen.phys.accum1 = 0;
354
    nd.pen.phys.accum2 = 0;
355
    await nd.machine.clearAccumulators();
356
  }
357
358
  /** Zero the step counter here, clear offsets, and mark the machine homed. */
359
  async setHome() {
360
    const nd = this.nd;
361
    await exhaustQueue(nd);
362
    await nd.machine.clearSteps();
363
    await writeStepOffsets(nd, 0, 0);
364
    await nd.machine.varWrite(1, 12);
365
    await this.readPosition();
366
  }
367
368
  /** Shift the origin offset by (dx, dy) inches. */
369
  async adjustOriginOffset(dx, dy) {
370
    const nd = this.nd;
371
    const offset = await readStepOffsets(nd);
372
    if (offset == null) {
373
      this.message('Error reading step positions.');
374
      return;
375
    }
376
    const offsetXy = stepsToXyPos(nd, offset[0] / 1000, offset[1] / 1000);
377
    const ab = xyToStepPos(nd, (offsetXy[0] + dx) * 1000, (offsetXy[1] + dy) * 1000);
378
    await writeStepOffsets(nd, ab[0], ab[1]);
379
    await this.readPosition();
380
  }
381
382
  async xyToStepPosWithOffset(x, y) {
383
    const offset = await readStepOffsets(this.nd);
384
    if (offset == null) return null;
385
    const offsetXy = stepsToXyPos(this.nd, offset[0] / 1000, offset[1] / 1000);
386
    return xyToStepPos(this.nd, x + offsetXy[0], y + offsetXy[1]);
387
  }
388
389
  /** Absolute HM move to (x, y) inches relative to the origin offset. */
390
  async precisionMoveTo(x, y, rate = 3000) {
391
    const nd = this.nd;
392
    const dest = await this.xyToStepPosWithOffset(x, y);
393
    if (dest == null) return;
394
    if (nd.options.preview) {
395
      nd.plotStatus.stats.addDist(nd, distance(x - nd.pen.phys.xpos, y - nd.pen.phys.ypos));
396
      nd.pen.phys.xpos = x;
397
      nd.pen.phys.ypos = y;
398
      return;
399
    }
400
    const read = await readStepPosition(nd);
401
    if (read == null) return;
402
    if (dest[0] !== read[0] || dest[1] !== read[1]) {
403
      await absMoveWrapper(nd, dest[0], dest[1], rate);
404
    }
405
    await this.readPosition();
406
  }
407
}