lego-nxt 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +16 -12
  3. data/Rakefile +9 -12
  4. data/lib/lego_nxt.rb +36 -0
  5. data/lib/nxt/commands/base.rb +56 -7
  6. data/lib/nxt/commands/input.rb +30 -12
  7. data/lib/nxt/commands/low_speed.rb +47 -0
  8. data/lib/nxt/commands/output.rb +9 -7
  9. data/lib/nxt/commands/program.rb +3 -0
  10. data/lib/nxt/commands/sound.rb +3 -0
  11. data/lib/nxt/commands/tone.rb +3 -0
  12. data/lib/nxt/connector/input/base.rb +9 -0
  13. data/lib/nxt/{connectors → connector}/input/color.rb +3 -0
  14. data/lib/nxt/{connectors → connector}/input/touch.rb +3 -0
  15. data/lib/nxt/connector/input/ultrasonic.rb +66 -0
  16. data/lib/nxt/connector/output/base.rb +9 -0
  17. data/lib/nxt/connector/output/motor.rb +117 -0
  18. data/lib/nxt/exceptions.rb +3 -1
  19. data/lib/nxt/interface/base.rb +18 -0
  20. data/lib/nxt/{interfaces → interface}/serial_port.rb +13 -10
  21. data/lib/nxt/{interfaces → interface}/usb.rb +10 -9
  22. data/lib/nxt/nxt_brick.rb +51 -45
  23. data/lib/nxt/patches/module.rb +5 -2
  24. data/lib/nxt/patches/string.rb +6 -17
  25. data/lib/nxt/protocols/i2c.rb +118 -0
  26. data/lib/nxt/utils/accessors.rb +8 -7
  27. data/lib/nxt/utils/assertions.rb +24 -0
  28. data/spec/matchers.rb +2 -0
  29. data/spec/nxt/connector/output/motor_spec.rb +52 -0
  30. data/spec/nxt/interface/serial_port_spec.rb +119 -0
  31. data/spec/nxt/nxt_brick_spec.rb +189 -120
  32. data/spec/spec_helper.rb +10 -1
  33. metadata +193 -59
  34. data/lib/nxt.rb +0 -27
  35. data/lib/nxt/connectors/input/ultrasonic.rb +0 -11
  36. data/lib/nxt/connectors/output/motor.rb +0 -116
  37. data/lib/nxt/interfaces/base.rb +0 -26
  38. data/spec/nxt/interfaces/serial_port_spec.rb +0 -73
@@ -0,0 +1,9 @@
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
+ end
8
+ end
9
+ end
@@ -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
@@ -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 < NotImplementedError; end
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 = 57600
13
+ BAUD_RATE = 57_600
11
14
  DATA_BITS = 8
12
15
  STOP_BITS = 1
13
16
  PARITY = ::SerialPort::NONE
14
- READ_TIMEOUT = 5000
17
+ READ_TIMEOUT = 5_000
15
18
 
16
19
  def initialize(dev)
17
- self.dev = (dev)
20
+ super()
21
+ self.dev = dev
18
22
  end
19
23
 
20
24
  def dev=(dev)
21
- raise InvalidDeviceError unless File.exists?(dev)
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.new("The #{dev} device is not a valid SerialPort")
40
+ raise SerialPortConnectionError, "The #{dev} device is not a valid SerialPort"
38
41
  end
39
42
 
40
43
  def disconnect
41
- @connection.close if self.connected?
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.unpack('v')[0])
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 = 10000
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
- if self.connected?
32
- @connection.release_interface(INTERFACE)
33
- @connection.close
34
- end
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
@@ -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 = [:a, :b, :c, :one, :two, :three, :four]
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 InvalidInterfaceError.new("There is no interface of type #{interface_type}.")
53
+ raise(InvalidInterfaceError, "There is no interface of type #{interface_type}.")
51
54
  end
52
55
 
53
- self.interface = NXT::Interface.const_get(interface_type).new(*interface_args)
56
+ @interface = NXT::Interface.const_get(interface_type).new(*interface_args)
54
57
 
55
- if block_given?
56
- begin
57
- self.connect
58
- yield(self)
59
- rescue Exception => e
60
- binding.pry
61
- ensure
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
- self.interface.connect
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
- self.interface.disconnect
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
- raise TypeError.new('Expected port to be a Symbol') unless port.is_a?(Symbol)
94
- raise TypeError.new('Expected identifier to be a Symbol') unless identifier.is_a?(Symbol)
95
- raise TypeError.new('Expected klass to be a Class') unless klass.is_a?(Class)
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
- unless PORTS.include?(port)
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
- port_variable = :"@#{port}"
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
- raise TypeError.new('Expected identifier to be a Symbol') unless identifier.is_a?(Symbol)
131
- !!@port_identifiers.delete(identifier)
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
- self.add(port, identifier, NXT::Connector.const_get(type_const).const_get(const))
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
@@ -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 = self.instance_variable_get(instance_var))
14
+ if (value = instance_variable_get(instance_var))
12
15
  value
13
16
  else
14
- self.instance_variable_set(instance_var, default)
17
+ instance_variable_set(instance_var, default)
15
18
  default
16
19
  end
17
20
  else