motorcontrolboard 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in motorcontrolboard.gemspec
4
+ gemspec
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,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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,3 @@
1
+ module Motorcontrolboard
2
+ VERSION = "0.0.1"
3
+ 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: []