Class: MotorControlBoard

Inherits:
Object
  • Object
show all
Defined in:
lib/motorcontrolboard.rb,
lib/motorcontrolboard/mcb_data.rb,
lib/motorcontrolboard/mcb_connection.rb

Overview

This class implements an interface to communicate with a Motor Control Board (irawiki.disco.unimib.it/irawiki/index.php/INFIND2011/12_Motor_control_board)

It uses an internal representation of the commands that can be sent to the board and also the representation of the memory locations where is possible to write/read along with the position, the type and the actual value.

A row in the internal representation, which corresponds to a memory location in the physical board, is composed by some fields:

  • 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

  • position: the position of the memory location, starting from zero

  • type: the tipe of the memory location. It can be one among FLSC accoring to the ARRAY::pack documentation

  • value: the value of the memory location

  • valid: a valid bit. If set, the row will be involved in the next read/write command

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ MotorControlBoard

Accept as argument an hash specifying #port, #vidpid, and or #baud_rate #data has to be initialized with the apposite function #initData



26
27
28
29
30
# File 'lib/motorcontrolboard.rb', line 26

def initialize(args = {})
    @port = args['port'] || '/dev/ttyUSB0'
    @baud_rate = args['baud_rate'] || 57600
    @vidpid = args['vidpid']
end

Instance Attribute Details

#baud_rateObject

Baud rate of the connection. In the actual firmware version it should be 57600



13
14
15
# File 'lib/motorcontrolboard/mcb_connection.rb', line 13

def baud_rate
  @baud_rate
end

#dataObject (readonly)

Internal representation of the board memory, as array of hashes



7
8
9
# File 'lib/motorcontrolboard/mcb_data.rb', line 7

def data
  @data
end

#portObject

String describing the port where the board is connected. Usually is something like ‘/dev/ttyUSB0’. Can be auto-discovered, setting the #vidpid variable if the vid:pid of the board is known



11
12
13
# File 'lib/motorcontrolboard/mcb_connection.rb', line 11

def port
  @port
end

#vidpidObject

Can be obtained through a command like ‘lsusb’.

Setting this variable allow to discover the port where the board is conencted automatically



8
9
10
# File 'lib/motorcontrolboard/mcb_connection.rb', line 8

def vidpid
  @vidpid
end

Instance Method Details

#addData(newData) ⇒ Object

Add data to internal representation

Params:

newData

data to add in array of hashes format

If the valid field is present, only rows with valid==1 will be taken into account. The matching row, selected with the #findData method, is updated with the new value and the valid bit is set



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/motorcontrolboard/mcb_data.rb', line 68

def addData(newData)
    if newData[0]['valid'] != nil 
        newData = newData.select{|newData| newData['valid']==1}
    end
    newData.each do |newValidDatum|
        if (index = findData(newValidDatum))
            @data[index]['value'] = newValidDatum['value']
            @data[index]['valid'] = 1
        else
            puts 'Unable to find entry matching mask_name and position'
            puts newValidDatum
        end
    end
    sortData()
end

#cmd_coastObject

Send the command coast



143
144
145
# File 'lib/motorcontrolboard.rb', line 143

def cmd_coast
    self.sendCommand('j')
end

#cmd_getObject

Send the command get



123
124
125
# File 'lib/motorcontrolboard.rb', line 123

def cmd_get
    self.sendCommand('G')
end

#cmd_gocalibObject

Send the command gocalib



135
136
137
# File 'lib/motorcontrolboard.rb', line 135

def cmd_gocalib
    self.sendCommand('b')
end

#cmd_goinitObject

Send the command goinit



139
140
141
# File 'lib/motorcontrolboard.rb', line 139

def cmd_goinit
    self.sendCommand('i')
end

#cmd_gorunningObject

Send the command gorunning



131
132
133
# File 'lib/motorcontrolboard.rb', line 131

def cmd_gorunning
    self.sendCommand('a')
end

#cmd_resetObject

Send the command reset



151
152
153
# File 'lib/motorcontrolboard.rb', line 151

def cmd_reset
    self.sendCommand('r')
end

#cmd_setObject

Send the command set



