@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
// @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