alert for persons in camera view
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
|
||||
name: Build MuCaPy Executable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-windows-exe:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install dependencies for Wine
|
||||
run: |
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo apt update
|
||||
sudo apt install -y wine64 wine32 unzip wget cabextract
|
||||
|
||||
- name: Install Windows Python under Wine
|
||||
run: |
|
||||
wget https://www.python.org/ftp/python/3.13.9/python-3.13.9-amd64.exe -O python_installer.exe
|
||||
wine python_installer.exe /quiet InstallAllUsers=1 PrependPath=1
|
||||
|
||||
- name: Upgrade pip and install PyInstaller (Windows)
|
||||
run: |
|
||||
wine python -m pip install --upgrade pip
|
||||
wine python -m pip install pyinstaller
|
||||
|
||||
- name: Build Windows executable
|
||||
run: |
|
||||
wine pyinstaller --onefile --windowed mucapy/main.py \
|
||||
--add-data "mucapy/styling;styling" \
|
||||
--add-data "mucapy/models;models" \
|
||||
--add-data "mucapy/todopackage;todopackage"
|
||||
|
||||
- name: Upload Windows executable
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mucapy-windows-exe
|
||||
path: dist/
|
||||
46
mucapy/compile.py
Normal file
46
mucapy/compile.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
import PyInstaller.__main__
|
||||
import PyQt5
|
||||
|
||||
# Paths
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
MAIN_SCRIPT = os.path.join(ROOT_DIR, "main.py")
|
||||
STYLING_DIR = os.path.join(ROOT_DIR, "styling")
|
||||
|
||||
# Icon paths
|
||||
PNG_ICON = os.path.join(STYLING_DIR, "logo.png")
|
||||
ICO_ICON = os.path.join(STYLING_DIR, "logo.ico")
|
||||
|
||||
# Convert PNG to ICO
|
||||
img = Image.open(PNG_ICON)
|
||||
img.save(ICO_ICON, format="ICO", sizes=[(256,256), (128,128), (64,64), (32,32), (16,16)])
|
||||
print(f"Converted {PNG_ICON} to {ICO_ICON}")
|
||||
|
||||
# Detect PyQt5 platforms folder automatically
|
||||
pyqt_dir = os.path.dirname(PyQt5.__file__)
|
||||
platforms_path = None
|
||||
|
||||
# Walk recursively to find the 'platforms' folder
|
||||
for root, dirs, files in os.walk(pyqt_dir):
|
||||
if 'platforms' in dirs:
|
||||
platforms_path = os.path.join(root, 'platforms')
|
||||
break
|
||||
|
||||
if platforms_path is None or not os.path.exists(platforms_path):
|
||||
raise FileNotFoundError(f"Could not locate PyQt5 'platforms' folder under {pyqt_dir}")
|
||||
|
||||
print(f"Using PyQt5 platforms folder: {platforms_path}")
|
||||
|
||||
# Build EXE with PyInstaller
|
||||
PyInstaller.__main__.run([
|
||||
MAIN_SCRIPT,
|
||||
'--noconfirm',
|
||||
'--onefile',
|
||||
'--windowed',
|
||||
f'--icon={ICO_ICON}',
|
||||
# Only include the platforms folder (minimal requirement for PyQt5)
|
||||
'--add-data', f'{platforms_path};PyQt5/Qt/plugins/platforms',
|
||||
])
|
||||
|
||||
print("Build complete! Check the 'dist' folder for the executable.")
|
||||
401
mucapy/main.py
401
mucapy/main.py
@@ -10,6 +10,7 @@ import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import ctypes
|
||||
import shutil
|
||||
import cv2
|
||||
import numpy as np
|
||||
import psutil # Add psutil import
|
||||
@@ -37,10 +38,18 @@ except ImportError as e:
|
||||
"3) Install VC++ runtime: https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
||||
"4) Restart the app after reinstalling.\n\n"
|
||||
f"Original error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
import todopackage.todo as todo # This shit will fail eventually | Or not IDK
|
||||
|
||||
# Audio alert dependencies
|
||||
import wave
|
||||
try:
|
||||
import simpleaudio as sa
|
||||
except Exception:
|
||||
sa = None
|
||||
# Force-disable simpleaudio to avoid potential native backend crashes; use OS-native players only
|
||||
sa = None
|
||||
|
||||
|
||||
def bytes_to_human(n: int) -> str:
|
||||
"""Convert a byte value to a human-readable string using base 1024.
|
||||
@@ -117,7 +126,7 @@ class Config:
|
||||
with open(self.config_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=4)
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error saving config: {e}")
|
||||
|
||||
@@ -374,6 +383,172 @@ class CameraScanThread(QThread):
|
||||
except Exception as e:
|
||||
print(f"CameraScanThread error: {e}")
|
||||
self.scan_finished.emit([], {})
|
||||
|
||||
class AlertWorker(QThread):
|
||||
"""Worker thread to play an alert sound safely without blocking UI.
|
||||
Uses winsound on Windows, external system players on Unix (afplay/paplay/aplay/ffplay),
|
||||
and falls back to simpleaudio if available. Supports cooperative stop.
|
||||
"""
|
||||
finished = pyqtSignal(bool, str) # success, message
|
||||
|
||||
def __init__(self, wav_path: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.wav_path = wav_path
|
||||
self._stop = False
|
||||
self._subproc = None
|
||||
self._play_obj = None
|
||||
|
||||
def stop(self):
|
||||
"""Request the worker to stop early."""
|
||||
try:
|
||||
self._stop = True
|
||||
if self._play_obj is not None:
|
||||
try:
|
||||
self._play_obj.stop()
|
||||
except Exception:
|
||||
pass
|
||||
if self._subproc is not None:
|
||||
try:
|
||||
self._subproc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _find_unix_player(self):
|
||||
"""Return (cmd_list, name) for an available player on Unix or (None, None)."""
|
||||
try:
|
||||
if sys.platform.startswith('darwin'):
|
||||
if shutil.which('afplay'):
|
||||
return (['afplay'], 'afplay')
|
||||
# Linux and others
|
||||
if shutil.which('paplay'):
|
||||
return (['paplay'], 'paplay')
|
||||
if shutil.which('aplay'):
|
||||
return (['aplay', '-q'], 'aplay')
|
||||
if shutil.which('ffplay'):
|
||||
return (['ffplay', '-nodisp', '-autoexit', '-loglevel', 'error'], 'ffplay')
|
||||
except Exception:
|
||||
pass
|
||||
return (None, None)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if not os.path.exists(self.wav_path):
|
||||
self.finished.emit(False, f"File not found: {self.wav_path}")
|
||||
return
|
||||
|
||||
# Windows path: prefer winsound (native, safe)
|
||||
if sys.platform.startswith('win'):
|
||||
ws_error = "unknown"
|
||||
try:
|
||||
import winsound as _ws # type: ignore
|
||||
# Resolve flags safely even if some attributes are missing
|
||||
SND_FILENAME = getattr(_ws, 'SND_FILENAME', 0x00020000)
|
||||
SND_SYNC = getattr(_ws, 'SND_SYNC', 0x0000) # 0 is synchronous by default
|
||||
flags = SND_FILENAME | SND_SYNC
|
||||
# Ensure PlaySound exists
|
||||
play_fn = getattr(_ws, 'PlaySound', None)
|
||||
if play_fn is None:
|
||||
raise RuntimeError('winsound.PlaySound not available')
|
||||
for _ in range(4):
|
||||
if self._stop:
|
||||
break
|
||||
try:
|
||||
play_fn(self.wav_path, flags)
|
||||
except Exception as e:
|
||||
# On failure, break to try alternative backends
|
||||
ws_error = str(e)
|
||||
break
|
||||
time.sleep(0.002)
|
||||
else:
|
||||
# Completed all 4 plays
|
||||
self.finished.emit(True, "Alert played")
|
||||
return
|
||||
# If here, winsound failed at some point; continue to fallbacks
|
||||
except Exception as e:
|
||||
ws_error = str(e)
|
||||
# Try simpleaudio on Windows as fallback
|
||||
if sa is not None:
|
||||
try:
|
||||
with wave.open(self.wav_path, 'rb') as wf:
|
||||
n_channels = max(1, wf.getnchannels())
|
||||
sampwidth = max(1, wf.getsampwidth())
|
||||
framerate = max(8000, wf.getframerate() or 44100)
|
||||
frames = wf.readframes(wf.getnframes())
|
||||
for _ in range(4):
|
||||
if self._stop:
|
||||
break
|
||||
self._play_obj = sa.play_buffer(frames, n_channels, sampwidth, framerate)
|
||||
self._play_obj.wait_done()
|
||||
time.sleep(0.002)
|
||||
self.finished.emit(True, "Alert played")
|
||||
return
|
||||
except Exception as e2:
|
||||
self.finished.emit(False, f"Playback error (winsound fallback -> simpleaudio): {e2}")
|
||||
return
|
||||
else:
|
||||
self.finished.emit(False, f"Audio backend not available (winsound failed: {ws_error})")
|
||||
return
|
||||
|
||||
# Non-Windows: try external players first
|
||||
cmd, name = self._find_unix_player()
|
||||
if cmd is not None:
|
||||
for _ in range(4):
|
||||
if self._stop:
|
||||
break
|
||||
try:
|
||||
self._subproc = subprocess.Popen(cmd + [self.wav_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
# Poll until done or stop requested
|
||||
while True:
|
||||
if self._stop:
|
||||
try:
|
||||
self._subproc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
ret = self._subproc.poll()
|
||||
if ret is not None:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
except Exception as e:
|
||||
# Try next backend
|
||||
cmd = None
|
||||
break
|
||||
finally:
|
||||
self._subproc = None
|
||||
time.sleep(0.002)
|
||||
if cmd is not None:
|
||||
self.finished.emit(True, "Alert played")
|
||||
return
|
||||
|
||||
# Fallback: simpleaudio if available
|
||||
if sa is not None:
|
||||
try:
|
||||
with wave.open(self.wav_path, 'rb') as wf:
|
||||
n_channels = max(1, wf.getnchannels())
|
||||
sampwidth = max(1, wf.getsampwidth())
|
||||
framerate = max(8000, wf.getframerate() or 44100)
|
||||
frames = wf.readframes(wf.getnframes())
|
||||
for _ in range(4):
|
||||
if self._stop:
|
||||
break
|
||||
self._play_obj = sa.play_buffer(frames, n_channels, sampwidth, framerate)
|
||||
self._play_obj.wait_done()
|
||||
time.sleep(0.002)
|
||||
self.finished.emit(True, "Alert played")
|
||||
return
|
||||
except Exception as e:
|
||||
self.finished.emit(False, f"Playback error (simpleaudio): {e}")
|
||||
return
|
||||
|
||||
self.finished.emit(False, "No audio backend available (afplay/paplay/aplay/ffplay/simpleaudio)")
|
||||
except Exception as e:
|
||||
try:
|
||||
self.finished.emit(False, str(e))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class MultiCamYOLODetector(QObject):
|
||||
cameras_scanned = pyqtSignal(list, dict) # Emits (available_cameras, index_to_name)
|
||||
def __init__(self, parent=None):
|
||||
@@ -594,8 +769,8 @@ class MultiCamYOLODetector(QObject):
|
||||
with open(classes_path, 'r') as f:
|
||||
self.classes = f.read().strip().split('\n')
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
|
||||
pass
|
||||
|
||||
np.random.seed(42)
|
||||
self.colors = np.random.randint(0, 255, size=(len(self.classes), 3), dtype='uint8')
|
||||
return True
|
||||
@@ -732,13 +907,26 @@ class MultiCamYOLODetector(QObject):
|
||||
|
||||
indices = cv2.dnn.NMSBoxes(boxes, confidences, self.confidence_threshold, 0.4)
|
||||
|
||||
person_detected = False
|
||||
if len(indices) > 0:
|
||||
for i in indices.flatten():
|
||||
(x, y, w, h) = boxes[i]
|
||||
color = [int(c) for c in self.colors[class_ids[i]]]
|
||||
cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
|
||||
text = f"{self.classes[class_ids[i]]}: {confidences[i]:.2f}"
|
||||
cls_name = self.classes[class_ids[i]] if 0 <= class_ids[i] < len(self.classes) else str(class_ids[i])
|
||||
text = f"{cls_name}: {confidences[i]:.2f}"
|
||||
cv2.putText(frame, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
|
||||
if not person_detected and str(cls_name).lower() == 'person':
|
||||
person_detected = True
|
||||
# Auto-trigger alert if a person is detected on any camera and alerts are enabled
|
||||
try:
|
||||
if person_detected:
|
||||
parent_window = self.parent()
|
||||
if parent_window is not None:
|
||||
# trigger_alert() has its own internal guards (enabled, cooldown, playing)
|
||||
parent_window.trigger_alert()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Detection error: {e}")
|
||||
|
||||
@@ -994,7 +1182,7 @@ class CameraDisplay(QLabel):
|
||||
with open(self.get_camera_display_style,"r") as cdst:
|
||||
self.setStyleSheet(cdst.read())
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
pass
|
||||
|
||||
self.setMinimumSize(320, 240)
|
||||
self.fullscreen_window = None
|
||||
@@ -1230,7 +1418,7 @@ class AboutWindow(QDialog):
|
||||
with open(toggle_btn_style,"r") as tgbstyle:
|
||||
self.toggle_btn.setStyleSheet(tgbstyle.read())
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
pass
|
||||
|
||||
# Debug shit
|
||||
#print("i did shit")
|
||||
@@ -1272,7 +1460,7 @@ class AboutWindow(QDialog):
|
||||
with open(style_file,"r") as aboutstyle:
|
||||
self.setStyleSheet(aboutstyle.read())
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
pass
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -1295,7 +1483,7 @@ class AboutWindow(QDialog):
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
print(f"Missing a Style File! => {todo_style_path}")
|
||||
exit(1)
|
||||
pass
|
||||
|
||||
# Create the labels for the fucking trodo ass shit ?
|
||||
self.todo_archive_object = todo
|
||||
@@ -1902,6 +2090,15 @@ class MainWindow(QMainWindow):
|
||||
self.camera_settings = {}
|
||||
self.detection_enabled = True # Add detection toggle flag
|
||||
|
||||
# Alert system state
|
||||
self.alert_enabled = bool(self.config.load_setting('alert_enabled', True))
|
||||
self._alert_playing = False
|
||||
self._alert_cooldown = False
|
||||
self._cooldown_timer = QTimer(self)
|
||||
self._cooldown_timer.setSingleShot(True)
|
||||
self._cooldown_timer.timeout.connect(self._on_cooldown_finished)
|
||||
self._alert_worker = None
|
||||
|
||||
# Load saved settings first
|
||||
self.load_saved_settings()
|
||||
|
||||
@@ -1918,8 +2115,8 @@ class MainWindow(QMainWindow):
|
||||
with open(style_file, "r") as mainstyle:
|
||||
self.setStyleSheet(mainstyle.read())
|
||||
except FileNotFoundError:
|
||||
exit(1)
|
||||
|
||||
pass
|
||||
|
||||
# Set palette for better dark mode support
|
||||
palette = self.palette()
|
||||
palette.setColor(palette.Window, QColor(45, 45, 45))
|
||||
@@ -1977,6 +2174,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if hasattr(self, 'model_label') and self.detector.model_dir:
|
||||
self.model_label.setText(f"Model: {os.path.basename(self.detector.model_dir)}")
|
||||
|
||||
# Ensure alert UI reflects backend availability and state
|
||||
try:
|
||||
self._update_alert_ui()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_menus(self):
|
||||
menubar = self.menuBar()
|
||||
@@ -2043,6 +2246,14 @@ class MainWindow(QMainWindow):
|
||||
self.toggle_detection_action.setShortcut('Ctrl+D')
|
||||
self.toggle_detection_action.triggered.connect(self.toggle_detection)
|
||||
view_menu.addAction(self.toggle_detection_action)
|
||||
|
||||
# Alert toggle moved to View menu
|
||||
self.toggle_alert_action = QAction('Enable Alert', self)
|
||||
self.toggle_alert_action.setCheckable(True)
|
||||
self.toggle_alert_action.setChecked(bool(self.alert_enabled))
|
||||
self.toggle_alert_action.setStatusTip('Play an audible alert when a person is detected')
|
||||
self.toggle_alert_action.triggered.connect(self.set_alert_enabled)
|
||||
view_menu.addAction(self.toggle_alert_action)
|
||||
|
||||
# Camera menu
|
||||
self.camera_menu = menubar.addMenu('Cameras')
|
||||
@@ -2331,6 +2542,8 @@ class MainWindow(QMainWindow):
|
||||
layout_layout.addWidget(self.layout_combo)
|
||||
settings_layout.addLayout(layout_layout)
|
||||
|
||||
# Button enablement determined dynamically based on backend availability
|
||||
|
||||
# Add screenshot button to settings
|
||||
screenshot_btn = QPushButton("Take Screenshot")
|
||||
screenshot_btn.clicked.connect(self.take_screenshot)
|
||||
@@ -2716,7 +2929,7 @@ class MainWindow(QMainWindow):
|
||||
bar.setStyleSheet(u60_style.read())
|
||||
except FileNotFoundError:
|
||||
print("Styling for CPU U60 not found!")
|
||||
exit(1)
|
||||
pass
|
||||
else:
|
||||
u60seperate = getpath.resource_path("styling/bar/seperate/u60.qss")
|
||||
try:
|
||||
@@ -2735,7 +2948,7 @@ class MainWindow(QMainWindow):
|
||||
bar.setStyleSheet(a85_styling.read())
|
||||
except FileNotFoundError:
|
||||
print("Styling for CPU u85 not found")
|
||||
exit(1)
|
||||
pass
|
||||
else:
|
||||
u85sep = getpath.resource_path("styling/bar/seperate/a85.qss")
|
||||
try:
|
||||
@@ -2754,7 +2967,7 @@ class MainWindow(QMainWindow):
|
||||
bar.setStyleSheet(else_style.read())
|
||||
except FileNotFoundError:
|
||||
print("No ElseStyling found!")
|
||||
exit(1)
|
||||
pass
|
||||
else:
|
||||
else_file_seperate = getpath.resource_path("styling/bar/seperate/else.qss")
|
||||
try:
|
||||
@@ -2775,6 +2988,162 @@ class MainWindow(QMainWindow):
|
||||
self.start_btn.setEnabled(False)
|
||||
self.stop_btn.setEnabled(False)
|
||||
|
||||
# ===== Alert system methods =====
|
||||
def set_alert_enabled(self, state: bool):
|
||||
try:
|
||||
self.alert_enabled = bool(state)
|
||||
self.config.save_setting('alert_enabled', self.alert_enabled)
|
||||
# Keep View menu action in sync
|
||||
try:
|
||||
if hasattr(self, 'toggle_alert_action'):
|
||||
self.toggle_alert_action.setChecked(self.alert_enabled)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
self._update_alert_ui()
|
||||
|
||||
def _has_audio_backend(self) -> bool:
|
||||
"""Check if any audio backend is available for alerts."""
|
||||
try:
|
||||
if sys.platform.startswith('win'):
|
||||
try:
|
||||
import winsound as _ws # type: ignore
|
||||
return hasattr(_ws, 'PlaySound')
|
||||
except Exception:
|
||||
return sa is not None
|
||||
# macOS/Linux external players
|
||||
if shutil.which('afplay') or shutil.which('paplay') or shutil.which('aplay') or shutil.which('ffplay'):
|
||||
return True
|
||||
return sa is not None
|
||||
except Exception:
|
||||
return sa is not None
|
||||
|
||||
def _update_alert_ui(self):
|
||||
try:
|
||||
backend_ok = self._has_audio_backend()
|
||||
# Update status tip on the View->Enable Alert action
|
||||
if hasattr(self, 'toggle_alert_action'):
|
||||
tip = []
|
||||
if not backend_ok:
|
||||
tip.append("No audio backend found (winsound/afplay/paplay/aplay/ffplay/simpleaudio)")
|
||||
if self._alert_cooldown:
|
||||
tip.append("Alert cooldown active (30s)")
|
||||
if self._alert_playing:
|
||||
tip.append("Alert is playing")
|
||||
if not self.alert_enabled:
|
||||
tip.append("Alert disabled")
|
||||
self.toggle_alert_action.setStatusTip("; ".join(tip) if tip else "Play an audible alert when a person is detected")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def trigger_alert(self):
|
||||
"""Trigger the alert sound if enabled and not in cooldown."""
|
||||
try:
|
||||
if not self.alert_enabled:
|
||||
return
|
||||
if self._alert_playing or self._alert_cooldown:
|
||||
return
|
||||
wav_path = getpath.resource_path("styling/sound/alert.wav")
|
||||
if not os.path.exists(wav_path):
|
||||
QMessageBox.warning(self, "Alert sound missing", f"Sound file not found:\n{wav_path}")
|
||||
return
|
||||
# Start worker (backend decided internally)
|
||||
self._alert_worker = AlertWorker(wav_path, self)
|
||||
self._alert_worker.finished.connect(self._on_alert_finished)
|
||||
self._alert_playing = True
|
||||
self._update_alert_ui()
|
||||
self._alert_worker.start()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Alert Error", f"Failed to trigger alert: {e}")
|
||||
self._alert_playing = False
|
||||
self._update_alert_ui()
|
||||
|
||||
def _on_alert_finished(self, success: bool, message: str):
|
||||
# Clean state and start cooldown on success
|
||||
try:
|
||||
self._alert_playing = False
|
||||
# Avoid holding a reference to finished thread
|
||||
try:
|
||||
if self._alert_worker:
|
||||
self._alert_worker.deleteLater()
|
||||
except Exception:
|
||||
pass
|
||||
self._alert_worker = None
|
||||
if success:
|
||||
self._alert_cooldown = True
|
||||
# 30 seconds cooldown
|
||||
self._cooldown_timer.start(30000)
|
||||
else:
|
||||
# Informative but non-fatal
|
||||
try:
|
||||
if message:
|
||||
print(f"Alert playback failed: {message}")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._update_alert_ui()
|
||||
|
||||
def _on_cooldown_finished(self):
|
||||
self._alert_cooldown = False
|
||||
self._update_alert_ui()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Ensure background workers and timers are stopped to avoid crashes on exit."""
|
||||
try:
|
||||
# Stop periodic timers
|
||||
try:
|
||||
if hasattr(self, 'timer') and self.timer is not None:
|
||||
self.timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self, 'hw_timer') and self.hw_timer is not None:
|
||||
self.hw_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self, '_cooldown_timer') and self._cooldown_timer is not None:
|
||||
self._cooldown_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stop alert worker if running
|
||||
try:
|
||||
if getattr(self, '_alert_worker', None) is not None:
|
||||
try:
|
||||
self._alert_worker.finished.disconnect(self._on_alert_finished)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self._alert_worker.isRunning():
|
||||
try:
|
||||
self._alert_worker.stop()
|
||||
except Exception:
|
||||
pass
|
||||
self._alert_worker.wait(2000)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._alert_worker.deleteLater()
|
||||
except Exception:
|
||||
pass
|
||||
self._alert_worker = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Disconnect cameras and stop camera threads
|
||||
try:
|
||||
if hasattr(self, 'detector') and self.detector is not None:
|
||||
self.detector.disconnect_cameras()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
event.accept()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class initQT:
|
||||
"""
|
||||
This is a QOL Change if you prefer to do it the hard way. Or you just like to get Fist Fucked then i suggest you remove the Function Calls in the
|
||||
@@ -2800,7 +3169,7 @@ class initQT:
|
||||
"echo $XDG_SESSION_TYPE"
|
||||
"Run this command in bash!"
|
||||
)
|
||||
exit(1)
|
||||
pass
|
||||
|
||||
def setenv(self):
|
||||
# Set the Session Type to the one it got
|
||||
@@ -2813,7 +3182,7 @@ class initQT:
|
||||
f"export XDG_SESSION_TYPE={self.session_type}"
|
||||
"run this command in bash"
|
||||
)
|
||||
exit(1)
|
||||
pass
|
||||
@staticmethod
|
||||
def shutupCV():
|
||||
# This needs some fixing as this only works before importing CV2 ; too much refactoring work tho!
|
||||
|
||||
BIN
mucapy/styling/sound/alert.wav
Normal file
BIN
mucapy/styling/sound/alert.wav
Normal file
Binary file not shown.
Reference in New Issue
Block a user