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 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: []