CI Log in

zach/dotter

src/control.js 197 lines · 6.9 KB · javascript
1
// Machine control helpers (port of nextdrawcore/serial_utils.py and
2
// nextdrawcore/dripfeed.py). Functions take the NextDraw instance `nd`.
3
4
import { peakRateCommand } from './calc.js';
5
6
// QG status byte bits
7
export const QG_LIMIT = 128;
8
export const QG_POWER_LOST = 64;
9
export const QG_BUTTON = 32;
10
export const QG_PEN_UP = 16;
11
export const QG_MOTION = 15;
12
13
/** Connection-time settings applied right after the EBB is verified. */
14
export async function connectSetup(nd) {
15
  await nd.machine.command('CU,3,1'); // Enable data-low LED
16
  await nd.machine.command('CU,4,16'); // FIFO depth of 16
17
  await nd.machine.command('CU,50,0'); // Don't auto-enable motors on motion commands
18
}
19
20
/** Spot-check motor power. Returns true if power looks present. */
21
export async function queryVoltage(nd) {
22
  if (nd.params.skipVoltageCheck) return true;
23
  if (nd.machine.connected && !nd.options.preview) {
24
    if (!(await nd.machine.queryVoltage(200))) {
25
      nd.warn('voltage');
26
      if (!nd.plotStatus.power) {
27
        await nd.machine.varWrite(0, 12); // Not homed
28
        await nd.machine.varWrite(0, 13); // Power lost
29
        await nd.machine.clearSteps();
30
        nd.plotStatus.power = true;
31
      }
32
      return false;
33
    }
34
  }
35
  return true;
36
}
37
38
/**
39
 * Read the QG status byte and record limit/button/power events on
40
 * nd.plotStatus. Returns the byte, or null.
41
 */
