Skip to content

Commit

Permalink
Update launcher to let the user choose between running the Depthai De…
Browse files Browse the repository at this point in the history
…mo or Depthai Viewer (#1068)

* Prompt user to choose between launching depthai demo or depthai viewer when using launcher

* Add back signal that I accidentally deleted in merge conflict

* Close launcher on choose app dialog cancellation

* Change demo card to use the old logo, update the viewer card to include new and beta. Fix initial resize to be symmetric

* close splash screen properly, move demo dependency installation into the else branch - how to show splash screen when installing depthai-viewer dependencies?

* Ensure that splashScreen.close gets called from the main thread - this fixes NSWindow crashes on macos
  • Loading branch information
zrezke authored Aug 4, 2023
1 parent ade464a commit 11f12f0
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 45 deletions.
86 changes: 86 additions & 0 deletions launcher/choose_app_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from PyQt5 import QtCore, QtWidgets
import os
from pathlib import Path


class ImageButton(QtWidgets.QPushButton):
def __init__(self, icon_path, parent=None):
super().__init__(parent)
icon_path = icon_path.replace('\\', '/')
self.setStyleSheet(f"""
QPushButton {{
border: none;
border-image: url({icon_path}) 0 0 0 0 stretch stretch;
}}
QPushButton:hover {{
border-image: url({icon_path}) 0 0 0 0 stretch stretch;
border: 3px solid #999999;
}}
""")


class CardWidget(QtWidgets.QWidget):
clicked = QtCore.pyqtSignal()

def __init__(self, title, image_path: Path, parent=None):
super(CardWidget, self).__init__(parent)

# Create an image button with the given card image
path = os.path.normpath(image_path)
button = ImageButton(path, self)

# Make the button fill all available space
button.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)

# Add the widget to the UI
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(button)

# Connect button to signal
button.clicked.connect(self.clicked)

def resizeEvent(self, event):
# Resize the widget to keep the aspect ratio of the image
width = self.width()
new_height = width * 64 // 177 # Adjust height based on width
self.resize(width, int(new_height))


class ChooseAppDialog(QtWidgets.QDialog):
viewerChosen: bool = False

def __init__(self, parent: QtWidgets.QWidget=None):
super(ChooseAppDialog, self).__init__(parent)
self.setWindowTitle("Choose an application")

hbox = QtWidgets.QHBoxLayout()
hbox.setContentsMargins(0, 0, 0, 0)
file_path = Path(os.path.abspath(os.path.dirname(__file__)))
demo_image_path = file_path / "demo_card.png"
viewer_image_path = file_path / "viewer_card.png"
demo_card = CardWidget("DepthAI Demo", demo_image_path)
viewer_card = CardWidget("DepthAI Viewer", viewer_image_path)
hbox.addWidget(demo_card)
hbox.addWidget(viewer_card)
self.setLayout(hbox)

demo_card.clicked.connect(self.runDemo)
viewer_card.clicked.connect(self.runViewer)

# Get screen dimensions
screen = QtWidgets.QApplication.instance().primaryScreen()
screen_size = screen.size()
width = screen_size.width() // 2
height = width // 2 * 64 // 177
self.resize(width, height)

@QtCore.pyqtSlot()
def runDemo(self):
self.accept()

@QtCore.pyqtSlot()
def runViewer(self):
self.viewerChosen = True
self.accept()
Binary file added launcher/demo_card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 115 additions & 44 deletions launcher/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# PyQt5
from PyQt5 import QtCore, QtGui, QtWidgets

from choose_app_dialog import ChooseAppDialog

# Constants
SCRIPT_DIRECTORY=Path(os.path.abspath(os.path.dirname(__file__)))
DEPTHAI_DEMO_SCRIPT='depthai_demo.py'
Expand Down Expand Up @@ -69,11 +71,11 @@ def flush(self):
# Create splash screen
splashScreen = SplashScreen(str(SCRIPT_DIRECTORY/'splash2.png'))

def closeSplash():
splashScreen.hide()

class Worker(QtCore.QThread):
signalUpdateQuestion = QtCore.pyqtSignal(str, str)
signalChooseApp = QtCore.pyqtSignal()
signalCloseSplash = QtCore.pyqtSignal()
sigInfo = QtCore.pyqtSignal(str, str)
sigCritical = QtCore.pyqtSignal(str, str)
sigWarning = QtCore.pyqtSignal(str, str)
Expand All @@ -89,6 +91,22 @@ def updateQuestion(self, title, message):
else:
self.shouldUpdate = False
return False

