Class: NXTComm

Inherits:
Object
  • Object
show all
Defined in:
lib/nxt_comm.rb

Overview

Description

Low-level interface for communicating directly with the NXT via a Bluetooth serial port. Implements direct commands outlined in Appendix 2-LEGO MINDSTORMS NXT Direct Commands.pdf

Not all functionality is implemented yet!

For instructions on creating a bluetooth serial port connection:

Examples

First create a new NXTComm object and pass the device.

@nxt = NXTComm.new("/dev/tty.NXT-DevB-1")

Rotate the motor connected to port B forwards indefinitely at 100% power:

@nxt.set_output_state(
  NXTComm::MOTOR_B,
  100,
  NXTComm::MOTORON,
  NXTComm::REGULATION_MODE_MOTOR_SPEED,
  100,
  NXTComm::MOTOR_RUN_STATE_RUNNING,
  0
)

Play a tone at 1000 Hz for 500 ms:

@nxt.play_tone(1000,500)

Print out the current battery level:

puts "Battery Level: #{@nxt.get_battery_level/1000.0} V"

Constant Summary collapse

SENSOR_1 =

sensors

0x00
SENSOR_2 =
0x01
SENSOR_3 =
0x02
SENSOR_4 =
0x03
MOTOR_A =

motors

0x00
MOTOR_B =
0x01
MOTOR_C =
0x02
MOTOR_ALL =
0xFF
COAST =

output mode

0x00
MOTORON =

motor will rotate freely?

0x01
BRAKE =

enables PWM power according to speed

0x02
REGULATED =

voltage is not allowed to float between PWM pulses, improves accuracy, uses more power

0x04
REGULATION_MODE_IDLE =

output regulation mode

0x00
REGULATION_MODE_MOTOR_SPEED =

disables regulation

0x01
REGULATION_MODE_MOTOR_SYNC =

auto adjust PWM duty cycle if motor is affected by physical load

0x02
MOTOR_RUN_STATE_IDLE =

output run state

0x00
MOTOR_RUN_STATE_RAMPUP =

disables power to motor

0x10
MOTOR_RUN_STATE_RUNNING =

ramping to a new SPEED set-point that is greater than the current SPEED set-point

0x20
MOTOR_RUN_STATE_RAMPDOWN =

enables power to motor

0x40
NO_SENSOR =

sensor type

0x00
SWITCH =
0x01
TEMPERATURE =
0x02
REFLECTION =
0x03
ANGLE =
0x04
LIGHT_ACTIVE =
0x05
LIGHT_INACTIVE =
0x06
SOUND_DB =
0x07
SOUND_DBA =
0x08
CUSTOM =
0x09
LOWSPEED =
0x0A
LOWSPEED_9V =
0x0B
NO_OF_SENSOR_TYPES =
0x0C
RAWMODE =

sensor mode

0x00
BOOLEANMODE =

report scaled value equal to raw value

0x20
TRANSITIONCNTMODE =

report scaled value as 1 true or 0 false, false if raw value > 55% of total range, true if < 45%

0x40
PERIODCOUNTERMODE =

report scaled value as number of transitions between true and false

0x60
PCTFULLSCALEMODE =

report scaled value as number of transitions from false to true, then back to false

0x80
CELSIUSMODE =

report scaled value as % of full scale reading for configured sensor type

0xA0
FAHRENHEITMODE =
0xC0
ANGLESTEPSMODE =

report scaled value as count of ticks on RCX-style rotation sensor

0xE0
SLOPEMASK =
0x1F
MODEMASK =
0xE0
@@op_codes =
{
  'start_program'             => 0x00,
  'stop_program'              => 0x01,
  'play_sound_file'           => 0x02,
  'play_tone'                 => 0x03,
  'set_output_state'          => 0x04,
  'set_input_mode'            => 0x05,
  'get_output_state'          => 0x06,
  'get_input_values'          => 0x07,
  'reset_input_scaled_value'  => 0x08,
  'message_write'             => 0x09,
  'reset_motor_position'      => 0x0A,
  'get_battery_level'         => 0x0B,
  'stop_sound_playback'       => 0x0C,
  'keep_alive'                => 0x0D,
  'ls_get_status'             => 0x0E,
  'ls_write'                  => 0x0F,
  'ls_read'                   => 0x10,
  'get_current_program_name'  => 0x11,
  # what happened to 0x12?  Dunno...
  'message_read'              => 0x13
}
@@error_codes =
{
  0x20 => "Pending communication transaction in progress",
  0x40 => "Specified mailbox queue is empty",
  0xBD => "Request failed (i.e. specified file not found)",
  0xBE => "Unknown command opcode",
  0xBF => "Insane packet",
  0xC0 => "Data contains out-of-range values",
  0xDD => "Communication bus error",
  0xDE => "No free memory in communication buffer",
  0xDF => "Specified channel/connection is not valid",
  0xE0 => "Specified channel/connection not configured or busy",
  0xEC => "No active program",
  0xED => "Illegal size specified",
  0xEE => "Illegal mailbox queue ID specified",
  0xEF => "Attempted to access invalid field of a structure",
  0xF0 => "Bad input or output specified",
  0xFB => "Insufficient memory available",
  0xFF => "Bad arguments"
}
@@mutex =
Mutex.new

