# roomba.py
#
# Zach Dodds 3/1/2006
# dodds@cs.hmc.edu
#
# python driver for the roomba SCI interface
#
# You will need pyserial (comes with win32all, also on MacOSX and Linux)
#
# and a serial connection to the roomba
# (there are several examples documented online, and
#  even some companies claiming they will start supplying
#  products to this end soon...)
#
# the Roomba class is simply an abstraction of the robot
#

import serial
import math
import time

# some module-level definitions for the robot commands
START = chr(128)    # already converted to bytes...
BAUD = chr(129)     # + 1 byte
CONTROL = chr(130)
SAFE = chr(131)
FULL = chr(132)
POWER = chr(133)
SPOT = chr(134)
CLEAN = chr(135)
MAX = chr(136)
DRIVE = chr(137)    # + 4 bytes
MOTORS = chr(138)   # + 1 byte
LEDS = chr(139)     # + 3 bytes
SONG = chr(140)     # + 2N+2 bytes, where N is the number of notes
PLAY = chr(141)     # + 1 byte
SENSORS = chr(142)  # + 1 byte
FORCESEEKINGDOCK = chr(143)

# the LEDs
LED_STATUS = 0
LED_SPOT = 1
LED_CLEAN = 2
LED_MAX = 3
LED_DIRTDETECT = 4
LED_POWER = 5

# the motors
MOTOR_MAINBRUSH = 0
MOTOR_VACUUM = 1
MOTOR_SIDEBRUSH = 2

# the four SCI modes
# the code will try to keep track of which mode the system is in,
# but this might not be 100% trivial...
OFF_MODE = 0
PASSIVE_MODE = 1
SAFE_MODE = 2
FULL_MODE = 3

# for printing the SCI modes
def modeStr( mode ):
    """ prints a string representing the input SCI mode """
    if mode == OFF_MODE: return 'OFF_MODE'
    if mode == PASSIVE_MODE: return 'PASSIVE_MODE'
    if mode == SAFE_MODE: return 'SAFE_MODE'
    if mode == FULL_MODE: return 'FULL_MODE'
    print 'Warning: unknown mode', mode, 'seen in modeStr'
    return 'UNKNOWN_MODE'

#
# some module-level functions for dealing with bits and bytes
#
def bytesOfR( r ):
    """ for looking at the raw bytes of a sensor reply, r """
    print 'raw r is', r
    for i in range(len(r)):
        print 'byte', i, 'is', ord(r[i])
    print 'finished with formatR'

def bitOfByte( bit, byte ):
    """ returns a 0 or 1: the value of the 'bit' of 'byte' """
    if bit < 0 or bit > 7:
        print 'Your bit of', bit, 'is out of range (0-7)'
        print 'returning 0'
        return 0
    return ((byte >> bit) & 0x01)

def toBinary( val, numbits ):
    """ prints numbits digits of val in binary """
    if numbits == 0:  return
    toBinary( val>>1 , numbits-1 )
    print (val & 0x01),  # print least significant bit


def fromBinary( s ):
    """ s is a string of 0's and 1's """
    if s == '': return 0
    lowbit = ord(s[-1]) - ord('0')
    return lowbit + 2*fromBinary( s[:-1] )
    

def twosComplementInt1byte( byte ):
    """ returns an int of the same value of the input
        int (a byte), but interpreted in two's
        complement
        the output range should be -128 to 127
    """
    # take everything except the top bit
    topbit = bitOfByte( 7, byte )
    lowerbits = byte & 127
    if topbit == 1:
        return lowerbits - (1 << 7)
    else:
        return lowerbits


def twosComplementInt2bytes( highByte, lowByte ):
    """ returns an int which has the same value
        as the twosComplement value stored in
        the two bytes passed in
        the output range should be -32768 to 32767
    """
    # take everything except the top bit
    topbit = bitOfByte( 7, highByte )
    lowerbits = highByte & 127
    unsignedInt = lowerbits << 8 | lowByte
    if topbit == 1:
        # with sufficient thought, I've convinced
        # myself of this... we'll see, I suppose.
        return unsignedInt - (1 << 15)
    else:
        return unsignedInt