@QtCore.pyqtSlot()
def chooseApp(self) -> None:
"""
Until Depthai Viewer is in beta, allow the user to choose between running the demo or the viewer.
"""
# If the dialog is rejected, the user has clicked exit - so we exit
dialog = ChooseAppDialog(splashScreen)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
self.viewerChosen = dialog.viewerChosen
else:
raise RuntimeError("User cancelled app choice dialog")

@QtCore.pyqtSlot()
def closeSplash(self):
splashScreen.close()

@QtCore.pyqtSlot(str,str)
def showInformation(self, title, message):
Expand All @@ -105,6 +123,8 @@ def showCritical(self, title, message):
def __init__(self, parent = None):
QtCore.QThread.__init__(self, parent)
self.signalUpdateQuestion[str, str].connect(self.updateQuestion, QtCore.Qt.BlockingQueuedConnection)
self.signalChooseApp.connect(self.chooseApp, QtCore.Qt.BlockingQueuedConnection)
self.signalCloseSplash.connect(self.closeSplash, QtCore.Qt.BlockingQueuedConnection)
self.sigInfo[str, str].connect(self.showInformation, QtCore.Qt.BlockingQueuedConnection)
self.sigCritical[str, str].connect(self.showCritical, QtCore.Qt.BlockingQueuedConnection)
self.sigWarning[str, str].connect(self.showWarning, QtCore.Qt.BlockingQueuedConnection)
Expand Down Expand Up @@ -306,58 +326,110 @@ def run(self):
self.sigWarning.emit(title, message)

try:
self.signalChooseApp.emit()
# Set to quit splash screen a little after subprocess is ran
skipSplashQuitFirstTime = False
def removeSplash():
time.sleep(2.5)
if not skipSplashQuitFirstTime:
closeSplash()
self.signalCloseSplash.emit()
quitThread = threading.Thread(target=removeSplash)
quitThread.start()
if self.viewerChosen:
print("Depthai Viewer chosen, checking if depthai-viewer is installed.")
# Check if depthai-viewer is installed
is_viewer_installed_cmd = [sys.executable, "-m", "pip", "show", "depthai-viewer"]
viewer_available_ret = subprocess.run(is_viewer_installed_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if viewer_available_ret.returncode != 0:
splashScreen.updateSplashMessage('Installing Depthai Viewer ...')
splashScreen.enableHeartbeat(True)
print("Depthai Viewer not installed, installing...")
# Depthai Viewer isn't installed, install it
# First upgrade pip
subprocess.run([sys.executable, "-m", "pip", "install", "-U", "pip"], check=True)
# Install depthai-viewer - Don't check, it can error out because of dependency conflicts but still install successfully
subprocess.run([sys.executable, "-m", "pip", "install", "depthai-viewer"])
# Check again if depthai-viewer is installed
viewer_available_ret = subprocess.run(is_viewer_installed_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if viewer_available_ret.returncode != 0:
raise RuntimeError("Depthai Viewer failed to install.")
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)

# All ready, run the depthai_demo.py as a separate process
ret = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)

# Print out stderr first
sys.stderr.write(ret.stderr.decode())

print(f'DepthAI Demo ret code: {ret.returncode}')
# Install dependencies if demo signaled missing dependencies
if ret.returncode == 42:
skipSplashQuitFirstTime = True
print(f'Dependency issue raised. Retrying by installing requirements and restarting demo.')