Instance Method Summary collapse

Constructor Details

#initialize(dev = $DEV) ⇒ NXTComm

Create a new instance of NXTComm. Be careful not to create more than one NXTComm object per serial port dev. If two NXTComms try to talk to the same dev, there will be trouble.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/nxt_comm.rb', line 205

def initialize(dev = $DEV)

  @@mutex.synchronize do
    begin
      @sp = SerialPort.new(dev, 57600, 8, 1, SerialPort::NONE)
    
      @sp.flow_control = SerialPort::HARD
      @sp.read_timeout = 5000
    rescue Errno::EBUSY
      raise "Cannot connect to #{dev}. The serial port is busy or unavailable."
    end
  end
  
  if @sp.nil?
    $stderr.puts "Cannot connect to #{dev}"
  else
    puts "Connected to: #{dev}" if $DEBUG
  end
end

Instance Method Details

#closeObject

Close the connection



226
227
228
229
230
# File 'lib/nxt_comm.rb', line 226

def close
  @@mutex.synchronize do
    @sp.close if @sp and not @sp.closed?
  end
end

#connected?Boolean

Returns true if the connection to the NXT is open; false otherwise

Returns:

  • (Boolean)


233
234
235
# File 'lib/nxt_comm.rb', line 233

def connected?
  not @sp.closed?
end

#get_battery_levelObject

Returns the battery voltage in millivolts.



508
509
510
511
512
# File 'lib/nxt_comm.rb', line 508

def get_battery_level
  cmd = []
  result = send_and_receive @@op_codes["get_battery_level"], cmd
  result == false ? false : result.from_hex_str.unpack("v")[0]
end

#get_current_program_nameObject

Returns the name of the program currently running on the NXT. Returns an error If no program is running.



580
581
582
583
584
# File 'lib/nxt_comm.rb', line 580

def get_current_program_name
  cmd = []
  result = send_and_receive @@op_codes["get_current_program_name"], cmd
  result == false ? false : result.from_hex_str.unpack("A*")[0]
end

#get_input_values(port) ⇒ Object

Get the current values from an input sensor port.

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)

Returns a hash with the following info (enumerated values see: set_input_mode):

{
  :port             => see: input ports,
  :valid            => boolean, true if new data value should be seen as valid data, 
  :calibrated       => boolean, true if calibration file found and used for 'Calibrated Value' field below, 
  :type             => see: sensor types, 
  :mode             => see: sensor modes, 
  :value_raw        => raw A/D value (device dependent),
  :value_normal     => normalized A/D value (0 - 1023),
  :value_scaled     => scaled value (mode dependent),
  :value_calibrated => calibrated value, scaled to calibration (CURRENTLY UNUSED)
}


425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/nxt_comm.rb', line 425

def get_input_values(port)
  cmd = [port]
  result = send_and_receive @@op_codes["get_input_values"], cmd

  if result
    result_parts = result.from_hex_str.unpack('C5v4')
    result_parts[1] == 0x01 ? result_parts[1] = true : result_parts[1] = false
    result_parts[2] == 0x01 ? result_parts[2] = true : result_parts[2] = false

    (7..8).each do |i|
      # convert to signed word
      # FIXME: is this right?
      result_parts[i] = -1*(result_parts[i]^0xffff) if result_parts[i] > 0xfff
    end

    {
      :port             => result_parts[0],
      :valid            => result_parts[1],
      :calibrated       => result_parts[2],
      :type             => result_parts[3],
      :mode             => result_parts[4],
      :value_raw        => result_parts[5],
      :value_normal     => result_parts[6],
      :value_scaled     => result_parts[7],
      :value_calibrated => result_parts[8],
    }
  else
    false
  end
end

#get_output_state(port) ⇒ Object

Get the state of the output motor port.

  • port - output port (MOTOR_A, MOTOR_B, MOTOR_C)

Returns a hash with the following info (enumerated values see: set_output_state):

{
  :port               => see: output ports,
  :power              => -100 - 100,
  :mode               => see: output modes,
  :reg_mode           => see: regulation modes,
  :turn_ratio         => -100 - 100 negative shifts power to left motor, positive to right, 50 = one stops, other moves, 100 = each motor moves in opposite directions,
  :run_state          => see: run states,
  :tacho_limit        => current limit on a movement in progress, if any,
  :tacho_count        => internal count, number of counts since last reset of the motor counter,
  :block_tacho_count  => current position relative to last programmed movement,
  :rotation_count     => current position relative to last reset of the rotation sensor for this motor
}


384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/nxt_comm.rb', line 384

def get_output_state(port)
  cmd = [port]
  result = send_and_receive @@op_codes["get_output_state"], cmd

  if result
    result_parts = result.from_hex_str.unpack('C6V4')
    (7..9).each do |i|
      result_parts[i] = result_parts[i].as_signed if result_parts[i].kind_of? Bignum
    end
  
    {
      :port               => result_parts[0],
      :power              => result_parts[1],
      :mode               => result_parts[2],
      :reg_mode           => result_parts[3],
      :turn_ratio         => result_parts[4],
      :run_state          => result_parts[5],
      :tacho_limit        => result_parts[6],
      :tacho_count        => result_parts[7],
      :block_tacho_count  => result_parts[8],
      :rotation_count     => result_parts[9]
    }
  else
    false
  end
end

#keep_aliveObject

Keep the connection alive and prevents NXT from going to sleep until sleep time. Also, returns the current sleep time limit in ms



523
524
525
526
527
# File 'lib/nxt_comm.rb', line 523

def keep_alive
  cmd = []
  result = send_and_receive @@op_codes["keep_alive"], cmd
  result == false ? false : result.from_hex_str.unpack("L")[0]
end

#ls_get_status(port) ⇒ Object

Get the status of an LS port (like ultrasonic sensor). Returns the count of available bytes to read.

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)



531
532
533
534
535
# File 'lib/nxt_comm.rb', line 531

def ls_get_status(port)
  cmd = [port]
  result = send_and_receive @@op_codes["ls_get_status"], cmd
  result[0]
end

#ls_read(port) ⇒ Object

Read data from from lowspeed I2C port (for receiving data from the ultrasonic sensor)

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)

Returns a hash containing:

{
  :bytes_read => number of bytes read
  :data       => Rx data (padded)
}

For LS communication on the NXT, data lengths are limited to 16 bytes per command.
Furthermore, this protocol does not support variable-length return packages, so the response
will always contain 16 data bytes, with invalid data bytes padded with zeroes.


564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/nxt_comm.rb', line 564

def ls_read(port)
  cmd = [port]
  result = send_and_receive @@op_codes["ls_read"], cmd
  if result
    result = result.from_hex_str
    {
      :bytes_read => result[0],
      :data       => result[1..-1]
    }
  else
    false
  end
end

#ls_write(port, i2c_msg) ⇒ Object

Write data to lowspeed I2C port (for talking to the ultrasonic sensor)

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)

  • i2c_msg - the I2C message to send to the lowspeed controller; the first byte specifies the transmitted data length, the second byte specifies the expected respone data length, and the remaining 16 bytes are the transmitted data. See UltrasonicComm for an example of an I2C sensor protocol implementation.

    For LS communication on the NXT, data lengths are limited to 16 bytes per command. Rx data length MUST be specified in the write command since reading from the device is done on a master-slave basis



546
547
548
549
550
551
# File 'lib/nxt_comm.rb', line 546

def ls_write(port,i2c_msg)
  cmd = [port] + i2c_msg
  result = send_and_receive @@op_codes["ls_write"], cmd
  result = true if result == ""
  result
end

#message_read(inbox_remote, inbox_local = 1, remove = false) ⇒ Object

Read a message from a specific inbox on the NXT.

  • inbox_remote - remote inbox number (1 - 10)

  • inbox_local - local inbox number (1 - 10) (not sure why you need this?)

  • remove - boolean, true - clears message from remote inbox



