Class: NXTComm
- Inherits:
-
Object
- Object
- NXTComm
- 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:
-
Linux: juju.org/articles/2006/10/22/bluetooth-serial-port-to-nxt-in-linux
-
OSX: juju.org/articles/2006/10/22/bluetooth-serial-port-to-nxt-in-osx
-
Windows: juju.org/articles/2006/08/16/ruby-serialport-nxt-on-windows
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
-
#close ⇒ Object
Close the connection.
-
#connected? ⇒ Boolean
Returns true if the connection to the NXT is open; false otherwise.
-
#get_battery_level ⇒ Object
Returns the battery voltage in millivolts.
-
#get_current_program_name ⇒ Object
Returns the name of the program currently running on the NXT.
-
#get_input_values(port) ⇒ Object
Get the current values from an input sensor port.
-
#get_output_state(port) ⇒ Object
Get the state of the output motor port.
-
#initialize(dev = $DEV) ⇒ NXTComm
constructor
Create a new instance of NXTComm.
-
#keep_alive ⇒ Object
Keep the connection alive and prevents NXT from going to sleep until sleep time.
-
#ls_get_status(port) ⇒ Object
Get the status of an LS port (like ultrasonic sensor).
-
#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) }. -
#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. -
#message_read(inbox_remote, inbox_local = 1, remove = false) ⇒ Object
Read a message from a specific inbox on the NXT.
-
#message_write(inbox, message) ⇒ Object
Write a message to a specific inbox on the NXT.
-
#play_sound_file(name, repeat = false) ⇒ Object
Play a sound file stored on the NXT.
-
#play_tone(freq, dur) ⇒ Object
Play a tone.
-
#recv_reply ⇒ Object
Process the reply.
-
#reset_input_scaled_value(port) ⇒ Object
Reset the scaled value on an input sensor port.
-
#reset_motor_position(port, relative = false) ⇒ Object
Reset the position of an output motor port.
-
#send_and_receive(op, cmd) ⇒ Object
Send message and return response.
-
#send_cmd(msg) ⇒ Object
Send direct command bytes.
-
#set_input_mode(port, type, mode) ⇒ Object
Set various parameters for an input sensor port.
-
#set_output_state(port, power, mode, reg_mode, turn_ratio, run_state, tacho_limit) ⇒ Object
Set various parameters for the output motor port(s).
-
#start_program(name) ⇒ Object
Start a program stored on the NXT.
-
#stop_program ⇒ Object
Stop any programs currently running on the NXT.
-
#stop_sound_playback ⇒ Object
Stop any currently playing sounds.
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
#close ⇒ Object
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
233 234 235 |
# File 'lib/nxt_comm.rb', line 233 def connected? not @sp.closed? end |
#get_battery_level ⇒ Object
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_name ⇒ Object
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_alive ⇒ Object
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 (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 (inbox,) cmd = [] cmd << inbox - 1 case .class.to_s when "String" cmd << .size + 1 .each_byte do |b| cmd << b end when "Fixnum" cmd << 5 # msg size + 1 #cmd.concat([(message & 255),(message >> 8),(message >> 16),(message >> 24)]) [].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_reply ⇒ Object
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_program ⇒ Object
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_playback ⇒ Object
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 |