119
120
121
# File 'lib/motorcontrolboard.rb', line 119

def cmd_set
    self.sendCommand('S')
end

#cmd_startObject

Send the command start



127
128
129
# File 'lib/motorcontrolboard.rb', line 127

def cmd_start
    self.sendCommand('x')
end

#cmd_uncoastObject

Send the command uncoast



147
148
149
# File 'lib/motorcontrolboard.rb', line 147

def cmd_uncoast
    self.sendCommand('u')
end

#cmd_whoisObject

Send the command whois and return the result



155
156
157
158
159
160
161
162
# File 'lib/motorcontrolboard.rb', line 155

def cmd_whois
    self.sendCommand('w')
    who = ""
    while (char = @sp.getc) != "\u0000"
        who << char
    end
    who
end

#connectObject

Connect to serial port specified by @port member

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. If block given it closes the connection at the end of the block



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/motorcontrolboard/mcb_connection.rb', line 29

def connect
	if findPort()
		puts "Automatically selected port #{@port}"
	end
    data_bits = 8
    stop_bits = 1
    parity = SerialPort::NONE
    begin
        @sp = SerialPort.new(@port, @baud_rate, data_bits, stop_bits, parity)
        @open = true

    rescue
        puts 'ERROR: Unable to find serial port ' + @port
        @open = false
    end

    if block_given?
        yield
        self.disconnect
        p "port closed"
    end
    @open
end

#dataMaskObject

Return the mask of the valid bits of the internal representation

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. The user is responsible to provide consistent data



37
38
39
40
# File 'lib/motorcontrolboard.rb', line 37

def dataMask
    sortData()
    '0b' + @data.inject(''){|mem, ob| ob['valid'].to_s + mem    }
end

#dataResetValidObject

Set valid bit to 0 for each row



96
97
98
# File 'lib/motorcontrolboard/mcb_data.rb', line 96

def dataResetValid
    @data.each {|row| row['valid']=0}
end

#dataSetValidObject

Set valid bit to 1 for each row



101
102
103
# File 'lib/motorcontrolboard/mcb_data.rb', line 101

def dataSetValid
    @data.each {|row| row['valid']=1}
end

#dataValuesObject

Return the values according to valid bit

The data get sorted but the position is not taken in account Values are also packed according to their type



46
47
48
49
# File 'lib/motorcontrolboard.rb', line 46

def dataValues
    sortData()
    @data.select{|data| data['valid']==1}.inject("") {|acc, val| acc << [val['value']].pack(val['type'])}
end

#disconnectObject

Disconnect from serial port



54
55
56
57
58
59
# File 'lib/motorcontrolboard/mcb_connection.rb', line 54

def disconnect
    if @open
        @open = false
        @sp.close
    end
end

#findData(needle) ⇒ Object

Return the index of the matching row diven some search params in hash format

Params:

needle

the hash to search. Search is performed by mask_name, position and type.

If the result contains one row only, its index is returned, otherwise nil is returned



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/motorcontrolboard/mcb_data.rb', line 50

def findData(needle)
    result = @data
    result = result.select { |a| a['mask_name']==needle['mask_name']} if needle['mask_name'] != nil
    result = result.select { |a| a['position']==needle['position']} if needle['position'] != nil
    result = result.select { |a| a['type']==needle['type']} if needle['type'] != nil
    if result.length == 1        
        @data.index result.first
    else
        nil
    end
end

#findPortObject

Find the port to which the board is connected, looking for the specified @vidpid



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/motorcontrolboard/mcb_connection.rb', line 62

def findPort
	begin
		if @vidpid
  	busNumber = Integer(`lsusb|grep #{@vidpid}`[4..6])
  	port = `ls /sys/bus/usb-serial/devices/ -ltrah |grep usb#{busNumber}`.chop
  	@port = '/dev/'+port.gsub(/ttyUSB[0-9]+/).first
  else
  	false
  end
 rescue
 	false
 end
end

#getByMask(mask) ⇒ Object

Read data according to the given mask



204
205
206
207
208
# File 'lib/motorcontrolboard.rb', line 204

def getByMask(mask)
    dataResetValid
    maskToValid(mask)
    getByValid
