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