def toTwosComplement2Bytes( value ):
    """ returns two bytes (ints) in high, low order
        whose bits form the input value when interpreted in
        two's complement
    """
    # if positive or zero, it's OK
    if value >= 0:
        eqBitVal = value
    # if it's negative, I think it is this
    else:
        eqBitVal = (1<<16) + value

    return ( (eqBitVal >> 8) & 0xFF, eqBitVal & 0xFF )


# 
# this class represents a snapshot of the robot's data
#
class SensorFrame:
    """ the sensorFrame class is really a struct whose
        fields are filled in by sensorStatus
    """

    def __init__(self):
        """ constructor -- set all fields to 0 

            see interpretSensorString for details
            on all of these fields
        """
        self.casterDrop = 0
        self.leftWheelDrop = 0
        self.rightWheelDrop = 0
        self.leftBump = 0
        self.rightBump = 0
        self.wallSensor = 0
        self.leftCliff = 0
        self.frontLeftCliff = 0
        self.frontRightCliff = 0
        self.rightCliff = 0
        self.virtualWall = 0
        self.driveLeft = 0
        self.driveRight = 0
        self.mainBrush = 0
        self.vacuum = 0
        self.sideBrush = 0
        self.leftDirt = 0
        self.rightDirt = 0
        self.remoteControlCommand = 0
        self.powerButton = 0
        self.spotButton = 0
        self.cleanButton = 0
        self.maxButton = 0
        self.distance = 0
        self.rawAngle = 0
        self.angleInRadians = 0
        self.chargingState = 0
        self.voltage = 0
        self.current = 0
        self.temperature = 0
        self.charge = 0
        self.capacity = 0

    def __str__(self):
        """ returns a string with the information
            from this SensorFrame
        """
        # there's probably a more efficient way to do this...
        # perhaps just making it all + instead of the separate
        # += would be more efficient
        s = ''
        s += 'casterDrop: ' + str(self.casterDrop) + '\n'
        s += 'leftWheelDrop: ' + str(self.leftWheelDrop) + '\n'
        s += 'rightWheelDrop: ' + str(self.rightWheelDrop) + '\n'
        s += 'leftBump: ' + str(self.leftBump) + '\n'
        s += 'rightBump: ' + str(self.rightBump) + '\n'
        s += 'wallSensor: ' + str(self.wallSensor) + '\n'
        s += 'leftCliff: ' + str(self.leftCliff) + '\n'
        s += 'frontLeftCliff: ' + str(self.frontLeftCliff) + '\n'
        s += 'frontRightCliff: ' + str(self.frontRightCliff) + '\n'
        s += 'rightCliff: ' + str(self.rightCliff) + '\n'
        s += 'virtualWall: ' + str(self.virtualWall) + '\n'
        s += 'driveLeft: ' + str(self.driveLeft) + '\n'
        s += 'driveRight: ' + str(self.driveRight) + '\n'
        s += 'mainBrush: ' + str(self.mainBrush) + '\n'
        s += 'vacuum: ' + str(self.vacuum) + '\n'
        s += 'sideBrush: ' + str(self.sideBrush) + '\n'
        s += 'leftDirt: ' + str(self.leftDirt) + '\n'
        s += 'rightDirt: ' + str(self.rightDirt) + '\n'
        s += 'remoteControlCommand: ' + str(self.remoteControlCommand) + '\n'
        s += 'powerButton: ' + str(self.powerButton) + '\n'
        s += 'spotButton: ' + str(self.spotButton) + '\n'
        s += 'cleanButton: ' + str(self.cleanButton) + '\n'
        s += 'maxButton: ' + str(self.maxButton) + '\n'
        s += 'distance: ' + str(self.distance) + '\n'
        s += 'rawAngle: ' + str(self.rawAngle) + '\n'
        s += 'angleInRadians: ' + str(self.angleInRadians) + '\n'
        s += 'chargingState: ' + str(self.chargingState) + '\n'
        s += 'voltage: ' + str(self.voltage) + '\n'
        s += 'current: ' + str(self.current) + '\n'
        s += 'temperature: ' + str(self.temperature) + '\n'
        s += 'charge: ' + str(self.charge) + '\n'
        s += 'capacity: ' + str(self.capacity) + '\n'
        return s


