diff --git a/QThread_design/CAMERA_THREAD_FIX_ANALYSIS.md b/QThread_design/CAMERA_THREAD_FIX_ANALYSIS.md index be4586a..8fec08f 100644 --- a/QThread_design/CAMERA_THREAD_FIX_ANALYSIS.md +++ b/QThread_design/CAMERA_THREAD_FIX_ANALYSIS.md @@ -35,7 +35,7 @@ When a camera malfunctioned, several blocking operations could freeze the entire A dedicated `VideoThread` class was added to handle all camera operations in the background. -#### `VideoThread` Class ([Lines 33-125](../app.py#L33-L125)) +#### `VideoThread` Class ([Lines 102-209](../app.py#L102-L209)) ```python class VideoThread(QThread): @@ -71,13 +71,13 @@ class VideoThread(QThread): | Component | Old Implementation | New Implementation | Location | |-----------|-------------------|-------------------|----------| -| **Imports** | `QTimer` (removed) | +`QThread`, +`pyqtSignal` | [Line 16](../app.py#L16) | -| **Video Thread** | None | `VideoThread` class | [Lines 33-125](../app.py#L33-L125) | +| **Imports** | `QTimer` (removed) | +`QThread`, +`pyqtSignal` | [Line 19](../app.py#L19) | +| **Video Thread** | None | `VideoThread` class | [Lines 102-209](../app.py#L102-L209) | | **Instance Variable** | `self.timer` (`QTimer`) | `self.video_thread` (`VideoThread`) | Initialization | -| **Frame Capture** | `view_video()` in main thread | [`on_frame_captured()`](../app.py#L817) | [Lines 817-846](../app.py#L817-L846) | -| **Error Handling** | Basic checks, timer stops | [`on_video_error()`](../app.py#L848) | [Lines 848-855](../app.py#L848-L855) | -| **Cleanup** | `self.timer.stop()` + globals | [`quit_video()`](../app.py#L861) | [Lines 861-865](../app.py#L861-L865) | -| **Start/Stop** | `self.timer.start(20)` | [`controlTimer()`](../app.py#L867) | [Lines 867-880](../app.py#L867-L880) | +| **Frame Capture** | `view_video()` in main thread | [`on_frame_captured()`](../app.py#L940) | [Lines 940-970](../app.py#L940-L970) | +| **Error Handling** | Basic checks, timer stops | [`on_video_error()`](../app.py#L971) | [Lines 971-978](../app.py#L971-L978) | +| **Cleanup** | `self.timer.stop()` + globals | [`stop_video()`](../app.py#L1005) | [Lines 1005-1031](../app.py#L1005-L1031) | +| **Start/Stop** | `self.timer.start(20)` | [`start_video()`](../app.py#L984) | [Lines 984-1003](../app.py#L984-L1003) | ### What Was Removed @@ -331,20 +331,22 @@ Main Thread Video Thread The threading solution adds approximately 150 lines providing production-quality features: -1. **[VideoThread class](../app.py#L33-L111)** (79 lines) +1. **[VideoThread class](../app.py#L102-L209)** (108 lines) - Thread lifecycle management - Camera initialization and cleanup - Frame capture loop - Error detection and reporting -2. **Signal handlers** (~40 lines) - - [`on_frame_captured()`](../app.py#L803) - Frame processing in UI thread - - [`on_video_error()`](../app.py#L835) - Error display in UI thread - - [`display_error_message()`](../app.py#L778) - Visual feedback +2. **Signal handlers** (~70 lines) + - [`on_frame_captured()`](../app.py#L940) - Frame processing in UI thread + - [`on_video_error()`](../app.py#L971) - Error display in UI thread + - [`display_error_message()`](../app.py#L931) - Visual error feedback + - [`display_info_message()`](../app.py#L935) - Visual info feedback + - [`_display_message()`](../app.py#L898) - Internal helper -3. **Thread management** (~30 lines) - - [`controlTimer()`](../app.py#L849) - Start/stop control - - [`quit_video()`](../app.py#L843) - Graceful shutdown +3. **Thread management** (~50 lines) + - [`start_video()`](../app.py#L984) - Start control + - [`stop_video()`](../app.py#L1005) - Graceful shutdown ### Key Features @@ -355,9 +357,9 @@ The threading solution adds approximately 150 lines providing production-quality ✅ Clean resource management ✅ User-friendly error messages -### Verdict +### Implementation Notes -This is the **industry standard** approach for hardware I/O in GUI applications. The implementation follows Qt best practices and represents production-quality, reliable code. +This approach follows Qt best practices for hardware I/O in GUI applications. --- @@ -404,5 +406,5 @@ Moved camera operations to a background thread with signal-based communication t - Professional error handling - Industry-standard architecture -### The Verdict -This follows Qt best practices and is how professional PyQt applications handle hardware I/O. +### Summary +This implementation follows Qt best practices for threading and hardware I/O. diff --git a/QThread_design/DOCUMENTATION_INDEX.md b/QThread_design/DOCUMENTATION_INDEX.md index 3cfaeac..883ec8c 100644 --- a/QThread_design/DOCUMENTATION_INDEX.md +++ b/QThread_design/DOCUMENTATION_INDEX.md @@ -35,8 +35,8 @@ Quick overview of the problem and solution (1 page) Visual side-by-side comparison of old vs. new implementations (2-3 pages) **What's inside**: -- Architecture diagrams -- Before/after code comparison +- Architecture visual diagrams +- Before/after code comparison (with snippets) - Key differences table - Real-world impact @@ -64,7 +64,6 @@ Implementation details and best practices (5-6 pages) - Detailed breakdown of what was changed - Comparison with industry standards - Verification procedures -- Q&A section **Read this if**: You want comprehensive information about the implementation, including why certain design decisions were made. @@ -105,39 +104,6 @@ Camera operations were moved to a background thread (`VideoThread` class) with s --- -## Document Structure - -**[SUMMARY.md](SUMMARY.md)** -- Problem overview -- Solution overview -- Architecture comparison -- Benefits - -**[QUICK_COMPARISON.md](QUICK_COMPARISON.md)** -- Visual diagrams -- Code snippets -- Before/after tables -- Impact analysis - -**[CAMERA_THREAD_FIX_ANALYSIS.md](CAMERA_THREAD_FIX_ANALYSIS.md)** -- Root cause analysis -- Detailed implementation -- Complete code review -- Technical benefits - -**[RECOMMENDATIONS.md](RECOMMENDATIONS.md)** -- Change breakdown -- Industry standards -- Verification procedures -- Q&A - -**[THREADING_FIX_README.md](THREADING_FIX_README.md)** -- Quick reference -- Comprehensive overview -- All aspects tied together - ---- - ## Key Takeaways ### For Developers diff --git a/QThread_design/QUICK_COMPARISON.md b/QThread_design/QUICK_COMPARISON.md index 24a1350..52bda36 100644 --- a/QThread_design/QUICK_COMPARISON.md +++ b/QThread_design/QUICK_COMPARISON.md @@ -151,6 +151,4 @@ The new implementation follows Qt best practices: ✅ **Thread-safe communication** - Uses Qt's signal/slot mechanism ✅ **Resource management** - Proper cleanup on shutdown ✅ **Error resilience** - Handles failures gracefully -✅ **Maintainability** - Clear, well-structured code - -This is exactly how professional PyQt applications handle hardware I/O. +✅ **Maintainability** - Clear, well-structured code diff --git a/QThread_design/RECOMMENDATIONS.md b/QThread_design/RECOMMENDATIONS.md index f44da8b..ffddd92 100644 --- a/QThread_design/RECOMMENDATIONS.md +++ b/QThread_design/RECOMMENDATIONS.md @@ -106,9 +106,8 @@ The implemented solution (~150 lines) provides: - ✅ Clean resource management - ✅ User-friendly error messages -### Verdict +### Key Characteristics -This implementation represents **industry-standard** professional quality: 1. **Reliability** - Handles edge cases that would otherwise crash 2. **User experience** - Provides helpful feedback instead of freezing 3. **Maintainability** - Clear, well-structured code @@ -294,26 +293,6 @@ The implementation follows Qt's official guidelines: --- -## Questions and Answers - -### Q: Is ~120 additional lines too much? - -**A**: No. For production code handling hardware I/O in a GUI application, this is the **minimum viable implementation**. Anything less would compromise reliability or user experience. - -### Q: Could simpler approaches work? - -**A**: Technically yes, but they would be **inappropriate for production**: -- No error handling → crashes and confusion -- No proper shutdown → resource leaks -- No frame validation → potential crashes -- Simpler = more fragile - -### Q: Is this approach standard? - -**A**: **Yes, absolutely**. This is exactly how professional Qt applications handle hardware I/O. It follows Qt's official threading guidelines and industry best practices. - ---- - ## Conclusion ### What Was Achieved @@ -329,14 +308,6 @@ The camera handling refactor transformed the application from: - **Maintainability**: Clean, well-structured code - **Reputation**: Application appears professional and polished -### Final Assessment - -This is an exemplary implementation of threading for hardware I/O in a PyQt application, following industry best practices. - -The implementation demonstrates: -- ✅ Strong understanding of Qt threading -- ✅ Commitment to code quality -- ✅ Focus on user experience -- ✅ Professional development practices +### Summary -This is how it should be done. +The implementation follows industry best practices for threading in PyQt applications, providing robust error handling and a responsive user interface. diff --git a/QThread_design/SUMMARY.md b/QThread_design/SUMMARY.md index e381c3f..bdcff14 100644 --- a/QThread_design/SUMMARY.md +++ b/QThread_design/SUMMARY.md @@ -69,7 +69,7 @@ Main UI Thread Video Thread ### What Was Added -**`self.video_thread` - `VideoThread` Class** ([Lines 33-125](../app.py#L33-L125)): +**`self.video_thread` - `VideoThread` Class** ([Lines 102-209](../app.py#L102-L209)): - Replaces the old `self.timer` (`QTimer`) approach - Runs camera capture in background thread - Emits signals for frames and errors diff --git a/QThread_design/THREADING_FIX_README.md b/QThread_design/THREADING_FIX_README.md index 9e90612..832421d 100644 --- a/QThread_design/THREADING_FIX_README.md +++ b/QThread_design/THREADING_FIX_README.md @@ -89,8 +89,6 @@ self.video_thread.wait() - **Removed**: ~30 lines (timer-based code) - **Net change**: +120 lines -**Is this too much?** No - this is the minimum for production-quality threading. See `CAMERA_THREAD_FIX_ANALYSIS.md` for detailed justification. - --- ## Benefits @@ -138,23 +136,18 @@ The fix works correctly when: - **Main Thread**: Receives signals, updates UI ### Key Classes/Methods -- [`VideoThread`](../app.py#L33-L111): Camera thread implementation -- [`on_frame_captured()`](../app.py#L803): UI thread frame handler -- [`on_video_error()`](../app.py#L835): UI thread error handler -- [`display_error_message()`](../app.py#L778): Visual error feedback +- [`VideoThread`](../app.py#L102-L209): Camera thread implementation +- [`on_frame_captured()`](../app.py#L940): UI thread frame handler +- [`on_video_error()`](../app.py#L971): UI thread error handler +- [`display_error_message()`](../app.py#L931): Visual error feedback +- [`display_info_message()`](../app.py#L935): Visual info feedback +- [`start_video()`](../app.py#L984): Start video capture +- [`stop_video()`](../app.py#L1005): Stop video capture --- -## Why This Approach? - -### Industry Standard -Every professional GUI application that handles hardware uses this pattern: -- Video editors (Premiere, DaVinci) -- 3D software (Blender, Maya) -- IDEs (VS Code, PyCharm) -- Communication apps (Zoom, Teams) +## Qt Threading Guidelines -### Qt Best Practices The implementation follows Qt's official threading guidelines: 1. ✅ Never block the UI thread 2. ✅ Use signals/slots for inter-thread communication @@ -162,20 +155,6 @@ The implementation follows Qt's official threading guidelines: 4. ✅ Handle errors gracefully 5. ✅ Clean resource management ---- - -## Implementation Quality - -The ~120 additional lines provide essential production features: - -- **Error handling** - Prevents crashes -- **Graceful shutdown** - Proper cleanup -- **Frame validation** - Data integrity -- **User feedback** - Clear error messages -- **Resource management** - No leaks - -The current implementation follows **industry best practices** for Qt applications handling hardware I/O. - See [`CAMERA_THREAD_FIX_ANALYSIS.md`](CAMERA_THREAD_FIX_ANALYSIS.md#5-implementation-components) for detailed component breakdown. --- @@ -217,10 +196,8 @@ For complete details, see the documentation files: **Problem**: Camera operations blocked UI thread **Solution**: Moved to background thread with signals **Result**: Responsive, professional application -**Code**: +120 lines of industry-standard threading -**Status**: ✅ Implemented and working - -This is exactly how professional PyQt applications handle hardware I/O. +**Code**: +120 lines +**Status**: ✅ Implemented and working --- diff --git a/README.md b/README.md index 2059e68..1428755 100644 --- a/README.md +++ b/README.md @@ -61,17 +61,22 @@ Run the application: ``` or ```bash +(.venv) $ python app.py --camera-device 4 +``` +or +```bash (.venv) $ python app.py --play-video /path/to/your/video.mp4 ``` -Use `--help` to display the available options +Use `--help` to display the available options: ```console (.venv) $ python app.py --help -usage: app.py [-h] [--play-video path] +usage: app.py [-h] [--camera-device idx | --play-video path] Smart Car Dashboard GUI options: -h, --help show this help message and exit + --camera-device idx [Optional] camera device index to use (default: 0) --play-video path [Optional] path to video file to play instead of camera ``` @@ -82,6 +87,7 @@ options: + ## Todo diff --git a/app.py b/app.py index 97641b0..507e1d3 100644 --- a/app.py +++ b/app.py @@ -6,16 +6,19 @@ import io import sys import argparse +from datetime import datetime +from http.client import responses as http_responses # import OpenCV module import cv2 import folium +import requests # PyQt5 imports - Core -from PyQt5.QtCore import QRect, QSize, Qt, QCoreApplication, QMetaObject, QThread, pyqtSignal +from PyQt5.QtCore import QRect, QSize, Qt, QCoreApplication, QMetaObject, QThread, pyqtSignal, QTimer # PyQt5 imports - GUI -from PyQt5.QtGui import QPixmap, QImage, QFont, QPainter, QPen +from PyQt5.QtGui import QPixmap, QImage, QFont, QPainter, QPen, QColor # PyQt5 imports - Widgets from PyQt5.QtWidgets import ( QApplication, QWidget, QHBoxLayout, QLabel, QFrame, QPushButton, @@ -30,6 +33,72 @@ from qtwidgets import AnimatedToggle +def get_current_location(): + """ + Get the current geographic location based on IP address. + + Returns: + tuple: (latitude, longitude) of the current location + Falls back to New York City if geolocation fails + """ + # List of geolocation services to try (in order) + services = [ + { + 'name': 'ipapi.co', + 'url': 'https://ipapi.co/json/', + 'lat_key': 'latitude', + 'lon_key': 'longitude', + 'city_key': 'city', + 'country_key': 'country_name', + 'status_check': None # No status field to check + }, + { + 'name': 'ip-api.com', + 'url': 'http://ip-api.com/json/', + 'lat_key': 'lat', + 'lon_key': 'lon', + 'city_key': 'city', + 'country_key': 'country', + 'status_check': ('status', 'success') # Must have status='success' + } + ] + + # Try each service in order + for service in services: + try: + print(f"Attempting to detect location via {service['name']}...") + response = requests.get(service['url'], timeout=3) + + if response.status_code == 200: + data = response.json() + + # Check status field if required + if service['status_check']: + key, expected_value = service['status_check'] + if data.get(key) != expected_value: + print(f"✗ {service['name']} returned unexpected status") + continue + + # Extract coordinates + latitude = data.get(service['lat_key']) + longitude = data.get(service['lon_key']) + + if latitude is not None and longitude is not None: + city = data.get(service['city_key'], 'Unknown') + country = data.get(service['country_key'], 'Unknown') + print(f"✓ Location detected: {city}, {country} ({latitude}, {longitude})") + return (latitude, longitude) + else: + status_msg = http_responses.get(response.status_code, "Unknown Error") + print(f"✗ {service['name']} returned status code: {response.status_code} ({status_msg})") + except Exception as e: + print(f"✗ {service['name']} failed: {e}") + + # Fallback to New York City if all services fail + print("⚠ Using fallback location: New York City") + return (40.7128, -74.0060) + + class VideoThread(QThread): """ Thread for handling video/camera capture operations. @@ -40,9 +109,11 @@ class VideoThread(QThread): # Signal emitted when an error occurs error_occurred = pyqtSignal(str) # Emits error message - def __init__(self, video_path=None): + def __init__(self, camera_device=0, video_path=None, start_frame=0): super().__init__() + self.camera_device = camera_device self.video_path = video_path + self.start_frame = start_frame self.cap = None self.running = False self._should_stop = False @@ -60,11 +131,11 @@ def read_frame(): self._should_stop = False try: - # Initialize video capture (use video file if provided, otherwise use camera device 0) + # Initialize video capture (use video file if provided, otherwise use camera device) if self.video_path: self.cap = cv2.VideoCapture(self.video_path) else: - self.cap = cv2.VideoCapture(0) + self.cap = cv2.VideoCapture(self.camera_device) # Check if capture device opened successfully if not self.cap.isOpened(): @@ -74,7 +145,13 @@ def read_frame(): else: self.error_occurred.emit("Camera not found or inaccessible!\n\n" "Please check camera connection and permissions.") - self.stop() # Signal to skip main loop and proceed to cleanup + # Mark thread as stopped & exit immediately (cleanup will be handled by finally block) + self.stop() + return + + # For video files, seek to the start frame position (resume support) + if self.video_path and self.start_frame > 0: + self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.start_frame) # Main capture loop while not self._should_stop: @@ -118,6 +195,12 @@ def stop(self): """Request the thread to stop.""" self._should_stop = True + def get_current_frame_position(self): + """Get the current frame position for video files (returns 0 for camera).""" + if self.cap is not None and self.video_path: + return int(self.cap.get(cv2.CAP_PROP_POS_FRAMES)) + return 0 + def cleanup(self): """Release camera resources.""" if self.cap is not None: @@ -134,9 +217,11 @@ class Ui_MainWindow(object): WEBCAM_WIDTH = 321 WEBCAM_HEIGHT = 331 - def __init__(self, video_path=None): + def __init__(self, camera_device=0, video_path=None): + self.camera_device = camera_device self.video_path = video_path self.video_thread = None + self.last_frame_position = 0 # Track video position for resume def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") @@ -177,20 +262,27 @@ def setupUi(self, MainWindow): " \n" "background-color: rgba(43,87,120,100);\n" "\n" +"}\n" +"\n" +"QPushButton:disabled{\n" +" \n" +" background-color: rgba(70,110,160,130);\n" +" color: rgba(200,220,240,180);\n" +"\n" "}") self.frame.setFrameShape(QFrame.StyledPanel) self.frame.setFrameShadow(QFrame.Raised) self.frame.setObjectName("frame") self.horizontalLayout = QHBoxLayout(self.frame) self.horizontalLayout.setObjectName("horizontalLayout") - self.btn_dash = QPushButton(self.frame) + self.btn_dashboard = QPushButton(self.frame) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn_dash.sizePolicy().hasHeightForWidth()) - self.btn_dash.setSizePolicy(sizePolicy) - self.btn_dash.setObjectName("btn_dash") - self.horizontalLayout.addWidget(self.btn_dash) + sizePolicy.setHeightForWidth(self.btn_dashboard.sizePolicy().hasHeightForWidth()) + self.btn_dashboard.setSizePolicy(sizePolicy) + self.btn_dashboard.setObjectName("btn_dashboard") + self.horizontalLayout.addWidget(self.btn_dashboard) self.btn_ac = QPushButton(self.frame) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -402,18 +494,18 @@ def setupUi(self, MainWindow): "color:#fff;") self.label_16.setAlignment(Qt.AlignCenter) self.label_16.setObjectName("label_16") - self.frame_AC = QFrame(self.centralwidget) - self.frame_AC.setGeometry(QRect(70, 120, 971, 411)) - self.frame_AC.setStyleSheet("QFrame{\n" + self.frame_ac = QFrame(self.centralwidget) + self.frame_ac.setGeometry(QRect(70, 120, 971, 411)) + self.frame_ac.setStyleSheet("QFrame{\n" "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(34, 46, 61), stop:1 rgba(34, 34, 47));\n" "\n" "border-radius:200px;\n" "\n" "}") - self.frame_AC.setFrameShape(QFrame.StyledPanel) - self.frame_AC.setFrameShadow(QFrame.Raised) - self.frame_AC.setObjectName("frame_AC") - self.circularProgressCPU = QFrame(self.frame_AC) + self.frame_ac.setFrameShape(QFrame.StyledPanel) + self.frame_ac.setFrameShadow(QFrame.Raised) + self.frame_ac.setObjectName("frame_ac") + self.circularProgressCPU = QFrame(self.frame_ac) self.circularProgressCPU.setGeometry(QRect(720, 80, 220, 220)) self.circularProgressCPU.setStyleSheet("QFrame{\n" " border-radius: 110px; \n" @@ -455,7 +547,7 @@ def setupUi(self, MainWindow): "}") self.label_19.setAlignment(Qt.AlignCenter) self.label_19.setObjectName("label_19") - self.weather = QFrame(self.frame_AC) + self.weather = QFrame(self.frame_ac) self.weather.setGeometry(QRect(330, 10, 341, 351)) self.weather.setStyleSheet("QFrame{\n" "border-radius:5px;\n" @@ -552,7 +644,7 @@ def setupUi(self, MainWindow): self.line.setFrameShape(QFrame.VLine) self.line.setFrameShadow(QFrame.Sunken) self.line.setObjectName("line") - self.circularIndoor = QFrame(self.frame_AC) + self.circularIndoor = QFrame(self.frame_ac) self.circularIndoor.setGeometry(QRect(70, 90, 220, 220)) self.circularIndoor.setStyleSheet("QFrame{\n" " border-radius: 110px; \n" @@ -594,7 +686,7 @@ def setupUi(self, MainWindow): "}") self.label_21.setAlignment(Qt.AlignCenter) self.label_21.setObjectName("label_21") - self.checked = AnimatedToggle(self.frame_AC) + self.checked = AnimatedToggle(self.frame_ac) self.checked.setGeometry(QRect(140, 310, 100, 50)) self.frame_music = QFrame(self.centralwidget) self.frame_music.setGeometry(QRect(70, 120, 971, 411)) @@ -738,12 +830,20 @@ def setupUi(self, MainWindow): " \n" "background-color: rgba(0,171,169,100);\n" "\n" +"}\n" +"\n" +"QPushButton:disabled{\n" +" \n" +" background-color: rgba(0,100,98,50);\n" +" color: rgba(200,220,240,180);\n" +"\n" "}") self.frame_map.setFrameShape(QFrame.NoFrame) self.frame_map.setFrameShadow(QFrame.Raised) self.frame_map.setObjectName("frame_map") - coordinate = (24.413274773214205, 88.96567734902074) + # Get current location based on IP address + coordinate = get_current_location() m = folium.Map( tiles='OpenStreetMap', zoom_start=10, @@ -758,16 +858,16 @@ def setupUi(self, MainWindow): self.map_plot.setHtml(data.getvalue().decode()) self.map_plot.setObjectName(u"map_plot") self.map_plot.setGeometry(QRect(100, 40, 391, 331)) - self.pushButton_5 = QPushButton(self.frame_map) - self.pushButton_5.setObjectName(u"pushButton_5") - self.pushButton_5.setGeometry(QRect(830, 240, 119, 37)) - sizePolicy.setHeightForWidth(self.pushButton_5.sizePolicy().hasHeightForWidth()) - self.pushButton_5.setSizePolicy(sizePolicy) - self.pushButton_6 = QPushButton(self.frame_map) - self.pushButton_6.setObjectName(u"pushButton_6") - self.pushButton_6.setGeometry(QRect(830, 190, 119, 37)) - sizePolicy.setHeightForWidth(self.pushButton_6.sizePolicy().hasHeightForWidth()) - self.pushButton_6.setSizePolicy(sizePolicy) + self.btn_start = QPushButton(self.frame_map) + self.btn_start.setObjectName(u"btn_start") + self.btn_start.setGeometry(QRect(830, 240, 119, 37)) + sizePolicy.setHeightForWidth(self.btn_start.sizePolicy().hasHeightForWidth()) + self.btn_start.setSizePolicy(sizePolicy) + self.btn_stop = QPushButton(self.frame_map) + self.btn_stop.setObjectName(u"btn_stop") + self.btn_stop.setGeometry(QRect(830, 190, 119, 37)) + sizePolicy.setHeightForWidth(self.btn_stop.sizePolicy().hasHeightForWidth()) + self.btn_stop.setSizePolicy(sizePolicy) self.webcam = QLabel(self.frame_map) self.webcam.setObjectName(u"webcam") @@ -789,30 +889,53 @@ def setupUi(self, MainWindow): ) self.label_km.setAlignment(Qt.AlignCenter) - def display_error_message(self, message): - """Display error message in the video area with proper styling.""" + # Setup timer for date/time updates + self.datetime_timer = QTimer(MainWindow) + self.datetime_timer.timeout.connect(self.update_datetime) + self.datetime_timer.start(1000) # Update every 1000ms (1 second) + self.update_datetime() # Initial update + + def _display_message(self, message, border_color, text_size=16): + """ + Internal helper to display a message in the video area with customizable styling. + + Args: + message: Text to display + border_color: QColor or Qt color for the border + text_size: Font size for the message text (default: 16) + """ # Create a QPixmap with the same dimensions as the webcam area - error_pixmap = QPixmap(Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT) - error_pixmap.fill(Qt.black) # Black background to match the UI + pixmap = QPixmap(Ui_MainWindow.WEBCAM_WIDTH, Ui_MainWindow.WEBCAM_HEIGHT) + pixmap.fill(Qt.black) # Black background to match the UI - # Draw the error message on the pixmap - painter = QPainter(error_pixmap) - painter.setPen(QPen(Qt.red, 2)) + # Draw the message on the pixmap + painter = QPainter(pixmap) + painter.setPen(QPen(border_color, 2)) painter.setFont(QFont("Arial", 12, QFont.Bold)) # Draw border painter.drawRect(2, 2, Ui_MainWindow.WEBCAM_WIDTH - 4, Ui_MainWindow.WEBCAM_HEIGHT - 4) - # Draw error message in center + # Draw message in center painter.setPen(QPen(Qt.white, 1)) - text_rect = error_pixmap.rect() + painter.setFont(QFont("Arial", text_size, QFont.Bold)) + text_rect = pixmap.rect() text_rect.adjust(10, 0, -10, 0) # Add some margin painter.drawText(text_rect, Qt.AlignCenter | Qt.TextWordWrap, message) painter.end() - # Set the error pixmap to the webcam label - self.webcam.setPixmap(error_pixmap) + # Set the pixmap to the webcam label + self.webcam.setPixmap(pixmap) + + def display_error_message(self, message): + """Display error message in the video area with red border.""" + self._display_message(message, Qt.red, text_size=12) + + def display_info_message(self, message): + """Display info message in the video area with teal border.""" + teal_color = QColor(0, 171, 169) + self._display_message(message, teal_color, text_size=16) def on_frame_captured(self, frame): """ @@ -848,29 +971,25 @@ def on_frame_captured(self, frame): def on_video_error(self, error_message): """ Handle video-related errors (from video thread or frame processing). - Displays error message and stops video gracefully. + Stops video gracefully and displays error message. This runs in the main UI thread. """ - self.display_error_message(error_message) - self.quit_video() + self.stop_video() # Stop first (may show info message for a short period of time) + self.display_error_message(error_message) # Then overwrite with error message def is_video_running(self): """Check if video thread is currently running.""" return self.video_thread is not None and self.video_thread.isRunning() - def quit_video(self): - """Stop the video thread and clean up resources.""" - if self.is_video_running(): - self.video_thread.stop() - self.video_thread.wait() # Wait for thread to finish cleanly - - def controlTimer(self): - """Toggle video capture on/off.""" - if self.is_video_running(): - self.quit_video() - else: - # Create and start the video thread - self.video_thread = VideoThread(video_path=self.video_path) + def start_video(self): + """Start the video thread (if not already running).""" + if not self.is_video_running(): + # Create and start the video thread (with resume position for videos) + self.video_thread = VideoThread( + camera_device=self.camera_device, + video_path=self.video_path, + start_frame=self.last_frame_position + ) # Connect signals to slots self.video_thread.frame_captured.connect(self.on_frame_captured) @@ -878,15 +997,48 @@ def controlTimer(self): # Start the thread self.video_thread.start() + + # Update button states: disable Start, enable Stop + self.btn_start.setEnabled(False) + self.btn_stop.setEnabled(True) + + def stop_video(self): + """Stop the video thread and clean up resources (if running).""" + # Update button states: enable Start, disable Stop. + # This must be done first, regardless of thread state, because if the thread + # failed during initialization (e.g., camera not found), it may have already + # finished by the time we reach this method. In that case, the if block below + # won't execute, but the buttons still need to be reset to the "stopped" state. + self.btn_start.setEnabled(True) + self.btn_stop.setEnabled(False) + + if self.is_video_running(): + # Save current frame position for video files (to support resume) + self.last_frame_position = self.video_thread.get_current_frame_position() + + # Disconnect signals first to prevent any more frames from being displayed + self.video_thread.frame_captured.disconnect(self.on_frame_captured) + self.video_thread.error_occurred.disconnect(self.on_video_error) + + # Stop the thread + self.video_thread.stop() + self.video_thread.wait() # Wait for thread to finish cleanly + + # Display paused/stopped message instead of frozen last frame + if self.video_path: + self.display_info_message("Video Paused\n\nPress Start to continue") + else: + self.display_info_message("Camera Off\n\nPress Start to turn on") def retranslateUi(self, MainWindow): _translate = QCoreApplication.translate MainWindow.setWindowTitle(_translate("CAR DASHBOARD", "MainWindow")) - self.btn_dash.setText(_translate("MainWindow", "DASHBOARD")) + self.btn_dashboard.setText(_translate("MainWindow", "DASHBOARD")) self.btn_ac.setText(_translate("MainWindow", "AC")) self.btn_music.setText(_translate("MainWindow", "MUSIC")) self.btn_map.setText(_translate("MainWindow", "MAP")) - self.date.setText(_translate("MainWindow", "Date - Time-")) + # Now using real-time date/time from update_datetime() + # self.date.setText(_translate("MainWindow", "Date - Time-")) self.label_7.setText(_translate("MainWindow", "Locked")) self.label_5.setText(_translate("MainWindow", "Open")) self.label_4.setText(_translate("MainWindow", "Locked")) @@ -914,49 +1066,63 @@ def retranslateUi(self, MainWindow): self.label_20.setText(_translate("MainWindow", "Volume")) self.label_28.setText(_translate("MainWindow", "Mixer")) self.label_33.setText(_translate("MainWindow", "02. Mrittu Utpadon Karkhana - Shonar Bangla Circus")) - self.pushButton_5.setText(_translate("MainWindow", "Start")) - self.pushButton_6.setText(_translate("MainWindow", "Stop")) - # btn function - self.btn_dash.clicked.connect(self.show_dashboard) - self.btn_ac.clicked.connect(self.show_AC) - self.btn_music.clicked.connect(self.show_Music) - self.btn_map.clicked.connect(self.show_Map) + self.btn_start.setText(_translate("MainWindow", "Start")) + self.btn_stop.setText(_translate("MainWindow", "Stop")) + # Main tab navigation buttons + self.btn_dashboard.clicked.connect(self.show_dashboard) + self.btn_ac.clicked.connect(self.show_ac) + self.btn_music.clicked.connect(self.show_music) + self.btn_map.clicked.connect(self.show_map) + # Map tab video control buttons + self.btn_start.clicked.connect(self.start_video) + self.btn_stop.clicked.connect(self.stop_video) + + def update_datetime(self): + """Update the date and time display with current date/time.""" + current_datetime = datetime.now() + # Format: Month Date, Year (line 1) + # HH:MM:SS (line 2) + formatted_datetime = current_datetime.strftime("%B %d, %Y\n%H:%M:%S") + self.date.setText(formatted_datetime) + + def _switch_tab(self, target_frame, target_button, enable_video=False): + """ + Internal helper to switch between tabs. - def show_dashboard(self): - if self.frame_dashboard.isVisible(): - return - self.quit_video() - self.frame_dashboard.setVisible(True) - self.frame_AC.setVisible(False) - self.frame_music.setVisible(False) - self.frame_map.setVisible(False) - - def show_AC(self): - if self.frame_AC.isVisible(): - return - self.quit_video() - self.frame_dashboard.setVisible(False) - self.frame_AC.setVisible(True) - self.frame_music.setVisible(False) - self.frame_map.setVisible(False) - - def show_Music(self): - if self.frame_music.isVisible(): - return - self.quit_video() - self.frame_dashboard.setVisible(False) - self.frame_AC.setVisible(False) - self.frame_music.setVisible(True) - self.frame_map.setVisible(False) - - def show_Map(self): - if self.frame_map.isVisible(): + Args: + target_frame: The frame widget to make visible + target_button: The button widget to disable + enable_video: Whether to enable video after switching + """ + # Don't switch if already on this tab + if target_frame.isVisible(): return - self.frame_dashboard.setVisible(False) - self.frame_AC.setVisible(False) - self.frame_music.setVisible(False) - self.frame_map.setVisible(True) - self.controlTimer() + + # Show the target frame, hide all other frames + for frame in [self.frame_dashboard, self.frame_ac, self.frame_music, self.frame_map]: + frame.setVisible(frame == target_frame) + + # Disable the active tab's button, enable all other buttons + for button in [self.btn_dashboard, self.btn_ac, self.btn_music, self.btn_map]: + button.setEnabled(button != target_button) + + # Control video based on whether we're going to Map tab or not + if enable_video: + self.start_video() + else: + self.stop_video() + + def show_dashboard(self): + self._switch_tab(self.frame_dashboard, self.btn_dashboard) + + def show_ac(self): + self._switch_tab(self.frame_ac, self.btn_ac) + + def show_music(self): + self._switch_tab(self.frame_music, self.btn_music) + + def show_map(self): + self._switch_tab(self.frame_map, self.btn_map, enable_video=True) def progress(self): self.speed.set_MaxValue(100) @@ -998,7 +1164,23 @@ def progress(self): if __name__ == "__main__": # Parse command-line arguments parser = argparse.ArgumentParser(description='Smart Car Dashboard GUI') - parser.add_argument('--play-video', metavar='path', type=str, help='[Optional] path to video file to play instead of camera') + + # Create mutually exclusive group for video source selection + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( + '--camera-device', + metavar='idx', + type=int, + default=0, + help='[Optional] camera device index to use (default: 0)' + ) + source_group.add_argument( + '--play-video', + metavar='path', + type=str, + help='[Optional] path to video file to play instead of camera' + ) + args = parser.parse_args() # Enable automatic high DPI scaling @@ -1010,7 +1192,7 @@ def progress(self): app = QApplication(sys.argv) main_app_window = QMainWindow() - ui = Ui_MainWindow(video_path=args.play_video) + ui = Ui_MainWindow(camera_device=args.camera_device, video_path=args.play_video) ui.setupUi(main_app_window) # Center window on screen diff --git a/requirements.txt b/requirements.txt index 861a895..21d4aea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ PyQt5==5.15.10 PyQtWebEngine==5.15.7 qtwidgets==1.1 qtpy +requests diff --git a/ss/5.PNG b/ss/5.PNG index 9cedfd7..eafe708 100644 Binary files a/ss/5.PNG and b/ss/5.PNG differ diff --git a/ss/6.PNG b/ss/6.PNG new file mode 100644 index 0000000..449e60a Binary files /dev/null and b/ss/6.PNG differ