end

#getByNames(*names) ⇒ Object

Read data according to the given names



199
200
201
# File 'lib/motorcontrolboard.rb', line 199

def getByNames(*names)
    getByMask(maskFromNames(*names))
end

#getByValidObject

Get data according to the valid bit



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/motorcontrolboard.rb', line 211

def getByValid()
    mask = dataMask()
    if (Integer(mask)!=0) 
        @data = @data.sort_by { |row| row['position'] }
        len = validDataLength
        cmd_get()
        sleep 0.1
        self.sendS([Integer(mask)].pack('L'))
        result = []
        readBytes = []
        begin
            Timeout::timeout(1) do
                len.times {readBytes << @sp.getbyte}
                readBytes.reverse!

                @data.select{|data| data['valid']==1}.each do |row| #do we need to revert this??
                    data=""
                    lenByType(row['type']).times do
                        data << readBytes.pop
                    end
                    value = data.unpack(row['type']).first
                    row['value'] = value
                    result << {'mask_name'=>row['mask_name'], 'value' => value}
                end
            end
        rescue
            puts 'Timeout to read with mask ' + mask
            puts 'Read ' + readBytes.length.to_s + '/' + len.to_s + ' bytes'
            puts 'READ:' + readBytes.to_s
        end
        return result
    end
end

#getMaxPosObject

Return the maximum value of the position field among all data



111
112
113
# File 'lib/motorcontrolboard/mcb_data.rb', line 111

def getMaxPos
    @data.max_by {|row| row['position']}['position']
end

#getSingleData(pos) ⇒ Object

Read data at the given position



194
195
196
# File 'lib/motorcontrolboard.rb', line 194

def getSingleData(pos)
    getByMask(maskByPos(pos))
end

#initData(initFile) ⇒ Object

Init the internal representation loading the given file.

Params:

initFile

the path to a yaml file containing the data structure to be used.

The file should contain at least mask_name, position and type. Also if value and valid are present they will be set to 0. Other fields are permitted although they will not be used: this functionality is not tested.

Data will be sorted by position



19
20
21
22
23
24
25
26
27
28
# File 'lib/motorcontrolboard/mcb_data.rb', line 19

def initData(initFile)
    @data = []
    initData = YAML.load_file(initFile)
    initData.each do |datum|
        datum['value']=0
        datum['valid']=0
    end
    @data = initData
    sortData()
end

#lenByType(type) ⇒ Object

Return the byte length of a type used by pack/unpack



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/motorcontrolboard.rb', line 81

def lenByType(type)
    case type
    when 'L', 'F'
        4
    when 'S'
        2
    when 'C'
        1
    else
        0
    end
end

#loadData(dataToLoad) ⇒ Object

Load data from a yaml representation

#addData is called so that everything is stored in the internal state



86
87
88
# File 'lib/motorcontrolboard/mcb_data.rb', line 86

def loadData(dataToLoad)
    addData(YAML.load(dataToLoad))
end

#loadDataFromFile(path) ⇒ Object

Load a yaml file and saves to state with #addData



91
92
93
# File 'lib/motorcontrolboard/mcb_data.rb', line 91

def loadDataFromFile(path)
    addData(YAML.load_file(path))
end

#maskByPos(pos) ⇒ Object

Return the mask conresponding to the given position



76
77
78
# File 'lib/motorcontrolboard.rb', line 76

def maskByPos(pos)
    return "0b"+"1"+"0"*pos
end

#maskFromNames(*names) ⇒ Object

Return a mask according to the names passed as parameters

The length of the mask will be the max pos + 1 so if there are missing/duplicated positions the mask will be inconsistent



62
63
64
65
66
67
68
69
# File 'lib/motorcontrolboard.rb', line 62

def maskFromNames(*names)
    mask = '0'*(getMaxPos()+1)
    names.each do |name|
        mask[positionFromName(name)]='1'
    end

    mask='0b'+mask.reverse
end

#maskToPos(mask) ⇒ Object

Return an array of poses given a mask



95
96
97
98
99
100
101
102
103
# File 'lib/motorcontrolboard.rb', line 95

