RoboAide
Project to improve a DIY robotic arm used for mobility assistance
RoboAide.py
Go to the documentation of this file.
1 # Description: GUI application for the control of the robotic arm
2 #
3 # Authors: Jeremie Bourque <Jeremie.Bourque@USherbrooke.ca>
4 # Jacob Kealey <Jacob.Kealey@USherbrooke.ca>
5 #
6 # Date created: 22-01-2020
7 
8 import os
9 import sys
10 
11 sys.path.append(os.path.dirname(os.path.realpath(__file__))) # import error fix
12 
13 
14 from PySide2.QtWidgets import QApplication, QMainWindow, QLineEdit, QVBoxLayout, QMenu, QListWidgetItem,\
15  QDialog, QDialogButtonBox, QLabel, QPushButton, QSlider, QListWidget, QMessageBox
16 from PySide2.QtGui import QIcon, QBrush
17 from PySide2.QtCore import QRect, Qt, QMutex, QThread, Signal
18 from PySide2.QtUiTools import QUiLoader
19 from Communication import MessageReception, MessageTransmission, initSerialConnection, scanAvailablePorts
20 from Drawer import Drawer
21 from collections import deque
22 
23 import warnings
24 import struct
25 
26 import json
27 import time
28 
29 
30 # To remove the following warning: DeprecationWarning: an integer is required
31 # (got type PySide2.QtWidgets.QDialogButtonBox.StandardButton).
32 # Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
33 # self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
34 warnings.filterwarnings("ignore","an integer is required", DeprecationWarning)
35 
36 # Change working directory to this script's directory to easily access mainwindow.ui
37 os.chdir(os.path.dirname(os.path.realpath(__file__)))
38 
39 ## App icon
40 icon = 'icon.png'
41 
42 class playSequence(QThread):
43  """
44  Tread to send the different positions to the motors during a sequence
45  """
46  def __init__(self,moves,motors, listOfSequencesHandler, mainWindow):
47  """
48  Initialization of the play sequence thread
49  :param moves: the moves of the sequence containing the different positions to reach
50  :param motors: the motors of the robot
51  :param listOfSequencesHandler: the handler of the different sequences
52  :param mainWindow: the main window of the UI
53  """
54  super(playSequence, self).__init__()
55  ## The moves of the sequence containing the different positions to reach
56  self.__moves=moves
57  ## The motors of the robot
58  self.__motors=motors
59  ## The handler of the different sequences
60  self.__listOfSequencesHandler = listOfSequencesHandler
61  ## The main window of the UI
62  self.__mainWindow = mainWindow
63 
64  # Make sure the thread close properly even if the app is closed
65  self.__mainWindow.app.aboutToQuit.connect(self.close)
66 
67  # connect a custom function to handle the different events when the thread is closed
68  self.finished.connect(self.stop)
69 
70  def run(self):
71  for move in self.__moves:
72  for motor in self.__motors:
73  self.__motors[motor].setGoalPosition(move.getMotorPosition(motor))
74  for motor in self.__motors:
75  while self.__motors[motor].getCurrentPosition() < self.__motors[motor].getGoalPosition() - 10 \
76  or self.__motors[motor].getCurrentPosition() > self.__motors[motor].getGoalPosition() + 10:
77  if not self.__mainWindow.shouldStop:
78  # print("current position:" + str(self.__motors[motor].getCurrentPosition()))
79  # print("goal position:" + str(self.__motors[motor].getGoalPosition()))
80  self.__motors[motor].setGoalPosition(move.getMotorPosition(motor))
81  self.msleep(250)
82  else:
83  break
84 
85 
86  def stop(self):
87  """
88  Handles the different events when the thread is closed
89  :return: None
90  """
91  self.__listOfSequencesHandler.robotInMotionMessage.accept()
92  self.__listOfSequencesHandler.enableUi()
93  self.__mainWindow.shouldStop = False
94 
95  def close(self):
96  """
97  To get out of the while loop
98  :return: None
99  """
100  self.__mainWindow.shouldStop = True
101 
102 
103 def loadSequences(listOfSequenceHandler,motors):
104  """
105  To load the saved sequence in the SaveSequence.json file and put them in the list of sequence
106  :param listOfSequenceHandler: To add the sequences to the list
107  :param motors: the motors of the robotic arm
108  :return: No return
109  """
110  try:
111  with open('SaveSequence.json') as save:
112  savedListOfSequences = json.load(save)
113  for sequence in savedListOfSequences:
114  for sequenceName in sequence:
115  savedSequence = Sequence(motors,sequenceName)
116  for move in sequence[sequenceName]:
117  savedMove = Move(motors)
118  for motor in move:
119  savedMove.setMotorPosition(motor, move[motor])
120  savedSequence.addMove(savedMove)
121  listOfSequenceHandler.addItem(savedSequence)
122  except FileNotFoundError:
123  print("Save file not found")
124 
126  """
127  Handler for the list of sequences
128  """
129  def __init__(self, mainWindow, motors):
130  """
131  Initializtion of the handler for the list of sequences
132  :param ui: The ui in which the list of sequence is in
133  :param motors: The dictionary of all the motors
134  """
135  ## The dictionary of all the motors
136  self.__motors = motors
137  ## The list of sequence
138  self.__listOfSequences = mainWindow.ui.listOfSequences
139  self.__listOfSequences.itemDoubleClicked.connect(self.playSequence)
140  ## The create a new sequence button
141  self.__createSequenceButton = mainWindow.ui.createSequenceButton
142  ## The ui in which the list of sequence is in
143  self.__ui = mainWindow.ui
144  ## The main window of the ui
145  self.__mainWindow = mainWindow
146  ## The window in which the new sequence will be created
147  self.__window = None
148  ## Thread to send the sequence to the motors
149  self.playSequenceThread = None
150  ## Message to make the user put a name to the sequence
151  # TODO: Set the size of the message to something appropriate
152  self.robotInMotionMessage = QMessageBox()
153  self.robotInMotionMessage.setIcon(QMessageBox.Information)
154  self.robotInMotionMessage.setWindowIcon(QIcon(icon))
155  self.robotInMotionMessage.setText("Robot is in motion")
156  self.robotInMotionMessage.addButton(QMessageBox.Abort)
157  self.robotInMotionMessage.rejected.connect(self.__mainWindow.stopMotors)
158  self.robotInMotionMessage.rejected.connect(self.enableUi)
159 
160  # Create a new window when the create sequence button is clicked
161  self.__createSequenceButton.clicked.connect(self.createWindow)
162 
163  # Connect the qwidgetlist to the custom right click menu
164  self.__listOfSequences.customContextMenuRequested.connect(self.showMenu)
165 
166  def addItem(self, item):
167  """
168  Add an item to the list of sequence
169  :param item: The QListWidgetItem sequence to add
170  :return: No return
171  """
172  self.__listOfSequences.addItem(item)
173 
175  """
176  Removes the selected item in the list
177  :return: No return
178  """
179  listItems = self.__listOfSequences.selectedItems()
180  if not listItems: return
181  for item in listItems:
182  self.__listOfSequences.takeItem(self.__listOfSequences.row(item))
183  # Load the save file
184  with open('SaveSequence.json', 'r') as save_file:
185  savedListOfSequences = json.load(save_file)
186  # Search for the sequence that needs to be deleted
187  indexSequence = -1
188  for sequence in range(len(savedListOfSequences)):
189  for sequenceName in savedListOfSequences[sequence]:
190  if sequenceName == item.getName():
191  # Delete the sequence
192  indexSequence = sequence
193  if indexSequence != -1:
194  savedListOfSequences.pop(indexSequence)
195 
196  # Rewrite the save file without the deleted sequence
197  with open('SaveSequence.json', 'w') as save_file:
198  json.dump(savedListOfSequences, save_file)
199 
200  def createWindow(self, modifySequence = False):
201  """
202  Create the window to create a new sequence
203  :param modifySequence: bool, if true there's a selected sequence that needs to be modified if false
204  it's a new sequence
205  :return: No return
206  """
207  if modifySequence:
208  self.__window = CreateSequenceWindow(self.__motors, self, self.getSelectedItems()[0], True)
209  else:
210  self.__window = CreateSequenceWindow(self.__motors, self, Sequence(self.__motors))
211  self.__ui.setEnabled(False)
212  # 2 first number QPoint where the window is created and 2 last QSize(the size of the window)
213  self.__window.setGeometry(QRect(150, 150, 600, 400))
214  self.__window.show()
215 
216  def showMenu(self, event):
217  """
218  The right click menu of the sequence of the list
219  :param event: The event (here right click) that makes the menu come up
220  :return: No return
221  """
222  menu = QMenu()
223  menu.addAction("Modify Sequence",lambda: self.createWindow(True))
224  # Add a button in the menu that when clicked, it deletes the sequence in the list
225  menu.addAction("Delete Sequence", self.removeSelectedItem)
226  menu.exec_(self.__listOfSequences.mapToGlobal(event))
227 
228  def enableUi(self):
229  """
230  Enable the ui
231  :return: No return
232  """
233  self.__ui.setEnabled(True)
234 
235  def getSelectedItems(self):
236  """
237  Accessor to the selected items of the list
238  :return: the selected items
239  """
240  # Returns the only item in the list because the list is set to be
241  return self.__listOfSequences.selectedItems()
242 
243  def playSequence(self):
244  self.playSequenceThread = playSequence(self.getSelectedItems()[0].getMoves(), self.__motors,self,self.__mainWindow)
245  self.__ui.setEnabled(False)
246  # self.robotInMotionMessage.rejected.connect(self.playSequenceThread.quit)
247  self.playSequenceThread.start()
248  self.robotInMotionMessage.exec_()
249 
250 class CreateSequenceWindow(QDialog):
251  """
252  Window for creating a new sequence
253  """
254  def __init__(self, motors={}, listOfSequenceHandler = None, sequence = None, modifySequence = False):
255  """
256  Initializtion of the window for creating a new sequence
257  :param motors: The dictionary of all the motors
258  :param listOfSequenceHandler: The handler of the list of sequence
259  """
260  QDialog.__init__(self)
261  # Set the window icon
262  appIcon = QIcon(icon)
263  self.setWindowIcon(appIcon)
264 
265  ## Flag if the sequence is a modified one or a new one
266  self.__modifySequence = modifySequence
267  ## The handler of the list of sequence
268  self.__listOfSequenceHandler = listOfSequenceHandler
269  ## The dictionary of all the motors
270  self.__motors = motors
271  ## The new sequence
272  self.__sequence = sequence
273  ## A dictionary of the positions of the new sequence
275  ## The layout of the create sequence window
276  self.__layout = QVBoxLayout(self)
277  ## The widget for the name of the sequenc
278  self.nameEntry = QLineEdit()
279  self.nameEntry.setText(self.__sequence.getName())
280  ## The label for the widget in which the name of the sequence is written
281  self.__nameLabel = QLabel("Sequence Name")
282  ## The list of the different moves that forms the sequence
283  self.__listOfMoveLabels = QListWidget()
284  moveNumber = 1
285  for move in self.__sequence.getMoves():
286  # Set text for the move label
287  labelText = "move " + str(moveNumber) + ": "
288  moveNumber += 1
289  for motor in self.__motors:
290  labelText += self.__motors[motor].getName() + " " + \
291  str(move.getMotorPosition(self.__motors[motor].getName())) + ", "
292  label = moveLabel(move, labelText, self.__motors)
293 
294  # insert label to the head of the list
295  self.__listOfMoveLabels.insertItem(0, label)
296 
297  # Put the sliders of the create sequence window in a list
298  ## List of sliders in the create sequence window
299  self.listOfSliders = []
300  dictOfSlider = dict()
301 
302  for motor in self.__motors:
303  dictOfSlider[motor] = QSlider(Qt.Horizontal)
304  dictOfSlider[motor].setMaximum(4095)
305  dictOfSlider[motor].setValue(self.__motors[motor].getCurrentPosition())
306  dictOfSlider[motor].sliderMoved.connect(self.__motors[motor].setGoalPosition)
307  self.listOfSliders.append(dictOfSlider[motor])
308 
309  ## Message to make the user put a name to the sequence
310  self.__noNameMessage = QMessageBox()
311  self.__noNameMessage.setIcon(QMessageBox.Warning)
312  self.__noNameMessage.setWindowIcon(appIcon)
313  self.__noNameMessage.setText("Please name your sequence before saving it")
314  self.__noNameMessage.setStandardButtons(QMessageBox.Ok)
315  # Renable the create sequence window and closes the message
316  self.__noNameMessage.accepted.connect(self.enableWindow)
317 
318  ## Warning message to make sure the user doen't want to save the sequence
319  self.__warningMessage = QMessageBox()
320  self.__warningMessage.setIcon(QMessageBox.Warning)
321  self.__warningMessage.setWindowIcon(appIcon)
322  self.__warningMessage.setText("Are you sure you want to close this window? Your sequence will not be saved")
323  self.__warningMessage.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
324  # Close the create sequence window and the message
325  self.__warningMessage.accepted.connect(self.reject)
326  # Renable the create sequence window and closes the message
327  self.__warningMessage.rejected.connect(self.enableWindow)
328 
329  # Set the text for the labels
330  ## Labels for the motors in the UI
331  self.__motorLabels = []
332  for motorNumber in range(0,len(motors)):
333  self.__motorLabels.append(QLabel("Motor " + str(motorNumber+1) + " position"))
334 
335  ## Button to add a move to the sequence and procede to the next move
336  self.nextMoveButton = QPushButton("Save Move")
337 
338  ## Buttons to accept or cancel the creation of a sequence
339  self.buttonBox = QDialogButtonBox()
340  self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
341  # If ok pressed add the sequence to the list
342  self.buttonBox.accepted.connect(lambda: self.addSequenceToList(self.__modifySequence))
343  # If cancel pressed close the create sequence window
344  self.buttonBox.rejected.connect(self.__warningMessage.exec)
345 
346  # Renable the main window when the create sequence closes
347  self.rejected.connect(self.__listOfSequenceHandler.enableUi)
348  self.accepted.connect(self.__listOfSequenceHandler.enableUi)
349 
350  self.nextMoveButton.clicked.connect(self.addMovetoSequence)
351 
352  self.__listOfMoveLabels.itemDoubleClicked.connect(self.moveDoubleClicked)
353 
354  # Build the vertical layout with the different widgets
355  self.__layout.addWidget(self.__nameLabel)
356  self.__layout.addWidget(self.nameEntry)
357  self.__layout.addWidget(self.__listOfMoveLabels)
358  for motorNumber in range(len(self.__motors)):
359  self.__layout.addWidget(self.__motorLabels[motorNumber])
360  self.__layout.addWidget(self.listOfSliders[motorNumber])
361  self.__layout.addWidget(self.nextMoveButton)
362  self.__layout.addWidget(self.buttonBox)
363 
364  # Connect the qwidgetlist to the custom right click menu
365  self.__listOfMoveLabels.setContextMenuPolicy(Qt.CustomContextMenu)
366  self.__listOfMoveLabels.customContextMenuRequested.connect(self.rightClickMenu)
367 
368  def setName(self, name):
369  """
370  Sets the name of the sequence with the user input
371  :param name: The name of the sequence
372  :return: No return
373  """
374  self.__sequence.setName(name)
375 
376  def getSequence(self):
377  """
378  Accessor of the sequence (for tests)
379  :return: the sequence
380  """
381  return self.__sequence
382 
384  """
385  Accessor of the list of move labels (for tests)
386  :return: the list of move labels
387  """
388  return self.__listOfMoveLabels
389 
390  def addSequenceToList(self, modifySequence = False):
391  """
392  Add the sequence to the list of sequence
393  :param modifySequence: bool, if true there's a selected sequence that needs to be modified if false
394  it's a new sequence
395  :return: No return
396  """
397  # TODO: look to move this method to the list of sequence handler
398  # TODO: don't let the user enter a sequence that has the same name as an old one
399  self.setName(self.nameEntry.text())
400  if self.__sequence.getName() != "":
401  # Load previously saved sequences
402  try:
403  with open('SaveSequence.json') as save:
404  savedListOfSequences = json.load(save)
405  except FileNotFoundError:
406  savedListOfSequences = []
407 
408  if modifySequence:
409  # Get the item that needs to be modified
410  # selectedSequence = self.__listOfSequenceHandler.getSelectedItems()
411  # Find the selected sequence in the list of saved ones
412  for sequence in savedListOfSequences:
413  if self.__sequence.getName() in sequence:
414  indexOfTheSequence = savedListOfSequences.index(sequence)
415  # remove the unmodified sequence to insert the modified sequence
416  savedListOfSequences.remove(sequence)
417  self.__listOfSequenceHandler.addItem(self.__sequence)
418  # Make the sequence json seriable
419  newSequence = dict()
420  newSequence[self.__sequence.getName()] = []
421  for moveNumber in range(len(self.__sequence.getMoves())):
422  newSequence[self.__sequence.getName()].append(
423  self.__sequence.getMoves()[moveNumber].getMovePositions())
424 
425  # Append new sequence to the old ones
426  savedListOfSequences.insert(indexOfTheSequence,newSequence)
427 
428  # Write the sequences to the file
429  with open('SaveSequence.json', 'w') as outfile:
430  json.dump(savedListOfSequences, outfile)
431 
432  self.accept()
433  else:
434  self.__listOfSequenceHandler.addItem(self.__sequence)
435  # Make the sequence json seriable
436  newSequence = dict()
437  newSequence[self.__sequence.getName()] = []
438  for moveNumber in range(len(self.__sequence.getMoves())):
439  newSequence[self.__sequence.getName()].append(
440  self.__sequence.getMoves()[moveNumber].getMovePositions())
441 
442  # Append new sequence to the old ones
443  savedListOfSequences.append(newSequence)
444 
445  # Write the sequences to the file
446  with open('SaveSequence.json', 'w') as outfile:
447  json.dump(savedListOfSequences, outfile)
448 
449  self.accept()
450  else:
451  self.setEnabled(False)
452  self.__noNameMessage.exec_()
453 
454  def addMovetoSequence(self):
455  """
456  Add the last move to the sequence
457  :return: No return
458  """
459  move = None
460  labelToModify = None
461  # Check if there's a move in modifying state
462  for row in range(self.__listOfMoveLabels.count()):
463  if not self.__listOfMoveLabels.item(row).getMove().isNew:
464  move = self.__listOfMoveLabels.item(row).getMove()
465  labelToModify = self.__listOfMoveLabels.item(row)
466  break
467  # verify if the move is a new one
468  if move is None:
469  # Create the new move and set his positions
470  move = Move(self.__motors)
471  i = 0
472  for motorName in self.__motors:
473  move.setMotorPosition(motorName, self.listOfSliders[i].value())
474  i += 1
475  self.__sequence.addMove(move)
476 
477  # Set text for the move label
478  labelText = "move " + str(self.__sequence.getNumberofMoves()) +": "
479  i = 0
480  for motor in self.__motors:
481  labelText += self.__motors[motor].getName() + " " +\
482  str(self.listOfSliders[i].value()) + ", "
483  i += 1
484  label = moveLabel(move,labelText,self.__motors)
485 
486  # insert label to the head of the list
487  self.__listOfMoveLabels.insertItem(0, label)
488  else:
489  # modify the move
490  i = 0
491  for motorName in self.__motors:
492  move.setMotorPosition(motorName, self.listOfSliders[i].value())
493  i += 1
494 
495  # modify the label of the move
496  textToEdit = labelToModify.text()
497  listOfTextToEdit = textToEdit.split(' ')
498  labelText = listOfTextToEdit[0] + " " + listOfTextToEdit[1] + " "
499  i = 0
500  for motor in self.__motors:
501  labelText += self.__motors[motor].getName() + " " + \
502  str(self.listOfSliders[i].value()) + ", "
503  i += 1
504  labelToModify.setText(labelText)
505  labelToModify.setSelected(False)
506  labelToModify.setBackground(Qt.white)
507 
508  # reset the state of the move
509  move.isNew = True
510 
511 
512  # Access the move positions when double clicked on
513  def moveDoubleClicked(self, moveItem):
514  """
515  Called when a move in the sequence is double clicked
516  :param moveItem: the move that was double clicked
517  :return: No return
518  """
519  moveItem.goToMoveOfTheLabel()
521 
523  counterMotors = 0
524  for motor in self.__motors:
525  self.listOfSliders[counterMotors].setValue(self.__motors[motor].getGoalPosition())
526  counterMotors += 1
527 
528  def enableWindow(self):
529  """
530  Enable the create sequence window
531  :return:
532  """
533  self.setEnabled(True)
534 
535  def rightClickMenu(self, event):
536  """
537  The right click menu of the move list
538  :param event: The event (here right click) that makes the menu come up
539  :return: No return
540  """
541  menu = QMenu()
542  # Add a button in the menu that when clicked, it puts a move in modifying state
543  menu.addAction("Modify Move",lambda: self.modifyMove(self.__listOfMoveLabels.selectedItems()[0]))
544  # Add a button in the menu that when clicked, it deletes a move in the list
545  menu.addAction("Delete Move",lambda: self.deleteMove(self.__listOfMoveLabels.selectedItems()[0]))
546  menu.exec_(self.__listOfMoveLabels.mapToGlobal(event))
547 
548  def deleteMove(self,label):
549  """
550  Delete a move and its label of the sequence
551  :param label: label of the move
552  :return: No return
553  """
554  # remove the label from the list
555  self.__listOfMoveLabels.takeItem(self.__listOfMoveLabels.row(label))
556  # remove the move from the sequence
557  self.__sequence.deleteMove(label.getMove())
558 
559  # rename the labels in the list of moves
560  for index in range(self.__sequence.getNumberofMoves()-1,-1,-1):
561  labelToModify = self.__listOfMoveLabels.item(index)
562  textToEdit = labelToModify.text()
563  listOfTextToEdit = textToEdit.split(' ')
564  listOfTextToEdit[1] = str(self.__sequence.getNumberofMoves()-index) + ':'
565  textToEdit = ' '.join(listOfTextToEdit)
566  self.__listOfMoveLabels.item(index).setText(textToEdit)
567 
568  def modifyMove(self,label):
569  """
570  Put a move to a modified state
571  :param label: label of the move
572  :return: No return
573  """
574  # Check if there's a move in modifying state
575  for row in range(self.__listOfMoveLabels.count()):
576  if not self.__listOfMoveLabels.item(row).getMove().isNew:
577  self.__listOfMoveLabels.item(row).getMove().isNew = True
578  self.__listOfMoveLabels.item(row).setBackground(QBrush(Qt.white))
579 
580  moveToModify = label.getMove()
581  moveToModify.isNew = False
582  label.setBackground(QBrush(Qt.darkCyan))
583  label.goToMoveOfTheLabel()
585 
586 class moveLabel(QListWidgetItem):
587  """
588  Class for the custom labels that are stored in the move list in the sequence creator
589  """
590  def __init__(self, move = None, text = None, motors = {}, parent=None):
591  """
592  Initialization of the move label
593  :param move: The move
594  :param text: The text in the label
595  :param motors: Dictionary of all the motors
596  :param parent: The Qt parent
597  """
598  QListWidgetItem.__init__(self, parent)
599  ## The move for the label
600  self.__move = move
601  ## The dictionary of all the motors
602  self.__motors = motors
603  self.setText(text)
604 
606  """
607  Handles the event when the move lable is double clicked
608  :return: No return1
609  """
610  self.__move.goToMove()
611 
612 
613  def getMove(self):
614  """
615  Accessor of the move of the label
616  :return: the move object
617  """
618  return self.__move
619 
620 # Class for a sequence of moves
621 class Sequence(QListWidgetItem):
622  """
623  Sequence is a series of moves that will be executed in a specific order
624  """
625  def __init__(self,motors = {}, name=""):
626  QListWidgetItem.__init__(self)
627  # QListWidgetItem method for setting the text of the item
628  self.setText(name)
629  ## A list of moves
630  self.__moves = []
631  ## The name of the sequence
632  self.__name = name
633  ## A dictionary of all the motors
634  self.__motors = motors
635 
636  def setName(self, name):
637  """
638  Sets the name of the sequence
639  :param name: The name
640  :return: No return
641  """
642  self.setText(name)
643  self.__name = name
644 
645  def getName(self):
646  """
647  Accesor for the sequence name
648  :return: The name of the sequence
649  """
650  return self.__name
651 
652  def addMove(self, newMove):
653  """
654  Adds a move to the list in the sequence
655  :param newMove: The move to add to the list
656  :return: No return
657  """
658  self.__moves.append(newMove)
659 
660  def getNumberofMoves(self):
661  """
662  :return: The number of moves in the sequence
663  """
664  return len(self.__moves)
665 
666  def getMoves(self):
667  """
668  Acessor of the moves of the sequence
669  :return: The moves of the sequence
670  """
671  return self.__moves
672 
673  def deleteMove(self, move):
674  """
675  Delete a specific move of the sequence
676  :param move: the move to be deleted
677  :return: No return
678  """
679  self.__moves.remove(move)
680 
681 # Class for a move of a sequence
682 class Move:
683  """
684  A move contains all the position of all the motors for a specific point in space
685  """
686  def __init__(self, motors = {}):
687  ## A dictionary of all motors
688  self.__motors = motors
689  ## To store the different positions of the move
690  self.__movePositions = dict()
691  ## State of the move (modified or new)
692  self.isNew = True
693  # Initialize move positions to an invalid position (-1)
694  for motor in self.__motors:
695  self.__movePositions[motor] = -1
696 
697  def setMotorPosition(self, motorName, position):
698  """
699  Setter of the position of a motor in the move
700  :param motorName: The name of the motor we want to set the positio
701  :param position: The position of the motor
702  :return: Return None if succesful and an error string if not
703  """
704  if motorName in self.__motors:
705  self.__movePositions[motorName] = position
706  return None
707  else:
708  return "There's no motor named that way"
709 
710  def getMotorPosition(self, motorName):
711  """
712  Accessor for the positions in the move
713  :param motorName: The name of the motor
714  :return: Return the position if succesful and an error string if not
715  """
716  if motorName in self.__movePositions:
717  return self.__movePositions[motorName]
718  else:
719  return "There's no motor named that way"
720 
721  def getMovePositions(self):
722  return self.__movePositions
723 
724  def goToMove(self):
725  for motor in self.__motors:
726  self.__motors[motor].setGoalPosition(self.getMotorPosition(motor))
727 
728 class Motor:
729  """
730  Class for a motor which has a position, a name and a status
731  """
732  def __init__(self, mainWindow=None, name="", goalPosition=0, status=False):
733  """
734  Initialization
735  :param mainWindow: The main window of the ui
736  :param name: The name of the motor
737  :param pos: The position of the motor
738  :param status: the status of the motor
739  """
740  ## The main window of the ui
741  self.__name = name
742  ## The goal position of the motor
743  self.__goalPosition = goalPosition
744  ## The current position of the motor
746  ## The status of the motor
747  self.__status = status
748  ## The main window of the ui
749  self.__window = mainWindow
750  self.mu = QMutex()
751 
752  def setGoalPosition(self, pos):
753  """
754  Setter of the goal positon
755  :param pos: the position
756  :return: No return
757  """
758  self.mu.lock()
759  self.__goalPosition=pos
760  self.mu.unlock()
761  print("%s: %d" % (self.__name, pos))
762  self.__window.sendMessage('a')
763 
764  def getGoalPosition(self):
765  """
766  Accessor of the goal position
767  :return: The goal position of the motor
768  """
769  self.mu.lock()
770  pos = self.__goalPosition
771  self.mu.unlock()
772  return pos
773 
774  def setCurrentPosition(self, pos):
775  """
776  Setter of the current position
777  :return: No return
778  """
779  self.mu.lock()
780  self.__currentPosition = pos
781  self.mu.unlock()
782 
784  """
785  Accessor of the current position
786  :return: The current position of the motor
787  """
788  self.mu.lock()
789  pos = self.__currentPosition
790  self.mu.unlock()
791  return pos
792 
793  def setName(self, name):
794  """
795  Setter of the name of the motor
796  :param name: The name of the motor
797  :return: No return
798  """
799  self.__name = name
800 
801  def getName(self):
802  """
803  Accessor of the name
804  :return: The name of the motor
805  """
806  return self.__name
807 
808  def setStatus(self, status):
809  """
810  Setter of the status of the motor
811  :param status: The status
812  :return: No return
813  """
814  self.__status = status
815 
816  def isEnabled(self):
817  """
818  Accessor of the status
819  :return: The status
820  """
821  return self.__status
822 
823 
824 class MainWindow(QMainWindow):
825  """
826  Main window class
827  """
828  def __init__(self, app):
829  """
830  MainWindow initialization
831  """
832  super(MainWindow, self).__init__()
833  ## app
834  self.app = app
835  ## ui object
836  self.ui = QUiLoader().load("mainwindow.ui")
837  self.ui.show()
838  self.ui.setWindowTitle("RoboAide")
839  self.setMinimumHeight(100)
840  self.setMinimumWidth(250)
841  # self.setMaximumHeight(200)
842  # self.setMaximumWidth(800)
843  self.setIcon()
844  self.msgMu = QMutex()
845 
846  self.numberOfMotors = 6
847  self.s, self.messageSize = makeStruct()
848 
849  # Connect button signals
850  self.ui.calibrateVerticalAxisButton.clicked.connect(self.calibrateVerticalAxis)
851 
853  self.populatePortsList()
854 
855  # ---------------
856  ## Dictionnary of all motor objects
857  self.dictMot = dict()
858  for i in range(1, self.numberOfMotors+1):
859  mot = Motor(self, "motor" + str(i))
860  self.dictMot[mot.getName()] = mot
861  # ---------------
862 
863  ## List of drawers
864  self.drawersList = []
865  for i in range(3):
866  self.drawersList.append(Drawer(self, "drawer" + str(i+1)))
867 
868  self.updateSliderPositions()
869 
870  ## ListOfSequencesHandler object
872 
873  # load the last save
875 
876  # Connect the slider signals
877  self.ui.slider_mot1.sliderMoved.connect(
878  lambda: self.dictMot["motor1"].setGoalPosition(self.ui.slider_mot1.value()))
879  self.ui.slider_mot2.sliderMoved.connect(
880  lambda: self.dictMot["motor2"].setGoalPosition(self.ui.slider_mot2.value()))
881  self.ui.slider_mot3.sliderMoved.connect(
882  lambda: self.dictMot["motor3"].setGoalPosition(self.ui.slider_mot3.value()))
883  self.ui.slider_mot4.sliderMoved.connect(
884  lambda: self.dictMot["motor4"].setGoalPosition(self.ui.slider_mot4.value()))
885  self.ui.slider_mot5.sliderMoved.connect(
886  lambda: self.dictMot["motor5"].setGoalPosition(self.ui.slider_mot5.value()))
887  self.ui.slider_mot6.sliderMoved.connect(
888  lambda: self.dictMot["motor6"].setGoalPosition(self.ui.slider_mot6.value()))
889 
890  # Connect button signals
891  self.ui.calibrateVerticalAxisButton.clicked.connect(self.calibrateVerticalAxis)
892 
893  # Connect drawer buttons signals
894  self.ui.drawer1Open.clicked.connect(self.drawersList[0].open)
895  self.ui.drawer2Open.clicked.connect(self.drawersList[1].open)
896  self.ui.drawer3Open.clicked.connect(self.drawersList[2].open)
897  self.ui.drawer1Close.clicked.connect(self.drawersList[0].close)
898  self.ui.drawer2Close.clicked.connect(self.drawersList[1].close)
899  self.ui.drawer3Close.clicked.connect(self.drawersList[2].close)
900 
901  # Connect the tab changed with updating the sliders
902  self.ui.tabWidget.currentChanged.connect(self.updateSliderPositions)
903 
904  # Serial communication
905  ## Outgoing message deque
906  self.msgDeque = deque(maxlen=3)
907  ## Stop indicator for the motors
908  self.shouldStop = False
909  ## Message reception QThread object
910  self.msgReception = MessageReception(self)
911  ## Message transmission QThread object
912  self.msgTransmission = MessageTransmission(self)
913  self.app.aboutToQuit.connect(self.msgReception.stop)
914  self.app.aboutToQuit.connect(self.msgTransmission.stop)
915  self.comm = None
916  self.serialConnected = None
917  try:
918  with open('SavePort.json') as save:
919  savedPort = json.load(save)
920  for index in range(1,len(self.ports_list)):
921  if self.ports_list[index].device==savedPort:
922  self.ui.portselection.setCurrentIndex(index)
923  self.connect_port(savedPort)
924  else:
925  print("The last port is not available")
926  except FileNotFoundError:
927  print("SavePort file not found")
928  self.ui.portselection.currentIndexChanged.connect(self.connect_port)
929 
930  def connect_port(self, lastPort=None):
931  """
932  Connect the selected port of the controller
933  :param: lastPort: name of the port that was last used
934  :return: None
935  """
936  if isinstance(lastPort, str):
937  commPort = lastPort
938  else:
939  commPort = self.ui.portselection.currentText()
940  self.comm, self.serialConnected = initSerialConnection(commPort)
941  if self.comm is not None:
942  self.app.aboutToQuit.connect(self.comm.close)
943  self.msgReception.start()
944  self.msgTransmission.start()
945  self.sendMessage('s')
946 
947  def setIcon(self):
948  """
949  Set main window icon
950  :return: None
951  """
952  appIcon = QIcon(icon)
953  self.ui.setWindowIcon(appIcon)
954 
955  def updateSliderPositions(self, index = 0):
956  """
957  Update motor slider positions
958  :return: None
959  """
960  if index == 0:
961  print("Updating motor slider positions")
962  self.ui.slider_mot1.setValue(self.dictMot["motor1"].getCurrentPosition())
963  self.ui.slider_mot2.setValue(self.dictMot["motor2"].getCurrentPosition())
964  self.ui.slider_mot3.setValue(self.dictMot["motor3"].getCurrentPosition())
965  self.ui.slider_mot4.setValue(self.dictMot["motor4"].getCurrentPosition())
966  self.ui.slider_mot5.setValue(self.dictMot["motor5"].getCurrentPosition())
967  self.ui.slider_mot6.setValue(self.dictMot["motor6"].getCurrentPosition())
968  print("Finished initializing slider positions")
969 
970  def populatePortsList(self):
971  """
972  Populate the available serial ports in the drop down menu
973  :return: None
974  """
975  print("Scanning and populating list of available serial ports")
976  for index in range(len(self.ports_list)):
977  result = isinstance(self.ports_list[index], str)
978  if not result:
979  self.ui.portselection.addItem(self.ports_list[index].device)
980  else:
981  self.ui.portselection.addItem(self.ports_list[index])
982 
983  def sendMessage(self, mode):
984  """
985  Package message and send on communication port
986  :param mode: mode in which the message should be interpreted by the controller
987  :return: None
988  """
989  if self.serialConnected:
990  values = (mode.encode(),
991  self.dictMot["motor1"].getGoalPosition(),
992  self.dictMot["motor2"].getGoalPosition(),
993  self.dictMot["motor3"].getGoalPosition(),
994  self.dictMot["motor4"].getGoalPosition(),
995  self.dictMot["motor5"].getGoalPosition(),
996  self.dictMot["motor6"].getGoalPosition(),
997  self.shouldStop,
998  self.drawersList[0].getState(),
999  self.drawersList[1].getState(),
1000  self.drawersList[2].getState(),
1001  b'\0')
1002  print("Outgoing: ", end='')
1003  print(values)
1004  packed_data = self.s.pack(*values)
1005  self.msgMu.lock()
1006  self.msgDeque.append(packed_data)
1007  self.msgMu.unlock()
1008  else:
1009  print("Error sending message, serial not connected")
1010 
1012  """
1013  Trigger vertical axis calibration
1014  :return: None
1015  """
1016  print("Calibrating vertical axis")
1017  self.sendMessage('c')
1018 
1019  def stopMotors(self):
1020  """
1021  Signal all motors to stop and empty message deque
1022  """
1023  self.shouldStop = True
1024  self.msgDeque.clear()
1025  self.sendMessage('a')
1026 
1027 
1029  """
1030  Make the struct and return it along with it's size in bytes
1031  :return: struct object, byte size of the struct
1032  """
1033  ## Define struct format for communicating messages
1034  # c: mode
1035  # 'a' -> absolute (send an absolute position between 0 and 100)
1036  # 'i' -> incremental (move from the current position from a certain increment)
1037  # 's' -> status (with this mode, the rest of the message is ignored. It's purpose is only for the controller to send it's status)
1038  # 'c' -> calibrate vertical axis (rest of the message is ignored)
1039  # c: operation mode
1040  # 6H: position of all motors
1041  # ?: stop indicator for all motors
1042  # 3?: open/close drawers (True = open, False = close)
1043  # c: end-of-message character
1044  #TODO: try adding little endian (<) to struct definition
1045  structDefinition = 'c6H?3?c'
1046  s = struct.Struct(structDefinition)
1047  return s, struct.calcsize(structDefinition)
1048 
1049 
1050 
1051 if __name__ == "__main__":
1052  app = QApplication(sys.argv)
1053  window = MainWindow(app)
1054  app.exec_()
1055  sys.exit()
dictMot
Dictionnary of all motor objects.
Definition: RoboAide.py:857
listOfSequencesHandler
ListOfSequencesHandler object.
Definition: RoboAide.py:871
def goToMove(self)
Definition: RoboAide.py:724
def loadSequences(listOfSequenceHandler, motors)
Definition: RoboAide.py:103
__window
The window in which the new sequence will be created.
Definition: RoboAide.py:147
def deleteMove(self, move)
Definition: RoboAide.py:673
__sequence
The new sequence.
Definition: RoboAide.py:272
def __init__(self, move=None, text=None, motors={}, parent=None)
Definition: RoboAide.py:590
def goToMoveOfTheLabel(self)
Definition: RoboAide.py:605
def connect_port(self, lastPort=None)
Definition: RoboAide.py:930
def __init__(self, motors={}, name="")
Definition: RoboAide.py:625
__createSequenceButton
The create a new sequence button.
Definition: RoboAide.py:141
__nameLabel
The label for the widget in which the name of the sequence is written.
Definition: RoboAide.py:281
msgReception
Message reception QThread object.
Definition: RoboAide.py:910
__movePositions
To store the different positions of the move.
Definition: RoboAide.py:690
isNew
State of the move (modified or new)
Definition: RoboAide.py:692
__layout
The layout of the create sequence window.
Definition: RoboAide.py:276
def updateSliderPositions(self, index=0)
Definition: RoboAide.py:955
def setName(self, name)
Definition: RoboAide.py:793
def setGoalPosition(self, pos)
Definition: RoboAide.py:752
def __init__(self, app)
Definition: RoboAide.py:828
def getMovePositions(self)
Definition: RoboAide.py:721
__motors
The motors of the robot.
Definition: RoboAide.py:58
__window
The main window of the ui.
Definition: RoboAide.py:749
def __init__(self, mainWindow, motors)
Definition: RoboAide.py:129
__ui
The ui in which the list of sequence is in.
Definition: RoboAide.py:143
def rightClickMenu(self, event)
Definition: RoboAide.py:535
__motors
The dictionary of all the motors.
Definition: RoboAide.py:136
__motors
A dictionary of all the motors.
Definition: RoboAide.py:634
listOfSliders
List of sliders in the create sequence window.
Definition: RoboAide.py:299
playSequenceThread
Thread to send the sequence to the motors.
Definition: RoboAide.py:149
__listOfSequencesHandler
The handler of the different sequences.
Definition: RoboAide.py:60
__mainWindow
The main window of the ui.
Definition: RoboAide.py:145
robotInMotionMessage
Message to make the user put a name to the sequence TODO: Set the size of the message to something ap...
Definition: RoboAide.py:152
def modifyMove(self, label)
Definition: RoboAide.py:568
__warningMessage
Warning message to make sure the user doen&#39;t want to save the sequence.
Definition: RoboAide.py:319
__listOfMoveLabels
The list of the different moves that forms the sequence.
Definition: RoboAide.py:283
__mainWindow
The main window of the UI.
Definition: RoboAide.py:62
__modifySequence
Flag if the sequence is a modified one or a new one.
Definition: RoboAide.py:266
def addMove(self, newMove)
Definition: RoboAide.py:652
def calibrateVerticalAxis(self)
Definition: RoboAide.py:1011
def getName(self)
Definition: RoboAide.py:801
__noNameMessage
Message to make the user put a name to the sequence.
Definition: RoboAide.py:310
__moves
A list of moves.
Definition: RoboAide.py:630
__name
The main window of the ui.
Definition: RoboAide.py:741
def makeStruct()
Definition: RoboAide.py:1028
msgTransmission
Message transmission QThread object.
Definition: RoboAide.py:912
def addSequenceToList(self, modifySequence=False)
Definition: RoboAide.py:390
def setCurrentPosition(self, pos)
Definition: RoboAide.py:774
__listOfSequenceHandler
The handler of the list of sequence.
Definition: RoboAide.py:268
__goalPosition
The goal position of the motor.
Definition: RoboAide.py:743
drawersList
List of drawers.
Definition: RoboAide.py:864
buttonBox
Buttons to accept or cancel the creation of a sequence.
Definition: RoboAide.py:339
nextMoveButton
Button to add a move to the sequence and procede to the next move.
Definition: RoboAide.py:336
def deleteMove(self, label)
Definition: RoboAide.py:548
__currentPosition
The current position of the motor.
Definition: RoboAide.py:745
def getGoalPosition(self)
Definition: RoboAide.py:764
def __init__(self, motors={})
Definition: RoboAide.py:686
__status
The status of the motor.
Definition: RoboAide.py:747
def getCurrentPosition(self)
Definition: RoboAide.py:783
__motorLabels
Labels for the motors in the UI.
Definition: RoboAide.py:331
def getNumberofMoves(self)
Definition: RoboAide.py:660
__wantedPositions
A dictionary of the positions of the new sequence.
Definition: RoboAide.py:274
nameEntry
The widget for the name of the sequenc.
Definition: RoboAide.py:278
def setName(self, name)
Definition: RoboAide.py:636
def __init__(self, mainWindow=None, name="", goalPosition=0, status=False)
Definition: RoboAide.py:732
def populatePortsList(self)
Definition: RoboAide.py:970
def initSerialConnection(port)
def sendMessage(self, mode)
Definition: RoboAide.py:983
def __init__(self, motors={}, listOfSequenceHandler=None, sequence=None, modifySequence=False)
Definition: RoboAide.py:254
__motors
The dictionary of all the motors.
Definition: RoboAide.py:602
__motors
The dictionary of all the motors.
Definition: RoboAide.py:270
def moveDoubleClicked(self, moveItem)
Definition: RoboAide.py:513
def setMotorPosition(self, motorName, position)
Definition: RoboAide.py:697
def createWindow(self, modifySequence=False)
Definition: RoboAide.py:200
def __init__(self, moves, motors, listOfSequencesHandler, mainWindow)
Definition: RoboAide.py:46
def setStatus(self, status)
Definition: RoboAide.py:808
shouldStop
Stop indicator for the motors.
Definition: RoboAide.py:908
__moves
The moves of the sequence containing the different positions to reach.
Definition: RoboAide.py:56
msgDeque
Outgoing message deque.
Definition: RoboAide.py:906
def getMoves(self)
Definition: RoboAide.py:666
__motors
A dictionary of all motors.
Definition: RoboAide.py:688
def isEnabled(self)
Definition: RoboAide.py:816
__name
The name of the sequence.
Definition: RoboAide.py:632
__move
The move for the label.
Definition: RoboAide.py:600
__listOfSequences
The list of sequence.
Definition: RoboAide.py:138
def getName(self)
Definition: RoboAide.py:645
def getMotorPosition(self, motorName)
Definition: RoboAide.py:710