UNPKG

akid-link

Version:

Porvide local hardware function to akid

392 lines (354 loc) 14.9 kB
const {SerialPort} = require('serialport'); const ansi = require('ansi-string'); const Session = require('./session'); const Arduino = require('../upload/arduino'); const Microbit = require('../upload/microbit'); const usbId = require('../lib/usb-id'); class SerialportSession extends Session { constructor (socket, userDataPath, toolsPath) { super(socket); this.userDataPath = userDataPath; this.toolsPath = toolsPath; this._type = 'serialport'; this.peripheral = null; this.peripheralParams = null; this.services = null; this.reportedPeripherals = {}; this.connectStateDetectorTimer = null; this.peripheralsScanorTimer = null; this.isRead = false; this.isInDisconnect = false; } async didReceiveCall (method, params, completion) { switch (method) { case 'discover': this.discover(params); completion(null, null); break; case 'connect': await this.connect(params); completion(null, null); break; case 'disconnect': await this.disconnect(); completion(null, null); break; case 'updateBaudrate': completion(await this.updateBaudrate(params), null); break; case 'write': completion(await this.write(params), null); break; case 'read': await this.read(params); completion(null, null); break; case 'upload': completion(await this.upload(params), null); break; case 'uploadFirmware': completion(await this.uploadFirmware(params), null); break; case 'getServices': completion((this.services || []).map(service => service.uuid), null); break; case 'pingMe': completion('willPing', null); this.sendRemoteRequest('ping', null, result => { console.log(`Got result from ping: ${result}`); }); break; default: throw new Error(`Method not found`); } } discover (params) { if (this.services) { throw new Error('cannot discover when connected'); } const {filters} = params; if (!Array.isArray(filters.pnpid) || filters.pnpid.length < 1) { throw new Error('discovery request must include filters'); } this.reportedPeripherals = {}; this.peripheralsScanorTimer = setInterval(() => { SerialPort.list().then(peripheral => { this.onAdvertisementReceived(peripheral, filters); }); }, 100); } onAdvertisementReceived (peripheral, filters) { if (peripheral) { peripheral.forEach(device => { const vendorId = String(device.vendorId).toUpperCase(); const productId = String(device.productId).toUpperCase(); const pnpid = `USB\\VID_${vendorId}&PID_${productId}`; const name = usbId[pnpid] ? usbId[pnpid] : 'Unknown device'; if (filters.pnpid.includes('*')) { this.reportedPeripherals[device.path] = device; this.sendRemoteRequest('didDiscoverPeripheral', { peripheralId: device.path, name: `${name} (${device.path})` }); } else if (filters.pnpid.includes(pnpid)) { this.reportedPeripherals[device.path] = device; this.sendRemoteRequest('didDiscoverPeripheral', { peripheralId: device.path, name: `${name} (${device.path})` }); } }); } } connect (params, isConnectAfterUpload = false) { return new Promise((resolve, reject) => { if (this.peripheral && this.peripheral.isOpen === true) { return reject(new Error('already connected to peripheral')); } const {peripheralId, peripheralConfig} = params; const peripheral = this.reportedPeripherals[peripheralId]; if (!peripheral) { return reject(new Error(`invalid peripheral ID: ${peripheralId}`)); } if (this.peripheralsScanorTimer) { clearInterval(this.peripheralsScanorTimer); this.peripheralsScanorTimer = null; } const port = new SerialPort({ path: peripheral.path, baudRate: peripheralConfig.config.baudRate, dataBits: peripheralConfig.config.dataBits, stopBits: peripheralConfig.config.stopBits, autoOpen: false }); const rts = (typeof peripheralConfig.config.rts === 'undefined') ? true : peripheralConfig.config.rts; const dtr = (typeof peripheralConfig.config.dtr === 'undefined') ? true : peripheralConfig.config.dtr; try { port.open(openErr => { if (openErr) { if (isConnectAfterUpload === true) { this.sendRemoteRequest('peripheralUnplug', null); } return reject(new Error(openErr)); } port.set({rts: rts, dtr: dtr}, setErr => { if (setErr) { if (isConnectAfterUpload === true) { this.sendRemoteRequest('peripheralUnplug', null); } return reject(new Error(setErr)); } this.peripheral = port; this.peripheralParams = params; // Scan COM status prevent device pulled out this.connectStateDetectorTimer = setInterval(() => { if (this.peripheral.isOpen === false) { clearInterval(this.connectStateDetectorTimer); this.disconnect(); this.sendRemoteRequest('peripheralUnplug', null); } }, 10); // Only when the receiver function is set, can isopen detect that the device is pulled out // A strange features of npm serialport package port.on('data', rev => { this.onMessageCallback(rev); }); port.on('error', error => { console.log('Akid Link Error:', error); this.disconnect(); this.sendRemoteRequest('peripheralUnplug', null); }); resolve(); }); }); } catch (err) { reject(err); } }); } onMessageCallback (rev) { const params = { encoding: 'base64', message: rev.toString('base64') }; if (this.isRead) { this.sendRemoteRequest('onMessage', params); } } updateBaudrate (params) { return new Promise((resolve, reject) => { if (!this.isInDisconnect) { this.peripheralParams.peripheralConfig.config.baudRate = params.baudRate; this.peripheral.update(params, err => { if (err) { return reject(new Error(`Error while attempting to update baudrate: ${err.message}`)); } const rts = (typeof this.peripheralParams.peripheralConfig.config.rts === 'undefined') ? true : this.peripheralParams.peripheralConfig.config.rts; const dtr = (typeof this.peripheralParams.peripheralConfig.config.dtr === 'undefined') ? true : this.peripheralParams.peripheralConfig.config.dtr; // After update baudrate, the rts and dtr will be automatically modified, // we have to set them again. this.peripheral.set({rts: rts, dtr: dtr}, setErr => { if (setErr) { this.sendRemoteRequest('peripheralUnplug', null); return reject(new Error(setErr)); } return resolve(); }); }); } }); } write (params) { return new Promise((resolve, reject) => { const {message, encoding} = params; const buffer = new Buffer.from(message, encoding); try { if (!this.isInDisconnect) { this.peripheral.write(buffer, 'binary', err => { if (err) { return reject(new Error(`Error while attempting to write: ${err.message}`)); } }); this.peripheral.drain(() => resolve(buffer.length)); } return resolve(); } catch (err) { return reject(err); } }); } read () { this.isRead = true; } disconnect () { this.isInDisconnect = true; return new Promise((resolve, reject) => { if (this.peripheral && this.peripheral.isOpen === true) { if (this.connectStateDetectorTimer) { clearInterval(this.connectStateDetectorTimer); this.connectStateDetectorTimer = null; } const peripheral = this.peripheral; try { peripheral.pause(); // clear all cache data peripheral.flush(() => { peripheral.close(error => { if (error) { this.isInDisconnect = false; return reject(Error(error)); } this.isInDisconnect = false; return resolve(); }); }); } catch (err) { this.isInDisconnect = false; return reject(err); } } }); } async upload (params) { const {message, config, encoding, library} = params; const code = new Buffer.from(message, encoding).toString(); let tool; const {baudRate} = this.peripheralParams.peripheralConfig.config; switch (config.type) { case 'arduino': tool = new Arduino(this.peripheral.path, config, this.userDataPath, this.toolsPath, this.sendstd.bind(this)); try { const exitCode = await tool.build(code, library); if (exitCode === 'Success') { try { this.sendstd(`${ansi.clear}Disconnect serial port\n`); await this.disconnect(); this.sendstd(`${ansi.clear}Disconnected successfully, flash program starting...\n`); await tool.flash(); await this.connect(this.peripheralParams, true); this.sendRemoteRequest('uploadSuccess', null); } catch (err) { this.sendRemoteRequest('uploadError', { message: ansi.red + err.message }); // if error in flash step. It is considered that the device has been removed. this.sendRemoteRequest('peripheralUnplug', null); } } } catch (err) { this.sendRemoteRequest('uploadError', { message: ansi.red + err.message }); } break; case 'microbit': tool = new Microbit(this.peripheral.path, config, this.userDataPath, this.toolsPath, this.sendstd.bind(this)); try { await this.disconnect(); await tool.flash(code, library); await this.connect(this.peripheralParams, true); await this.updateBaudrate({baudRate: 115200}); this.sendstd(`${ansi.clear}Reset device\n`); await this.write({message: '04', encoding: 'hex'}); await this.updateBaudrate({baudRate: baudRate}); this.sendRemoteRequest('uploadSuccess', null); } catch (err) { this.sendRemoteRequest('uploadError', { message: ansi.red + err }); this.sendRemoteRequest('peripheralUnplug', null); } break; } } async uploadFirmware (params) { let tool; switch (params.type) { case 'arduino': tool = new Arduino(this.peripheral.path, params, this.userDataPath, this.toolsPath, this.sendstd.bind(this)); try { this.sendstd(`${ansi.clear}Disconnect serial port\n`); await this.disconnect(); this.sendstd(`${ansi.clear}Disconnected successfully, flash program starting...\n`); await tool.flashRealtimeFirmware(); await this.connect(this.peripheralParams, true); this.sendRemoteRequest('uploadSuccess', null); } catch (err) { this.sendRemoteRequest('uploadError', { message: ansi.red + err.message }); } break; } } sendstd (message) { if (this._socket) { this.sendRemoteRequest('uploadStdout', { message: message }); } } dispose () { this.disconnect(); super.dispose(); this.socket = null; this.peripheral = null; this.peripheralParams = null; this.services = null; this.reportedPeripherals = null; if (this.connectStateDetectorTimer) { clearInterval(this.connectStateDetectorTimer); this.connectStateDetectorTimer = null; } if (this.peripheralsScanorTimer) { clearInterval(this.peripheralsScanorTimer); this.peripheralsScanorTimer = null; } } } module.exports = SerialportSession;