EventEmitter = require 'events' noble = require 'noble' serialport = require 'serialport' BleChannel = require './ble_channel.coffee' SerialChannel = require './serial_channel.coffee' # Agent that discovers spheros. class DiscoveryClass extends EventEmitter # Creates a new discovery agent. constructor: -> @_serialPorts = null @_peripherals = null @reset() @_started = false @_bleScanStarted = false noble.on 'stateChange', @_onBleStateChange.bind(@) noble.on 'discover', @_onBleDiscover.bind(@) @_powerState = noble.state # Starts discovering Bluetooth robots. # # @return {DiscoveryClass} this start: -> return if @_started @_started = true serialport.list @_onSerialPortList.bind(@) @_onBleStateChange noble.state @ # Stops discovering Bluetooth robots. # # @return {DiscoveryClass} this stop: -> return if @_started is false @_started = false if @_bleScanStarted is true @_bleScanStarted = false noble.stopScanning() @ # Erases the list of discovered Bluetooth devices. # # @return {DiscoveryClass} this reset: -> @_serialPorts = {} @_peripherals = {} @ # Discovers Bluetooth robots until a desired robot shows up. # # @param {String} sourceId the source ID of the communication channel to the # desired robot # @return {Promise} resolved with a communication channel to the # desired robot findChannel: (sourceId) -> @findChannels([sourceId]) .then (channels) -> channels[sourceId] # Discovers Bluetooth robots until a set of desired robots show up. # # @param {Array} sourceIds the source IDs of the communication # channels to the desired robots # @return {Promise>} resolved with an object that # maps source IDs to communication channels to the desired robots findChannels: (sourceIds) -> channelsLeft = sourceIds.length channels = {} for sourceId in sourceIds channels[sourceId] = null new Promise (resolve, reject) => @stop() @reset() onChannel = (channel) => sourceId = channel.sourceId if sourceId of channels and channels[sourceId] is null channels[sourceId] = channel channelsLeft -= 1 if channelsLeft is 0 @removeListener 'channel', onChannel @removeListener 'error', onError @stop() resolve channels else channel.close() onError = (error) => @removeListener 'channel', onChannel @removeListener 'error', onError @stop() reject error @addListener 'channel', onChannel @addListener 'error', onError @start() # Called when the Bluetooth LE's powered on state changes. # # @param {String} newState the new Bluetooth LE power state _onBleStateChange: (newState) -> @_powerState = newState if newState is 'poweredOn' and @_started is true and @_bleScanStarted is false @_bleScanStarted = true noble.startScanning [], true, (error) => if error @emit 'error', error # Called when a Bluetooth LE peripheral is found. # # @param {Peripheral} peripheral the bluetooth LE peripheral that was # discovered _onBleDiscover: (peripheral) -> return unless peripheral.connectable uuid = peripheral.id return if uuid of @_peripherals @_peripherals[uuid] = true BleChannel.fromPeripheral(peripheral) .then (bleChannel) => return if bleChannel is null if @_started is true @emit 'channel', bleChannel else bleChannel.close() .catch (error) => @emit 'error', error # Called when serialport returns a list of ports. # # @param {Error} error # @param {Array} ports list of ports _onSerialPortList: (error, ports) -> if error @emit 'error', error return unless ports and @_started is true for port in ports continue unless rfconnPath = port.comName @_onSerialPort rfconnPath # Called when a serial port is discovered. # # @param {String} rfconnPath the path to the serial port _onSerialPort: (rfconnPath) -> return if rfconnPath of @_serialPorts @_serialPorts[rfconnPath] = true @emit 'channel', new SerialChannel(rfconnPath) module.exports = new DiscoveryClass