ruby-nxt 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
1
+ # ruby-nxt Control Mindstorms NXT via Bluetooth Serial Port Connection
2
+ # Copyright (C) 2006 Matt Zukowski <matt@roughest.net>
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software Foundation,
15
+ # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
+
17
+ require "autodetect_nxt"
18
+
19
+ require "nxt_comm"
20
+ require "motor"
21
+
22
+ require "sensors/touch_sensor"
23
+ require "sensors/sound_sensor"
24
+ require "light_sensor"
25
+ require "ultrasonic_sensor"
26
+
27
+ # High-level interface for controlling motors and sensors connected to the NXT.
28
+ # Currently only motors and some other misc functionality is implemented.
29
+ #
30
+ # Examples:
31
+ #
32
+ # nxt = NXT.new('/dev/tty.NXT-DevB-1')
33
+ #
34
+ # nxt.motor_a do |m|
35
+ # m.forward(:degrees => 180, :power => 15)
36
+ # end
37
+ #
38
+ # nxt.motors_bc do |m|
39
+ # m.backward(:time => 5, :power => 20)
40
+ # end
41
+ #
42
+ # nxt.motors_abc do |m|
43
+ # m.reset_tacho
44
+ # m.forward(:time => 3, :power => 10)
45
+ # puts "Motor #{m.name} moved #{m.read_state[:degree_count]} degrees."
46
+ # end
47
+ #
48
+ # nxt.disconnect
49
+ #
50
+ # Be sure to call NXT#disconnect when done sending commands, otherwise there may be trouble
51
+ # if you try to connect or send commands again afterwards.
52
+ #
53
+ class NXT
54
+
55
+ # Initialize the NXT. This creates three Motor instances and one kind of each sensor.
56
+ # It is assumed that the sensors are connected to the standard ports as follows:
57
+ # * Port 1: Touch
58
+ # * Port 2: Sound
59
+ # * Port 3: Light
60
+ # * Port 4: Ultrasonic
61
+ # You can specify the path to the serialport device (e.g. '/dev/tty.NXT-DevB-1')
62
+ # or omit the argument to use the serialport device specified in the global
63
+ # $DEV variable.
64
+ def initialize(dev = $DEV)
65
+ @nxt = NXTComm.new(dev)
66
+
67
+ @motors = {}
68
+ @motors[:a] = Motor.new(@nxt, :a)
69
+ @motors[:b] = Motor.new(@nxt, :b)
70
+ @motors[:c] = Motor.new(@nxt, :c)
71
+
72
+ @sensors = {}
73
+ @sensors[1] = TouchSensor.new(@nxt, NXTComm::SENSOR_1)
74
+ @sensors[2] = SoundSensor.new(@nxt, NXTComm::SENSOR_2)
75
+ @sensors[3] = LightSensor.new(@nxt, NXTComm::SENSOR_3)
76
+ @sensors[4] = UltrasonicSensor.new(@nxt, NXTComm::SENSOR_4)
77
+
78
+ @motor_threads = {}
79
+ @sensor_threads = {}
80
+ end
81
+
82
+ def method_missing(method, *args, &block)
83
+ name = method.id2name
84
+ if /^motor_([abc])$/ =~ name
85
+ motor($1, block)
86
+ elsif /^motors_([abc]+?)$/ =~ name
87
+ motors($1, block)
88
+ elsif /^sensor_([1234])$/ =~ name
89
+ sensor($1, block)
90
+ elsif /^sensor_(touch|sound|light|ultrasonic)$/ =~ name or
91
+ /^(touch|sound|light|ultrasonic)_sensor$/ =~ name
92
+ case $1
93
+ when 'touch'
94
+ sensor(1, block)
95
+ when 'sound'
96
+ sensor(2, block)
97
+ when 'light'
98
+ sensor(3, block)
99
+ when 'ultrasonic'
100
+ sensor(4, block)
101
+ else
102
+ raise "'#{$1}' is not a valid sensor."
103
+ end
104
+ else
105
+ # if the method is not recognized, we assume it is a low-level NXTComm command
106
+ m = @nxt.method(method)
107
+ m.call(*args)
108
+ end
109
+ end
110
+
111
+ # Runs the given proc for multiple motors.
112
+ # You should use the motors_xxx dynamic method instead of calling this directly.
113
+ # For example...
114
+ #
115
+ # nxt.motors_abc {|m| m.forward(:degrees => 180)}
116
+ #
117
+ # ...would run the given block simultanously on all three motors,
118
+ # while...
119
+ #
120
+ # nxt.motors_bc {|m| m.forward(:degrees => 180)}
121
+ #
122
+ # ...would only run it on motors B and C.
123
+ def motors(which, proc)
124
+ which = which.scan(/\w/) if which.kind_of? String
125
+ which = which.uniq
126
+ which = @motors.keys if which.nil? or which.empty?
127
+
128
+ which.each do |id|
129
+ motor(id, proc)
130
+ end
131
+ end
132
+
133
+ # Runs the given proc for the given motor.
134
+ # You should use the motor_x dynamic method instead of calling this directly.
135
+ # For example...
136
+ #
137
+ # nxt.motor_a {|m| m.forward(:degrees => 180)}
138
+ #
139
+ # ...would rotate motor A by 180 degrees,
140
+ # while...
141
+ #
142
+ # nxt.motor_b {|m| m.forward(:degrees => 180)}
143
+ #
144
+ # ...would do the same for motor B.
145
+ def motor(id, proc)
146
+ id = id.intern if id.kind_of? String
147
+
148
+ # If a thread for this motor is already running, wait until it's finished.
149
+ # In other words, don't try to send another command to the motor if it is already
150
+ # doing something else; wait until it's done and then send.
151
+ # FIXME: I think this blocks the entire program... is that what we really want?
152
+ # I think it is, but need to think about it more...
153
+ @motor_threads[id].join if (@motor_threads[id] and @motor_threads[id].alive?)
154
+
155
+ t = Thread.new(@motors[id]) do |m|
156
+ proc.call(m)
157
+ end
158
+
159
+ @motor_threads[id] = t
160
+ end
161
+
162
+ def sensor(id, proc)
163
+ id = id.to_i
164
+
165
+ @sensor_threads[id].join if (@sensor_threads[id] and @sensor_threads[id].alive?)
166
+
167
+ t = Thread.new(@sensors[id]) do |m|
168
+ proc.call(m)
169
+ end
170
+
171
+ # FIXME: this blocks until we get something back from the sensor... probably
172
+ # not the smartest way to do this
173
+ t.join
174
+
175
+ # FIXME: do we need to store the thread? it will always be dead by this point..
176
+ @sensor_threads[id] = t
177
+ end
178
+
179
+ # Waits for all running jobs to finish and cleanly closes
180
+ # all connections to the NXT device.
181
+ # You should _always_ call this when done sending commands
182
+ # to the NXT.
183
+ def disconnect
184
+ @sensor_threads.each {|i,t| t.join}
185
+ @motor_threads.each {|i,t| t.join}
186
+ @nxt.close
187
+ end
188
+
189
+ end
@@ -0,0 +1,596 @@
1
+ # ruby-nxt Control Mindstorms NXT via Bluetooth Serial Port Connection
2
+ # Copyright (C) 2006 Tony Buser <tbuser@gmail.com> - http://juju.org
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 2 of the License
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software Foundation,
15
+ # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
+
17
+ begin
18
+ # Need to do a Kernel::require otherwise when included with rubygems, it fails
19
+ Kernel::require "serialport"
20
+ rescue LoadError
21
+ puts
22
+ puts "You must have the ruby-serialport library installed!"
23
+ puts "You can download ruby-serialport from http://rubyforge.org/projects/ruby-serialport/"
24
+ puts
25
+ exit 1
26
+ end
27
+ require "thread"
28
+ require "commands"
29
+
30
+ class Array
31
+ def to_hex_str
32
+ self.collect{|e| "0x%02x " % e}
33
+ end
34
+ end
35
+
36
+ class String
37
+ def to_hex_str
38
+ str = ""
39
+ self.each_byte {|b| str << '0x%02x ' % b}
40
+ str
41
+ end
42
+
43
+ def from_hex_str
44
+ data = self.split(' ')
45
+ str = ""
46
+ data.each{|h| eval "str += '%c' % #{h}"}
47
+ str
48
+ end
49
+ end
50
+
51
+ class Bignum
52
+ # This is needed because String#unpack() can't handle little-endian signed longs...
53
+ # instead we unpack() as a little-endian unsigned long (i.e. 'V') and then use this
54
+ # method to convert to signed long.
55
+ def as_signed
56
+ -1*(self^0xffffffff) if self > 0xfffffff
57
+ end
58
+ end
59
+
60
+ # = Description
61
+ #
62
+ # Low-level interface for communicating directly with the NXT via
63
+ # a Bluetooth serial port. Implements direct commands outlined in
64
+ # Appendix 2-LEGO MINDSTORMS NXT Direct Commands.pdf
65
+ #
66
+ # Not all functionality is implemented yet!
67
+ #
68
+ # For instructions on creating a bluetooth serial port connection:
69
+ # * Linux: http://juju.org/articles/2006/10/22/bluetooth-serial-port-to-nxt-in-linux
70
+ # * OSX: http://juju.org/articles/2006/10/22/bluetooth-serial-port-to-nxt-in-osx
71
+ # * Windows: http://juju.org/articles/2006/08/16/ruby-serialport-nxt-on-windows
72
+ #
73
+ # =Examples
74
+ #
75
+ # First create a new NXTComm object and pass the device.
76
+ #
77
+ # @nxt = NXTComm.new("/dev/tty.NXT-DevB-1")
78
+ #
79
+ # Rotate the motor connected to port B forwards indefinitely at 100% power:
80
+ #
81
+ # @nxt.set_output_state(
82
+ # NXTComm::MOTOR_B,
83
+ # 100,
84
+ # NXTComm::MOTORON,
85
+ # NXTComm::REGULATION_MODE_MOTOR_SPEED,
86
+ # 100,
87
+ # NXTComm::MOTOR_RUN_STATE_RUNNING,
88
+ # 0
89
+ # )
90
+ #
91
+ # Play a tone at 1000 Hz for 500 ms:
92
+ #
93
+ # @nxt.play_tone(1000,500)
94
+ #
95
+ # Print out the current battery level:
96
+ #
97
+ # puts "Battery Level: #{@nxt.get_battery_level/1000.0} V"
98
+ #
99
+ class NXTComm
100
+
101
+ # sensors
102
+ SENSOR_1 = 0x00
103
+ SENSOR_2 = 0x01
104
+ SENSOR_3 = 0x02
105
+ SENSOR_4 = 0x03
106
+
107
+ # motors
108
+ MOTOR_A = 0x00
109
+ MOTOR_B = 0x01
110
+ MOTOR_C = 0x02
111
+ MOTOR_ALL = 0xFF
112
+
113
+ # output mode
114
+ COAST = 0x00 # motor will rotate freely?
115
+ MOTORON = 0x01 # enables PWM power according to speed
116
+ BRAKE = 0x02 # voltage is not allowed to float between PWM pulses, improves accuracy, uses more power
117
+ REGULATED = 0x04 # required in conjunction with output regulation mode setting
118
+
119
+ # output regulation mode
120
+ REGULATION_MODE_IDLE = 0x00 # disables regulation
121
+ REGULATION_MODE_MOTOR_SPEED = 0x01 # auto adjust PWM duty cycle if motor is affected by physical load
122
+ REGULATION_MODE_MOTOR_SYNC = 0x02 # attempt to keep rotation in sync with another motor that has this set, also involves turn ratio
123
+
124
+ # output run state
125
+ MOTOR_RUN_STATE_IDLE = 0x00 # disables power to motor
126
+ MOTOR_RUN_STATE_RAMPUP = 0x10 # ramping to a new SPEED set-point that is greater than the current SPEED set-point
127
+ MOTOR_RUN_STATE_RUNNING = 0x20 # enables power to motor
128
+ MOTOR_RUN_STATE_RAMPDOWN = 0x40 # ramping to a new SPEED set-point that is less than the current SPEED set-point
129
+
130
+ # sensor type
131
+ NO_SENSOR = 0x00
132
+ SWITCH = 0x01
133
+ TEMPERATURE = 0x02
134
+ REFLECTION = 0x03
135
+ ANGLE = 0x04
136
+ LIGHT_ACTIVE = 0x05
137
+ LIGHT_INACTIVE = 0x06
138
+ SOUND_DB = 0x07
139
+ SOUND_DBA = 0x08
140
+ CUSTOM = 0x09
141
+ LOWSPEED = 0x0A
142
+ LOWSPEED_9V = 0x0B
143
+ NO_OF_SENSOR_TYPES = 0x0C
144
+
145
+ # sensor mode
146
+ RAWMODE = 0x00 # report scaled value equal to raw value
147
+ BOOLEANMODE = 0x20 # report scaled value as 1 true or 0 false, false if raw value > 55% of total range, true if < 45%
148
+ TRANSITIONCNTMODE = 0x40 # report scaled value as number of transitions between true and false
149
+ PERIODCOUNTERMODE = 0x60 # report scaled value as number of transitions from false to true, then back to false
150
+ PCTFULLSCALEMODE = 0x80 # report scaled value as % of full scale reading for configured sensor type
151
+ CELSIUSMODE = 0xA0
152
+ FAHRENHEITMODE = 0xC0
153
+ ANGLESTEPSMODE = 0xE0 # report scaled value as count of ticks on RCX-style rotation sensor
154
+ SLOPEMASK = 0x1F
155
+ MODEMASK = 0xE0
156
+
157
+ @@op_codes = {
158
+ 'start_program' => 0x00,
159
+ 'stop_program' => 0x01,
160
+ 'play_sound_file' => 0x02,
161
+ 'play_tone' => 0x03,
162
+ 'set_output_state' => 0x04,
163
+ 'set_input_mode' => 0x05,
164
+ 'get_output_state' => 0x06,
165
+ 'get_input_values' => 0x07,
166
+ 'reset_input_scaled_value' => 0x08,
167
+ 'message_write' => 0x09,
168
+ 'reset_motor_position' => 0x0A,
169
+ 'get_battery_level' => 0x0B,
170
+ 'stop_sound_playback' => 0x0C,
171
+ 'keep_alive' => 0x0D,
172
+ 'ls_get_status' => 0x0E,
173
+ 'ls_write' => 0x0F,
174
+ 'ls_read' => 0x10,
175
+ 'get_current_program_name' => 0x11,
176
+ # what happened to 0x12? Dunno...
177
+ 'message_read' => 0x13
178
+ }
179
+
180
+ @@error_codes = {
181
+ 0x20 => "Pending communication transaction in progress",
182
+ 0x40 => "Specified mailbox queue is empty",
183
+ 0xBD => "Request failed (i.e. specified file not found)",
184
+ 0xBE => "Unknown command opcode",
185
+ 0xBF => "Insane packet",
186
+ 0xC0 => "Data contains out-of-range values",
187
+ 0xDD => "Communication bus error",
188
+ 0xDE => "No free memory in communication buffer",
189
+ 0xDF => "Specified channel/connection is not valid",
190
+ 0xE0 => "Specified channel/connection not configured or busy",
191
+ 0xEC => "No active program",
192
+ 0xED => "Illegal size specified",
193
+ 0xEE => "Illegal mailbox queue ID specified",
194
+ 0xEF => "Attempted to access invalid field of a structure",
195
+ 0xF0 => "Bad input or output specified",
196
+ 0xFB => "Insufficient memory available",
197
+ 0xFF => "Bad arguments"
198
+ }
199
+
200
+ @@mutex = Mutex.new
201
+
202
+ # Create a new instance of NXTComm.
203
+ # Be careful not to create more than one NXTComm object per serial port dev.
204
+ # If two NXTComms try to talk to the same dev, there will be trouble.
205
+ def initialize(dev = $DEV)
206
+
207
+ @@mutex.synchronize do
208
+ begin
209
+ @sp = SerialPort.new(dev, 57600, 8, 1, SerialPort::NONE)
210
+
211
+ @sp.flow_control = SerialPort::HARD
212
+ @sp.read_timeout = 5000
213
+ rescue Errno::EBUSY
214
+ raise "Cannot connect to #{dev}. The serial port is busy or unavailable."
215
+ end
216
+ end
217
+
218
+ if @sp.nil?
219
+ $stderr.puts "Cannot connect to #{dev}"
220
+ else
221
+ puts "Connected to: #{dev}" if $DEBUG
222
+ end
223
+ end
224
+
225
+ # Close the connection
226
+ def close
227
+ @@mutex.synchronize do
228
+ @sp.close if @sp and not @sp.closed?
229
+ end
230
+ end
231
+
232
+ # Returns true if the connection to the NXT is open; false otherwise
233
+ def connected?
234
+ not @sp.closed?
235
+ end
236
+
237
+ # Send message and return response
238
+ def send_and_receive(op,cmd)
239
+ msg = [op] + cmd + [0x00]
240
+
241
+ send_cmd(msg)
242
+ ok,response = recv_reply
243
+
244
+ if ok and response[1] == op
245
+ data = response[3..response.size]
246
+ # TODO ? if data contains a \n character, ruby seems to pass the parts before and after the \n
247
+ # as two different parameters... we need to encode the data into a format that doesn't
248
+ # contain any \n's and then decode it in the receiving method
249
+ data = data.to_hex_str
250
+ elsif !ok
251
+ $stderr.puts response
252
+ data = false
253
+ else
254
+ $stderr.puts "ERROR: Unexpected response #{response}"
255
+ data = false
256
+ end
257
+ data
258
+ end
259
+
260
+ # Send direct command bytes
261
+ def send_cmd(msg)
262
+ @@mutex.synchronize do
263
+ msg = [0x00] + msg # always request a response
264
+ #puts "Message Size: #{msg.size}" if $DEBUG
265
+ msg = [(msg.size & 255),(msg.size >> 8)] + msg
266
+ puts "Sending Message: #{msg.to_hex_str}" if $DEBUG
267
+ msg.each do |b|
268
+ @sp.putc b
269
+ end
270
+ end
271
+ end
272
+
273
+ # Process the reply
274
+ def recv_reply
275
+ @@mutex.synchronize do
276
+ begin
277
+ while (len_header = @sp.sysread(2))
278
+ msg = @sp.sysread(len_header.unpack("v")[0])
279
+ puts "Received Message: #{len_header.to_hex_str}#{msg.to_hex_str}" if $DEBUG
280
+
281
+ if msg[0] != 0x02
282
+ error = "ERROR: Returned something other then a reply telegram"
283
+ return [false,error]
284
+ end
285
+
286
+ if msg[2] != 0x00
287
+ error = "ERROR: #{@@error_codes[msg[2]]}"
288
+ return [false,error]
289
+ end
290
+
291
+ return [true,msg]
292
+ end
293
+ rescue EOFError
294
+ raise "Cannot read from the NXT. Make sure the device is on and connected."
295
+ end
296
+ end
297
+ end
298
+
299
+ # Start a program stored on the NXT.
300
+ # * <tt>name</tt> - file name of the program
301
+ def start_program(name)
302
+ cmd = []
303
+ name.each_byte do |b|
304
+ cmd << b
305
+ end
306
+ result = send_and_receive @@op_codes["start_program"], cmd
307
+ result = true if result == ""
308
+ result
309
+ end
310
+
311
+ # Stop any programs currently running on the NXT.
312
+ def stop_program
313
+ cmd = []
314
+ result = send_and_receive @@op_codes["stop_program"], cmd
315
+ result = true if result == ""
316
+ result
317
+ end
318
+
319
+ # Play a sound file stored on the NXT.
320
+ # * <tt>name</tt> - file name of the sound file to play
321
+ # * <tt>repeat</tt> - Loop? (true or false)
322
+ def play_sound_file(name,repeat = false)
323
+ cmd = []
324
+ repeat ? cmd << 0x01 : cmd << 0x00
325
+ name.each_byte do |b|
326
+ cmd << b
327
+ end
328
+ result = send_and_receive @@op_codes["play_sound_file"], cmd
329
+ result = true if result == ""
330
+ result
331
+ end
332
+
333
+ # Play a tone.
334
+ # * <tt>freq</tt> - frequency for the tone in Hz
335
+ # * <tt>dur</tt> - duration for the tone in ms
336
+ def play_tone(freq,dur)
337
+ cmd = [(freq & 255),(freq >> 8),(dur & 255),(dur >> 8)]
338
+ result = send_and_receive @@op_codes["play_tone"], cmd
339
+ result = true if result == ""
340
+ result
341
+ end
342
+
343
+ # Set various parameters for the output motor port(s).
344
+ # * <tt>port</tt> - output port (MOTOR_A, MOTOR_B, MOTOR_C, or MOTOR_ALL)
345
+ # * <tt>power</tt> - power set point (-100 - 100)
346
+ # * <tt>mode</tt> - output mode (MOTORON, BRAKE, REGULATED)
347
+ # * <tt>reg_mode</tt> - regulation mode (REGULATION_MODE_IDLE, REGULATION_MODE_MOTOR_SPEED, REGULATION_MODE_MOTOR_SYNC)
348
+ # * <tt>turn_ratio</tt> - 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
349
+ # * <tt>run_state</tt> - run state (MOTOR_RUN_STATE_IDLE, MOTOR_RUN_STATE_RAMPUP, MOTOR_RUN_STATE_RUNNING, MOTOR_RUN_STATE_RAMPDOWN)
350
+ # * <tt>tacho_limit</tt> - tacho limit (number, 0 - run forever)
351
+ def set_output_state(port,power,mode,reg_mode,turn_ratio,run_state,tacho_limit)
352
+ cmd = [port,power,mode,reg_mode,turn_ratio,run_state] + [tacho_limit].pack("V").unpack("C4")
353
+ result = send_and_receive @@op_codes["set_output_state"], cmd
354
+ result = true if result == ""
355
+ result
356
+ end
357
+
358
+ # Set various parameters for an input sensor port.
359
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
360
+ # * <tt>type</tt> - sensor type (NO_SENSOR, SWITCH, TEMPERATURE, REFLECTION, ANGLE, LIGHT_ACTIVE, LIGHT_INACTIVE, SOUND_DB, SOUND_DBA, CUSTOM, LOWSPEED, LOWSPEED_9V, NO_OF_SENSOR_TYPES)
361
+ # * <tt>mode</tt> - sensor mode (RAWMODE, BOOLEANMODE, TRANSITIONCNTMODE, PERIODCOUNTERMODE, PCTFULLSCALEMODE, CELSIUSMODE, FAHRENHEITMODE, ANGLESTEPMODE, SLOPEMASK, MODEMASK)
362
+ def set_input_mode(port,type,mode)
363
+ cmd = [port,type,mode]
364
+ result = send_and_receive @@op_codes["set_input_mode"], cmd
365
+ result = true if result == ""
366
+ result
367
+ end
368
+
369
+ # Get the state of the output motor port.
370
+ # * <tt>port</tt> - output port (MOTOR_A, MOTOR_B, MOTOR_C)
371
+ # Returns a hash with the following info (enumerated values see: set_output_state):
372
+ # {
373
+ # :port => see: output ports,
374
+ # :power => -100 - 100,
375
+ # :mode => see: output modes,
376
+ # :reg_mode => see: regulation modes,
377
+ # :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,
378
+ # :run_state => see: run states,
379
+ # :tacho_limit => current limit on a movement in progress, if any,
380
+ # :tacho_count => internal count, number of counts since last reset of the motor counter,
381
+ # :block_tacho_count => current position relative to last programmed movement,
382
+ # :rotation_count => current position relative to last reset of the rotation sensor for this motor
383
+ # }
384
+ def get_output_state(port)
385
+ cmd = [port]
386
+ result = send_and_receive @@op_codes["get_output_state"], cmd
387
+
388
+ if result
389
+ result_parts = result.from_hex_str.unpack('C6V4')
390
+ (7..9).each do |i|
391
+ result_parts[i] = result_parts[i].as_signed if result_parts[i].kind_of? Bignum
392
+ end
393
+
394
+ {
395
+ :port => result_parts[0],
396
+ :power => result_parts[1],
397
+ :mode => result_parts[2],
398
+ :reg_mode => result_parts[3],
399
+ :turn_ratio => result_parts[4],
400
+ :run_state => result_parts[5],
401
+ :tacho_limit => result_parts[6],
402
+ :tacho_count => result_parts[7],
403
+ :block_tacho_count => result_parts[8],
404
+ :rotation_count => result_parts[9]
405
+ }
406
+ else
407
+ false
408
+ end
409
+ end
410
+
411
+ # Get the current values from an input sensor port.
412
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
413
+ # Returns a hash with the following info (enumerated values see: set_input_mode):
414
+ # {
415
+ # :port => see: input ports,
416
+ # :valid => boolean, true if new data value should be seen as valid data,
417
+ # :calibrated => boolean, true if calibration file found and used for 'Calibrated Value' field below,
418
+ # :type => see: sensor types,
419
+ # :mode => see: sensor modes,
420
+ # :value_raw => raw A/D value (device dependent),
421
+ # :value_normal => normalized A/D value (0 - 1023),
422
+ # :value_scaled => scaled value (mode dependent),
423
+ # :value_calibrated => calibrated value, scaled to calibration (CURRENTLY UNUSED)
424
+ # }
425
+ def get_input_values(port)
426
+ cmd = [port]
427
+ result = send_and_receive @@op_codes["get_input_values"], cmd
428
+
429
+ if result
430
+ result_parts = result.from_hex_str.unpack('C5v4')
431
+ result_parts[1] == 0x01 ? result_parts[1] = true : result_parts[1] = false
432
+ result_parts[2] == 0x01 ? result_parts[2] = true : result_parts[2] = false
433
+
434
+ (7..8).each do |i|
435
+ # convert to signed word
436
+ # FIXME: is this right?
437
+ result_parts[i] = -1*(result_parts[i]^0xffff) if result_parts[i] > 0xfff
438
+ end
439
+
440
+ {
441
+ :port => result_parts[0],
442
+ :valid => result_parts[1],
443
+ :calibrated => result_parts[2],
444
+ :type => result_parts[3],
445
+ :mode => result_parts[4],
446
+ :value_raw => result_parts[5],
447
+ :value_normal => result_parts[6],
448
+ :value_scaled => result_parts[7],
449
+ :value_calibrated => result_parts[8],
450
+ }
451
+ else
452
+ false
453
+ end
454
+ end
455
+
456
+ # Reset the scaled value on an input sensor port.
457
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
458
+ def reset_input_scaled_value(port)
459
+ cmd = [port]
460
+ result = send_and_receive @@op_codes["reset_input_scaled_value"], cmd
461
+ result = true if result == ""
462
+ result
463
+ end
464
+
465
+ # Write a message to a specific inbox on the NXT. This is used to send a message to a currently running program.
466
+ # * <tt>inbox</tt> - inbox number (1 - 10)
467
+ # * <tt>message</tt> - message data
468
+ def message_write(inbox,message)
469
+ cmd = []
470
+ cmd << inbox - 1
471
+ case message.class.to_s
472
+ when "String"
473
+ cmd << message.size + 1
474
+ message.each_byte do |b|
475
+ cmd << b
476
+ end
477
+ when "Fixnum"
478
+ cmd << 5 # msg size + 1
479
+ #cmd.concat([(message & 255),(message >> 8),(message >> 16),(message >> 24)])
480
+ [message].pack("V").each_byte{|b| cmd << b}
481
+ when "TrueClass"
482
+ cmd << 2 # msg size + 1
483
+ cmd << 1
484
+ when "FalseClass"
485
+ cmd << 2 # msg size + 1
486
+ cmd << 0
487
+ else
488
+ raise "Invalid message type"
489
+ end
490
+ result = send_and_receive @@op_codes["message_write"], cmd
491
+ result = true if result == ""
492
+ result
493
+ end
494
+
495
+ # Reset the position of an output motor port.
496
+ # * <tt>port</tt> - output port (MOTOR_A, MOTOR_B, MOTOR_C)
497
+ # * <tt>relative</tt> - boolean, true - position relative to last movement, false - absolute position
498
+ def reset_motor_position(port,relative = false)
499
+ cmd = []
500
+ cmd << port
501
+ relative ? cmd << 0x01 : cmd << 0x00
502
+ result = send_and_receive @@op_codes["reset_motor_position"], cmd
503
+ result = true if result == ""
504
+ result
505
+ end
506
+
507
+ # Returns the battery voltage in millivolts.
508
+ def get_battery_level
509
+ cmd = []
510
+ result = send_and_receive @@op_codes["get_battery_level"], cmd
511
+ result == false ? false : result.from_hex_str.unpack("v")[0]
512
+ end
513
+
514
+ # Stop any currently playing sounds.
515
+ def stop_sound_playback
516
+ cmd = []
517
+ result = send_and_receive @@op_codes["stop_sound_playback"], cmd
518
+ result = true if result == ""
519
+ result
520
+ end
521
+
522
+ # Keep the connection alive and prevents NXT from going to sleep until sleep time. Also, returns the current sleep time limit in ms
523
+ def keep_alive
524
+ cmd = []
525
+ result = send_and_receive @@op_codes["keep_alive"], cmd
526
+ result == false ? false : result.from_hex_str.unpack("L")[0]
527
+ end
528
+
529
+ # Get the status of an LS port (like ultrasonic sensor). Returns the count of available bytes to read.
530
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
531
+ def ls_get_status(port)
532
+ cmd = [port]
533
+ result = send_and_receive @@op_codes["ls_get_status"], cmd
534
+ result[0]
535
+ end
536
+
537
+ # Write data to lowspeed I2C port (for talking to the ultrasonic sensor)
538
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
539
+ # * <tt>i2c_msg</tt> - the I2C message to send to the lowspeed controller; the first byte
540
+ # specifies the transmitted data length, the second byte specifies the expected respone
541
+ # data length, and the remaining 16 bytes are the transmitted data. See UltrasonicComm
542
+ # for an example of an I2C sensor protocol implementation.
543
+ #
544
+ # For LS communication on the NXT, data lengths are limited to 16 bytes per command. Rx data length
545
+ # MUST be specified in the write command since reading from the device is done on a master-slave basis
546
+ def ls_write(port,i2c_msg)
547
+ cmd = [port] + i2c_msg
548
+ result = send_and_receive @@op_codes["ls_write"], cmd
549
+ result = true if result == ""
550
+ result
551
+ end
552
+
553
+ # Read data from from lowspeed I2C port (for receiving data from the ultrasonic sensor)
554
+ # * <tt>port</tt> - input port (SENSOR_1, SENSOR_2, SENSOR_3, SENSOR_4)
555
+ # Returns a hash containing:
556
+ # {
557
+ # :bytes_read => number of bytes read
558
+ # :data => Rx data (padded)
559
+ # }
560
+ #
561
+ # For LS communication on the NXT, data lengths are limited to 16 bytes per command.
562
+ # Furthermore, this protocol does not support variable-length return packages, so the response
563
+ # will always contain 16 data bytes, with invalid data bytes padded with zeroes.
564
+ def ls_read(port)
565
+ cmd = [port]
566
+ result = send_and_receive @@op_codes["ls_read"], cmd
567
+ if result
568
+ result = result.from_hex_str
569
+ {
570
+ :bytes_read => result[0],
571
+ :data => result[1..-1]
572
+ }
573
+ else
574
+ false
575
+ end
576
+ end
577
+
578
+ # Returns the name of the program currently running on the NXT.
579
+ # Returns an error If no program is running.
580
+ def get_current_program_name
581
+ cmd = []
582
+ result = send_and_receive @@op_codes["get_current_program_name"], cmd
583
+ result == false ? false : result.from_hex_str.unpack("A*")[0]
584
+ end
585
+
586
+ # Read a message from a specific inbox on the NXT.
587
+ # * <tt>inbox_remote</tt> - remote inbox number (1 - 10)
588
+ # * <tt>inbox_local</tt> - local inbox number (1 - 10) (not sure why you need this?)
589
+ # * <tt>remove</tt> - boolean, true - clears message from remote inbox
590
+ def message_read(inbox_remote,inbox_local = 1,remove = false)
591
+ cmd = [inbox_remote, inbox_local]
592
+ remove ? cmd << 0x01 : cmd << 0x00
593
+ result = send_and_receive @@op_codes["message_read"], cmd
594
+ result == false ? false : result[2..-1].from_hex_str.unpack("A*")[0]
595
+ end
596
+ end