UNPKG

@jcoreio/iron-pi-device-client

Version:

Client library for reading and writing Iron Pi input and output states

209 lines (182 loc) 7.6 kB
// @flow import logger from 'log4jcore' import EventEmitter from '@jcoreio/typed-event-emitter' import {MessageClient as IPCMessageClient} from 'socket-ipc' import VError from 'verror' import { METHOD_GET_NETWORK_SETTINGS, METHOD_GET_NETWORK_STATE, METHOD_IS_SSH_ENABLED, METHOD_SET_NETWORK_SETTINGS, METHOD_SET_SSH_ENABLED, METHOD_SET_SYSTEM_PASSWORD, UNIX_SOCKET_PATH } from './ipcTypes' import type { DeviceInputStates, HardwareInfo, MessageFromDriver, MessageToDriver, MethodValue, NetworkSettings, SetLEDs, SetOutputs } from './ipcTypes' export type {DetectedDevice, DeviceInputState, DeviceInputStates, DeviceModel, DeviceOutputState, HardwareInfo, LEDCommand, SetLEDs, SetOutputs} from './ipcTypes' const log = logger('iron-pi-device-client') export const EVENT_DEVICE_INPUT_STATES = 'deviceInputStates' export const EVENT_DEVICES_DETECTED = 'devicesDetected' export type IronPiDeviceClientEmittedEvents = { deviceInputStates: [DeviceInputStates], devicesDetected: [HardwareInfo], error: [Error], connection: [], // indicates a new connection to the UNIX socket used to communicate with the driver close: [], // indicates closing the UNIX socket used to communicate with the driver } type InFlightMethodRequest = { timeout: TimeoutID, resolve: (result: ?MethodValue) => void, reject: (err: Error) => void, } export class IronPiDeviceClient extends EventEmitter<IronPiDeviceClientEmittedEvents> { _ipcClient: Object _hardwareInfo: ?HardwareInfo /** mapping from request serial number to request state */ _inFlightRequests: Map<number, InFlightMethodRequest> = new Map() _curMethodSerial: number = 0 connected = false deviceInputStates: ?DeviceInputStates = undefined /** * @param unixSocketPath Optional override of default UNIX socket path, which is '/tmp/socket-iron-pi'. */ constructor({ unixSocketPath, host, port }: { unixSocketPath?: ?string, host?: ?string, port?: ?number }) { super() const socketOpts = port ? { host, port } : { path: unixSocketPath || UNIX_SOCKET_PATH } const ipcClient = this._ipcClient = new IPCMessageClient(socketOpts) ipcClient.on('message', this._onIPCMessage) ipcClient.on('error', (err: any) => this.emit('error', new VError(err, 'IronPiDeviceClient socket error'))) ipcClient.on('connection', () => this.emit('connection')) ipcClient.on('close', () => { this.connected = false this.emit('close') }) } start() { this._ipcClient.start() .catch((err: Error) => { log.error('could not connect to iron pi hardware agent', err) }) } hardwareInfo(): ?HardwareInfo { return this._hardwareInfo } setOutputs(setOutputs: SetOutputs) { const {outputs} = setOutputs if (!Array.isArray(outputs) || outputs.find(out => typeof out !== 'object')) throw Error('outputs property must be an array of Objects with the format {address: number, levels: Array<boolean>}') this._send({ setOutputs }) } setLEDs(setLEDs: SetLEDs) { const {leds} = setLEDs if (!Array.isArray(leds) || leds.find(cmd => typeof cmd !== 'object')) throw Error('leds property must be an array of Objects with the format {address: number, colors: string, onTime: number, offTime: number, idleTime: number}') this._send({ setLEDs }) } async getNetworkState(): Promise<NetworkSettings> { return ((await this._callMethod(METHOD_GET_NETWORK_STATE)): any) } async getNetworkSettings(): Promise<NetworkSettings> { return ((await this._callMethod(METHOD_GET_NETWORK_SETTINGS)): any) } async setNetworkSettings(settings: NetworkSettings): Promise<void> { await this._callMethod(METHOD_SET_NETWORK_SETTINGS, settings) } async isSSHEnabled(): Promise<boolean> { return ((await this._callMethod(METHOD_IS_SSH_ENABLED)): any) } async setSSHEnabled(enabled: boolean): Promise<void> { await this._callMethod(METHOD_SET_SSH_ENABLED, enabled) } async setSystemPassword(password: string): Promise<void> { await this._callMethod(METHOD_SET_SYSTEM_PASSWORD, password) } /** * Checks that a unit has no issues that need to be resolved before shipping * @return {string[]|[]} list of problems, or an empty array if there are no problems to report */ checkDeviceForShipping(): string[] { if (!this.connected) return ['not connected to the Iron Pi hardware agent'] const { deviceInputStates } = this if (!deviceInputStates) return ['Iron Pi hardware agent has not reported input states'] const { hasError, inputStates } = deviceInputStates const issues = [] if (hasError !== false) issues.push('Iron Pi hardware agent has errors') const [ mainBoardInputStates ] = inputStates if (mainBoardInputStates) { const { digitalInputs, analogInputs, connectButtonPressed } = mainBoardInputStates for (let inputIdx = 0; inputIdx < 8; ++inputIdx) { const inputName = `Input ${inputIdx + 1}` const digitalValue = digitalInputs[inputIdx] const analogValue = analogInputs[inputIdx] if (typeof digitalValue !== 'boolean') issues.push(`${inputName} digital input is missing`) else if (digitalValue) issues.push(`${inputName} digital input is high`) if (!Number.isFinite(analogValue)) issues.push(`${inputName} analog input is missing`) else if (Math.abs(analogValue) > 0.1) issues.push(`${inputName} analog reading is out of range: ${analogValue.toFixed(2)}V`) } if (connectButtonPressed) issues.push('Connect button is pressed') } else { issues.push('Missing input states for main board') } return issues } _callMethod(methodName: string, methodArg: MethodValue = {}): Promise<?MethodValue> { return new Promise((resolve: (?MethodValue) => void, reject: (Error) => void) => { if (this._ipcClient.isConnected()) { const methodSerial = ++this._curMethodSerial const timeout = setTimeout(() => { this._inFlightRequests.delete(methodSerial) reject(Error('method call timed out')) }, 20000) this._inFlightRequests.set(methodSerial, { timeout, resolve, reject }) this._send({ methodName, methodSerial, methodArg, }) } else { reject(Error('not connected to iron pi hardware agent')) } }) } _send(msg: MessageToDriver) { this._ipcClient.send(JSON.stringify(msg)) } _onIPCMessage = (event: {data: string}) => { try { const msg: MessageFromDriver = JSON.parse(event.data) log.trace('rx from driver:', msg) const {hardwareInfo, deviceInputStates, methodSerial, methodResult, methodError } = msg if (methodSerial) { const request: ?InFlightMethodRequest = this._inFlightRequests.get(methodSerial) if (request) { clearTimeout(request.timeout) if (methodError) { request.reject(Error(`method error: ${methodError}`)) } else { request.resolve(methodResult) } } else { log.error(`could not find method serial: ${methodSerial}`) } } if (hardwareInfo) { this._hardwareInfo = hardwareInfo this.connected = true this.emit(EVENT_DEVICES_DETECTED, hardwareInfo) } if (deviceInputStates) { this.deviceInputStates = deviceInputStates this.emit(EVENT_DEVICE_INPUT_STATES, deviceInputStates) } } catch (err) { log.error('could not process an incoming IPC message', err) } } } export default IronPiDeviceClient