class SpheroPwn::Session

A communication session with a robot.

Public Class Methods

new(channel) click to toggle source

@param {Channel} channel the byte-level communication channel with the

robot
# File lib/sphero_pwn/session.rb, line 7
def initialize(channel)
  @channel = channel


  # Must be acquired to change any of the values below.
  @sequence_lock = Mutex.new
  # Maps sequence numbers to responses expected from the server.
  @pending_responses = {}
  # Sweeps the space of valid sequence numbers.
  @last_sequence = 0
end
valid_checksum?(header_bytes, data_bytes, checksum) click to toggle source

Checks if a message's checksum matches its contents.

@param {Array<Number>} header_bytes the header semantics differ between

command responses and async messages, but both have 3 bytes

@param {Array<Number>} data_bytes

# File lib/sphero_pwn/session.rb, line 162
def self.valid_checksum?(header_bytes, data_bytes, checksum)
  sum = 0
  header_bytes.each { |byte| sum += byte }
  data_bytes.each { |byte| sum += byte }
  checksum == ((sum & 0xFF) ^ 0xFF)
end

Public Instance Methods

close() click to toggle source

Terminates this session and closes the underlying communication channel.

@return {Session} self

# File lib/sphero_pwn/session.rb, line 23
def close
  @channel.close
  self
end
recv_message() click to toggle source

Reads a message from the robot.

This method blocks until a message is available. The method can be called in a loop on a dedicated thread, and will synchronize correctly with {Session#send_command}.

@return {Response, Async} the response read from the channel; can be nil if

no message was received or if the checksum verification failed
# File lib/sphero_pwn/session.rb, line 67
def recv_message
  start_of_packet = @channel.recv_bytes 1
  return nil if start_of_packet.empty? || start_of_packet.ord != 0xFF

  packet_type = @channel.recv_bytes 1
  return nil if packet_type.empty?
  case packet_type.ord
  when 0xFF
    read_response
  when 0xFE
    read_async_message
  else
    nil
  end
end
recv_until_response() click to toggle source

Reads messages from the robot until a response is received.

@return {Response} the response received

# File lib/sphero_pwn/session.rb, line 47
def recv_until_response
  loop do
    message = recv_message
    if message && message.kind_of?(SpheroPwn::Response)
      return message
    end

    # TODO(pwnall): customizable sleep interval
    sleep 0.05
  end
end
send_command(command) click to toggle source

@param {Command} command the command to be sent @return {Session} self

# File lib/sphero_pwn/session.rb, line 30
def send_command(command)
  sequence = 0
  if command.expects_response?
    @sequence_lock.synchronize do
      sequence = alloc_sequence
      @pending_responses[sequence] = command.response_class
    end
  end

  bytes = command.to_bytes sequence
  @channel.send_bytes bytes
  self
end

Private Instance Methods

alloc_sequence() click to toggle source

Finds an unused sequence number.

Sending a command that requires a response allocates a sequence number. Receiving the required response frees up the sequence number.

The caller should own the sequence_lock mutex.

@return {Number} a sequence number that can be used for a command; the

sequence number is not considered to be allocated until the caller
inserts it as a key in @pending_commands
# File lib/sphero_pwn/session.rb, line 93
def alloc_sequence
  begin
    @last_sequence = (@last_sequence + 1) & 0xFF
  end while @pending_responses.has_key? @last_sequence
  @last_sequence
end
read_async_message() click to toggle source

Reads an asynchronous message from the channel.

This assumes that the start-of-packet was already read and indicates an asynchronous message.

@return {Response} the parsed response

# File lib/sphero_pwn/session.rb, line 136
def read_async_message
  header_bytes = @channel.recv_bytes(3).unpack('C*')
  class_id, length_msb, length_lsb  = *header_bytes
  return nil unless length_msb && length_lsb

  # It may seem that it'd be better to look up the sequence number and bail
  # early if we don't find it. However, in order to avoid misleading error
  # messages, we don't want to touch anything in the message until we know
  # that the checksum is valid.
  data_length = (length_msb << 8) | length_lsb
  return nil unless data = @channel.recv_bytes(data_length)
  data_bytes = data.unpack 'C*'
  checksum = data_bytes.pop
  unless self.class.valid_checksum?(header_bytes, data_bytes, checksum)
    return nil
  end

  SpheroPwn::Asyncs.create class_id, data_bytes
end
read_response() click to toggle source

Reads the response to a command from the channel.

This assumes that the start-of-packet was already read and indicates a command response.

@return {Response} the parsed response

# File lib/sphero_pwn/session.rb, line 107
def read_response
  header_bytes = @channel.recv_bytes(3).unpack('C*')
  response_code, sequence, data_length = *header_bytes
  return nil unless data_length

  # It may seem that it'd be better to look up the sequence number and bail
  # early if we don't find it. However, in order to avoid misleading error
  # messages, we don't want to touch anything in the message until we know
  # that the checksum is valid.
  return nil unless data = @channel.recv_bytes(data_length)
  data_bytes = data.unpack 'C*'
  checksum = data_bytes.pop
  unless self.class.valid_checksum?(header_bytes, data_bytes, checksum)
    return nil
  end

  klass = @sequence_lock.synchronize { @pending_responses.delete sequence }
  return nil if klass.nil?

  klass.new response_code, sequence, data_bytes
end