rufirmata 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +0 -0
- data/README.rdoc +0 -0
- data/lib/rufirmata/arduino.rb +6 -0
- data/lib/rufirmata/board.rb +177 -0
- data/lib/rufirmata/extensions.rb +5 -0
- data/lib/rufirmata/pin.rb +123 -0
- data/lib/rufirmata/port.rb +62 -0
- data/lib/rufirmata/version.rb +3 -0
- data/lib/rufirmata.rb +99 -0
- data/spec/board_spec.rb +228 -0
- data/spec/pin_spec.rb +89 -0
- data/spec/rufirmata_spec.rb +7 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/stubs/fake_serial.rb +50 -0
- metadata +136 -0
data/LICENSE
ADDED
File without changes
|
data/README.rdoc
ADDED
File without changes
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Rufirmata
|
2
|
+
|
3
|
+
class Board
|
4
|
+
include Observables::Base
|
5
|
+
|
6
|
+
attr_reader :serial_port, :name, :analog, :digital, :digital_ports, :board_type,
|
7
|
+
:taken,:firmata_version,:firmware, :listening
|
8
|
+
|
9
|
+
def initialize(serial_port, options={})
|
10
|
+
options[:serial_port] = serial_port
|
11
|
+
@serial_port ||= Rufirmata.create_serial_port(options)
|
12
|
+
@name = options[:name] || serial_port
|
13
|
+
@board_type =
|
14
|
+
case options[:board_type]
|
15
|
+
when Hash : options[:board_type]
|
16
|
+
when Symbol : Rufirmata::BOARD_TYPES[options[:board_type]]
|
17
|
+
else Rufirmata::BOARD_TYPES[:arduino]
|
18
|
+
end
|
19
|
+
@taken = { :analog => { }, :digital =>{ } }
|
20
|
+
@listening = false
|
21
|
+
initialize_layout
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"Board #{name} on #{serial_port}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def write_command(*commands)
|
29
|
+
message = commands.shift.chr
|
30
|
+
commands.each { |command| message += command.chr }
|
31
|
+
@serial_port.write(message)
|
32
|
+
end
|
33
|
+
|
34
|
+
def write(data)
|
35
|
+
@serial_port.write(data)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sends a SysEx msg.
|
39
|
+
# :arg sysex_cmd: A sysex command byteggg
|
40
|
+
# :arg data: A list of data values
|
41
|
+
def send_sysex(sysex_cmd, data=[])
|
42
|
+
write_command Rufirmata::START_SYSEX
|
43
|
+
write_command sysex_cmd
|
44
|
+
data.each do |b|
|
45
|
+
begin
|
46
|
+
byte = b.chr
|
47
|
+
rescue
|
48
|
+
byte = (b >> 7).chr
|
49
|
+
end#TODO send multiple bytes
|
50
|
+
write(byte)
|
51
|
+
end
|
52
|
+
write_command Rufirmata::END_SYSEX
|
53
|
+
end
|
54
|
+
|
55
|
+
def close
|
56
|
+
@listening = false
|
57
|
+
@serial_port.close()
|
58
|
+
end
|
59
|
+
alias stop close
|
60
|
+
|
61
|
+
def start_listening
|
62
|
+
@listening = true
|
63
|
+
@listener = Thread.new do
|
64
|
+
while @listening
|
65
|
+
iterate
|
66
|
+
sleep 0.001
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
# Reads and handles data from the microcontroller over the serial port.
|
73
|
+
#This method should be called in a main loop, or in an
|
74
|
+
#:class:`Iterator` instance to keep this boards pin values up to date
|
75
|
+
def iterate
|
76
|
+
data = @serial_port.getc
|
77
|
+
return unless data
|
78
|
+
|
79
|
+
received_data = []
|
80
|
+
|
81
|
+
if data < Rufirmata::START_SYSEX
|
82
|
+
command = data & 0xF0
|
83
|
+
handler = find_handler(command)
|
84
|
+
return unless handler
|
85
|
+
received_data << (data & 0x0F)
|
86
|
+
while received_data.length < method(handler).arity
|
87
|
+
received_data << @serial_port.getc
|
88
|
+
end
|
89
|
+
|
90
|
+
elsif data == Rufirmata::START_SYSEX
|
91
|
+
|
92
|
+
data = @serial_port.getc
|
93
|
+
handler = find_handler(data)
|
94
|
+
return unless handler
|
95
|
+
data = @serial_port.getc
|
96
|
+
while data != Rufirmata::END_SYSEX
|
97
|
+
received_data << data
|
98
|
+
data = @serial_port.getc
|
99
|
+
end
|
100
|
+
|
101
|
+
else
|
102
|
+
handler = find_handler(data)
|
103
|
+
return unless handler
|
104
|
+
while received_data.length < method(handler).arity
|
105
|
+
received_data << @serial_port.getc
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
send(handler,*received_data) if handler
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def find_handler(command)
|
116
|
+
case command
|
117
|
+
when Rufirmata::ANALOG_MESSAGE : :handle_analog_message
|
118
|
+
when Rufirmata::DIGITAL_MESSAGE : :handle_digital_message
|
119
|
+
when Rufirmata::REPORT_VERSION : :handle_report_version
|
120
|
+
when Rufirmata::REPORT_FIRMWARE : :handle_report_firmware
|
121
|
+
else return
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def handle_analog_message(pin_number, lsb, msb)
|
126
|
+
value = (((msb << 7) + lsb).to_f / 1023).prec(4)
|
127
|
+
self.analog[pin_number].value = value if self.analog[pin_number].reporting
|
128
|
+
end
|
129
|
+
|
130
|
+
#Digital messages always go by the whole port. This means we have a
|
131
|
+
# bitmask wich we update the port.
|
132
|
+
def handle_digital_message(port_number, lsb, msb)
|
133
|
+
mask = (msb << 7) + lsb
|
134
|
+
self.digital_ports[port_number].update(mask)
|
135
|
+
end
|
136
|
+
|
137
|
+
def handle_report_version(major, minor)
|
138
|
+
@firmata_version = [major,minor]
|
139
|
+
end
|
140
|
+
|
141
|
+
def handle_report_firmware(*data)
|
142
|
+
major = data.shift
|
143
|
+
minor = data.shift
|
144
|
+
@firmata_version = [major,minor]
|
145
|
+
# TODO this is more complicated, values is send as 7 bit bytes
|
146
|
+
@firmware = data.map{|byte|byte.chr}.join('')
|
147
|
+
end
|
148
|
+
|
149
|
+
def initialize_layout
|
150
|
+
@analog, @digital, @digital_ports = [], [], []
|
151
|
+
|
152
|
+
@board_type[:analog_pins].each do |pin|
|
153
|
+
@analog << Rufirmata::Pin.new(self, pin, Rufirmata::ANALOG)
|
154
|
+
end
|
155
|
+
|
156
|
+
digital_pins = @board_type[:digital_pins]
|
157
|
+
|
158
|
+
if @board_type[:use_ports]
|
159
|
+
(0...digital_pins.length/7).each { |port| @digital_ports << Port.new(self, port)}
|
160
|
+
else
|
161
|
+
(0..digital_pins).each do |pin|
|
162
|
+
@digital << Rufirmata::Pin.new(self, pin, Rufirmata::DIGITAL)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
(@digital + @analog).each do |pin|
|
167
|
+
pin.set_observer do |sender, type, args|
|
168
|
+
notifier.publish type, args.merge(:pin=>sender)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
end #init layout
|
173
|
+
|
174
|
+
|
175
|
+
end #board
|
176
|
+
|
177
|
+
end #rufirmata
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Rufirmata
|
2
|
+
#A Pin representation
|
3
|
+
class Pin
|
4
|
+
include Observables::Base
|
5
|
+
|
6
|
+
attr_reader :board, :pin_number, :pin_type, :mode, :port, :pwm, :reporting, :value
|
7
|
+
|
8
|
+
def initialize(board,pin_number,pin_type,port = nil)
|
9
|
+
@board = board
|
10
|
+
@port = port
|
11
|
+
@pin_number = pin_number
|
12
|
+
@pin_type = pin_type
|
13
|
+
@reporting = false
|
14
|
+
@value = nil
|
15
|
+
@pwm = false
|
16
|
+
@mode = Rufirmata::INPUT
|
17
|
+
if pin_type == Rufirmata::DIGITAL
|
18
|
+
@pwm = board.board_type[:pwm_pins].include?(pin_number)
|
19
|
+
@mode = board.board_type[:disabled_pins].include?(pin_number) ?
|
20
|
+
Rufirmata::UNAVAILABLE : Rufirmata::OUTPUT
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
type = self.pin_type == Rufirmata::ANALOG ? "Analog" : "Digital"
|
26
|
+
"#{type} pin #{pin_number}"
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Set the mode of operation for the pin
|
31
|
+
# Can be one of the pin modes: INPUT, OUTPUT, ANALOG or PWM
|
32
|
+
def mode=(mode)
|
33
|
+
#Can be Rufirmata::INPUT, OUTPUT, ANALOG, PWM or UNAVAILABLE
|
34
|
+
return if @mode == mode #Nothing is changing, so nothing to do
|
35
|
+
|
36
|
+
raise "#{to_s} does not have PWM capabilities" if mode == Rufirmata::PWM and !pwm
|
37
|
+
raise "#{to_s} cannot be used through Firmata" if @mode == Rufirmata::UNAVAILABLE
|
38
|
+
|
39
|
+
changing :pin_mode_changed, :changes=>{ :from=>@mode, :to=>mode } do
|
40
|
+
@mode = mode
|
41
|
+
unless mode == Rufirmata::UNAVAILABLE
|
42
|
+
board.write_command(Rufirmata::SET_PIN_MODE, pin_number, mode)
|
43
|
+
enable_reporting if mode == Rufirmata::INPUT
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def reporting=(reporting)
|
50
|
+
return if @reporting == reporting
|
51
|
+
changing :reporting_changed, :changes=>{ :from=>@reporting, :to=>reporting } do
|
52
|
+
@reporting = reporting
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def value=(new_value)
|
57
|
+
return if @value == new_value
|
58
|
+
changing :value_changed, :changes=>{ :from=>@value, :to=>new_value } do
|
59
|
+
@value = new_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#Set an input pin to report values
|
64
|
+
def enable_reporting
|
65
|
+
raise "#{to_s} is not an input and therefore cannot report" unless mode == Rufirmata::INPUT
|
66
|
+
if pin_type == Rufirmata::ANALOG
|
67
|
+
self.reporting = true
|
68
|
+
board.write_command(Rufirmata::REPORT_ANALOG + pin_number, 1)
|
69
|
+
elsif port
|
70
|
+
port.enable_reporting
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Disable the reporting of an input pin
|
75
|
+
def disable_reporting
|
76
|
+
if pin_type == Rufirmata::ANALOG
|
77
|
+
@reporting = false
|
78
|
+
board.write_command(Rufirmata::REPORT_ANALOG + pin_number, 0)
|
79
|
+
elsif port
|
80
|
+
port.disable_reporting
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
#Returns the output value of the pin. This value is updated by the
|
85
|
+
#boards `Board.iterate` method. Value is always in the range 0.0 - 1.0
|
86
|
+
def read
|
87
|
+
raise "Cannot read pin #{to_s} because it is marked as UNAVAILABLE" if mode == Rufirmata::UNAVAILABLE
|
88
|
+
value
|
89
|
+
end
|
90
|
+
|
91
|
+
#Output a voltage from the pin
|
92
|
+
#
|
93
|
+
# :arg value: Uses value as a boolean if the pin is in output mode, or
|
94
|
+
# expects a float from 0 to 1 if the pin is in PWM mode.
|
95
|
+
def write(new_value)
|
96
|
+
raise "#{to_s} cannot be used through Firmata" if mode == Rufirmata::UNAVAILABLE
|
97
|
+
raise "#{to_s} is set up as an INPUT and therefore cannot be written to" if mode == Rufirmata::INPUT
|
98
|
+
if (new_value != value)
|
99
|
+
@value = new_value
|
100
|
+
if mode == Rufirmata::OUTPUT
|
101
|
+
port ? port.write() :
|
102
|
+
board.write_command(Rufirmata::DIGITAL_MESSAGE, pin_number, value)
|
103
|
+
elsif mode == Rufirmata::PWM
|
104
|
+
val = (@value * 255).to_i
|
105
|
+
board.write_command(Rufirmata::ANALOG_MESSAGE + pin_number, val % 128, val >> 7)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end #write
|
109
|
+
|
110
|
+
def send_sysex(sysex_cmd, data=[])
|
111
|
+
#Sends a SysEx message.
|
112
|
+
# :arg sysex_cmd: A sysex command byte
|
113
|
+
# :arg data: A list of data values
|
114
|
+
board.write_command(Rufirmata::START_SYSEX)
|
115
|
+
board.write_command(systex_cmd)
|
116
|
+
data.each do |byte|
|
117
|
+
byte = begin; byte.chr; rescue RangeError; (byte >> 7).chr; end
|
118
|
+
board.write(byte)
|
119
|
+
end
|
120
|
+
board.write_command(Rufirmata::END_SYSEX)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rufirmata
|
2
|
+
#An 8-bit port on the board
|
3
|
+
class Port
|
4
|
+
|
5
|
+
attr_reader :board, :port_number, :pins
|
6
|
+
attr_accessor :reporting
|
7
|
+
|
8
|
+
def initialize(board, port_number)
|
9
|
+
@board = board
|
10
|
+
@port_number = port_number
|
11
|
+
@reporting = false
|
12
|
+
@pins = []
|
13
|
+
(0..7).each do |pin|
|
14
|
+
pin_num = pin + port_number * 8
|
15
|
+
pins << (pin = Rufirmata::Pin.new(board, pin_num, Rufirmata::DIGITAL, self))
|
16
|
+
board.digital << pin
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s; "Digital Port #{port_number}"; end
|
21
|
+
|
22
|
+
#Enable reporting of values for the whole port
|
23
|
+
def enable_reporting
|
24
|
+
@reporting = true
|
25
|
+
board.write_command(Rufirmata::REPORT_DIGITAL + port_number, 1)
|
26
|
+
pins.each {|pin|pin.reporting = true}
|
27
|
+
end
|
28
|
+
|
29
|
+
#Disable reporting of values for the whole port
|
30
|
+
def disable_reporting
|
31
|
+
@reporting = false
|
32
|
+
board.write_command(Rufirmata::REPORT_DIGITAL + port_number, 0)
|
33
|
+
end
|
34
|
+
|
35
|
+
#Set the output pins of the port to the correct state
|
36
|
+
def write
|
37
|
+
mask = 0
|
38
|
+
pins.each do |pin|
|
39
|
+
if (pin.mode == Rufirmata::OUTPUT)
|
40
|
+
if (pin.value == 1)
|
41
|
+
pin_nr = pin.pin_number - port_number * 8
|
42
|
+
mask |= 1 << pin_nr
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
board.write_command(Rufirmata::DIGITAL_MESSAGE + port_number, mask % 128, mask >> 7)
|
47
|
+
end
|
48
|
+
|
49
|
+
#Update the values for the pins marked as input with the mask
|
50
|
+
def update(mask)
|
51
|
+
if reporting
|
52
|
+
pins.each do |pin|
|
53
|
+
if pin.mode == Rufirmata::INPUT
|
54
|
+
pin_nr = pin.pin_number - port_number * 8
|
55
|
+
pin.value = (mask & (1 << pin_nr)) > 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/rufirmata.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'serialport'
|
3
|
+
require 'observables'
|
4
|
+
|
5
|
+
base_dir = File.dirname(__FILE__)
|
6
|
+
[
|
7
|
+
'version',
|
8
|
+
'extensions',
|
9
|
+
'pin',
|
10
|
+
'port',
|
11
|
+
'board',
|
12
|
+
'arduino'
|
13
|
+
].each {|req| require File.join(base_dir,'rufirmata',req)}
|
14
|
+
|
15
|
+
module Rufirmata
|
16
|
+
# Message command bytes - straight from Firmata.h
|
17
|
+
DIGITAL_MESSAGE = 0x90 # send data for a digital pin
|
18
|
+
ANALOG_MESSAGE = 0xE0 # send data for an analog pin (or PWM)
|
19
|
+
DIGITAL_PULSE = 0x91 # SysEx command to send a digital pulse
|
20
|
+
|
21
|
+
# PULSE_MESSAGE = 0xA0 # proposed pulseIn/Out msg (SysEx)
|
22
|
+
# SHIFTOUT_MESSAGE = 0xB0 # proposed shiftOut msg (SysEx)
|
23
|
+
REPORT_ANALOG = 0xC0 # enable analog input by pin #
|
24
|
+
REPORT_DIGITAL = 0xD0 # enable digital input by port pair
|
25
|
+
START_SYSEX = 0xF0 # start a MIDI SysEx msg
|
26
|
+
SET_PIN_MODE = 0xF4 # set a pin to INPUT/OUTPUT/PWM/etc
|
27
|
+
END_SYSEX = 0xF7 # end a MIDI SysEx msg
|
28
|
+
REPORT_VERSION = 0xF9 # report firmware version
|
29
|
+
SYSTEM_RESET = 0xFF # reset from MIDI
|
30
|
+
QUERY_FIRMWARE = 0x79 # query the firmware name
|
31
|
+
|
32
|
+
# extended command set using sysex (0-127/0x00-0x7F)
|
33
|
+
# 0x00-0x0F reserved for user-defined commands */
|
34
|
+
SERVO_CONFIG = 0x70 # set max angle, minPulse, maxPulse, freq
|
35
|
+
STRING_DATA = 0x71 # a string message with 14-bits per char
|
36
|
+
SHIFT_DATA = 0x75 # a bitstream to/from a shift register
|
37
|
+
I2C_REQUEST = 0x76 # send an I2C read/write request
|
38
|
+
I2C_REPLY = 0x77 # a reply to an I2C read request
|
39
|
+
I2C_CONFIG = 0x78 # config I2C settings such as delay times and power pins
|
40
|
+
REPORT_FIRMWARE = 0x79 # report name and version of the firmware
|
41
|
+
SAMPLING_INTERVAL = 0x7A # set the poll rate of the main loop
|
42
|
+
SYSEX_NON_REALTIME = 0x7E # MIDI Reserved for non-realtime messages
|
43
|
+
SYSEX_REALTIME = 0x7F # MIDI Reserved for realtime messages
|
44
|
+
|
45
|
+
|
46
|
+
# Pin modes.
|
47
|
+
# except from UNAVAILABLE taken from Firmata.h
|
48
|
+
UNAVAILABLE = -1
|
49
|
+
INPUT = 0 # as defined in wiring.h
|
50
|
+
OUTPUT = 1 # as defined in wiring.h
|
51
|
+
ANALOG = 2 # analog pin in analogInput mode
|
52
|
+
PWM = 3 # digital pin in PWM output mode
|
53
|
+
|
54
|
+
# Pin types
|
55
|
+
DIGITAL = OUTPUT # same as OUTPUT below
|
56
|
+
# ANALOG is already defined above
|
57
|
+
|
58
|
+
BOARD_TYPES = {
|
59
|
+
:arduino => {
|
60
|
+
:digital_pins => (0..13).to_a,
|
61
|
+
:analog_pins => (0..5).to_a,
|
62
|
+
:pwm_pins => [3, 5, 6, 9, 10, 11],
|
63
|
+
:use_ports => true,
|
64
|
+
:disabled_pins => [0, 1, 14, 15] #Rx, Tx, Crystal
|
65
|
+
},
|
66
|
+
:arduino_mega => {
|
67
|
+
:digital_pins => (0..53).to_a,
|
68
|
+
:analog_pins => (0..15).to_a,
|
69
|
+
:pwm_pints => (2..14).to_a,
|
70
|
+
:use_ports => true,
|
71
|
+
:disabled_pins => [0, 1, 14, 15] #Rx, Tx, Crystal
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
class << self
|
76
|
+
attr_reader :serial_ports
|
77
|
+
|
78
|
+
def create_serial_port(options={})
|
79
|
+
@serial_ports ||= {}
|
80
|
+
options = {
|
81
|
+
:serial_port => "/dev/ttyUSB0",
|
82
|
+
:baud_rate => 57600,
|
83
|
+
:parity => SerialPort::NONE,
|
84
|
+
:data_bits => 8,
|
85
|
+
:stop_bits => 1
|
86
|
+
}.merge(options)
|
87
|
+
|
88
|
+
sp = options[:serial_port]
|
89
|
+
@serial_ports[options] =
|
90
|
+
SerialPort.new(sp,
|
91
|
+
options[:baud_rate],
|
92
|
+
options[:data_bits],
|
93
|
+
options[:stop_bits],
|
94
|
+
options[:parity])
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/spec/board_spec.rb
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rufirmata::Board do
|
4
|
+
before(:each) do
|
5
|
+
Rufirmata.stub(:create_serial_port) { FakeSerial.new }
|
6
|
+
@board = Rufirmata::Board.new("/dev/ttyUSB0")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "handlers" do
|
10
|
+
it "should return nil for a failed analog read" do
|
11
|
+
@board.analog[3].reporting = true
|
12
|
+
@board.analog[3].read.should be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# This sould set it correctly. 1023 (127, 7 in to 7 bit bytes) is the
|
16
|
+
# max value an analog pin will send and it should result in a value 1
|
17
|
+
it "should handle an analog message" do
|
18
|
+
@board.analog[3].reporting = true
|
19
|
+
@board.send(:handle_analog_message,3,127,7)
|
20
|
+
@board.analog[3].read.should == 1.0
|
21
|
+
end
|
22
|
+
|
23
|
+
# A digital message sets the value for a whole port. We will set pin
|
24
|
+
# 5 (That is on port 0) to 1 to test if this is working.
|
25
|
+
it "should handle a digital message" do
|
26
|
+
@board.digital_ports[0].reporting = true
|
27
|
+
@board.digital[5].mode = Rufirmata::INPUT
|
28
|
+
mask = 0
|
29
|
+
mask |= 1 << 5 # set the bit for pin 5 to to 1
|
30
|
+
@board.digital[5].read.should be_nil
|
31
|
+
@board.send(:handle_digital_message, 0, mask % 128, mask >> 7)
|
32
|
+
@board.digital[5].read.should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should handle report version" do
|
36
|
+
@board.firmata_version.should be_nil
|
37
|
+
@board.send(:handle_report_version,2,1)
|
38
|
+
@board.firmata_version.should == [2,1]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should handle report firmware" do
|
42
|
+
@board.firmware.should be_nil
|
43
|
+
data = [2,1]
|
44
|
+
"Firmware_name".each_byte { |b|data << b}
|
45
|
+
@board.send(:handle_report_firmware,*data)
|
46
|
+
@board.firmware.should == 'Firmware_name'
|
47
|
+
@board.firmata_version.should == [2,1]
|
48
|
+
end
|
49
|
+
|
50
|
+
# type command channel first byte second byte
|
51
|
+
# ---------------------------------------------------------------------------
|
52
|
+
# analog I/O message 0xE0 pin # LSB(bits 0-6) MSB(bits 7-13)
|
53
|
+
describe "incoming analog message" do
|
54
|
+
it "should ignore incoming data when reporting=false" do
|
55
|
+
@board.serial_port.write_as_chars Rufirmata::ANALOG_MESSAGE + 4, 127, 7
|
56
|
+
@board.iterate()
|
57
|
+
@board.analog[4].read.should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should set incoming data on the pin value when reporting=true" do
|
61
|
+
@board.analog[4].enable_reporting
|
62
|
+
@board.serial_port.clear
|
63
|
+
@board.serial_port.write_as_chars Rufirmata::ANALOG_MESSAGE + 4, 127, 7
|
64
|
+
@board.iterate
|
65
|
+
@board.analog[4].read.should == 1.0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# type command channel first byte second byte
|
70
|
+
# ---------------------------------------------------------------------------
|
71
|
+
# digital I/O message 0x90 port LSB(bits 0-6) MSB(bits 7-13)
|
72
|
+
describe "incoming digital message" do
|
73
|
+
it "should set incoming digital data for input pins" do
|
74
|
+
@board.digital[9].mode = Rufirmata::INPUT
|
75
|
+
@board.serial_port.clear
|
76
|
+
mask = 0; mask |= 1 << (9 - 8) #set the bit for pin 9 to 1
|
77
|
+
@board.digital[9].read.should be_nil
|
78
|
+
@board.serial_port.write_as_chars Rufirmata::DIGITAL_MESSAGE + 1, mask % 128, mask >> 7
|
79
|
+
@board.iterate
|
80
|
+
@board.digital[9].read.should be_true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# version report format
|
85
|
+
# -------------------------------------------------
|
86
|
+
# 0 version report header (0xF9) (MIDI Undefined)
|
87
|
+
# 1 major version (0-127)
|
88
|
+
# 2 minor version (0-127)
|
89
|
+
describe "incoming report version" do
|
90
|
+
it "should set the firmata version" do
|
91
|
+
@board.serial_port.write_as_chars Rufirmata::REPORT_VERSION, 2, 1
|
92
|
+
@board.iterate
|
93
|
+
@board.firmata_version.should == [2,1]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Receive Firmware Name and Version (after query)
|
98
|
+
# 0 START_SYSEX (0xF0)
|
99
|
+
# 1 queryFirmware (0x79)
|
100
|
+
# 2 major version (0-127)
|
101
|
+
# 3 minor version (0-127)
|
102
|
+
# 4 first 7-bits of firmware name
|
103
|
+
# 5 second 7-bits of firmware name
|
104
|
+
# x ...for as many bytes as it needs)
|
105
|
+
# 6 END_SYSEX (0xF7)
|
106
|
+
describe "incoming report firmware" do
|
107
|
+
before(:each) do
|
108
|
+
@board.serial_port.write_as_chars Rufirmata::START_SYSEX, Rufirmata::REPORT_FIRMWARE, 2, 1
|
109
|
+
@board.serial_port.write 'Firmware_name'.split(//)
|
110
|
+
@board.serial_port.write_as_chars Rufirmata::END_SYSEX
|
111
|
+
@board.iterate
|
112
|
+
end
|
113
|
+
it "should have set the appropriate firmware name" do
|
114
|
+
@board.firmware.should == 'Firmware_name'
|
115
|
+
end
|
116
|
+
it "should have set the firmata version" do
|
117
|
+
@board.firmata_version.should == [2,1]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# type command channel first byte second byte
|
122
|
+
# ---------------------------------------------------------------------------
|
123
|
+
# report analog pin 0xC0 pin # disable/enable(0/1) - n/a -
|
124
|
+
describe "report analog" do
|
125
|
+
it "should write an enable reporting command to the serial port" do
|
126
|
+
@board.analog[1].enable_reporting
|
127
|
+
@board.serial_port.should have_received_bytes( 0xC0 + 1, 1 )
|
128
|
+
@board.analog[1].reporting.should be_true
|
129
|
+
end
|
130
|
+
it "should write a disable reporting command to the serial port" do
|
131
|
+
@board.analog[1].reporting = true
|
132
|
+
@board.analog[1].disable_reporting
|
133
|
+
@board.serial_port.should have_received_bytes( 0xC0 + 1, 0 )
|
134
|
+
@board.analog[1].reporting.should be_false
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# type command channel first byte second byte
|
139
|
+
# ---------------------------------------------------------------------------
|
140
|
+
# report digital port 0xD0 port disable/enable(0/1) - n/a -
|
141
|
+
describe "report digital" do
|
142
|
+
before(:each){ @board.digital[8].mode = Rufirmata::INPUT; @board.serial_port.clear }
|
143
|
+
# This should enable reporting of whole port 1
|
144
|
+
it "should write an enable reporting command for the port" do
|
145
|
+
@board.digital[8].enable_reporting
|
146
|
+
@board.serial_port.should have_received_bytes( 0xD0 + 1, 1 )
|
147
|
+
@board.digital[8].reporting.should be_true
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should write a disable reporting command for the port" do
|
151
|
+
@board.digital[8].reporting = true
|
152
|
+
@board.digital[8].disable_reporting
|
153
|
+
@board.serial_port.should have_received_bytes( 0xD0 + 1, 0 )
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Generic Sysex Message
|
158
|
+
# 0 START_SYSEX (0xF0)
|
159
|
+
# 1 sysex command (0x00-0x7F)
|
160
|
+
# x between 0 and MAX_DATA_BYTES 7-bit bytes of arbitrary data
|
161
|
+
# last END_SYSEX (0xF7)
|
162
|
+
describe "SYSEX" do
|
163
|
+
it "should send a sysex message" do
|
164
|
+
@board.send_sysex 0x79, [1,2,3]
|
165
|
+
@board.serial_port.should have_received_bytes 0xF0, 0x79, 1, 2, 3, 0xF7
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should receive a sysex message" do
|
169
|
+
@board.serial_port.write [0xF0, 0x79, 2, 1, 'a'[0], 'b'[0], 'c'[0], 0xF7].map{ |b|b.chr}
|
170
|
+
while @board.serial_port.length > 0
|
171
|
+
@board.iterate
|
172
|
+
end
|
173
|
+
@board.firmata_version.should == [2,1]
|
174
|
+
@board.firmware.should == 'abc'
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "sending extraneous data" do
|
179
|
+
it "should be ignored" do
|
180
|
+
@board.analog[4].enable_reporting
|
181
|
+
@board.serial_port.clear
|
182
|
+
#Garbage
|
183
|
+
@board.serial_port.write_as_chars( *(0..10).to_a )
|
184
|
+
#Set analog port 4 to 1
|
185
|
+
@board.serial_port.write_as_chars Rufirmata::ANALOG_MESSAGE + 4, 127, 7
|
186
|
+
#More garbage
|
187
|
+
@board.serial_port.write_as_chars( *(0..10).to_a )
|
188
|
+
while @board.serial_port.length > 0
|
189
|
+
@board.iterate
|
190
|
+
end
|
191
|
+
@board.analog[4].read.should == 1.0
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "notifications" do
|
196
|
+
before :each do
|
197
|
+
@changes = []
|
198
|
+
@board.subscribe do |type,args|
|
199
|
+
@changes << [type,args]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
describe "when a pin is changed" do
|
205
|
+
before(:each){@board.analog[0].value = 1.1}
|
206
|
+
|
207
|
+
it "should issue before and after change notifications" do
|
208
|
+
@changes.length.should == 2
|
209
|
+
@changes[0][0].should == :before_value_changed
|
210
|
+
@changes[1][0].should == :after_value_changed
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should pass the pin in the event args" do
|
214
|
+
@changes[0][1][:pin].should == @board.analog[0]
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should provide the previous and new values of the pin" do
|
218
|
+
@changes.each do |c|
|
219
|
+
c[1][:changes].should == { :from=>nil, :to=>1.1}
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
data/spec/pin_spec.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rufirmata::Pin do
|
4
|
+
before(:each) do
|
5
|
+
Rufirmata.stub(:create_serial_port) { FakeSerial.new }
|
6
|
+
@board = Rufirmata::Board.new("/dev/ttyUSB0")
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "notifications" do
|
10
|
+
before :each do
|
11
|
+
@changes = []
|
12
|
+
@pin = Rufirmata::Pin.new(@board, 2, Rufirmata::DIGITAL)
|
13
|
+
@pin.subscribe do |type, args|
|
14
|
+
@changes << [type, args]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#mode" do
|
19
|
+
before(:each) { @pin.mode = Rufirmata::INPUT }
|
20
|
+
|
21
|
+
it "should issue a change notice before and after the change" do
|
22
|
+
@changes.count.should == 2
|
23
|
+
@changes[0][0].should == :before_pin_mode_changed
|
24
|
+
@changes[1][0].should == :after_pin_mode_changed
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should notify changes to the pin mode" do
|
28
|
+
@changes.each do |c|
|
29
|
+
c[1].changes[:from].should == Rufirmata::OUTPUT
|
30
|
+
c[1].changes[:to].should == Rufirmata::INPUT
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not issue a notification if nothing changed" do
|
35
|
+
@changes.clear
|
36
|
+
@pin.mode = Rufirmata::INPUT
|
37
|
+
@changes.length.should == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#reporting" do
|
43
|
+
before(:each){ @pin.reporting = true }
|
44
|
+
|
45
|
+
it "should issue a change before and after reporting changed" do
|
46
|
+
@changes.length.should == 2
|
47
|
+
@changes[0][0].should == :before_reporting_changed
|
48
|
+
@changes[1][0].should == :after_reporting_changed
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should notify changes to reporting" do
|
52
|
+
@changes.each do |c|
|
53
|
+
c[1].changes[:from].should == false
|
54
|
+
c[1].changes[:to].should == true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not issue a change when not changed" do
|
59
|
+
@changes.clear
|
60
|
+
@pin.reporting = true
|
61
|
+
@changes.length.should == 0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#reporting" do
|
66
|
+
before(:each){ @pin.value = 1 }
|
67
|
+
|
68
|
+
it "should issue a change before and after value changed" do
|
69
|
+
@changes.length.should == 2
|
70
|
+
@changes[0][0].should == :before_value_changed
|
71
|
+
@changes[1][0].should == :after_value_changed
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should notify changes to value" do
|
75
|
+
@changes.each do |c|
|
76
|
+
c[1].changes[:from].should == nil
|
77
|
+
c[1].changes[:to].should == 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not issue a change when not changed" do
|
82
|
+
@changes.clear
|
83
|
+
@pin.value = 1
|
84
|
+
@changes.length.should == 0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
require 'rufirmata'
|
6
|
+
require 'rspec'
|
7
|
+
|
8
|
+
# Requires supporting files with custom matchers and macros, etc,
|
9
|
+
# in ./support/ and its subdirectories.
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
11
|
+
Dir["#{File.dirname(__FILE__)}/stubs/**/*.rb"].each {|f| require f}
|
12
|
+
|
13
|
+
require 'rspec/expectations'
|
14
|
+
|
15
|
+
RSpec::Matchers.define :have_received_bytes do |*expected|
|
16
|
+
match do |actual|
|
17
|
+
res = actual.read()
|
18
|
+
serial_msg = res
|
19
|
+
until res.nil? or res.empty?
|
20
|
+
res = actual.read()
|
21
|
+
serial_msg += res
|
22
|
+
end
|
23
|
+
expected = expected.map{|b|b.chr}.join('')
|
24
|
+
actual.instance_eval "def last_received; '#{serial_msg.inspect}'; end"
|
25
|
+
expected == serial_msg
|
26
|
+
end
|
27
|
+
failure_message_for_should do |actual|
|
28
|
+
"expected to receive #{expected.inspect} but received #{actual.last_received}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Rspec.configure do |config|
|
33
|
+
# == Mock Framework
|
34
|
+
|
35
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
36
|
+
#
|
37
|
+
# config.mock_with :mocha
|
38
|
+
# config.mock_with :flexmock
|
39
|
+
# config.mock_with :rr
|
40
|
+
config.mock_with :rspec
|
41
|
+
|
42
|
+
config.before(:each) do
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
# A Mock object for ruby's Serial. Functions as a fifo-stack. Push to
|
3
|
+
# it with ``write``, read from it with ``read``.
|
4
|
+
#
|
5
|
+
# >>> s = Serial.new('someport', 4800)
|
6
|
+
# >>> s.read()
|
7
|
+
# ''
|
8
|
+
# >>> s.write(chr(100))
|
9
|
+
# >>> s.write('blaat')
|
10
|
+
# >>> s.write(100000)
|
11
|
+
# >>> s.read(2)
|
12
|
+
# ['d', 'blaat']
|
13
|
+
# >>> s.read()
|
14
|
+
# 100000
|
15
|
+
# >>> s.read()
|
16
|
+
# ''
|
17
|
+
# >>> s.read(2)
|
18
|
+
# ['', '']
|
19
|
+
# >>> s.close()
|
20
|
+
class FakeSerial < Array
|
21
|
+
|
22
|
+
def getc
|
23
|
+
read[0]
|
24
|
+
end
|
25
|
+
|
26
|
+
def read(count=1)
|
27
|
+
if count > 1
|
28
|
+
val = []
|
29
|
+
[i..count].each do |i|
|
30
|
+
val << (shift || '')
|
31
|
+
end
|
32
|
+
else
|
33
|
+
val = (shift || '')
|
34
|
+
end
|
35
|
+
val
|
36
|
+
end
|
37
|
+
|
38
|
+
def write(value)
|
39
|
+
(self << value).flatten!
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_as_chars(*vals)
|
43
|
+
write vals.map{ |v| v.chr }
|
44
|
+
end
|
45
|
+
|
46
|
+
def close
|
47
|
+
clear()
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rufirmata
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Nathan Stults
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-11-27 00:00:00 -08:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: ruby-serialport
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: observables
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rspec
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 1
|
58
|
+
segments:
|
59
|
+
- 2
|
60
|
+
- 1
|
61
|
+
version: "2.1"
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: fuubar
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
description: A Ruby port of pyFirmata, software for interfacing with firmata-enabled microcontrollers
|
79
|
+
email: hereiam@sonic.net
|
80
|
+
executables: []
|
81
|
+
|
82
|
+
extensions: []
|
83
|
+
|
84
|
+
extra_rdoc_files: []
|
85
|
+
|
86
|
+
files:
|
87
|
+
- lib/rufirmata.rb
|
88
|
+
- lib/rufirmata/extensions.rb
|
89
|
+
- lib/rufirmata/pin.rb
|
90
|
+
- lib/rufirmata/arduino.rb
|
91
|
+
- lib/rufirmata/board.rb
|
92
|
+
- lib/rufirmata/port.rb
|
93
|
+
- lib/rufirmata/version.rb
|
94
|
+
- spec/stubs/fake_serial.rb
|
95
|
+
- spec/spec_helper.rb
|
96
|
+
- spec/board_spec.rb
|
97
|
+
- spec/pin_spec.rb
|
98
|
+
- spec/rufirmata_spec.rb
|
99
|
+
- LICENSE
|
100
|
+
- README.rdoc
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://github.com/PlasticLizard/rufirmata
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project: "[none]"
|
131
|
+
rubygems_version: 1.3.7
|
132
|
+
signing_key:
|
133
|
+
specification_version: 3
|
134
|
+
summary: A ruby firmata client for interfacing with microcontollers running firmata compatible firmware
|
135
|
+
test_files: []
|
136
|
+
|