| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683 |
- import sys
- 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, QComboBox, QFileDialog, QDialog, QDialogButtonBox)
- from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
- 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
- import json
- import os
-
- # 获取图片的base64编码
- value = data.get('value', '{}')
- print(f"收到原始value: {value}")
-
- # 如果 value 已经是 dict,直接使用
- if isinstance(value, dict):
- img_base64 = value.get("base64", "")
- plate = value.get("plate", "")
- else:
- # 如果是字符串,尝试解析 JSON
- value_dict = json.loads(value)
- img_base64 = value_dict.get("base64", "")
- plate = value_dict.get("plate", "")
-
- print(f"解析到车牌: {plate}")
-
- 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)
-
- # 获取用户选择的保存目录
- folder_path = self.get_folder_path()
- if not folder_path:
- self.error.emit("未选择文件夹,无法保存图片")
- return
-
- # 确保目录存在
- if not os.path.exists(folder_path):
- try:
- os.makedirs(folder_path)
- except Exception as e:
- self.error.emit(f"创建目录失败: {str(e)}")
- return
-
- # 保存到用户选择的目录
- filepath = os.path.join(folder_path, plate, "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,
- "plate": plate
- }
- 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)
-
- def __init__(self, username, password):
- super().__init__()
- self.username = username
- self.password = password
-
- def run(self):
- 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':
- 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 QueryThread(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_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':
- 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 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.car_scan_state = {} # 扫码状态(新增)
- self.triggered_records = {}
- self.is_monitoring = False
- # 扫码任务相关
- self.scan_task_queue = [] # 待处理的车辆队列
- self.is_processing_scan = False # 是否正在处理扫码任务
- self.scan_timer = QTimer() # 定时器用于间隔执行
- self.scan_timer.timeout.connect(self.process_next_scan)
- self.current_scan_plate = None # 当前等待发送2001的车牌
- self.car_task_state = {}
- # 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.last_load_client = "" # 上次选择的装车客户端userID
- self.last_unload_client = "" # 上次选择的卸车客户端userID
- self.last_load_region = "" # 上次选择的装车区域ID
- self.last_unload_region = "" # 上次选择的卸车区域ID
-
- self.initUI()
- self.load_config()
-
- def initUI(self):
- self.setWindowTitle('车辆定位监控系统')
- 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()
- login_layout.addWidget(self.username_input)
- login_layout.addWidget(QLabel("密码:"))
- self.password_input = QLineEdit()
- self.password_input.setEchoMode(QLineEdit.Password)
- 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)
-
- # ===== 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(220)
- ws_layout.addWidget(self.send_client_combo)
-
- ws_layout.addWidget(QLabel("卸车客户端:"))
- self.receive_client_combo = QComboBox()
- self.receive_client_combo.setMinimumWidth(220)
- 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)
-
- folder_layout.addStretch()
- folder_group.setLayout(folder_layout)
- main_layout.addWidget(folder_group)
-
- # ===== 区域监控设置 =====
- 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)
- 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)
- region_layout.addWidget(self.stop_btn, 0, 6)
-
- region_layout.addWidget(QLabel("检测间隔:10秒"), 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)
-
- 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(6) # 减少一列
- self.table.setHorizontalHeaderLabels(["启用", "车队名", "车牌号", "状态", "定位地址", "操作"])
- self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
- self.table.setAlternatingRowColors(True)
- table_layout.addWidget(self.table)
-
- 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)
- 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_layout.addWidget(QLabel("日志"))
- self.log_output = QTextEdit()
- self.log_output.setReadOnly(True)
- self.log_output.document().setMaximumBlockCount(500)
- log_layout.addWidget(self.log_output)
-
- splitter.addWidget(log_widget)
- 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')
- print(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(data.get('plate', ''))
- elif msg_type == 'drive': # 收到"开车"命令
- car_plate = data.get('value', '')
- if car_plate:
- self.add_log(f"📤 收到外部WebSocket开车命令,车辆: {car_plate}")
- # 设置状态为"已发车"
- self.car_task_state[car_plate] = '已发车'
- self.update_task_state_column()
- self.add_log(f"📌 {car_plate} 状态更新为: 已发车")
- # 发送2001指令
- self.send_cmd_to_vehicle(car_plate, "2001")
- # 清空当前车牌
- self.current_scan_plate = None
- # 继续处理下一辆车
- self.process_next_scan()
- else:
- self.add_log(f"⚠️ 收到开车命令但车牌为空")
- elif msg_type == 'clear': # 新增:处理clear事件
- car_plate = data.get('value', '')
- if car_plate:
- self.add_log(f"🧹 收到清除命令,车辆: {car_plate}")
- self.clear_vehicle_enable(car_plate)
- else:
- self.add_log(f"⚠️ 收到清除命令但车牌为空")
- def clear_vehicle_enable(self, car_plate):
- """取消指定车辆的启用勾选,并自动重启监控"""
- if not car_plate:
- return
-
- # 在表格中查找该车牌
- found = False
- for row in range(self.table.rowCount()):
- plate_item = self.table.item(row, 2) # 车牌号在第2列
- if plate_item and plate_item.text() == car_plate:
- # 获取启用复选框
- enable_checkbox = self.table.cellWidget(row, 0)
- if enable_checkbox:
- # 取消勾选
- enable_checkbox.setChecked(False)
- # 更新状态字典
- self.car_checked_state[car_plate] = False
- # 更新任务状态
- self.car_task_state[car_plate] = '空闲'
- self.update_task_state_column()
- self.add_log(f"✅ 已取消车辆 {car_plate} 的启用勾选")
- found = True
- break
-
- # 如果在表格中没找到该车牌,但在状态字典中有记录
- if not found:
- if car_plate in self.car_checked_state:
- self.car_checked_state[car_plate] = False
- self.car_task_state[car_plate] = '空闲'
- self.add_log(f"✅ 已清除车辆 {car_plate} 的启用状态(列表中未找到)")
- else:
- self.add_log(f"⚠️ 未找到车辆 {car_plate},无法取消启用")
- return
-
- # 如果监控正在运行,自动重启
- if self.is_monitoring:
- self.add_log("🔄 自动重启监控...")
- self.stop_monitoring() # 直接调用停止方法
- QTimer.singleShot(1000, self.start_monitoring) # 1秒后直接调用启动方法
- def start_scan_tasks(self, plate):
- """开始执行扫码任务 - 只处理收到的车牌"""
- if self.is_processing_scan:
- self.add_log("⏳ 扫码任务正在处理中,忽略本次请求")
- return
-
- if not plate:
- self.add_log("⚠️ 收到的车牌为空,跳过")
- return
-
- # 检查"启用"复选框
- if not self.car_checked_state.get(plate, False):
- self.add_log(f"⚠️ 车辆 {plate} 未勾选启用,跳过")
- return
-
- # 检查该车牌是否存在
- car_exists = False
- for row in range(self.table.rowCount()):
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
- if car_plate == plate:
- car_exists = True
- break
-
- if not car_exists:
- self.add_log(f"⚠️ 车辆 {plate} 不在列表中,跳过")
- return
-
- # 设置状态为"等待扫码"
- self.car_task_state[plate] = '等待扫码'
- self.update_task_state_column()
- self.add_log(f"📌 {plate} 状态更新为: 等待扫码")
-
- # 初始化队列 - 只包含这一个车牌
- self.scan_task_queue = [plate]
- self.is_processing_scan = True
- self.current_scan_plate = None
-
- self.add_log(f"🚀 开始扫码任务,车辆: {plate}")
-
- # 立即处理第一个(也是唯一一个)
- 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)
-
- self.add_log(f"⏳ 等待外部WebSocket通知发送2001指令到: {car_plate},超时时间2分钟")
- 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("请选择卸车客户端", "")
-
- # 保存所有客户端的显示名称和userID映射
- client_map = {}
- 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
- display_name = f"{client_id}"
- client_map[user_id] = display_name
- 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)} 个在线扫码客户端")
-
- # 尝试恢复上次选择的客户端
- self.restore_client_selection()
-
- def restore_client_selection(self):
- """恢复上次选择的客户端"""
- # 恢复装车客户端
- if self.last_load_client:
- index = self.send_client_combo.findData(self.last_load_client)
- if index >= 0:
- self.send_client_combo.setCurrentIndex(index)
- self.add_log(f"🔄 恢复装车客户端选择: {self.send_client_combo.currentText()}")
- else:
- self.add_log(f"⚠️ 上次选择的装车客户端 (userID: {self.last_load_client}) 不在线,请重新选择")
- self.last_load_client = "" # 清空无效的选择
-
- # 恢复卸车客户端
- if self.last_unload_client:
- index = self.receive_client_combo.findData(self.last_unload_client)
- if index >= 0:
- self.receive_client_combo.setCurrentIndex(index)
- self.add_log(f"🔄 恢复卸车客户端选择: {self.receive_client_combo.currentText()}")
- else:
- self.add_log(f"⚠️ 上次选择的卸车客户端 (userID: {self.last_unload_client}) 不在线,请重新选择")
- self.last_unload_client = "" # 清空无效的选择
-
- 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.login_thread = LoginThread(username, password)
- self.login_thread.finished.connect(self.on_login_success)
- self.login_thread.error.connect(self.on_login_error)
- self.login_thread.start()
-
- def on_login_success(self, data):
- self.session_id = data.get('sessionId')
- self.username = self.username_input.text().strip()
- if self.session_id:
- self.login_btn.setEnabled(True)
- self.status_label.setText(f"已登录: {self.username}")
- self.refresh_region_btn.setEnabled(True)
- self.start_btn.setEnabled(True)
- 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("登录失败")
- QMessageBox.critical(self, "错误", error_msg)
-
- # ===== 加载区域 =====
- def load_regions(self):
- if not self.session_id:
- QMessageBox.warning(self, "警告", "请先登录")
- return
-
- 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("请选择卸车区域", "")
-
- # 保存区域ID到显示名称的映射
- region_map = {}
- for region in self.regions:
- region_name = region.get('regionName', '未知')
- region_id = region.get('regionId', '')
- display = f"{region_name}"
- region_map[region_id] = display
- 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)} 个区域")
-
- # 尝试恢复上次选择的区域
- self.restore_region_selection()
-
- def restore_region_selection(self):
- """恢复上次选择的区域"""
- # 恢复装车区域
- if self.last_load_region:
- index = self.load_region_combo.findData(self.last_load_region)
- if index >= 0:
- self.load_region_combo.setCurrentIndex(index)
- self.add_log(f"🔄 恢复装车区域选择: {self.load_region_combo.currentText()}")
- else:
- self.add_log(f"⚠️ 上次选择的装车区域已不存在,请重新选择")
- self.last_load_region = "" # 清空无效的选择
-
- # 恢复卸车区域
- if self.last_unload_region:
- index = self.unload_region_combo.findData(self.last_unload_region)
- if index >= 0:
- self.unload_region_combo.setCurrentIndex(index)
- self.add_log(f"🔄 恢复卸车区域选择: {self.unload_region_combo.currentText()}")
- else:
- self.add_log(f"⚠️ 上次选择的卸车区域已不存在,请重新选择")
- self.last_unload_region = "" # 清空无效的选择
-
- def on_regions_error(self, error_msg):
- self.refresh_region_btn.setEnabled(True)
- self.add_log(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)
- self.query_thread.error.connect(self.on_query_error)
- self.query_thread.start()
-
- def on_query_success(self, data):
- 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', '')
-
- # 第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)
-
- # 第1列:车队名
- self.table.setItem(row, 1, QTableWidgetItem(car.get('teamName', '')))
-
- # 第2列:车牌号
- self.table.setItem(row, 2, QTableWidgetItem(car_plate))
-
- # 第3列:状态(车辆定位状态)
- self.table.setItem(row, 3, QTableWidgetItem(car.get('stateCn', '')))
-
- # 第4列:定位地址
- self.table.setItem(row, 4, QTableWidgetItem(car.get('addr', '')))
-
- # 第5列:操作按钮(测试发车)
- test_btn = QPushButton("测试发车")
- test_btn.clicked.connect(lambda checked, plate=car_plate: self.on_test_drive(plate))
- self.table.setCellWidget(row, 5, test_btn)
-
- # 更新任务状态列(在表格后面添加一列显示任务状态)
- self.update_task_state_column()
-
- self.statusBar().showMessage(f"车辆列表更新: {len(car_list)} 辆车")
- def update_task_state_column(self):
- """更新任务状态列(在表格末尾添加)"""
- # 检查是否已有任务状态列,如果没有则添加
- if self.table.columnCount() < 7:
- self.table.setColumnCount(7)
- self.table.setHorizontalHeaderLabels(["启用", "车队名", "车牌号", "状态", "定位地址", "操作", "任务状态"])
-
- for row in range(self.table.rowCount()):
- car_plate = self.table.item(row, 2).text() if self.table.item(row, 2) else ''
- if car_plate:
- task_state = self.car_task_state.get(car_plate, '空闲')
- state_item = QTableWidgetItem(task_state)
- # 根据状态设置颜色
- if task_state == '空闲':
- state_item.setBackground(Qt.green)
- elif task_state == '等待扫码':
- state_item.setBackground(Qt.yellow)
- elif task_state == '已发车':
- state_item.setBackground(Qt.red)
- self.table.setItem(row, 6, state_item)
- def on_query_error(self, error_msg):
- self.add_log(error_msg)
-
- # ===== 表格操作 =====
- def save_check_state(self):
- """保存启用和扫码状态"""
- for row in range(self.table.rowCount()):
- # 启用复选框
- 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] = 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_enable_changed(self, car_plate, state):
- """启用复选框状态变化"""
- self.car_checked_state[car_plate] = (state == Qt.Checked)
-
- 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, 3).text() if self.table.item(row, 3) else ''
- if car_plate:
- self.car_checked_state[car_plate] = True
-
- 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, 3).text() if self.table.item(row, 3) else ''
- if car_plate:
- self.car_checked_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_index = self.load_region_combo.currentIndex()
- unload_index = self.unload_region_combo.currentIndex()
-
- 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.save_current_selections()
-
- self.is_monitoring = True
- self.start_btn.setEnabled(False)
- self.stop_btn.setEnabled(True)
- 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_region}, 卸车区域: {unload_region}")
- self.add_log(f"装车客户端: {load_client}, 卸车客户端: {unload_client}")
- self.add_log(f"文件夹路径: {folder}")
- self.add_log("=" * 50)
-
- # 启动两个定时器
- self.refresh_timer.start(10000)
- self.region_timer.start(10000)
-
- self.statusBar().showMessage("监控运行中...")
-
- def save_current_selections(self):
- """保存当前选择的客户端和区域"""
- # 保存客户端选择
- self.last_load_client = self.send_client_combo.currentData() or ""
- self.last_unload_client = self.receive_client_combo.currentData() or ""
-
- # 保存区域选择
- self.last_load_region = self.load_region_combo.currentData() or ""
- self.last_unload_region = self.unload_region_combo.currentData() or ""
-
- # 保存到配置文件
- self.save_config()
-
- 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.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("监控已停止")
- self.statusBar().showMessage("监控已停止")
-
- # ===== 区域检测 =====
- def check_regions(self):
- """检测区域车辆"""
- if not self.is_monitoring:
- return
- # 如果正在执行扫码任务,跳过区域检测
- if self.is_processing_scan:
- self.add_log("⏳ 正在执行扫码任务,跳过本次区域检测")
- 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 on_region_check(self, data, load_region_id, unload_region_id):
- """处理区域检测结果"""
- regions = data.get('list', [])
- current_time = datetime.now()
-
- load_triggered = False # 添加标记
-
- 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 self.is_processing_scan:
- load_triggered = True
-
- # 检查卸车区域(只有装车没触发才执行)
- if not load_triggered and unload_region_id and region_id == unload_region_id:
- self.check_region_cars(region_name, car_list, 'unload', current_time)
-
- 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
- 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:
- 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}
- 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.car_task_state[car_plate] = '等待扫码'
- self.update_task_state_column()
- self.add_log(f"📌 {car_plate} 状态更新为: 等待扫码 (检测到{task_name})")
-
- self.add_log(f"🚛 {car_plate} 在 {region_name} 检测到{task_name}")
- self.execute_task(car_plate, task_type)
- break
-
- # ===== 执行任务 =====
- 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
-
- # 获取对应的客户端 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(),
- 'password': self.password_input.text(),
- 'folder_path': self.folder_path_input.text(),
- 'last_load_client': self.last_load_client,
- 'last_unload_client': self.last_unload_client,
- 'last_load_region': self.last_load_region,
- 'last_unload_region': self.last_unload_region
- }
- 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', ''))
- self.password_input.setText(config.get('password', ''))
- folder_path = config.get('folder_path', '')
- if folder_path:
- self.folder_path_input.setText(folder_path)
- self.folder_path = folder_path
-
- # 加载上次选择的客户端
- self.last_load_client = config.get('last_load_client', '')
- self.last_unload_client = config.get('last_unload_client', '')
-
- # 加载上次选择的区域
- self.last_load_region = config.get('last_load_region', '')
- self.last_unload_region = config.get('last_unload_region', '')
- 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()
- )
- # ==================== 启动 ====================
- class CDKVerificationDialog(QDialog):
- """CDK验证对话框"""
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle("软件授权验证")
- self.setModal(True)
- self.setFixedSize(400, 200)
-
- layout = QVBoxLayout()
-
- # 说明标签
- info_label = QLabel("请输入您的CDK授权码以继续使用软件:")
- info_label.setStyleSheet("font-size: 12px; margin: 10px 0;")
- layout.addWidget(info_label)
-
- # CDK输入框
- self.cdk_input = QLineEdit()
- self.cdk_input.setPlaceholderText("请输入CDK授权码...")
- self.cdk_input.setEchoMode(QLineEdit.Password) # 隐藏输入
- self.cdk_input.setStyleSheet("padding: 8px; font-size: 12px;")
- layout.addWidget(self.cdk_input)
-
- # 提示标签(用于显示错误信息)
- self.status_label = QLabel("")
- self.status_label.setStyleSheet("color: red; font-size: 11px;")
- layout.addWidget(self.status_label)
-
- # 按钮
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- button_box.accepted.connect(self.accept)
- button_box.rejected.connect(self.reject)
- layout.addWidget(button_box)
-
- self.setLayout(layout)
-
- def get_cdk(self):
- """获取输入的CDK"""
- return self.cdk_input.text().strip()
- def verify_cdk(cdk_code, cdk_type="三一平台"):
- """
- 验证CDK
- :param cdk_code: CDK码
- :param cdk_type: CDK类型(默认为"三一平台")
- :return: (是否通过, 消息)
- """
- try:
- # 构建请求URL
- url = f"https://user.port.run/cdk/verify/{cdk_type}/{cdk_code}"
-
- # 发送GET请求
- response = requests.get(url, timeout=10)
-
- if response.status_code == 200:
- result = response.json()
- # 根据接口返回判断:err=0 表示验证通过
- if result.get('err') == 0:
- return True, result.get('message', '验证通过')
- else:
- return False, result.get('message', '验证失败')
- else:
- return False, f"服务器响应异常 (HTTP {response.status_code})"
-
- except requests.exceptions.Timeout:
- return False, "网络请求超时,请检查网络连接"
- except requests.exceptions.ConnectionError:
- return False, "无法连接到验证服务器,请检查网络"
- except requests.exceptions.RequestException as e:
- return False, f"网络请求失败: {str(e)}"
- except json.JSONDecodeError:
- return False, "服务器返回数据格式错误"
- except Exception as e:
- return False, f"验证过程发生未知错误: {str(e)}"
- def save_cdk_to_file(cdk_code):
- """保存CDK到本地文件"""
- try:
- config = {
- "cdk": cdk_code,
- "verified_time": time.time(),
- "verified_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- }
- with open("cdk_config.json", "w", encoding='utf-8') as f:
- json.dump(config, f, indent=2, ensure_ascii=False)
- return True
- except Exception as e:
- print(f"保存CDK配置失败: {e}")
- return False
- def load_cdk_from_file():
- """从本地文件加载CDK"""
- try:
- if os.path.exists("cdk_config.json"):
- with open("cdk_config.json", "r", encoding='utf-8') as f:
- config = json.load(f)
- return config.get("cdk", "")
- except Exception as e:
- print(f"加载CDK配置失败: {e}")
- return ""
- def main():
- app = QApplication(sys.argv)
-
- # ========== CDK验证(替换原有的许可证验证) ==========
- try:
- # 尝试从文件加载之前保存的CDK
- saved_cdk = load_cdk_from_file()
- cdk_valid = False
-
- # 如果之前有保存的CDK,先尝试自动验证
- if saved_cdk:
- is_valid, msg = verify_cdk(saved_cdk)
- if is_valid:
- print(f"自动验证通过: {msg}")
- cdk_valid = True
- else:
- print(f"自动验证失败: {msg}")
-
- # 如果需要手动输入验证
- if not cdk_valid:
- max_attempts = 3
- attempts = 0
-
- while attempts < max_attempts:
- # 创建验证对话框
- dialog = CDKVerificationDialog()
- dialog.setWindowTitle(f"软件授权验证 (尝试 {attempts + 1}/{max_attempts})")
-
- if dialog.exec_() == QDialog.Accepted:
- cdk_code = dialog.get_cdk()
-
- if not cdk_code:
- QMessageBox.warning(None, "输入错误", "CDK不能为空,请重新输入")
- attempts += 1
- continue
-
- # 执行验证
- is_valid, msg = verify_cdk(cdk_code)
-
- if is_valid:
- # 验证通过,保存CDK
- save_cdk_to_file(cdk_code)
- QMessageBox.information(None, "授权成功", f"CDK验证通过!\n{msg}")
- cdk_valid = True
- break
- else:
- # 验证失败
- attempts += 1
- if attempts >= max_attempts:
- QMessageBox.critical(None, "授权失败",
- f"CDK验证失败:{msg}\n\n已达到最大尝试次数 ({max_attempts}),程序将退出。")
- else:
- QMessageBox.warning(None, "授权失败",
- f"CDK验证失败:{msg}\n\n剩余尝试次数: {max_attempts - attempts}")
- else:
- # 用户取消了对话框
- QMessageBox.information(None, "操作取消", "您取消了授权验证,程序将退出。")
- sys.exit(1)
-
- if not cdk_valid:
- sys.exit(1)
-
- except Exception as e:
- QMessageBox.critical(None, "错误", f"CDK验证过程出错: {e}")
- sys.exit(1)
- # ============================================
-
- window = MainWindow()
- window.show()
- sys.exit(app.exec_())
- if __name__ == '__main__':
- main()
|