# present message of installing dependencies
splashScreen.updateSplashMessage('Installing DepthAI Requirements ...')
splashScreen.enableHeartbeat(True)
viewer_version = version.parse(viewer_available_ret.stdout.decode().splitlines()[1].split(" ")[1].strip())
print(f"Installed Depthai Viewer version: {viewer_version}")
# Get latest depthai-viewer version
latest_ret = subprocess.run([sys.executable, "-m", "pip", "index", "versions", "depthai-viewer"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if latest_ret.returncode != 0:
raise RuntimeError("Couldn't get latest depthai-viewer version.")
latest_viewer_version = version.parse(latest_ret.stdout.decode().split("LATEST:")[1].strip())
print(f"Latest Depthai Viewer version: {latest_viewer_version}")
if latest_viewer_version > viewer_version:
# Update is available, ask user if they want to update
title = 'DepthAI Viewer update available'
message = f'Version {str(latest_viewer_version)} of depthai-viewer is available, current version {str(viewer_version)}. Would you like to update?'
self.signalUpdateQuestion.emit(title, message)
if self.shouldUpdate:
splashScreen.updateSplashMessage(f'Updating Depthai Viewer to version {latest_viewer_version} ...')
splashScreen.enableHeartbeat(True)
# Update depthai-viewer
subprocess.run([sys.executable, "-m", "pip", "install", "-U", "depthai-viewer"])
# Test again to see if viewer is installed and updated
viewer_available_ret = subprocess.run(is_viewer_installed_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if viewer_available_ret.returncode != 0:
raise RuntimeError(f"Installing version {latest_viewer_version} failed.")
viewer_version = version.parse(viewer_available_ret.stdout.decode().splitlines()[1].split(" ")[1].strip())
if latest_viewer_version > viewer_version:
raise RuntimeError("Depthai Viewer failed to update.")
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)

# All ready, run the depthai-viewer as a seperate process
ret = subprocess.run([sys.executable, "-m", "depthai_viewer"])
else:
# All ready, run the depthai_demo.py as a separate process
ret = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)

# Install requirements for depthai_demo.py
MAX_RETRY_COUNT = 3
installReqCall = None
for retry in range(0, MAX_RETRY_COUNT):
installReqCall = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_INSTALL_REQUIREMENTS_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)
if installReqCall.returncode == 0:
break
if installReqCall.returncode != 0:
# Some error happened. Notify user
title = 'Error Installing DepthAI Requirements'
message = f"Couldn't install DepthAI requirements. Check internet connection and try again. Log available at: {LOG_FILE_PATH}"
print(f'Message Box ({title}): {message}')
print(f'Install dependencies call failed with return code: {installReqCall.returncode}, message: {installReqCall.stderr.decode()}')
self.sigCritical.emit(title, message)
raise Exception(title)
# Print out stderr first
sys.stderr.write(ret.stderr.decode())

# Remove message and animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)
print(f'DepthAI Demo ret code: {ret.returncode}')
# Install dependencies if demo signaled missing dependencies
if ret.returncode == 42:
skipSplashQuitFirstTime = True
print(f'Dependency issue raised. Retrying by installing requirements and restarting demo.')

quitThread.join()
skipSplashQuitFirstTime = False
quitThread = threading.Thread(target=removeSplash)
quitThread.start()
# present message of installing dependencies
splashScreen.updateSplashMessage('Installing DepthAI Requirements ...')
splashScreen.enableHeartbeat(True)

# All ready, run the depthai_demo.py as a separate process
subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository)
# Install requirements for depthai_demo.py
MAX_RETRY_COUNT = 3
installReqCall = None
for retry in range(0, MAX_RETRY_COUNT):
installReqCall = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_INSTALL_REQUIREMENTS_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)
if installReqCall.returncode == 0:
break
if installReqCall.returncode != 0:
# Some error happened. Notify user
title = 'Error Installing DepthAI Requirements'
message = f"Couldn't install DepthAI requirements. Check internet connection and try again. Log available at: {LOG_FILE_PATH}"
print(f'Message Box ({title}): {message}')
print(f'Install dependencies call failed with return code: {installReqCall.returncode}, message: {installReqCall.stderr.decode()}')
self.sigCritical.emit(title, message)
raise Exception(title)

# Remove message and animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)

quitThread.join()
skipSplashQuitFirstTime = False
quitThread = threading.Thread(target=removeSplash)
quitThread.start()

# All ready, run the depthai_demo.py as a separate process
subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository)
except:
pass
finally:
Expand All @@ -368,8 +440,7 @@ def removeSplash():
print(f'Unknown error occured ({ex}), exiting...')
finally:
# At the end quit anyway
closeSplash()
splashScreen.close()
self.signalCloseSplash.emit()
qApp.exit()

qApp.worker = Worker()
Expand Down
Binary file added launcher/viewer_card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion launcher/windows/installer_win64.iss
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ Source: "..\{#MyAppIconName}"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\launcher.py"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\requirements.txt"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\splash2.png"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\demo_card.png"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\viewer_card.png"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\splash_screen.py"; DestDir: "{app}"; Flags: ignoreversion
; Source: "..\choose_app_dialog.py"; DestDir: "{app}"; Flags: ignoreversion
; ; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Icons]
Expand Down Expand Up @@ -208,4 +211,4 @@ end;
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
Type: filesandordirs; Name: "{app}"

0 comments on commit 11f12f0

Please sign in to comment.