ruby-nxt 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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