590
591
592
593
594
595
# File 'lib/nxt_comm.rb', line 590

def message_read(inbox_remote,inbox_local = 1,remove = false)
  cmd = [inbox_remote, inbox_local]
  remove ? cmd << 0x01 : cmd << 0x00
  result = send_and_receive @@op_codes["message_read"], cmd
  result == false ? false : result[2..-1].from_hex_str.unpack("A*")[0]
end

#message_write(inbox, message) ⇒ Object

Write a message to a specific inbox on the NXT. This is used to send a message to a currently running program.

  • inbox - inbox number (1 - 10)

  • message - message data



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/nxt_comm.rb', line 468

def message_write(inbox,message)
  cmd = []
  cmd << inbox - 1
  case message.class.to_s
    when "String"
      cmd << message.size + 1
      message.each_byte do |b|
        cmd << b
      end
    when "Fixnum"
      cmd << 5 # msg size + 1
      #cmd.concat([(message & 255),(message >> 8),(message >> 16),(message >> 24)])
      [message].pack("V").each_byte{|b| cmd << b}
    when "TrueClass"
      cmd << 2 # msg size + 1
      cmd << 1
    when "FalseClass"
      cmd << 2 # msg size + 1
      cmd << 0
    else
      raise "Invalid message type"
  end
  result = send_and_receive @@op_codes["message_write"], cmd
  result = true if result == ""
  result
end

#play_sound_file(name, repeat = false) ⇒ Object

Play a sound file stored on the NXT.

  • name - file name of the sound file to play

  • repeat - Loop? (true or false)



322
323
324
325
326
327
328
329
330
331
# File 'lib/nxt_comm.rb', line 322

def play_sound_file(name,repeat = false)
  cmd = []
  repeat ? cmd << 0x01 : cmd << 0x00
  name.each_byte do |b|
    cmd << b
  end
  result = send_and_receive @@op_codes["play_sound_file"], cmd
  result = true if result == ""
  result
end

#play_tone(freq, dur) ⇒ Object

Play a tone.

  • freq - frequency for the tone in Hz

  • dur - duration for the tone in ms



336
337
338
339
340
341
# File 'lib/nxt_comm.rb', line 336

def play_tone(freq,dur)
  cmd = [(freq & 255),(freq >> 8),(dur & 255),(dur >> 8)]
  result = send_and_receive @@op_codes["play_tone"], cmd
  result = true if result == ""
  result
end

#recv_replyObject

Process the reply



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/nxt_comm.rb', line 274

def recv_reply
  @@mutex.synchronize do
    begin
      while (len_header = @sp.sysread(2))
        msg = @sp.sysread(len_header.unpack("v")[0])
        puts "Received Message: #{len_header.to_hex_str}#{msg.to_hex_str}" if $DEBUG
      
        if msg[0] != 0x02
          error = "ERROR: Returned something other then a reply telegram"
          return [false,error]
        end
      
        if msg[2] != 0x00
          error = "ERROR: #{@@error_codes[msg[2]]}"
          return [false,error]
        end
      
        return [true,msg]
      end
    rescue EOFError
    	raise "Cannot read from the NXT. Make sure the device is on and connected."
    end
  end
end

#reset_input_scaled_value(port) ⇒ Object

Reset the scaled value on an input sensor port.

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)



458
459
460
461
462
463
# File 'lib/nxt_comm.rb', line 458

def reset_input_scaled_value(port)
  cmd = [port]
  result = send_and_receive @@op_codes["reset_input_scaled_value"], cmd
  result = true if result == ""
  result
end

#reset_motor_position(port, relative = false) ⇒ Object

Reset the position of an output motor port.

  • port - output port (MOTOR_A, MOTOR_B, MOTOR_C)

  • relative - boolean, true - position relative to last movement, false - absolute position



498
499
500
501
502
503
504
505
# File 'lib/nxt_comm.rb', line 498

def reset_motor_position(port,relative = false)
  cmd = []
  cmd << port
  relative ? cmd << 0x01 : cmd << 0x00
  result = send_and_receive @@op_codes["reset_motor_position"], cmd
  result = true if result == ""
  result
end

#send_and_receive(op, cmd) ⇒ Object

Send message and return response



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/nxt_comm.rb', line 238

