#!/usr/bin/env python3 import argparse, logging, os, sys, traceback, time from logging.handlers import RotatingFileHandler from urllib.parse import urlparse # openCV imports from cv2 import cvtColor, COLOR_BGR2RGB # Qt imports from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QSize, QThread, QMutex from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QStyle, QWidget, QMenu, QAction, QStatusBar, QLabel, QHBoxLayout, QVBoxLayout, QTextEdit, QPushButton, QApplication, QTabWidget, QButtonGroup, QGridLayout, QFrame, QCheckBox # Other imports import json import numpy as np import copy import threading # Custom modules import from modules.SettingsDialog import SettingsDialog from modules.ConnectionDialog import ConnectionDialog from modules.DetectionManager import DetectionManager from modules.PrinterManager import PrinterManager from modules.StatusTipFilter import StatusTipFilter ########################################################################### Core application class class App(QMainWindow): # Global mutex __mutex = QMutex() # Signals ######## Detection Manager startVideoSignal = pyqtSignal() setImagePropertiesSignal = pyqtSignal(object) getVideoFrameSignal = pyqtSignal() # Endstop detection signals toggleEndstopDetectionSignal = pyqtSignal(bool) toggleEndstopAutoDetectionSignal = pyqtSignal(bool) # Nozzle detection signals toggleNozzleDetectionSignal = pyqtSignal(bool) toggleNozzleAutoDetectionSignal = pyqtSignal(bool) # UV coordinates update signal getUVCoordinatesSignal = pyqtSignal() # Master detection enable/disable signal toggleDetectionSignal = pyqtSignal(bool) ######## Printer Manager connectSignal = pyqtSignal(object) disconnectSignal = pyqtSignal(object) moveRelativeSignal = pyqtSignal(object) moveAbsoluteSignal = pyqtSignal(object) callToolSignal = pyqtSignal(int) unloadToolSignal = pyqtSignal() pollCoordinatesSignal = pyqtSignal() pollCurrentToolSignal = pyqtSignal() setOffsetsSignal = pyqtSignal(object) limitAxesSignal = pyqtSignal() flushBufferSignal = pyqtSignal() saveToFirmwareSignal = pyqtSignal() announceSignal = pyqtSignal(bool) # Settings Dialog resetImageSignal = pyqtSignal() pushbuttonSize = 38 # default move speed in feedrate/min __moveSpeed = 6000 # Maximum number of retries for detection __maxRetries = 3 # Maximum runtime (in seconds) for calibration cycles __maxRuntime = 120 # Timeout for Qthread termination (DetectionManager and PrinterManager) __detectionManagerThreadWaitTime = 20 __printerManagerThreadWaitTime = 60 ########################################################################### Initialize class def __init__(self, parent=None): # send calling to log _logger.debug('*** calling App.__init__') # output greeting to log _logger.info('Initializing application.. ') # call QMainWindow init function super().__init__() #### class attributes definition if(True): # main window size self.windowWidthOriginal, self.windowHeightOriginal = 800, 600 # main camera capture size # self.imageWidthOriginal, self.imageHeightOriginal = 640, 480 # stylesheets used for interface status updates self.styleGreen = '* { background-color: green; color: white;} QToolTip { color: black; background-color: #DDDDDD } *:hover{background-color: #003874; color: #ffa000;} *:pressed{background-color: #ffa000; color: #003874;}' self.styleRed = '* { background-color: red; color: white;} QToolTip { color: black; background-color: #DDDDDD }' self.styleDisabled = '* { background-color: #cccccc; color: #999999; border-style: solid;} QToolTip { color: black; background-color: #DDDDDD }' self.styleOrange = '* { background-color: dark-grey; color: #ffa000;} QToolTip { color: black; background-color: #DDDDDD }' self.styleBlue = '* { background-color: #003874; color: #ffa000;} QToolTip { color: black; background-color: #DDDDDD }' self.styleDefault = '* { background-color: rgba(0,0,0,0); color: black;} QToolTip { color: black; background-color: #DDDDDD }' # standby image placeholder self.standbyImage = QPixmap('./resources/background.png') self.errorImage = QPixmap('./resources/error.png') # user-defined cameras array self.__cameras = [] # active camera self.__activeCamera ={} # user-defined printers array self.__printers = [] # active printer self.__activePrinter = {} # Control Point self.__cpCoordinates = {'X': None, 'Y': None, 'Z': None} self.__currentPosition = {'X': None, 'Y': None, 'Z': None} self.__stateManualCPCapture = False self.__stateAutoCPCapture = False self.__stateEndstopAutoCalibrate = False self.__restorePosition = None self.__firstConnection = False self.state = 0 # Camera transform matrix self.transformMatrix = None self.transform_input = None self.mpp = None # Nozzle detection self.__stateAutoNozzleAlignment = False self.__stateOverrideManualNozzleAlignment = False self.singleToolOffsetsCapture = False self.__displayCrosshair = False #### setup window properties if(True): _logger.debug(' .. setting up window properties..') self.setWindowFlag(Qt.WindowContextHelpButtonHint,False) self.setWindowTitle('TAMV') self.setWindowIcon(QIcon('./resources/jubilee.png')) screen = QDesktopWidget().availableGeometry() self.setGeometry(QStyle.alignedRect(Qt.LeftToRight,Qt.AlignHCenter,QSize(self.windowWidthOriginal,self.windowHeightOriginal),screen)) self.setMinimumWidth(self.windowWidthOriginal) self.setMinimumHeight(self.windowHeightOriginal) appScreen = self.frameGeometry() appScreen.moveCenter(screen.center()) self.move(appScreen.topLeft()) self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) # create stylehseets self.globalStylesheet = '\ QLabel#instructions_text {\ background-color: rgba(255,153,0,.4);\ }\ QPushButton {\ border: 1px solid #adadad;\ border-style: outset;\ border-radius: 4px;\ font: 16px;\ padding: 2px;\ }\ QPushButton>QToolTip {\ color: black;\ }\ QPushButton#calibrating:enabled {\ background-color: orange;\ color: white;\ }\ QPushButton#completed:enabled {\ background-color: blue;\ color: white;\ }\ QPushButton:hover,QPushButton:enabled:hover,QPushButton:enabled:!checked:hover,QPushButton#completed:enabled:hover {\ background-color: #003874;\ color: #ffa000;\ border: 1px solid #aaaaaa;\ }\ QPushButton:pressed,QPushButton:enabled:pressed,QPushButton:enabled:checked,QPushButton#completed:enabled:pressed {\ background-color: #ffa000;\ border: 1px solid #aaaaaa;\ }\ QPushButton:enabled {\ background-color: green;\ color: white;\ }\ QLabel#labelPlus {\ font: 20px;\ padding: 0px;\ }\ QPushButton#plus:enabled {\ font: 20px;\ padding: 0px;\ background-color: #eeeeee;\ color: #000000;\ }\ QPushButton#plus:enabled:hover {\ font: 20px;\ padding: 0px;\ background-color: green;\ color: #000000;\ }\ QPushButton#plus:enabled:pressed {\ font: 20px;\ padding: 0px;\ background-color: #FF0000;\ color: #222222;\ }\ QPushButton#debug,QMessageBox > #debug {\ background-color: blue;\ color: white;\ }\ QPushButton#debug:hover, QMessageBox > QAbstractButton#debug:hover {\ background-color: green;\ color: white;\ }\ QPushButton#debug:pressed, QMessageBox > QAbstractButton#debug:pressed {\ background-color: #ffa000;\ border-style: inset;\ color: white;\ }\ QPushButton#active, QMessageBox > QAbstractButton#active {\ background-color: green;\ color: white;\ }\ QPushButton#active:pressed,QMessageBox > QAbstractButton#active:pressed {\ background-color: #ffa000;\ }\ QPushButton#terminate {\ background-color: red;\ color: white;\ }\ QPushButton#terminate:pressed {\ background-color: #c0392b;\ }\ QPushButton:disabled, QPushButton#terminate:disabled {\ background-color: #cccccc;\ color: #999999;\ }\ QInputDialog QDialogButtonBox > QPushButton:enabled, QDialog QPushButton:enabled,QPushButton[checkable="true"]:enabled {\ background-color: none;\ color: black;\ border: 1px solid #adadad;\ border-style: outset;\ border-radius: 4px;\ font: 14px;\ padding: 6px;\ }\ QPushButton:enabled:checked {\ background-color: #ffa000;\ border: 1px solid #aaaaaa;\ }\ QInputDialog QDialogButtonBox > QPushButton:pressed, QDialog QPushButton:pressed {\ background-color: #ffa000;\ }\ QInputDialog QDialogButtonBox > QPushButton:hover:!pressed, QDialog QPushButton:hover:!pressed {\ background-color: #003874;\ color: #ffa000;\ }\ QToolTip, QLabel > QToolTip, QPushButton > QLabel> QToolTip {\ color: black;\ }\ ' self.setStyleSheet(self.globalStylesheet) #### Driver API imports if(True): try: self.__firmwareList = [] self.__driverList = [] with open('drivers.json','r') as inputfile: driverJSON = json.load(inputfile) _logger.info(' .. loading drivers..') for driverEntry in driverJSON: self.__firmwareList.append(driverEntry['firmware']) self.__driverList.append(driverEntry['filename']) except: _logger.critical('Cannot load driver definitions: ' + traceback.format_exc()) raise SystemExit('Cannot load driver definitions.') #### Setup components # load user parameters if(True): try: with open('./config/settings.json','r') as inputfile: self.__userSettings = json.load(inputfile) except FileNotFoundError: try: # try and see if moving from older build with open('./settings.json','r') as inputfile: self.__userSettings = json.load(inputfile) try: _logger.info(' .. moving settings file to /config/settings.json.. ') # create config folder if it doesn't exist os.makedirs('./config',exist_ok=True) # move settings.json to new config folder os.replace('./settings.json','./config/settings.json') except: _logger.warning('Cannot rename old settings.json, leaving in place and using new file.') except FileNotFoundError: # create config folder if it doesn't exist os.makedirs('./config',exist_ok=True) # No settings file defined, create a default file _logger.info(' .. creating new settings.json..') self.__userSettings = {} # create a camera array self.__userSettings['camera'] = [ { 'video_src': 0, 'display_width': '640', 'display_height': '480', 'default': 1 } ] # Create a printer array self.__userSettings['printer'] = [ { 'address': 'http://localhost', 'password': 'reprap', 'name': 'My Duet', 'nickname': 'Default', 'controller' : 'RRF', 'version': '', 'default': 1, 'rotated': 0, 'tools': [ { 'number': 0, 'name': 'Tool 0', 'nozzleSize': 0.4, 'offsets': [0,0,0] } ] } ] try: # set class attributes self._cameraWidth = int(self.__userSettings['camera'][0]['display_width']) self._cameraHeight = int(self.__userSettings['camera'][0]['display_height']) self._videoSrc = self.__userSettings['camera'][0]['video_src'] # save default settings file with open('./config/settings.json','w') as outputfile: json.dump(self.__userSettings, outputfile) except Exception as e1: errorMsg = 'Error reading user settings file.' + traceback.format_exc() _logger.critical(errorMsg) raise SystemExit(errorMsg) _logger.info(' .. reading configuration settings..') # Fetch defined cameras if(True): defaultCameraDefined = False for source in self.__userSettings['camera']: try: self.__cameras.append(source) if(source['default'] == 1 and defaultCameraDefined is False): self.__activeCamera = source defaultCameraDefined = True elif(defaultCameraDefined): source['default'] = 0 continue except KeyError as ke: source['default'] = 0 continue if(defaultCameraDefined is False): self.__userSettings['camera'][0]['default'] = 1 self.__activeCamera = self.__userSettings['camera'][0] self._cameraHeight = int(self.__activeCamera['display_height']) self._cameraWidth = int(self.__activeCamera['display_width']) self._videoSrc = self.__activeCamera['video_src'] if(len(str(self._videoSrc)) == 1 or str(self._videoSrc) == "-1"): self._videoSrc = int(self._videoSrc) # Fetch defined machines if(True): defaultPrinterDefined = False for machine in self.__userSettings['printer']: # Find default printer first try: self.__printers.append(machine) if(machine['default'] == 1): self.__activePrinter = machine defaultPrinterDefined = True except KeyError as ke: # no default field detected - create a default if not already done if(defaultPrinterDefined is False): machine['default'] = 1 defaultPrinterDefined = True else: machine['default'] = 0 # Check if password doesn't exist try: temp = machine['password'] except KeyError: machine['password'] = 'reprap' # Check if nickname doesn't exist try: temp = machine['nickname'] except KeyError: machine['nickname'] = machine['name'] # Check if controller doesn't exist try: temp = machine['controller'] except KeyError: machine['controller'] = 'RRF' # Check if version doesn't exist try: temp = machine['version'] except KeyError: machine['version'] = '' # Check if rotated kinematics doesn't exist try: temp = machine['rotated'] except KeyError: machine['rotated'] = 0 # Check if tools doesn't exist try: temp = machine['tools'] except KeyError: machine['tools'] = [ { 'number': 0, 'name': 'Tool 0', 'nozzleSize': 0.4, 'offsets': [0,0,0] } ] if(machine['default'] == 1): self.__activePrinter = machine # Check if we have no default machine if(defaultPrinterDefined is False): self.__activePrinter = self.__userSettings['printer'][0] if(self.__activePrinter['controller'] == 'RRF'): (_errCode, _errMsg, self.printerURL) = self.sanitizeURL(self.__activePrinter['address']) self.__activePrinter['address'] = self.printerURL if _errCode > 0: # invalid input _logger.error('Invalid printer URL detected in settings.json') _logger.error(_errMsg) _logger.info('Defaulting to \"http://localhost\"...') self.printerURL = 'http://localhost' ##### Settings Dialog self.__settingsGeometry = None # Note: settings dialog is created when user clicks the button #### Setup interface ##### Menu bar self.setupMenu() ##### Status bar self.setupStatusbar() ##### GUI elements self.setupMainWindow() # send exiting to log _logger.debug('*** exiting App.__init__') ########################################################################### Menu setup def setupMenu(self): # send calling to log _logger.debug('*** calling App.setupMenu') self.menubar = self.menuBar() self.menubar.installEventFilter(StatusTipFilter(self)) #### File menu fileMenu = QMenu('&File', self) self.menubar.addMenu(fileMenu) #### Preferences self.preferencesAction = QAction(self) self.preferencesAction.setText('&Preferences..') self.preferencesAction.triggered.connect(self.displayPreferences) fileMenu.addAction(self.preferencesAction) #### Save offsets to firmware self.saveFirmwareAction = QAction(self) self.saveFirmwareAction.setText('&Save offsets..') self.saveFirmwareAction.triggered.connect(self.saveOffsets) fileMenu.addAction(self.saveFirmwareAction) # Quit self.quitAction = QAction(self) self.quitAction.setText('&Quit') self.quitAction.triggered.connect(self.close) fileMenu.addSeparator() fileMenu.addAction(self.quitAction) # send exiting to log _logger.debug('*** exiting App.setupMenu') ########################################################################### Status bar setup def setupStatusbar(self): # send calling to log _logger.debug('*** calling App.setupStatusbar') self.statusBar = QStatusBar() self.statusBar.showMessage('Welcome.') self.setStatusBar(self.statusBar) #### CP coodinate status self.cpLabel = QLabel('CP: undef') self.statusBar.addPermanentWidget(self.cpLabel) self.cpLabel.setStyleSheet(self.styleOrange) #### Connection status self.connectionStatusLabel = QLabel('Disconnected') self.connectionStatusLabel.setStyleSheet(self.styleOrange) self.statusBar.addPermanentWidget(self.connectionStatusLabel) # send exiting to log _logger.debug('*** exiting App.setupStatusbar') ########################################################################### Main window setup def setupMainWindow(self): # send calling to log _logger.debug('*** calling App.setupMenu') self.containerLayout = QVBoxLayout() self.containerLayout.setSpacing(8) self.centralWidget.setLayout(self.containerLayout) #### Main grid layout if(True): self.mainLayout = QHBoxLayout() self.mainLayout.setSpacing(8) self.containerLayout.addLayout(self.mainLayout) ##### Left toolbar if(True): self.leftToolbarLayout = QVBoxLayout() self.leftToolbarLayout.setAlignment(Qt.AlignTop) self.mainLayout.addLayout(self.leftToolbarLayout) # Connect button self.connectButton = QPushButton('+') self.connectButton.setStyleSheet(self.styleDisabled) self.connectButton.setMinimumSize(self.pushbuttonSize,self.pushbuttonSize) self.connectButton.setMaximumSize(self.pushbuttonSize,self.pushbuttonSize) self.connectButton.setToolTip('Connect..') self.connectButton.setDisabled(True) self.connectButton.clicked.connect(self.connectPrinter) self.leftToolbarLayout.addWidget(self.connectButton)#, 1, 0, 1, 1, Qt.AlignLeft|Qt.AlignTop) # Disconnect button self.disconnectButton = QPushButton('D') self.disconnectButton.setStyleSheet(self.styleDisabled) self.disconnectButton.setMinimumSize(self.pushbuttonSize,self.pushbuttonSize) self.disconnectButton.setMaximumSize(self.pushbuttonSize,self.pushbuttonSize) self.disconnectButton.setToolTip('Disconnect..') self.disconnectButton.setDisabled(True) self.disconnectButton.clicked.connect(self.haltPrinterOperation) self.leftToolbarLayout.addWidget(self.disconnectButton)#, 1, 0, 1, 1, Qt.AlignLeft|Qt.AlignTop) # Crosshair button self.crosshairDisplayButton = QPushButton('-+-') self.crosshairDisplayButton.setStyleSheet(self.styleBlue) self.crosshairDisplayButton.setMinimumSize(self.pushbuttonSize,self.pushbuttonSize) self.crosshairDisplayButton.setMaximumSize(self.pushbuttonSize,self.pushbuttonSize) self.crosshairDisplayButton.setToolTip('toggle crosshair on display') self.crosshairDisplayButton.setDisabled(False) self.crosshairDisplayButton.setChecked(False) self.crosshairDisplayButton.clicked.connect(self.toggleCrosshair) self.leftToolbarLayout.addWidget(self.crosshairDisplayButton) # Setup Control Point button self.cpSetupButton = QPushButton('CP') self.cpSetupButton.setStyleSheet(self.styleDisabled) self.cpSetupButton.setMinimumSize(self.pushbuttonSize,self.pushbuttonSize) self.cpSetupButton.setMaximumSize(self.pushbuttonSize,self.pushbuttonSize) self.cpSetupButton.setToolTip('Setup Control Point..') self.cpSetupButton.setDisabled(True) self.cpSetupButton.clicked.connect(self.setupCPCapture) self.leftToolbarLayout.addWidget(self.cpSetupButton) # CP Automated Capture button self.cpAutoCaptureButton = QPushButton('Auto') self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled) self.cpAutoCaptureButton.setMinimumSize(self.pushbuttonSize,self.pushbuttonSize) self.cpAutoCaptureButton.setMaximumSize(self.pushbuttonSize,self.pushbuttonSize) self.cpAutoCaptureButton.setToolTip('Automated CP Capture..') self.cpAutoCaptureButton.setDisabled(True) self.cpAutoCaptureButton.clicked.connect(self.setupCPAutoCapture) self.leftToolbarLayout.addWidget(self.cpAutoCaptureButton) ##### Main image preview if(True): self.image = QLabel(self) self.image.resize(self._cameraWidth, self._cameraHeight) self.image.setMinimumWidth(self._cameraWidth) self.image.setMinimumHeight(self._cameraHeight) self.image.setAlignment(Qt.AlignLeft) # self.image.setStyleSheet('text-align:center; border: 1px solid black') self.image.setPixmap(self.standbyImage) self.mainLayout.addWidget(self.image)#, 1, 1, 1, -1, Qt.AlignLeft|Qt.AlignTop) ##### Right toolbar if(True): # Jog Panel self.tabPanel = QTabWidget() self.firstTab = QWidget() self.jogPanel = QGridLayout() self.jogPanel.setAlignment(Qt.AlignRight|Qt.AlignTop) self.jogPanel.setSpacing(5) # create jogPanel buttons ## increment size self.button_1 = QPushButton('1') self.button_1.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_1.setMaximumHeight(self.pushbuttonSize) self.button_1.setToolTip('set jog distance to 1 unit') self.button_01 = QPushButton('.1') self.button_01.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_01.setMaximumHeight(self.pushbuttonSize) self.button_01.setToolTip('set jog distance to 0.1 unit') self.button_001 = QPushButton('.01') self.button_001.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_001.setMaximumHeight(self.pushbuttonSize) self.button_001.setToolTip('set jog distance to 0.01 unit') self.incrementButtonGroup = QButtonGroup() self.incrementButtonGroup.addButton(self.button_1) self.incrementButtonGroup.addButton(self.button_01) self.incrementButtonGroup.addButton(self.button_001) self.incrementButtonGroup.setExclusive(True) self.button_1.setCheckable(True) self.button_01.setCheckable(True) self.button_001.setCheckable(True) self.button_1.setChecked(True) # horizontal separators self.incrementLine = QFrame() self.incrementLine.setFrameShape(QFrame.HLine) self.incrementLine.setLineWidth(1) self.incrementLine.setFrameShadow(QFrame.Sunken) self.keypadLine = QFrame() self.keypadLine.setFrameShape(QFrame.HLine) self.keypadLine.setFrameShadow(QFrame.Sunken) self.keypadLine.setLineWidth(1) ## X movement self.button_x_left = QPushButton('-X', objectName='plus') self.button_x_left.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_x_left.setMaximumHeight(self.pushbuttonSize) self.button_x_left.setToolTip('jog X-') self.button_x_left.clicked.connect(self.xleftClicked) self.button_x_right = QPushButton('X+', objectName='plus') self.button_x_right.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_x_right.setMaximumHeight(self.pushbuttonSize) self.button_x_right.setToolTip('jog X+') self.button_x_right.clicked.connect(self.xRightClicked) ## Y movement self.button_y_left = QPushButton('-Y', objectName='plus') self.button_y_left.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_y_left.setMaximumHeight(self.pushbuttonSize) self.button_y_left.setToolTip('jog Y-') self.button_y_left.clicked.connect(self.yleftClicked) self.button_y_right = QPushButton('Y+', objectName='plus') self.button_y_right.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_y_right.setMaximumHeight(self.pushbuttonSize) self.button_y_right.setToolTip('jog Y+') self.button_y_right.clicked.connect(self.yRightClicked) ## Z movement self.button_z_down = QPushButton('-Z', objectName='plus') self.button_z_down.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_z_down.setMaximumHeight(self.pushbuttonSize) self.button_z_down.setToolTip('jog Z-') self.button_z_down.clicked.connect(self.zleftClicked) self.button_z_up = QPushButton('Z+', objectName='plus') self.button_z_up.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) self.button_z_up.setMaximumHeight(self.pushbuttonSize) self.button_z_up.setToolTip('jog Z+') self.button_z_up.clicked.connect(self.zRightClicked) ## layout jogPanel buttons # add increment buttons self.jogPanel.addWidget(self.button_001,0,0) self.jogPanel.addWidget(self.button_01,0,1) self.jogPanel.addWidget(self.button_1,1,1) # add separator self.jogPanel.addWidget(self.incrementLine, 2,0,1,2) # add X movement buttons self.jogPanel.addWidget(self.button_x_left,3,0) self.jogPanel.addWidget(self.button_x_right,3,1) # add Y movement buttons self.jogPanel.addWidget(self.button_y_left,4,0) self.jogPanel.addWidget(self.button_y_right,4,1) # add Z movement buttons self.jogPanel.addWidget(self.button_z_down,5,0) self.jogPanel.addWidget(self.button_z_up,5,1) # add separator self.jogPanel.addWidget(self.keypadLine, 6,0,1,2) self.tabPanel.setDisabled(True) self.mainLayout.addWidget(self.tabPanel) self.firstTab.setLayout(self.jogPanel) self.tabPanel.addTab(self.firstTab,'Jog') self.tabPanel.setFixedWidth(95) self.tabPanel.setTabBarAutoHide(True) self.tabPanel.setStyleSheet('QTabWidget::pane {\ margin: -13px -9px -13px -9px;\ border: 1px solid white;\ padding: 0px;\ }') #### Footer layout if(True): self.footerLayout = QGridLayout() self.footerLayout.setSpacing(0) self.containerLayout.addLayout(self.footerLayout) # Intructions box self.instructionsBox = QTextEdit() self.instructionsBox.setReadOnly(True) self.instructionsBox.setFixedSize(640, 45) self.instructionsBox.setVisible(False) self.footerLayout.addWidget(self.instructionsBox, 0,0,1,-1,Qt.AlignLeft|Qt.AlignVCenter) # Manual CP Capture button self.manualCPCaptureButton = QPushButton('Save CP') self.manualCPCaptureButton.setStyleSheet(self.styleDisabled) self.manualCPCaptureButton.setMinimumSize(self.pushbuttonSize*2,self.pushbuttonSize) self.manualCPCaptureButton.setMaximumSize(self.pushbuttonSize*2,self.pushbuttonSize) self.manualCPCaptureButton.setToolTip('Capture current machine coordinates as the Control Point.') self.manualCPCaptureButton.setDisabled(True) self.manualCPCaptureButton.setVisible(False) self.manualCPCaptureButton.clicked.connect(self.manualCPCapture) self.footerLayout.addWidget(self.manualCPCaptureButton, 0,1,1,1,Qt.AlignRight|Qt.AlignVCenter) # Override Manual Tool offset Capture button self.overrideManualOffsetCaptureButton = QPushButton('Capture offset') self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled) self.overrideManualOffsetCaptureButton.setMinimumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.overrideManualOffsetCaptureButton.setMaximumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.overrideManualOffsetCaptureButton.setToolTip('Capture current position and calculate tool offset.') self.overrideManualOffsetCaptureButton.setDisabled(True) self.overrideManualOffsetCaptureButton.setVisible(False) self.overrideManualOffsetCaptureButton.clicked.connect(self.overrideManualToolOffsetCapture) self.footerLayout.addWidget(self.overrideManualOffsetCaptureButton, 0,1,1,1,Qt.AlignRight|Qt.AlignVCenter) # Manual Tool offset Capture button self.manualToolOffsetCaptureButton = QPushButton('Capture offset') self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled) self.manualToolOffsetCaptureButton.setMinimumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.manualToolOffsetCaptureButton.setMaximumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.manualToolOffsetCaptureButton.setToolTip('Capture current position and calculate tool offset.') self.manualToolOffsetCaptureButton.setDisabled(True) self.manualToolOffsetCaptureButton.setVisible(False) self.manualToolOffsetCaptureButton.clicked.connect(self.manualToolOffsetCapture) self.footerLayout.addWidget(self.manualToolOffsetCaptureButton, 0,0,1,1,Qt.AlignRight|Qt.AlignVCenter) # Start Alignment button self.startAlignToolsButton = QPushButton('Align Tools') self.startAlignToolsButton.setStyleSheet(self.styleDisabled) self.startAlignToolsButton.setMinimumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.startAlignToolsButton.setMaximumSize(self.pushbuttonSize*3,self.pushbuttonSize) self.startAlignToolsButton.setToolTip('Start automated tool offset calibration') self.startAlignToolsButton.setDisabled(True) self.startAlignToolsButton.setVisible(False) self.startAlignToolsButton.clicked.connect(self.startAlignTools) self.footerLayout.addWidget(self.startAlignToolsButton, 0,1,1,1,Qt.AlignRight|Qt.AlignVCenter) # Resume auto alignment button self.resumeAutoToolAlignmentButton = QPushButton('Resume Auto Align') self.resumeAutoToolAlignmentButton.setMinimumSize(self.pushbuttonSize*4,self.pushbuttonSize) self.resumeAutoToolAlignmentButton.setMaximumSize(self.pushbuttonSize*4,self.pushbuttonSize) self.resumeAutoToolAlignmentButton.setToolTip('Resume automated calibration') self.resumeAutoToolAlignmentButton.setDisabled(True) self.resumeAutoToolAlignmentButton.setVisible(False) self.resumeAutoToolAlignmentButton.clicked.connect(self.resumeAutoAlignment) self.footerLayout.addWidget(self.resumeAutoToolAlignmentButton, 0,0,1,1,Qt.AlignRight|Qt.AlignVCenter) #### Set current interface state to disconnected self.stateDisconnected() # send exiting to log _logger.debug('*** exiting App.setupMenu') ########################################################################### Jog Panel functions def xleftClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'X': str(-1*incrementDistance)}} self.moveRelativeSignal.emit(params) def xRightClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'X': str(incrementDistance)}} self.moveRelativeSignal.emit(params) def yleftClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'Y': str(-1*incrementDistance)}} self.moveRelativeSignal.emit(params) def yRightClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'Y': str(incrementDistance)}} self.moveRelativeSignal.emit(params) def zleftClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'Z': str(-1*incrementDistance)}} self.moveRelativeSignal.emit(params) def zRightClicked(self): # disable buttons self.tabPanel.setDisabled(True) # fetch current increment value if self.button_1.isChecked(): incrementDistance = 1 elif self.button_01.isChecked(): incrementDistance = 0.1 elif self.button_001.isChecked(): incrementDistance = 0.01 params = {'moveSpeed': self.__moveSpeed, 'position':{'Z': str(incrementDistance)}} self.moveRelativeSignal.emit(params) ########################################################################### GUI State functions def stateDisconnected(self): # Settings option in menu self.preferencesAction.setDisabled(False) # Connect button self.connectButton.setVisible(True) self.connectButton.setDisabled(False) self.connectButton.setStyleSheet(self.styleGreen) # Disconnect button self.disconnectButton.setVisible(False) self.disconnectButton.setDisabled(True) self.disconnectButton.setStyleSheet(self.styleDisabled) # Setup CP button self.cpSetupButton.setVisible(False) self.cpSetupButton.setDisabled(True) self.cpSetupButton.setStyleSheet(self.styleDisabled) # CP Automated Capture button self.cpAutoCaptureButton.setVisible(False) self.cpAutoCaptureButton.setDisabled(True) self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled) # Manual capture button self.manualCPCaptureButton.setVisible(False) self.manualCPCaptureButton.setDisabled(True) self.manualCPCaptureButton.setStyleSheet(self.styleDisabled) # Start Alignment button self.startAlignToolsButton.setVisible(False) self.startAlignToolsButton.setDisabled(True) self.startAlignToolsButton.setText('Align Tools') self.startAlignToolsButton.setStyleSheet(self.styleDisabled) # Override Manual Tool offset Capture button self.overrideManualOffsetCaptureButton.setVisible(False) self.overrideManualOffsetCaptureButton.setDisabled(True) self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled) # Manual Tool offset Capture button self.manualToolOffsetCaptureButton.setDisabled(True) self.manualToolOffsetCaptureButton.setVisible(False) self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled) # Resume auto alignment button self.resumeAutoToolAlignmentButton.setVisible(False) self.resumeAutoToolAlignmentButton.setDisabled(True) self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled) # Delete tool buttons count = self.jogPanel.count() for i in range(11,count): item = self.jogPanel.itemAt(i) widget = item.widget() widget.setVisible(False) # widget.deleteLater() self.resetCalibration() # Crosshair display button self.crosshairDisplayButton.setVisible(True) self.crosshairDisplayButton.setDisabled(False) if(self.__displayCrosshair): self.crosshairDisplayButton.setStyleSheet(self.styleOrange) self.crosshairDisplayButton.setChecked(True) else: self.crosshairDisplayButton.setStyleSheet(self.styleBlue) self.crosshairDisplayButton.setChecked(False) # Jog panel tab self.tabPanel.setDisabled(True) def stateConnected(self): # Settings option in menu self.preferencesAction.setDisabled(False) # Connect button self.connectButton.setVisible(False) self.connectButton.setDisabled(True) self.connectButton.setStyleSheet(self.styleDisabled) # Disconnect button self.disconnectButton.setVisible(True) self.disconnectButton.setDisabled(False) self.disconnectButton.setStyleSheet(self.styleRed) self.disconnectButton.setText('D') self.disconnectButton.setToolTip('Disconnect..') # Setup CP button self.cpSetupButton.setVisible(True) self.cpSetupButton.setDisabled(False) self.cpSetupButton.setStyleSheet(self.styleGreen) # CP Automated Capture button self.cpAutoCaptureButton.setVisible(False) self.cpAutoCaptureButton.setDisabled(True) self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled) # Manual capture button self.manualCPCaptureButton.setVisible(False) self.manualCPCaptureButton.setDisabled(True) self.manualCPCaptureButton.setStyleSheet(self.styleDisabled) # Start Alignment button self.startAlignToolsButton.setVisible(False) self.startAlignToolsButton.setDisabled(True) self.startAlignToolsButton.setText('Align Tools') self.startAlignToolsButton.setStyleSheet(self.styleDisabled) # Override Manual Tool offset Capture button self.overrideManualOffsetCaptureButton.setVisible(False) self.overrideManualOffsetCaptureButton.setDisabled(True) self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled) # Manual Tool offset Capture button self.manualToolOffsetCaptureButton.setDisabled(True) self.manualToolOffsetCaptureButton.setVisible(False) self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled) # Resume auto alignment button self.resumeAutoToolAlignmentButton.setVisible(False) self.resumeAutoToolAlignmentButton.setDisabled(True) self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled) # Jog panel tab self.tabPanel.setDisabled(False) # Add tool checkboxes to right panel self.toolButtons = [] self.toolCheckboxes = [] # highest tool number storage numTools = max(self.__activePrinter['tools'], key= lambda x:int(x['number']))['number'] _logger.debug('Highest tool number is: ' + str(numTools)) # Delete old toolbuttons, if they exist # Delete tool buttons count = self.jogPanel.count() for i in range(11,count): item = self.jogPanel.itemAt(i) widget = item.widget() widget.deleteLater() currentTool = 0 for tool in range(numTools+1): # add tool buttons toolButton = QPushButton('T' + str(tool)) toolButton.setObjectName('toolButton_'+str(tool)) toolButton.setFixedSize(self.pushbuttonSize,self.pushbuttonSize) toolButton.clicked.connect(self.identifyToolButton) # check if current index exists in tool numbers from machine if(any(d.get('number', -1) == tool for d in self.__activePrinter['tools'])): toolTip = 'Fetch T' + str(tool) + ' to current machine position.' toolTip += '
'
toolTip += 'X: ' + "{:>9.3f}".format(self.__activePrinter['tools'][currentTool]['offsets'][0])
toolTip += '
'
toolTip += 'Y: ' + "{:>9.3f}".format(self.__activePrinter['tools'][currentTool]['offsets'][1])
toolTip += '
'
toolTip += 'Z: ' + "{:>9.3f}".format(self.__activePrinter['tools'][currentTool]['offsets'][2])
toolTip += ''
currentTool += 1
toolButton.setToolTip(toolTip)
self.toolButtons.append(toolButton)
# add tool checkboxes
toolCheckbox = QCheckBox()
toolCheckbox.setObjectName('toolCheckbox_' + str(tool))
toolCheckbox.setToolTip('Add T' + str(tool) + ' to calibration.')
toolCheckbox.setChecked(True)
toolCheckbox.setCheckable(True)
toolCheckbox.setObjectName('toolCheckbox_' + str(tool))
self.toolCheckboxes.append(toolCheckbox)
# Display tool buttons
for i,button in enumerate(self.toolButtons):
button.setCheckable(True)
index = button.objectName()
index = int(index[-1:])
if int(self.__activePrinter['currentTool']) == index:
button.setChecked(True)
else:
button.setChecked(False)
# button.clicked.connect(self.callTool)
self.jogPanel.addWidget(button, (7+i), 0, Qt.AlignCenter|Qt.AlignHCenter)
# Display tool checkboxes
for i,checkbox in enumerate(self.toolCheckboxes):
checkbox.setCheckable(True)
checkbox.setChecked(True)
# button.clicked.connect(self.callTool)
self.jogPanel.addWidget(checkbox, (7+i), 1, Qt.AlignCenter|Qt.AlignHCenter)
# Alignment/Detection state reset
self.state = 0
# Endstop calibration state flags
self.__stateManualCPCapture = False
self.__stateAutoCPCapture = False
self.__stateEndstopAutoCalibrate = False
self.toggleEndstopAutoDetectionSignal.emit(False)
# Nozzle calibration state flags
self.__stateOverrideManualNozzleAlignment = False
self.__stateAutoNozzleAlignment = False
self.toggleNozzleAutoDetectionSignal.emit(False)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(False)
if(self.__displayCrosshair):
self.crosshairDisplayButton.setStyleSheet(self.styleOrange)
self.crosshairDisplayButton.setChecked(True)
else:
self.crosshairDisplayButton.setStyleSheet(self.styleBlue)
self.crosshairDisplayButton.setChecked(False)
_logger.debug('Tool data and interface created successfully.')
def stateCPSetup(self):
# Settings option in menu
self.preferencesAction.setDisabled(False)
# Connect button
self.connectButton.setVisible(False)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(False)
self.disconnectButton.setStyleSheet(self.styleRed)
self.disconnectButton.setText('C')
self.disconnectButton.setToolTip('Cancel..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(True)
self.cpSetupButton.setStyleSheet(self.styleDisabled)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(True)
self.cpAutoCaptureButton.setDisabled(False)
self.cpAutoCaptureButton.setStyleSheet(self.styleOrange)
# Manual capture button
self.manualCPCaptureButton.setVisible(True)
self.manualCPCaptureButton.setDisabled(False)
self.manualCPCaptureButton.setStyleSheet(self.styleGreen)
# Start Alignment button
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.startAlignToolsButton.setText('Align Tools')
self.startAlignToolsButton.setStyleSheet(self.styleDisabled)
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(False)
if(self.__displayCrosshair):
self.crosshairDisplayButton.setStyleSheet(self.styleOrange)
self.crosshairDisplayButton.setChecked(True)
else:
self.crosshairDisplayButton.setStyleSheet(self.styleBlue)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(False)
def stateCPAuto(self):
# Settings option in menu
self.preferencesAction.setDisabled(False)
# Connect button
self.connectButton.setVisible(False)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(False)
self.disconnectButton.setStyleSheet(self.styleRed)
self.disconnectButton.setText('C')
self.disconnectButton.setToolTip('Cancel..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(True)
self.cpSetupButton.setStyleSheet(self.styleDisabled)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(True)
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Manual capture button
self.manualCPCaptureButton.setVisible(False)
self.manualCPCaptureButton.setDisabled(True)
self.manualCPCaptureButton.setStyleSheet(self.styleDisabled)
# Start Alignment button
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.startAlignToolsButton.setText('Align Tools')
self.startAlignToolsButton.setStyleSheet(self.styleDisabled)
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(True)
self.crosshairDisplayButton.setStyleSheet(self.styleDisabled)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(True)
def stateCalibrateReady(self):
# Settings option in menu
self.preferencesAction.setDisabled(False)
# Connect button
self.connectButton.setVisible(False)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(False)
self.disconnectButton.setStyleSheet(self.styleRed)
self.disconnectButton.setText('C')
self.disconnectButton.setToolTip('Cancel..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(False)
self.cpSetupButton.setStyleSheet(self.styleBlue)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(False)
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Manual capture button
self.manualCPCaptureButton.setVisible(False)
self.manualCPCaptureButton.setDisabled(True)
self.manualCPCaptureButton.setStyleSheet(self.styleDisabled)
# Start Alignment button
self.startAlignToolsButton.setVisible(True)
self.startAlignToolsButton.setDisabled(False)
self.startAlignToolsButton.setText('Align Tools')
self.startAlignToolsButton.setStyleSheet(self.styleGreen)
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(False)
if(self.__displayCrosshair):
self.crosshairDisplayButton.setStyleSheet(self.styleOrange)
self.crosshairDisplayButton.setChecked(True)
else:
self.crosshairDisplayButton.setStyleSheet(self.styleBlue)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(False)
# Tool buttons
for button in self.toolButtons:
button.setStyleSheet('')
button.setDisabled(False)
def stateCalibtrateRunning(self):
# Settings option in menu
self.preferencesAction.setDisabled(True)
# Connect button
self.connectButton.setVisible(False)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(False)
self.disconnectButton.setStyleSheet(self.styleRed)
self.disconnectButton.setText('C')
self.disconnectButton.setToolTip('Cancel..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(True)
self.cpSetupButton.setStyleSheet(self.styleDisabled)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(False)
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Manual capture button
self.manualCPCaptureButton.setVisible(False)
self.manualCPCaptureButton.setDisabled(True)
self.manualCPCaptureButton.setStyleSheet(self.styleDisabled)
# Start Alignment button
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.startAlignToolsButton.setText('Detecting..')
self.startAlignToolsButton.setStyleSheet(self.styleOrange+' * {font: italic}')
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(True)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(True)
self.crosshairDisplayButton.setStyleSheet(self.styleDisabled)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(True)
# Tool selection buttons disable
for button in self.toolButtons:
button.setDisabled(True)
def stateCalibrateComplete(self):
# Settings option in menu
self.preferencesAction.setDisabled(False)
# Connect button
self.connectButton.setVisible(False)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(False)
self.disconnectButton.setStyleSheet(self.styleRed)
self.disconnectButton.setText('D')
self.disconnectButton.setToolTip('Disconnect..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(False)
self.cpSetupButton.setStyleSheet(self.styleBlue)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(False)
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Manual capture button
self.manualCPCaptureButton.setVisible(False)
self.manualCPCaptureButton.setDisabled(True)
self.manualCPCaptureButton.setStyleSheet(self.styleDisabled)
# Start Alignment button
self.startAlignToolsButton.setVisible(True)
self.startAlignToolsButton.setDisabled(False)
self.startAlignToolsButton.setText('Align Tools')
self.startAlignToolsButton.setStyleSheet(self.styleGreen)
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(False)
if(self.__displayCrosshair):
self.crosshairDisplayButton.setStyleSheet(self.styleOrange)
self.crosshairDisplayButton.setChecked(True)
else:
self.crosshairDisplayButton.setStyleSheet(self.styleBlue)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(False)
# Tool buttons
for button in self.toolButtons:
button.setStyleSheet('')
button.setDisabled(False)
def stateExiting(self):
# Settings option in menu
self.preferencesAction.setDisabled(True)
# Connect button
self.connectButton.setVisible(True)
self.connectButton.setDisabled(True)
self.connectButton.setStyleSheet(self.styleDisabled)
# Disconnect button
self.disconnectButton.setVisible(True)
self.disconnectButton.setDisabled(True)
self.disconnectButton.setStyleSheet(self.styleDisabled)
self.disconnectButton.setText('-')
self.disconnectButton.setToolTip('Disconnecting..')
# Setup CP button
self.cpSetupButton.setVisible(True)
self.cpSetupButton.setDisabled(True)
self.cpSetupButton.setStyleSheet(self.styleDisabled)
# CP Automated Capture button
self.cpAutoCaptureButton.setVisible(True)
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Manual capture button
self.manualCPCaptureButton.setVisible(False)
self.manualCPCaptureButton.setDisabled(True)
self.manualCPCaptureButton.setStyleSheet(self.styleDisabled)
# Start Alignment button
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.startAlignToolsButton.setText('Align Tools')
self.startAlignToolsButton.setStyleSheet(self.styleGreen)
# Override Manual Tool offset Capture button
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Manual Tool offset Capture button
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# Resume auto alignment button
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
# Crosshair display button
self.crosshairDisplayButton.setVisible(True)
self.crosshairDisplayButton.setDisabled(True)
self.crosshairDisplayButton.setStyleSheet(self.styleDisabled)
self.crosshairDisplayButton.setChecked(False)
# Jog panel tab
self.tabPanel.setDisabled(True)
########################################################################### User interactions
def setupCPCapture(self):
self.__stateSetupCPCapture = True
self.toggleEndstopDetectionSignal.emit(True)
# enable detection
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
# update machine coordinates
self.pollCoordinatesSignal.emit()
# Get original physical coordinates
self.originalPrinterPosition = self.__currentPosition
self.__restorePosition = copy.deepcopy(self.__currentPosition)
self.resetCalibrationVariables()
self.stateCPSetup()
self.repaint()
def manualCPCapture(self):
self.__stateSetupCPCapture = False
self.__stateManualCPCapture = True
# update machine coordinates
self.pollCoordinatesSignal.emit()
# stop endstop detection
self.toggleEndstopDetectionSignal.emit(False)
# update GUI
self.stateCalibrateReady()
def setupCPAutoCapture(self):
# send calling to log
_logger.debug('*** calling App.setupCPAutoCapture')
self.startTime = time.time()
#################################### Camera Calibration
# Update GUI state
self.stateCPAuto()
# set state flag to handle camera calibration
self.__stateEndstopAutoCalibrate = True
self.__stateAutoCPCapture = True
# Start detector in DetectionManager
self.toggleEndstopAutoDetectionSignal.emit(True)
self.uv = [None, None]
self.unloadToolSignal.emit()
self.pollCoordinatesSignal.emit()
# send exiting to log
_logger.debug('*** exiting App.setupCPAutoCapture')
def haltCPAutoCapture(self):
self.resetCalibration()
self.resetCalibrationVariables()
self.statusBar.showMessage('CP calibration cancelled.')
# Reset GUI
self.stateConnected()
def haltNozzleCapture(self):
self.resetNozzleAlignment()
self.statusBar.showMessage('Nozzle calibration halted.')
# Reset GUI
self.stateCalibrateReady()
def overrideManualToolOffsetCapture(self):
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
# set tool alignment state to trigger offset calculation
self.state = 100
self.pollCoordinatesSignal.emit()
def manualToolOffsetCapture(self):
self.manualToolOffsetCaptureButton.setDisabled(True)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
params={'toolIndex': int(self.__activePrinter['currentTool']), 'position': self.__currentPosition, 'cpCoordinates': self.__cpCoordinates, 'continue': False}
self.setOffsetsSignal.emit(params)
def resetCalibration(self):
# Reset program state, and frame capture control to defaults
self.__stateEndstopAutoCalibrate = False
self.__stateAutoCPCapture = False
self.__stateSetupCPCapture = False
self.__stateOverrideManualNozzleAlignment = False
self.__stateAutoNozzleAlignment = False
self.toggleEndstopAutoDetectionSignal.emit(False)
self.toggleNozzleAutoDetectionSignal.emit(False)
self.toggleEndstopDetectionSignal.emit(False)
self.toggleDetectionSignal.emit(False)
self.__displayCrosshair = False
self.resetCalibrationVariables()
if(self.__restorePosition is not None):
self.unloadToolSignal.emit()
self.getVideoFrameSignal.emit()
def resetNozzleAlignment(self):
# Reset program state, and frame capture control to defaults
self.__stateEndstopAutoCalibrate = False
self.__stateAutoCPCapture = False
self.__stateSetupCPCapture = False
self.__stateOverrideManualNozzleAlignment = False
self.__stateAutoNozzleAlignment = False
self.toggleEndstopAutoDetectionSignal.emit(False)
self.toggleNozzleAutoDetectionSignal.emit(False)
self.toggleEndstopDetectionSignal.emit(False)
self.toggleDetectionSignal.emit(False)
self.__displayCrosshair = False
self.resetCalibrationVariables()
self.tabPanel.setDisabled(True)
self.unloadToolSignal.emit()
self.getVideoFrameSignal.emit()
# Function to reset calibrateTools variables
def resetCalibrationVariables(self):
# Setup camera calibration move coordinates
self.calibrationCoordinates = [ [0,-0.5], [0.294,-0.405], [0.476,-0.155], [0.476,0.155], [0.294,0.405], [0,0.5], [-0.294,0.405], [-0.476,0.155], [-0.476,-0.155], [-0.294,-0.405] ]
# reset all variables
self.guessPosition = [1,1]
self.uv = [None, None]
self.olduv = self.uv
if(self.transformMatrix is None or self.mpp is None):
_logger.debug('Camera calibration matrix reset.')
self.state = 0
elif(len(self.transformMatrix) < 6):
self.transformMatrix = None
self.mpp = None
self.state = 0
else:
self.state = 200
self.detect_count = 0
self.space_coordinates = []
self.camera_coordinates = []
self.retries = 0
self.calibrationMoves = 0
self.repeatCounter = 0
def startAlignTools(self):
# send calling to log
_logger.debug('*** calling App.startAlignTools')
self.startTime = time.time()
# start as automated alignment state
self.__stateOverrideManualNozzleAlignment = False
self.__stateAutoNozzleAlignment = True
# Update GUI state
self.stateCalibtrateRunning()
self.updateStatusbarMessage('Starting tool alignment..')
# Create array for tools included in alignment process
self.alignmentToolSet = []
self.workingToolset = None
for checkbox in self.toolCheckboxes:
if(checkbox.isChecked()):
self.alignmentToolSet.append(int(checkbox.objectName()[13:]))
if(len(self.alignmentToolSet) > 0):
self.toggleNozzleAutoDetectionSignal.emit(True)
self.calibrateTools(self.alignmentToolSet)
else:
# tell user no tools selected
errorMsg = 'No tools selected for alignment'
self.updateStatusbarMessage(errorMsg)
_logger.info(errorMsg)
self.haltNozzleCapture()
# send exiting to log
_logger.debug('*** exiting App.startAlignTools')
def identifyToolButton(self):
self.tabPanel.setDisabled(True)
sender = self.sender()
for button in self.toolButtons:
if button.objectName() != sender.objectName():
button.setChecked(False)
toolIndex = int(sender.objectName()[11:])
if(toolIndex != int(self.__activePrinter['currentTool'])):
self.updateStatusbarMessage('>>> Loading T' + str(toolIndex) + '.. >>>')
self.callTool(toolIndex)
else:
self.updateStatusbarMessage('<<< Unloading tool.. <<<')
self.callTool(-1)
def calibrateTools(self, toolset=None):
# toolset is passed the first time we call the function
# this creates the workingToolset, from which we pop each tool on each cycle
if(toolset is not None):
self.workingToolset = toolset
# check if we still have tools to calibrate in the workingToolset list
if(len(self.workingToolset)>0):
# grab the first item in the list (FIFO)
toolIndex = self.workingToolset[0]
_logger.info('Calibrating T' + str(toolIndex) +'..')
self.startAlignToolsButton.setText('Detecting T'+ str(toolIndex) +'..')
# delete the tool from the list before processing it
self.workingToolset.pop(0)
# update toolButtons GUI to indiciate which tool we're working on
for button in self.toolButtons:
buttonName = 'toolButton_' + str(toolIndex)
if(button.objectName() == buttonName):
button.setStyleSheet(self.styleOrange)
self.resetCalibrationVariables()
# main program state flags
self.__stateOverrideManualNozzleAlignment = False
self.__stateAutoNozzleAlignment = True
# DetectionManager state flag
self.toggleNozzleAutoDetectionSignal.emit(True)
# Capture start time of tool calibration run
self.toolTime = time.time()
# Call tool and start calibration
self.callTool(toolIndex)
else:
# entire list has been processed, output results
calibration_time = np.around(time.time() - self.startTime,1)
_logger.info('Calibration completed (' + str(calibration_time) + 's) with a resolution of ' + str(self.mpp) + '/pixel')
# save to firmware
self.saveOffsets()
# reset GUI
self.stateCalibrateComplete()
self.repaint()
# reset state flags for main program and Detection Manager
self.resetNozzleAlignment()
def autoCalibrate(self):
self.tabPanel.setDisabled(True)
# create variable for tracking the calibration run time, if not already defined
try:
if(runtime):
pass
except:
runtime = time.time()
if(self.uv is not None):
if(self.uv[0] is not None and self.uv[1] is not None):
# First calibration step
if(self.state == 0):
_logger.debug('*** State: ' + str(self.state) + ' Coords:' + str(self.__currentPosition) + ' UV: ' + str(self.uv) + ' old UV: ' + str(self.olduv))
self.updateStatusbarMessage('Calibrating camera step 0..')
if(self.olduv is not None):
if(self.olduv[0] == self.uv[0] and self.olduv[1] == self.uv[1]):
self.repeatCounter += 1
if(self.repeatCounter > 5):
self.nozzleDetectionFailed()
return
# loop through again
self.retries += 1
self.pollCoordinatesSignal.emit()
return
else:
self.repeatCounter = 0
self.olduv = self.uv
self.space_coordinates = []
self.camera_coordinates = []
self.space_coordinates.append((self.__currentPosition['X'], self.__currentPosition['Y']))
self.camera_coordinates.append((self.uv[0], self.uv[1]))
# move carriage for calibration
self.offsetX = self.calibrationCoordinates[0][0]
self.offsetY = self.calibrationCoordinates[0][1]
params = {'position':{'X': self.offsetX, 'Y': self.offsetY}}
self.lastState = 0
self.state = 1
self.moveRelativeSignal.emit(params)
return
elif(self.state >= 1 and self.state < len(self.calibrationCoordinates)):
# capture the UV data when a calibration move has been executed, before returning to original position
if(self.state != self.lastState and self.state < len(self.calibrationCoordinates)):
_logger.debug('*** State: ' + str(self.state) + ' Coords:' + str(self.__currentPosition) + ' UV: ' + str(self.uv) + ' old UV: ' + str(self.olduv))
if(self.olduv is not None):
if(self.olduv[0] == self.uv[0] and self.olduv[1] == self.uv[1]):
self.repeatCounter += 1
if(self.repeatCounter > 10):
self.nozzleDetectionFailed()
return
# loop through again
self.retries += 1
self.pollCoordinatesSignal.emit()
return
else:
self.repeatCounter = 0
# Calculate mpp at first move
if(self.state == 1):
self.mpp = np.around(0.5/self.getDistance(self.olduv[0],self.olduv[1],self.uv[0],self.uv[1]),3)
# save position as previous position
self.olduv = self.uv
# save machine coordinates for detected nozzle
self.space_coordinates.append((self.__currentPosition['X'], self.__currentPosition['Y']))
# save camera coordinates
self.camera_coordinates.append((self.uv[0],self.uv[1]))
# return carriage to relative center of movement
self.offsetX = (-1*self.offsetX)
self.offsetY = (-1*self.offsetY)
self.lastState = self.state
params = {'position':{'X': self.offsetX, 'Y': self.offsetY}}
self.moveRelativeSignal.emit(params)
return
# otherwise, move the carriage from the original position to the next step and increment state
else:
self.updateStatusbarMessage('Calibrating camera step ' + str(self.state) + '..')
_logger.debug('Step ' + str(self.state) + ' detection UV: ' + str(self.uv))
# move carriage to next calibration point
self.offsetX = self.calibrationCoordinates[self.state][0]
self.offsetY = self.calibrationCoordinates[self.state][1]
self.lastState = int(self.state)
self.state += 1
params = {'position':{'X': self.offsetX, 'Y': self.offsetY}}
self.moveRelativeSignal.emit(params)
return
elif(self.state == len(self.calibrationCoordinates)):
# Camera calibration moves completed.
if(self.olduv is not None):
if(self.olduv[0] == self.uv[0] and self.olduv[1] == self.uv[1]):
self.repeatCounter += 1
if(self.repeatCounter > 10):
self.nozzleDetectionFailed()
return
# loop through again
self.retries += 1
self.pollCoordinatesSignal.emit()
return
else:
self.repeatCounter = 0
# Update GUI thread with current status and percentage complete
updateMessage = 'Millimeters per pixel is ' + str(self.mpp)
self.updateStatusbarMessage(updateMessage)
_logger.info(updateMessage)
# save position as previous position
self.olduv = self.uv
# save machine coordinates for detected nozzle
self.space_coordinates.append((self.__currentPosition['X'], self.__currentPosition['Y']))
# save camera coordinates
self.camera_coordinates.append((self.uv[0],self.uv[1]))
# calculate camera transformation matrix
cameraCalibrationTime = np.around(time.time() - self.startTime,1)
_logger.info('Camera calibrated (' + str(cameraCalibrationTime) + 's); aligning..')
# Calculate transformation matrix
self.transform_input = [(self.space_coordinates[i], self.normalize_coords(camera)) for i, camera in enumerate(self.camera_coordinates)]
self.transformMatrix, self.transform_residual = self.least_square_mapping(self.transform_input)
# define camera center in machine coordinate space
self.newCenter = self.transformMatrix.T @ np.array([0, 0, 0, 0, 0, 1])
self.guessPosition[0]= np.around(self.newCenter[0],3)
self.guessPosition[1]= np.around(self.newCenter[1],3)
_logger.info('Calibration positional guess: ' + str(self.guessPosition))
# Set next calibration variables state
self.state = 200
self.retries = 0
self.calibrationMoves = 0
params = {'position':{'X': self.guessPosition[0], 'Y': self.guessPosition[1]}}
self.moveAbsoluteSignal.emit(params)
return
elif(self.state == 200):
# Update GUI with current status
if(self.__stateEndstopAutoCalibrate):
updateMessage = 'Endstop calibration step ' + str(self.calibrationMoves) + '.. (MPP=' + str(self.mpp) +')'
else:
updateMessage = 'Tool ' + str(self.__activePrinter['currentTool']) + ' calibration step ' + str(self.calibrationMoves) + '.. (MPP=' + str(self.mpp) +')'
self.updateStatusbarMessage(updateMessage)
# if(self.olduv is not None):
# if(self.olduv[0] == self.uv[0] and self.olduv[1] == self.uv[1]):
# # print('Repeating detection: ' + str(self.repeatCounter))
# self.repeatCounter += 1
# if(self.repeatCounter > 10):
# print('Failed to detect.')
# self.nozzleDetectionFailed()
# return
# # loop through again
# print('Retrying', self.retries)
# self.retries += 1
# self.pollCoordinatesSignal.emit()
# return
# else:
# # print('Took ' + str(self.repeatCounter) + ' attempts.')
# self.repeatCounter = 0
# increment moves counter
self.calibrationMoves += 1
# nozzle detected, frame rotation is set, start
self.cx,self.cy = self.normalize_coords(self.uv)
self.v = [self.cx**2, self.cy**2, self.cx*self.cy, self.cx, self.cy, 0]
self.offsets = -1*(0.55*self.transformMatrix.T @ self.v)
self.offsets[0] = np.around(self.offsets[0],3)
self.offsets[1] = np.around(self.offsets[1],3)
_logger.debug('*** State: ' + str(self.state) + ' retries: ' + str(self.retries) + ' X' + str(self.__currentPosition['X']) + ' Y' + str(self.__currentPosition['Y']) + ' UV: ' + str(self.uv) + ' old UV: ' + str(self.olduv) + ' Offsets: ' + str(self.offsets))
# Add rounding handling for endstop alignment
if(self.__stateEndstopAutoCalibrate):
if(abs(self.offsets[0])+abs(self.offsets[1]) <= 0.02):
self.offsets[0] = 0.0
self.offsets[1] = 0.0
# Start timer if we're calibrating the CP using the automated endstop detection
try:
if(self.toolTime):
pass
except:
self.toolTime = time.time()
# calculate current calibration cycle runtime
runtime = np.around(time.time() - self.toolTime,1)
# check if too much time has passed
if(runtime > 120 or self.calibrationMoves > 30):
print('Runtime:', runtime, ' moves: ', self.calibrationMoves)
self.retries = 10
# Otherwise, check if we're not aligned to the center
elif(self.offsets[0] != 0.0 or self.offsets[1] != 0.0):
self.olduv = self.uv
params = {'position': {'X': self.offsets[0], 'Y': self.offsets[1]}, 'moveSpeed':1000}
self.moveRelativeSignal.emit(params)
_logger.debug('Calibration move X{0:-1.3f} Y{1:-1.3f} F1000 '.format(self.offsets[0],self.offsets[1]))
return
# finally, we're aligned to the center, and we should update the tool offsets
elif(self.offsets[0] == 0.0 and self.offsets[1] == 0.0):
# endstop calibration wrapping up
if(self.__stateEndstopAutoCalibrate):
updateMessage = 'Endstop auto-calibrated in ' + str(self.calibrationMoves) + ' steps. (MPP=' + str(self.mpp) +')'
# update state flags for endstop alignment
self.__stateAutoCPCapture = False
self.__stateEndstopAutoCalibrate = False
self.__stateManualCPCapture = False
# update detection manager state
self.toggleEndstopAutoDetectionSignal.emit(False)
# disable detection manager frame analysis
self.toggleDetectionSignal.emit(False)
self.__displayCrosshair = False
# Set CP location
self.__cpCoordinates['X'] = np.around(self.__currentPosition['X'],2)
self.__cpCoordinates['Y'] = np.around(self.__currentPosition['Y'],2)
self.__cpCoordinates['Z'] = np.around(self.__currentPosition['Z'],2)
# Update GUI statusbar with CP coordinates and green status
self.cpLabel.setText('CP: ('+ str(self.__cpCoordinates['X']) + ', ' + str(self.__cpCoordinates['Y']) + ')')
self.cpLabel.setStyleSheet(self.styleGreen)
# Reset entire GUI for next state
self.stateCalibrateReady()
self.repaint()
# tool calibration wrapping up
elif(self.__stateAutoNozzleAlignment):
updateMessage = 'Tool ' + str(self.__activePrinter['currentTool']) + ' has been calibrated.'
self.state = 100
self.retries = 0
self.updateStatusbarMessage(updateMessage)
self.pollCoordinatesSignal.emit()
return
elif(self.retries < 100 and runtime <= self.__maxRuntime):
self.retries += 1
self.pollCoordinatesSignal.emit()
return
if(self.retries < self.__maxRetries):
self.retries += 1
# enable detection
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
self.pollCoordinatesSignal.emit()
return
# If we've reached this part of the code, we've run over our limit of retries
def nozzleDetectionFailed(self):
self.state = -99
# End auto calibration
self.__stateAutoNozzleAlignment = False
self.__stateOverrideManualNozzleAlignment = True
# calibrating nozzle manual
self.tabPanel.setDisabled(False)
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setVisible(True)
self.overrideManualOffsetCaptureButton.setDisabled(False)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleBlue)
self.resumeAutoToolAlignmentButton.setVisible(True)
self.resumeAutoToolAlignmentButton.setDisabled(False)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleGreen)
self.toggleNozzleAutoDetectionSignal.emit(False)
self.toggleNozzleDetectionSignal.emit(True)
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
def resumeAutoAlignment(self):
if(self.transformMatrix is None or self.mpp is None):
self.state = 0
elif(len(self.transformMatrix) < 6):
self.transformMatrix = None
self.mpp = None
self.state = 0
else:
self.state = 200
self.retries = 0
self.repeatCounter = 0
self.uv = [None, None]
self.olduv = [None, None]
self.__stateAutoNozzleAlignment = True
self.toolTime = time.time()
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
self.overrideManualOffsetCaptureButton.setVisible(False)
self.overrideManualOffsetCaptureButton.setDisabled(True)
self.overrideManualOffsetCaptureButton.setStyleSheet(self.styleDisabled)
self.startAlignToolsButton.setVisible(True)
self.startAlignToolsButton.setDisabled(True)
self.updateStatusbarMessage('Resuming auto detection of current tool..')
self.toggleNozzleAutoDetectionSignal.emit(True)
self.pollCoordinatesSignal.emit()
########################################################################### Interface with Detection Manager
def createDetectionManagerThread(self, announce=True):
if(announce):
_logger.info(' .. starting Detection Manager.. ')
# Thread setup
self.detectionThread = QThread()
self.detectionManager = DetectionManager(videoSrc=self._videoSrc, width=self._cameraWidth, height=self._cameraHeight, parent=None)
self.detectionManager.moveToThread(self.detectionThread)
# Thread management signals and slots
self.detectionManager.errorSignal.connect(self.detectionManagerError)
self.detectionThread.started.connect(self.detectionManager.processFrame)
self.detectionThread.finished.connect(self.detectionManager.quit)
self.detectionThread.finished.connect(self.detectionManager.deleteLater)
self.detectionThread.finished.connect(self.detectionThread.deleteLater)
# Video frame signals and slots
self.detectionManager.detectionManagerNewFrameSignal.connect(self.refreshImage)
self.detectionManager.detectionManagerReadySignal.connect(self.startVideo)
self.getVideoFrameSignal.connect(self.detectionManager.processFrame)
# Camera image properties signals and slots
self.setImagePropertiesSignal.connect(self.detectionManager.relayImageProperties)
self.resetImageSignal.connect(self.detectionManager.relayResetImage)
# Endstop alignment signals and slots
self.toggleEndstopDetectionSignal.connect(self.detectionManager.toggleEndstopDetection)
self.toggleEndstopAutoDetectionSignal.connect(self.detectionManager.toggleEndstopAutoDetection)
# Nozzle alignment signals and slots
self.toggleNozzleDetectionSignal.connect(self.detectionManager.toggleNozzleDetection)
self.toggleNozzleAutoDetectionSignal.connect(self.detectionManager.toggleNozzleAutoDetection)
# UV coordinates update signals and slots
self.getUVCoordinatesSignal.connect(self.detectionManager.sendUVCoorindates)
self.detectionManager.detectionManagerUVCoordinatesSignal.connect(self.saveUVCoordinates)
# Master detection swtich enable/disable
self.toggleDetectionSignal.connect(self.detectionManager.enableDetection)
@pyqtSlot(object)
def startVideo(self, cameraProperties):
# send calling to log
_logger.debug('*** calling App.startVideo')
# capture camera properties
self.__activeCamera = cameraProperties
self.startVideoSignal.emit()
# send exiting to log
_logger.debug('*** exiting App.startVideo')
@pyqtSlot(object)
def refreshImage(self, data):
self.__mutex.lock()
frame = data[0]
self.image.setPixmap(frame)
self.__mutex.unlock()
self.getVideoFrameSignal.emit()
@pyqtSlot(object)
def updateStatusbarMessage(self, message):
self.__mutex.lock()
# send calling to log
_logger.debug('*** calling App.updateStatusbarMessage')
try:
self.statusBar.showMessage(message)
self.statusBar.setStyleSheet('')
except:
errorMsg = 'Error sending message to statusbar.'
_logger.error(errorMsg)
self.__mutex.unlock()
app.processEvents()
# send exiting to log
_logger.debug('*** exiting App.updateStatusbarMessage')
@pyqtSlot(object)
def detectionManagerError(self, message):
self.haltPrinterOperation(silent=True)
# self.__mutex.lock()
try:
self.resetCalibration()
self.stateExiting()
self.statusBar.showMessage(message)
self.statusBar.setStyleSheet(self.styleRed)
self.cpLabel.setStyleSheet(self.styleOrange)
self.cpLabel.setVisible(False)
self.connectionStatusLabel.setStyleSheet(self.styleOrange)
self.connectionStatusLabel.setText('Critical Error')
self.__mutex.unlock()
if(self.__restorePosition is not None):
self.moveAbsoluteSignal.emit(self.__restorePosition)
except Exception as e:
print(e)
# self.__mutex.unlock()
errorMsg = 'Error sending message to statusbar.'
_logger.error(errorMsg)
# Kill thread
self.detectionThread.quit()
self.detectionThread.wait(self.__detectionManagerThreadWaitTime)
if(self.__restorePosition is not None):
self.printerThread.quit()
self.printerThread.wait(self.__printerManagerThreadWaitTime)
# display error image
self.image.setPixmap(self.errorImage)
########################################################################### Interface with Printer Manager
def createPrinterManagerThread(self,announce=True):
if(announce):
_logger.info(' .. starting Printer Manager.. ')
try:
self.printerManager = PrinterManager(parent=None, firmwareList=self.__firmwareList, driverList=self.__driverList, announcemode=announce)
self.printerThread = QThread()
self.printerManager.moveToThread(self.printerThread)
self.printerManager.errorSignal.connect(self.printerError)
# self.printerManager.printerDisconnectedSignal.connect(self.printerThread.quit)
# self.printerManager.printerDisconnectedSignal.connect(self.printerManager.deleteLater)
self.printerThread.finished.connect(self.printerManager.quit)
self.printerThread.finished.connect(self.printerManager.deleteLater)
self.printerThread.finished.connect(self.printerThread.deleteLater)
# PrinterManager object signals
self.printerManager.updateStatusbarSignal.connect(self.updateStatusbarMessage)
self.printerManager.activePrinterSignal.connect(self.printerConnected)
self.printerManager.printerDisconnectedSignal.connect(self.printerDisconnected)
self.connectSignal.connect(self.printerManager.connectPrinter)
self.disconnectSignal.connect(self.printerManager.disconnectPrinter)
self.announceSignal.connect(self.printerManager.setAnnounceMode)
self.moveRelativeSignal.connect(self.printerManager.moveRelative)
self.moveAbsoluteSignal.connect(self.printerManager.moveAbsolute)
self.printerManager.moveCompleteSignal.connect(self.printerMoveComplete)
self.pollCoordinatesSignal.connect(self.printerManager.getCoordinates)
self.printerManager.coordinatesSignal.connect(self.saveCurrentPosition)
self.callToolSignal.connect(self.printerManager.callTool)
self.unloadToolSignal.connect(self.printerManager.unloadTools)
self.printerManager.toolLoadedSignal.connect(self.toolLoaded)
self.pollCurrentToolSignal.connect(self.printerManager.currentTool)
self.printerManager.toolIndexSignal.connect(self.registerActiveTool)
self.printerManager.offsetsSetSignal.connect(self.calibrateOffsetsApplied)
self.setOffsetsSignal.connect(self.printerManager.calibrationSetOffset)
self.saveToFirmwareSignal.connect(self.printerManager.saveOffsets)
self.printerThread.start()#priority=QThread.TimeCriticalPriority)
except Exception as e:
print(e)
errorMsg = 'Failed to start PrinterManager.'
_logger.critical(errorMsg)
self.printerError(errorMsg)
def connectPrinter(self):
self.connectButton.setStyleSheet(self.styleDisabled)
self.connectButton.setDisabled(True)
self.preferencesAction.setDisabled(True)
self.statusBar.setStyleSheet("")
self.connectionStatusLabel.setText('Connecting..')
self.connectionStatusLabel.setStyleSheet(self.styleBlue)
self.repaint()
self.connection_dialog = ConnectionDialog(parent=self, newPrinter=False, settings=self.__userSettings, stylesheet=self.globalStylesheet)
connectionDialogReturn = self.connection_dialog.exec()
# Destroy connection_dialog window
self.connection_dialog = None
if(connectionDialogReturn == -2):
# new printer definition
pass
elif(connectionDialogReturn >= 0):
# fetch printer from user objects
self.__activePrinter = self.__userSettings['printer'][connectionDialogReturn]
try:
self.announceSignal.emit(False)
if(self.printerThread.isRunning() is True):
# Printer already running, delete thread and restart
self.printerThread.quit()
self.printerThread.wait(self.__printerManagerThreadWaitTime)
self.createPrinterManagerThread(announce=False)
except: self.createPrinterManagerThread(announce=False)
self.announceSignal.emit(True)
self.connectSignal.emit(self.__activePrinter)
else:
# user closed window, update GUI back to default state
# Settings option in menu
self.preferencesAction.setDisabled(False)
# Connect button
self.connectButton.setVisible(True)
self.connectButton.setDisabled(False)
self.connectButton.setStyleSheet(self.styleGreen)
def haltPrinterOperation(self, **kwargs):
try:
silent = kwargs['silent']
except: silent = False
# disconnect button
self.disconnectButton.setStyleSheet(self.styleDisabled)
self.disconnectButton.setDisabled(True)
# Cancel CP Capture
if(self.__stateAutoCPCapture is True or self.__stateEndstopAutoCalibrate is True or self.__stateSetupCPCapture is True):
self.haltCPAutoCapture()
return
# Cancel Nozzle Detection
if(self.__stateAutoNozzleAlignment is True or self.__stateOverrideManualNozzleAlignment is True):
self.haltNozzleCapture()
return
# Disconnect from printer
# Status label
self.connectionStatusLabel.setText('Disconnecting..')
self.connectionStatusLabel.setStyleSheet(self.styleBlue)
# Setup CP button
self.cpSetupButton.setStyleSheet(self.styleDisabled)
self.cpSetupButton.setDisabled(True)
# CP Automated Capture button
self.cpAutoCaptureButton.setDisabled(True)
self.cpAutoCaptureButton.setStyleSheet(self.styleDisabled)
# Jog panel
self.tabPanel.setDisabled(True)
self.repaint()
params = {}
if(silent):
params['noUpdate'] = True
else:
params['noUpdate'] = False
# restore position
if(self.__cpCoordinates['X'] is not None and self.__cpCoordinates['Y'] is not None):
_logger.debug('Restoring to CP position..')
params['parkPosition'] = self.__cpCoordinates
self.disconnectSignal.emit(params)
elif(self.__restorePosition is not None):
_logger.debug('Restoring to master restore position..')
params['parkPosition'] = self.__restorePosition
self.disconnectSignal.emit(params)
else:
self.disconnectSignal.emit(params)
self.resetCalibrationVariables()
self.updateStatusbarMessage('Printer disconnected.')
@pyqtSlot(object)
def printerConnected(self, printerJSON):
self.__mutex.lock()
# send calling to log
_logger.debug('*** calling App.printerConnected')
self.__activePrinter = printerJSON
# Status label
self.connectionStatusLabel.setText(self.__activePrinter['nickname'] + ' [' + self.__activePrinter['controller'] + ' v' + self.__activePrinter['version'] + ']')
self.connectionStatusLabel.setStyleSheet(self.styleGreen)
# poll for position
self.__firstConnection = True
self.__mutex.unlock()
self.pollCoordinatesSignal.emit()
# Gui state
self.stateConnected()
self.repaint()
# send exiting to log
_logger.debug('*** exiting App.printerConnected')
@pyqtSlot(object)
def printerDisconnected(self, **kwargs):
self.__mutex.lock()
try:
message = kwargs['message']
except: message = None
# send calling to log
_logger.debug('*** calling App.printerDisconnected')
# update status bar
if(message is not None):
self.statusBar.showMessage(message)
self.statusBar.setStyleSheet("")
# CP Label
self.cpLabel.setText('CP: undef')
self.cpLabel.setStyleSheet(self.styleOrange)
# Status label
self.connectionStatusLabel.setText('Disconnected')
self.connectionStatusLabel.setStyleSheet(self.styleOrange)
self.__firstConnection = True
self.__restorePosition = None
self.__mutex.unlock()
self.stateDisconnected()
self.repaint()
# send exiting to log
_logger.debug('*** exiting App.printerDisconnected')
@pyqtSlot(object)
def printerError(self, message):
_logger.debug('Printer Error: ' + message)
if(self.__restorePosition is not None):
params = {'parkPosition': self.__restorePosition}
self.disconnectSignal.emit(params)
else:
self.disconnectSignal.emit(None)
# Kill printer thread
try:
self.printerThread.quit()
self.printerThread.wait(self.__printerManagerThreadWaitTime)
except: _logger.warning('Printer thread not created yet.')
self.printerDisconnected(message=message)
self.statusBar.setStyleSheet(self.styleRed)
def callTool(self, toolNumber=-1):
# disable detection
self.toggleDetectionSignal.emit(False)
self.__displayCrosshair = False
toolNumber = int(toolNumber)
if(toolNumber == -1):
self.startAlignToolsButton.setText('Unloading..')
self.unloadToolSignal.emit()
return
try:
self.startAlignToolsButton.setText('Loading T' +str(toolNumber) + '..')
self.callToolSignal.emit(toolNumber)
except:
errorMsg = 'Unable to call tool from printer: ' + str(toolNumber)
_logger.error(errorMsg)
self.printerError(errorMsg)
@pyqtSlot()
def toolLoaded(self):
self.pollCurrentToolSignal.emit()
if(self.__cpCoordinates['X'] is not None and self.__cpCoordinates['Y'] is not None):
params = {'protected':True,'moveSpeed': 5000, 'position':{'X': self.__cpCoordinates['X'], 'Y': self.__cpCoordinates['Y'], 'Z': self.__cpCoordinates['Z']}}
else:
params = {'protected':True,'moveSpeed': 5000, 'position':{'X': self.__currentPosition['X'], 'Y': self.__currentPosition['Y'], 'Z': self.__currentPosition['Z']}}
self.moveAbsoluteSignal.emit(params)
@pyqtSlot(int)
def registerActiveTool(self, toolIndex):
self.__mutex.lock()
self.__activePrinter['currentTool'] = toolIndex
for button in self.toolButtons:
if(button.objectName() != ('toolButton_'+str(toolIndex))):
button.setChecked(False)
else:
button.setChecked(True)
self.__mutex.unlock()
if(self.__stateAutoNozzleAlignment is False and self.__stateOverrideManualNozzleAlignment is False and int(toolIndex) != -1):
self.startAlignToolsButton.setVisible(False)
if(self.__cpCoordinates is not None and self.__cpCoordinates['X'] is not None):
self.manualToolOffsetCaptureButton.setEnabled(True)
self.manualToolOffsetCaptureButton.setVisible(True)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleGreen)
else:
self.manualToolOffsetCaptureButton.setEnabled(False)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
elif(int(toolIndex) != -1):
self.manualToolOffsetCaptureButton.setEnabled(False)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
self.startAlignToolsButton.setVisible(True)
self.startAlignToolsButton.setText('Detecting T' + str(toolIndex) +'..')
else:
self.manualToolOffsetCaptureButton.setEnabled(False)
self.manualToolOffsetCaptureButton.setVisible(False)
self.manualToolOffsetCaptureButton.setStyleSheet(self.styleDisabled)
self.startAlignToolsButton.setVisible(True)
@pyqtSlot()
def printerMoveComplete(self):
self.tabPanel.setDisabled(False)
if(self.__stateAutoCPCapture and self.__stateEndstopAutoCalibrate):
# enable detection
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
statusMsg = '(Endstop auto detection active.)'
self.updateStatusbarMessage(statusMsg)
_logger.debug(statusMsg)
# Calibrating camera, go based on state
self.tabPanel.setDisabled(True)
elif(self.__stateAutoNozzleAlignment is True):
# enable detection
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
statusMsg = '(Tool/nozzle auto detection active.)'
self.updateStatusbarMessage(statusMsg)
_logger.debug(statusMsg)
# calibrating nozzle auto
self.tabPanel.setDisabled(True)
elif(self.__stateOverrideManualNozzleAlignment is True):
# enable detection
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
statusMsg = '(Tool/nozzle manual override active.)'
self.updateStatusbarMessage(statusMsg)
_logger.debug(statusMsg)
# calibrating nozzle manual
self.startAlignToolsButton.setVisible(False)
self.startAlignToolsButton.setDisabled(True)
self.manualCPCaptureButton.setVisible(True)
return
elif(self.__stateManualCPCapture is True):
statusMsg = '(Endstop/CP manual override active.)'
self.updateStatusbarMessage(statusMsg)
_logger.debug(statusMsg)
else:
statusMsg = 'Ready.'
self.updateStatusbarMessage(statusMsg)
_logger.debug(statusMsg)
self.pollCoordinatesSignal.emit()
@pyqtSlot(object)
def saveUVCoordinates(self, uvCoordinates):
self.uv = uvCoordinates
if(self.__stateEndstopAutoCalibrate is True or self.__stateAutoNozzleAlignment is True):
if(uvCoordinates is None):
# failed to detect, poll coordinates again
self.retries += 1
self.startAlignToolsButton.setText('Retrying (' + str(self.retries) + ')..')
# check if we've exceeded our maxRetries or maxRuntime
if(self.retries > self.__maxRetries):
if(self.__stateEndstopAutoCalibrate is True):
self.updateStatusbarMessage('Failed to detect endstop.')
_logger.warning('Failed to detect endstop. Cancelled operation.')
self.__stateAutoCPCapture = False
self.__stateEndstopAutoCalibrate = False
self.toggleEndstopAutoDetectionSignal.emit(False)
self.haltCPAutoCapture()
elif(self.__stateAutoNozzleAlignment is True):
updateMessage = 'Failed to detect nozzle. Try manual override.'
self.updateStatusbarMessage(updateMessage)
_logger.warning(updateMessage)
self.nozzleDetectionFailed()
return
self.pollCoordinatesSignal.emit()
return
self.autoCalibrate()
@pyqtSlot(object)
def saveCurrentPosition(self, coordinates):
self.__mutex.lock()
self.__currentPosition = coordinates
self.__mutex.unlock()
_logger.debug('Coordinates received:' + str(coordinates))
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
if(self.__stateManualCPCapture is True):
_logger.debug('saveCurrentPosition: manual CP capture')
self.__cpCoordinates = coordinates
self.__stateManualCPCapture = False
self.cpLabel.setText('CP: ('+ str(self.__cpCoordinates['X']) + ', ' + str(self.__cpCoordinates['Y']) + ')')
self.cpLabel.setStyleSheet(self.styleGreen)
self.updateStatusbarMessage('CP captured.')
self.repaint()
elif(self.__stateEndstopAutoCalibrate is True or self.__stateAutoNozzleAlignment is True):
if(self.state != 100):
if(int(self.__activePrinter['currentTool']) > -1):
_logger.debug('saveCurrentPosition: autoCalibrate nozzle for T' + str(int(self.__activePrinter['currentTool'])))
else:
_logger.debug('saveCurrentPosition: autoCalibrate endstop.')
elif(self.state == 100):
_logger.debug('saveCurrentPosition: autoCalibrate nozzle set offsets for T' + str(int(self.__activePrinter['currentTool'])))
# set state to detect next tool
self.state = 200
self.__stateAutoNozzleAlignment = True
self.toggleNozzleAutoDetectionSignal.emit(True)
params={'toolIndex': int(self.__activePrinter['currentTool']), 'position': coordinates, 'cpCoordinates': self.__cpCoordinates, 'continue': True}
self.setOffsetsSignal.emit(params)
return
self.getUVCoordinatesSignal.emit()
elif(self.__stateOverrideManualNozzleAlignment is True):
_logger.debug('saveCurrentPosition: manual nozzle set offsets for T' + str(int(self.__activePrinter['currentTool'])))
# set state to detect next tool
self.state = 200
self.__stateAutoNozzleAlignment = True
self.toggleNozzleAutoDetectionSignal.emit(True)
params={'toolIndex': int(self.__activePrinter['currentTool']), 'position': coordinates, 'cpCoordinates': self.__cpCoordinates, 'continue': True}
self.setOffsetsSignal.emit(params)
return
elif(self.__firstConnection):
_logger.debug('saveCurrentPosition: firstConnection')
self.__restorePosition = copy.deepcopy(self.__currentPosition)
self.__firstConnection = False
@pyqtSlot(object)
def calibrateOffsetsApplied(self, params=None):
try:
offsets = params['offsets']
except: offsets = None
try:
__continue = params['continue']
except: __continue = True
self.resumeAutoToolAlignmentButton.setDisabled(True)
self.resumeAutoToolAlignmentButton.setVisible(False)
self.resumeAutoToolAlignmentButton.setStyleSheet(self.styleDisabled)
for button in self.toolButtons:
buttonName = 'toolButton_' + str(self.__activePrinter['currentTool'])
if(button.objectName() == buttonName):
toolTip = 'Fetch T' + str(self.__activePrinter['currentTool']) + ' to current machine position.'
toolTip += ''
toolTip += 'X: ' + "{:>9.3f}".format(offsets['X'])
toolTip += '
'
toolTip += 'Y: ' + "{:>9.3f}".format(offsets['Y'])
toolTip += '
'
toolTip += 'Z: ' + "{:>9.3f}".format(offsets['Z'])
toolTip += ''
button.setToolTip(toolTip)
if(__continue is True):
toolCalibrationTime = np.around(time.time() - self.toolTime,1)
successMsg = 'Tool ' + str(self.__activePrinter['currentTool']) + ': (X' + str(offsets['X']) + ', Y' + str(offsets['Y']) + ', Z' + str(offsets['Z']) + ') -- [' + str(toolCalibrationTime) + 's].'
self.updateStatusbarMessage(successMsg)
_logger.info(successMsg)
self.state = 200
self.retries = 0
self.__stateAutoNozzleAlignment = True
self.__stateOverrideManualNozzleAlignment = False
self.toggleNozzleAutoDetectionSignal.emit(True)
self.calibrateTools(self.workingToolset)
else:
successMsg = 'Tool ' + str(self.__activePrinter['currentTool']) + ': (X' + str(offsets['X']) + ', Y' + str(offsets['Y']) + ', Z' + str(offsets['Z']) + ').'
self.updateStatusbarMessage(successMsg)
_logger.info(successMsg)
def saveOffsets(self):
self.saveToFirmwareSignal.emit()
_logger.info('Offsets saved to firmware.')
########################################################################### Interface with Settings Dialog
def displayPreferences(self, event=None, newPrinterFlag=False):
_logger.debug('*** calling App.displayPreferences')
# check if we already have a printer manager thread, and start it
try:
if(self.printerThread.isRunning() is False):
self.createPrinterManagerThread(announce=False)
except: self.createPrinterManagerThread(announce=False)
# Set up settings window
try:
self.settingsDialog = SettingsDialog(parent=self, newPrinter=newPrinterFlag, geometry=self.__settingsGeometry, settings=self.__userSettings, firmwareList=self.__firmwareList, cameraProperties=self.__activeCamera)
# # Signals
# self.settingsDialog.settingsAlignmentPollSignal.connect()
# self.settingsDialog.settingsChangeVideoSrcSignal.connect()
# self.settingsDialog.settingsRequestCameraProperties.connect()
# self.settingsDialog.settingsRequestImageProperties.connect()
self.settingsDialog.settingsResetImageSignal.connect(self.relayResetCameraDefaults)
self.settingsDialog.settingsSetImagePropertiesSignal.connect(self.relayImageParameters)
self.settingsDialog.settingsUpdateSignal.connect(self.updateSettings)
self.settingsDialog.settingsStatusbarSignal.connect(self.updateStatusbarMessage)
self.settingsDialog.settingsGeometrySignal.connect(self.saveSettingsGeometry)
self.settingsDialog.rejected.connect(self.settingsDialog.deleteLater)
self.settingsDialog.accepted.connect(self.settingsDialog.deleteLater)
self.settingsDialog.settingsNewPrinter.connect(self.saveNewPrinter)
except:
errorMsg = 'Cannot start settings dialog.'
_logger.exception(errorMsg)
return
# self.settingsDialog.update_settings.connect(self.updateSettings)
self.settingsDialog.exec()
_logger.debug('*** exiting App.displayPreferences')
@pyqtSlot(object)
def relayImageParameters(self, imageProperties):
self.setImagePropertiesSignal.emit(imageProperties)
@pyqtSlot()
def relayResetCameraDefaults(self):
self.resetImageSignal.emit()
@pyqtSlot(object)
def updateSettings(self, settingOptions):
_logger.debug('*** calling App.updateSettings')
self.__userSettings = settingOptions
self.saveUserSettings()
_logger.debug('*** exiting App.updateSettings')
@pyqtSlot(object)
def saveNewPrinter(self, newSettingsOptions):
self.__userSettings = newSettingsOptions
self.saveUserSettings()
newPrinterIndex = len(newSettingsOptions['printer'])-1
self.__activePrinter = newSettingsOptions['printer'][newPrinterIndex]
self.connectSignal.emit(self.__activePrinter)
@pyqtSlot(object)
def saveSettingsGeometry(self, geometry):
self.__settingsGeometry = geometry
########################################################################### Utilities
def sanitizeURL(self, inputString='http://localhost'):
_logger.debug('*** calling App.sanitizeURL')
_errCode = 0
_errMsg = ''
_printerURL = 'http://localhost'
u = urlparse(inputString)
if(u[0] ==''):
u = u._replace(scheme="http")
scheme = u[0]
if(scheme.lower() not in ['http','https']):
_errCode = 1
_errMsg = 'Invalid scheme. Please only use http/https connections.'
else:
if(u.netloc == ''):
_printerURL = u.scheme + '://' + u.path
else:
_printerURL = u.geturl()
_logger.debug('Sanitized ' + inputString + ' to address: ' + _printerURL)
_logger.debug('*** exiting App.sanitizeURL')
return(_errCode, _errMsg, _printerURL)
def saveUserSettings(self):
# save user settings.json
_logger.debug('*** calling App.saveUserSettings')
try:
# get default camera from user settings
cameraSet = False
# new_video_src = 0
# for camera in self.__userSettings['camera']:
# try:
# if(camera['default'] == 1 and cameraSet is False):
# # if new default, switch feed
# if(self._videoSrc != new_video_src):
# new_video_src = camera['video_src']
# self.video_thread.changeVideoSrc(newSrc=new_video_src)
# self._videoSrc = new_video_src
# cameraSet = True
# continue
# elif(cameraSet):
# # already have a default, unset other entries
# camera['default'] = 0
# except KeyError:
# # No default camera defined, add key
# camera['default'] = 0
# continue
# check if there are no cameras set as default
if(cameraSet is False):
# Set first camera entry to be the default source
self.__userSettings['camera'][0]['default'] = 1
# try:
# # activate default camera feed
# self.video_thread.changeVideoSrc(newSrc=new_video_src)
# self._videoSrc = new_video_src
# except:
# _logger.critical('Cannot load default camera source.')
# Save settings to file
with open('./config/settings.json','w') as outputfile:
json.dump(self.__userSettings, outputfile)
_logger.info('User preferences saved to settings.json')
self.updateStatusbarMessage('User preferences saved to settings.json')
self.statusBar.setStyleSheet('')
except Exception as e1:
_logger.error('Error saving user settings file.' + str(e1))
self.statusBar.showMessage('Error saving user settings file.')
self.statusBar.setStyleSheet(self.styleRed)
_logger.debug('*** exiting App.saveUserSettings')
def getDistance(self, x1, y1, x0, y0):
_logger.debug('*** calling CalibrateNozzles.getDistance')
x1_float = float(x1)
x0_float = float(x0)
y1_float = float(y1)
y0_float = float(y0)
x_dist = (x1_float - x0_float) ** 2
y_dist = (y1_float - y0_float) ** 2
retVal = np.sqrt((x_dist + y_dist))
returnVal = np.around(retVal,3)
_logger.debug('*** exiting CalibrateNozzles.getDistance')
return(returnVal)
def normalize_coords(self,coords):
xdim, ydim = self._cameraWidth, self._cameraHeight
returnValue = (coords[0] / xdim - 0.5, coords[1] / ydim - 0.5)
return(returnValue)
def least_square_mapping(self,calibration_points):
# Compute a 2x2 map from displacement vectors in screen space to real space.
n = len(calibration_points)
real_coords, pixel_coords = np.empty((n,2)),np.empty((n,2))
for i, (r,p) in enumerate(calibration_points):
real_coords[i] = r
pixel_coords[i] = p
x,y = pixel_coords[:,0],pixel_coords[:,1]
A = np.vstack([x**2,y**2,x * y, x,y,np.ones(n)]).T
transform = np.linalg.lstsq(A, real_coords, rcond = None)
return transform[0], transform[1].mean()
def toggleCrosshair(self):
if(self.__stateAutoCPCapture is False and self.__stateAutoNozzleAlignment is False and self.__stateEndstopAutoCalibrate is False):
if(self.__displayCrosshair is False):
self.crosshairDisplayButton.setChecked(True)
self.crosshairDisplayButton.setStyleSheet(self.styleOrange)
self.__displayCrosshair = True
self.toggleNozzleDetectionSignal.emit(True)
self.toggleDetectionSignal.emit(True)
self.__displayCrosshair = True
else:
self.crosshairDisplayButton.setChecked(False)
self.crosshairDisplayButton.setStyleSheet(self.styleBlue)
self.__displayCrosshair = False
self.toggleNozzleDetectionSignal.emit(False)
self.toggleDetectionSignal.emit(False)
self.__displayCrosshair = False
########################################################################### Close application handler
def closeEvent(self, event):
_logger.debug('*** calling App.closeEvent')
self.stateExiting()
try:
_logger.info('Closing TAMV..')
self.statusBar.showMessage('Shutting down TAMV..')
self.repaint()
try:
self.detectionThread.quit()
self.detectionThread.wait(self.__detectionManagerThreadWaitTime)
except: pass
try:
self.printerThread.quit()
self.printerThread.wait(self.__printerManagerThreadWaitTime)
except: pass
except Exception:
_logger.critical('Close event error: \n' + traceback.format_exc())
# Output farewell message
print()
print('Thank you for using TAMV!')
print('Check out www.jubilee3d.com')
_logger.debug('*** exiting App.closeEvent')
super(App, self).closeEvent(event)
########################################################################### First run setups
def show(self):
self.createDetectionManagerThread()
self.createPrinterManagerThread()
_logger.info('Initialization complete.')
# Output welcome message
print()
print(' Welcome to TAMV!')
print()
super().show()
def startModules(self):
self.detectionThread.start(priority=QThread.TimeCriticalPriority)
self.announceSignal.emit(False)
if __name__=='__main__':
### Setup OS options
# os.putenv("QT_LOGGING_RULES","qt5ct.debug=true")
os.putenv("OPENCV_VIDEOIO_DEBUG", "0")
# os.putenv("OPENCV_VIDEOIO_PRIORITY_LIST", "DSHOW,FFMPEG,GSTREAMER")
### Setup argmument parser
parser = argparse.ArgumentParser(description='Program to allign multiple tools on Duet/klipper based printers, using machine vision.', allow_abbrev=False)
parser.add_argument('-d','--debug',action='store_true',help='Enable debug output to terminal')
# Execute argument parser
args=vars(parser.parse_args())
### Setup logging
_logger = logging.getLogger("TAMV")
_logger.setLevel(logging.DEBUG)
### # file handler logging
fileFormatter = logging.Formatter('%(asctime)s --- %(levelname)s --- M:%(name)s --- T:%(threadName)s --- F:%(funcName)s --- L:%(lineno)d --- %(message)s')
# migrate logs to /log path
try:
os.makedirs('./log', exist_ok=True)
except:
print()
print('Cannot create \"./log folder.')
print('Please create this folder manually and restart TAMV')
raise SystemExit('Cannot retrieve log path.')
if(os.path.exists('./TAMV.log')):
if(os.path.exists('./log/TAMV.log')):
print()
print('Deleting old log file ./TAMV.log..')
try:
os.remove('./TAMV.log')
except:
print('Cannot delete old log file \"./TAMV.log\", ignoring it..')
print('Log file now located in \"./log/TAMV.log\"')
else:
try:
print()
print('Moving log file to \"./log/TAMV.log\"..')
os.replace('./TAMV.log','./log/TAMV.log')
except:
print('Unknown OS error while moving log file to new folder.')
print('Please move \"./TAMV.log\" to \"./log/TAMV.log\" and restart TAMV.')
raise SystemExit('Cannot move log file.')
fh = RotatingFileHandler('./log/TAMV.log',backupCount=1,maxBytes=1000000)
if(args['debug']):
fh.setLevel(logging.DEBUG)
else:
fh.setLevel(logging.INFO)
fh.setFormatter(fileFormatter)
_logger.addHandler(fh)
### # console handler logging
consoleFormatter = logging.Formatter(fmt='%(levelname)-9s: %(message)s')
ch = logging.StreamHandler()
if(args['debug']):
ch.setLevel(logging.DEBUG)
else:
ch.setLevel(logging.INFO)
ch.setFormatter(consoleFormatter)
_logger.addHandler(ch)
### # log startup messages
print()
_logger.warning('This is an alpha release. Always use only when standing next to your machine and ready to hit EMERGENCY STOP.')
print()
### start GUI application
app = QApplication(sys.argv)
a = App()
a.show()
t = threading.Thread(target=a.startModules)
t.start()
sys.exit(app.exec())