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.
- 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
|