def send_and_receive(op,cmd)
  msg = [op] + cmd + [0x00]
  
  send_cmd(msg)
  ok,response = recv_reply
  
  if ok and response[1] == op
    data = response[3..response.size]
    # TODO ? if data contains a \n character, ruby seems to pass the parts before and after the \n
    # as two different parameters... we need to encode the data into a format that doesn't
    # contain any \n's and then decode it in the receiving method
    data = data.to_hex_str
  elsif !ok
    $stderr.puts response
    data = false
  else
    $stderr.puts "ERROR: Unexpected response #{response}"
    data = false
  end
  data
end

#send_cmd(msg) ⇒ Object

Send direct command bytes



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/nxt_comm.rb', line 261

def send_cmd(msg)
  @@mutex.synchronize do
    msg = [0x00] + msg # always request a response
    #puts "Message Size: #{msg.size}" if $DEBUG
    msg = [(msg.size & 255),(msg.size >> 8)] + msg
    puts "Sending Message: #{msg.to_hex_str}" if $DEBUG
    msg.each do |b|
      @sp.putc b
    end
  end
end

#set_input_mode(port, type, mode) ⇒ Object

Set various parameters for an input sensor port.

  • port - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)

  • type - sensor type (NO_SENSOR, SWITCH, TEMPERATURE, REFLECTION, ANGLE, LIGHT_ACTIVE, LIGHT_INACTIVE, SOUND_DB, SOUND_DBA, CUSTOM, LOWSPEED, LOWSPEED_9V, NO_OF_SENSOR_TYPES)

  • mode - sensor mode (RAWMODE, BOOLEANMODE, TRANSITIONCNTMODE, PERIODCOUNTERMODE, PCTFULLSCALEMODE, CELSIUSMODE, FAHRENHEITMODE, ANGLESTEPMODE, SLOPEMASK, MODEMASK)



362
363
364
365
366
367
# File 'lib/nxt_comm.rb', line 362

def set_input_mode(port,type,mode)
  cmd = [port,type,mode]
  result = send_and_receive @@op_codes["set_input_mode"], cmd
  result = true if result == ""
  result
end

#set_output_state(port, power, mode, reg_mode, turn_ratio, run_state, tacho_limit) ⇒ Object

Set various parameters for the output motor port(s).

  • port - output port (MOTOR_A, MOTOR_B, MOTOR_C, or MOTOR_ALL)

  • power - power set point (-100 - 100)

  • mode - output mode (MOTORON, BRAKE, REGULATED)

  • reg_mode - regulation mode (REGULATION_MODE_IDLE, REGULATION_MODE_MOTOR_SPEED, REGULATION_MODE_MOTOR_SYNC)

  • turn_ratio - turn ratio (-100 - 100) negative shifts power to left motor, positive to right, 50 = one stops, other moves, 100 = each motor moves in opposite directions

  • run_state - run state (MOTOR_RUN_STATE_IDLE, MOTOR_RUN_STATE_RAMPUP, MOTOR_RUN_STATE_RUNNING, MOTOR_RUN_STATE_RAMPDOWN)

  • tacho_limit - tacho limit (number, 0 - run forever)



351
352
353
354
355
356
# File 'lib/nxt_comm.rb', line 351

def set_output_state(port,power,mode,reg_mode,turn_ratio,run_state,tacho_limit)
  cmd = [port,power,mode,reg_mode,turn_ratio,run_state] + [tacho_limit].pack("V").unpack("C4")
  result = send_and_receive @@op_codes["set_output_state"], cmd
  result = true if result == ""
  result
end

#start_program(name) ⇒ Object

Start a program stored on the NXT.

  • name - file name of the program



301
302
303
304
305
306
307
308
309
# File 'lib/nxt_comm.rb', line 301

def start_program(name)
  cmd = []
  name.each_byte do |b|
    cmd << b
  end
  result = send_and_receive @@op_codes["start_program"], cmd
  result = true if result == ""
  result
end

#stop_programObject

Stop any programs currently running on the NXT.



312
313
314
315
316
317
# File 'lib/nxt_comm.rb', line 312

def stop_program
  cmd = []
  result = send_and_receive @@op_codes["stop_program"], cmd
  result = true if result == ""
  result
end

#stop_sound_playbackObject

Stop any currently playing sounds.



515
516
517
518
519
520
# File 'lib/nxt_comm.rb', line 515

def stop_sound_playback
  cmd = []
  result = send_and_receive @@op_codes["stop_sound_playback"], cmd
  result = true if result == ""
  result
end