lego-nxt 0.2.0 → 0.3.0
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.
- checksums.yaml +5 -5
- data/README.md +16 -12
- data/Rakefile +9 -12
- data/lib/lego_nxt.rb +36 -0
- data/lib/nxt/commands/base.rb +56 -7
- data/lib/nxt/commands/input.rb +30 -12
- data/lib/nxt/commands/low_speed.rb +47 -0
- data/lib/nxt/commands/output.rb +9 -7
- data/lib/nxt/commands/program.rb +3 -0
- data/lib/nxt/commands/sound.rb +3 -0
- data/lib/nxt/commands/tone.rb +3 -0
- data/lib/nxt/connector/input/base.rb +9 -0
- data/lib/nxt/{connectors → connector}/input/color.rb +3 -0
- data/lib/nxt/{connectors → connector}/input/touch.rb +3 -0
- data/lib/nxt/connector/input/ultrasonic.rb +66 -0
- data/lib/nxt/connector/output/base.rb +9 -0
- data/lib/nxt/connector/output/motor.rb +117 -0
- data/lib/nxt/exceptions.rb +3 -1
- data/lib/nxt/interface/base.rb +18 -0
- data/lib/nxt/{interfaces → interface}/serial_port.rb +13 -10
- data/lib/nxt/{interfaces → interface}/usb.rb +10 -9
- data/lib/nxt/nxt_brick.rb +51 -45
- data/lib/nxt/patches/module.rb +5 -2
- data/lib/nxt/patches/string.rb +6 -17
- data/lib/nxt/protocols/i2c.rb +118 -0
- data/lib/nxt/utils/accessors.rb +8 -7
- data/lib/nxt/utils/assertions.rb +24 -0
- data/spec/matchers.rb +2 -0
- data/spec/nxt/connector/output/motor_spec.rb +52 -0
- data/spec/nxt/interface/serial_port_spec.rb +119 -0
- data/spec/nxt/nxt_brick_spec.rb +189 -120
- data/spec/spec_helper.rb +10 -1
- metadata +193 -59
- data/lib/nxt.rb +0 -27
- data/lib/nxt/connectors/input/ultrasonic.rb +0 -11
- data/lib/nxt/connectors/output/motor.rb +0 -116
- data/lib/nxt/interfaces/base.rb +0 -26
- data/spec/nxt/interfaces/serial_port_spec.rb +0 -73
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NXT
|
4
|
+
module Connector
|
5
|
+
# Holds implementations of connectors that are output based.
|
6
|
+
module Output
|
7
|
+
# Implements the "motor" output for the NXT 2.0 module.
|
8
|
+
class Motor
|
9
|
+
include NXT::Command::Output
|
10
|
+
include NXT::Utils::Assertions
|
11
|
+
extend NXT::Utils::Accessors
|
12
|
+
|
13
|
+
DURATION_TYPE = %i[seconds degrees rotations].freeze
|
14
|
+
DURATION_AFTER = %i[coast brake].freeze
|
15
|
+
DIRECTION = %i[forwards backwards].freeze
|
16
|
+
|
17
|
+
attr_accessor :port, :interface
|
18
|
+
|
19
|
+
attr_combined_accessor :duration, 0
|
20
|
+
attr_combined_accessor :duration_type, :seconds
|
21
|
+
attr_combined_accessor :duration_after, :stop
|
22
|
+
attr_combined_accessor :direction, :forwards
|
23
|
+
|
24
|
+
attr_setter :direction, is_key_in: DIRECTION
|
25
|
+
|
26
|
+
def initialize(port, interface)
|
27
|
+
@port = port
|
28
|
+
@interface = interface
|
29
|
+
end
|
30
|
+
|
31
|
+
def duration=(duration, options = {})
|
32
|
+
raise(TypeError, 'Expected duration to be a number') unless duration.is_a?(Integer)
|
33
|
+
|
34
|
+
@duration = duration
|
35
|
+
|
36
|
+
self.duration_type = options[:type]
|
37
|
+
self.duration_after = options[:after]
|
38
|
+
|
39
|
+
case @duration_type
|
40
|
+
when :rotations
|
41
|
+
self.tacho_limit = @duration * 360
|
42
|
+
when :degrees
|
43
|
+
self.tacho_limit = @duration
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def forwards
|
48
|
+
self.direction = :forwards
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def backwards
|
53
|
+
self.direction = :backwards
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def stop(mode = :coast)
|
58
|
+
self.power = 0
|
59
|
+
self.mode = mode
|
60
|
+
|
61
|
+
move
|
62
|
+
end
|
63
|
+
|
64
|
+
# takes block for response, or can return the response instead.
|
65
|
+
def move
|
66
|
+
update_output_state(duration.positive? && duration_type != :seconds)
|
67
|
+
|
68
|
+
if duration.positive? && duration_type == :seconds
|
69
|
+
wait_after_move
|
70
|
+
else
|
71
|
+
reset
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset
|
76
|
+
self.duration = 0
|
77
|
+
self.direction = :forwards
|
78
|
+
self.power = 75
|
79
|
+
self.mode = :motor_on
|
80
|
+
self.regulation_mode = :idle
|
81
|
+
self.run_state = :running
|
82
|
+
self.tacho_limit = 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def duration_type=(type)
|
89
|
+
if type.nil?
|
90
|
+
@duration_type = :seconds
|
91
|
+
else
|
92
|
+
assert_in(:type, type, DURATION_TYPE)
|
93
|
+
@duration_type = type
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def duration_after=(after)
|
98
|
+
if after.nil?
|
99
|
+
@duration_after = :stop
|
100
|
+
else
|
101
|
+
unless @duration_type == :seconds
|
102
|
+
raise(TypeError, 'The after option is only available when the unit duration is in seconds.')
|
103
|
+
end
|
104
|
+
|
105
|
+
assert_in(:after, after, DURATION_AFTER)
|
106
|
+
@duration_after = after
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def wait_after_move
|
111
|
+
sleep(duration)
|
112
|
+
reset
|
113
|
+
stop(duration_after)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/nxt/exceptions.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NXT
|
2
4
|
module Exceptions
|
3
5
|
# Raised when a class has not implemented a method from the base class
|
4
6
|
# that is required to be overriden.
|
5
|
-
class InterfaceNotImplemented <
|
7
|
+
class InterfaceNotImplemented < RuntimeError; end
|
6
8
|
|
7
9
|
# Raised when an invalid interface is attempted to be constructed.
|
8
10
|
class InvalidInterfaceError < TypeError; end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NXT
|
4
|
+
module Interface
|
5
|
+
# The base implementation of all communication interfaces. This is
|
6
|
+
# effectively the basic set of abstract methods that an interface needs to
|
7
|
+
# define to slot into this framework.
|
8
|
+
class Base
|
9
|
+
def send
|
10
|
+
raise(InterfaceNotImplemented, 'The #send method must be implemented.')
|
11
|
+
end
|
12
|
+
|
13
|
+
def receive
|
14
|
+
raise(InterfaceNotImplemented, 'The #receive method must be implemented.')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,44 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'serialport'
|
2
4
|
|
3
5
|
module NXT
|
4
6
|
module Interface
|
7
|
+
# Implements serial port connectivity to the NXT 2.0 module.
|
5
8
|
class SerialPort < Base
|
6
9
|
include NXT::Exceptions
|
7
10
|
|
8
11
|
attr_reader :dev
|
9
12
|
|
10
|
-
BAUD_RATE =
|
13
|
+
BAUD_RATE = 57_600
|
11
14
|
DATA_BITS = 8
|
12
15
|
STOP_BITS = 1
|
13
16
|
PARITY = ::SerialPort::NONE
|
14
|
-
READ_TIMEOUT =
|
17
|
+
READ_TIMEOUT = 5_000
|
15
18
|
|
16
19
|
def initialize(dev)
|
17
|
-
|
20
|
+
super()
|
21
|
+
self.dev = dev
|
18
22
|
end
|
19
23
|
|
20
24
|
def dev=(dev)
|
21
|
-
raise InvalidDeviceError unless File.
|
25
|
+
raise InvalidDeviceError unless File.exist?(dev)
|
26
|
+
|
22
27
|
@dev = dev
|
23
28
|
end
|
24
29
|
|
25
30
|
def connect
|
26
31
|
@connection = ::SerialPort.new(@dev, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY)
|
27
32
|
|
28
|
-
if @connection.nil?
|
29
|
-
raise SerialPortConnectionError.new("Could not establish a SerialPort connection to #{dev}")
|
30
|
-
end
|
33
|
+
raise SerialPortConnectionError, "Could not establish a SerialPort connection to #{dev}" if @connection.nil?
|
31
34
|
|
32
35
|
@connection.flow_control = ::SerialPort::HARD
|
33
36
|
@connection.read_timeout = READ_TIMEOUT
|
34
37
|
|
35
38
|
@connection
|
36
39
|
rescue ArgumentError
|
37
|
-
raise SerialPortConnectionError
|
40
|
+
raise SerialPortConnectionError, "The #{dev} device is not a valid SerialPort"
|
38
41
|
end
|
39
42
|
|
40
43
|
def disconnect
|
41
|
-
@connection.close if
|
44
|
+
@connection.close if connected?
|
42
45
|
end
|
43
46
|
|
44
47
|
def connected?
|
@@ -69,7 +72,7 @@ module NXT
|
|
69
72
|
#
|
70
73
|
# Reference: Appendix 1, Page 22
|
71
74
|
length = @connection.sysread(2)
|
72
|
-
@connection.sysread(length.
|
75
|
+
@connection.sysread(length.unpack1('v')).from_hex_str
|
73
76
|
end
|
74
77
|
end
|
75
78
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'libusb'
|
2
4
|
|
3
5
|
module NXT
|
4
6
|
module Interface
|
7
|
+
# Implements USB connectivity to the NXT 2.0 module.
|
5
8
|
class Usb < Base
|
6
9
|
include NXT::Exceptions
|
7
10
|
|
@@ -9,7 +12,7 @@ module NXT
|
|
9
12
|
ID_PRODUCT_NXT = 0x0002
|
10
13
|
OUT_ENDPOINT = 0x01
|
11
14
|
IN_ENDPOINT = 0x82
|
12
|
-
TIMEOUT =
|
15
|
+
TIMEOUT = 10_000
|
13
16
|
READSIZE = 64
|
14
17
|
INTERFACE = 0
|
15
18
|
|
@@ -17,9 +20,7 @@ module NXT
|
|
17
20
|
@usb_context = LIBUSB::Context.new
|
18
21
|
@dev = @usb_context.devices(idVendor: ID_VENDOR_LEGO, idProduct: ID_PRODUCT_NXT).first
|
19
22
|
|
20
|
-
if @dev.nil?
|
21
|
-
raise UsbConnectionError.new("Could not find NXT attached as USB device")
|
22
|
-
end
|
23
|
+
raise UsbConnectionError, 'Could not find NXT attached as USB device' if @dev.nil?
|
23
24
|
|
24
25
|
@connection = @dev.open
|
25
26
|
@connection.claim_interface(INTERFACE)
|
@@ -28,10 +29,10 @@ module NXT
|
|
28
29
|
end
|
29
30
|
|
30
31
|
def disconnect
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
return unless connected?
|
33
|
+
|
34
|
+
@connection.release_interface(INTERFACE)
|
35
|
+
@connection.close
|
35
36
|
end
|
36
37
|
|
37
38
|
def connected?
|
@@ -44,7 +45,7 @@ module NXT
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def receive
|
47
|
-
@connection.bulk_transfer(endpoint: IN_ENDPOINT, dataIn: READSIZE, timeout: TIMEOUT)
|
48
|
+
@connection.bulk_transfer(endpoint: IN_ENDPOINT, dataIn: READSIZE, timeout: TIMEOUT).from_hex_str
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
data/lib/nxt/nxt_brick.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This class is the entry point for end-users creating their own list of
|
2
4
|
# commands to execute remotely on a Lego NXT brick.
|
3
5
|
#
|
@@ -20,10 +22,11 @@
|
|
20
22
|
# nxt.disconnect
|
21
23
|
class NXTBrick
|
22
24
|
include NXT::Exceptions
|
25
|
+
include NXT::Utils::Assertions
|
23
26
|
|
24
27
|
# An enumeration of possible ports, both input and output, that the NXT brick
|
25
28
|
# can have connectors attached to.
|
26
|
-
PORTS = [
|
29
|
+
PORTS = %i[a b c one two three four].freeze
|
27
30
|
|
28
31
|
# Get the instance of the interface that this runner class is using to connect
|
29
32
|
# to the NXT brick.
|
@@ -47,33 +50,31 @@ class NXTBrick
|
|
47
50
|
interface_type = interface_type.to_s.classify
|
48
51
|
|
49
52
|
unless NXT::Interface.constants.include?(interface_type.to_sym)
|
50
|
-
raise
|
53
|
+
raise(InvalidInterfaceError, "There is no interface of type #{interface_type}.")
|
51
54
|
end
|
52
55
|
|
53
|
-
|
56
|
+
@interface = NXT::Interface.const_get(interface_type).new(*interface_args)
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
self.disconnect
|
63
|
-
end
|
58
|
+
return unless block_given?
|
59
|
+
|
60
|
+
begin
|
61
|
+
connect
|
62
|
+
yield(self)
|
63
|
+
ensure
|
64
|
+
disconnect
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
68
|
# Connect using the given interface to the NXT brick.
|
68
69
|
def connect
|
69
|
-
|
70
|
+
@interface.connect
|
70
71
|
end
|
71
72
|
|
72
73
|
# Close the connection to the NXT brick, and dispose of any resources that
|
73
74
|
# this instance of NXTBrick is using. Any commands run against this runner
|
74
75
|
# after calling disconnect will fail.
|
75
76
|
def disconnect
|
76
|
-
|
77
|
+
@interface.disconnect
|
77
78
|
end
|
78
79
|
|
79
80
|
# Add a new connector instance, binding a specific identifier to the given
|
@@ -90,45 +91,31 @@ class NXTBrick
|
|
90
91
|
# be, though it must be able to hook in correctly
|
91
92
|
# with the NXT library.
|
92
93
|
def add(port, identifier, klass)
|
93
|
-
|
94
|
-
|
95
|
-
|
94
|
+
assert_in('port', port, PORTS)
|
95
|
+
assert_responds_to('identifier', identifier, :to_sym)
|
96
|
+
assert_type('klass', klass, Class)
|
97
|
+
|
98
|
+
if respond_to?(identifier)
|
99
|
+
if instance_variable_get(:"@#{port}").nil?
|
100
|
+
raise(
|
101
|
+
InvalidIdentifierError,
|
102
|
+
"Cannot use identifier #{identifier}, a method on #{self.class} is already using it."
|
103
|
+
)
|
104
|
+
end
|
96
105
|
|
97
|
-
|
98
|
-
raise TypeError.new("Expected port to be one of: :#{PORTS.join(', :')}")
|
106
|
+
raise(PortTakenError, "Port #{port} is already set, call remove first")
|
99
107
|
end
|
100
108
|
|
101
|
-
|
102
|
-
|
103
|
-
if !self.respond_to?(identifier)
|
104
|
-
# Makes a new instance of the class and pushes it into our instance variable
|
105
|
-
# for the given port.
|
106
|
-
self.instance_variable_set(port_variable, klass.new(port, self.interface))
|
107
|
-
|
108
|
-
# Given that that succeeded, all that remains is to add the identifier
|
109
|
-
# to our lookup Hash. We'll use this Hash later on within method_missing.
|
110
|
-
@port_identifiers[identifier] = port
|
111
|
-
|
112
|
-
# Define a method on the eigenclass of this instance.
|
113
|
-
(class << self; self; end).send(:define_method, identifier) do
|
114
|
-
self.instance_variable_get(port_variable)
|
115
|
-
end
|
116
|
-
else
|
117
|
-
if !self.instance_variable_get(port_variable).nil?
|
118
|
-
raise PortTakenError.new("Port #{port} is already set, call remove first")
|
119
|
-
else
|
120
|
-
raise InvalidIdentifierError.new("Cannot use identifier #{identifier}, a method on #{self.class} is already using it.")
|
121
|
-
end
|
122
|
-
end
|
109
|
+
define_port_handler_method(port, identifier, klass)
|
123
110
|
end
|
124
111
|
|
125
112
|
# Remove the assigned (if any) connector instance from the given
|
126
|
-
# identifier.
|
113
|
+
# identifier.interface
|
127
114
|
#
|
128
115
|
# @param Symbol identifier The identifier to search for and remove.
|
129
116
|
def remove(identifier)
|
130
|
-
|
131
|
-
|
117
|
+
assert_responds_to('identifier', identifier, :to_sym)
|
118
|
+
!@port_identifiers.delete(identifier.to_sym).nil?
|
132
119
|
end
|
133
120
|
|
134
121
|
# This will dynamically add methods like:
|
@@ -146,8 +133,27 @@ class NXTBrick
|
|
146
133
|
# people don't pass in the correct number of params, it says helpfully
|
147
134
|
# '1 of 2' args passed (or something similar).
|
148
135
|
define_method("add_#{const.to_s.underscore}_#{type_const.to_s.underscore}") do |port, identifier|
|
149
|
-
|
136
|
+
add(port, identifier, NXT::Connector.const_get(type_const).const_get(const))
|
150
137
|
end
|
151
138
|
end
|
152
139
|
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def define_port_handler_method(port, identifier, klass)
|
144
|
+
port_variable = :"@#{port}"
|
145
|
+
|
146
|
+
# Makes a new instance of the class and pushes it into our instance variable
|
147
|
+
# for the given port.
|
148
|
+
instance_variable_set(port_variable, klass.new(port, interface))
|
149
|
+
|
150
|
+
# Given that that succeeded, all that remains is to add the identifier
|
151
|
+
# to our lookup Hash. We'll use this Hash later on within method_missing.
|
152
|
+
@port_identifiers[identifier.to_sym] = port
|
153
|
+
|
154
|
+
# Define a method on the eigenclass of this instance.
|
155
|
+
(class << self; self; end).send(:define_method, identifier.to_sym) do
|
156
|
+
instance_variable_get(port_variable)
|
157
|
+
end
|
158
|
+
end
|
153
159
|
end
|
data/lib/nxt/patches/module.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Some patches that extend the default Ruby Module with some useful methods.
|
1
4
|
class Module
|
2
5
|
# Creates an invariant accessor that allows getting and setting from the same
|
3
6
|
# endpoint. It will operate in getter mode if you don't pass any arguments
|
@@ -8,10 +11,10 @@ class Module
|
|
8
11
|
define_method(sym) do |*args|
|
9
12
|
if args.empty?
|
10
13
|
instance_var = :"@#{sym}"
|
11
|
-
if (value =
|
14
|
+
if (value = instance_variable_get(instance_var))
|
12
15
|
value
|
13
16
|
else
|
14
|
-
|
17
|
+
instance_variable_set(instance_var, default)
|
15
18
|
default
|
16
19
|
end
|
17
20
|
else
|