Staubli_VAL3_simplified.py
Go to the documentation of this file.
1 # Copyright 2017 - RoboDK Software S.L. - http://www.robodk.com/
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 # http://www.apache.org/licenses/LICENSE-2.0
6 # Unless required by applicable law or agreed to in writing, software
7 # distributed under the License is distributed on an "AS IS" BASIS,
8 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9 # See the License for the specific language governing permissions and
10 # limitations under the License.
11 
12 # ----------------------------------------------------
13 # This file is a POST PROCESSOR for Robot Offline Programming to generate programs
14 # for a Staubli (VAL3) robot with RoboDK
15 #
16 # To edit/test this POST PROCESSOR script file:
17 # Select "Program"->"Add/Edit Post Processor", then select your post or create a new one.
18 # You can edit this file using any text editor or Python editor. Using a Python editor allows to quickly evaluate a sample program at the end of this file.
19 # Python should be automatically installed with RoboDK
20 #
21 # You can also edit the POST PROCESSOR manually:
22 # 1- Open the *.py file with Python IDLE (right click -> Edit with IDLE)
23 # 2- Make the necessary changes
24 # 3- Run the file to open Python Shell: Run -> Run module (F5 by default)
25 # 4- The "test_post()" function is called automatically
26 # Alternatively, you can edit this file using a text editor and run it with Python
27 #
28 # To use a POST PROCESSOR file you must place the *.py file in "C:/RoboDK/Posts/"
29 # To select one POST PROCESSOR for your robot in RoboDK you must follow these steps:
30 # 1- Open the robot panel (double click a robot)
31 # 2- Select "Parameters"
32 # 3- Select "Unlock advanced options"
33 # 4- Select your post as the file name in the "Robot brand" box
34 #
35 # To delete an existing POST PROCESSOR script, simply delete this file (.py file)
36 #
37 # ----------------------------------------------------
38 # More information about RoboDK Post Processors and Offline Programming here:
39 # http://www.robodk.com/help#PostProcessor
40 # http://www.robodk.com/doc/en/PythonAPI/postprocessor.html
41 # ----------------------------------------------------
42 
43 
44 # ----------------------------------------------------
45 # Import RoboDK tools
46 from robodk import *
47 
48 
49 # Program.pjx file (references data file as %s.dtx)
50 PROGRAM_PJX = '''<?xml version="1.0" encoding="utf-8" ?>
51 <Project xmlns="http://www.staubli.com/robotics/VAL3/Project/3">
52  <Parameters version="s7.3.1" stackSize="5000" millimeterUnit="true" />
53  <Programs>
54  <Program file="start.pgx" />
55  <Program file="stop.pgx" />
56  </Programs>
57  <Database>
58  <Data file="%s.dtx" />
59  </Database>
60  <Libraries>
61  </Libraries>
62 </Project>
63 '''
64 
65 DATA_DTX = '''<?xml version="1.0" encoding="utf-8" ?>
66 <Database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.staubli.com/robotics/VAL3/Data/2">
67  <Datas>
68  <Data name="%s" access="public" xsi:type="array" type="frame" size="%i">
69 %s </Data>
70  <Data name="jPark" access="public" xsi:type="array" type="jointRx" size="1">
71  <Value key="0" j1="0.000" j2="0.000" j3="90.000" j4="0.000" j5="90.000" j6="0.000" />
72  </Data>
73  <Data name="%s" access="public" xsi:type="array" type="jointRx" size="%i">
74 %s </Data>
75  <Data name="mNomSpeed" access="public" xsi:type="array" type="mdesc" size="1">
76  <Value key="0" accel="100" vel="100" decel="100" tmax="99999" rmax="99999" blend="off" leave="50" reach="50" />
77  </Data>
78  <Data name="%s" access="public" xsi:type="array" type="mdesc" size="%i">
79 %s </Data>
80  <Data name="nTraj" access="public" xsi:type="array" type="num" size="1"/>
81  <Data name="nTimeStop" access="private" xsi:type="array" type="num" size="1"/>
82  <Data name="nTimeStart" access="private" xsi:type="array" type="num" size="1"/>
83  <Data name="nMode" access="private" xsi:type="array" type="num" size="1"/>
84  <Data name="nEtat" access="private" xsi:type="array" type="num" size="1"/>
85  <Data name="%s" access="public" xsi:type="array" type="pointRx" size="%i">
86 %s </Data>
87  <Data name="%s" access="public" xsi:type="array" type="tool" size="%i">
88 %s </Data>
89  </Datas>
90 </Database>
91 '''
92 
93 # start.pjx file (references data file as %s.dtx)
94 START_PGX = '''<?xml version="1.0" encoding="utf-8" ?>
95 <Programs xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.staubli.com/robotics/VAL3/Program/2">
96  <Program name="start" access="public">
97  <Code><![CDATA[
98 begin
99 %s
100 end
101  ]]></Code>
102  </Program>
103 </Programs>
104 '''
105 
106 STOP_PGX = '''<?xml version="1.0" encoding="utf-8" ?>
107 <Programs xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.staubli.com/robotics/VAL3/Program/2">
108  <Program name="stop" access="private">
109  <Code><![CDATA[
110  begin
111  resetMotion()
112  disablePower()
113  end
114  ]]></Code>
115  </Program>
116 </Programs>
117 '''
118 
119 
120 def Pose_2_Staubli(pose):
121  """Converts a pose to a Staubli target target"""
122  #return Pose_2_Adept(pose)# older versions (V+)
123  return Pose_2_TxyzRxyz(pose)# newer versions (VAL3)
124 
125 # ----------------------------------------------------
126 def pose_2_str(pose):
127  """Prints a pose target"""
128  [x,y,z,r,p,w] = Pose_2_Staubli(pose)
129  return ('x="%.3f" y="%.3f" z="%.3f" rx="%.3f" ry="%.3f" rz="%.3f"' % (x,y,z,r,p,w))
130 
131 def angles_2_str(angles):
132  """Prints a joint target for Staubli VAL3 XML"""
133  str = ''
134  for i in range(len(angles)):
135  str = str + ('j%i="%.5f" ' % (i, angles[i]))
136  str = str[:-1]
137  return str
138 
139 def getSaveFolder(strdir='C:\\', strtitle='Save program folder ...'):
140  import tkinter
141  from tkinter import filedialog
142  options = {}
143  options['initialdir'] = strdir
144  options['title'] = strtitle
145  root = tkinter.Tk()
146  root.withdraw()
147  file_path = tkinter.filedialog.askdirectory(**options)
148  return file_path
149 
150 # ----------------------------------------------------
151 # Object class that handles the robot instructions/syntax
152 class RobotPost(object):
153  """Robot post object"""
154  # other variables
155  ROBOT_POST = ''
156  ROBOT_NAME = ''
157  PROG_FILES = []
158  PROG_PGX = ''
159  PROG_DTX = ''
160  LOG = ''
161  nAxes = 6
162  TAB_PGX = ' '
163  DEFAULT_SPEED = 150
164  DEFAULT_SMOOTH = 0.1
165  SPEED = DEFAULT_SPEED
166  SMOOTH = DEFAULT_SMOOTH
167  REF_NAME = 'fReference'
168  REF_CURRENT = 'world[0]'
169  REF_DATA = ''
170  REF_COUNT = 0
171  TOOL_NAME = 'tTool'
172  TOOL_CURRENT = 'flange[0]'
173  TOOL_DATA = ''
174  TOOL_COUNT = 0
175  SPEED_NAME = 'mSpeed'
176  SPEED_CURRENT = 'mNomSpeed'
177  SPEED_DATA = ''
178  SPEED_COUNT = 0
179  JOINT_NAME = 'jJoint'
180  JOINT_DATA = ''
181  JOINT_COUNT = 0
182  POINT_NAME = 'pPoint'
183  POINT_DATA = ''
184  POINT_COUNT = 0
185 
186  def __init__(self, robotpost=None, robotname=None, robot_axes = 6, **kwargs):
187  self.ROBOT_POST = robotpost
188  self.ROBOT_NAME = robotname
189  self.PROG = ''
190  self.LOG = ''
191  self.nAxes = robot_axes
192 
193  def ProgStart(self, progname):
194  self.addline('// Program %s start' % progname)
195 
196  def ProgFinish(self, progname):
197  self.addline('')
198  self.addline('waitEndMove()')
199  self.addline('// Program %s end' % progname)
200 
201  def ProgSave(self, folder, progname, ask_user = False, show_result = False):
202  if ask_user or not DirExists(folder):
203  foldersave = getSaveFolder(folder, 'Save program as...')
204  if foldersave is not None and len(foldersave) > 0:
205  foldersave = foldersave
206  else:
207  return
208  else:
209  foldersave = folder
210 
211  folderprog = foldersave + '/' + progname
212  if not DirExists(folderprog):
213  import os
214  os.makedirs(folderprog)
215  #-----------------------------------
216  # start.pgx
217  start_file = folderprog + '/start.pgx'
218  fid = open(start_file, "w")
219  fid.write(START_PGX % self.PROG_PGX)
220  fid.close()
221  #-----------------------------------
222  # stop.pgx
223  stop_file = folderprog + '/stop.pgx'
224  fid = open(stop_file, "w")
225  fid.write(STOP_PGX)
226  fid.close()
227  #-----------------------------------
228  # program.pjx
229  project_file = folderprog + '/%s.pjx' % progname
230  fid = open(project_file, "w")
231  fid.write(PROGRAM_PJX % progname)
232  fid.close()
233  print('SAVED: %s\n' % project_file)
234  #-----------------------------------
235  # program.dtx
236  program_data = folderprog + '/%s.dtx' % progname
237  fid = open(program_data, "w")
238  fid.write(DATA_DTX % (self.REF_NAME, self.REF_COUNT, self.REF_DATA, self.JOINT_NAME, self.JOINT_COUNT, self.JOINT_DATA, self.SPEED_NAME, self.SPEED_COUNT, self.SPEED_DATA, self.POINT_NAME, self.POINT_COUNT, self.POINT_DATA, self.TOOL_NAME, self.TOOL_COUNT, self.TOOL_DATA))
239  fid.close()
240  #-----------------------------------
241 
242  #self.UploadFTP(folderprog)
243  self.PROG_FILES = folderprog
244 
245  if show_result:
246  if type(show_result) is str:
247  # Open file with provided application
248  import subprocess
249  p = subprocess.Popen([show_result, start_file])
250  p = subprocess.Popen([show_result, program_data])
251  elif type(show_result) is list:
252  import subprocess
253  p = subprocess.Popen(show_result + [filesave])
254  else:
255  # open file with default application
256  import os
257  os.startfile(start_file)
258  os.startfile(program_data)
259  if len(self.LOG) > 0:
260  mbox('Program generation LOG:\n\n' + self.LOG)
261 
262  def ProgSendRobot(self, robot_ip, remote_path, ftp_user, ftp_pass):
263  """Send a program to the robot using the provided parameters. This method is executed right after ProgSave if we selected the option "Send Program to Robot".
264  The connection parameters must be provided in the robot connection menu of RoboDK"""
265  UploadFTP(self.PROG_FILES, robot_ip, remote_path, ftp_user, ftp_pass)
266 
267  def MoveJ(self, pose, joints, conf_RLF=None):
268  """Add a joint movement"""
269  #nTraj=movej(jJoints[0],tTool[0],mSpeed[0])
270  #waitEndMove()
271  # <Value key="0" j1="0.000" j2="-10.000" j3="100.000" j4="0.000" j5="0.000" j6="-90.000" />
272  variable = '%s[%i]' % (self.JOINT_NAME, self.JOINT_COUNT)
273  self.JOINT_DATA = self.JOINT_DATA + ' <Value key="%i" %s />\n' % (self.JOINT_COUNT, angles_2_str(joints))
274  self.JOINT_COUNT = self.JOINT_COUNT + 1
275  self.addline('nTraj=movej(%s,%s,%s)' % (variable, self.TOOL_CURRENT, self.SPEED_CURRENT))
276  #self.addline('waitEndMove()')
277 
278  def MoveL(self, pose, joints, conf_RLF=None):
279  """Add a linear movement"""
280  #nTraj=movej(jJoints[0],tTool[0],mSpeed[0])
281  #waitEndMove()
282  # <Value key="0" x="-36.802" y="-6.159" z="500.000" rx="135.407" ry="80.416" rz="46.453" shoulder="lefty" elbow="epositive" wrist="wpositive" fatherId="fPartReal[0]" />
283  if conf_RLF == None:
284  str_config = 'shoulder="lefty" elbow="epositive" wrist="wpositive"'
285  else:
286  [rear, lowerarm, flip] = conf_RLF
287  str_config = 'shoulder="%s" elbow="%s" wrist="%s"' % ("righty" if rear>0 else "lefty", "enegative" if lowerarm>0 else "epositive", "wnegative" if flip>0 else "wpositive")
288  variable = '%s[%i]' % (self.POINT_NAME, self.POINT_COUNT)
289  self.POINT_DATA = self.POINT_DATA + ' <Value key="%i" %s %s fatherId="%s" />\n' % (self.POINT_COUNT, pose_2_str(pose), str_config, self.REF_CURRENT)
290  self.POINT_COUNT = self.POINT_COUNT + 1
291  self.addline('nTraj=movel(%s,%s,%s)' % (variable, self.TOOL_CURRENT, self.SPEED_CURRENT))
292 
293  def MoveC(self, pose1, joints1, pose2, joints2, conf_RLF_1=None, conf_RLF_2=None):
294  """Add a circular movement"""
295  # Needs to be checked
296  if conf_RLF_1 == None:
297  str_config = 'shoulder="lefty" elbow="epositive" wrist="wpositive"'
298  else:
299  [rear, lowerarm, flip] = conf_RLF_1
300  str_config = 'shoulder="%s" elbow="%s" wrist="%s"' % ("righty" if rear>0 else "lefty", "enegative" if lowerarm>0 else "epositive", "wnegative" if flip>0 else "wpositive")
301  variable1 = '%s[%i]' % (self.POINT_NAME, self.POINT_COUNT)
302  variable2 = '%s[%i]' % (self.POINT_NAME, self.POINT_COUNT+1)
303  self.POINT_DATA = self.POINT_DATA + ' <Value key="%i" %s %s fatherId="%s" />\n' % (self.POINT_COUNT, pose_2_str(pose1), str_config, self.REF_CURRENT)
304  self.POINT_DATA = self.POINT_DATA + ' <Value key="%i" %s %s fatherId="%s" />\n' % (self.POINT_COUNT+1, pose_2_str(pose2), str_config, self.REF_CURRENT)
305  self.POINT_COUNT = self.POINT_COUNT + 2
306  self.addline('nTraj=movec(%s,%s,%s,%s)' % (variable1, variable2, self.TOOL_CURRENT, self.SPEED_CURRENT))
307 
308  def setFrame(self, pose, frame_id=None, frame_name=None):
309  """Change the robot reference frame"""
310  # <Value key="0" x="600.000" y="0.000" z="-465.000" rx="0.400" ry="0.100" rz="-45.000" fatherId="world[0]" />
311  self.REF_CURRENT = '%s[%i]' % (self.REF_NAME, self.REF_COUNT)
312  self.REF_DATA = self.REF_DATA + ' <Value key="%i" %s fatherId="world[0]" />\n' % (self.REF_COUNT, pose_2_str(pose))
313  self.REF_COUNT = self.REF_COUNT + 1
314 
315  def setTool(self, pose, tool_id=None, tool_name=None):
316  """Change the robot TCP"""
317  # <Value key="0" x="-5.972" y="209.431" z="55.323" rx="-90.190" ry="-0.880" rz="89.997" fatherId="flange[0]" ioLink="valve1" />
318  self.TOOL_CURRENT = '%s[%i]' % (self.TOOL_NAME, self.TOOL_COUNT)
319  self.TOOL_DATA = self.TOOL_DATA + ' <Value key="%i" %s fatherId="flange[0]" ioLink="valve1" />\n' % (self.TOOL_COUNT, pose_2_str(pose))
320  self.TOOL_COUNT = self.TOOL_COUNT + 1
321 
322  def Pause(self, time_ms):
323  """Pause the robot program"""
324  if time_ms < 0:
325  self.addline('popUpMsg("Paused. Press OK to continue")')
326  else:
327  self.addline('delay(%.3f)' % (time_ms*0.001))
328 
329  def setSpeed(self, speed_mms):
330  """Changes the robot speed (in mm/s)"""
331  # <Value key="0" accel="100" vel="100" decel="100" tmax="50" rmax="100" blend="joint" leave="0.1" reach="0.1" />
332  SPEED = speed_mms
333  self.SPEED_CURRENT = '%s[%i]' % (self.SPEED_NAME, self.SPEED_COUNT)
334  # blend = "off" / "joint" / "Cartesian"
335  #self.SPEED_DATA = self.SPEED_DATA + ' <Value key="%i" accel="100" vel="100" decel="100" tmax="%.1f" rmax="100" blend="cartesian" leave="%.1f" reach="%0.1f" />\n' % (self.SPEED_COUNT, speed_mms, self.SMOOTH, self.SMOOTH)
336  self.SPEED_DATA = self.SPEED_DATA + ' <Value key="%i" tmax="%.1f" rmax="100" leave="%.1f" reach="%0.1f" blend="cartesian" />\n' % (self.SPEED_COUNT, speed_mms, self.SMOOTH, self.SMOOTH)
337  self.SPEED_COUNT = self.SPEED_COUNT + 1
338 
339  def setAcceleration(self, accel_mmss):
340  """Changes the robot acceleration (in mm/s2)"""
341  self.ACCEL_MMSS = accel_mmss
342 
343  def setSpeedJoints(self, speed_degs):
344  """Changes the robot joint speed (in deg/s)"""
345  self.SPEED_DEGS = speed_degs
346 
347  def setAccelerationJoints(self, accel_degss):
348  """Changes the robot joint acceleration (in deg/s2)"""
349  self.ACCEL_DEGSS = accel_degss
350 
351  def setZoneData(self, zone_mm):
352  """Changes the zone data approach (makes the movement more smooth)"""
353  self.SMOOTH = zone_mm
354 
355  def setDO(self, io_var, io_value):
356  """Sets a variable (output) to a given value"""
357  if type(io_var) != str: # set default variable name if io_var is a number
358  io_var = 'OUT[%s]' % str(io_var)
359  if type(io_value) != str: # set default variable value if io_value is a number
360  if io_value > 0:
361  io_value = 'TRUE'
362  else:
363  io_value = 'FALSE'
364 
365  # at this point, io_var and io_value must be string values
366  self.addline('%s=%s' % (io_var, io_value))
367 
368  def waitDI(self, io_var, io_value, timeout_ms=-1):
369  """Waits for an input io_var to attain a given value io_value. Optionally, a timeout can be provided."""
370  if type(io_var) != str: # set default variable name if io_var is a number
371  io_var = 'IN[%s]' % str(io_var)
372  if type(io_value) != str: # set default variable value if io_value is a number
373  if io_value > 0:
374  io_value = 'TRUE'
375  else:
376  io_value = 'FALSE'
377 
378  # at this point, io_var and io_value must be string values
379  if timeout_ms < 0:
380  self.addline('WAIT FOR %s==%s' % (io_var, io_value))
381  else:
382  self.addline('WAIT FOR %s==%s TIMEOUT=%.1f' % (io_var, io_value, timeout_ms))
383 
384  def RunCode(self, code, is_function_call = False):
385  """Adds code or a function call"""
386  if is_function_call:
387  code.replace(' ','_')
388  if not code.endswith(')'):
389  code = code + '()'
390  #call prog:start()
391  #self.addline('call prog:%s' % code)# add as a call
392  self.addline('//call prog:%s' % code)# add as a comment
393  else:
394  self.addline(code)
395 
396  def RunMessage(self, message, iscomment = False):
397  """Display a message in the robot controller screen (teach pendant)"""
398  if iscomment:
399  self.addline('// ' + message)
400  else:
401  self.addline('popUpMsg("%s")' % message)
402 
403 # ------------------ private ----------------------
404  def addline(self, newline):
405  """Add a program line"""
406  self.PROG_PGX = self.PROG_PGX + self.TAB_PGX + newline + '\n'
407 
408  def addlog(self, newline):
409  """Add a log message"""
410  self.LOG = self.LOG + newline + '\n'
411 
412 # -------------------------------------------------
413 # ------------ For testing purposes ---------------
414 def Pose(xyzrpw):
415  [x,y,z,r,p,w] = xyzrpw
416  a = r*math.pi/180
417  b = p*math.pi/180
418  c = w*math.pi/180
419  ca = math.cos(a)
420  sa = math.sin(a)
421  cb = math.cos(b)
422  sb = math.sin(b)
423  cc = math.cos(c)
424  sc = math.sin(c)
425  return Mat([[cb*ca, ca*sc*sb - cc*sa, sc*sa + cc*ca*sb, x],[cb*sa, cc*ca + sc*sb*sa, cc*sb*sa - ca*sc, y],[-sb, cb*sc, cc*cb, z],[0,0,0,1]])
426 
427 def test_post():
428  """Test the post with a basic program"""
429 
430  robot = RobotPost()
431 
432  robot.ProgStart("Program")
433  robot.RunMessage("Program generated by RoboDK using a custom post processor", True)
434  robot.setFrame(Pose([807.766544, -963.699898, 41.478944, 0, 0, 0]))
435  robot.setTool(Pose([62.5, -108.253175, 100, -60, 90, 0]))
436  robot.MoveJ(Pose([200, 200, 500, 180, 0, 180]), [-46.18419, -6.77518, -20.54925, 71.38674, 49.58727, -302.54752] )
437  robot.MoveL(Pose([200, 250, 348.734575, 180, 0, -150]), [-41.62707, -8.89064, -30.01809, 60.62329, 49.66749, -258.98418] )
438  robot.MoveL(Pose([200, 200, 262.132034, 180, 0, -150]), [-43.73892, -3.91728, -35.77935, 58.57566, 54.11615, -253.81122] )
439  robot.RunMessage("Setting air valve 1 on")
440  robot.RunCode("TCP_On", True)
441  robot.Pause(1000)
442  robot.MoveL(Pose([200, 250, 348.734575, 180, 0, -150]), [-41.62707, -8.89064, -30.01809, 60.62329, 49.66749, -258.98418] )
443  robot.MoveL(Pose([250, 300, 278.023897, 180, 0, -150]), [-37.52588, -6.32628, -34.59693, 53.52525, 49.24426, -251.44677] )
444  robot.MoveL(Pose([250, 250, 191.421356, 180, 0, -150]), [-39.75778, -1.04537, -40.37883, 52.09118, 54.15317, -246.94403] )
445  robot.RunMessage("Setting air valve off")
446  robot.RunCode("TCP_Off", True)
447  robot.Pause(1000)
448  robot.MoveL(Pose([250, 300, 278.023897, 180, 0, -150]), [-37.52588, -6.32628, -34.59693, 53.52525, 49.24426, -251.44677] )
449  robot.MoveL(Pose([250, 200, 278.023897, 180, 0, -150]), [-41.85389, -1.95619, -34.89154, 57.43912, 52.34162, -253.73403] )
450  robot.MoveL(Pose([250, 150, 191.421356, 180, 0, -150]), [-43.82111, 3.29703, -40.29493, 56.02402, 56.61169, -249.23532] )
451  robot.ProgFinish("Program")
452  # robot.ProgSave(".","Program",True)
453  print(robot.PROG_PGX)
454  if len(robot.LOG) > 0:
455  mbox('Program generation LOG:\n\n' + robot.LOG)
456 
457  input("Press Enter to close...")
458 
459 if __name__ == "__main__":
460  """Function to call when the module is executed by itself: test"""
461  test_post()
def getSaveFolder(strdir='C:\\', strtitle='Save program folder ...')
def ProgSave(self, folder, progname, ask_user=False, show_result=False)
def setFrame(self, pose, frame_id=None, frame_name=None)
def UploadFTP(program, robot_ip, remote_path, ftp_user, ftp_pass)
Definition: robodk.py:1312
def ProgSendRobot(self, robot_ip, remote_path, ftp_user, ftp_pass)
def mbox(msg, b1='OK', b2='Cancel', frame=True, t=False, entry=None)
Definition: robodk.py:1458
def MoveC(self, pose1, joints1, pose2, joints2, conf_RLF_1=None, conf_RLF_2=None)
def __init__(self, robotpost=None, robotname=None, robot_axes=6, kwargs)


ros_robodk_post_processors
Author(s): Victor Lamoine - Institut Maupertuis
autogenerated on Sun Jun 7 2020 03:50:22