motorcontrolboard 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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +41 -0
- data/Rakefile +2 -0
- data/lib/motorcontrolboard.rb +243 -0
- data/lib/motorcontrolboard/mcb_connection.rb +110 -0
- data/lib/motorcontrolboard/mcb_data.rb +118 -0
- data/lib/motorcontrolboard/version.rb +3 -0
- data/motorcontrolboard.gemspec +19 -0
- metadata +71 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Francesco Sacchi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Motorcontrolboard
|
2
|
+
|
3
|
+
Interface library for MotorControlBoard board
|
4
|
+
http://irawiki.disco.unimib.it/irawiki/index.php/INFIND2011/12_Motor_control_board
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'motorcontrolboard'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install motorcontrolboard
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
#!/usr/bin/env ruby
|
23
|
+
require 'motorcontrolboard'
|
24
|
+
|
25
|
+
#params for serial port
|
26
|
+
initParams = { 'port' => "/dev/ttyUSB0",
|
27
|
+
'baud_rate' => 57600,
|
28
|
+
'vidpid' => '10c4:ea60'}
|
29
|
+
|
30
|
+
@m = MotorControlBoard.new(initParams)
|
31
|
+
@m.initData('init.yaml')
|
32
|
+
@m.connect
|
33
|
+
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
41
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "rubygems"
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
require_relative "motorcontrolboard/mcb_connection.rb"
|
6
|
+
require_relative "motorcontrolboard/mcb_data.rb"
|
7
|
+
|
8
|
+
|
9
|
+
# This class implements an interface to communicate with a Motor Control Board
|
10
|
+
# (http://irawiki.disco.unimib.it/irawiki/index.php/INFIND2011/12_Motor_control_board)
|
11
|
+
#
|
12
|
+
# It uses an internal representation of the commands that can be sent to the board
|
13
|
+
# and also the representation of the memory locations where is possible to write/read
|
14
|
+
# along with the position, the type and the actual value.
|
15
|
+
#
|
16
|
+
# A row in the internal representation, which corresponds to a memory location
|
17
|
+
# in the physical board, is composed by some fields:
|
18
|
+
# * mask_name: a symbol to target the field. Is derived from the mask defined in the serial header file of the firmware. The symbol is obtained by removing the initial MASK_ and making the name lowercase
|
19
|
+
# * position: the position of the memory location, starting from zero
|
20
|
+
# * type: the tipe of the memory location. It can be one among FLSC accoring to the ARRAY::pack documentation
|
21
|
+
# * value: the value of the memory location
|
22
|
+
# * valid: a valid bit. If set, the row will be involved in the next read/write command
|
23
|
+
class MotorControlBoard
|
24
|
+
# Accept as argument an hash specifying #port, #vidpid, and or #baud_rate
|
25
|
+
# #data has to be initialized with the apposite function #initData
|
26
|
+
def initialize(args = {})
|
27
|
+
@port = args['port'] || '/dev/ttyUSB0'
|
28
|
+
@baud_rate = args['baud_rate'] || 57600
|
29
|
+
@vidpid = args['vidpid']
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Return the mask of the valid bits of the internal representation
|
34
|
+
#
|
35
|
+
# The data get sorted but the position is not taken in account so for example if a position is missing a zero will not be added.
|
36
|
+
# The user is responsible to provide consistent data
|
37
|
+
def dataMask
|
38
|
+
sortData()
|
39
|
+
'0b' + @data.inject(''){|mem, ob| ob['valid'].to_s + mem }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the values according to valid bit
|
43
|
+
#
|
44
|
+
# The data get sorted but the position is not taken in account
|
45
|
+
# Values are also packed according to their type
|
46
|
+
def dataValues
|
47
|
+
sortData()
|
48
|
+
@data.select{|data| data['valid']==1}.inject("") {|acc, val| acc << [val['value']].pack(val['type'])}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Set the valid bit according to a given mask
|
52
|
+
def maskToValid(mask)
|
53
|
+
maskToPos(mask).each do |pos|
|
54
|
+
(@data.select{|row| row['position']==pos}.first)['valid']=1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Return a mask according to the names passed as parameters
|
60
|
+
#
|
61
|
+
# The length of the mask will be the max pos + 1 so if there are missing/duplicated positions the mask will be inconsistent
|
62
|
+
def maskFromNames(*names)
|
63
|
+
mask = '0'*(getMaxPos()+1)
|
64
|
+
names.each do |name|
|
65
|
+
mask[positionFromName(name)]='1'
|
66
|
+
end
|
67
|
+
|
68
|
+
mask='0b'+mask.reverse
|
69
|
+
end
|
70
|
+
# Return the position given the name
|
71
|
+
def positionFromName(name)
|
72
|
+
(@data.select{|newData| newData['mask_name']==name}.first)['position']
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return the mask conresponding to the given position
|
76
|
+
def maskByPos(pos)
|
77
|
+
return "0b"+"1"+"0"*pos
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the byte length of a type used by pack/unpack
|
81
|
+
def lenByType(type)
|
82
|
+
case type
|
83
|
+
when 'L', 'F'
|
84
|
+
4
|
85
|
+
when 'S'
|
86
|
+
2
|
87
|
+
when 'C'
|
88
|
+
1
|
89
|
+
else
|
90
|
+
0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return an array of poses given a mask
|
95
|
+
def maskToPos(mask)
|
96
|
+
pos = []
|
97
|
+
mask[2..-1].reverse.each_char.with_index do |e, i|
|
98
|
+
if e=='1'
|
99
|
+
pos << i
|
100
|
+
end
|
101
|
+
end
|
102
|
+
pos
|
103
|
+
end
|
104
|
+
|
105
|
+
# commands
|
106
|
+
def sendCommand(command)
|
107
|
+
startByte()
|
108
|
+
sendC(command)
|
109
|
+
if @echo
|
110
|
+
puts 'sent: ' + command
|
111
|
+
puts 'waiting for return'
|
112
|
+
rec = @sp.getc
|
113
|
+
puts 'received: ' + rec
|
114
|
+
puts 'does they match? ' + (command==rec).to_s
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Send the command set
|
119
|
+
def cmd_set
|
120
|
+
self.sendCommand('S')
|
121
|
+
end
|
122
|
+
# Send the command get
|
123
|
+
def cmd_get
|
124
|
+
self.sendCommand('G')
|
125
|
+
end
|
126
|
+
# Send the command start
|
127
|
+
def cmd_start
|
128
|
+
self.sendCommand('x')
|
129
|
+
end
|
130
|
+
# Send the command gorunning
|
131
|
+
def cmd_gorunning
|
132
|
+
self.sendCommand('a')
|
133
|
+
end
|
134
|
+
# Send the command gocalib
|
135
|
+
def cmd_gocalib
|
136
|
+
self.sendCommand('b')
|
137
|
+
end
|
138
|
+
# Send the command goinit
|
139
|
+
def cmd_goinit
|
140
|
+
self.sendCommand('i')
|
141
|
+
end
|
142
|
+
# Send the command coast
|
143
|
+
def cmd_coast
|
144
|
+
self.sendCommand('j')
|
145
|
+
end
|
146
|
+
# Send the command uncoast
|
147
|
+
def cmd_uncoast
|
148
|
+
self.sendCommand('u')
|
149
|
+
end
|
150
|
+
# Send the command reset
|
151
|
+
def cmd_reset
|
152
|
+
self.sendCommand('r')
|
153
|
+
end
|
154
|
+
# Send the command whois and return the result
|
155
|
+
def cmd_whois
|
156
|
+
self.sendCommand('w')
|
157
|
+
who = ""
|
158
|
+
while (char = @sp.getc) != "\u0000"
|
159
|
+
who << char
|
160
|
+
end
|
161
|
+
who
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
# Set data according to valid bit. The value saved in the internal representation will be set
|
166
|
+
def setByValid()
|
167
|
+
mask = dataMask()
|
168
|
+
values = dataValues()
|
169
|
+
cmd_set()
|
170
|
+
sleep 0.1
|
171
|
+
self.sendS([Integer(mask)].pack('L'))
|
172
|
+
sleep 0.1
|
173
|
+
self.sendS(values)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Send single data by name
|
177
|
+
#
|
178
|
+
# Params:
|
179
|
+
# +name+:: The symbol matching the name of the mask
|
180
|
+
# +val+:: The value to assign
|
181
|
+
def setByName(name, val)
|
182
|
+
dataResetValid
|
183
|
+
maskToValid(maskFromNames(name))
|
184
|
+
(@data.select {|row| row['mask_name']==name}.first)['value']=val
|
185
|
+
setByValid()
|
186
|
+
end
|
187
|
+
|
188
|
+
# yet to be implemented.. is this useful?
|
189
|
+
def setByPos(pos, val)
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
# Read data at the given position
|
194
|
+
def getSingleData(pos)
|
195
|
+
getByMask(maskByPos(pos))
|
196
|
+
end
|
197
|
+
|
198
|
+
# Read data according to the given names
|
199
|
+
def getByNames(*names)
|
200
|
+
getByMask(maskFromNames(*names))
|
201
|
+
end
|
202
|
+
|
203
|
+
# Read data according to the given mask
|
204
|
+
def getByMask(mask)
|
205
|
+
dataResetValid
|
206
|
+
maskToValid(mask)
|
207
|
+
getByValid
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get data according to the valid bit
|
211
|
+
def getByValid()
|
212
|
+
mask = dataMask()
|
213
|
+
if (Integer(mask)!=0)
|
214
|
+
@data = @data.sort_by { |row| row['position'] }
|
215
|
+
len = validDataLength
|
216
|
+
cmd_get()
|
217
|
+
sleep 0.1
|
218
|
+
self.sendS([Integer(mask)].pack('L'))
|
219
|
+
result = []
|
220
|
+
readBytes = []
|
221
|
+
begin
|
222
|
+
Timeout::timeout(1) do
|
223
|
+
len.times {readBytes << @spr.getc}
|
224
|
+
readBytes.reverse!
|
225
|
+
|
226
|
+
@data.select{|data| data['valid']==1}.each do |row| #do we need to revert this??
|
227
|
+
data=""
|
228
|
+
lenByType(row['type']).times do
|
229
|
+
data << readBytes.pop
|
230
|
+
end
|
231
|
+
value = data.unpack(row['type']).first
|
232
|
+
row['value'] = value
|
233
|
+
result << {'mask_name'=>row['mask_name'], 'value' => value}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
rescue
|
237
|
+
puts 'Timeout to read with mask ' + mask
|
238
|
+
puts 'Read ' + readBytes.length.to_s + '/' + len.to_s + ' bytes'
|
239
|
+
end
|
240
|
+
return result
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "serialport"
|
3
|
+
class MotorControlBoard
|
4
|
+
# Descriptor of the vid/pid of the board. Something like '10c4:ea60'.
|
5
|
+
# Can be obtained through a command like 'lsusb'.
|
6
|
+
#
|
7
|
+
# Setting this variable allow to discover the port where the board is conencted automatically
|
8
|
+
attr_accessor :vidpid
|
9
|
+
# String describing the port where the board is connected.
|
10
|
+
# Usually is something like '/dev/ttyUSB0'. Can be auto-discovered, setting the #vidpid variable if the vid:pid of the board is known
|
11
|
+
attr_accessor :port
|
12
|
+
# Baud rate of the connection. In the actual firmware version it should be 57600
|
13
|
+
attr_accessor :baud_rate
|
14
|
+
# attr_reader :sp
|
15
|
+
|
16
|
+
# Shortcut to set a '/dev/ttyUSBx' port. It also connects to that port
|
17
|
+
#
|
18
|
+
# Params:
|
19
|
+
# +num+:: number of the ttyUSB port to be set
|
20
|
+
def setPort(num)
|
21
|
+
@port = '/dev/ttyUSB' + num.to_s
|
22
|
+
connect
|
23
|
+
end
|
24
|
+
|
25
|
+
# Connect to serial port specified by @port member
|
26
|
+
#
|
27
|
+
# If @vidpid is set, it checks if that device is connected to a serial port and that port is chosen, regardless of the @port value.
|
28
|
+
# If block given it closes the connection at the end of the block
|
29
|
+
def connect
|
30
|
+
if findPort()
|
31
|
+
puts "Automatically selected port #{@port}"
|
32
|
+
end
|
33
|
+
data_bits = 8
|
34
|
+
stop_bits = 1
|
35
|
+
parity = SerialPort::NONE
|
36
|
+
begin
|
37
|
+
@sp = SerialPort.new(@port, @baud_rate, data_bits, stop_bits, parity)
|
38
|
+
@spr = SerialPort.new(@port, @baud_rate, data_bits, stop_bits, parity)
|
39
|
+
@open = true
|
40
|
+
|
41
|
+
rescue
|
42
|
+
puts 'ERROR: Unable to find serial port ' + @port
|
43
|
+
@open = false
|
44
|
+
end
|
45
|
+
|
46
|
+
if block_given?
|
47
|
+
yield
|
48
|
+
self.disconnect
|
49
|
+
p "port closed"
|
50
|
+
end
|
51
|
+
@open
|
52
|
+
end
|
53
|
+
|
54
|
+
# Disconnect from serial port
|
55
|
+
def disconnect
|
56
|
+
if @open
|
57
|
+
@open = false
|
58
|
+
@sp.close
|
59
|
+
@spr.close
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Find the port to which the board is connected, looking for the specified @vidpid
|
64
|
+
def findPort
|
65
|
+
begin
|
66
|
+
if @vidpid
|
67
|
+
busNumber = Integer(`lsusb|grep #{@vidpid}`[4..6])
|
68
|
+
port = `ls /sys/bus/usb-serial/devices/ -ltrah |grep usb#{busNumber}`.chop
|
69
|
+
@port = '/dev/'+port.gsub(/ttyUSB[0-9]+/).first
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
rescue
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Send a single char
|
79
|
+
def sendC(char)
|
80
|
+
if (!@open)
|
81
|
+
connect()
|
82
|
+
end
|
83
|
+
@sp.putc char.chr
|
84
|
+
end
|
85
|
+
|
86
|
+
# Send a string of char
|
87
|
+
def sendS(string)
|
88
|
+
string.each_char do |char|
|
89
|
+
sendC(char)
|
90
|
+
end
|
91
|
+
# puts 'sent: ' + string
|
92
|
+
# puts 'waiting for return'
|
93
|
+
# rec = ''
|
94
|
+
# string.length.times {rec << @sp.getc}
|
95
|
+
# puts 'received: ' + rec
|
96
|
+
# match = (string.eql? rec.force_encoding('ASCII-8BIT'))
|
97
|
+
# puts 'equal: ' + match.to_s
|
98
|
+
# if !match
|
99
|
+
# string.length.times.with_index do |i|
|
100
|
+
# puts string[i].ord + ' ' + rec[i].force_encoding('ASCII-8BIT').ord + ' ' + (string[i].encode('ASCII-8BIT') == rec[i].force_encoding('ASCII-8BIT')).to_s
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
def startByte()
|
107
|
+
sendC(0x55)
|
108
|
+
sleep 0.1
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
class MotorControlBoard
|
5
|
+
|
6
|
+
# Internal representation of the board memory, as array of hashes
|
7
|
+
attr_reader :data
|
8
|
+
|
9
|
+
# Init the internal representation loading the given file.
|
10
|
+
#
|
11
|
+
# Params:
|
12
|
+
# +initFile+:: the path to a yaml file containing the data structure to be used.
|
13
|
+
# The file should contain at least mask_name, position and type.
|
14
|
+
# Also if value and valid are present they will be set to 0.
|
15
|
+
# Other fields are permitted although they will not be used:
|
16
|
+
# this functionality is not tested.
|
17
|
+
#
|
18
|
+
# Data will be sorted by position
|
19
|
+
def initData(initFile)
|
20
|
+
@data = []
|
21
|
+
initData = YAML.load_file(initFile)
|
22
|
+
initData.each do |datum|
|
23
|
+
datum['value']=0
|
24
|
+
datum['valid']=0
|
25
|
+
end
|
26
|
+
@data = initData
|
27
|
+
sortData()
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return internal data in yaml format
|
31
|
+
def saveData
|
32
|
+
YAML.dump @data
|
33
|
+
end
|
34
|
+
|
35
|
+
# Dump internal data to yaml format and save to file
|
36
|
+
# Params:
|
37
|
+
# +path+:: path where to save the yaml data
|
38
|
+
def saveDataToFile(path)
|
39
|
+
File.open(path, 'w') do |f|
|
40
|
+
YAML.dump(@data, f)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the index of the matching row diven some search params in hash format
|
45
|
+
#
|
46
|
+
# Params:
|
47
|
+
# +needle+:: the hash to search. Search is performed by mask_name, position and type.
|
48
|
+
#
|
49
|
+
# If the result contains one row only, its index is returned, otherwise nil is returned
|
50
|
+
def findData(needle)
|
51
|
+
result = @data
|
52
|
+
result = result.select { |a| a['mask_name']==needle['mask_name']} if needle['mask_name'] != nil
|
53
|
+
result = result.select { |a| a['position']==needle['position']} if needle['position'] != nil
|
54
|
+
result = result.select { |a| a['type']==needle['type']} if needle['type'] != nil
|
55
|
+
if result.length == 1
|
56
|
+
@data.index result.first
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add data to internal representation
|
63
|
+
#
|
64
|
+
# Params:
|
65
|
+
# +newData+:: data to add in array of hashes format
|
66
|
+
# If the valid field is present, only rows with valid==1 will be taken into account.
|
67
|
+
# The matching row, selected with the #findData method, is updated with the new value and the valid bit is set
|
68
|
+
def addData(newData)
|
69
|
+
if newData[0]['valid'] != nil
|
70
|
+
newData = newData.select{|newData| newData['valid']==1}
|
71
|
+
end
|
72
|
+
newData.each do |newValidDatum|
|
73
|
+
if (index = findData(newValidDatum))
|
74
|
+
@data[index]['value'] = newValidDatum['value']
|
75
|
+
@data[index]['valid'] = 1
|
76
|
+
else
|
77
|
+
puts 'Unable to find entry matching mask_name and position'
|
78
|
+
puts newValidDatum
|
79
|
+
end
|
80
|
+
end
|
81
|
+
sortData()
|
82
|
+
end
|
83
|
+
# Load data from a yaml representation
|
84
|
+
#
|
85
|
+
# #addData is called so that everything is stored in the internal state
|
86
|
+
def loadData(dataToLoad)
|
87
|
+
addData(YAML.load(dataToLoad))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Load a yaml file and saves to state with #addData
|
91
|
+
def loadDataFromFile(path)
|
92
|
+
addData(YAML.load_file(path))
|
93
|
+
end
|
94
|
+
|
95
|
+
# Set valid bit to 0 for each row
|
96
|
+
def dataResetValid
|
97
|
+
@data.each {|row| row['valid']=0}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set valid bit to 1 for each row
|
101
|
+
def dataSetValid
|
102
|
+
@data.each {|row| row['valid']=1}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Sort data by position
|
106
|
+
def sortData
|
107
|
+
@data = @data.sort_by { |row| row['position'] }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return the maximum value of the position field among all data
|
111
|
+
def getMaxPos
|
112
|
+
@data.max_by {|row| row['position']}['position']
|
113
|
+
end
|
114
|
+
|
115
|
+
def validDataLength
|
116
|
+
@data.select{|row| row['valid']==1}.inject(0){|sum, row| sum+lenByType(row['type'])}
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/motorcontrolboard/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Francesco Sacchi"]
|
6
|
+
gem.email = ["depsir@gmail.com"]
|
7
|
+
gem.description = %q{Interface library for MotorControlBoard board}
|
8
|
+
gem.summary = %q{}
|
9
|
+
gem.homepage = "http://irawiki.disco.unimib.it/irawiki/index.php/INFIND2011/12_Motor_control_board"
|
10
|
+
|
11
|
+
gem.add_dependency('serialport', '>= 1.1.0')
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "motorcontrolboard"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = Motorcontrolboard::VERSION
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: motorcontrolboard
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Francesco Sacchi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: serialport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.0
|
30
|
+
description: Interface library for MotorControlBoard board
|
31
|
+
email:
|
32
|
+
- depsir@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- lib/motorcontrolboard.rb
|
43
|
+
- lib/motorcontrolboard/mcb_connection.rb
|
44
|
+
- lib/motorcontrolboard/mcb_data.rb
|
45
|
+
- lib/motorcontrolboard/version.rb
|
46
|
+
- motorcontrolboard.gemspec
|
47
|
+
homepage: http://irawiki.disco.unimib.it/irawiki/index.php/INFIND2011/12_Motor_control_board
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.24
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: ''
|
71
|
+
test_files: []
|