lego-nxt 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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