UNPKG

brickpi-coffeescript

Version:

BrickPi API implementation in CoffeeScript for JS/CS

420 lines (353 loc) 15.8 kB
{SerialPort} = require 'serialport' Promise = require 'bluebird' BrickPiError = require './BrickPiError' SerialPortError = require './SerialPortError' {replicate, sum, clamp, sign, limit} = require './utils' PORT_A = 0 PORT_B = 1 PORT_C = 2 PORT_D = 3 PORT_1 = 0 PORT_2 = 1 PORT_3 = 2 PORT_4 = 3 MASK_D0_M = 0x01 MASK_D1_M = 0x02 MASK_9V = 0x04 MASK_D0_S = 0x08 MASK_D1_S = 0x10 BYTE_MSG_TYPE = 0 # MSG_TYPE is the first byte. MSG_TYPE_CHANGE_ADDR = 1 # Change the UART address. MSG_TYPE_SENSOR_TYPE = 2 # Change/set the sensor type. MSG_TYPE_VALUES = 3 # Set the motor speed and direction, and return the sesnors and encoders. MSG_TYPE_E_STOP = 4 # Float motors immidately MSG_TYPE_TIMEOUT_SETTINGS = 5 # Set the timeout # New UART address (MSG_TYPE_CHANGE_ADDR) BYTE_NEW_ADDRESS = 1 # Sensor setup (MSG_TYPE_SENSOR_TYPE) BYTE_SENSOR_1_TYPE = 1 BYTE_SENSOR_2_TYPE = 2 BYTE_TIMEOUT=1 TYPE_MOTOR_PWM = 0 TYPE_MOTOR_SPEED = 1 TYPE_MOTOR_POSITION = 2 TYPE_SENSOR_RAW = 0 # - 31 TYPE_SENSOR_LIGHT_OFF = 0 TYPE_SENSOR_LIGHT_ON = (MASK_D0_M | MASK_D0_S) TYPE_SENSOR_TOUCH = 32 TYPE_SENSOR_ULTRASONIC_CONT = 33 TYPE_SENSOR_ULTRASONIC_SS = 34 TYPE_SENSOR_RCX_LIGHT = 35 # tested minimally TYPE_SENSOR_COLOR_FULL = 36 TYPE_SENSOR_COLOR_RED = 37 TYPE_SENSOR_COLOR_GREEN = 38 TYPE_SENSOR_COLOR_BLUE = 39 TYPE_SENSOR_COLOR_NONE = 40 TYPE_SENSOR_I2C = 41 TYPE_SENSOR_I2C_9V = 42 BIT_I2C_MID = 0x01 # Do one of those funny clock pulses between writing and reading. defined for each device. BIT_I2C_SAME = 0x02 # The transmit data, and the number of bytes to read and write isn't going to change. defined for each device. INDEX_RED = 0 INDEX_GREEN = 1 INDEX_BLUE = 2 INDEX_BLANK = 3 module.exports = class BrickPi constructor: -> @address = [1, 2] # Communication addresses @timeout = 0 # Communication timeout (how long in ms since the last valid # communication before floating the motors). 0 disables the timeout. # Motors @motorSpeed = replicate 0, 4 # Motor speeds, from -255 to 255 @motorEnable = replicate 0, 4 # Motor enable/disable # Encoders @encoderOffset = Array 4 # Encoder offsets (possibly not yet implemented, but it should be) @encoder = Array 4 # Encoder values # Sensors @sensor = Array 4 # Primary sensor values @sensorArray = Array 4 for [1..4] # For more sensor values for the sensor (e.g. for color sensor FULL mode). @sensorType = replicate 0, 4 # Sensor types @sensorSettings = Array 8 for [1..4] # Sensor settings, used for specifying I2C settings. # I2C @sensorI2CDevices = Array 4 # How many I2C devices are on each bus (1 - 8). @sensorI2CSpeed = Array 4 # The I2C speed. @sensorI2CAddr = Array 8 for [1..4] # The I2C address of each device on each bus. @sensorI2CWrite = Array 8 for [1..4] # How many bytes to write @sensorI2CRead = Array 8 for [1..4] # How many bytes to read @sensorI2COut = Array 16 for [1..8] for [1..4] # The I2C bytes to write @sensorI2CIn = Array 16 for [1..8] for [1..4] # The I2C input buffers @zeroFlags() # @flags buffer is used for writing data @retried = 0 @serialPort = Promise.promisifyAll new SerialPort '/dev/ttyAMA0', baudrate: 500000, false @receiveBuffer = [] zeroFlags: -> @flags = replicate 0, 256 @bitOffset = 0 setFlags: (inputFlags) -> @flags[i] = flag for flag, i in inputFlags changeAddress: (oldAddress, newAddress) -> @flags[BYTE_MSG_TYPE] = MSG_TYPE_CHANGE_ADDR @flags[BYTE_NEW_ADDRESS] = newAddress @send oldAddress, 2, @flags .then => @receive 5 .then (inputflags) => unless inputflags.length is 1 and @flags[BYTE_MSG_TYPE] is MSG_TYPE_CHANGE_ADDR throw new BrickPiError "Incorrect message received while changing address" @setFlags inputFlags # setTimeout: -> # for i in [0, 1] # @flags[BYTE_MSG_TYPE] = MSG_TYPE_TIMEOUT_SETTINGS # @flags[BYTE_TIMEOUT] = @timeout & 0xFF # @flags[BYTE_TIMEOUT + 1] = (@timeout / 256 ) & 0xFF # @flags[BYTE_TIMEOUT + 2] = (@timeout / 65536 ) & 0xFF # @flags[BYTE_TIMEOUT + 3] = (@timeout / 16777216) & 0xFF # @send @address[i], 5, @flags # [res, BytesReceived, inputFlags] = @receive 0.002500 # if res #error # return -1 # for j in [0...inputFlags.length] # @flags[j] = inputFlags[j] # if not (BytesReceived is 1 and @flags[BYTE_MSG_TYPE] is MSG_TYPE_TIMEOUT_SETTINGS) # return -1 # i+=1 # return 0 # def motorRotateDegree(power, deg, port, sampling_time=.1): # """Rotate the selected motors by specified degre # Args: # power : an array of the power values at which to rotate the motors (0-255) # deg : an array of the angle's (in degrees) by which to rotate each of the motor # port : an array of the port's on which the motor is connected # sampling_time : (optional) the rate(in seconds) at which to read the data in the encoders # Returns: # 0 on success # Usage: # Pass the arguments in a list. if a single motor has to be controlled then the arguments should be # passed like elements of an array, e.g, motorRotateDegree([255],[360],[PORT_A]) or # motorRotateDegree([255, 255],[360, 360],[PORT_A, PORT_B]) # """ # num_motor=power.length #Number of motors being used # init_val=[0]*num_motor # final_val=[0]*num_motor # @updateValues() # for i in [0...num_motor] # @motorEnable[port[i]] = 1 #Enable the Motors # power[i]=abs(power[i]) # @motorSpeed[port[i]] = power[i] if deg[i]>0 else -power[i] #For running clockwise and anticlockwise # init_val[i]=@encoder[port[i]] #Initial reading of the encoder # final_val[i]=init_val[i]+(deg[i]*2) #Final value when the motor has to be stopped;One encoder value counts for 0.5 degrees # run_stat=[0]*num_motor # while True: # result = @updateValues() #Ask BrickPi to update values for sensors/motors # if not result # for i in [0...num_motor] #Do for each of the motors # if run_stat[i]==1 # continue # if(deg[i]>0 and final_val[i]>init_val[i]) or (deg[i]<0 and final_val[i]<init_val[i]) #Check if final value reached for each of the motors # init_val[i]=@encoder[port[i]] #Read the encoder degrees # else: # run_stat[i]=1 # @motorSpeed[port[i]]=-power[i] if deg[i]>0 else power[i] #Run the motors in reverse direction to stop instantly # @updateValues() # time.sleep(.04) # @motorEnable[port[i]] = 0 # @updateValues() # time.sleep(sampling_time) #sleep for the sampling time given (default100 ms) # if(all(e==1 for e in run_stat)) #If all the motors have already completed their rotation, then stop # break # return 0 # Reads bits from @flags, moving the cursor by number of `bits` _getBits: (byteOffset, bitOffset, bits) -> result = 0 for i in [bits...0] result <<= 1 offset = bitOffset + @bitOffset + i - 1 result |= @flags[byteOffset + offset // 8] >> (offset % 8) & 0x01 @bitOffset += bits return result # Writes bits to @flags, moving the cursor by number of `bits` _addBits: (byteOffset, bitOffset, bits, value) -> for i in [0...bits] if value & 0x01 offset = bitOffset + @bitOffset + i @flags[byteOffset + offset // 8] |= 0x01 << (offset % 8) value //= 2 @bitOffset += bits # How many bits are needed to represent an integer # returns 1 to 8a _bitsNeeded: (value) -> for i in [0...32] break if value is 0 value //= 2 return i _readBits: (bits) -> @_getBits 1, 0, bits # Setup the sensors setupSensors: -> for i in [0, 1] @zeroFlags() @flags[BYTE_MSG_TYPE] = MSG_TYPE_SENSOR_TYPE @flags[BYTE_SENSOR_1_TYPE] = @sensorType[PORT_1 + i * 2] # PORT_1, PORT_3 @flags[BYTE_SENSOR_2_TYPE] = @sensorType[PORT_2 + i * 2] # PORT_2, PORT_4 for j in [0, 1] port = i * 2 + j # 0, 1, 2, 3 # Setup I2C sensors sensorTypeFlag = @flags[BYTE_SENSOR_1_TYPE + j] # 1_TYPE, 2_TYPE, 1_TYPE, 2_TYPE if sensorTypeFlag in [TYPE_SENSOR_I2C, TYPE_SENSOR_I2C_9V] @_addBits 3, 0, 8, @sensorI2CSpeed[port] # normalize @sensorI2CDevices[port] clamp @sensorI2CDevices, port, 1, 8 @_addBits 3, 0, 3, @sensorI2CDevices[port] - 1 for device in [0...@sensorI2CDevices[port]] @_addBits 3, 0, 7, @sensorI2CAddr[port][device] >> 1 @_addBits 3, 0, 2, @sensorSettings[port][device] if @sensorSettings[port][device] & BIT_I2C_SAME @_addBits 3, 0, 4, @sensorI2CWrite[port][device] @_addBits 3, 0, 4, @sensorI2CRead[port][device] for out_byte in [0...@sensorI2CWrite[port][device]] @_addBits 3, 0, 8, @sensorI2COut[port][device][out_byte] messageLength = (@bitOffset + 7) // 8 + 3 #eq to UART_TX_BYTES @send @address[i], messageLength, @flags .then => @receive 500 .then (inputflags) => unless inputflags.length is 1 and @flags[BYTE_MSG_TYPE] is MSG_TYPE_SENSOR_TYPE throw new BrickPiError "Incorrect message received while changing address" @setFlags inputFlags # Computes [direction, speed] vector given possibly negative integer value # @direction 0 maps to forward, 1 to backward _speedVector: (speed) -> [direction, speed] = sign speed [direction, limit speed, 255] # Send all attributes to the brick updateValues: -> ret = false for i in [0, 1] if not ret @retried = 0 #Retry Communication from here, if failed @zeroFlags() @flags[BYTE_MSG_TYPE] = MSG_TYPE_VALUES # Write data for encoders for j in [0, 1] port = i * 2 + j # 0, 1, 2, 3 # User reset the encoder if @encoderOffset[port] # 1 for set encoder, 5 bits for the size of the new offset value in bits # then the value and one bit for direction @_addBits 1, 0, 1, 1 [tempEncoderDirection, tempValue] = sign @encoderOffset[port] tempBitsNeeded = @_bitsNeeded(tempValue) + 1 @_addBits 1, 0, 5, tempBitsNeeded @_addBits 1, 0, tempBitsNeeded, tempValue << 1 | tempEncoderDirection else # 0 not setting encoders @_addBits 1, 0, 1, 0 # Write data for motor speeds for j in [0, 1] port = i * 2 + j # 0, 1, 2, 3 [direction, speed] = _speedVector @motorSpeed[port] # 8 bits for speed, 1 bit for direction, 1 bit for enabling @_addBits 1, 0, 10, speed << 2 | direction << 1 | @motorEnable[port] # Write data for I2C sensors for j in [0, 1] port = i * 2 + j if @sensorType[port] in [TYPE_SENSOR_I2C, TYPE_SENSOR_I2C_9V] for device in [0...@sensorI2CDevices[port]] if not (@sensorSettings[port][device] & BIT_I2C_SAME) @_addBits 1, 0, 4, @sensorI2CWrite[port][device] @_addBits 1, 0, 4, @sensorI2CRead[port][device] for out_byte in [0...@sensorI2CWrite[port][device]] @_addBits 1, 0, 8, @sensorI2COut[port][device][out_byte] device += 1 messageLength = (@bitOffset + 7) // 8 + 1 #eq to UART_TX_BYTES @send @address[i], messageLength, @flags .then => @receive 8 .then (inputFlags) -> @setFlags inputFlags @encoderOffset[i * 2 + PORT_A] = 0 @encoderOffset[i * 2 + PORT_B] = 0 if @flags[BYTE_MSG_TYPE] isnt MSG_TYPE_VALUES throw new SerialPortError "Incorrect returned message type" .catch SerialPortError, => if @retried < 2 ret = yes @retried++ # continue in the outside loop else throw new BrickPiError "Failed retry" # return from updateValues .then => ret = false # Read from beginning @bitOffset = 0 # Read encoder values tempBitsUsed = [] tempBitsUsed.append @_readBits 5 tempBitsUsed.append @_readBits 5 for j in [0, 1] tempEncoderVal = @_readBits tempBitsUsed[j] direction = if tempEncoderVal & 0x01 then -1 else 1 @encoder[j + i * 2] = direction * (tempEncoderVal // 2) # Read sensors for j in [0, 1] port = j + i * 2 # 0, 1, 2, 3 sensorType = @sensorType[port] if sensorType is TYPE_SENSOR_TOUCH @sensor[port] = @_readBits 1 else if sensorType in [TYPE_SENSOR_ULTRASONIC_CONT, TYPE_SENSOR_ULTRASONIC_SS] @sensor[port] = @_readBits 8 else if sensorType is TYPE_SENSOR_COLOR_FULL @sensor[port] = @_readBits 3 colorSensorValues = @sensorArray[port] colorSensorValues[INDEX_BLANK] = @_readBits 10 colorSensorValues[INDEX_RED] = @_readBits 10 colorSensorValues[INDEX_GREEN] = @_readBits 10 colorSensorValues[INDEX_BLUE] = @_readBits 10 # Read IC2 sensors else if sensorType in [TYPE_SENSOR_I2C, TYPE_SENSOR_I2C_9V] @sensor[port] = @_readBits @sensorI2CDevices[port] for device in [0...@sensorI2CDevices[port]] if @sensor[port] & 0x01 << device for in_byte in [0...@sensorI2CRead[port][device]] @sensorI2CIn[port][device][in_byte] = @_readBits 8 #For all the light, color and raw sensors else @sensor[j + i * 2] = @_readBits 10 # @returns a promise resolved once the serial port connection has been established setup: -> @serialPort.on 'error', (error) -> console.log "Serial port error: #{error}" @serialPort.on 'close', -> console.log "Serial port closed" @serialPort.on 'data', @_handleDataReceived @serialPort.openAsync() destroy: -> @serialPort.closeAsync() # Computes a checksum for a message of bytes in `data`, optionally # targetting an address `dest` # @returns a byte checksum: (data, dest = 0) -> (dest + data.length + sum data) % 256 # Sends to `dest` `byteCount` of values from `outputFlags` # @returns a promise resolved once the data has been transmitted send: (dest, byteCount, outputFlags) -> trimmedOutputFlags = outputFlags[...byteCount] checksum = @checksum trimmedOutputFlags, dest @serialPort.writeAsync [dest, checksum, byteCount].concat trimmedOutputFlags .then @serialPort.drainAsync() receive: (timeout) -> new Promise (resolve, reject) => start = +new Date buffer = [] @serialPort.on 'data', (data) => # time out reject new SerialPortError "Timeout" if +new Date - timeout > start # copy data buffer.push i for i in data # message received if buffer.length >= buffer[1] inputFlags = buffer[2..] reject new SerialPortError "Corrupted data" if buffer[0] isnt @checksum inputFlags resolve inputFlags _handleDataReceived: (data) => @receiveBuffer.push i for i in data module.exports = {BrickPi, PORT_A, PORT_B}