rufirmata 0.0.1
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.
- 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
|
+
|