UNPKG

@commaai/pandajs

Version:

JavaScript interface for communicating with Panda devices.

287 lines (253 loc) 6.74 kB
import { packCAN, unpackCAN } from 'can-message'; import Event from 'weakmap-event'; import { partial } from 'ap'; import now from 'performance-now'; import raf from 'raf'; import { timeout } from 'thyming'; import wait from './delay'; // how many messages to batch at maximum when reading as fast as we can const MAX_MESSAGE_QUEUE = 5000; const MessageEvent = Event(); const ErrorEvent = Event(); const ConnectEvent = Event(); const DisconnectEvent = Event(); export default class Panda { constructor(options) { // setup event handlers this.onMessage = partial(MessageEvent.listen, this); this.onError = partial(ErrorEvent.listen, this); this.onConnect = partial(ConnectEvent.listen, this); this.onDisconnect = partial(DisconnectEvent.listen, this); // initialize device object this.device = options.device; this.device.onError(partial(ErrorEvent.broadcast, this)); this.device.onConnect(this.connectHandler.bind(this)); this.device.onDisconnect(this.disconnectHandler.bind(this)); // member variables this.paused = true; this.messageQueue = []; // function binding this.readLoop = this.readLoop.bind(this); this.flushMessageQueue = this.flushMessageQueue.bind(this); } // state getters isConnected() { return !!this.connected; } isPaused() { return !!this.paused; } // methods async connect() { if (this.isConnected()) { return this.connected; } await this.device.connect(); var serialNumber = await this.getSerialNumber(); this.connectHandler(serialNumber); return serialNumber; } async disconnect() { if (!this.isConnected()) { return false; } return this.device.disconnect(); } async start() { await this.connect(); return this.unpause(); } async pause() { var wasPaused = this.isPaused(); this.paused = true; return !wasPaused; } async resume() { return this.unpause(); } async unpause() { var wasPaused = this.isPaused(); if (!wasPaused) { return false; } this.paused = false; this.startReading(); return wasPaused; } // vendor API methods async getHealth() { let buf = await this.vendorRequest('health', { request: 0xd2, value: 0, index: 0 }, 13); let voltage = buf.readUInt32LE(0) / 1000; let current = buf.readUInt32LE(4) / 1000; let isStarted = buf.readInt8(8) === 1; let controlsAreAllowed = buf.readInt8(9) === 1; let isGasInterceptorDetector = buf.readInt8(10) === 1; let isStartSignalDetected = buf.readInt8(11) === 1; let isStartedAlt = buf.readInt8(12) === 1; return { voltage, current, isStarted, controlsAreAllowed, isGasInterceptorDetector, isStartSignalDetected, isStartedAlt }; } async getDeviceMetadata() { let buf = await this.vendorRequest('getDeviceMetadata', { request: 0xd0, value: 0, index: 0 }, 0x20); let serial = buf.slice(0, 0x10); // serial is the wifi style serial let secret = buf.slice(0x10, 0x10 + 10); let hashSig = buf.slice(0x1c); return [serial.toString(), secret.toString()]; } async getSerialNumber() { var [serial, secret] = await this.getDeviceMetadata(); return serial; } async getSecret() { var [serial, secret] = await this.getDeviceMetadata(); return secret; } async getVersion() { let buf = await this.vendorRequest('getVersion', { request: 0xd6, value: 0, index: 0 }, 0x40); return buf.toString(); } async getType() { let buf = await this.vendorRequest('getType', { request: 0xc1, value: 0, index: 0 }, 0x40); return buf.length ? buf[0] : null; } async isWhite() { return await this.getType() === 1; } async isGrey() { return await this.getType() === 2; } async isBlack() { return await this.getType() === 3; } async hasObd() { return await this.getType() > 2; } async setObd(obd) { await this.vendorWrite('setObd', { request: 0xdb, value: obd ? 1 : 0, index: 0 }); } async setSafetyMode(mode) { await this.vendorWrite('setSafetyMode', { request: 0xdc, value: mode, index: 0 }); } // i/o wrappers async vendorRequest (event, controlParams, length) { try { let result = await this.device.vendorRequest(controlParams, length); return result.data; } catch (err) { ErrorEvent.broadcast(this, { event: 'Panda.' + event + ' failed', error: err }); throw err; } } async vendorWrite (event, controlParams, message) { if (!message || !message.length) { message = Buffer.from([]); } try { let result = await this.device.vendorWrite(controlParams, message); return result.data; } catch (err) { ErrorEvent.broadcast(this, { event: 'Panda.' + event + ' failed', error: err }); throw err; } } // event handlers connectHandler(usbId) { this.connected = usbId; ConnectEvent.broadcast(this, usbId); } disconnectHandler() { const previousConnection = this.connected; this.connected = false; this.paused = true; DisconnectEvent.broadcast(this, previousConnection); } // message queueing and flushing needsFlushMessageQueue() { this.needsFlush = true; if (this.flushEvent) { return this.flushEvent; } var unlisten = raf(this.flushMessageQueue); this.flushEvent = () => { raf.cancel(unlisten); this.flushEvent = false; }; return this.flushEvent; } flushMessageQueue() { this.flushEvent(); if (this.needsFlush && this.messageQueue.length) { let messageQueue = this.messageQueue; this.messageQueue = []; this.needsFlush = false; MessageEvent.broadcast(this, messageQueue); } } // internal reading loop startReading() { if (this.isReading) { return true; } if (this.isPaused()) { return false; } // start loop! this.isReading = true; this.readLoop(); } async readLoop() { if (this.isPaused()) { this.isReading = false; return false; } this.isReading = true; for (let i = 0; i < MAX_MESSAGE_QUEUE; ++i) { let data = await this.device.nextMessage(); let receiptTime = now() / 1000 let canMessages = unpackCAN(data); if (!canMessages.length) { await wait(1); continue; } this.messageQueue.push({ time: receiptTime, canMessages }); this.needsFlushMessageQueue(); } this.needsFlushMessageQueue(); // repeat! timeout(this.readLoop); } }