def maskToPos(mask)
    pos = []
    mask[2..-1].reverse.each_char.with_index do |e, i| 
        if e=='1' 
            pos << i
         end
     end
    pos
end

#maskToValid(mask) ⇒ Object

Set the valid bit according to a given mask



52
53
54
55
56
# File 'lib/motorcontrolboard.rb', line 52

def maskToValid(mask)
    maskToPos(mask).each do |pos|
        (@data.select{|row| row['position']==pos}.first)['valid']=1
    end
end

#positionFromName(name) ⇒ Object

Return the position given the name



71
72
73
# File 'lib/motorcontrolboard.rb', line 71

def positionFromName(name)
    (@data.select{|newData| newData['mask_name']==name}.first)['position']
end

#saveDataObject

Return internal data in yaml format



31
32
33
# File 'lib/motorcontrolboard/mcb_data.rb', line 31

def saveData
    YAML.dump @data
end

#saveDataToFile(path) ⇒ Object

Dump internal data to yaml format and save to file Params:

path

path where to save the yaml data



38
39
40
41
42
# File 'lib/motorcontrolboard/mcb_data.rb', line 38

def saveDataToFile(path)
    File.open(path, 'w') do |f|
        YAML.dump(@data, f)
    end
end

#sendC(char) ⇒ Object

Send a single char



77
78
79
80
81
82
# File 'lib/motorcontrolboard/mcb_connection.rb', line 77

def sendC(char)
    if (!@open)
        connect()
    end
    @sp.putc char.chr
end

#sendCommand(command) ⇒ Object

commands



106
107
108
109
110
111
112
113
114
115
116
# File 'lib/motorcontrolboard.rb', line 106

def sendCommand(command)
    startByte()
    sendC(command)
    if @echo
        puts 'sent: ' + command
        puts 'waiting for return'
        rec = @sp.getc
        puts 'received: ' + rec
        puts 'does they match? ' + (command==rec).to_s
    end
end

#sendS(string) ⇒ Object

Send a string of char



85
86
87
88
89
# File 'lib/motorcontrolboard/mcb_connection.rb', line 85

def sendS(string)
    string.each_char do |char| 
        sendC(char) 
    end
end

#setByName(name, val) ⇒ Object

Send single data by name

Params:

name

The symbol matching the name of the mask

val

The value to assign



181
182
183
184
185
186
# File 'lib/motorcontrolboard.rb', line 181

def setByName(name, val)
    dataResetValid
    maskToValid(maskFromNames(name))
    (@data.select {|row| row['mask_name']==name}.first)['value']=val
    setByValid()
end

#setByPos(pos, val) ⇒ Object

yet to be implemented.. is this useful?



189
190
# File 'lib/motorcontrolboard.rb', line 189

def setByPos(pos, val)
end

#setByValidObject

Set data according to valid bit. The value saved in the internal representation will be set



166
167
168
169
170
171
172
173
174
# File 'lib/motorcontrolboard.rb', line 166

def setByValid()
    mask = dataMask()
    values = dataValues()
    cmd_set()
    sleep 0.1
    self.sendS([Integer(mask)].pack('L'))
    sleep 0.1
    self.sendS(values)
end

#setPort(num) ⇒ Object

Shortcut to set a ‘/dev/ttyUSBx’ port. It also connects to that port

Params:

num

number of the ttyUSB port to be set



20
21
22
23
# File 'lib/motorcontrolboard/mcb_connection.rb', line 20

def setPort(num)
    @port = '/dev/ttyUSB' + num.to_s
    connect
end

#sortDataObject

Sort data by position



106
107
108
# File 'lib/motorcontrolboard/mcb_data.rb', line 106

def sortData
    @data = @data.sort_by { |row| row['position'] }
end

#startByteObject



91
92
93
94
# File 'lib/motorcontrolboard/mcb_connection.rb', line 91

def startByte()
    sendC(0x55)
    sleep 0.1
end

#validDataLengthObject



115
116
117
# File 'lib/motorcontrolboard/mcb_data.rb', line 115

def validDataLength
    @data.select{|row| row['valid']==1}.inject(0){|sum, row| sum+lenByType(row['type'])}
end