|
|
@@ -3,17 +3,211 @@ import hashlib
|
|
|
import requests
|
|
|
import json
|
|
|
import time
|
|
|
+import threading
|
|
|
+import os
|
|
|
+import re
|
|
|
+import ssl
|
|
|
+import subprocess
|
|
|
from datetime import datetime, timedelta
|
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
|
|
QTableWidget, QTableWidgetItem, QHeaderView,
|
|
|
QCheckBox, QMessageBox, QGroupBox, QTextEdit,
|
|
|
- QGridLayout, QSplitter)
|
|
|
+ QGridLayout, QSplitter, QComboBox, QFileDialog)
|
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|
|
-from PyQt5.QtGui import QTextDocument
|
|
|
+import websocket
|
|
|
+import threading
|
|
|
+import time
|
|
|
+
|
|
|
+# ==================== WebSocket 客户端 ====================
|
|
|
+
|
|
|
+class WebSocketClient(QThread):
|
|
|
+ connected = pyqtSignal()
|
|
|
+ disconnected = pyqtSignal()
|
|
|
+ message_received = pyqtSignal(dict)
|
|
|
+ error = pyqtSignal(str)
|
|
|
+
|
|
|
+ def __init__(self, get_folder_callback=None):
|
|
|
+ super().__init__()
|
|
|
+ self.ws = None
|
|
|
+ self.is_running = False
|
|
|
+ self.user_id = None
|
|
|
+ self.type_str = "自动扫码"
|
|
|
+ self.is_admin = True
|
|
|
+ self.device_id = None
|
|
|
+ self.ws_lock = threading.Lock()
|
|
|
+ self.get_folder_callback = get_folder_callback # 获取文件夹路径的回调函数
|
|
|
+
|
|
|
+ def connect(self, device_id):
|
|
|
+ """连接WebSocket服务器"""
|
|
|
+ self.device_id = device_id
|
|
|
+ self.is_running = True
|
|
|
+ self.start()
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ """在子线程中运行WebSocket连接"""
|
|
|
+ try:
|
|
|
+ self._connect_ws()
|
|
|
+ except Exception as e:
|
|
|
+ self.error.emit(f"WebSocket连接错误: {str(e)}")
|
|
|
+
|
|
|
+ def _connect_ws(self):
|
|
|
+ """建立WebSocket连接并处理消息"""
|
|
|
+ while self.is_running:
|
|
|
+ try:
|
|
|
+ # 创建WebSocketApp时添加ssl_options参数禁用证书验证
|
|
|
+ self.ws = websocket.WebSocketApp(
|
|
|
+ 'wss://ws.lamp.run',
|
|
|
+ on_open=self._on_open,
|
|
|
+ on_message=self._on_message,
|
|
|
+ on_error=self._on_error,
|
|
|
+ on_close=self._on_close
|
|
|
+ )
|
|
|
+
|
|
|
+ # 运行WebSocket,禁用SSL证书验证
|
|
|
+ # 添加 sslopt={"cert_reqs": ssl.CERT_NONE} 参数
|
|
|
+ import ssl
|
|
|
+ self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
|
+
|
|
|
+ # 如果连接断开,等待3秒重连
|
|
|
+ if self.is_running:
|
|
|
+ time.sleep(3)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.error.emit(f"连接异常: {str(e)}")
|
|
|
+ if self.is_running:
|
|
|
+ time.sleep(3)
|
|
|
+
|
|
|
+ def _on_open(self, ws):
|
|
|
+ """连接打开时的回调"""
|
|
|
+ self.connected.emit()
|
|
|
+ # 发送登录消息
|
|
|
+ login_msg = {
|
|
|
+ "route": "login",
|
|
|
+ "type": self.type_str,
|
|
|
+ "admin": self.is_admin,
|
|
|
+ "id": self.device_id
|
|
|
+ }
|
|
|
+ ws.send(json.dumps(login_msg))
|
|
|
+
|
|
|
+ def _on_message(self, ws, message):
|
|
|
+ """接收消息的回调"""
|
|
|
+ try:
|
|
|
+ data = json.loads(message)
|
|
|
+
|
|
|
+ # 处理用户信息
|
|
|
+ if data.get('type') == 'userInfo':
|
|
|
+ self.user_id = data.get('value', {}).get('userID')
|
|
|
+ if self.user_id:
|
|
|
+ self.message_received.emit(data)
|
|
|
+ # 处理图片接收
|
|
|
+ elif data.get('type') == 'imgIn':
|
|
|
+ self.handle_image_received(data)
|
|
|
+ else:
|
|
|
+ self.message_received.emit(data)
|
|
|
+
|
|
|
+ except json.JSONDecodeError:
|
|
|
+ pass
|
|
|
+ except Exception as e:
|
|
|
+ print(f"处理消息异常: {e}")
|
|
|
+
|
|
|
+ def handle_image_received(self, data):
|
|
|
+ """处理接收到的图片"""
|
|
|
+ try:
|
|
|
+ import base64
|
|
|
+
|
|
|
+ # 获取图片的base64编码
|
|
|
+ img_base64 = data.get('value', '')
|
|
|
+ if not img_base64:
|
|
|
+ self.error.emit("图片数据为空")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 获取发送者ID
|
|
|
+ sender_id = data.get('id', 'unknown')
|
|
|
+
|
|
|
+ # 去除可能的base64前缀
|
|
|
+ if ',' in img_base64:
|
|
|
+ img_base64 = img_base64.split(',')[1]
|
|
|
+
|
|
|
+ # 解码base64
|
|
|
+ image_data = base64.b64decode(img_base64)
|
|
|
+
|
|
|
+ # 固定保存到C盘根目录
|
|
|
+ filepath = "C:/imgIn.jpeg"
|
|
|
+
|
|
|
+ # 保存图片
|
|
|
+ with open(filepath, 'wb') as f:
|
|
|
+ f.write(image_data)
|
|
|
+
|
|
|
+ print(f"✅ 图片已保存: {filepath}")
|
|
|
+
|
|
|
+ # 通知主窗口图片已保存
|
|
|
+ success_msg = {
|
|
|
+ 'type': 'imgSaved',
|
|
|
+ 'filepath': filepath,
|
|
|
+ 'filename': "imgIn.jpeg",
|
|
|
+ 'sender_id': sender_id
|
|
|
+ }
|
|
|
+ self.message_received.emit(success_msg)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ error_msg = f"保存图片失败: {str(e)}"
|
|
|
+ print(f"❌ {error_msg}")
|
|
|
+ self.error.emit(error_msg)
|
|
|
+
|
|
|
+ def get_folder_path(self):
|
|
|
+ """获取保存文件夹路径"""
|
|
|
+ if self.get_folder_callback:
|
|
|
+ return self.get_folder_callback()
|
|
|
+ return ""
|
|
|
+
|
|
|
+ def _on_error(self, ws, error):
|
|
|
+ """错误回调"""
|
|
|
+ self.error.emit(f"WebSocket错误: {str(error)}")
|
|
|
+
|
|
|
+ def _on_close(self, ws, close_status_code, close_msg):
|
|
|
+ """连接关闭回调"""
|
|
|
+ self.disconnected.emit()
|
|
|
+
|
|
|
+ def send(self, route, value="", send_to=None):
|
|
|
+ """发送消息,send_to 使用 userID"""
|
|
|
+ with self.ws_lock:
|
|
|
+ if not self.ws:
|
|
|
+ return False
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 检查连接状态
|
|
|
+ if self.ws.sock and self.ws.sock.connected:
|
|
|
+ msg = {
|
|
|
+ "route": route,
|
|
|
+ "type": self.type_str,
|
|
|
+ "userID": self.user_id,
|
|
|
+ "id": send_to, # send_to 是 userID
|
|
|
+ "value": value
|
|
|
+ }
|
|
|
+ self.ws.send(json.dumps(msg))
|
|
|
+ return True
|
|
|
+ else:
|
|
|
+ return False
|
|
|
+ except Exception as e:
|
|
|
+ print(f"发送消息错误: {e}")
|
|
|
+ return False
|
|
|
+
|
|
|
+ def get_list(self):
|
|
|
+ """获取在线客户端列表"""
|
|
|
+ return self.send("getList")
|
|
|
+
|
|
|
+ def stop(self):
|
|
|
+ """停止连接"""
|
|
|
+ self.is_running = False
|
|
|
+ if self.ws:
|
|
|
+ try:
|
|
|
+ self.ws.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+# ==================== 登录线程 ====================
|
|
|
|
|
|
class LoginThread(QThread):
|
|
|
- """登录线程"""
|
|
|
finished = pyqtSignal(dict)
|
|
|
error = pyqtSignal(str)
|
|
|
|
|
|
@@ -26,10 +220,8 @@ class LoginThread(QThread):
|
|
|
try:
|
|
|
md5_password = hashlib.md5(self.password.encode('utf-8')).hexdigest()
|
|
|
url = f"http://119.91.58.175:9999/gps-web/api/login.jsp?userId={self.username}&password={md5_password}&loginType=user&onlyValidate=false&plateColor=&loginWay=interface&loginLang=zh_CN"
|
|
|
-
|
|
|
response = requests.get(url, timeout=10)
|
|
|
response.encoding = 'utf-8'
|
|
|
-
|
|
|
if response.status_code == 200:
|
|
|
data = response.json()
|
|
|
if data.get('rspCode') == '1':
|
|
|
@@ -41,8 +233,9 @@ class LoginThread(QThread):
|
|
|
except Exception as e:
|
|
|
self.error.emit(f"登录异常: {str(e)}")
|
|
|
|
|
|
+# ==================== 查询车辆线程 ====================
|
|
|
+
|
|
|
class QueryThread(QThread):
|
|
|
- """查询车辆定位线程"""
|
|
|
finished = pyqtSignal(dict)
|
|
|
error = pyqtSignal(str)
|
|
|
|
|
|
@@ -53,10 +246,8 @@ class QueryThread(QThread):
|
|
|
def run(self):
|
|
|
try:
|
|
|
url = f"http://119.91.58.175:9999/gps-web/api/get_gps_r.jsp?sessionId={self.session_id}&teamId=&carIds=&simple=false"
|
|
|
-
|
|
|
response = requests.get(url, timeout=15)
|
|
|
response.encoding = 'utf-8'
|
|
|
-
|
|
|
if response.status_code == 200:
|
|
|
data = response.json()
|
|
|
if data.get('rspCode') == '1':
|
|
|
@@ -68,158 +259,541 @@ class QueryThread(QThread):
|
|
|
except Exception as e:
|
|
|
self.error.emit(f"查询异常: {str(e)}")
|
|
|
|
|
|
+# ==================== 查询区域线程 ====================
|
|
|
+
|
|
|
+class RegionQueryThread(QThread):
|
|
|
+ finished = pyqtSignal(dict)
|
|
|
+ error = pyqtSignal(str)
|
|
|
+
|
|
|
+ def __init__(self, session_id):
|
|
|
+ super().__init__()
|
|
|
+ self.session_id = session_id
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ url = f"http://119.91.58.175:9999/gps-web/api/get_region_car.jsp?sessionId={self.session_id}®ionIds="
|
|
|
+ response = requests.get(url, timeout=10)
|
|
|
+ response.encoding = 'utf-8'
|
|
|
+ if response.status_code == 200:
|
|
|
+ data = response.json()
|
|
|
+ if data.get('rspCode') == '1':
|
|
|
+ self.finished.emit(data)
|
|
|
+ else:
|
|
|
+ self.error.emit(f"查询区域失败: {data.get('msg', '未知错误')}")
|
|
|
+ else:
|
|
|
+ self.error.emit(f"请求失败: HTTP {response.status_code}")
|
|
|
+ except Exception as e:
|
|
|
+ self.error.emit(f"查询异常: {str(e)}")
|
|
|
+
|
|
|
+# ==================== 主窗口 ====================
|
|
|
+
|
|
|
class MainWindow(QMainWindow):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.session_id = None
|
|
|
self.username = None
|
|
|
self.car_data = []
|
|
|
- self.car_checked_state = {} # 存储每辆车的勾选状态
|
|
|
- self.triggered_records = {} # 存储触发记录 {car_plate: {'load': last_time, 'unload': last_time}}
|
|
|
+ self.car_checked_state = {} # 启用状态
|
|
|
+ self.car_scan_state = {} # 扫码状态(新增)
|
|
|
+ self.triggered_records = {}
|
|
|
self.is_monitoring = False
|
|
|
- self.refresh_timer = QTimer()
|
|
|
- self.refresh_timer.timeout.connect(self.auto_refresh)
|
|
|
+
|
|
|
+ # 扫码任务相关
|
|
|
+ self.scan_task_queue = [] # 待处理的车辆队列
|
|
|
+ self.is_processing_scan = False # 是否正在处理扫码任务
|
|
|
+ self.scan_timer = QTimer() # 定时器用于间隔执行
|
|
|
+ self.scan_timer.timeout.connect(self.process_next_scan)
|
|
|
+
|
|
|
+ # WebSocket相关
|
|
|
+ self.ws_client = None
|
|
|
+ self.online_clients = {} # userID -> 客户端信息
|
|
|
+
|
|
|
+ # 文件夹路径
|
|
|
+ self.folder_path = ""
|
|
|
+
|
|
|
+ # 定时器
|
|
|
+ self.refresh_timer = QTimer() # 10秒刷新车辆列表
|
|
|
+ self.refresh_timer.timeout.connect(self.handle_query)
|
|
|
+ self.region_timer = QTimer() # 10秒检测区域
|
|
|
+ self.region_timer.timeout.connect(self.check_regions)
|
|
|
+
|
|
|
+ # 区域相关
|
|
|
+ self.regions = []
|
|
|
+ self.region_cars = {}
|
|
|
+
|
|
|
self.initUI()
|
|
|
+ self.load_config()
|
|
|
|
|
|
def initUI(self):
|
|
|
self.setWindowTitle('车辆定位监控系统')
|
|
|
- self.setGeometry(100, 100, 1400, 800)
|
|
|
+ self.setGeometry(100, 100, 1500, 850)
|
|
|
|
|
|
- # 中央窗口
|
|
|
central_widget = QWidget()
|
|
|
self.setCentralWidget(central_widget)
|
|
|
main_layout = QVBoxLayout(central_widget)
|
|
|
|
|
|
- # 登录区域
|
|
|
+ # ===== 登录区域 =====
|
|
|
login_group = QGroupBox("登录")
|
|
|
login_layout = QHBoxLayout()
|
|
|
-
|
|
|
login_layout.addWidget(QLabel("账号:"))
|
|
|
self.username_input = QLineEdit()
|
|
|
- self.username_input.setPlaceholderText("请输入账号")
|
|
|
self.username_input.setText("深金川")
|
|
|
login_layout.addWidget(self.username_input)
|
|
|
-
|
|
|
login_layout.addWidget(QLabel("密码:"))
|
|
|
self.password_input = QLineEdit()
|
|
|
- self.password_input.setPlaceholderText("请输入密码")
|
|
|
self.password_input.setEchoMode(QLineEdit.Password)
|
|
|
self.password_input.setText("123456")
|
|
|
login_layout.addWidget(self.password_input)
|
|
|
-
|
|
|
self.login_btn = QPushButton("登录")
|
|
|
self.login_btn.clicked.connect(self.handle_login)
|
|
|
login_layout.addWidget(self.login_btn)
|
|
|
-
|
|
|
self.status_label = QLabel("未登录")
|
|
|
login_layout.addWidget(self.status_label)
|
|
|
-
|
|
|
login_layout.addStretch()
|
|
|
login_group.setLayout(login_layout)
|
|
|
main_layout.addWidget(login_group)
|
|
|
|
|
|
- # 地址输入区域
|
|
|
- addr_group = QGroupBox("地址监控设置")
|
|
|
- addr_layout = QGridLayout()
|
|
|
+ # ===== WebSocket控制 =====
|
|
|
+ ws_group = QGroupBox("WebSocket控制")
|
|
|
+ ws_layout = QHBoxLayout()
|
|
|
+
|
|
|
+ self.ws_status_label = QLabel("未连接")
|
|
|
+ ws_layout.addWidget(self.ws_status_label)
|
|
|
+
|
|
|
+ self.refresh_clients_btn = QPushButton("刷新客户端列表")
|
|
|
+ self.refresh_clients_btn.clicked.connect(self.refresh_client_list)
|
|
|
+ self.refresh_clients_btn.setEnabled(False)
|
|
|
+ ws_layout.addWidget(self.refresh_clients_btn)
|
|
|
+
|
|
|
+ ws_layout.addWidget(QLabel("装车客户端:"))
|
|
|
+ self.send_client_combo = QComboBox()
|
|
|
+ self.send_client_combo.setMinimumWidth(120)
|
|
|
+ ws_layout.addWidget(self.send_client_combo)
|
|
|
+
|
|
|
+ ws_layout.addWidget(QLabel("卸车客户端:"))
|
|
|
+ self.receive_client_combo = QComboBox()
|
|
|
+ self.receive_client_combo.setMinimumWidth(120)
|
|
|
+ ws_layout.addWidget(self.receive_client_combo)
|
|
|
+
|
|
|
+ # 测试按钮
|
|
|
+ self.test_send_btn = QPushButton("测试发")
|
|
|
+ self.test_send_btn.clicked.connect(self.test_send)
|
|
|
+ self.test_send_btn.setEnabled(False)
|
|
|
+ ws_layout.addWidget(self.test_send_btn)
|
|
|
+
|
|
|
+ self.test_receive_btn = QPushButton("测试收")
|
|
|
+ self.test_receive_btn.clicked.connect(self.test_receive)
|
|
|
+ self.test_receive_btn.setEnabled(False)
|
|
|
+ ws_layout.addWidget(self.test_receive_btn)
|
|
|
+
|
|
|
+ ws_layout.addStretch()
|
|
|
+ ws_group.setLayout(ws_layout)
|
|
|
+ main_layout.addWidget(ws_group)
|
|
|
+
|
|
|
+ # ===== 文件夹选择 =====
|
|
|
+ folder_group = QGroupBox("文件路径设置")
|
|
|
+ folder_layout = QHBoxLayout()
|
|
|
+
|
|
|
+ folder_layout.addWidget(QLabel("选择文件夹:"))
|
|
|
+ self.folder_path_input = QLineEdit()
|
|
|
+ self.folder_path_input.setPlaceholderText("请选择文件夹路径")
|
|
|
+ self.folder_path_input.setMinimumWidth(400)
|
|
|
+ folder_layout.addWidget(self.folder_path_input)
|
|
|
+
|
|
|
+ self.browse_folder_btn = QPushButton("浏览文件夹")
|
|
|
+ self.browse_folder_btn.clicked.connect(self.browse_folder)
|
|
|
+ folder_layout.addWidget(self.browse_folder_btn)
|
|
|
+
|
|
|
+ self.clear_folder_btn = QPushButton("清空")
|
|
|
+ self.clear_folder_btn.clicked.connect(self.clear_folder)
|
|
|
+ folder_layout.addWidget(self.clear_folder_btn)
|
|
|
|
|
|
- addr_layout.addWidget(QLabel("装车地址:"), 0, 0)
|
|
|
- self.load_addr_input = QLineEdit()
|
|
|
- self.load_addr_input.setPlaceholderText("请输入装车地址关键词")
|
|
|
- addr_layout.addWidget(self.load_addr_input, 0, 1)
|
|
|
+ folder_layout.addStretch()
|
|
|
+ folder_group.setLayout(folder_layout)
|
|
|
+ main_layout.addWidget(folder_group)
|
|
|
|
|
|
- addr_layout.addWidget(QLabel("卸车地址:"), 0, 2)
|
|
|
- self.unload_addr_input = QLineEdit()
|
|
|
- self.unload_addr_input.setPlaceholderText("请输入卸车地址关键词")
|
|
|
- addr_layout.addWidget(self.unload_addr_input, 0, 3)
|
|
|
+ # ===== 区域监控设置 =====
|
|
|
+ region_group = QGroupBox("区域监控设置")
|
|
|
+ region_layout = QGridLayout()
|
|
|
+
|
|
|
+ region_layout.addWidget(QLabel("装车区域:"), 0, 0)
|
|
|
+ self.load_region_combo = QComboBox()
|
|
|
+ self.load_region_combo.setMinimumWidth(150)
|
|
|
+ region_layout.addWidget(self.load_region_combo, 0, 1)
|
|
|
+
|
|
|
+ region_layout.addWidget(QLabel("卸车区域:"), 0, 2)
|
|
|
+ self.unload_region_combo = QComboBox()
|
|
|
+ self.unload_region_combo.setMinimumWidth(150)
|
|
|
+ region_layout.addWidget(self.unload_region_combo, 0, 3)
|
|
|
+
|
|
|
+ self.refresh_region_btn = QPushButton("刷新区域")
|
|
|
+ self.refresh_region_btn.clicked.connect(self.load_regions)
|
|
|
+ self.refresh_region_btn.setEnabled(False)
|
|
|
+ region_layout.addWidget(self.refresh_region_btn, 0, 4)
|
|
|
|
|
|
- # 控制按钮
|
|
|
self.start_btn = QPushButton("启动监控")
|
|
|
self.start_btn.clicked.connect(self.start_monitoring)
|
|
|
self.start_btn.setEnabled(False)
|
|
|
- addr_layout.addWidget(self.start_btn, 0, 4)
|
|
|
+ region_layout.addWidget(self.start_btn, 0, 5)
|
|
|
|
|
|
self.stop_btn = QPushButton("停止监控")
|
|
|
self.stop_btn.clicked.connect(self.stop_monitoring)
|
|
|
self.stop_btn.setEnabled(False)
|
|
|
- addr_layout.addWidget(self.stop_btn, 0, 5)
|
|
|
+ region_layout.addWidget(self.stop_btn, 0, 6)
|
|
|
|
|
|
- self.manual_query_btn = QPushButton("手动刷新")
|
|
|
- self.manual_query_btn.clicked.connect(self.handle_query)
|
|
|
- self.manual_query_btn.setEnabled(False)
|
|
|
- addr_layout.addWidget(self.manual_query_btn, 0, 6)
|
|
|
+ region_layout.addWidget(QLabel("检测间隔:10秒"), 0, 7)
|
|
|
|
|
|
- addr_layout.addWidget(QLabel("刷新间隔:5秒"), 0, 7)
|
|
|
+ # 区域状态显示条
|
|
|
+ self.region_status_label = QLabel("请选择区域")
|
|
|
+ self.region_status_label.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }")
|
|
|
+ region_layout.addWidget(self.region_status_label, 1, 0, 1, 8)
|
|
|
|
|
|
- addr_group.setLayout(addr_layout)
|
|
|
- main_layout.addWidget(addr_group)
|
|
|
+ region_group.setLayout(region_layout)
|
|
|
+ main_layout.addWidget(region_group)
|
|
|
|
|
|
- # 分割器:表格和日志
|
|
|
+ # ===== 车辆列表 =====
|
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
|
|
|
- # 车辆信息表格
|
|
|
table_widget = QWidget()
|
|
|
table_layout = QVBoxLayout(table_widget)
|
|
|
table_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
self.table = QTableWidget()
|
|
|
- self.table.setColumnCount(5)
|
|
|
- self.table.setHorizontalHeaderLabels(["选择", "车队名", "车牌号", "在线状态", "定位地址"])
|
|
|
- self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
|
|
|
+ # 修改列数:增加"扫码"列和"操作"列
|
|
|
+ self.table.setColumnCount(7)
|
|
|
+ self.table.setHorizontalHeaderLabels(["启用", "扫码", "车队名", "车牌号", "状态", "定位地址", "操作"])
|
|
|
+ self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch)
|
|
|
self.table.setAlternatingRowColors(True)
|
|
|
table_layout.addWidget(self.table)
|
|
|
|
|
|
- # 表格按钮
|
|
|
- table_btn_layout = QHBoxLayout()
|
|
|
- self.select_all_btn = QPushButton("全选")
|
|
|
- self.select_all_btn.clicked.connect(self.select_all)
|
|
|
- table_btn_layout.addWidget(self.select_all_btn)
|
|
|
+ btn_layout = QHBoxLayout()
|
|
|
+ self.select_all_btn = QPushButton("全选启用")
|
|
|
+ self.select_all_btn.clicked.connect(self.select_all_enable)
|
|
|
+ btn_layout.addWidget(self.select_all_btn)
|
|
|
+ self.deselect_all_btn = QPushButton("取消全选启用")
|
|
|
+ self.deselect_all_btn.clicked.connect(self.deselect_all_enable)
|
|
|
+ btn_layout.addWidget(self.deselect_all_btn)
|
|
|
|
|
|
- self.deselect_all_btn = QPushButton("取消全选")
|
|
|
- self.deselect_all_btn.clicked.connect(self.deselect_all)
|
|
|
- table_btn_layout.addWidget(self.deselect_all_btn)
|
|
|
+ self.select_all_scan_btn = QPushButton("全选扫码")
|
|
|
+ self.select_all_scan_btn.clicked.connect(self.select_all_scan)
|
|
|
+ btn_layout.addWidget(self.select_all_scan_btn)
|
|
|
+ self.deselect_all_scan_btn = QPushButton("取消全选扫码")
|
|
|
+ self.deselect_all_scan_btn.clicked.connect(self.deselect_all_scan)
|
|
|
+ btn_layout.addWidget(self.deselect_all_scan_btn)
|
|
|
|
|
|
- table_btn_layout.addStretch()
|
|
|
- table_layout.addLayout(table_btn_layout)
|
|
|
+ btn_layout.addStretch()
|
|
|
+ table_layout.addLayout(btn_layout)
|
|
|
|
|
|
splitter.addWidget(table_widget)
|
|
|
|
|
|
- # 日志输出框
|
|
|
+ # ===== 日志 =====
|
|
|
log_widget = QWidget()
|
|
|
log_layout = QVBoxLayout(log_widget)
|
|
|
log_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
-
|
|
|
- log_label = QLabel("日志输出")
|
|
|
- log_layout.addWidget(log_label)
|
|
|
-
|
|
|
+ log_layout.addWidget(QLabel("日志"))
|
|
|
self.log_output = QTextEdit()
|
|
|
self.log_output.setReadOnly(True)
|
|
|
- # 限制日志行数 - 使用document().setMaximumBlockCount()
|
|
|
- self.log_output.document().setMaximumBlockCount(1000)
|
|
|
+ self.log_output.document().setMaximumBlockCount(500)
|
|
|
log_layout.addWidget(self.log_output)
|
|
|
|
|
|
splitter.addWidget(log_widget)
|
|
|
- splitter.setSizes([500, 300])
|
|
|
-
|
|
|
+ splitter.setSizes([450, 300])
|
|
|
main_layout.addWidget(splitter)
|
|
|
|
|
|
- # 状态栏
|
|
|
self.statusBar().showMessage("就绪")
|
|
|
-
|
|
|
- # 初始化触发记录
|
|
|
self.triggered_records = {}
|
|
|
|
|
|
+ def get_value_with_plate(self, car_plate):
|
|
|
+ """生成带车牌的文件路径: 文件夹路径/车牌.jpg"""
|
|
|
+ folder = self.folder_path_input.text().strip()
|
|
|
+ if not folder:
|
|
|
+ return ""
|
|
|
+ # 确保路径格式正确
|
|
|
+ folder = folder.rstrip('/\\')
|
|
|
+ return f"{folder}/{car_plate}.jpg"
|
|
|
+
|
|
|
+ def get_exe_path_with_plate(self, car_plate):
|
|
|
+ """生成可执行文件路径: 文件夹路径/车牌号/运行程序.exe"""
|
|
|
+ folder = self.folder_path_input.text().strip()
|
|
|
+ if not folder:
|
|
|
+ return ""
|
|
|
+ # 确保路径格式正确
|
|
|
+ folder = folder.rstrip('/\\')
|
|
|
+ return f"{folder}/{car_plate}/运行程序.exe"
|
|
|
+
|
|
|
+ # ===== 文件夹选择 =====
|
|
|
+ def browse_folder(self):
|
|
|
+ """浏览并选择文件夹"""
|
|
|
+ folder_path = QFileDialog.getExistingDirectory(
|
|
|
+ self,
|
|
|
+ "选择文件夹",
|
|
|
+ self.folder_path_input.text() if self.folder_path_input.text() else "",
|
|
|
+ QFileDialog.ShowDirsOnly
|
|
|
+ )
|
|
|
+
|
|
|
+ if folder_path:
|
|
|
+ self.folder_path = folder_path
|
|
|
+ self.folder_path_input.setText(folder_path)
|
|
|
+ self.add_log(f"📁 已选择文件夹: {folder_path}")
|
|
|
+ self.save_config()
|
|
|
+
|
|
|
+ def clear_folder(self):
|
|
|
+ """清空文件夹路径"""
|
|
|
+ self.folder_path = ""
|
|
|
+ self.folder_path_input.clear()
|
|
|
+ self.add_log("已清空文件夹路径")
|
|
|
+ self.save_config()
|
|
|
+
|
|
|
+ # ===== WebSocket连接 =====
|
|
|
+ def connect_websocket(self, device_id):
|
|
|
+ """连接WebSocket服务器"""
|
|
|
+ if self.ws_client and self.ws_client.is_running:
|
|
|
+ return
|
|
|
+
|
|
|
+ self.ws_status_label.setText("连接中...")
|
|
|
+ self.ws_status_label.setStyleSheet("QLabel { color: orange; }")
|
|
|
+
|
|
|
+ # 创建WebSocket客户端,传入获取文件夹路径的回调
|
|
|
+ self.ws_client = WebSocketClient(get_folder_callback=self.get_folder_path_for_ws)
|
|
|
+ self.ws_client.connected.connect(self.on_ws_connected)
|
|
|
+ self.ws_client.disconnected.connect(self.on_ws_disconnected)
|
|
|
+ self.ws_client.message_received.connect(self.on_ws_message)
|
|
|
+ self.ws_client.error.connect(self.on_ws_error)
|
|
|
+ self.ws_client.connect(device_id)
|
|
|
+
|
|
|
+ def get_folder_path_for_ws(self):
|
|
|
+ """提供给WebSocket客户端的文件夹路径获取方法"""
|
|
|
+ return self.folder_path_input.text().strip()
|
|
|
+
|
|
|
+ def on_ws_connected(self):
|
|
|
+ """WebSocket连接成功"""
|
|
|
+ self.refresh_clients_btn.setEnabled(True)
|
|
|
+ self.test_send_btn.setEnabled(True)
|
|
|
+ self.test_receive_btn.setEnabled(True)
|
|
|
+ self.ws_status_label.setText("已连接")
|
|
|
+ self.ws_status_label.setStyleSheet("QLabel { color: green; }")
|
|
|
+ self.add_log("✅ WebSocket连接成功")
|
|
|
+
|
|
|
+ # 连接成功后获取客户端列表
|
|
|
+ QTimer.singleShot(1000, self.refresh_client_list)
|
|
|
+
|
|
|
+ def on_ws_disconnected(self):
|
|
|
+ """WebSocket连接断开"""
|
|
|
+ self.refresh_clients_btn.setEnabled(False)
|
|
|
+ self.test_send_btn.setEnabled(False)
|
|
|
+ self.test_receive_btn.setEnabled(False)
|
|
|
+ self.ws_status_label.setText("已断开")
|
|
|
+ self.ws_status_label.setStyleSheet("QLabel { color: red; }")
|
|
|
+ self.add_log("⚠️ WebSocket连接断开")
|
|
|
+
|
|
|
+ def on_ws_error(self, error_msg):
|
|
|
+ """WebSocket错误"""
|
|
|
+ self.add_log(f"❌ WebSocket错误: {error_msg}")
|
|
|
+
|
|
|
+ def on_ws_message(self, data):
|
|
|
+ """处理WebSocket消息"""
|
|
|
+ msg_type = data.get('type')
|
|
|
+ self.add_log(json.dumps(data, ensure_ascii=False))
|
|
|
+ if msg_type == 'getList':
|
|
|
+ # 客户端列表
|
|
|
+ clients = data.get('value', [])
|
|
|
+ self.on_client_list_received(clients)
|
|
|
+ elif msg_type == 'userInfo':
|
|
|
+ self.add_log(f"✅ 用户信息已获取: {data.get('value', {}).get('userID')}")
|
|
|
+ elif msg_type == 'imgSaved':
|
|
|
+ # 图片保存成功,触发扫码任务
|
|
|
+ self.add_log(f"📸 图片已保存: {data.get('filepath', '')}")
|
|
|
+ self.start_scan_tasks()
|
|
|
+ elif msg_type == 'drive': # 收到"开车"命令
|
|
|
+ # 外部WebSocket发来的开车命令
|
|
|
+ car_plate = data.get('value', '')
|
|
|
+ if car_plate:
|
|
|
+ self.add_log(f"📤 收到外部WebSocket开车命令,车辆: {car_plate}")
|
|
|
+ # 发送2001指令
|
|
|
+ self.send_cmd_to_vehicle(car_plate, "2001")
|
|
|
+ # 继续处理下一辆车
|
|
|
+ self.process_next_scan()
|
|
|
+ else:
|
|
|
+ self.add_log(f"⚠️ 收到开车命令但车牌为空")
|
|
|
+
|
|
|
+
|
|
|
+ def start_scan_tasks(self):
|
|
|
+ """开始执行扫码任务"""
|
|
|
+ # 如果正在处理,忽略新的请求
|
|
|
+ if self.is_processing_scan:
|
|
|
+ self.add_log("⏳ 扫码任务正在处理中,忽略本次请求")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 获取所有勾选了扫码的车辆
|
|
|
+ scan_vehicles = []
|
|
|
+ for row in range(self.table.rowCount()):
|
|
|
+ scan_checkbox = self.table.cellWidget(row, 1)
|
|
|
+ if scan_checkbox and scan_checkbox.isChecked():
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
+ if car_plate:
|
|
|
+ scan_vehicles.append(car_plate)
|
|
|
+
|
|
|
+ if not scan_vehicles:
|
|
|
+ self.add_log("⚠️ 没有勾选扫码的车辆")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 初始化队列
|
|
|
+ self.scan_task_queue = scan_vehicles.copy()
|
|
|
+ self.is_processing_scan = True
|
|
|
+
|
|
|
+ self.add_log(f"🚀 开始扫码任务,共 {len(scan_vehicles)} 辆车")
|
|
|
+ self.add_log(f"📋 车辆列表: {', '.join(scan_vehicles)}")
|
|
|
+
|
|
|
+ # 立即处理第一个
|
|
|
+ self.process_next_scan()
|
|
|
+
|
|
|
+ def process_next_scan(self):
|
|
|
+ """处理下一个扫码任务"""
|
|
|
+ # 如果监控已停止,不再处理
|
|
|
+ if not self.is_monitoring:
|
|
|
+ self.add_log("⏹️ 监控已停止,取消扫码任务")
|
|
|
+ self.is_processing_scan = False
|
|
|
+ self.scan_task_queue = []
|
|
|
+ self.current_scan_plate = None
|
|
|
+ self.scan_timer.stop()
|
|
|
+ return
|
|
|
+
|
|
|
+ if not self.scan_task_queue:
|
|
|
+ # 所有任务完成
|
|
|
+ self.is_processing_scan = False
|
|
|
+ self.add_log("✅ 所有扫码任务已完成")
|
|
|
+ self.statusBar().showMessage("扫码任务全部完成")
|
|
|
+ self.scan_timer.stop()
|
|
|
+ self.current_scan_plate = None
|
|
|
+ return
|
|
|
+
|
|
|
+ # 取出第一个车辆
|
|
|
+ car_plate = self.scan_task_queue.pop(0)
|
|
|
+ self.current_scan_plate = car_plate
|
|
|
+ self.add_log(f"🔄 正在处理扫码车辆: {car_plate} (剩余 {len(self.scan_task_queue)} 辆)")
|
|
|
+
|
|
|
+ # 执行测试发车(标记为来自扫码任务)
|
|
|
+ self.on_test_drive(car_plate, from_scan=True)
|
|
|
+
|
|
|
+ # 启动定时器,1分钟后发送指令2001,然后处理下一辆
|
|
|
+ # 使用 QTimer.singleShot 单次定时器,传入当前车牌
|
|
|
+ self.add_log(f"⏳ 等待外部WebSocket通知发送2001指令到: {car_plate}")
|
|
|
+ # QTimer.singleShot(60000, lambda: self.send_2001_and_next(car_plate))
|
|
|
+
|
|
|
+ def send_2001_and_next(self, car_plate):
|
|
|
+ """发送2001指令给指定车辆,然后继续处理下一个"""
|
|
|
+ self.add_log(f"📤 发送指令 2001 到车辆: {car_plate}")
|
|
|
+ self.send_cmd_to_vehicle(car_plate, "2001")
|
|
|
+
|
|
|
+ # 发送完指令后,继续处理下一辆(立即执行,不再等待)
|
|
|
+ self.process_next_scan()
|
|
|
+
|
|
|
+ def check_scan_complete(self):
|
|
|
+ """检查扫码任务是否完成"""
|
|
|
+ if not self.scan_task_queue and self.is_processing_scan:
|
|
|
+ self.is_processing_scan = False
|
|
|
+ self.add_log("✅ 所有扫码任务已完成")
|
|
|
+ self.statusBar().showMessage("扫码任务全部完成")
|
|
|
+
|
|
|
+ def on_client_list_received(self, clients):
|
|
|
+ """处理客户端列表,过滤掉 admin 为 true 的客户端"""
|
|
|
+ self.online_clients = {}
|
|
|
+ self.send_client_combo.clear()
|
|
|
+ self.receive_client_combo.clear()
|
|
|
+
|
|
|
+ # 添加默认选项
|
|
|
+ self.send_client_combo.addItem("请选择装车客户端")
|
|
|
+ self.receive_client_combo.addItem("请选择卸车客户端")
|
|
|
+
|
|
|
+ for client in clients:
|
|
|
+ # 过滤掉 admin 为 true 的客户端(这些是管理端,不是扫码客户端)
|
|
|
+ if client.get('admin', False):
|
|
|
+ continue
|
|
|
+
|
|
|
+ client_id = client.get('id')
|
|
|
+ user_id = client.get('userID')
|
|
|
+ is_active = client.get('active', False)
|
|
|
+
|
|
|
+ if client_id and user_id and is_active:
|
|
|
+ # 使用 userID 作为键存储客户端信息
|
|
|
+ self.online_clients[user_id] = client
|
|
|
+ # 显示时显示 id,但存储 userID
|
|
|
+ display_name = f"{client_id}"
|
|
|
+ self.send_client_combo.addItem(display_name, user_id)
|
|
|
+ self.receive_client_combo.addItem(display_name, user_id)
|
|
|
+ self.add_log(f"发现客户端: {client_id} (userID: {user_id})")
|
|
|
+
|
|
|
+ self.add_log(f"✅ 已加载 {len(self.online_clients)} 个在线扫码客户端")
|
|
|
+
|
|
|
+ def refresh_client_list(self):
|
|
|
+ """刷新客户端列表"""
|
|
|
+ if not self.ws_client:
|
|
|
+ return
|
|
|
+
|
|
|
+ if not self.ws_client.is_running:
|
|
|
+ self.add_log("❌ WebSocket未连接")
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.ws_client.send("getList"):
|
|
|
+ self.add_log("📡 正在获取客户端列表...")
|
|
|
+ else:
|
|
|
+ self.add_log("❌ 发送获取列表命令失败")
|
|
|
+
|
|
|
+ def test_send(self):
|
|
|
+ """测试发送(装车)"""
|
|
|
+ if not self.ws_client or not self.ws_client.is_running:
|
|
|
+ self.add_log("❌ WebSocket未连接")
|
|
|
+ return
|
|
|
+
|
|
|
+ user_id = self.send_client_combo.currentData()
|
|
|
+ if not user_id:
|
|
|
+ self.add_log("❌ 请选择装车客户端")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 测试时使用一个示例车牌
|
|
|
+ test_plate = "测试车牌"
|
|
|
+ value = self.get_value_with_plate(test_plate)
|
|
|
+
|
|
|
+ if not value:
|
|
|
+ self.add_log("⚠️ 未选择文件夹,将发送空值")
|
|
|
+
|
|
|
+ if self.ws_client.send("发货", value, user_id):
|
|
|
+ self.add_log(f"✅ 测试发送(装车)指令已发送到 {user_id},value: {value}")
|
|
|
+ else:
|
|
|
+ self.add_log("❌ 测试发送失败")
|
|
|
+
|
|
|
+ def test_receive(self):
|
|
|
+ """测试接收(卸车)"""
|
|
|
+ if not self.ws_client or not self.ws_client.is_running:
|
|
|
+ self.add_log("❌ WebSocket未连接")
|
|
|
+ return
|
|
|
+
|
|
|
+ user_id = self.receive_client_combo.currentData()
|
|
|
+ if not user_id:
|
|
|
+ self.add_log("❌ 请选择卸车客户端")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 测试时使用一个示例车牌
|
|
|
+ test_plate = "测试车牌"
|
|
|
+ value = self.get_value_with_plate(test_plate)
|
|
|
+
|
|
|
+ if not value:
|
|
|
+ self.add_log("⚠️ 未选择文件夹,将发送空值")
|
|
|
+
|
|
|
+ if self.ws_client.send("收货", value, user_id):
|
|
|
+ self.add_log(f"✅ 测试接收(卸车)指令已发送到 {user_id},value: {value}")
|
|
|
+ else:
|
|
|
+ self.add_log("❌ 测试接收失败")
|
|
|
+
|
|
|
+ # ===== 登录 =====
|
|
|
def handle_login(self):
|
|
|
username = self.username_input.text().strip()
|
|
|
password = self.password_input.text().strip()
|
|
|
-
|
|
|
if not username or not password:
|
|
|
QMessageBox.warning(self, "警告", "请输入账号和密码")
|
|
|
return
|
|
|
-
|
|
|
self.login_btn.setEnabled(False)
|
|
|
self.status_label.setText("登录中...")
|
|
|
- self.statusBar().showMessage("正在登录...")
|
|
|
-
|
|
|
self.login_thread = LoginThread(username, password)
|
|
|
self.login_thread.finished.connect(self.on_login_success)
|
|
|
self.login_thread.error.connect(self.on_login_error)
|
|
|
@@ -227,33 +801,88 @@ class MainWindow(QMainWindow):
|
|
|
|
|
|
def on_login_success(self, data):
|
|
|
self.session_id = data.get('sessionId')
|
|
|
- self.username = data.get('userName', '')
|
|
|
-
|
|
|
+ self.username = self.username_input.text().strip()
|
|
|
if self.session_id:
|
|
|
self.login_btn.setEnabled(True)
|
|
|
self.status_label.setText(f"已登录: {self.username}")
|
|
|
- self.manual_query_btn.setEnabled(True)
|
|
|
+ self.refresh_region_btn.setEnabled(True)
|
|
|
self.start_btn.setEnabled(True)
|
|
|
- self.statusBar().showMessage(f"登录成功,Session ID: {self.session_id[:20]}...")
|
|
|
-
|
|
|
- # 登录成功后自动查询
|
|
|
+ self.statusBar().showMessage("登录成功")
|
|
|
+ self.load_regions()
|
|
|
self.handle_query()
|
|
|
+
|
|
|
+ # 自动连接WebSocket,使用用户名作为ID
|
|
|
+ self.connect_websocket(self.username)
|
|
|
+ self.save_config()
|
|
|
else:
|
|
|
self.on_login_error("登录响应中没有sessionId")
|
|
|
|
|
|
def on_login_error(self, error_msg):
|
|
|
self.login_btn.setEnabled(True)
|
|
|
self.status_label.setText("登录失败")
|
|
|
- self.statusBar().showMessage("登录失败")
|
|
|
QMessageBox.critical(self, "错误", error_msg)
|
|
|
|
|
|
- def handle_query(self):
|
|
|
+ # ===== 加载区域 =====
|
|
|
+ def load_regions(self):
|
|
|
if not self.session_id:
|
|
|
QMessageBox.warning(self, "警告", "请先登录")
|
|
|
return
|
|
|
|
|
|
- self.manual_query_btn.setEnabled(False)
|
|
|
- self.statusBar().showMessage("正在查询车辆定位...")
|
|
|
+ self.refresh_region_btn.setEnabled(False)
|
|
|
+ self.statusBar().showMessage("正在加载区域...")
|
|
|
+
|
|
|
+ self.region_thread = RegionQueryThread(self.session_id)
|
|
|
+ self.region_thread.finished.connect(self.on_regions_loaded)
|
|
|
+ self.region_thread.error.connect(self.on_regions_error)
|
|
|
+ self.region_thread.start()
|
|
|
+
|
|
|
+ def on_regions_loaded(self, data):
|
|
|
+ self.refresh_region_btn.setEnabled(True)
|
|
|
+ self.regions = data.get('list', [])
|
|
|
+
|
|
|
+ self.load_region_combo.clear()
|
|
|
+ self.unload_region_combo.clear()
|
|
|
+ self.load_region_combo.addItem("请选择装车区域")
|
|
|
+ self.unload_region_combo.addItem("请选择卸车区域")
|
|
|
+
|
|
|
+ for region in self.regions:
|
|
|
+ region_name = region.get('regionName', '未知')
|
|
|
+ region_id = region.get('regionId', '')
|
|
|
+ display = f"{region_name}"
|
|
|
+ self.load_region_combo.addItem(display, region_id)
|
|
|
+ self.unload_region_combo.addItem(display, region_id)
|
|
|
+ self.region_cars[region_id] = region.get('carList', [])
|
|
|
+
|
|
|
+ self.update_region_status()
|
|
|
+ self.statusBar().showMessage(f"加载了 {len(self.regions)} 个区域")
|
|
|
+ self.add_log(f"✅ 加载了 {len(self.regions)} 个区域")
|
|
|
+
|
|
|
+ def on_regions_error(self, error_msg):
|
|
|
+ self.refresh_region_btn.setEnabled(True)
|
|
|
+ QMessageBox.critical(self, "错误", error_msg)
|
|
|
+
|
|
|
+ def update_region_status(self):
|
|
|
+ """更新区域状态显示条"""
|
|
|
+ load_text = self.load_region_combo.currentText() if self.load_region_combo.currentIndex() > 0 else "未选择"
|
|
|
+ unload_text = self.unload_region_combo.currentText() if self.unload_region_combo.currentIndex() > 0 else "未选择"
|
|
|
+
|
|
|
+ # 获取车辆数量
|
|
|
+ load_cars = 0
|
|
|
+ unload_cars = 0
|
|
|
+ if self.load_region_combo.currentIndex() > 0:
|
|
|
+ region_id = self.load_region_combo.currentData()
|
|
|
+ load_cars = len(self.region_cars.get(region_id, []))
|
|
|
+ if self.unload_region_combo.currentIndex() > 0:
|
|
|
+ region_id = self.unload_region_combo.currentData()
|
|
|
+ unload_cars = len(self.region_cars.get(region_id, []))
|
|
|
+
|
|
|
+ status = f"🚛 装车: {load_text} ({load_cars}辆) | 📦 卸车: {unload_text} ({unload_cars}辆)"
|
|
|
+ self.region_status_label.setText(status)
|
|
|
+
|
|
|
+ # ===== 查询车辆列表 =====
|
|
|
+ def handle_query(self):
|
|
|
+ if not self.session_id:
|
|
|
+ return
|
|
|
|
|
|
self.query_thread = QueryThread(self.session_id)
|
|
|
self.query_thread.finished.connect(self.on_query_success)
|
|
|
@@ -261,174 +890,445 @@ class MainWindow(QMainWindow):
|
|
|
self.query_thread.start()
|
|
|
|
|
|
def on_query_success(self, data):
|
|
|
- self.manual_query_btn.setEnabled(True)
|
|
|
-
|
|
|
car_list = data.get('list', [])
|
|
|
self.car_data = car_list
|
|
|
-
|
|
|
- # 保存当前勾选状态
|
|
|
self.save_check_state()
|
|
|
-
|
|
|
self.table.setRowCount(len(car_list))
|
|
|
|
|
|
for row, car in enumerate(car_list):
|
|
|
car_plate = car.get('carPlate', '')
|
|
|
|
|
|
- # 选择框 - 恢复保存的状态
|
|
|
- checkbox = QCheckBox()
|
|
|
- checkbox.setChecked(self.car_checked_state.get(car_plate, False))
|
|
|
- checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_check_changed(plate, state))
|
|
|
- self.table.setCellWidget(row, 0, checkbox)
|
|
|
+ # 第0列:启用复选框
|
|
|
+ enable_checkbox = QCheckBox()
|
|
|
+ enable_checkbox.setChecked(self.car_checked_state.get(car_plate, False))
|
|
|
+ enable_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_enable_changed(plate, state))
|
|
|
+ self.table.setCellWidget(row, 0, enable_checkbox)
|
|
|
|
|
|
- # 车队名
|
|
|
- team_name = car.get('teamName', '')
|
|
|
- self.table.setItem(row, 1, QTableWidgetItem(team_name))
|
|
|
+ # 第1列:扫码复选框
|
|
|
+ scan_checkbox = QCheckBox()
|
|
|
+ scan_checkbox.setChecked(self.car_scan_state.get(car_plate, False))
|
|
|
+ scan_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_scan_changed(plate, state))
|
|
|
+ self.table.setCellWidget(row, 1, scan_checkbox)
|
|
|
|
|
|
- # 车牌号
|
|
|
- self.table.setItem(row, 2, QTableWidgetItem(car_plate))
|
|
|
+ # 第2列:车队名
|
|
|
+ self.table.setItem(row, 2, QTableWidgetItem(car.get('teamName', '')))
|
|
|
|
|
|
- # 在线状态
|
|
|
- state_cn = car.get('stateCn', '')
|
|
|
- self.table.setItem(row, 3, QTableWidgetItem(state_cn))
|
|
|
+ # 第3列:车牌号
|
|
|
+ self.table.setItem(row, 3, QTableWidgetItem(car_plate))
|
|
|
|
|
|
- # 定位地址
|
|
|
- addr = car.get('addr', '')
|
|
|
- self.table.setItem(row, 4, QTableWidgetItem(addr))
|
|
|
-
|
|
|
- self.statusBar().showMessage(f"查询成功,共 {len(car_list)} 辆车")
|
|
|
+ # 第4列:状态
|
|
|
+ self.table.setItem(row, 4, QTableWidgetItem(car.get('stateCn', '')))
|
|
|
+
|
|
|
+ # 第5列:定位地址
|
|
|
+ self.table.setItem(row, 5, QTableWidgetItem(car.get('addr', '')))
|
|
|
+
|
|
|
+ # 第6列:操作按钮(测试发车)
|
|
|
+ test_btn = QPushButton("测试发车")
|
|
|
+ test_btn.clicked.connect(lambda checked, plate=car_plate: self.on_test_drive(plate))
|
|
|
+ self.table.setCellWidget(row, 6, test_btn)
|
|
|
|
|
|
- # 如果正在监控,进行地址匹配检查
|
|
|
- if self.is_monitoring:
|
|
|
- self.check_address_match()
|
|
|
+ self.statusBar().showMessage(f"车辆列表更新: {len(car_list)} 辆车")
|
|
|
|
|
|
def on_query_error(self, error_msg):
|
|
|
- self.manual_query_btn.setEnabled(True)
|
|
|
- self.statusBar().showMessage("查询失败")
|
|
|
QMessageBox.critical(self, "错误", error_msg)
|
|
|
|
|
|
+ # ===== 表格操作 =====
|
|
|
def save_check_state(self):
|
|
|
- """保存当前表格中的勾选状态"""
|
|
|
+ """保存启用和扫码状态"""
|
|
|
for row in range(self.table.rowCount()):
|
|
|
- checkbox = self.table.cellWidget(row, 0)
|
|
|
- if checkbox:
|
|
|
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
|
|
|
+ # 启用复选框
|
|
|
+ enable_checkbox = self.table.cellWidget(row, 0)
|
|
|
+ if enable_checkbox:
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
if car_plate:
|
|
|
- self.car_checked_state[car_plate] = checkbox.isChecked()
|
|
|
+ self.car_checked_state[car_plate] = enable_checkbox.isChecked()
|
|
|
+
|
|
|
+ # 扫码复选框
|
|
|
+ scan_checkbox = self.table.cellWidget(row, 1)
|
|
|
+ if scan_checkbox:
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
+ if car_plate:
|
|
|
+ self.car_scan_state[car_plate] = scan_checkbox.isChecked()
|
|
|
|
|
|
- def on_check_changed(self, car_plate, state):
|
|
|
- """勾选框状态变化时保存"""
|
|
|
+ def on_enable_changed(self, car_plate, state):
|
|
|
+ """启用复选框状态变化"""
|
|
|
self.car_checked_state[car_plate] = (state == Qt.Checked)
|
|
|
|
|
|
- def select_all(self):
|
|
|
+ def on_scan_changed(self, car_plate, state):
|
|
|
+ """扫码复选框状态变化"""
|
|
|
+ self.car_scan_state[car_plate] = (state == Qt.Checked)
|
|
|
+
|
|
|
+ def select_all_enable(self):
|
|
|
+ """全选启用"""
|
|
|
for row in range(self.table.rowCount()):
|
|
|
checkbox = self.table.cellWidget(row, 0)
|
|
|
if checkbox:
|
|
|
checkbox.setChecked(True)
|
|
|
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
if car_plate:
|
|
|
self.car_checked_state[car_plate] = True
|
|
|
|
|
|
- def deselect_all(self):
|
|
|
+ def deselect_all_enable(self):
|
|
|
+ """取消全选启用"""
|
|
|
for row in range(self.table.rowCount()):
|
|
|
checkbox = self.table.cellWidget(row, 0)
|
|
|
if checkbox:
|
|
|
checkbox.setChecked(False)
|
|
|
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
if car_plate:
|
|
|
self.car_checked_state[car_plate] = False
|
|
|
|
|
|
+ def select_all_scan(self):
|
|
|
+ """全选扫码"""
|
|
|
+ for row in range(self.table.rowCount()):
|
|
|
+ checkbox = self.table.cellWidget(row, 1)
|
|
|
+ if checkbox:
|
|
|
+ checkbox.setChecked(True)
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
+ if car_plate:
|
|
|
+ self.car_scan_state[car_plate] = True
|
|
|
+
|
|
|
+ def deselect_all_scan(self):
|
|
|
+ """取消全选扫码"""
|
|
|
+ for row in range(self.table.rowCount()):
|
|
|
+ checkbox = self.table.cellWidget(row, 1)
|
|
|
+ if checkbox:
|
|
|
+ checkbox.setChecked(False)
|
|
|
+ car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
|
|
|
+ if car_plate:
|
|
|
+ self.car_scan_state[car_plate] = False
|
|
|
+
|
|
|
+ # ===== 测试发车功能 =====
|
|
|
+ def on_test_drive(self, car_plate, from_scan=False):
|
|
|
+ """测试发车按钮点击事件"""
|
|
|
+ # 如果是扫码任务且监控已停止,不再执行
|
|
|
+ if from_scan and not self.is_monitoring:
|
|
|
+ self.add_log(f"⏹️ 监控已停止,取消测试发车: {car_plate}")
|
|
|
+ return
|
|
|
+ if not car_plate:
|
|
|
+ if not from_scan:
|
|
|
+ self.add_log("❌ 车牌号为空,无法执行测试发车")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 获取可执行文件路径
|
|
|
+ exe_path = self.get_exe_path_with_plate(car_plate)
|
|
|
+
|
|
|
+ if not exe_path:
|
|
|
+ if not from_scan:
|
|
|
+ self.add_log(f"❌ 未选择文件夹,无法执行测试发车: {car_plate}")
|
|
|
+ QMessageBox.warning(self, "警告", "请先选择文件夹路径")
|
|
|
+ else:
|
|
|
+ self.add_log(f"❌ 未选择文件夹,跳过: {car_plate}")
|
|
|
+ # 继续处理下一个
|
|
|
+ self.scan_timer.start(1000)
|
|
|
+ return
|
|
|
+
|
|
|
+ # 检查文件是否存在
|
|
|
+ if not os.path.exists(exe_path):
|
|
|
+ if not from_scan:
|
|
|
+ self.add_log(f"❌ 可执行文件不存在: {exe_path}")
|
|
|
+ QMessageBox.warning(self, "错误", f"可执行文件不存在:\n{exe_path}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"❌ 可执行文件不存在,跳过: {car_plate}")
|
|
|
+ # 继续处理下一个
|
|
|
+ self.scan_timer.start(1000)
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 获取exe所在目录作为工作目录
|
|
|
+ exe_dir = os.path.dirname(exe_path)
|
|
|
+
|
|
|
+ if from_scan:
|
|
|
+ self.add_log(f"🚀 [扫码任务] 执行测试发车: {car_plate}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"🚀 执行测试发车: {car_plate}")
|
|
|
+ self.add_log(f"📂 执行路径: {exe_path}")
|
|
|
+ self.add_log(f"📁 工作目录: {exe_dir}")
|
|
|
+
|
|
|
+ # 使用 ShellExecute 启动
|
|
|
+ import ctypes
|
|
|
+ result = ctypes.windll.shell32.ShellExecuteW(
|
|
|
+ None, # 父窗口句柄
|
|
|
+ "open", # 操作类型
|
|
|
+ exe_path, # 要打开的程序路径
|
|
|
+ "", # 命令行参数
|
|
|
+ exe_dir, # 工作目录
|
|
|
+ 1 # 窗口显示状态 (1=正常显示)
|
|
|
+ )
|
|
|
+
|
|
|
+ # ShellExecuteW 返回值 > 32 表示成功
|
|
|
+ if result > 32:
|
|
|
+ self.add_log(f"✅ [扫码任务] 测试发车启动成功: {car_plate}")
|
|
|
+ self.statusBar().showMessage(f"测试发车已启动: {car_plate}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"❌ [扫码任务] 启动失败: {car_plate},错误码: {result}")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.add_log(f"❌ [扫码任务] 执行失败 {car_plate}: {str(e)}")
|
|
|
+
|
|
|
+ # 注意:这里的定时器会在 process_next_scan 中启动,不需要在这里启动
|
|
|
+
|
|
|
+ # ===== 监控 =====
|
|
|
def start_monitoring(self):
|
|
|
- """启动监控"""
|
|
|
- load_addr = self.load_addr_input.text().strip()
|
|
|
- unload_addr = self.unload_addr_input.text().strip()
|
|
|
+ load_index = self.load_region_combo.currentIndex()
|
|
|
+ unload_index = self.unload_region_combo.currentIndex()
|
|
|
|
|
|
- if not load_addr and not unload_addr:
|
|
|
- QMessageBox.warning(self, "警告", "请至少输入一个地址关键词")
|
|
|
+ if load_index <= 0 and unload_index <= 0:
|
|
|
+ QMessageBox.warning(self, "警告", "请选择装车区域和/或卸车区域")
|
|
|
return
|
|
|
|
|
|
if not self.session_id:
|
|
|
QMessageBox.warning(self, "警告", "请先登录")
|
|
|
return
|
|
|
|
|
|
+ if self.send_client_combo.currentIndex() <= 0:
|
|
|
+ QMessageBox.warning(self, "警告", "请选择装车客户端")
|
|
|
+ return
|
|
|
+
|
|
|
+ if self.receive_client_combo.currentIndex() <= 0:
|
|
|
+ QMessageBox.warning(self, "警告", "请选择卸车客户端")
|
|
|
+ return
|
|
|
+
|
|
|
+ if not self.ws_client or not self.ws_client.is_running:
|
|
|
+ QMessageBox.warning(self, "警告", "WebSocket未连接")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 检查是否选择了文件夹
|
|
|
+ if not self.folder_path_input.text().strip():
|
|
|
+ reply = QMessageBox.question(
|
|
|
+ self,
|
|
|
+ "确认",
|
|
|
+ "未选择文件夹,将发送空值,是否继续?",
|
|
|
+ QMessageBox.Yes | QMessageBox.No,
|
|
|
+ QMessageBox.No
|
|
|
+ )
|
|
|
+ if reply == QMessageBox.No:
|
|
|
+ return
|
|
|
+
|
|
|
self.is_monitoring = True
|
|
|
self.start_btn.setEnabled(False)
|
|
|
self.stop_btn.setEnabled(True)
|
|
|
- self.manual_query_btn.setEnabled(False)
|
|
|
+ self.refresh_region_btn.setEnabled(False)
|
|
|
|
|
|
- # 清空触发记录
|
|
|
self.triggered_records = {}
|
|
|
|
|
|
+ load_region = self.load_region_combo.currentText() if load_index > 0 else "无"
|
|
|
+ unload_region = self.unload_region_combo.currentText() if unload_index > 0 else "无"
|
|
|
+
|
|
|
+ load_client = self.send_client_combo.currentText()
|
|
|
+ unload_client = self.receive_client_combo.currentText()
|
|
|
+
|
|
|
+ folder = self.folder_path_input.text().strip() or "无"
|
|
|
+
|
|
|
self.add_log("=" * 50)
|
|
|
- self.add_log(f"监控已启动 - 装车地址: {load_addr or '无'}, 卸车地址: {unload_addr or '无'}")
|
|
|
+ self.add_log(f"监控启动 - 装车区域: {load_region}, 卸车区域: {unload_region}")
|
|
|
+ self.add_log(f"装车客户端: {load_client}, 卸车客户端: {unload_client}")
|
|
|
+ self.add_log(f"文件夹路径: {folder}")
|
|
|
self.add_log("=" * 50)
|
|
|
|
|
|
- # 立即执行一次检查
|
|
|
- self.handle_query()
|
|
|
+ # 启动两个定时器
|
|
|
+ self.refresh_timer.start(10000)
|
|
|
+ self.region_timer.start(10000)
|
|
|
|
|
|
- # 启动定时器,每5秒刷新一次
|
|
|
- self.refresh_timer.start(5000)
|
|
|
self.statusBar().showMessage("监控运行中...")
|
|
|
|
|
|
def stop_monitoring(self):
|
|
|
- """停止监控"""
|
|
|
self.is_monitoring = False
|
|
|
self.refresh_timer.stop()
|
|
|
+ self.region_timer.stop()
|
|
|
self.start_btn.setEnabled(True)
|
|
|
self.stop_btn.setEnabled(False)
|
|
|
- self.manual_query_btn.setEnabled(True)
|
|
|
+ self.refresh_region_btn.setEnabled(True)
|
|
|
+
|
|
|
+ # 停止扫码任务
|
|
|
+ self.scan_timer.stop()
|
|
|
+ self.is_processing_scan = False
|
|
|
+ self.scan_task_queue = []
|
|
|
+ self.current_scan_plate = None
|
|
|
|
|
|
- self.add_log("=" * 50)
|
|
|
self.add_log("监控已停止")
|
|
|
- self.add_log("=" * 50)
|
|
|
self.statusBar().showMessage("监控已停止")
|
|
|
|
|
|
- def auto_refresh(self):
|
|
|
- """自动刷新"""
|
|
|
- if self.is_monitoring and self.session_id:
|
|
|
- self.handle_query()
|
|
|
+ # ===== 区域检测 =====
|
|
|
+ def check_regions(self):
|
|
|
+ """检测区域车辆"""
|
|
|
+ if not self.is_monitoring:
|
|
|
+ return
|
|
|
+
|
|
|
+ load_region_id = self.load_region_combo.currentData() if self.load_region_combo.currentIndex() > 0 else None
|
|
|
+ unload_region_id = self.unload_region_combo.currentData() if self.unload_region_combo.currentIndex() > 0 else None
|
|
|
+
|
|
|
+ if not load_region_id and not unload_region_id:
|
|
|
+ return
|
|
|
+
|
|
|
+ self.region_thread = RegionQueryThread(self.session_id)
|
|
|
+ self.region_thread.finished.connect(lambda data: self.on_region_check(data, load_region_id, unload_region_id))
|
|
|
+ self.region_thread.error.connect(lambda err: self.add_log(f"❌ 检测区域失败: {err}"))
|
|
|
+ self.region_thread.start()
|
|
|
|
|
|
- def check_address_match(self):
|
|
|
- """检查地址匹配"""
|
|
|
- load_keyword = self.load_addr_input.text().strip()
|
|
|
- unload_keyword = self.unload_addr_input.text().strip()
|
|
|
+ def on_region_check(self, data, load_region_id, unload_region_id):
|
|
|
+ """处理区域检测结果"""
|
|
|
+ regions = data.get('list', [])
|
|
|
current_time = datetime.now()
|
|
|
+ self.add_log(json.dumps(regions))
|
|
|
+ for region in regions:
|
|
|
+ region_id = region.get('regionId')
|
|
|
+ region_name = region.get('regionName', '未知')
|
|
|
+ car_list = region.get('carList', [])
|
|
|
+
|
|
|
+ # 更新车辆数据
|
|
|
+ self.region_cars[region_id] = car_list
|
|
|
+
|
|
|
+ # 检查装车区域
|
|
|
+ if load_region_id and region_id == load_region_id:
|
|
|
+ self.check_region_cars(region_name, car_list, 'load', current_time)
|
|
|
+
|
|
|
+ # 检查卸车区域
|
|
|
+ if unload_region_id and region_id == unload_region_id:
|
|
|
+ self.check_region_cars(region_name, car_list, 'unload', current_time)
|
|
|
|
|
|
- for row in range(self.table.rowCount()):
|
|
|
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
|
|
|
- addr = self.table.item(row, 4).text() if self.table.item(row, 4) else ''
|
|
|
+ self.update_region_status()
|
|
|
+
|
|
|
+ def check_region_cars(self, region_name, car_list, task_type, current_time):
|
|
|
+ for car_id in car_list:
|
|
|
+ if not car_id:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 先初始化车牌
|
|
|
+ car_plate = None
|
|
|
+ car_state = None
|
|
|
+ # 根据carId找车牌和状态
|
|
|
+ for car in self.car_data:
|
|
|
+ if str(car.get('carId')) == str(car_id):
|
|
|
+ car_plate = car.get('carPlate')
|
|
|
+ car_state = car.get('stateCn', '') # 获取状态
|
|
|
+ break
|
|
|
|
|
|
- if not car_plate or not addr:
|
|
|
+ # 找不到车牌直接跳过
|
|
|
+ if not car_plate:
|
|
|
+ continue
|
|
|
+ # 状态过滤:只处理 "在线-停车" 的车辆
|
|
|
+ if "在线-停车" not in car_state:
|
|
|
+ continue
|
|
|
+ # 核心:启用过滤(必须放这里)
|
|
|
+ if not self.car_checked_state.get(car_plate, False):
|
|
|
continue
|
|
|
|
|
|
- # 初始化该车辆的触发记录
|
|
|
+ # 初始化记录
|
|
|
if car_plate not in self.triggered_records:
|
|
|
self.triggered_records[car_plate] = {'load': None, 'unload': None}
|
|
|
-
|
|
|
- # 检查装车地址
|
|
|
- if load_keyword and load_keyword in addr:
|
|
|
- last_trigger = self.triggered_records[car_plate]['load']
|
|
|
- if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
|
|
|
- self.triggered_records[car_plate]['load'] = current_time
|
|
|
- self.add_log(f"🚛 {car_plate} 到达装车位置 ({load_keyword})")
|
|
|
- self.statusBar().showMessage(f"{car_plate} 到达装车位置")
|
|
|
-
|
|
|
- # 检查卸车地址
|
|
|
- if unload_keyword and unload_keyword in addr:
|
|
|
- last_trigger = self.triggered_records[car_plate]['unload']
|
|
|
- if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
|
|
|
- self.triggered_records[car_plate]['unload'] = current_time
|
|
|
- self.add_log(f"🚛 {car_plate} 到达卸车位置 ({unload_keyword})")
|
|
|
- self.statusBar().showMessage(f"{car_plate} 到达卸车位置")
|
|
|
-
|
|
|
- def add_log(self, message):
|
|
|
- """添加日志"""
|
|
|
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
- log_line = f"[{timestamp}] {message}"
|
|
|
- self.log_output.append(log_line)
|
|
|
+
|
|
|
+ last_trigger = self.triggered_records[car_plate][task_type]
|
|
|
+
|
|
|
+ if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
|
|
|
+ self.triggered_records[car_plate][task_type] = current_time
|
|
|
+ task_name = "装车" if task_type == 'load' else "卸车"
|
|
|
+ self.add_log(f"🚛 {car_plate} 在 {region_name} 检测到{task_name}")
|
|
|
+ self.execute_task(car_plate, task_type)
|
|
|
+
|
|
|
+ # ===== 执行任务 =====
|
|
|
+ def execute_task(self, car_plate, task_type):
|
|
|
+ """通过WebSocket执行任务"""
|
|
|
+ if not self.ws_client or not self.ws_client.is_running:
|
|
|
+ self.add_log("❌ WebSocket未连接,无法执行任务")
|
|
|
+ return
|
|
|
|
|
|
- # 保持最新日志可见
|
|
|
- scrollbar = self.log_output.verticalScrollBar()
|
|
|
- scrollbar.setValue(scrollbar.maximum())
|
|
|
+ # 获取对应的客户端 userID
|
|
|
+ if task_type == 'load':
|
|
|
+ user_id = self.send_client_combo.currentData()
|
|
|
+ else:
|
|
|
+ user_id = self.receive_client_combo.currentData()
|
|
|
+
|
|
|
+ if not user_id:
|
|
|
+ self.add_log(f"❌ 未选择{task_type}客户端")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 生成带车牌的文件路径: 文件夹路径/车牌.jpg
|
|
|
+ value = self.get_value_with_plate(car_plate)
|
|
|
+
|
|
|
+ if not value:
|
|
|
+ self.add_log(f"⚠️ 未选择文件夹,发送空值,车牌: {car_plate}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"📁 生成文件路径: {value}")
|
|
|
+
|
|
|
+ # 发送指令
|
|
|
+ route = "发货" if task_type == 'load' else "收货"
|
|
|
+ success = self.ws_client.send(route, value, user_id)
|
|
|
+
|
|
|
+ if success:
|
|
|
+ self.add_log(f"📤 发送{route}指令到 {user_id},车牌: {car_plate},value: {value}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"❌ 发送{route}指令失败")
|
|
|
+
|
|
|
+ # ===== 发送指令(手动) =====
|
|
|
+ def send_cmd_to_vehicle(self, car_plate, text_msg):
|
|
|
+ """发送指令到车辆(保留原有功能)"""
|
|
|
+ if not self.session_id or not self.username:
|
|
|
+ self.add_log("❌ 未登录,无法发送指令")
|
|
|
+ return
|
|
|
+
|
|
|
+ params = {"textMsg": text_msg, "textType": 1, "signBit": "10010000"}
|
|
|
+ url = "http://119.91.58.175:9999/gps-web/api/send_cmd.jsp"
|
|
|
+ req_params = {
|
|
|
+ "userId": self.username,
|
|
|
+ "cmdId": "500",
|
|
|
+ "cmd": "CmdSendText",
|
|
|
+ "params": json.dumps(params, ensure_ascii=False),
|
|
|
+ "sessionId": self.session_id,
|
|
|
+ "plate": car_plate
|
|
|
+ }
|
|
|
+
|
|
|
+ try:
|
|
|
+ response = requests.get(url, params=req_params, timeout=10)
|
|
|
+ response.encoding = 'utf-8'
|
|
|
+ self.add_log(f"📤 发送指令到 {car_plate}: {text_msg}")
|
|
|
+ if response.status_code == 200:
|
|
|
+ data = response.json()
|
|
|
+ code = data.get('rspCode')
|
|
|
+ if code == '0':
|
|
|
+ self.add_log(f"✅ 指令发送成功")
|
|
|
+ else:
|
|
|
+ self.add_log(f"⚠️ {data.get('rspDesc', '发送失败')}")
|
|
|
+ else:
|
|
|
+ self.add_log(f"❌ HTTP {response.status_code}")
|
|
|
+ except Exception as e:
|
|
|
+ self.add_log(f"❌ {str(e)}")
|
|
|
+
|
|
|
+ # ===== 配置 =====
|
|
|
+ def save_config(self):
|
|
|
+ config = {
|
|
|
+ 'username': self.username_input.text(),
|
|
|
+ 'folder_path': self.folder_path_input.text()
|
|
|
+ }
|
|
|
+ try:
|
|
|
+ with open('config.json', 'w', encoding='utf-8') as f:
|
|
|
+ json.dump(config, f, indent=4, ensure_ascii=False)
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ def load_config(self):
|
|
|
+ if os.path.exists('config.json'):
|
|
|
+ try:
|
|
|
+ with open('config.json', 'r', encoding='utf-8') as f:
|
|
|
+ config = json.load(f)
|
|
|
+ self.username_input.setText(config.get('username', '深金川'))
|
|
|
+ folder_path = config.get('folder_path', '')
|
|
|
+ if folder_path:
|
|
|
+ self.folder_path_input.setText(folder_path)
|
|
|
+ self.folder_path = folder_path
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # ===== 日志 =====
|
|
|
+ def add_log(self, message, level="INFO"):
|
|
|
+ """添加日志"""
|
|
|
+ timestamp = datetime.now().strftime("%H:%M:%S")
|
|
|
+ self.log_output.append(f"[{timestamp}] {message}")
|
|
|
+ self.log_output.verticalScrollBar().setValue(
|
|
|
+ self.log_output.verticalScrollBar().maximum()
|
|
|
+ )
|
|
|
+
|
|
|
+# ==================== 启动 ====================
|
|
|
|
|
|
def main():
|
|
|
app = QApplication(sys.argv)
|