ruby-nxt 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +340 -0
- data/README +49 -0
- data/examples/commands.rb +180 -0
- data/examples/drb_client.rb +11 -0
- data/examples/drb_server.rb +40 -0
- data/examples/mary.rb +78 -0
- data/examples/move_by_degrees.rb +42 -0
- data/examples/nxt_comm_demo.rb +65 -0
- data/examples/nxt_remote_control.rb +70 -0
- data/examples/socket_server.rb +37 -0
- data/lib/autodetect_nxt.rb +36 -0
- data/lib/brick.rb +53 -0
- data/lib/commands.rb +40 -0
- data/lib/commands/light_sensor.rb +82 -0
- data/lib/commands/mixins/motor.rb +84 -0
- data/lib/commands/mixins/sensor.rb +38 -0
- data/lib/commands/motor.rb +136 -0
- data/lib/commands/move.rb +210 -0
- data/lib/commands/rotation_sensor.rb +72 -0
- data/lib/commands/sound.rb +102 -0
- data/lib/commands/sound_sensor.rb +67 -0
- data/lib/commands/touch_sensor.rb +84 -0
- data/lib/commands/ultrasonic_sensor.rb +84 -0
- data/lib/motor.rb +235 -0
- data/lib/nxt.rb +189 -0
- data/lib/nxt_comm.rb +596 -0
- data/lib/sensors/light_sensor.rb +45 -0
- data/lib/sensors/sensor.rb +93 -0
- data/lib/sensors/sound_sensor.rb +48 -0
- data/lib/sensors/touch_sensor.rb +35 -0
- data/lib/sensors/ultrasonic_sensor.rb +95 -0
- data/lib/ultrasonic_comm.rb +102 -0
- data/test/bt_test.rb +10 -0
- data/test/interactive/interactive_test_helper.rb +89 -0
- data/test/interactive/test_sensors.rb +236 -0
- data/test/test.rb +22 -0
- data/test/test_helper.rb +1 -0
- data/test/unit/motor_test.rb +177 -0
- data/test/unit/nxt_comm_test.rb +155 -0
- data/test/unit/nxt_test.rb +60 -0
- metadata +95 -0
data/lib/nxt.rb
ADDED
@@ -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
|
data/lib/nxt_comm.rb
ADDED
@@ -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
|