lego-nxt 0.2.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.
@@ -0,0 +1,11 @@
1
+ module NXT
2
+ module Connector
3
+ module Input
4
+ class Ultrasonic
5
+ def initialize(port)
6
+ @port = port
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,116 @@
1
+ module NXT
2
+ module Connector
3
+ module Output
4
+ class Motor
5
+ include NXT::Command::Output
6
+ extend NXT::Utils::Accessors
7
+
8
+ DURATION_TYPE = [:seconds, :degrees, :rotations].freeze
9
+ DURATION_AFTER = [:coast, :brake].freeze
10
+ DIRECTION = [:forwards, :backwards].freeze
11
+
12
+ attr_accessor :port, :interface
13
+
14
+ attr_combined_accessor :duration, 0
15
+ attr_combined_accessor :duration_type, :seconds
16
+ attr_combined_accessor :duration_after, :stop
17
+ attr_combined_accessor :direction, :forwards
18
+
19
+ attr_setter :direction, is_key_in: DIRECTION
20
+
21
+ def initialize(port, interface)
22
+ @port = port
23
+ @interface = interface
24
+ end
25
+
26
+ def duration=(duration, options = {})
27
+ raise TypeError.new('Expected duration to be a number') unless duration.is_a?(Integer)
28
+ @duration = duration
29
+
30
+ if options.include?(:type)
31
+ type = options[:type]
32
+
33
+ unless DURATION_TYPE.include?(type)
34
+ raise TypeError.new("Expected duration type to be one of: :#{DURATION_TYPE.join(', :')}")
35
+ end
36
+
37
+ @duration_type = type
38
+ else
39
+ @duration_type = :seconds
40
+ end
41
+
42
+ if options.include?(:after)
43
+ if @duration_type == :seconds
44
+ after = options[:after]
45
+
46
+ unless DURATION_AFTER.include?(after)
47
+ raise TypeError.new("Expected after option to be one of: :#{DURATION_AFTER.join(', :')}")
48
+ end
49
+
50
+ @duration_after = after
51
+ else
52
+ raise TypeError.new('The after option is only available when the unit duration is in seconds.')
53
+ end
54
+ else
55
+ @duration_after = :stop
56
+ end
57
+
58
+ case @duration_type
59
+ when :rotations
60
+ self.tacho_limit = @duration * 360
61
+ when :degrees
62
+ self.tacho_limit = @duration
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ def forwards
69
+ self.direction = :forwards
70
+ self
71
+ end
72
+
73
+ def backwards
74
+ self.direction = :backwards
75
+ self
76
+ end
77
+
78
+ def stop(type = :coast)
79
+ self.power = 0
80
+ self.mode = :coast
81
+
82
+ self.move
83
+ end
84
+
85
+ # takes block for response, or can return the response instead.
86
+ def move
87
+ response_required = false
88
+
89
+ if self.duration > 0 && self.duration_type != :seconds
90
+ response_required = true
91
+ end
92
+
93
+ set_output_state(response_required)
94
+
95
+ if self.duration > 0 && self.duration_type == :seconds
96
+ sleep(self.duration)
97
+ self.reset
98
+ self.stop(self.duration_after)
99
+ else
100
+ self.reset
101
+ end
102
+ end
103
+
104
+ def reset
105
+ self.duration = 0
106
+ self.direction = :forwards
107
+ self.power = 75
108
+ self.mode = :motor_on
109
+ self.regulation_mode = :idle
110
+ self.run_state = :running
111
+ self.tacho_limit = 0
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,26 @@
1
+ module NXT
2
+ module Exceptions
3
+ # Raised when a class has not implemented a method from the base class
4
+ # that is required to be overriden.
5
+ class InterfaceNotImplemented < NotImplementedError; end
6
+
7
+ # Raised when an invalid interface is attempted to be constructed.
8
+ class InvalidInterfaceError < TypeError; end
9
+
10
+ # Raised when a port is attempted to be named when it already has been.
11
+ class PortTakenError < TypeError; end
12
+
13
+ # Raised when an invalid name is attempted to be given to a port.
14
+ class InvalidIdentifierError < TypeError; end
15
+
16
+ # Raised when the device file attempted to be used for communication is
17
+ # for whatever reason does not exist or is not correct.
18
+ class InvalidDeviceError < TypeError; end
19
+
20
+ # Raised when communication with a Serial Port connection fails.
21
+ class SerialPortConnectionError < RuntimeError; end
22
+
23
+ # Raised when communication with a USB Port connection fails.
24
+ class UsbConnectionError < RuntimeError; end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module NXT
2
+ module Interface
3
+ class Base
4
+ def send_and_receive(msg, response_required = true)
5
+ unless response_required
6
+ msg[0] = msg[0] | 0x80
7
+ end
8
+
9
+ self.send(msg)
10
+
11
+ if response_required
12
+ response = self.receive
13
+ response
14
+ end
15
+ end
16
+
17
+ def send
18
+ raise InterfaceNotImplemented.new('The #send method must be implemented.')
19
+ end
20
+
21
+ def receive
22
+ raise InterfaceNotImplemented.new('The #receive method must be implemented.')
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,76 @@
1
+ require 'serialport'
2
+
3
+ module NXT
4
+ module Interface
5
+ class SerialPort < Base
6
+ include NXT::Exceptions
7
+
8
+ attr_reader :dev
9
+
10
+ BAUD_RATE = 57600
11
+ DATA_BITS = 8
12
+ STOP_BITS = 1
13
+ PARITY = ::SerialPort::NONE
14
+ READ_TIMEOUT = 5000
15
+
16
+ def initialize(dev)
17
+ self.dev = (dev)
18
+ end
19
+
20
+ def dev=(dev)
21
+ raise InvalidDeviceError unless File.exists?(dev)
22
+ @dev = dev
23
+ end
24
+
25
+ def connect
26
+ @connection = ::SerialPort.new(@dev, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY)
27
+
28
+ if @connection.nil?
29
+ raise SerialPortConnectionError.new("Could not establish a SerialPort connection to #{dev}")
30
+ end
31
+
32
+ @connection.flow_control = ::SerialPort::HARD
33
+ @connection.read_timeout = READ_TIMEOUT
34
+
35
+ @connection
36
+ rescue ArgumentError
37
+ raise SerialPortConnectionError.new("The #{dev} device is not a valid SerialPort")
38
+ end
39
+
40
+ def disconnect
41
+ @connection.close if self.connected?
42
+ end
43
+
44
+ def connected?
45
+ @connection && !@connection.closed?
46
+ end
47
+
48
+ def send(msg)
49
+ # The expected data package structure for NXT Bluetooth communication is:
50
+ #
51
+ # [Length Byte 1, Length Byte 2, Command Type, Command, ...]
52
+ #
53
+ # So here we calculate the two leading length bytes, and rely on the
54
+ # passed in argument to give us the rest of the message to send.
55
+ #
56
+ # Note that the length is stored in Little Endian ie. LSB -> MSB
57
+ #
58
+ # Reference: Appendix 1, Page 22
59
+ msg = [(msg.length & 255), (msg.length >> 8)] + msg
60
+
61
+ msg.each do |b|
62
+ @connection.putc(b)
63
+ end
64
+ end
65
+
66
+ def receive
67
+ # This gets the length of the received data from the header that was sent
68
+ # to us. We unpack it, as it's stored as a 16-bit Little Endian number.
69
+ #
70
+ # Reference: Appendix 1, Page 22
71
+ length = @connection.sysread(2)
72
+ @connection.sysread(length.unpack('v')[0])
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,51 @@
1
+ require 'libusb'
2
+
3
+ module NXT
4
+ module Interface
5
+ class Usb < Base
6
+ include NXT::Exceptions
7
+
8
+ ID_VENDOR_LEGO = 0x0694
9
+ ID_PRODUCT_NXT = 0x0002
10
+ OUT_ENDPOINT = 0x01
11
+ IN_ENDPOINT = 0x82
12
+ TIMEOUT = 10000
13
+ READSIZE = 64
14
+ INTERFACE = 0
15
+
16
+ def connect
17
+ @usb_context = LIBUSB::Context.new
18
+ @dev = @usb_context.devices(idVendor: ID_VENDOR_LEGO, idProduct: ID_PRODUCT_NXT).first
19
+
20
+ if @dev.nil?
21
+ raise UsbConnectionError.new("Could not find NXT attached as USB device")
22
+ end
23
+
24
+ @connection = @dev.open
25
+ @connection.claim_interface(INTERFACE)
26
+
27
+ @connection
28
+ end
29
+
30
+ def disconnect
31
+ if self.connected?
32
+ @connection.release_interface(INTERFACE)
33
+ @connection.close
34
+ end
35
+ end
36
+
37
+ def connected?
38
+ # FIXME: How do we check if the device is connected?
39
+ @connection
40
+ end
41
+
42
+ def send(msg)
43
+ @connection.bulk_transfer(endpoint: OUT_ENDPOINT, dataOut: msg.pack('C*'), timeout: TIMEOUT)
44
+ end
45
+
46
+ def receive
47
+ @connection.bulk_transfer(endpoint: IN_ENDPOINT, dataIn: READSIZE, timeout: TIMEOUT)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,153 @@
1
+ # This class is the entry point for end-users creating their own list of
2
+ # commands to execute remotely on a Lego NXT brick.
3
+ #
4
+ # An instance of this class provides all the endpoints necessary to:
5
+ #
6
+ # * educate the API on the connected input and output devices; and,
7
+ # * access these input and output devices and run commands using them.
8
+ #
9
+ # @example Creating an instance using a block, with one motor output.
10
+ #
11
+ # NXTBrick.new(interface) do |nxt|
12
+ # nxt.add_motor_output(:a, :front_left)
13
+ # end
14
+ #
15
+ # @example Creating an instance without a block, with one motor and one light sensor.
16
+ #
17
+ # nxt = NXTBrick.new(interface)
18
+ # nxt.add_motor_output(:a, :front_left)
19
+ # # ...
20
+ # nxt.disconnect
21
+ class NXTBrick
22
+ include NXT::Exceptions
23
+
24
+ # An enumeration of possible ports, both input and output, that the NXT brick
25
+ # can have connectors attached to.
26
+ PORTS = [:a, :b, :c, :one, :two, :three, :four]
27
+
28
+ # Get the instance of the interface that this runner class is using to connect
29
+ # to the NXT brick.
30
+ attr_accessor :interface
31
+
32
+ # Accessors for output ports on the NXT brick. These will be populated with
33
+ # the appropriate instances of their respective output connectors.
34
+ attr_reader :a, :b, :c
35
+
36
+ # Accessors for input ports on the NXT brick. These will be populated with the
37
+ # appropriate instances of their respective input connectors.
38
+ attr_reader :one, :two, :three, :four
39
+
40
+ # We mandate that all added port connections have an identifier associated
41
+ # with it. This is so that code is not fragile when port swapping needs to
42
+ # be done.
43
+ attr_reader :port_identifiers
44
+
45
+ def initialize(interface_type, *interface_args)
46
+ @port_identifiers = {}
47
+ interface_type = interface_type.to_s.classify
48
+
49
+ unless NXT::Interface.constants.include?(interface_type.to_sym)
50
+ raise InvalidInterfaceError.new("There is no interface of type #{interface_type}.")
51
+ end
52
+
53
+ self.interface = NXT::Interface.const_get(interface_type).new(*interface_args)
54
+
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
64
+ end
65
+ end
66
+
67
+ # Connect using the given interface to the NXT brick.
68
+ def connect
69
+ self.interface.connect
70
+ end
71
+
72
+ # Close the connection to the NXT brick, and dispose of any resources that
73
+ # this instance of NXTBrick is using. Any commands run against this runner
74
+ # after calling disconnect will fail.
75
+ def disconnect
76
+ self.interface.disconnect
77
+ end
78
+
79
+ # Add a new connector instance, binding a specific identifier to the given
80
+ # port.
81
+ #
82
+ # If the given port already is bound, an exception will be thrown. The
83
+ # instance given though can be of any class, presuming it talks the
84
+ # correct language.
85
+ #
86
+ # @param Symbol port The port to bind to.
87
+ # @param Symbol identifier The identifier to associate with this port.
88
+ # @param Class klass The Class to instantiate as the instance of this
89
+ # port. There is no limitation on what type this can
90
+ # be, though it must be able to hook in correctly
91
+ # with the NXT library.
92
+ 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)
96
+
97
+ unless PORTS.include?(port)
98
+ raise TypeError.new("Expected port to be one of: :#{PORTS.join(', :')}")
99
+ end
100
+
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
123
+ end
124
+
125
+ # Remove the assigned (if any) connector instance from the given
126
+ # identifier.
127
+ #
128
+ # @param Symbol identifier The identifier to search for and remove.
129
+ def remove(identifier)
130
+ raise TypeError.new('Expected identifier to be a Symbol') unless identifier.is_a?(Symbol)
131
+ !!@port_identifiers.delete(identifier)
132
+ end
133
+
134
+ # This will dynamically add methods like:
135
+ #
136
+ # * add_light_input
137
+ # * add_motor_output
138
+ # * add_ultrasonic_input
139
+ #
140
+ # This means they don't have to provide the class each and every time. For
141
+ # connectors they have added themselves, it's likely best to use the
142
+ # {#add} method.
143
+ NXT::Connector.constants.each do |type_const|
144
+ NXT::Connector.const_get(type_const).constants.each do |const|
145
+ # We don't use a splat here for the args, because that way when
146
+ # people don't pass in the correct number of params, it says helpfully
147
+ # '1 of 2' args passed (or something similar).
148
+ 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))
150
+ end
151
+ end
152
+ end
153
+ end