#
# the robot class
#
class Roomba:
    """ the Roomba class is an abstraction of the Roomba's
        SCI interface, including communication and a bit
        of processing of the strings passed back and forth
 
        when you create an object of type Roomba, the code
        will try to open a connection to it - so, it will fail
        if it's not attached!
 
        Right now, the code assumes the SCI begins in "off" mode
        (listening for the Start command), which is true upon
        taking out the battery and replacing it.
    """
    # to do: check if we can start in other modes...
 
    def __init__(self, PORT, startingMode=OFF_MODE):
        """ the constructor which tries to open the
            connection to the robot at port PORT
        """
        # to do: check for a safe but short timeout value...
        # to do: use the timeout to do more error checking than
        #        is currently done...
        # the -1 here is because windows starts counting from 1
        # in the control panel, but not in pyserial... ?
        self.ser = serial.Serial(PORT-1, baudrate=57600, timeout=0.5)
        #self.ser.open()  # will throw an exception if not openable
 
        # this will (try to) track the SCI mode of the robot
        # OFF_MODE: at power up
        # PASSIVE_MODE: read sensors and execute virtual button presses
        #               after a virtual button press
        # SAFE_MODE: control, but safety features still on
        # FULL_MODE: complete control ... stay close!
        self.sciMode = startingMode

        # this will put the robot into PASSIVE_MODE
        # it seems to be OK, even if it's already in PASSIVE_MODE
        self.start()
 
        # I guess that's it... more with odometry, etc.

        # LED states stored in a dictionary
        # they start at all zeros
        self.led = {}
        self.led[ LED_STATUS ] = 0
        self.led[ LED_SPOT ] = 0
        self.led[ LED_CLEAN ] = 0
        self.led[ LED_MAX ] = 0
        self.led[ LED_DIRTDETECT ] = 0
        self.led[ LED_POWER ] = (0, 0)

        # motor states also stored in a dictionary
        # they also start at all zeros
        self.motors = {}
        self.motors[ MOTOR_MAINBRUSH ] = 0
        self.motors[ MOTOR_VACUUM ] = 0
        self.motors[ MOTOR_SIDEBRUSH ] = 0
 

    def __str__(self):
        """ for printing the robot's state """
        s = ''
        s += 'serial connection:' + str(self.ser) + '\n'
        s += 'robot sci mode:' + modeStr(self.sciMode) + '\n'
        # odometry
        # timestamp
        # last sensor frame
        # other stuff
        return s
 
    def start(self):
        """ changes from OFF_MODE to PASSIVE_MODE """
        self.ser.write( START )
        # they recommend 20 ms between mode-changing commands
        time.sleep(0.03)
        # change the mode we think we're in...
        self.sciMode = PASSIVE_MODE
        # no response here, so we don't get any...
        return

    def close(self):
        """ turns off the power and puts the robot into PASSIVE_MODE 
            then, it closes the serial port
        """
        # is there other clean up to be done?
        self.ser.write( POWER )
        # they recommend 20 ms between mode-changing commands
        time.sleep(0.03)
        # change the mode we think we're in...
        self.sciMode = PASSIVE_MODE
        # no response here, so we don't get any...

        # close the serial port
        self.ser.close()
        return

    def closeSer(self):
        """ just disconnects the serial port """
        self.ser.close()
        return

    def openSer(self):
        """ opens the port again """
        self.ser.open()
        return


    def drive(self, roomba_mm_sec, roomba_radius_mm, turn_dir='CW'):
        """ implements the drive command as specified
            the turn_dir should be either 'CW' or 'CCW' for
            clockwise or counterclockwise - this is only
            used if roomba_radius_mm == 0 (or rounds down to 0)
            other drive-related calls are available
        """
        # first, they should be ints
        #   in case they're being generated mathematically
        if type(roomba_mm_sec) != type(42):
            roomba_mm_sec = int(roomba_mm_sec)
        if type(roomba_radius_mm) != type(42):
            roomba_radius_mm = int(roomba_radius_mm)

        # we check that the inputs are within limits
        # if not, we cap them there
        if roomba_mm_sec < -500:
            roomba_mm_sec = -500
        if roomba_mm_sec > 500:
            roomba_mm_sec = 500

        # if the radius is beyond the limits, we go straight
        if roomba_radius_mm < -2000:
            roomba_radius_mm = 32768
        if roomba_radius_mm > 2000:
            roomba_radius_mm = 32768
        
        # get the two bytes from the velocity
        # these come back as numbers, so we will chr them
        velHighVal, velLowVal = toTwosComplement2Bytes( roomba_mm_sec )

        # get the two bytes from the radius in the same way
        # note the special cases
        if roomba_radius_mm == 0:
            if turn_dir == 'CW':
                roomba_radius_mm = -1
            else:
                roomba_radius_mm = 1
        radiusHighVal, radiusLowVal = toTwosComplement2Bytes( roomba_radius_mm )

        # print 'bytes are', velHighVal, velLowVal, radiusHighVal, radiusLowVal

        # send these bytes and set the stored velocities
        self.ser.write( DRIVE )
        self.ser.write( chr(velHighVal) )
        self.ser.write( chr(velLowVal) )
        self.ser.write( chr(radiusHighVal) )
        self.ser.write( chr(radiusLowVal) )


    def setLED(self, led_in, state, optstate=0):
	""" sets led off (if state is 0) and on (if state is 1)
	    
	    led can be roomba.LED_STATUS
	               roomba.LED_SPOT
	               roomba.LED_CLEAN
	               roomba.LED_MAX
	               roomba.LED_DIRTDETECT
		       roomba.LED_POWER

            for roomba.LED_STATUS, state can be 0-3
	    for roomba.LED_POWER,  state can be 0-255 (intensity)
	                    and optstate can be 0-255 (color)
        """
        # change the dictionary appropriately
        # then call setAbsoluteLEDs on the dictionary

        # should error-check here!
        if led_in == LED_POWER:
            self.led[ led_in ] = (state, optstate)

        else:
            self.led[ led_in ] = state

        self.setDictionaryLEDs()




    def setDictionaryLEDs(self):
        """ this calls setAbsoluteLEDs with the contents of the
            robot's current dictionary of led states
        """
        poweramt, powercoloramt = self.led[ LED_POWER ]
        self.setAbsoluteLEDs( status=self.led[ LED_STATUS ], \
                              spot=self.led[ LED_SPOT ], \
                              clean=self.led[ LED_CLEAN ], \
                              max=self.led[ LED_MAX ], \
                              dirtdetect=self.led[ LED_DIRTDETECT ], \
                              power=poweramt, \
                              powercolor=powercoloramt )



    def setAbsoluteLEDs(self, status=0, spot=0, clean=0, max=0, dirtdetect=0, power=0, powercolor=0):
        """ this function implements the LED control directly
            there should be individual components implemented, too

            the inputs all default to 0; they can be
              status = 0 (off), 1 (red), 2 (green), 3 (amber)
              spot = 0 (off), 1 (on)
              clean = 0 (off), 1 (on)
              max = 0 (off), 1 (on)
              dirtdetect = 0 (off), 1 (on)
              power = 0 to 255 (intensity)
              powercolor = 0 to 255 (color: 0 is green, 255 is red)
        """
        # make sure we're within range...
        if status != 1 and status != 2 and status != 3: status = 0
        if spot != 0: spot = 1
        if clean != 0: clean = 1
        if max != 0: max = 1
        if dirtdetect != 0: dirtdetect = 1
        power = int(power)
        powercolor = int(powercolor)
        if power < 0: power = 0
        if power > 255: power = 255
        if powercolor < 0: powercolor = 0
        if powercolor > 255: powercolor = 255
        # create the first byte
        firstByteVal = (status << 4) | (spot << 3) | (clean << 2) | (max << 1) | dirtdetect

        # send these as bytes
        # print 'bytes are', firstByteVal, powercolor, power

        # send these bytes and set the stored velocities
        self.ser.write( LEDS )
        self.ser.write( chr(firstByteVal) )
        self.ser.write( chr(powercolor) )
        self.ser.write( chr(power) )

        return


    def getRawSensorData(self, packetnumber):
        """ gets back a raw string of sensor data
            which then can be used to create a SensorFrame
        """
        if packetnumber != 1 and packetnumber != 2 and packetnumber != 3: 
            packetnumber = 0

        self.ser.write( SENSORS )
        self.ser.write( chr(packetnumber) )

        r = self.ser.read(size=26)

        # need error checking or sanity checking or something!

        return r


    def setSong(self, songNumber, songDataList):
        """ this stores a song to roomba's memory to play later
            with the playSong command

            songNumber must be between 0 and 15 (inclusive)
            songDataList is a list of (note, duration) pairs (up to 16)
                         note is the midi note number, from 31 to 127
                              (outside this range, the note is a rest)
                         duration is from 0 to 255 in 1/64ths of a second
        """
        # any notes to play?
        if len(songDataList) < 1:
            print 'No data in the songDataList'
            return

        if songNumber < 0: songNumber = 0
        if songNumber > 15: songNumber = 15

        # indicate that a song is coming
        self.ser.write( SONG )
        self.ser.write( chr(songNumber) )

        L = min(len(songDataList), 16)
        self.ser.write( chr(L) )

        # loop through the notes, up to 16
        for note in songDataList[:L]:
            # make sure its a tuple, or else we rest for 1/4 second
            if type(note) == type( () ):
                #more error checking here!
                self.ser.write( chr(note[0]) )  # note number
                self.ser.write( chr(note[1]) )  # duration
            else:
                self.ser.write( chr(30) )   # a rest note
                self.ser.write( chr(16) )   # 1/4 of a second

        # no change to the SCI mode
        return


    def playSong(self, songNumber):
        """ plays song songNumber """
        #
        if songNumber < 0: songNumber = 0
        if songNumber > 15: songNumber = 15

        self.ser.write( PLAY )
        self.ser.write( chr(songNumber) )

    def playNote(self, noteNumber, duration, songNumber=0):
        """ plays a single note as a song at songNumber """
        # set the song
        self.setSong(songNumber, [(noteNumber,duration)])
        self.playSong(songNumber)

    def setMainBrush(self, state):
        """ turns mainbrush motor on (state==1) or off (state==0) """
        if state: state = 1
        else: state = 0
        self.motors[MOTOR_MAINBRUSH] = state
        self.setDictionaryMotors()

    def setVacuum(self, state):
        """ turns mainbrush motor on (state==1) or off (state==0) """
        if state: state = 1
        else: state = 0
        self.motors[MOTOR_VACUUM] = state
        self.setDictionaryMotors()

    def setSideBrush(self, state):
        """ turns mainbrush motor on (state==1) or off (state==0) """
        if state: state = 1
        else: state = 0
        self.motors[MOTOR_SIDEBRUSH] = state
        self.setDictionaryMotors()

    def setDictionaryMotors(self):
        """ we set the motors according to our dictionary """
        self.setAbsoluteMotors( mainbrush=self.motors[ MOTOR_MAINBRUSH ], \
                                vacuum=self.motors[ MOTOR_VACUUM ], \
                                sidebrush=self.motors[ MOTOR_SIDEBRUSH ] )

    def setAbsoluteMotors(self, mainbrush=0, vacuum=0, sidebrush=0):
        """ sets each motor on (1) or off (0) """
        # should check if in Safe or Full mode!!

        # make sure each is 1 or 0
        if mainbrush: mainbrush = 1
        else: mainbrush = 0
        if vacuum: vacuum = 1
        else: vacuum = 0
        if sidebrush: sidebrush = 1
        else: sidebrush = 0

        # set the motors' state
        self.ser.write( MOTORS )
        self.ser.write( chr( (mainbrush << 2) | (vacuum << 1) | sidebrush ) )


    def readSensors(self):
        """ right now, this gets ALL the sensor data and 
            returns a SensorFrame with that data
        """
        r = self.getRawSensorData(0)
        s = self.interpretSensorString(r)
        return s


    def buttonPress(self, button):
        """ this "presses" (virtually) one of the roomba's
            buttons. The robot goes into PASSIVE_MODE and
            it will start the button's specified behavior

            button should be one of
            roomba.POWER
            roomba.SPOT
            roomba.CLEAN or
            roomba.MAX
        """
        if button == POWER:
            self.ser.write( POWER )
        elif button == SPOT:
            self.ser.write( SPOT )
        elif button == CLEAN:
            self.ser.write( CLEAN )
        elif button == MAX:
            self.ser.write( MAX )
        else:
            print 'The button', button, 'in buttonPress was not recognized.'
            print 'Not sending anything to the robot.'
            return

        # the robot should go into passive mode
        self.sciMode = PASSIVE_MODE
        # they recommend 20 ms between mode-changing commands
        time.sleep(0.03)
        # no response expected
        return


    def powerOff(self):
        """ pressed the power button (virtually) """
        self.buttonPress( POWER )

        
    def toFullMode(self):
        """ changes the state (from SAFE_MODE) to FULL_MODE
        """
        if self.sciMode == SAFE_MODE:
            self.ser.write( FULL )
            # they recommend 20 ms between mode-changing commands
            time.sleep(0.03)
            # change the mode we think we're in...
            self.sciMode = SAFE_MODE
            # no response here, so we don't get any...
            return

        print 'Going to FULL_MODE from', modeStr(self.sciMode),
        print 'not permitted.'
        print 'Not sending anything.'
        return


    def toSafeMode(self):
        """ changes the state (from PASSIVE_MODE or FULL_MODE)
            to SAFE_MODE
        """
        if self.sciMode == PASSIVE_MODE:
            self.ser.write( CONTROL )
            # they recommend 20 ms between mode-changing commands
            time.sleep(0.03)
            # change the mode we think we're in...
            self.sciMode = SAFE_MODE
            # no response here, so we don't get any...
            return

        if self.sciMode == FULL_MODE:
            self.ser.write( SAFE )
            # they recommend 20 ms between mode-changing commands
            time.sleep(0.03)
            # change the mode we think we're in...
            self.sciMode = SAFE_MODE
            # no response here, so we don't get any...
            return

        if self.sciMode == OFF_MODE:
            print 'Going to SAFE_MODE from OFF_MODE not permitted.'
            return

        if self.sciMode == SAFE_MODE:
            # print 'Already in SAFE_MODE'
            return
            


    def getMode(self):
        """ returns one of OFF_MODE, PASSIVE_MODE, SAFE_MODE, FULL_MODE """
        # but how right is it?
        return self.sciMode


    def setBaudRate(self, baudrate):
        """ sets the communications rate to the desired value """
        # check for OK value
        baudcode = 10  # 57600, the default
        if baudrate == 300: baudcode = 0
        elif baudrate == 600: baudcode = 1
        elif baudrate == 1200: baudcode = 2
        elif baudrate == 2400: baudcode = 3
        elif baudrate == 4800: baudcode = 4
        elif baudrate == 9600: baudcode = 5
        elif baudrate == 14400: baudcode = 6
        elif baudrate == 19200: baudcode = 7
        elif baudrate == 28800: baudcode = 8
        elif baudrate == 38400: baudcode = 9
        elif baudrate == 57600: baudcode = 10
        elif baudrate == 115200: baudcode = 11
        else:
            print 'The baudrate of', baudrate, 'in setBaudRate'
            print 'was not recognized. Not sending anything.'
            return
        # otherwise, send off the message
        self.ser.write( START )
        self.ser.write( chr(baudcode) )
        # the recommended pause
        time.sleep(0.1)
        # change the mode we think we're in...
        self.sciMode = PASSIVE_MODE
        # no response here, so we don't get any...
        return


    def interpretSensorString( self, r ):
        """ This returns a sensorFrame object with its fields
            filled in from the raw sensor return string, r, which
            has to be the full 3-packet (26-byte) string. 
    
            r is obtained by writing [142][0] to the serial port.
        """
        # check length
        if len(r) != 26:
            print 'You have input an incorrectly formatted string to'
            print 'sensorStatus. It needs to have 26 bytes (full sensors).'
            print 'The input is', r
            return
    
        s = SensorFrame()
    
        # convert r so that it is a list of 26 ints instead of 26 chrs
        r = [ ord(c) for c in r ]
    
        # packet number 1 (10 bytes)
    
        # byte 0: bumps and wheeldrops
        s.casterDrop = bitOfByte( 4, r[0] )
        s.leftWheelDrop = bitOfByte( 3, r[0] )
        s.rightWheelDrop = bitOfByte( 2, r[0] )
        s.leftBump = bitOfByte( 1, r[0] )
        s.rightBump = bitOfByte( 0, r[0] )
    
        # byte 1: wall sensor, the IR looking to the right
        s.wallSensor = bitOfByte( 0, r[1] )
    
        # byte 2: left cliff sensor
        s.leftCliff = bitOfByte( 0, r[2] )
    
        # byte 3: front left cliff sensor
        s.frontLeftCliff = bitOfByte( 0, r[3] )
    
        # byte 4: front right cliff sensor
        s.frontRightCliff = bitOfByte( 0, r[4] )
    
        # byte 5: right cliff sensor
        s.rightCliff = bitOfByte( 0, r[5] )
    
        # byte 6: virtual wall detector (the separate unit)
        s.virtualWall = bitOfByte( 0, r[6] )
    
        # byte 7: motor overcurrents byte
        s.driveLeft = bitOfByte( 4, r[7] )
        s.driveRight = bitOfByte( 3, r[7] )
        s.mainBrush = bitOfByte( 2, r[7] )
        s.vacuum = bitOfByte( 1, r[7] )
        s.sideBrush = bitOfByte( 0, r[7] )
    
        # byte 8: dirt detector left
        # the dirt-detecting sensors are acoustic impact sensors
        # basically, they hear the dirt (or don't) going by toward the back
        # this value ranges from 0 (no dirt) to 255 (lots of dirt)
        s.leftDirt = r[8]
    
        # byte 9: dirt detector right
        # some roomba's don't have the right dirt detector
        # the dirt detectors are metallic disks near the brushes
        s.rightDirt = r[9]
    
        # packet number 2 (6 bytes)
    
        # byte 10: remote control command
        # this is the value of the remote control command currently
        # being seen by the roomba, it is 255 if there is no command
        # not all roombas have a remote control...
        s.remoteControlCommand = r[10]
    
        # byte 11: button presses
        s.powerButton = bitOfByte( 3, r[11] )
        s.spotButton = bitOfByte( 2, r[11] )
        s.cleanButton = bitOfByte( 1, r[11] )
        s.maxButton = bitOfByte( 0, r[11] )
    
        # bytes 12 and 13: distance
        # the distance that roomba has traveled, in mm, since the
        # last time this data was requested (not from a SensorFrame,
        # but from the roomba)
        # It will stay at the max or min (32767 or -32768) if
        # not polled often enough, i.e., it then means "a long way"
        # It is the sum of the two drive wheels' distances, divided by 2
        s.distance = twosComplementInt2bytes( r[12], r[13] )
    
        # bytes 14 and 15: angle
        s.rawAngle = twosComplementInt2bytes( r[14], r[15] )
        # the distance between the wheels is 258 mm
        s.angleInRadians = 2.0 * s.rawAngle / 258.0
        # check this!!
    
        # packet number 3 (10 bytes)
    
        # byte 16: charging state
        # 0 == not charging
        # 1 == charging recovery
        # 2 == charging
        # 3 == trickle charging
        # 4 == waiting
        # 5 == charging error
        s.chargingState = r[16]
    
        # bytes 17 and 18: voltage in millivolts
        # this is unsigned, so we don't use two's complement
        s.voltage = r[17] << 8 | r[18]
        # check this for byte order!!
    
        # bytes 19 and 20: current in milliamps
        # this is signed, from -32768 to 32767
        # negative currents are flowing out of the battery
        # positive currents are flowing into the battery (charging)
        s.current = twosComplementInt2bytes( r[19], r[20] )
    
        # byte 21: temperature of the battery
        # this is in degrees celsius
        s.temperature = twosComplementInt1byte( r[21] )
    
        # bytes 22 and 23: charge of the battery in milliamp-hours
        # this is two unsigned bytes
        s.charge = r[22] << 8 | r[23]
    
        # bytes 24 and 25: estimated capacity of the roomba's battery
        # in units of milliamp-hours
        # when the charge reaches this value, the battery is
        # considered fully charged
        s.capacity = r[24] << 8 | r[25]
    
        return s

