|
|
@@ -6,6 +6,7 @@ import logging
|
|
|
import threading
|
|
|
import queue
|
|
|
import subprocess
|
|
|
+from concurrent.futures import ThreadPoolExecutor
|
|
|
from collections import defaultdict
|
|
|
from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
QPushButton, QLabel, QGridLayout, QScrollArea,
|
|
|
@@ -99,6 +100,7 @@ class MainWindow(QMainWindow):
|
|
|
self._bucket_signal_labels: dict = {} # bucket.id → QLabel
|
|
|
self._last_signal_check: float = 0.0
|
|
|
self._signal_check_thread: threading.Thread = None
|
|
|
+ self._offline_reported: set = set() # 已播报离线的 IP
|
|
|
|
|
|
# 连接跨线程信号到主线程槽
|
|
|
self.rfid_tag_signal.connect(self._on_rfid_tag_impl)
|
|
|
@@ -129,6 +131,7 @@ class MainWindow(QMainWindow):
|
|
|
self.showFullScreen()
|
|
|
self.update_video_size()
|
|
|
self.init_hardware()
|
|
|
+ self._started = True
|
|
|
|
|
|
self.timer = QTimer()
|
|
|
self.timer.timeout.connect(self.check_idle)
|
|
|
@@ -471,7 +474,9 @@ class MainWindow(QMainWindow):
|
|
|
return os.path.join(base, "assets", "voices")
|
|
|
|
|
|
def _voice_msg_to_path(self, msg: str) -> str:
|
|
|
- mapping = {"挂包中": "hooking", "挂包成功": "ready", "放包中": "releasing"}
|
|
|
+ mapping = {"挂包中": "hooking", "挂包成功": "ready", "放包中": "releasing",
|
|
|
+ "读卡器离线": "offline_reader", "左侧摄像头离线": "offline_cam_left",
|
|
|
+ "右侧摄像头离线": "offline_cam_right"}
|
|
|
if msg in mapping:
|
|
|
return os.path.join(self._voices_dir, f"{mapping[msg]}.wav")
|
|
|
m = re.search(r'到达(\d+)号工位', msg)
|
|
|
@@ -744,7 +749,10 @@ class MainWindow(QMainWindow):
|
|
|
if not m:
|
|
|
return False
|
|
|
lat = self._camera_signal.get(m.group(1))
|
|
|
- return lat is not None and lat >= 0
|
|
|
+ # 尚未检测过该 IP 时,默认认为在线,先尝试连接
|
|
|
+ if lat is None:
|
|
|
+ return True
|
|
|
+ return lat >= 0
|
|
|
def on_bucket_clicked(self, bucket: BucketModel):
|
|
|
if self.switch_mode != SwitchMode.HAND:
|
|
|
self.switch_mode = SwitchMode.HAND
|
|
|
@@ -760,6 +768,9 @@ class MainWindow(QMainWindow):
|
|
|
self.module_status.setText("通讯状态: RFID正常")
|
|
|
else:
|
|
|
self.module_status.setText("通讯状态: RFID离线")
|
|
|
+ logger.warning("读卡器离线")
|
|
|
+ if self.voice_enabled and getattr(self, '_started', False):
|
|
|
+ self._speak("读卡器离线")
|
|
|
|
|
|
# ---------- 硬件初始化 ----------
|
|
|
def init_hardware(self):
|
|
|
@@ -889,6 +900,8 @@ class MainWindow(QMainWindow):
|
|
|
player.start()
|
|
|
elif left_cam:
|
|
|
self.show_timeout_on_label(self.left_label)
|
|
|
+ if self.voice_enabled:
|
|
|
+ self._speak("左侧摄像头离线")
|
|
|
|
|
|
if right_cam and self._is_camera_online(right_cam):
|
|
|
w = self.right_label.width()
|
|
|
@@ -902,6 +915,8 @@ class MainWindow(QMainWindow):
|
|
|
player.start()
|
|
|
elif right_cam:
|
|
|
self.show_timeout_on_label(self.right_label)
|
|
|
+ if self.voice_enabled:
|
|
|
+ self._speak("右侧摄像头离线")
|
|
|
else:
|
|
|
self._unsubscribe_all_topics()
|
|
|
self.left_label.clear()
|
|
|
@@ -1242,23 +1257,19 @@ class MainWindow(QMainWindow):
|
|
|
if not ips:
|
|
|
return
|
|
|
|
|
|
- def ping_one(ip: str) -> tuple:
|
|
|
+ def check_one(ip: str) -> tuple:
|
|
|
try:
|
|
|
- if sys.platform == "win32":
|
|
|
- r = subprocess.run(["ping", "-n", "1", ip], capture_output=True, timeout=5, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
|
|
- else:
|
|
|
- r = subprocess.run(["ping", "-c", "1", ip], capture_output=True, timeout=5, text=True)
|
|
|
- if r.returncode != 0:
|
|
|
- return (ip, -1)
|
|
|
- m = re.search(r'time[=<]\s*(\d+(?:\.\d+)?)\s*ms', r.stdout, re.IGNORECASE)
|
|
|
- return (ip, float(m.group(1)) if m else 0)
|
|
|
- except subprocess.TimeoutExpired:
|
|
|
- return (ip, -1)
|
|
|
+ import socket
|
|
|
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
+ s.settimeout(2)
|
|
|
+ r = s.connect_ex((ip, 554))
|
|
|
+ s.close()
|
|
|
+ return (ip, 0 if r == 0 else -1)
|
|
|
except Exception:
|
|
|
return (ip, -1)
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=len(ips)) as pool:
|
|
|
- results = dict(pool.map(ping_one, ips))
|
|
|
+ results = dict(pool.map(check_one, ips))
|
|
|
self.camera_signal_signal.emit(results)
|
|
|
|
|
|
def _on_camera_signal(self, results: dict):
|
|
|
@@ -1266,10 +1277,33 @@ class MainWindow(QMainWindow):
|
|
|
self._camera_signal.update(results)
|
|
|
now_online = {ip for ip, lat in results.items() if lat >= 0}
|
|
|
revived = now_online & was_offline
|
|
|
+ went_offline = {ip for ip, lat in results.items() if lat < 0}
|
|
|
+ if went_offline and self.active_bucket:
|
|
|
+ for cam in self.active_bucket.cameras:
|
|
|
+ m = re.search(r'rtsp://(?:[^@]+@)?([^:/]+)', cam.rtsp_url)
|
|
|
+ if m and m.group(1) in went_offline:
|
|
|
+ ip = m.group(1)
|
|
|
+ if ip not in self._offline_reported and self.voice_enabled:
|
|
|
+ self._offline_reported.add(ip)
|
|
|
+ side_name = "左" if cam.position == CameraPosition.LEFT else "右"
|
|
|
+ self._speak(f"{side_name}侧摄像头离线")
|
|
|
+ logger.warning(f"摄像头离线: {ip}")
|
|
|
+ side = "left" if cam.position == CameraPosition.LEFT else "right"
|
|
|
+ label = self.left_label if side == "left" else self.right_label
|
|
|
+ cur = self.left_player if side == "left" else self.right_player
|
|
|
+ if cur is not None:
|
|
|
+ cur.stop()
|
|
|
+ if side == "left":
|
|
|
+ self.left_player = None
|
|
|
+ else:
|
|
|
+ self.right_player = None
|
|
|
+ self._cancel_rtsp_retry(side)
|
|
|
+ self.show_timeout_on_label(label)
|
|
|
if revived and self.active_bucket:
|
|
|
for cam in self.active_bucket.cameras:
|
|
|
m = re.search(r'rtsp://(?:[^@]+@)?([^:/]+)', cam.rtsp_url)
|
|
|
if m and m.group(1) in revived:
|
|
|
+ self._offline_reported.discard(m.group(1))
|
|
|
side = "left" if cam.position == CameraPosition.LEFT else "right"
|
|
|
cur = self.left_player if side == "left" else self.right_player
|
|
|
if cur is None:
|