42
export async function readStatusByte(nd) {
43
  if (nd.options.preview) return null;
44
  const qg = await nd.machine.queryStatusByte();
45
  if (nd.machine.err != null) {
46
    nd.plotStatus.connection = true;
47
    return null;
48
  }
49
  if (qg == null) {
50
    if (nd.plotStatus.monitor) nd.plotStatus.connection = true;
51
    nd.plotStatus.monitor = true;
52
    return null;
53
  }
54
  if (qg & QG_LIMIT) nd.plotStatus.limit = true;
55
  if (qg & QG_BUTTON) nd.plotStatus.button = true;
56
  if ((qg & QG_POWER_LOST) && !nd.params.skipVoltageCheck) {
57
    if (!nd.plotStatus.power) {
58
      await nd.machine.varWrite(0, 12);
59
      await nd.machine.varWrite(0, 13);
60
      await nd.machine.clearSteps();
61
    }
62
    nd.plotStatus.power = true;
63
  }
64
  return qg;
65
}
66
67
/** Wait until queued motion has finished (polls QG every 50 ms). */
68
export async function exhaustQueue(nd) {
69
  if (!nd.machine.connected) return;
70
  for (;;) {
71
    if (nd.receivePauseRequest()) break;
72
    const qg = await readStatusByte(nd);
73
    if (qg == null) return;
74
    if ((qg & QG_MOTION) === 0 || nd.plotStatus.button) return;
75
    await nd.clock.sleep(50);
76
  }
77
}
78
79
/** Short absolute "dog leg" correction move to a motor step position. */
80
export async function absMoveWrapper(nd, pos1, pos2, rate) {
81
  if (nd.options.preview || !nd.machine.connected) return;
82
  await nd.machine.absMove(rate, Math.trunc(pos1), Math.trunc(pos2));
83
}
84
85
/** Enable motors at the configured resolution and compute speed scales. */
86
export async function enableMotors(nd) {
87
  const speedPendown = nd.useLayerSpeed ? nd.layerSpeedPendown : nd.options.speedPendown;
88
  let res1 = 0;
89
  let res2 = 0;
90
  if (!nd.options.preview) {
91
    await readStatusByte(nd);
92
    await nd.machine.command('CU,60,135'); // Power monitoring threshold ~4 V
93
    const response = await nd.machine.motorsQueryEnabled();
94
    if (response == null) return;
95
    [res1, res2] = await nd.machine.motorsQueryEnabled();
96
    await readStatusByte(nd); // Clears the power-lost bit if set.
97
    if (nd.plotStatus.power) {
98
      await nd.machine.varWrite(0, 12);
99
      nd.plotStatus.power = false;
100
    }
101
  }
102
  const res = nd.params.resolution === 1 ? 1 : 2;
103
  if (!nd.options.preview && !(res1 === res && res2 === res)) {
104
    await nd.machine.motorsEnable(res, res);
105
    await nd.machine.varWrite(0, 12); // Not homed
106
    await nd.machine.clearSteps();
107
  }
108
  nd.stepScale = res === 1 ? 2.0 * nd.params.nativeResFactor : nd.params.nativeResFactor;
109
  // A 45° move puts all of the motion on one motor, at sqrt(2) x the XY speed.
110
  // In high-resolution mode the stock speed limit sits right at the firmware's
111
  // 25 kHz step limit for such moves, and S-curve rounding can overshoot it,
112
  // so keep a 0.5% margin. (Not in the Python driver; only affects ~100% speeds.)
113
  const maxXy = 0.995 * nd.params.maxStepRate * 1000 / (Math.SQRT2 * nd.stepScale);
114
  nd.speedPendown = Math.min(speedPendown * nd.params.speedLimit / 100.0, maxXy);
115
  nd.speedPenup = Math.min(nd.options.speedPenup * nd.params.speedUp / 100.0, maxXy);
116
}
117
118
export async function readStepPosition(nd) {
119
  if (nd.machine.connected && !nd.options.preview) {
120
    await exhaustQueue(nd);
121
    return nd.machine.querySteps();
122
  }
123
  return null;
124
}
125
126
/** Origin offsets, stored in EBB RAM as steps * 1000. */
127
export async function readStepOffsets(nd) {
128
  if (nd.options.preview) return [0, 0];
129
  if (nd.machine.connected) {
130
    const o1 = await nd.machine.varReadInt32(24);
131
    const o2 = await nd.machine.varReadInt32(28);
132
    if (o1 != null && o2 != null) return [o1, o2];
133
  }
134
  return null;
135
}
136
137
export async function writeStepOffsets(nd, offset1, offset2) {
138
  if (nd.machine.connected && !nd.options.preview) {
139
    const ok1 = await nd.machine.varWriteInt32(offset1, 24);
140
    const ok2 = await nd.machine.varWriteInt32(offset2, 28);
141
    return ok1 && ok2;
142
  }
143
  return false;
144
}
145
146
// --- dripfeed ---
147
148
/** Send a planned move list to the machine, honoring pause/stop requests. */
149
export async function feed(nd, moveList) {
150
  if (moveList == null || nd.plotStatus.stopped) return;
151
  for (const move of moveList) {
152
    const alreadyStopped = nd.plotStatus.stopped;
153
    await nd.pauseCheck();
154
    if (nd.plotStatus.stopped && !alreadyStopped) {
155
      await nd.machine.command('SP,3'); // Raise pen now, overriding queued lowering.
156
      return;
157
    }
158
    if (nd.pen.phys.xpos == null) return;
159
    if (!move || move.length === 0) continue;
160
    const kind = move[0];
161
    if (kind === 'lower') {
162
      await nd.pen.lower(nd);
163
    } else if (kind === 'raise') {
164
      await nd.pen.raise(nd);
165
    } else if (kind === 'T3' || kind === 'TD') {
166
      await feedTimed(nd, move);
167
      if (nd.plotStatus.stopped) return;
168
    }
169
  }
170
}
171
172
/** Largest step rate the EBB can hold: one step per 40 µs tick. */
173
export const MAX_ISR_RATE = 2147483647;
174
175
async function feedTimed(nd, move) {
176
  const [kind, mov, [inches, xyzPos]] = move;
177
  const moveTimeMs = (kind === 'TD' ? 2 * mov[0] : mov[0]) / 25.0; // 25 ticks per ms
178
  if (!mov.every(Number.isSafeInteger) || mov[0] < 1 || peakRateCommand(kind, mov) > MAX_ISR_RATE) {
179
    // Last line of defense: never send a move the firmware can't execute.
180
    nd.log(`Refusing unsafe motion command ${kind},${mov.join(',')}; stopping.`);
181
    await nd.machine.command('SP,3');
182
    nd.pen.phys.zUp = true;
183
    nd.plotStatus.stopped = 107;
184
    return;
185
  }
186
  if (nd.options.preview) {
187
    nd.plotStatus.stats.ptEstimate += moveTimeMs;
188
  } else {
189
    await nd.machine.command(`${kind},${mov.join(',')}`);
190
    if (moveTimeMs > 50) await nd.clock.sleep(moveTimeMs - 30);
191
  }
192
  nd.plotStatus.stats.addDist(nd, inches);
193
  nd.pen.phys.xpos = xyzPos.xpos;
194
  nd.pen.phys.ypos = xyzPos.ypos;
195
  nd.pen.phys.accum1 = xyzPos.accum1;
196
  nd.pen.phys.accum2 = xyzPos.accum2;
197
}