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