Controllare siBOT dalla piattaforma HBR
Fiorella Sibona • 14th February, 2018 • read in 3 mins

In questo tutorial vedremo come collegare il manipolatore antropomorfo, siBOT, alla piattaforma HBR.
siBOT è l'insieme del design EEZYBOT MK2, progetto italino Open Source, e del sistema di nodi ROS necessari a controllarlo. Ho utilizzato questo braccio per testare l'architettura sviluppata per il mio progetto di tesi, NTBD, di cui trovate maggiori informazioni in questo [post]({{ site.baseurl }}{% post_url /it/blog/2018-01-17-ntbd-guide-part-I %}).
1. Ingredienti
Ci serviranno:
- 1 braccio siBOT (braccio EEZYBOT MK2 + nodo ROS Arduino)
- 1 Raspberry Pi con l'immagine HBrain sull'SD
2. Controllo nello spazio dei motori
2.1 Generatore di valori servo desiderati - Sketch
Create un nuovo sketch e chiamatelo motors_generator_sibot. Il contenuto dovrà essere il seguente:
import dotbot_rosfrom std_msgs.msg import Int16MultiArrayimport timeclass Node(dotbot_ros.DotbotNode):node_name = 'motors_generator'def setup(self):self.pub = dotbot_ros.Publisher('motors_nointerp', Int16MultiArray)self.gripper = 25motors_list = [90, 90, 90, 180, 100, 30, 0, 140, 60]self.gripp_list = ['open','closed','open']n = 3self.m_list = [motors_list[i:i+n] for i in range(0, len(motors_list), n)]self.loop_rate = dotbot_ros.Rate(0.33)def loop(self):for indx, motors_seq in enumerate(self.m_list):seq = Int16MultiArray()if self.gripp_list[indx] == 'open':self.gripper = 100else:self.gripper = 25seq.data = [m for m in motors_seq, self.gripper]self.pub.publish(seq)time.sleep(3)
2.2 Generatore di valori servo desiderati - Sketch con commenti
Qui di seguito riporto il codice commentato. Come potete vedere, la struttura è quella utilizzata finora per i nodi ROS su HBrain: vengono definite le funzioni setup() e loop(), con l'aggiunta di una funzione di callback.
import dotbot_rosfrom std_msgs.msg import Int16MultiArrayimport timeclass Node(dotbot_ros.DotbotNode):node_name = 'motors_generator'def setup(self):# Definiamo un publisherper pubblicare le sequenze di motoriself.pub = dotbot_ros.Publisher('motors_nointerp', Int16MultiArray)# Inizializziamo il valore del gripper a chiusoself.gripper = 25# Sequenza di angoli desiderati (da leggere 3 alla volta)motors_list = [90, 90, 90, 180, 100, 30, 0, 140, 60]# Sequenza di configurazioni desiderate per il gripperself.gripp_list = ['open','closed','open']# Come detto, prendiamo i valori in gruppi da n elementin = 3# Il seguente codice ritorna una lista di liste da 3 elementi:# [[motor_seq1], [motor_seq2],...[motor_seqn]]self.m_list = [motors_list[i:i+n] for i in range(0, len(motors_list), n)]self.loop_rate = dotbot_ros.Rate(0.33)def loop(self):# Durante il ciclo for nella lista di liste, salvo anche l'indice# di ogni elemento così da poter ciclare nella lista di valori per il gripperfor indx, motors_seq in enumerate(self.m_list):seq = Int16MultiArray()if self.gripp_list[indx] == 'open':self.gripper = 100else:self.gripper = 25seq.data = [m for m in motors_seq, self.gripper]# Pubblichiamo sul topic definito da pub la sequenza di motori,# unione degli angoli dei miniservo dei giunti e il valore per il gripper.self.pub.publish(seq)time.sleep(3)
3. Controllo della posizione dell'End Effector
Abbiamo visto come controllare i valori dei motori di siBOT, ma spesso nella robotica industriale l'obbiettivo è controllare la posizione e l'orientamento dell'End Effector (EE) che in questo caso corrisponde al giunto della pinza (gripper).
3.1 Generatore di posizioni EE - Sketch
Create un nuovo sketch e chiamatelo positions_generator_sibot. Il contenuto dovrà essere il seguente:
import dotbot_rosfrom geometry_msgs.msg import Pointfrom std_msgs.msg import Stringimport timeclass Node(dotbot_ros.DotbotNode):node_name = 'position_generator'def setup(self):self.pub = dotbot_ros.Publisher('desired_position_nointerp', Point)self.pubG = dotbot_ros.Publisher('gripper_value', String)desPos = [150, 0, 235, 130, 50, 140, 130, -30, 90]self.gripp_list = ['open','closed','open']n = 3self.desP = [desPos[i:i+n] for i in range(0, len(desPos), n)]self.loop_rate = dotbot_ros.Rate(0.33)def loop(self):for indx,pos in enumerate(self.desP):self.pubG.publish(self.gripp_list[indx])des_pos = Point(*pos)self.pub.publish(des_pos)time.sleep(3)
3.2 Generatore di posizioni EE desiderate - Sketch con commenti
Come potete notare, nel caso delle posizioni desiderate non abbiamo la funzione di callback all'arrivo di messaggi sul topic in cui viene specificato lo stato della pinza (gripper_value): questa funzione di callback verrà infatti definita nel nodo per la cinematica inversa in cui vengono pubblicati i valori dei servo-motori insieme al valore per il gripper sull'apposito topic.
import dotbot_rosfrom geometry_msgs.msg import Pointimport timeclass Node(dotbot_ros.DotbotNode):node_name = 'position_generator'def setup(self):# Definiamo un publisher per pubblicare la sequenza di posizioni# per l'EE ed uno per il valore del gripperself.pub = dotbot_ros.Publisher('desired_position_nointerp', Point)self.pubG = dotbot_ros.Publisher('gripper_value', String)# Posizioni in coordinate cartesiane espresse in millimetri# (da leggere [x1,y1,z1,x2,y2,z2...xn,yn,zn])desPos = [150, 0, 235, 130, 50, 140, 130, -30, 90]# Sequenza di configurazioni desiderate per il gripperself.gripp_list = ['open','closed','open']# Come detto, prendiamo i valori in gruppi da n elementin = 3# Il seguente codice ritorna una lista di liste da 3 elementi. Ogni lista# è una posizione in coordinate Cartesiane:# [[pos1], [pos2],...[posn]]self.desP = [desPos[i:i+n] for i in range(0, len(desPos), n)]self.loop_rate = dotbot_ros.Rate(0.33)def loop(self):# Durante il ciclo for nella lista di liste, salvo anche l'indice# di ogni elemento così da poter ciclare nella lista di valori per il gripperfor indx,pos in enumerate(self.desP):# Pubblichiamo sul topic gripper_value la stringa per definire se la pinza# debba essere aperta o chiusa in quella configurazioneself.pubG.publish(self.gripp_list[indx])des_pos = Point(*pos)# Pubblichiamo sul topic definito da pub la posizione desiderata, come messaggio Pointself.pub.publish(des_pos)time.sleep(5)
3.3 Nodo di cinematica inversa - Sketch
Una volta che le posizioni desiderate verranno pubblicate, sarà necessario convertirle nei valori dei servo corrispondenti. Esistono molte soluzioni a seconda della complessità del problema (per esempio i gradi di libertà, Degrees Of Freedom) ma quella calcolata da me è di tipo geometrico, ancora possibile visti i 3 DOF del braccio. Create un nuovo sketch e chiamatelo IK_sibot. Il contenuto dovrà essere il seguente:
import dotbot_rosimport mathfrom std_msgs.msg import Int16MultiArrayfrom std_msgs.msg import Stringfrom geometry_msgs.msg import Pointfrom sys import stdoutclass Node(dotbot_ros.DotbotNode):node_name = 'IK'def setup(self):dotbot_ros.Subscriber("desired_position", Point, self.callback)dotbot_ros.Subscriber("gripper_value", String, self.gripper_callback)self.pub = dotbot_ros.Publisher('motors', Int16MultiArray)self.gripper = 25def hipo(self,x,y):return math.sqrt(x*x + y*y)def lawOfCosines(self,a,b,c):rate = (a*a + b*b - c*c) / (2 * a * b)if abs(rate) > 1:if max(rate,0) == 0:rate = -1if max(rate,0) == rate:rate = 1return math.acos(rate)def deg(self,rad):return rad * 180 / math.pidef gripper_callback(self, msg):if msg.data == "open":self.gripper = 100else:self.gripper = 25def callback(self,data):L0 = 50L1 = 35L2 = 150L3 = 150cartP = {'xEE':data.x, 'yEE': data.y, 'zEE': data.z}L = L0 + L1cylP = {'theta': math.atan(cartP['yEE']/cartP['xEE']), 'r':self.hipo(cartP['xEE'], cartP['yEE']), 'zhat':cartP['zEE']-L}zhat = cylP['zhat']rho = self.hipo(cylP['r'], zhat)M1 = 2*cylP['theta'] + math.pi/2M2 = math.atan(zhat/cylP['r']) + self.lawOfCosines(L2,rho,L3)M3 = M2 + self.lawOfCosines(L2,L3,rho) - math.pi/2angles = [M1,math.pi - M2,M3]values = Int16MultiArray()values.data = [self.deg(angle) for angle in angles]values.data.append(self.gripper)if values.data[0] > 180:values.data[0] = 180print " motor 1 has been saturated!"if values.data[1] > 145:values.data[1] = 145print " motor 2 has been saturated!"if values.data[1] < 55:values.data[1] = 55print " motor 2 has been saturated!"if values.data[2] > 110:values.data[2] = 110print " motor 3 has been saturated!"if values.data[2] < 20:values.data[2] = 20print " motor 3 has been saturated!"stdout.flush()self.pub.publish(values)
3.4 Nodo di cinematica inversa - Sketch con commenti
import dotbot_rosimport mathfrom std_msgs.msg import Int16MultiArrayfrom std_msgs.msg import Stringfrom geometry_msgs.msg import Pointfrom sys import stdoutclass Node(dotbot_ros.DotbotNode):node_name = 'IK'def setup(self):dotbot_ros.Subscriber("desired_position", Point, self.callback)dotbot_ros.Subscriber("gripper_value", String, self.gripper_callback)self.pub = dotbot_ros.Publisher('motors', Int16MultiArray)self.gripper = 25# Qui vengono definite alcune funzioni utili a risolvere la cinematica inversadef hipo(self,x,y):return math.sqrt(x*x + y*y)def lawOfCosines(self,a,b,c):rate = (a*a + b*b - c*c) / (2 * a * b)if abs(rate) > 1:if max(rate,0) == 0:rate = -1if max(rate,0) == rate:rate = 1return math.acos(rate)def deg(self,rad):return rad * 180 / math.pi# Funzione di callback per impostare il valore del servo della pinza# a seconda della stringa pubblicata su gripper_valuedef gripper_callback(self, msg):if msg.data == "open":self.gripper = 100else:self.gripper = 25# Funzione di callback, chiamata alla ricezione della posizione desiderata# sull'apposito topic, in cui vengono calcolati i valori per i servo ed uniti al valore del gripperdef callback(self,data):L0 = 50L1 = 35L2 = 150L3 = 150# Posizione desiderata estrapolata dal messaggio di tipo PointcartP = {'xEE':data.x, 'yEE': data.y, 'zEE': data.z}L = L0 + L1# Posizione desiderata in coordinate cilindrichecylP = {'theta': math.atan(cartP['yEE']/cartP['xEE']), 'r':self.hipo(cartP['xEE'], cartP['yEE']), 'zhat':cartP['zEE']-L}zhat = cylP['zhat']rho = self.hipo(cylP['r'], zhat)M1 = 2*cylP['theta'] + math.pi/2M2 = math.atan(zhat/cylP['r']) + self.lawOfCosines(L2,rho,L3)M3 = M2 + self.lawOfCosines(L2,L3,rho) - math.pi/2angles = [M1,math.pi - M2,M3]values = Int16MultiArray()values.data = [self.deg(angle) for angle in angles]values.data.append(self.gripper)# Limitiamo i valori dei motori ai limiti definiti dalla struttura fisica del robotif values.data[0] > 180:values.data[0] = 180print " motor 1 has been saturated!"if values.data[1] > 145:values.data[1] = 145print " motor 2 has been saturated!"if values.data[1] < 55:values.data[1] = 55print " motor 2 has been saturated!"if values.data[2] > 110:values.data[2] = 110print " motor 3 has been saturated!"if values.data[2] < 20:values.data[2] = 20print " motor 3 has been saturated!"stdout.flush()# Pubblichiamo sul topic definito da pub la sequenza di motori,# unione degli angoli dei miniservo dei giunti e il valore per il gripper.self.pub.publish(values)
4. Interpolazione
I nodi per controllare il manipolatore sono pronti però manca ancora un nodo che renda i movimenti da una configurazione all'altra più fluidi: un nodo di interpolazione. Per mantenere il tutto semplice assumiamo che non ci siano ostacoli da evitare ed implementiamo un'interpolazione di tipo lineare. Implementiamo quindi un nodo di path planning molto semplice.
4.1 Nodo di path planning - Sketch con commenti
Procederò direttamente a riportare il codice commentato. Copiate questo codice in uno sketch chiamato linear_interp_sibot.
import dotbot_rosimport mathfrom geometry_msgs.msg import Pointfrom std_msgs.msg import Int16MultiArrayimport timeclass Node(dotbot_ros.DotbotNode):node_name = 'interpolator'def setup(self):# Definiamo i publisher e subscriberself.pub = dotbot_ros.Publisher('desired_position', Point)self.pubM = dotbot_ros.Publisher('motors', Int16MultiArray)dotbot_ros.Subscriber("desired_position_nointerp", Point, self.callback)dotbot_ros.Subscriber("motors_nointerp", Int16MultiArray, self.motors_callback)self.i = 0self.pointA = Point()self.pointB = Point()self.motorA = Int16MultiArray()self.motorB = Int16MultiArray()# Qui vengono alcune funzioni utili al calcolo dei punti di interpolazionedef coord_distance_AB(self,a,b):d = Point()d.x = abs(b.x-a.x)d.y = abs(b.y-a.y)d.z = abs(b.z-a.z)return ddef values_distance_AB(self,a,b):d = Int16MultiArray()d.data.append(abs(b.data[0]-a.data[0]))d.data.append(abs(b.data[1]-a.data[1]))d.data.append(abs(b.data[2]-a.data[2]))return d# Funzione di callback chiamata alla ricezione di una sequenza di valori per i motori# Legge la sequenza corrente poi la paragona a quella precedente per calcolare i valori intermedi# in 20 steps (N=20)def motors_callback(self,data):N = 20self.i += 1if self.i == 1:self.motorA = dataself.pubM.publish(self.motorA)else:self.motorB = datad = self.values_distance_AB(self.motorA, self.motorB)if d.data[0] == 0 and d.data[1] == 0 and d.data[2] == 0 and d.data[3] == 0 :self.motorA = self.motorBself.pubM.publish(self.motorA)else:for self.i in range(1,N+1):M = Int16MultiArray()if self.motorA.data[0] < self.motorB.data[0]:M.data.append(self.motorA.data[0] + d.data[0]/N)else:M.data.append(self.motorA.data[0] - d.data[0]/N)if self.motorA.data[1] < self.motorB.data[1]:M.data.append(self.motorA.data[1] + d.data[1]/N)else:M.data.append(self.motorA.data[1] - d.data[1]/N)if self.motorA.data[2] < self.motorB.data[2]:M.data.append(self.motorA.data[2] + d.data[2]/N)else:M.data.append(self.motorA.data[2] - d.data[2]/N)M.data.append(self.motorB.data[3])self.pubM.publish(M)self.motorA = Mself.i += 1time.sleep(0.05)# Funzione di callback chiamata alla ricezione di una nuova posizione per l'EE# Legge la posizione corrente poi la paragona a quella precedente per calcolare i valori intermedi# in 20 steps (N=20)def callback(self,data):N = 20self.i += 1if self.i == 1:self.pointA = dataself.pub.publish(self.pointA)else:self.pointB = datad = self.coord_distance_AB(self.pointA, self.pointB)if d.x == 0 and d.y == 0 and d.z == 0:self.pointA = self.pointBself.pub.publish(self.pointA)else:for self.i in range(1,N+1):P = Point()if self.pointA.x < self.pointB.x:P.x = self.pointA.x + d.x/Nelse:P.x = self.pointA.x - d.x/Nif self.pointA.y < self.pointB.y:P.y = self.pointA.y + d.y/Nelse:P.y = self.pointA.y - d.y/Nif self.pointA.z < self.pointB.z:P.z = self.pointA.z + d.z/Nelse:P.z = self.pointA.z - d.z/Nself.pub.publish(P)self.pointA = Pself.i += 1time.sleep(0.05)
NOTA: i valori per i servo possono essere solo interi, l'interpolazione quindi genera valori che possono essere diversi da quelli desiderati.
5. Sketch Arduino
Per poter controllare il manipolatore è necessario controllarne i motori con una scheda Arduino su cui verrà eseguito un nodo ROS seriale che leggerà i messaggi sui topic da noi specificati. Caricate sulla vostra scheda Arduino il seguente sketch:
/** siBOT servo control*/#include <Servo.h>#include <ros.h>#include <std_msgs/Int16MultiArray.h>#define USE_USBCONros::NodeHandle nh;Servo servo1, servo2, servo3, servo4;// Funzione di callback (quando una nuova sequenza di motori viene pubblicata// scrivi gli angoli sui rispettivi pin)void motors_cb( const std_msgs::Int16MultiArray& angles_msg){servo1.write(angles_msg.data[0]);servo2.write(angles_msg.data[1]);servo3.write(angles_msg.data[2]);servo4.write(angles_msg.data[3]);}// Definizione del subscriber. Notate che il topic è specifico al robot su cui stanno// eseguendo i nodi ROS (/hotbot/ in questo caso)ros::Subscriber<std_msgs::Int16MultiArray> sub("/hotbot/motors",motors_cb);void setup(){nh.initNode();nh.subscribe(sub);servo1.attach(2); //attach it to pin 2servo2.attach(3);servo3.attach(4);servo4.attach(5);}void loop(){nh.spinOnce();delay(1);}
6. Avvio di Rosserial e run dei nodi
Affinchè il nodo seriale che esegue sulla scheda Arduino venga reso noto al sistema ROS, un nodo python seriale apposito deve essere lanciato. Per fare ciò attraverso la piattaforma, bisogna aprire questo link e cliccare su "Apri Manager Robot". Una volta aperta la pagina, dove compare "rosserial" cliccare su start per avviare il nodo seriale.
Una volta lanciato il nodo seriale, non ci resta che runnare i nodi necessari al controllo del robot, a seconda del controllo scelto:
- Controllo nello spazio dei motori: motors_generator_sibot, linear_interp_sibot
- Controllo della posizione dell'EE: positions_generator_sibot, IK_sibot, linear_interp_sibot
7. Esercizi
- Scrivere uno sketch chiamato set_motors_sibot che invii una sequenza per i motori ad ogni esecuzione di loop().
- Scrivere uno sketch chiamato set_position_sibot che invii una posizione per l'EE ad ogni esecuzione di loop().
Hint: sfruttate il codice fornito e modificatelo per renderlo più semplice ed usarlo come base per pubblicare sui topic giusti. Inoltre, per fare in modo che l'interpolazione venga eseguita basta runnare lo sketch linear_interp_sibot oltre allo sketch che avrete creato.
Ciao ciao! :hibiscus: