-
Notifications
You must be signed in to change notification settings - Fork 1
/
programmer.js
402 lines (317 loc) · 11.4 KB
/
programmer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
'use strict'
const Serial = require('serialport')
const EventEmitter = require('events').EventEmitter
const fs = require('fs')
/**
* Instantiable bootloader programmer class. Provides access to serial port UART programmer
* @extends EventEmitter
*/
module.exports = class Programmer extends EventEmitter {
/**
* Create a programmer instance
* @param {number} baudRate - A standard UART baudrate, defaults to 115200
*/
constructor(port = '/dev/ttyUSB0', baudRate = 115200) {
super()
this.CRCLookup = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1c1, 0xf1ef
]
this.debugEnable()
// Debug level
this.debug = 0
// Set serial baud rate
this.baudRate = baudRate
// Open the USB port
this._configurePort().then(() => console.debug((`Port '${this._port}' opened sucessfully!`)))
}
async _configurePort(port = '/dev/ttyUSB0') {
// Fetch available USB ports
let usbPorts = await new Promise((resolve, reject) => {
return Serial.list((e, p) => {
if (e) return reject(e)
return resolve(p.filter(n => n.comName.indexOf('USB') > -1))
})
})
// Set instance port
this._port = usbPorts[0].comName
// Set up UART
const serialOptions = {
baudRate: this.baudRate,
dataBits: 8,
parity: 'none',
stopBits: 1
}
// Open first available (for now)
this.port = new Serial(this._port, serialOptions)
this.responseData = []
// Read data when it is received
this.port.on('data', data => this._handleData(data))
return true
}
//
// Private incoming data handler
//
_handleData(data) {
// Clear response timeout
clearTimeout(this.responseTimeout)
this.responseData.push(...Array.from(data))
if (this.responseData.length < 4 ||
this.responseData[this.responseData.length - 1] !== 0x04 ||
this.responseData[this.responseData.length - 2] == 0x10)
return
// Unescape control characters and strip front and back
let response = this.unescape(this.responseData.slice(1, -1))
// Empty response buffer
this.responseData = []
// Set the last response
this.lastResponse = response.slice(1, -2)
// Emit new processed data event
this.emit('newData', this.lastResponse)
}
/**
* Alias for checking connection status
* @return {boolean} - connection status
*/
get connected() {
return !!this.port && this.port.isOpen()
}
/**
* Enable debugging
*/
debugEnable() {
// Set up debugging
console._debug = 1
// Override debug method, haters
console.debug = (...args) => console._debug && console.log(...args)
}
debugDisable() {
// Clear debug flag
console._debug = 0
}
/**
* This resolves when the device is properly connected - a good starting point for a program
* @return {Promise} - a promise that resolves when the programmer is connected
*/
onceConnected() {
return new Promise((resolve, reject) => {
let f;
(f = () => {
if (this.connected) return resolve()
else if (this.stopped) return reject()
else return setTimeout(() => f(), 100)
})()
})
}
/**
* Cyclic Redundency Check (CRC) 16-bit data - Verified
* @param {array} data - an array of bytes to calculate the CRC for
* @return {array} - an array of 2 bytes that represent the CRC
*/
crc16(data) {
let i = 0
let crc = 0
data.forEach(byte => {
i = (crc >> 12) ^ (byte >> 4)
crc = this.CRCLookup[i & 0x000f] ^ (crc << 4)
i = (crc >> 12) ^ (byte >> 0)
crc = this.CRCLookup[i & 0x000f] ^ (crc << 4)
})
// Return unicode array
return [crc & 0x00ff, (crc >> 8) & 0x00ff]
}
/**
* Escape Control Characters
* @param {array} data - array of bytes to escape
* @return {array} - escaped byte array
*/
escape(data) {
// Special characters
const check = [0x10, 0x01, 0x04]
// Escaped arrat to be built
let escaped = []
let temp = []
// Escape the data
temp = data.map(d => (check.includes(d) ? [0x0010, d] : d))
temp.forEach(c => {
if (typeof c == 'object') escaped.push(...c)
else escaped.push(c)
})
// Return unicode array
return escaped
}
/**
* Unescape Control Characters
* @param {array} data - array of bytes to unescape
* @return {array} - unescaped byte array
*/
unescape(data) {
let escaping = false
let unescaped = []
data.forEach(c => {
if (escaping) {
unescaped.push(c)
escaping = false
} else if (c == 0x10) {
escaping = true
} else unescaped.push(c)
})
// Return unicode array
return unescaped
}
/**
* Send A Command - synchronous
* @param {array} command - byte array representing command
* @return {number} - length of command sent
*/
async send(command) {
// Escape control characters
command = this.escape(command)
// Build request
let request = [0x01, ...command, ...(this.escape(this.crc16(command))), 0x04]
// Verify connection
if (!this.connected) throw 'Port not connected, unable to write.'
// Write request to serial
await new Promise((resolve, reject) => {
this.port.write(Buffer.from(request), e => e ? reject(e) : resolve())
})
// Start timeout
clearTimeout(this.responseTimeout)
this.responseTimeout = setTimeout(() => {
console.debug('Bootloader response timeout.')
this.responseData = []
this.emit('responseTimeout', 'Error on command: ' + [...command])
}, 2000)
return request.length
}
/**
* Upload/Flash a Hex File
* @param {string} filename - location of Intel formatted hexfile to upload
* @return {Promise} - promise that resolves on upload success, rejects on error
* @fires Programmer#uploadProgress
*/
upload(filename = 'test.hex') {
// Read file line-by-line syncronously
var lines = fs.readFileSync(filename, 'utf-8').split('\n')
// Allow enough listeners to accomodate for each line
this.setMaxListeners(lines.length + 10)
// Check Intel HEX format
if (lines.find(l => l.length < 7)) throw ('Invalid hex file')
// Byte progress object
let bytes = {
total: lines.reduce((total, l) => total += (l.length - 1) / 2, 0),
sent: 0,
get percent() {
return bytes.sent / bytes.total
}
}
// Current line being sent
var currentLine = 0
// Send each line individually
return new Promise((resolve, reject) => {
this.per = 0
var sendLine = () => {
let line = lines[currentLine]
// Remove colon and convert to byte array
line = line
.slice(1)
.match(/.{1,2}/g)
.map(c => parseInt(c, 16))
// Send the line
this.send([0x03, ...line])
// If the command times out, reject promise
const timeoutCb = e => reject(e)
this.once('responseTimeout', timeoutCb)
// Wait for new data to be received
this.once('newData', d => {
// Remove timeout listener
this.removeListener('responseTimeout', timeoutCb)
// Update status bytes
bytes.sent += line.length
/**
* Upload Progress events
* @event Programmer#uploadProgress
* @type {object}
* @property {number} total - Total bytes to be sent
* @property {number} sent - Bytes sent
* @property {number} percent - Percentage of bytes sent
*/
if (bytes.percent > this.per + 0.01 || bytes.percent >= 1.0) {
this.per = bytes.percent
this.emit('uploadProgress', bytes)
}
// Write a dot to terminal
if (this.debug) process.stdout.write('.')
currentLine++
// Recurse or resolve
if (currentLine < lines.length - 1) sendLine()
else resolve(true)
})
}
// Recursively send lines until EOF is reached
sendLine()
})
}
/**
* Fetch bootloader version
* @return {Promise} - promise that resolves when the version is returned
*/
version() {
console.debug('Querying Bootloader Version..')
// Send version command
this.send([0x01])
// Create a promise
return new Promise((resolve, reject) => {
// If the command times out, reject promise
const timeoutCb = e => reject(e)
this.once('responseTimeout', timeoutCb)
// Once new data arrives, print the version (if not corrupted)
this.once('newData', d => {
// Verify command in response
if (d[0] !== 0x01) reject('Unexpected response type from bootloader')
// Format version hex characters
var prettyVersion = '0' + d[0] + '0' + d[1]
// Print version in debug mode
console.debug(`Version: ${prettyVersion}`)
// Remove timeout listener
this.removeListener('responseTimeout', timeoutCb)
// Resolve promise with formatted version string
resolve(prettyVersion)
})
}).catch(() => {
console.debug('Problem getting version.')
})
}
/**
* Fetch bootloader identifier
* @return {Promise} - promise that resolves when the version is returned
*/
identifier() {
console.debug('Querying Bootloader Identifier..')
// Send version command
this.send([0x06])
// Create a promise
return new Promise((resolve, reject) => {
// If the command times out, reject promise
const timeoutCb = e => reject(e)
this.once('responseTimeout', timeoutCb)
// Once new data arrives, print the version (if not corrupted)
this.once('newData', d => {
// Remove timeout listener
this.removeListener('responseTimeout', timeoutCb)
// Resolve promise with formatted version string
resolve(d)
})
}).catch((e) => {
console.debug('Problem getting identifier.', e)
})
}
/**
* Run program
*/
run() {
console.debug('Running Program..')
// Send run command
this.send([0x05])
}
}