main.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import sys
  2. import os
  3. import yaml
  4. import requests
  5. import cv2
  6. import numpy as np
  7. from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
  8. QHBoxLayout, QPushButton, QLabel, QMessageBox,
  9. QGroupBox, QTextEdit, QStatusBar, QSizePolicy,
  10. QSystemTrayIcon, QMenu)
  11. from PyQt6.QtCore import QThread, pyqtSignal, Qt, pyqtSlot, QPoint
  12. from PyQt6.QtGui import QImage, QPixmap, QFont, QPainter, QColor, QBrush, QPen, QAction, QCursor, QIcon
  13. class ConfigManager:
  14. DEFAULT_CONFIG = {
  15. "video_stream_url": "http://222.243.138.146:9002/live/cam890134962b.live.mp4",
  16. "door_ip": "192.168.188.205",
  17. "door_port": 14460,
  18. "door_password": "",
  19. "door_id": 1
  20. }
  21. @staticmethod
  22. def load_config(config_path="config.yaml"):
  23. if not os.path.exists(config_path):
  24. return ConfigManager.DEFAULT_CONFIG
  25. try:
  26. with open(config_path, 'r', encoding='utf-8') as f:
  27. config = yaml.safe_load(f)
  28. if not config:
  29. return ConfigManager.DEFAULT_CONFIG
  30. # Merge with defaults to ensure all keys exist
  31. final_config = ConfigManager.DEFAULT_CONFIG.copy()
  32. final_config.update(config)
  33. return final_config
  34. except Exception as e:
  35. print(f"Error loading config: {e}")
  36. return ConfigManager.DEFAULT_CONFIG
  37. class VideoThread(QThread):
  38. change_pixmap_signal = pyqtSignal(QImage)
  39. connection_status_signal = pyqtSignal(bool)
  40. def __init__(self, url):
  41. super().__init__()
  42. self.url = url
  43. self.running = True
  44. def run(self):
  45. # Attempt to open the video stream
  46. # Note: ws:// streams with .mp4 extension often require specific handling
  47. # or an ffmpeg backend. We try standard cv2 first.
  48. cap = cv2.VideoCapture(self.url)
  49. if not cap.isOpened():
  50. # Fallback/Retry logic could go here
  51. pass
  52. while self.running:
  53. ret, frame = cap.read()
  54. if ret:
  55. self.connection_status_signal.emit(True)
  56. rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  57. h, w, ch = rgb_image.shape
  58. bytes_per_line = ch * w
  59. convert_to_qt_format = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
  60. p = convert_to_qt_format.scaled(480, 360, Qt.AspectRatioMode.KeepAspectRatio)
  61. self.change_pixmap_signal.emit(p)
  62. else:
  63. self.connection_status_signal.emit(False)
  64. # Try to reconnect if stream drops
  65. cap.release()
  66. cv2.waitKey(1000) # Wait a bit before reconnecting
  67. cap = cv2.VideoCapture(self.url)
  68. cap.release()
  69. def stop(self):
  70. self.running = False
  71. self.wait()
  72. class RemoteOpenWorker(QThread):
  73. result_signal = pyqtSignal(str, bool)
  74. def __init__(self, ip, port, password, door_id):
  75. super().__init__()
  76. self.ip = ip
  77. self.port = port
  78. self.password = password
  79. self.door_id = door_id
  80. def run(self):
  81. url = f"http://{self.ip}:{self.port}/openDoorControl"
  82. payload = {
  83. "pass": self.password,
  84. "doorId": self.door_id
  85. }
  86. try:
  87. response = requests.post(url, json=payload, timeout=5)
  88. if response.status_code == 200:
  89. try:
  90. data = response.json()
  91. if data.get("result") is True:
  92. self.result_signal.emit(f"Remote Open Success: {data.get('message', 'OK')}", True)
  93. else:
  94. self.result_signal.emit(f"Remote Open Failed: {data.get('message', 'Unknown error')}", False)
  95. except ValueError:
  96. self.result_signal.emit(f"Invalid JSON response: {response.text}", False)
  97. else:
  98. self.result_signal.emit(f"HTTP Error: {response.status_code}", False)
  99. except Exception as e:
  100. self.result_signal.emit(f"Connection Error: {str(e)}", False)
  101. class DoorControlWorker(QThread):
  102. result_signal = pyqtSignal(str, bool) # message, success
  103. def __init__(self, ip, port, password, control_way):
  104. super().__init__()
  105. self.ip = ip
  106. self.port = port
  107. self.password = password
  108. self.control_way = control_way
  109. def run(self):
  110. url = f"http://{self.ip}:{self.port}/setEmergencyControl"
  111. payload = {
  112. "pass": self.password,
  113. "controlWay": self.control_way
  114. }
  115. try:
  116. # Image implies form-data or json?
  117. # The postman screenshot shows 'Body' -> 'raw' -> 'JSON'.
  118. response = requests.post(url, json=payload, timeout=5)
  119. if response.status_code == 200:
  120. try:
  121. data = response.json()
  122. # Check 'result' field based on screenshot
  123. if data.get("result") is True:
  124. self.result_signal.emit(f"Success: {data.get('message', 'OK')}", True)
  125. else:
  126. self.result_signal.emit(f"Failed: {data.get('message', 'Unknown error')}", False)
  127. except ValueError:
  128. self.result_signal.emit(f"Invalid JSON response: {response.text}", False)
  129. else:
  130. self.result_signal.emit(f"HTTP Error: {response.status_code}", False)
  131. except Exception as e:
  132. self.result_signal.emit(f"Connection Error: {str(e)}", False)
  133. class FloatingButton(QWidget):
  134. clicked = pyqtSignal()
  135. def __init__(self, parent=None):
  136. super().__init__(parent)
  137. self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool)
  138. self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
  139. self.resize(60, 60)
  140. self.dragging = False
  141. self.offset = QPoint()
  142. # Initial position (e.g., right-center of screen)
  143. screen = QApplication.primaryScreen().geometry()
  144. self.move(screen.width() - 80, screen.height() // 2)
  145. def paintEvent(self, event):
  146. painter = QPainter(self)
  147. painter.setRenderHint(QPainter.RenderHint.Antialiasing)
  148. # Draw circle
  149. painter.setBrush(QBrush(QColor("#2196F3"))) # Blue like the "Always Open" button
  150. painter.setPen(QPen(Qt.GlobalColor.white, 2))
  151. painter.drawEllipse(5, 5, 50, 50)
  152. # Draw "Open" text or icon
  153. painter.setPen(Qt.GlobalColor.white)
  154. font = painter.font()
  155. font.setBold(True)
  156. font.setPointSize(10)
  157. painter.setFont(font)
  158. painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "Open")
  159. def mousePressEvent(self, event):
  160. if event.button() == Qt.MouseButton.LeftButton:
  161. self.dragging = True
  162. self.offset = event.globalPosition().toPoint() - self.pos()
  163. self.start_pos = event.globalPosition().toPoint()
  164. def mouseMoveEvent(self, event):
  165. if self.dragging:
  166. self.move(event.globalPosition().toPoint() - self.offset)
  167. def mouseReleaseEvent(self, event):
  168. if event.button() == Qt.MouseButton.LeftButton:
  169. self.dragging = False
  170. end_pos = event.globalPosition().toPoint()
  171. dist = (end_pos - self.start_pos).manhattanLength()
  172. if dist < 5: # Threshold
  173. self.clicked.emit()
  174. class MainWindow(QMainWindow):
  175. def __init__(self):
  176. super().__init__()
  177. self.config = ConfigManager.load_config()
  178. self.setWindowTitle("大门控制工具 (Door Control)")
  179. self.resize(450, 800) # Phone-like aspect ratio
  180. # Set Application Icon
  181. icon_path = "icon.png"
  182. if os.path.exists(icon_path):
  183. self.app_icon = QIcon(icon_path)
  184. else:
  185. # Fallback to generated door icon
  186. self.app_icon = self.create_door_icon()
  187. self.setWindowIcon(self.app_icon)
  188. # Also set the taskbar icon explicitely for some Windows versions if needed,
  189. # but usually setWindowIcon handles it if the window is shown.
  190. QApplication.setWindowIcon(self.app_icon)
  191. # Main Layout
  192. central_widget = QWidget()
  193. self.setCentralWidget(central_widget)
  194. main_layout = QVBoxLayout(central_widget)
  195. main_layout.setSpacing(20)
  196. main_layout.setContentsMargins(20, 20, 20, 20)
  197. # 1. Video Section (Top)
  198. # video_group = QGroupBox("实时监控")
  199. # Simplified: Just a label, maybe with a frame
  200. video_container = QWidget()
  201. video_layout = QVBoxLayout(video_container)
  202. video_layout.setContentsMargins(0,0,0,0)
  203. self.video_label = QLabel("正在连接视频流...\nConnecting...")
  204. self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
  205. self.video_label.setStyleSheet("background-color: black; color: white; font-size: 14px; border-radius: 10px;")
  206. self.video_label.setMinimumSize(400, 300)
  207. self.video_label.setMaximumHeight(350)
  208. video_layout.addWidget(self.video_label)
  209. main_layout.addWidget(video_container)
  210. # 2. Controls Section (Bottom)
  211. controls_container = QWidget()
  212. controls_layout = QVBoxLayout(controls_container)
  213. controls_layout.setSpacing(15)
  214. # Common button style
  215. btn_font = QFont()
  216. btn_font.setPointSize(16) # Bigger font
  217. btn_font.setBold(True)
  218. def create_btn(text, color):
  219. btn = QPushButton(text)
  220. btn.setFont(btn_font)
  221. btn.setMinimumHeight(60) # Taller buttons
  222. btn.setStyleSheet(f"""
  223. QPushButton {{
  224. background-color: {color};
  225. color: white;
  226. border-radius: 8px;
  227. }}
  228. QPushButton:pressed {{
  229. background-color: {color}dd; # Slightly darker on press
  230. }}
  231. """)
  232. return btn
  233. # Remote Open Button
  234. self.btn_remote_open = create_btn("远程开门 (Remote Open)", "#FF9800") # Orange
  235. self.btn_remote_open.clicked.connect(self.send_remote_open)
  236. controls_layout.addWidget(self.btn_remote_open)
  237. # Normal Mode
  238. self.btn_online = create_btn("正常模式 (Normal - 0)", "#4CAF50") # Green
  239. self.btn_online.clicked.connect(lambda: self.send_command(0))
  240. controls_layout.addWidget(self.btn_online)
  241. # Always Open
  242. self.btn_open = create_btn("常开模式 (Always Open - 1)", "#2196F3") # Blue
  243. self.btn_open.clicked.connect(lambda: self.send_command(1))
  244. controls_layout.addWidget(self.btn_open)
  245. # Always Closed
  246. self.btn_close = create_btn("常闭模式 (Always Closed - 2)", "#f44336") # Red
  247. self.btn_close.clicked.connect(lambda: self.send_command(2))
  248. controls_layout.addWidget(self.btn_close)
  249. main_layout.addWidget(controls_container)
  250. # 3. Log Section (Very bottom, smaller)
  251. self.log_output = QTextEdit()
  252. self.log_output.setReadOnly(True)
  253. self.log_output.setMaximumHeight(100) # Smaller log area
  254. self.log_output.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 5px;")
  255. main_layout.addWidget(self.log_output)
  256. # Start Video
  257. self.start_video()
  258. # --- System Tray & Floating Button ---
  259. self.tray_icon = QSystemTrayIcon(self)
  260. self.tray_icon.setIcon(self.app_icon)
  261. # Create Tray Menu
  262. tray_menu = QMenu()
  263. show_action = QAction("显示 (Show)", self)
  264. show_action.triggered.connect(self.show_window)
  265. quit_action = QAction("退出 (Quit)", self)
  266. quit_action.triggered.connect(self.quit_app)
  267. tray_menu.addAction(show_action)
  268. tray_menu.addAction(quit_action)
  269. self.tray_icon.setContextMenu(tray_menu)
  270. self.tray_icon.activated.connect(self.on_tray_icon_activated)
  271. self.tray_icon.show()
  272. # Floating Button
  273. self.floating_button = FloatingButton()
  274. self.floating_button.clicked.connect(self.show_window)
  275. # Initially hidden if main window is shown
  276. self.floating_button.hide()
  277. def create_door_icon(self):
  278. pixmap = QPixmap(64, 64)
  279. pixmap.fill(Qt.GlobalColor.transparent)
  280. painter = QPainter(pixmap)
  281. painter.setRenderHint(QPainter.RenderHint.Antialiasing)
  282. # Draw Door Frame
  283. painter.setBrush(QBrush(QColor("#4CAF50"))) # Green door
  284. painter.setPen(QPen(QColor("#388E3C"), 2))
  285. painter.drawRoundedRect(12, 4, 40, 56, 4, 4)
  286. # Draw Door Knob
  287. painter.setBrush(QBrush(QColor("#FFC107"))) # Gold knob
  288. painter.setPen(Qt.PenStyle.NoPen)
  289. painter.drawEllipse(42, 32, 6, 6)
  290. painter.end()
  291. return QIcon(pixmap)
  292. def show_window(self):
  293. self.show()
  294. self.setWindowState(Qt.WindowState.WindowNoState)
  295. self.activateWindow()
  296. self.floating_button.hide()
  297. def quit_app(self):
  298. if hasattr(self, 'video_thread'):
  299. self.video_thread.stop()
  300. QApplication.quit()
  301. def on_tray_icon_activated(self, reason):
  302. if reason == QSystemTrayIcon.ActivationReason.Trigger:
  303. if self.isVisible():
  304. self.hide()
  305. self.floating_button.show()
  306. else:
  307. self.show_window()
  308. def closeEvent(self, event):
  309. if self.tray_icon.isVisible():
  310. self.hide()
  311. self.floating_button.show()
  312. event.ignore()
  313. else:
  314. self.quit_app()
  315. def start_video(self):
  316. url = self.config.get("video_stream_url")
  317. self.log(f"Attempting to connect to stream: {url}")
  318. self.video_thread = VideoThread(url)
  319. self.video_thread.change_pixmap_signal.connect(self.update_image)
  320. self.video_thread.connection_status_signal.connect(self.update_video_status)
  321. self.video_thread.start()
  322. @pyqtSlot(QImage)
  323. def update_image(self, qt_img):
  324. self.video_label.setPixmap(QPixmap.fromImage(qt_img))
  325. self.video_label.setText("") # Clear loading text
  326. @pyqtSlot(bool)
  327. def update_video_status(self, connected):
  328. if not connected and not self.video_label.pixmap():
  329. self.video_label.setText("视频流连接失败或已断开\nStream Disconnected")
  330. def send_remote_open(self):
  331. self.log("Sending remote open command...")
  332. self.set_buttons_enabled(False)
  333. self.remote_worker = RemoteOpenWorker(
  334. self.config['door_ip'],
  335. self.config['door_port'],
  336. self.config['door_password'],
  337. self.config['door_id']
  338. )
  339. self.remote_worker.result_signal.connect(self.handle_command_result)
  340. self.remote_worker.finished.connect(lambda: self.set_buttons_enabled(True))
  341. self.remote_worker.start()
  342. def send_command(self, mode):
  343. mode_text = {0: "正常 (Normal)", 1: "常开 (Always Open)", 2: "常闭 (Always Closed)"}[mode]
  344. self.log(f"Sending command: {mode_text}...")
  345. # Disable buttons temporarily
  346. self.set_buttons_enabled(False)
  347. self.worker = DoorControlWorker(
  348. self.config['door_ip'],
  349. self.config['door_port'],
  350. self.config['door_password'],
  351. mode
  352. )
  353. self.worker.result_signal.connect(self.handle_command_result)
  354. self.worker.finished.connect(lambda: self.set_buttons_enabled(True))
  355. self.worker.start()
  356. def handle_command_result(self, message, success):
  357. if success:
  358. self.log(f"SUCCESS: {message}")
  359. # QMessageBox.information(self, "成功", f"操作成功\n{message}")
  360. else:
  361. self.log(f"ERROR: {message}")
  362. # QMessageBox.warning(self, "错误", f"操作失败\n{message}")
  363. def set_buttons_enabled(self, enabled):
  364. self.btn_remote_open.setEnabled(enabled)
  365. self.btn_online.setEnabled(enabled)
  366. self.btn_open.setEnabled(enabled)
  367. self.btn_close.setEnabled(enabled)
  368. def log(self, message):
  369. self.log_output.append(message)
  370. # Scroll to bottom
  371. cursor = self.log_output.textCursor()
  372. cursor.movePosition(cursor.MoveOperation.End)
  373. self.log_output.setTextCursor(cursor)
  374. if __name__ == "__main__":
  375. app = QApplication(sys.argv)
  376. window = MainWindow()
  377. window.show()
  378. sys.exit(app.exec())