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,52 @@
1
+ # NXT
2
+
3
+ Control a Lego NXT 2.0 brick using Ruby code. This library works by piping
4
+ commands over a serialport connection to the brick, allowing you to write Ruby
5
+ scripts to control your bot. This means you can use both the Bluetooth and USB
6
+ serial ports provided on the brick as interfaces within your code.
7
+
8
+ This project used to be based on "ruby-nxt", and Tony Buser's subsequent rewrite
9
+ "nxt". It is now a complete rewrite, based heavily in some parts on the
10
+ aforesaid projects internally, but with a brand new external API that should
11
+ prove cleaner and easier to work with.
12
+
13
+ This code implements direct command, as outlined in "Appendix 2-LEGO MINDSTORMS
14
+ NXT Direct Commands.pdf". Not all functionality is implemented yet!
15
+
16
+ ## Getting Started
17
+
18
+ ### Connect to Your NXT Brick
19
+
20
+ In order to start coding with your NXT, you'll need to set up either a
21
+ Bluetooth or USB connection to it. Follow one of the below set of steps, and
22
+ make a note of the `/dev/*` address you end up using to point to the NXT.
23
+
24
+ ### Creating a Bluetooth Serialport Connection
25
+
26
+ For instructions on creating a bluetooth serialport connection:
27
+
28
+ * Linux: http://tonybuser.com/bluetooth-serial-port-to-nxt-in-linux
29
+ * Max OSX: http://tonybuser.com/bluetooth-serial-port-to-nxt-in-osx
30
+ * Windows: http://tonybuser.com/ruby-serialportnxt-on-windows
31
+
32
+ ### Creating a USB Serialport Connection
33
+
34
+ TODO
35
+
36
+ Once you have your NXT Connected
37
+
38
+ ## Documentation and Examples
39
+
40
+ The NXT project has been heavily documented using nice, clean, human readable
41
+ markdown. YARD is used to generated the docs, and the options have been included
42
+ in our .yardopts file, so simply run a YARD server to read them:
43
+
44
+ yard server
45
+
46
+ This documents the API, both internal and external. For bite-sized chunks of NXT
47
+ code that is much more appropriate for beginners,
48
+ [have a look at the examples](https://github.com/nathankleyn/nxt/tree/master/examples).
49
+
50
+ In addition to this, you might find the tests quite helpful. There are currently
51
+ only RSpec unit tests, which can be found in the `spec` directory; the plan is
52
+ to add some decent feature tests soon.
@@ -0,0 +1,35 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rdoc/task'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RDoc::Task.new do |rdoc|
7
+ files = ['README.markdown', 'lib/**/*.rb']
8
+ rdoc.rdoc_files.add(files)
9
+ rdoc.main = 'README.markdown'
10
+ rdoc.title = 'ruby_events Docs'
11
+ rdoc.rdoc_dir = 'doc/rdoc'
12
+ rdoc.options << '--line-numbers'
13
+ end
14
+
15
+ RSpec::Core::RakeTask.new(:spec)
16
+
17
+ desc 'NXT related tasks'
18
+ namespace :nxt do
19
+ desc 'Detect a connected NXT brick within /dev.'
20
+ task :detect do
21
+ unless $DEV ||= ENV['NXT'] || ENV['DEV']
22
+ begin
23
+ devices = Dir['/dev/*NXT*']
24
+ if devices.size > 0
25
+ $DEV = devices[0]
26
+ puts "Detected a NXT brick at '#{$DEV}'."
27
+ else
28
+ puts 'Could not detect any connected NXT bricks.'
29
+ end
30
+ rescue
31
+ # FIXME: The /dev directory isn't there, possibly running on Windows.
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'active_support/inflector'
4
+
5
+ require 'nxt/utils/accessors'
6
+
7
+ require 'nxt/patches/module'
8
+
9
+ require 'nxt/exceptions'
10
+
11
+ require 'nxt/interfaces/base'
12
+ require 'nxt/interfaces/usb'
13
+ require 'nxt/interfaces/serial_port'
14
+
15
+ require 'nxt/commands/base'
16
+ require 'nxt/commands/input'
17
+ require 'nxt/commands/output'
18
+ require 'nxt/commands/program'
19
+ require 'nxt/commands/sound'
20
+ require 'nxt/commands/tone'
21
+
22
+ require 'nxt/connectors/input/color'
23
+ require 'nxt/connectors/input/touch'
24
+ require 'nxt/connectors/input/ultrasonic'
25
+ require 'nxt/connectors/output/motor'
26
+
27
+ require 'nxt/nxt_brick'
@@ -0,0 +1,28 @@
1
+ module NXT
2
+ module Command
3
+ module Base
4
+ private
5
+
6
+ COMMAND_TYPES = {
7
+ direct: 0x00,
8
+ system: 0x01,
9
+ reply: 0x02
10
+ }.freeze
11
+
12
+ PORTS = {
13
+ a: 0x00,
14
+ b: 0x01,
15
+ c: 0x02,
16
+ one: 0x00,
17
+ two: 0x01,
18
+ three: 0x02,
19
+ four: 0x03,
20
+ all: 0xFF
21
+ }
22
+
23
+ def port_as_byte(port)
24
+ PORTS[port]
25
+ end
26
+ end
27
+ end
28
+ end
File without changes
@@ -0,0 +1,105 @@
1
+ module NXT
2
+ module Command
3
+ # An implementation of all the output related NXT commands:
4
+ #
5
+ # * setoutputstate
6
+ # * getoutputstate
7
+ #
8
+ # This is used predominantly to interface with the servo-motor connectors
9
+ # that come prepackaged with NXT kits.
10
+ #
11
+ # This class can also be used to talk to other third-party accessories
12
+ # connected in the output ports on the NXT brick.
13
+ #
14
+ # This class does not actually talk to the chosen interface for the NXT
15
+ # brick. Instead, it outputs messages in byte arrays ready to be serialised
16
+ # to the brick over the appropriate interface from within the {NXT::Brick}
17
+ # class.
18
+ module Output
19
+ include NXT::Command::Base
20
+ extend NXT::Utils::Accessors
21
+
22
+ @@command_type = COMMAND_TYPES[:direct]
23
+ @@command_identifier = 0x04
24
+
25
+ # The mode enum. This is a list of possible values when setting the mode
26
+ # byte.
27
+ #
28
+ # Reference: Appendix 2, Page 6
29
+ MODE = {
30
+ # Motor will rotate freely.
31
+ # NOTE: This is not documented in the Appendixes.
32
+ coast: 0x00,
33
+ # Turn on the specified motor.
34
+ motor_on: 0x01,
35
+ # Use run/brake instead of run/float in PWM. This means the voltage is
36
+ # not allowed to float between PWM pulses, improving accuracy at the
37
+ # expense of greater power usage.
38
+ brake: 0x02,
39
+ # Turns on the regulation. This is required when setting a regulation
40
+ # mode setting.
41
+ regulated: 0x04
42
+ }.freeze
43
+
44
+ # The regulation mode enum. This is a list of possible values when
45
+ # setting the regulation mode byte.
46
+ #
47
+ # Reference: Appendix 2, Page 6
48
+ REGULATION_MODE = {
49
+ # No regulation will be enabled.
50
+ idle: 0x00,
51
+ # Power control will be enabled on specific output.
52
+ motor_speed: 0x01,
53
+ # Synchronisation will be enabled. This requires two output ports to
54
+ # have this enabled before it will work.
55
+ motor_sync: 0x02
56
+ }.freeze
57
+
58
+ # The run state enum. This is a list of possible values when setting the
59
+ # run state byte.
60
+ #
61
+ # Reference: Appendix 2, Page 6
62
+ RUN_STATE = {
63
+ # Output will be idle.
64
+ idle: 0x00,
65
+ # Output will ramp-up to the desired speed.
66
+ ramp_up: 0x10,
67
+ # Output will be running.
68
+ running: 0x20,
69
+ # Output will ramp-down to the desired speed.
70
+ ramp_down: 0x40
71
+ }.freeze
72
+
73
+ attr_combined_accessor :power, 75
74
+ attr_combined_accessor :mode, :motor_on
75
+ attr_combined_accessor :regulation_mode, :idle
76
+ attr_combined_accessor :run_state, :running
77
+ attr_combined_accessor :tacho_limit, 0
78
+
79
+ attr_setter :power, is: Integer
80
+ attr_setter :mode, is_key_in: MODE
81
+ attr_setter :regulation_mode, is_key_in: REGULATION_MODE
82
+ attr_setter :run_state, is_key_in: RUN_STATE
83
+ attr_setter :tacho_limit, is: Integer
84
+
85
+ def set_output_state(response_required = false)
86
+ # Pack this value into a 32-bit unsigned little-endian binary string,
87
+ # then unpack it into 4 8 bit unsigned integer chunks. We are
88
+ # converting the passed in value to a little endian, unsigned long
89
+ # value.
90
+ tacho_limit_as_bytes = [self.tacho_limit].pack('V').unpack('C4')
91
+
92
+ @interface.send_and_receive([
93
+ @@command_type,
94
+ @@command_identifier,
95
+ port_as_byte(self.port),
96
+ self.power,
97
+ MODE[self.mode],
98
+ REGULATION_MODE[self.regulation_mode],
99
+ 0, # turn ratio
100
+ RUN_STATE[self.run_state]
101
+ ] + tacho_limit_as_bytes, response_required)
102
+ end
103
+ end
104
+ end
105
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,11 @@
1
+ module NXT
2
+ module Connector
3
+ module Input
4
+ class Color
5
+ def initialize(port)
6
+ @port = port
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module NXT
2
+ module Connector
3
+ module Input
4
+ class Touch
5
+ def initialize(port)
6
+ @port = port
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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,126 @@
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
+
115
+ # overrides
116
+ def set_output_state(response_required=true)
117
+ original_power = self.power
118
+ self.power *= -1 if self.direction == :backwards
119
+
120
+ super
121
+
122
+ self.power = original_power
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,23 @@
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
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module NXT
2
+ module Interface
3
+ class Base
4
+ def send_and_receive(msg, response_required = true)
5
+ if response_required
6
+ msg[0] = msg[0] | (response_required ? 0x80 : 0x00)
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
+
25
+ def connect
26
+ raise InterfaceNotImplemented.new('The #connect method must be implemented')
27
+ end
28
+
29
+ def disconnect
30
+ raise InterfaceNotImplemented.new('The #disconnect method must be implemented')
31
+ end
32
+ end
33
+ end
34
+ 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
+ @connection.flow_control = ::SerialPort::HARD
30
+ @connection.read_timeout = READ_TIMEOUT
31
+ else
32
+ raise SerialPortConnectionError.new("Could not establish a SerialPort connection to #{dev}")
33
+ end
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 @connection && !@connection.closed?
42
+ end
43
+
44
+ def connected?
45
+ !@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,8 @@
1
+ require 'usb'
2
+
3
+ module NXT
4
+ module Interface
5
+ class USB < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,155 @@
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
+ validate_interface(interface_type)
50
+ self.interface = NXT::Interface.const_get(interface_type).new(*interface_args)
51
+
52
+ if block_given?
53
+ begin
54
+ self.connect
55
+ yield(self)
56
+ ensure
57
+ self.disconnect
58
+ end
59
+ end
60
+ end
61
+
62
+ # Connect using the given interface to the NXT brick.
63
+ def connect
64
+ self.interface.connect
65
+ end
66
+
67
+ # Close the connection to the NXT brick, and dispose of any resources that
68
+ # this instance of NXTBrick is using. Any commands run against this runner
69
+ # after calling disconnect will fail.
70
+ def disconnect
71
+ self.interface.disconnect
72
+ end
73
+
74
+ # Add a new connector instance, binding a specific identifier to the given
75
+ # port.
76
+ #
77
+ # If the given port already is bound, an exception will be thrown. The
78
+ # instance given though can be of any class, presuming it talks the
79
+ # correct language.
80
+ #
81
+ # @param Symbol port The port to bind to.
82
+ # @param Symbol identifier The identifier to associate with this port.
83
+ # @param Class klass The Class to instantiate as the instance of this
84
+ # port. There is no limitation on what type this can
85
+ # be, though it must be able to hook in correctly
86
+ # with the NXT library.
87
+ def add(port, identifier, klass)
88
+ raise TypeError.new('Expected port to be a Symbol') unless port.is_a?(Symbol)
89
+ raise TypeError.new('Expected identifier to be a Symbol') unless identifier.is_a?(Symbol)
90
+ raise TypeError.new('Expected klass to be a Class') unless klass.is_a?(Class)
91
+
92
+ unless PORTS.include?(port)
93
+ raise TypeError.new("Expected port to be one of: :#{PORTS.join(', :')}")
94
+ end
95
+
96
+ port_variable = :"@#{port}"
97
+
98
+ if !self.respond_to?(identifier)
99
+ # Makes a new instance of the class and pushes it into our instance variable
100
+ # for the given port.
101
+ self.instance_variable_set(port_variable, klass.new(port, self.interface))
102
+
103
+ # Given that that succeeded, all that remains is to add the identifier
104
+ # to our lookup Hash. We'll use this Hash later on within method_missing.
105
+ @port_identifiers[identifier] = port
106
+
107
+ # Define a method on the eigenclass of this instance.
108
+ (class << self; self; end).send(:define_method, identifier) do
109
+ self.instance_variable_get(port_variable)
110
+ end
111
+ else
112
+ if !self.instance_variable_get(port_variable).nil?
113
+ raise PortTakenError.new("Port #{port} is already set, call remove first")
114
+ else
115
+ raise InvalidIdentifierError.new("Cannot use identifier #{identifier}, a method on #{self.class} is already using it.")
116
+ end
117
+ end
118
+ end
119
+
120
+ # Remove the assigned (if any) connector instance from the given
121
+ # identifier.
122
+ #
123
+ # @param Symbol identifier The identifier to search for and remove.
124
+ def remove(identifier)
125
+ raise TypeError.new('Expected identifier to be a Symbol') unless identifier.is_a?(Symbol)
126
+ !!@port_identifiers.delete(identifier)
127
+ end
128
+
129
+ # This will dynamically add methods like:
130
+ #
131
+ # * add_light_input
132
+ # * add_motor_output
133
+ # * add_ultrasonic_input
134
+ #
135
+ # This means they don't have to provide the class each and every time. For
136
+ # connectors they have added themselves, it's likely best to use the
137
+ # {#add} method.
138
+ NXT::Connector.constants.each do |type_const|
139
+ NXT::Connector.const_get(type_const).constants.each do |const|
140
+ # We don't use a splat here for the args, because that way when
141
+ # people don't pass in the correct number of params, it says helpfully
142
+ # '1 of 2' args passed (or something similar).
143
+ define_method("add_#{const.to_s.underscore}_#{type_const.to_s.underscore}") do |port, identifier|
144
+ self.add(port, identifier, NXT::Connector.const_get(type_const).const_get(const))
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+ def validate_interface(interface_type_name_string)
151
+ unless NXT::Interface.constants.include?(interface_type_name_string.to_sym)
152
+ raise InvalidInterfaceError.new("There is no interface of type #{interface_type_name_string}.")
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,22 @@
1
+ class Module
2
+ # Creates an invariant accessor that allows getting and setting from the same
3
+ # endpoint. It will operate in getter mode if you don't pass any arguments
4
+ # when calling it, otherwise it will work in setter mode. Useful when needing
5
+ # to chain methods (you can't chain standard attr_writer methods because
6
+ # of the `= something` part).
7
+ def attr_combined_accessor(sym, default = nil)
8
+ define_method(sym) do |*args|
9
+ if args.empty?
10
+ instance_var = :"@#{sym}"
11
+ if (value = self.instance_variable_get(instance_var))
12
+ value
13
+ else
14
+ self.instance_variable_set(instance_var, default)
15
+ default
16
+ end
17
+ else
18
+ send(:"#{sym}=", *args)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ class String
2
+ # Convert the given string to a hexadecimal representation of the same data.
3
+ # This method is non-destructive, it will return a new copy of the string
4
+ # convered to hex.
5
+ def to_hex_str
6
+ str = ''
7
+ self.each_byte {|b| str << '0x%02x ' % b}
8
+ str
9
+ end
10
+
11
+ #
12
+ def from_hex_str_two
13
+ data = self.split(' ')
14
+ str = ''
15
+ data.each {|h| eval "str += '%c' % #{h}"}
16
+ str
17
+ end
18
+
19
+ def from_hex_str
20
+ data = self.split(' ')
21
+ str = ''
22
+
23
+ data.each do |h|
24
+ str += '%c' % h
25
+ end
26
+
27
+ str
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ module NXT
2
+ module Utils
3
+ module Accessors
4
+ def attr_setter(name, options)
5
+ define_method("#{name}=") do |value|
6
+ if options.include?(:is)
7
+ raise TypeError.new('Expected value to be a number') unless duration.is_a?(options[:is])
8
+ end
9
+
10
+ if options.include?(:is_key_in) && !options[:is_key_in].include?(value)
11
+ raise TypeError.new("Expected value to be one of: :#{options[:is_key_in].keys.join(', :')}")
12
+ end
13
+
14
+ instance_variable_set("@#{name}", value)
15
+ self
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :have_constant do |expected|
4
+ match do |actual|
5
+ actual.class.constants.include?(expected)
6
+ end
7
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe NXT::Interface::SerialPort do
4
+ before do
5
+ @device = '/dev/zero'
6
+ @bad_device = 'hello world'
7
+ end
8
+
9
+ subject do
10
+ NXT::Interface::SerialPort.new(@device)
11
+ end
12
+
13
+ describe 'constants' do
14
+ it 'should have a BAUD_RATE constant' do
15
+ should have_constant(:BAUD_RATE)
16
+ should have_constant(:DATA_BITS)
17
+ should have_constant(:STOP_BITS)
18
+ should have_constant(:PARITY)
19
+ should have_constant(:READ_TIMEOUT)
20
+ end
21
+ end
22
+
23
+ describe 'accessors' do
24
+ it 'should have read/write accessor for @dev' do
25
+ should respond_to(:dev)
26
+ should respond_to(:dev=)
27
+ end
28
+ end
29
+
30
+ describe '#initialize' do
31
+ it 'should set the device to the incomming argument' do
32
+ subject.dev.should equal(@device)
33
+ end
34
+
35
+ it 'should raise an exception when trying to connect to invalid dev files' do
36
+ expect do
37
+ serial_port = subject.class.new(@bad_device)
38
+ end.to raise_exception(InvalidDeviceError)
39
+ end
40
+ end
41
+
42
+ describe '#connect' do
43
+ it 'should raise an exception when the SerialPort connection failed' do
44
+ expect do
45
+ subject.connect
46
+ end.to raise_exception(SerialPortConnectionError, "The #{@device} device is not a valid SerialPort")
47
+ end
48
+
49
+ it 'should raise an exception when the SerialPort connection is nil' do
50
+ ::SerialPort.should_receive(:new).and_return(nil)
51
+ expect do
52
+ subject.connect
53
+ end.to raise_exception(SerialPortConnectionError, "Could not establish a SerialPort connection to #{@device}")
54
+ end
55
+
56
+ it 'should set the flow control and read timeout when the connection is established' do
57
+ serial_port_stub = stub()
58
+ serial_port_stub.should_receive(:flow_control=).with(::SerialPort::HARD).once
59
+ serial_port_stub.should_receive(:read_timeout=).with(subject.class::READ_TIMEOUT).once
60
+ ::SerialPort.should_receive(:new).and_return(serial_port_stub)
61
+
62
+ subject.connect
63
+ end
64
+ end
65
+
66
+ describe '#send' do
67
+
68
+ end
69
+
70
+ describe '#receive' do
71
+
72
+ end
73
+ end
@@ -0,0 +1,199 @@
1
+ require 'spec_helper'
2
+
3
+ module NXT
4
+ module Interface
5
+ class MockInterface < Base
6
+ def connect; end
7
+ def disconnect; end
8
+ end
9
+ end
10
+ end
11
+
12
+ describe NXTBrick do
13
+ before do
14
+ @interface = :mock_interface
15
+ end
16
+
17
+ subject do
18
+ NXTBrick.new(@interface)
19
+ end
20
+
21
+ describe 'accessors' do
22
+ it 'should have read/write accessors for @interface' do
23
+ should respond_to(:interface)
24
+ should respond_to(:interface=)
25
+ end
26
+
27
+ it 'should have a read accessor for @a' do
28
+ should respond_to(:a)
29
+ should_not respond_to(:a=)
30
+ end
31
+
32
+ it 'should have a read accessor for @b' do
33
+ should respond_to(:b)
34
+ should_not respond_to(:b=)
35
+ end
36
+
37
+ it 'should have a read accessor for @c' do
38
+ should respond_to(:c)
39
+ should_not respond_to(:c=)
40
+ end
41
+
42
+ it 'should have a read accessor for @one' do
43
+ should respond_to(:one)
44
+ should_not respond_to(:one=)
45
+ end
46
+
47
+ it 'should have a read accessor for @two' do
48
+ should respond_to(:two)
49
+ should_not respond_to(:two=)
50
+ end
51
+
52
+ it 'should have a read accessor for @three' do
53
+ should respond_to(:three)
54
+ should_not respond_to(:three=)
55
+ end
56
+
57
+ it 'should have a read accessor for @four' do
58
+ should respond_to(:four)
59
+ should_not respond_to(:four=)
60
+ end
61
+
62
+ it 'should have a read accessor for @port_identifiers' do
63
+ should respond_to(:port_identifiers)
64
+ should_not respond_to(:port_identifiers=)
65
+ end
66
+ end
67
+
68
+ describe '#initialize' do
69
+ it 'should raise an exception if an invalid type of interface is given' do
70
+ expect do
71
+ NXTBrick.new(:foobar_biz_buzz)
72
+ end.to raise_exception(InvalidInterfaceError)
73
+ end
74
+
75
+ it 'should set the interface to the incomming argument' do
76
+ subject.interface.class.should equal(NXT::Interface::MockInterface)
77
+ end
78
+
79
+ it 'should call yield if given a block, passing self' do
80
+ block_called = false
81
+
82
+ NXTBrick.new(@interface) do |nxt|
83
+ block_called = true
84
+ nxt.should be_an_instance_of(NXTBrick)
85
+ end
86
+
87
+ block_called.should be_true
88
+ end
89
+ end
90
+
91
+ describe '#add' do
92
+ it 'should raise an exception if an invalid type of port is given' do
93
+ expect do
94
+ subject.add('not a symbol', :symbol, Class)
95
+ end.to raise_exception(TypeError, 'Expected port to be a Symbol')
96
+ end
97
+
98
+ it 'should raise an exception if an invalid type of identifier is given' do
99
+ expect do
100
+ subject.add(:symbol, 'not a symbol', Class)
101
+ end.to raise_exception(TypeError, 'Expected identifier to be a Symbol')
102
+ end
103
+
104
+ it 'should raise an exception if an invalid type of klass is given' do
105
+ expect do
106
+ subject.add(:symbol, :symbol, 'not a class')
107
+ end.to raise_exception(TypeError, 'Expected klass to be a Class')
108
+ end
109
+
110
+ it 'should raise an exception if an invalid port number or letter is given' do
111
+ expect do
112
+ subject.add(:invalid_port, :symbol, Class)
113
+ end.to raise_exception(TypeError, 'Expected port to be one of: :a, :b, :c, :one, :two, :three, :four')
114
+ end
115
+
116
+ it 'should create a new instance of the passed klass and store it in the attribute for the given port' do
117
+ port = :a
118
+ class_stub = Class.new
119
+ class_return_stub = stub()
120
+
121
+ class_stub.should_receive(:new) do
122
+ class_return_stub
123
+ end.with(port, anything).once()
124
+
125
+ subject.add(port, :hello, class_stub)
126
+
127
+ subject.send(port).should equal(class_return_stub)
128
+ end
129
+
130
+ it 'should raise an exception if the port given is already set' do
131
+ port = :a
132
+
133
+ class_stub = Class.new
134
+ class_stub.stub(:new)
135
+ subject.stub(:hello)
136
+ subject.instance_variable_set(:"@#{port}", 'some value already there')
137
+
138
+ expect do
139
+ subject.add(port, :hello, class_stub)
140
+ end.to raise_error(PortTakenError, "Port #{port} is already set, call remove first")
141
+ end
142
+
143
+ it 'should raise an exception if trying to use an identifier that is the name of a defined methodz' do
144
+ port = :a
145
+ identifier = :hello
146
+
147
+ class_stub = Class.new
148
+ class_stub.stub(:new)
149
+ subject.stub(identifier)
150
+
151
+ expect do
152
+ subject.add(port, :hello, class_stub)
153
+ end.to raise_error(InvalidIdentifierError, "Cannot use identifier #{identifier}, a method on NXTBrick is already using it.")
154
+ end
155
+
156
+ it 'should set up the port identifiers correctly' do
157
+ port = :a
158
+ identifier = :hello_world
159
+ class_stub = Class.new
160
+ class_stub.stub(:new)
161
+
162
+ subject.add(port, identifier, class_stub)
163
+
164
+ subject.port_identifiers[identifier].should equal(port)
165
+ end
166
+ end
167
+
168
+ describe '#remove' do
169
+ it 'should raise an exception if an invalid type of identifier is given' do
170
+ expect do
171
+ subject.remove('not a symbol')
172
+ end.to raise_exception(TypeError, 'Expected identifier to be a Symbol')
173
+ end
174
+
175
+ it 'should remove any matching identifiers' do
176
+ identifier = :hello_world
177
+ port_identifiers = {}
178
+ subject.instance_variable_set(:@port_identifiers, port_identifiers)
179
+
180
+ port_identifiers.should_receive(:delete).with(identifier).once()
181
+ subject.remove(identifier)
182
+ end
183
+
184
+ it 'should return a boolean indicating whether it removed anything' do
185
+ identifier = :hello_world
186
+ port_identifiers = {}
187
+ port_identifiers[identifier] = true
188
+ subject.instance_variable_set(:@port_identifiers, port_identifiers)
189
+
190
+ return_value = subject.remove(identifier)
191
+ return_value.should be_true
192
+
193
+ port_identifiers.should_not include(identifier)
194
+
195
+ return_value = subject.remove(identifier)
196
+ return_value.should be_false
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,4 @@
1
+ require 'matchers'
2
+ require 'nxt'
3
+
4
+ include NXT::Exceptions
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nxt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tony Heupel
9
+ - Nathan Kleyn <nathan@unfinitydesign.com>
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-03-03 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: serialport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: 1.0.4
31
+ - !ruby/object:Gem::Dependency
32
+ name: activesupport
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 3.2.2
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 3.2.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: ruby-usb
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.1
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: 0.2.1
63
+ - !ruby/object:Gem::Dependency
64
+ name: rspec
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ version: 2.8.0
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: 2.8.0
79
+ - !ruby/object:Gem::Dependency
80
+ name: pry
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ~>
85
+ - !ruby/object:Gem::Version
86
+ version: 0.9.8.4
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: 0.9.8.4
95
+ - !ruby/object:Gem::Dependency
96
+ name: yard
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ version: 0.7.5
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 0.7.5
111
+ - !ruby/object:Gem::Dependency
112
+ name: redcarpet
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ~>
117
+ - !ruby/object:Gem::Version
118
+ version: 2.1.0
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ~>
125
+ - !ruby/object:Gem::Version
126
+ version: 2.1.0
127
+ description: Control an NXT 2.0 See http://github.com/nathankleyn/nxt for more information.
128
+ email: tony@heupel.org
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files:
132
+ - README.markdown
133
+ files:
134
+ - README.markdown
135
+ - Rakefile
136
+ - lib/nxt/commands/base.rb
137
+ - lib/nxt/commands/input.rb
138
+ - lib/nxt/commands/output.rb
139
+ - lib/nxt/commands/program.rb
140
+ - lib/nxt/commands/sound.rb
141
+ - lib/nxt/commands/tone.rb
142
+ - lib/nxt/connectors/input/color.rb
143
+ - lib/nxt/connectors/input/touch.rb
144
+ - lib/nxt/connectors/input/ultrasonic.rb
145
+ - lib/nxt/connectors/output/motor.rb
146
+ - lib/nxt/exceptions.rb
147
+ - lib/nxt/interfaces/base.rb
148
+ - lib/nxt/interfaces/serial_port.rb
149
+ - lib/nxt/interfaces/usb.rb
150
+ - lib/nxt/nxt_brick.rb
151
+ - lib/nxt/patches/module.rb
152
+ - lib/nxt/patches/string.rb
153
+ - lib/nxt/utils/accessors.rb
154
+ - lib/nxt.rb
155
+ - spec/matchers.rb
156
+ - spec/nxt/interfaces/serial_port_spec.rb
157
+ - spec/nxt/nxt_brick_spec.rb
158
+ - spec/spec_helper.rb
159
+ homepage: http://github.com/tchype/nxt
160
+ licenses: []
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ! '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ! '>='
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubyforge_project:
179
+ rubygems_version: 1.8.23
180
+ signing_key:
181
+ specification_version: 3
182
+ summary: Control an NXT 2.0
183
+ test_files: []
184
+ has_rdoc: