CI Log in

zach/dotter

test/protocol.test.js 193 lines · 6.9 KB · javascript
1
import { describe, expect, test } from 'bun:test';
2
import { commandName, compareVersions, EBB } from '../src/ebb.js';
3
import { commands, connected, makeMachine } from './helpers.js';
4
5
/** Scripted transport: responds to each command via a function. */
6
class FakeTransport {
7
  constructor(respond) {
8
    this.respond = respond;
9
    this.out = [];
10
    this.sent = [];
11
  }
12
13
  async open() {}
14
15
  async close() {}
16
17
  async clearInput() {
18
    this.out = [];
19
  }
20
21
  async write(text) {
22
    for (const cmd of text.split('\r').filter(Boolean)) {
23
      this.sent.push(cmd);
24
      const r = this.respond(cmd);
25
      if (r != null) this.out.push(...[].concat(r));
26
    }
27
  }
28
29
  async readLine() {
30
    return this.out.length ? this.out.shift() : '';
31
  }
32
}
33
34
const EBB_V = 'EBBv13_and_above EB Firmware Version 3.0.3\r\n';
35
36
describe('helpers', () => {
37
  test('commandName', () => {
38
    expect(commandName('SM,10,1,1')).toBe('SM');
39
    expect(commandName('R')).toBe('R');
40
    expect(commandName('A,1')).toBe('A');
41
    expect(commandName('QG')).toBe('QG');
42
  });
43
44
  test('compareVersions', () => {
45
    expect(compareVersions('3.0.2', '3.0.2')).toBe(0);
46
    expect(compareVersions('3.0.10', '3.0.2')).toBe(1);
47
    expect(compareVersions('2.9', '3.0.2')).toBe(-1);
48
  });
49
});
50
51
describe('EBB serial layer', () => {
52
  test('connects, enters future syntax, reads nickname', async () => {
53
    const t = new FakeTransport((c) => {
54
      if (c === 'v') return EBB_V;
55
      if (c === 'CU,10,1') return 'OK\r\n';
56
      if (c === 'QT') return 'QT,plotbot\n';
57
      return `${c.slice(0, 2)}\n`;
58
    });
59
    const ebb = new EBB();
60
    expect(await ebb.connect(t)).toBe(true);
61
    expect(ebb.version).toBe('3.0.3');
62
    expect(ebb.name).toBe('plotbot');
63
    expect(t.sent).toEqual(['v', 'CU,10,1', 'QT']);
64
  });
65
66
  test('rejects firmware older than 3.0.2', async () => {
67
    const t = new FakeTransport((c) => (c === 'v' ? 'EBBv13_and_above EB Firmware Version 2.8.1\r\n' : 'OK\r\n'));
68
    const ebb = new EBB();
69
    expect(await ebb.connect(t)).toBe(false);
70
    expect(ebb.err).toContain('not supported');
71
    expect(t.sent).toEqual(['v']); // Nothing else is sent to an unsupported board.
72
  });
73
74
  test('rejects a device that is not an EBB', async () => {
75
    const t = new FakeTransport(() => 'hello\r\n');
76
    const ebb = new EBB();
77
    expect(await ebb.connect(t)).toBe(false);
78
    expect(t.sent).toEqual(['v', 'v']); // Two identification attempts, as plotink does.
79
  });
80
81
  test('query strips the echoed name; command checks the echo', async () => {
82
    const t = new FakeTransport((c) => {
83
      if (c === 'v') return EBB_V;
84
      if (c === 'QS') return 'QS,1421,-429\n';
85
      if (c === 'QG') return 'QG,3E\n';
86
      if (c === 'QE') return 'QE,16,0\n';
87
      return `${c.split(',')[0]}\n`;
88
    });
89
    const ebb = new EBB();
90
    await ebb.connect(t);
91
    expect(await ebb.querySteps()).toEqual([1421, -429]);
92
    expect(await ebb.queryStatusByte()).toBe(0x3e);
93
    expect(await ebb.motorsQueryEnabled()).toEqual([1, 0]);
94
    expect(await ebb.command('SM,10,0,0')).toBe(true);
95
  });
96
97
  test('an EBB error stops all further commands', async () => {
98
    const t = new FakeTransport((c) => {
99
      if (c === 'v') return EBB_V;
100
      if (c.startsWith('SM')) return '!8 Err: Unknown command\n';
101
      return `${c.split(',')[0]}\n`;
102
    });
103
    const ebb = new EBB();
104
    await ebb.connect(t);
105
    expect(await ebb.command('SM,1,1,1')).toBe(false);
106
    expect(ebb.err).toContain('Err');
107
    const before = t.sent.length;
108
    expect(await ebb.command('SP,0')).toBe(false);
109
    expect(await ebb.query('QS')).toBe(null);
110
    expect(t.sent.length).toBe(before); // Nothing more goes to the machine.
111
  });
112
113
  test('a missing reply is a timeout error', async () => {
114
    const t = new FakeTransport((c) => (c === 'v' ? EBB_V : c === 'QT' ? 'QT\n' : null));
115
    const ebb = new EBB();
116
    await ebb.connect(t);
117
    expect(await ebb.command('SP,1')).toBe(false);
118
    expect(ebb.err).toContain('timeout');
119
  });
120
121
  test('a reply for a different command is an error', async () => {
122
    const t = new FakeTransport((c) => (c === 'v' ? EBB_V : 'XX\n'));
123
    const ebb = new EBB();
124
    await ebb.connect(t);
125
    expect(await ebb.command('SP,1')).toBe(false);
126
    expect(ebb.err).toContain('Unexpected response');
127
  });
128
129
  test('int32 variables round-trip, including negatives', async () => {
130
    const { nd } = await connected({}, (n) => { n.options.homing = false; });
131
    for (const v of [0, 1, -1, 123456789, -2147483648, 2147483647]) {
132
      await nd.machine.varWriteInt32(v, 24);
133
      expect(await nd.machine.varReadInt32(24)).toBe(v);
134
    }
135
  });
136
});
137
138
describe('NextDraw connect sequence', () => {
139
  test('fresh machine: configures, raises pen, enables motors, sets home', async () => {
140
    const { sim } = await connected({}, (n) => { n.options.homing = false; });
141
    expect(commands(sim)).toEqual([
142
      'v', 'CU,10,1', 'QT',
143
      'CU,3,1', 'CU,4,16', 'CU,50,0', // connection setup
144
      'QC', // power check
145
      'QL,10', 'QL,11', // cached pen configuration (none yet)
146
      'SC,11,231', 'SC,12,154', 'SC,4,9720', 'SC,5,8280', // servo rates and positions
147
      'SP,1,45,2', // raise pen on pin B2
148
      'SC,8,1', 'SL,162,10', 'SL,142,11', // 1 PWM channel; cache pen config
149
      'CU,60,135', 'QE', 'QE', 'EM,1,1', 'SL,0,12', 'CS', // enable motors at 16x, not homed
150
      'QL,12', // homed?
151
      'CS', 'SL,0,24', 'SL,0,25', 'SL,0,26', 'SL,0,27', 'SL,0,28', 'SL,0,29', 'SL,0,30', 'SL,0,31',
152
      'SL,1,12', // set home here, zero offsets, mark homed
153
      'QS', 'QL,24', 'QL,25', 'QL,26', 'QL,27', 'QL,28', 'QL,29', 'QL,30', 'QL,31',
154
      'T3,1,0,0,0,0,0,0,3', // read position and clear accumulators
155
    ]);
156
    expect(sim.violations).toEqual([]);
157
  });
158
159
  test('reconnect: pen and homing state are reused, nothing moves', async () => {
160
    const { nd, sim, clock } = await connected({ startX: 2, startY: 3 });
161
    await nd.moveto(1, 1);
162
    await nd.disconnect();
163
    const n = sim.log.length;
164
    const { NextDraw } = await import('../src/nextdraw.js');
165
    const nd2 = new NextDraw({ transport: sim, clock, log: () => {} });
166
    nd2.interactive();
167
    expect(await nd2.connect()).toBe(true);
168
    const cmds = sim.log.slice(n).map((l) => l.cmd);
169
    expect(cmds.some((c) => /^(SP|SM|T3,[^1]|TD|HM|EM)/.test(c))).toBe(false);
170
    expect(nd2.currentPos()[0]).toBeCloseTo(1, 3);
171
    expect(nd2.currentPos()[1]).toBeCloseTo(1, 3);
172
  });
173
174
  test('connect fails cleanly without a machine', async () => {
175
    const { nd, sim, messages } = makeMachine();
176
    sim.unplug();
177
    expect(await nd.connect()).toBe(false);
178
    expect(nd.plotStatus.stopped).toBe(101);
179
    expect(messages.join(' ')).toContain('Failed to connect');
180
  });
181
182
  test('errors.connect = true throws instead', async () => {
183
    const { nd, sim } = makeMachine();
184
    sim.unplug();
185
    nd.errors.connect = true;
186
    await expect(nd.connect()).rejects.toThrow('Failed to connect');
187
  });
188
189
  test('motion API requires a connection', async () => {
190
    const { nd } = makeMachine();
191
    await expect(nd.moveto(1, 1)).rejects.toThrow('Not connected');
192
  });
193
});