三一平台.py 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340
  1. import sys
  2. import hashlib
  3. import requests
  4. import json
  5. import time
  6. import threading
  7. import os
  8. import re
  9. import ssl
  10. import subprocess
  11. from datetime import datetime, timedelta
  12. from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
  13. QHBoxLayout, QLabel, QLineEdit, QPushButton,
  14. QTableWidget, QTableWidgetItem, QHeaderView,
  15. QCheckBox, QMessageBox, QGroupBox, QTextEdit,
  16. QGridLayout, QSplitter, QComboBox, QFileDialog)
  17. from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
  18. import websocket
  19. import threading
  20. import time
  21. # ==================== WebSocket 客户端 ====================
  22. class WebSocketClient(QThread):
  23. connected = pyqtSignal()
  24. disconnected = pyqtSignal()
  25. message_received = pyqtSignal(dict)
  26. error = pyqtSignal(str)
  27. def __init__(self, get_folder_callback=None):
  28. super().__init__()
  29. self.ws = None
  30. self.is_running = False
  31. self.user_id = None
  32. self.type_str = "自动扫码"
  33. self.is_admin = True
  34. self.device_id = None
  35. self.ws_lock = threading.Lock()
  36. self.get_folder_callback = get_folder_callback # 获取文件夹路径的回调函数
  37. def connect(self, device_id):
  38. """连接WebSocket服务器"""
  39. self.device_id = device_id
  40. self.is_running = True
  41. self.start()
  42. def run(self):
  43. """在子线程中运行WebSocket连接"""
  44. try:
  45. self._connect_ws()
  46. except Exception as e:
  47. self.error.emit(f"WebSocket连接错误: {str(e)}")
  48. def _connect_ws(self):
  49. """建立WebSocket连接并处理消息"""
  50. while self.is_running:
  51. try:
  52. # 创建WebSocketApp时添加ssl_options参数禁用证书验证
  53. self.ws = websocket.WebSocketApp(
  54. 'wss://ws.lamp.run',
  55. on_open=self._on_open,
  56. on_message=self._on_message,
  57. on_error=self._on_error,
  58. on_close=self._on_close
  59. )
  60. # 运行WebSocket,禁用SSL证书验证
  61. # 添加 sslopt={"cert_reqs": ssl.CERT_NONE} 参数
  62. import ssl
  63. self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
  64. # 如果连接断开,等待3秒重连
  65. if self.is_running:
  66. time.sleep(3)
  67. except Exception as e:
  68. self.error.emit(f"连接异常: {str(e)}")
  69. if self.is_running:
  70. time.sleep(3)
  71. def _on_open(self, ws):
  72. """连接打开时的回调"""
  73. self.connected.emit()
  74. # 发送登录消息
  75. login_msg = {
  76. "route": "login",
  77. "type": self.type_str,
  78. "admin": self.is_admin,
  79. "id": self.device_id
  80. }
  81. ws.send(json.dumps(login_msg))
  82. def _on_message(self, ws, message):
  83. """接收消息的回调"""
  84. try:
  85. data = json.loads(message)
  86. # 处理用户信息
  87. if data.get('type') == 'userInfo':
  88. self.user_id = data.get('value', {}).get('userID')
  89. if self.user_id:
  90. self.message_received.emit(data)
  91. # 处理图片接收
  92. elif data.get('type') == 'imgIn':
  93. self.handle_image_received(data)
  94. else:
  95. self.message_received.emit(data)
  96. except json.JSONDecodeError:
  97. pass
  98. except Exception as e:
  99. print(f"处理消息异常: {e}")
  100. def handle_image_received(self, data):
  101. """处理接收到的图片"""
  102. try:
  103. import base64
  104. # 获取图片的base64编码
  105. img_base64 = data.get('value', '')
  106. if not img_base64:
  107. self.error.emit("图片数据为空")
  108. return
  109. # 获取发送者ID
  110. sender_id = data.get('id', 'unknown')
  111. # 去除可能的base64前缀
  112. if ',' in img_base64:
  113. img_base64 = img_base64.split(',')[1]
  114. # 解码base64
  115. image_data = base64.b64decode(img_base64)
  116. # 固定保存到C盘根目录
  117. filepath = "C:/imgIn.jpeg"
  118. # 保存图片
  119. with open(filepath, 'wb') as f:
  120. f.write(image_data)
  121. print(f"✅ 图片已保存: {filepath}")
  122. # 通知主窗口图片已保存
  123. success_msg = {
  124. 'type': 'imgSaved',
  125. 'filepath': filepath,
  126. 'filename': "imgIn.jpeg",
  127. 'sender_id': sender_id
  128. }
  129. self.message_received.emit(success_msg)
  130. except Exception as e:
  131. error_msg = f"保存图片失败: {str(e)}"
  132. print(f"❌ {error_msg}")
  133. self.error.emit(error_msg)
  134. def get_folder_path(self):
  135. """获取保存文件夹路径"""
  136. if self.get_folder_callback:
  137. return self.get_folder_callback()
  138. return ""
  139. def _on_error(self, ws, error):
  140. """错误回调"""
  141. self.error.emit(f"WebSocket错误: {str(error)}")
  142. def _on_close(self, ws, close_status_code, close_msg):
  143. """连接关闭回调"""
  144. self.disconnected.emit()
  145. def send(self, route, value="", send_to=None):
  146. """发送消息,send_to 使用 userID"""
  147. with self.ws_lock:
  148. if not self.ws:
  149. return False
  150. try:
  151. # 检查连接状态
  152. if self.ws.sock and self.ws.sock.connected:
  153. msg = {
  154. "route": route,
  155. "type": self.type_str,
  156. "userID": self.user_id,
  157. "id": send_to, # send_to 是 userID
  158. "value": value
  159. }
  160. self.ws.send(json.dumps(msg))
  161. return True
  162. else:
  163. return False
  164. except Exception as e:
  165. print(f"发送消息错误: {e}")
  166. return False
  167. def get_list(self):
  168. """获取在线客户端列表"""
  169. return self.send("getList")
  170. def stop(self):
  171. """停止连接"""
  172. self.is_running = False
  173. if self.ws:
  174. try:
  175. self.ws.close()
  176. except:
  177. pass
  178. # ==================== 登录线程 ====================
  179. class LoginThread(QThread):
  180. finished = pyqtSignal(dict)
  181. error = pyqtSignal(str)
  182. def __init__(self, username, password):
  183. super().__init__()
  184. self.username = username
  185. self.password = password
  186. def run(self):
  187. try:
  188. md5_password = hashlib.md5(self.password.encode('utf-8')).hexdigest()
  189. 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"
  190. response = requests.get(url, timeout=10)
  191. response.encoding = 'utf-8'
  192. if response.status_code == 200:
  193. data = response.json()
  194. if data.get('rspCode') == '1':
  195. self.finished.emit(data)
  196. else:
  197. self.error.emit(f"登录失败: {data.get('msg', '未知错误')}")
  198. else:
  199. self.error.emit(f"请求失败: HTTP {response.status_code}")
  200. except Exception as e:
  201. self.error.emit(f"登录异常: {str(e)}")
  202. # ==================== 查询车辆线程 ====================
  203. class QueryThread(QThread):
  204. finished = pyqtSignal(dict)
  205. error = pyqtSignal(str)
  206. def __init__(self, session_id):
  207. super().__init__()
  208. self.session_id = session_id
  209. def run(self):
  210. try:
  211. url = f"http://119.91.58.175:9999/gps-web/api/get_gps_r.jsp?sessionId={self.session_id}&teamId=&carIds=&simple=false"
  212. response = requests.get(url, timeout=15)
  213. response.encoding = 'utf-8'
  214. if response.status_code == 200:
  215. data = response.json()
  216. if data.get('rspCode') == '1':
  217. self.finished.emit(data)
  218. else:
  219. self.error.emit(f"查询失败: {data.get('msg', '未知错误')}")
  220. else:
  221. self.error.emit(f"请求失败: HTTP {response.status_code}")
  222. except Exception as e:
  223. self.error.emit(f"查询异常: {str(e)}")
  224. # ==================== 查询区域线程 ====================
  225. class RegionQueryThread(QThread):
  226. finished = pyqtSignal(dict)
  227. error = pyqtSignal(str)
  228. def __init__(self, session_id):
  229. super().__init__()
  230. self.session_id = session_id
  231. def run(self):
  232. try:
  233. url = f"http://119.91.58.175:9999/gps-web/api/get_region_car.jsp?sessionId={self.session_id}&regionIds="
  234. response = requests.get(url, timeout=10)
  235. response.encoding = 'utf-8'
  236. if response.status_code == 200:
  237. data = response.json()
  238. if data.get('rspCode') == '1':
  239. self.finished.emit(data)
  240. else:
  241. self.error.emit(f"查询区域失败: {data.get('msg', '未知错误')}")
  242. else:
  243. self.error.emit(f"请求失败: HTTP {response.status_code}")
  244. except Exception as e:
  245. self.error.emit(f"查询异常: {str(e)}")
  246. # ==================== 主窗口 ====================
  247. class MainWindow(QMainWindow):
  248. def __init__(self):
  249. super().__init__()
  250. self.session_id = None
  251. self.username = None
  252. self.car_data = []
  253. self.car_checked_state = {} # 启用状态
  254. self.car_scan_state = {} # 扫码状态(新增)
  255. self.triggered_records = {}
  256. self.is_monitoring = False
  257. # 扫码任务相关
  258. self.scan_task_queue = [] # 待处理的车辆队列
  259. self.is_processing_scan = False # 是否正在处理扫码任务
  260. self.scan_timer = QTimer() # 定时器用于间隔执行
  261. self.scan_timer.timeout.connect(self.process_next_scan)
  262. # WebSocket相关
  263. self.ws_client = None
  264. self.online_clients = {} # userID -> 客户端信息
  265. # 文件夹路径
  266. self.folder_path = ""
  267. # 定时器
  268. self.refresh_timer = QTimer() # 10秒刷新车辆列表
  269. self.refresh_timer.timeout.connect(self.handle_query)
  270. self.region_timer = QTimer() # 10秒检测区域
  271. self.region_timer.timeout.connect(self.check_regions)
  272. # 区域相关
  273. self.regions = []
  274. self.region_cars = {}
  275. self.initUI()
  276. self.load_config()
  277. def initUI(self):
  278. self.setWindowTitle('车辆定位监控系统')
  279. self.setGeometry(100, 100, 1500, 850)
  280. central_widget = QWidget()
  281. self.setCentralWidget(central_widget)
  282. main_layout = QVBoxLayout(central_widget)
  283. # ===== 登录区域 =====
  284. login_group = QGroupBox("登录")
  285. login_layout = QHBoxLayout()
  286. login_layout.addWidget(QLabel("账号:"))
  287. self.username_input = QLineEdit()
  288. self.username_input.setText("深金川")
  289. login_layout.addWidget(self.username_input)
  290. login_layout.addWidget(QLabel("密码:"))
  291. self.password_input = QLineEdit()
  292. self.password_input.setEchoMode(QLineEdit.Password)
  293. self.password_input.setText("123456")
  294. login_layout.addWidget(self.password_input)
  295. self.login_btn = QPushButton("登录")
  296. self.login_btn.clicked.connect(self.handle_login)
  297. login_layout.addWidget(self.login_btn)
  298. self.status_label = QLabel("未登录")
  299. login_layout.addWidget(self.status_label)
  300. login_layout.addStretch()
  301. login_group.setLayout(login_layout)
  302. main_layout.addWidget(login_group)
  303. # ===== WebSocket控制 =====
  304. ws_group = QGroupBox("WebSocket控制")
  305. ws_layout = QHBoxLayout()
  306. self.ws_status_label = QLabel("未连接")
  307. ws_layout.addWidget(self.ws_status_label)
  308. self.refresh_clients_btn = QPushButton("刷新客户端列表")
  309. self.refresh_clients_btn.clicked.connect(self.refresh_client_list)
  310. self.refresh_clients_btn.setEnabled(False)
  311. ws_layout.addWidget(self.refresh_clients_btn)
  312. ws_layout.addWidget(QLabel("装车客户端:"))
  313. self.send_client_combo = QComboBox()
  314. self.send_client_combo.setMinimumWidth(120)
  315. ws_layout.addWidget(self.send_client_combo)
  316. ws_layout.addWidget(QLabel("卸车客户端:"))
  317. self.receive_client_combo = QComboBox()
  318. self.receive_client_combo.setMinimumWidth(120)
  319. ws_layout.addWidget(self.receive_client_combo)
  320. # 测试按钮
  321. self.test_send_btn = QPushButton("测试发")
  322. self.test_send_btn.clicked.connect(self.test_send)
  323. self.test_send_btn.setEnabled(False)
  324. ws_layout.addWidget(self.test_send_btn)
  325. self.test_receive_btn = QPushButton("测试收")
  326. self.test_receive_btn.clicked.connect(self.test_receive)
  327. self.test_receive_btn.setEnabled(False)
  328. ws_layout.addWidget(self.test_receive_btn)
  329. ws_layout.addStretch()
  330. ws_group.setLayout(ws_layout)
  331. main_layout.addWidget(ws_group)
  332. # ===== 文件夹选择 =====
  333. folder_group = QGroupBox("文件路径设置")
  334. folder_layout = QHBoxLayout()
  335. folder_layout.addWidget(QLabel("选择文件夹:"))
  336. self.folder_path_input = QLineEdit()
  337. self.folder_path_input.setPlaceholderText("请选择文件夹路径")
  338. self.folder_path_input.setMinimumWidth(400)
  339. folder_layout.addWidget(self.folder_path_input)
  340. self.browse_folder_btn = QPushButton("浏览文件夹")
  341. self.browse_folder_btn.clicked.connect(self.browse_folder)
  342. folder_layout.addWidget(self.browse_folder_btn)
  343. self.clear_folder_btn = QPushButton("清空")
  344. self.clear_folder_btn.clicked.connect(self.clear_folder)
  345. folder_layout.addWidget(self.clear_folder_btn)
  346. folder_layout.addStretch()
  347. folder_group.setLayout(folder_layout)
  348. main_layout.addWidget(folder_group)
  349. # ===== 区域监控设置 =====
  350. region_group = QGroupBox("区域监控设置")
  351. region_layout = QGridLayout()
  352. region_layout.addWidget(QLabel("装车区域:"), 0, 0)
  353. self.load_region_combo = QComboBox()
  354. self.load_region_combo.setMinimumWidth(150)
  355. region_layout.addWidget(self.load_region_combo, 0, 1)
  356. region_layout.addWidget(QLabel("卸车区域:"), 0, 2)
  357. self.unload_region_combo = QComboBox()
  358. self.unload_region_combo.setMinimumWidth(150)
  359. region_layout.addWidget(self.unload_region_combo, 0, 3)
  360. self.refresh_region_btn = QPushButton("刷新区域")
  361. self.refresh_region_btn.clicked.connect(self.load_regions)
  362. self.refresh_region_btn.setEnabled(False)
  363. region_layout.addWidget(self.refresh_region_btn, 0, 4)
  364. self.start_btn = QPushButton("启动监控")
  365. self.start_btn.clicked.connect(self.start_monitoring)
  366. self.start_btn.setEnabled(False)
  367. region_layout.addWidget(self.start_btn, 0, 5)
  368. self.stop_btn = QPushButton("停止监控")
  369. self.stop_btn.clicked.connect(self.stop_monitoring)
  370. self.stop_btn.setEnabled(False)
  371. region_layout.addWidget(self.stop_btn, 0, 6)
  372. region_layout.addWidget(QLabel("检测间隔:10秒"), 0, 7)
  373. # 区域状态显示条
  374. self.region_status_label = QLabel("请选择区域")
  375. self.region_status_label.setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }")
  376. region_layout.addWidget(self.region_status_label, 1, 0, 1, 8)
  377. region_group.setLayout(region_layout)
  378. main_layout.addWidget(region_group)
  379. # ===== 车辆列表 =====
  380. splitter = QSplitter(Qt.Vertical)
  381. table_widget = QWidget()
  382. table_layout = QVBoxLayout(table_widget)
  383. table_layout.setContentsMargins(0, 0, 0, 0)
  384. self.table = QTableWidget()
  385. # 修改列数:增加"扫码"列和"操作"列
  386. self.table.setColumnCount(7)
  387. self.table.setHorizontalHeaderLabels(["启用", "扫码", "车队名", "车牌号", "状态", "定位地址", "操作"])
  388. self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch)
  389. self.table.setAlternatingRowColors(True)
  390. table_layout.addWidget(self.table)
  391. btn_layout = QHBoxLayout()
  392. self.select_all_btn = QPushButton("全选启用")
  393. self.select_all_btn.clicked.connect(self.select_all_enable)
  394. btn_layout.addWidget(self.select_all_btn)
  395. self.deselect_all_btn = QPushButton("取消全选启用")
  396. self.deselect_all_btn.clicked.connect(self.deselect_all_enable)
  397. btn_layout.addWidget(self.deselect_all_btn)
  398. self.select_all_scan_btn = QPushButton("全选扫码")
  399. self.select_all_scan_btn.clicked.connect(self.select_all_scan)
  400. btn_layout.addWidget(self.select_all_scan_btn)
  401. self.deselect_all_scan_btn = QPushButton("取消全选扫码")
  402. self.deselect_all_scan_btn.clicked.connect(self.deselect_all_scan)
  403. btn_layout.addWidget(self.deselect_all_scan_btn)
  404. btn_layout.addStretch()
  405. table_layout.addLayout(btn_layout)
  406. splitter.addWidget(table_widget)
  407. # ===== 日志 =====
  408. log_widget = QWidget()
  409. log_layout = QVBoxLayout(log_widget)
  410. log_layout.setContentsMargins(0, 0, 0, 0)
  411. log_layout.addWidget(QLabel("日志"))
  412. self.log_output = QTextEdit()
  413. self.log_output.setReadOnly(True)
  414. self.log_output.document().setMaximumBlockCount(500)
  415. log_layout.addWidget(self.log_output)
  416. splitter.addWidget(log_widget)
  417. splitter.setSizes([450, 300])
  418. main_layout.addWidget(splitter)
  419. self.statusBar().showMessage("就绪")
  420. self.triggered_records = {}
  421. def get_value_with_plate(self, car_plate):
  422. """生成带车牌的文件路径: 文件夹路径/车牌.jpg"""
  423. folder = self.folder_path_input.text().strip()
  424. if not folder:
  425. return ""
  426. # 确保路径格式正确
  427. folder = folder.rstrip('/\\')
  428. return f"{folder}/{car_plate}.jpg"
  429. def get_exe_path_with_plate(self, car_plate):
  430. """生成可执行文件路径: 文件夹路径/车牌号/运行程序.exe"""
  431. folder = self.folder_path_input.text().strip()
  432. if not folder:
  433. return ""
  434. # 确保路径格式正确
  435. folder = folder.rstrip('/\\')
  436. return f"{folder}/{car_plate}/运行程序.exe"
  437. # ===== 文件夹选择 =====
  438. def browse_folder(self):
  439. """浏览并选择文件夹"""
  440. folder_path = QFileDialog.getExistingDirectory(
  441. self,
  442. "选择文件夹",
  443. self.folder_path_input.text() if self.folder_path_input.text() else "",
  444. QFileDialog.ShowDirsOnly
  445. )
  446. if folder_path:
  447. self.folder_path = folder_path
  448. self.folder_path_input.setText(folder_path)
  449. self.add_log(f"📁 已选择文件夹: {folder_path}")
  450. self.save_config()
  451. def clear_folder(self):
  452. """清空文件夹路径"""
  453. self.folder_path = ""
  454. self.folder_path_input.clear()
  455. self.add_log("已清空文件夹路径")
  456. self.save_config()
  457. # ===== WebSocket连接 =====
  458. def connect_websocket(self, device_id):
  459. """连接WebSocket服务器"""
  460. if self.ws_client and self.ws_client.is_running:
  461. return
  462. self.ws_status_label.setText("连接中...")
  463. self.ws_status_label.setStyleSheet("QLabel { color: orange; }")
  464. # 创建WebSocket客户端,传入获取文件夹路径的回调
  465. self.ws_client = WebSocketClient(get_folder_callback=self.get_folder_path_for_ws)
  466. self.ws_client.connected.connect(self.on_ws_connected)
  467. self.ws_client.disconnected.connect(self.on_ws_disconnected)
  468. self.ws_client.message_received.connect(self.on_ws_message)
  469. self.ws_client.error.connect(self.on_ws_error)
  470. self.ws_client.connect(device_id)
  471. def get_folder_path_for_ws(self):
  472. """提供给WebSocket客户端的文件夹路径获取方法"""
  473. return self.folder_path_input.text().strip()
  474. def on_ws_connected(self):
  475. """WebSocket连接成功"""
  476. self.refresh_clients_btn.setEnabled(True)
  477. self.test_send_btn.setEnabled(True)
  478. self.test_receive_btn.setEnabled(True)
  479. self.ws_status_label.setText("已连接")
  480. self.ws_status_label.setStyleSheet("QLabel { color: green; }")
  481. self.add_log("✅ WebSocket连接成功")
  482. # 连接成功后获取客户端列表
  483. QTimer.singleShot(1000, self.refresh_client_list)
  484. def on_ws_disconnected(self):
  485. """WebSocket连接断开"""
  486. self.refresh_clients_btn.setEnabled(False)
  487. self.test_send_btn.setEnabled(False)
  488. self.test_receive_btn.setEnabled(False)
  489. self.ws_status_label.setText("已断开")
  490. self.ws_status_label.setStyleSheet("QLabel { color: red; }")
  491. self.add_log("⚠️ WebSocket连接断开")
  492. def on_ws_error(self, error_msg):
  493. """WebSocket错误"""
  494. self.add_log(f"❌ WebSocket错误: {error_msg}")
  495. def on_ws_message(self, data):
  496. """处理WebSocket消息"""
  497. msg_type = data.get('type')
  498. self.add_log(json.dumps(data, ensure_ascii=False))
  499. if msg_type == 'getList':
  500. # 客户端列表
  501. clients = data.get('value', [])
  502. self.on_client_list_received(clients)
  503. elif msg_type == 'userInfo':
  504. self.add_log(f"✅ 用户信息已获取: {data.get('value', {}).get('userID')}")
  505. elif msg_type == 'imgSaved':
  506. # 图片保存成功,触发扫码任务
  507. self.add_log(f"📸 图片已保存: {data.get('filepath', '')}")
  508. self.start_scan_tasks()
  509. elif msg_type == 'drive': # 收到"开车"命令
  510. # 外部WebSocket发来的开车命令
  511. car_plate = data.get('value', '')
  512. if car_plate:
  513. self.add_log(f"📤 收到外部WebSocket开车命令,车辆: {car_plate}")
  514. # 发送2001指令
  515. self.send_cmd_to_vehicle(car_plate, "2001")
  516. # 继续处理下一辆车
  517. self.process_next_scan()
  518. else:
  519. self.add_log(f"⚠️ 收到开车命令但车牌为空")
  520. def start_scan_tasks(self):
  521. """开始执行扫码任务"""
  522. # 如果正在处理,忽略新的请求
  523. if self.is_processing_scan:
  524. self.add_log("⏳ 扫码任务正在处理中,忽略本次请求")
  525. return
  526. # 获取所有勾选了扫码的车辆
  527. scan_vehicles = []
  528. for row in range(self.table.rowCount()):
  529. scan_checkbox = self.table.cellWidget(row, 1)
  530. if scan_checkbox and scan_checkbox.isChecked():
  531. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  532. if car_plate:
  533. scan_vehicles.append(car_plate)
  534. if not scan_vehicles:
  535. self.add_log("⚠️ 没有勾选扫码的车辆")
  536. return
  537. # 初始化队列
  538. self.scan_task_queue = scan_vehicles.copy()
  539. self.is_processing_scan = True
  540. self.add_log(f"🚀 开始扫码任务,共 {len(scan_vehicles)} 辆车")
  541. self.add_log(f"📋 车辆列表: {', '.join(scan_vehicles)}")
  542. # 立即处理第一个
  543. self.process_next_scan()
  544. def process_next_scan(self):
  545. """处理下一个扫码任务"""
  546. # 如果监控已停止,不再处理
  547. if not self.is_monitoring:
  548. self.add_log("⏹️ 监控已停止,取消扫码任务")
  549. self.is_processing_scan = False
  550. self.scan_task_queue = []
  551. self.current_scan_plate = None
  552. self.scan_timer.stop()
  553. return
  554. if not self.scan_task_queue:
  555. # 所有任务完成
  556. self.is_processing_scan = False
  557. self.add_log("✅ 所有扫码任务已完成")
  558. self.statusBar().showMessage("扫码任务全部完成")
  559. self.scan_timer.stop()
  560. self.current_scan_plate = None
  561. return
  562. # 取出第一个车辆
  563. car_plate = self.scan_task_queue.pop(0)
  564. self.current_scan_plate = car_plate
  565. self.add_log(f"🔄 正在处理扫码车辆: {car_plate} (剩余 {len(self.scan_task_queue)} 辆)")
  566. # 执行测试发车(标记为来自扫码任务)
  567. self.on_test_drive(car_plate, from_scan=True)
  568. # 启动定时器,1分钟后发送指令2001,然后处理下一辆
  569. # 使用 QTimer.singleShot 单次定时器,传入当前车牌
  570. self.add_log(f"⏳ 等待外部WebSocket通知发送2001指令到: {car_plate}")
  571. # QTimer.singleShot(60000, lambda: self.send_2001_and_next(car_plate))
  572. def send_2001_and_next(self, car_plate):
  573. """发送2001指令给指定车辆,然后继续处理下一个"""
  574. self.add_log(f"📤 发送指令 2001 到车辆: {car_plate}")
  575. self.send_cmd_to_vehicle(car_plate, "2001")
  576. # 发送完指令后,继续处理下一辆(立即执行,不再等待)
  577. self.process_next_scan()
  578. def check_scan_complete(self):
  579. """检查扫码任务是否完成"""
  580. if not self.scan_task_queue and self.is_processing_scan:
  581. self.is_processing_scan = False
  582. self.add_log("✅ 所有扫码任务已完成")
  583. self.statusBar().showMessage("扫码任务全部完成")
  584. def on_client_list_received(self, clients):
  585. """处理客户端列表,过滤掉 admin 为 true 的客户端"""
  586. self.online_clients = {}
  587. self.send_client_combo.clear()
  588. self.receive_client_combo.clear()
  589. # 添加默认选项
  590. self.send_client_combo.addItem("请选择装车客户端")
  591. self.receive_client_combo.addItem("请选择卸车客户端")
  592. for client in clients:
  593. # 过滤掉 admin 为 true 的客户端(这些是管理端,不是扫码客户端)
  594. if client.get('admin', False):
  595. continue
  596. client_id = client.get('id')
  597. user_id = client.get('userID')
  598. is_active = client.get('active', False)
  599. if client_id and user_id and is_active:
  600. # 使用 userID 作为键存储客户端信息
  601. self.online_clients[user_id] = client
  602. # 显示时显示 id,但存储 userID
  603. display_name = f"{client_id}"
  604. self.send_client_combo.addItem(display_name, user_id)
  605. self.receive_client_combo.addItem(display_name, user_id)
  606. self.add_log(f"发现客户端: {client_id} (userID: {user_id})")
  607. self.add_log(f"✅ 已加载 {len(self.online_clients)} 个在线扫码客户端")
  608. def refresh_client_list(self):
  609. """刷新客户端列表"""
  610. if not self.ws_client:
  611. return
  612. if not self.ws_client.is_running:
  613. self.add_log("❌ WebSocket未连接")
  614. return
  615. if self.ws_client.send("getList"):
  616. self.add_log("📡 正在获取客户端列表...")
  617. else:
  618. self.add_log("❌ 发送获取列表命令失败")
  619. def test_send(self):
  620. """测试发送(装车)"""
  621. if not self.ws_client or not self.ws_client.is_running:
  622. self.add_log("❌ WebSocket未连接")
  623. return
  624. user_id = self.send_client_combo.currentData()
  625. if not user_id:
  626. self.add_log("❌ 请选择装车客户端")
  627. return
  628. # 测试时使用一个示例车牌
  629. test_plate = "测试车牌"
  630. value = self.get_value_with_plate(test_plate)
  631. if not value:
  632. self.add_log("⚠️ 未选择文件夹,将发送空值")
  633. if self.ws_client.send("发货", value, user_id):
  634. self.add_log(f"✅ 测试发送(装车)指令已发送到 {user_id},value: {value}")
  635. else:
  636. self.add_log("❌ 测试发送失败")
  637. def test_receive(self):
  638. """测试接收(卸车)"""
  639. if not self.ws_client or not self.ws_client.is_running:
  640. self.add_log("❌ WebSocket未连接")
  641. return
  642. user_id = self.receive_client_combo.currentData()
  643. if not user_id:
  644. self.add_log("❌ 请选择卸车客户端")
  645. return
  646. # 测试时使用一个示例车牌
  647. test_plate = "测试车牌"
  648. value = self.get_value_with_plate(test_plate)
  649. if not value:
  650. self.add_log("⚠️ 未选择文件夹,将发送空值")
  651. if self.ws_client.send("收货", value, user_id):
  652. self.add_log(f"✅ 测试接收(卸车)指令已发送到 {user_id},value: {value}")
  653. else:
  654. self.add_log("❌ 测试接收失败")
  655. # ===== 登录 =====
  656. def handle_login(self):
  657. username = self.username_input.text().strip()
  658. password = self.password_input.text().strip()
  659. if not username or not password:
  660. QMessageBox.warning(self, "警告", "请输入账号和密码")
  661. return
  662. self.login_btn.setEnabled(False)
  663. self.status_label.setText("登录中...")
  664. self.login_thread = LoginThread(username, password)
  665. self.login_thread.finished.connect(self.on_login_success)
  666. self.login_thread.error.connect(self.on_login_error)
  667. self.login_thread.start()
  668. def on_login_success(self, data):
  669. self.session_id = data.get('sessionId')
  670. self.username = self.username_input.text().strip()
  671. if self.session_id:
  672. self.login_btn.setEnabled(True)
  673. self.status_label.setText(f"已登录: {self.username}")
  674. self.refresh_region_btn.setEnabled(True)
  675. self.start_btn.setEnabled(True)
  676. self.statusBar().showMessage("登录成功")
  677. self.load_regions()
  678. self.handle_query()
  679. # 自动连接WebSocket,使用用户名作为ID
  680. self.connect_websocket(self.username)
  681. self.save_config()
  682. else:
  683. self.on_login_error("登录响应中没有sessionId")
  684. def on_login_error(self, error_msg):
  685. self.login_btn.setEnabled(True)
  686. self.status_label.setText("登录失败")
  687. QMessageBox.critical(self, "错误", error_msg)
  688. # ===== 加载区域 =====
  689. def load_regions(self):
  690. if not self.session_id:
  691. QMessageBox.warning(self, "警告", "请先登录")
  692. return
  693. self.refresh_region_btn.setEnabled(False)
  694. self.statusBar().showMessage("正在加载区域...")
  695. self.region_thread = RegionQueryThread(self.session_id)
  696. self.region_thread.finished.connect(self.on_regions_loaded)
  697. self.region_thread.error.connect(self.on_regions_error)
  698. self.region_thread.start()
  699. def on_regions_loaded(self, data):
  700. self.refresh_region_btn.setEnabled(True)
  701. self.regions = data.get('list', [])
  702. self.load_region_combo.clear()
  703. self.unload_region_combo.clear()
  704. self.load_region_combo.addItem("请选择装车区域")
  705. self.unload_region_combo.addItem("请选择卸车区域")
  706. for region in self.regions:
  707. region_name = region.get('regionName', '未知')
  708. region_id = region.get('regionId', '')
  709. display = f"{region_name}"
  710. self.load_region_combo.addItem(display, region_id)
  711. self.unload_region_combo.addItem(display, region_id)
  712. self.region_cars[region_id] = region.get('carList', [])
  713. self.update_region_status()
  714. self.statusBar().showMessage(f"加载了 {len(self.regions)} 个区域")
  715. self.add_log(f"✅ 加载了 {len(self.regions)} 个区域")
  716. def on_regions_error(self, error_msg):
  717. self.refresh_region_btn.setEnabled(True)
  718. QMessageBox.critical(self, "错误", error_msg)
  719. def update_region_status(self):
  720. """更新区域状态显示条"""
  721. load_text = self.load_region_combo.currentText() if self.load_region_combo.currentIndex() > 0 else "未选择"
  722. unload_text = self.unload_region_combo.currentText() if self.unload_region_combo.currentIndex() > 0 else "未选择"
  723. # 获取车辆数量
  724. load_cars = 0
  725. unload_cars = 0
  726. if self.load_region_combo.currentIndex() > 0:
  727. region_id = self.load_region_combo.currentData()
  728. load_cars = len(self.region_cars.get(region_id, []))
  729. if self.unload_region_combo.currentIndex() > 0:
  730. region_id = self.unload_region_combo.currentData()
  731. unload_cars = len(self.region_cars.get(region_id, []))
  732. status = f"🚛 装车: {load_text} ({load_cars}辆) | 📦 卸车: {unload_text} ({unload_cars}辆)"
  733. self.region_status_label.setText(status)
  734. # ===== 查询车辆列表 =====
  735. def handle_query(self):
  736. if not self.session_id:
  737. return
  738. self.query_thread = QueryThread(self.session_id)
  739. self.query_thread.finished.connect(self.on_query_success)
  740. self.query_thread.error.connect(self.on_query_error)
  741. self.query_thread.start()
  742. def on_query_success(self, data):
  743. car_list = data.get('list', [])
  744. self.car_data = car_list
  745. self.save_check_state()
  746. self.table.setRowCount(len(car_list))
  747. for row, car in enumerate(car_list):
  748. car_plate = car.get('carPlate', '')
  749. # 第0列:启用复选框
  750. enable_checkbox = QCheckBox()
  751. enable_checkbox.setChecked(self.car_checked_state.get(car_plate, False))
  752. enable_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_enable_changed(plate, state))
  753. self.table.setCellWidget(row, 0, enable_checkbox)
  754. # 第1列:扫码复选框
  755. scan_checkbox = QCheckBox()
  756. scan_checkbox.setChecked(self.car_scan_state.get(car_plate, False))
  757. scan_checkbox.stateChanged.connect(lambda state, plate=car_plate: self.on_scan_changed(plate, state))
  758. self.table.setCellWidget(row, 1, scan_checkbox)
  759. # 第2列:车队名
  760. self.table.setItem(row, 2, QTableWidgetItem(car.get('teamName', '')))
  761. # 第3列:车牌号
  762. self.table.setItem(row, 3, QTableWidgetItem(car_plate))
  763. # 第4列:状态
  764. self.table.setItem(row, 4, QTableWidgetItem(car.get('stateCn', '')))
  765. # 第5列:定位地址
  766. self.table.setItem(row, 5, QTableWidgetItem(car.get('addr', '')))
  767. # 第6列:操作按钮(测试发车)
  768. test_btn = QPushButton("测试发车")
  769. test_btn.clicked.connect(lambda checked, plate=car_plate: self.on_test_drive(plate))
  770. self.table.setCellWidget(row, 6, test_btn)
  771. self.statusBar().showMessage(f"车辆列表更新: {len(car_list)} 辆车")
  772. def on_query_error(self, error_msg):
  773. QMessageBox.critical(self, "错误", error_msg)
  774. # ===== 表格操作 =====
  775. def save_check_state(self):
  776. """保存启用和扫码状态"""
  777. for row in range(self.table.rowCount()):
  778. # 启用复选框
  779. enable_checkbox = self.table.cellWidget(row, 0)
  780. if enable_checkbox:
  781. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  782. if car_plate:
  783. self.car_checked_state[car_plate] = enable_checkbox.isChecked()
  784. # 扫码复选框
  785. scan_checkbox = self.table.cellWidget(row, 1)
  786. if scan_checkbox:
  787. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  788. if car_plate:
  789. self.car_scan_state[car_plate] = scan_checkbox.isChecked()
  790. def on_enable_changed(self, car_plate, state):
  791. """启用复选框状态变化"""
  792. self.car_checked_state[car_plate] = (state == Qt.Checked)
  793. def on_scan_changed(self, car_plate, state):
  794. """扫码复选框状态变化"""
  795. self.car_scan_state[car_plate] = (state == Qt.Checked)
  796. def select_all_enable(self):
  797. """全选启用"""
  798. for row in range(self.table.rowCount()):
  799. checkbox = self.table.cellWidget(row, 0)
  800. if checkbox:
  801. checkbox.setChecked(True)
  802. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  803. if car_plate:
  804. self.car_checked_state[car_plate] = True
  805. def deselect_all_enable(self):
  806. """取消全选启用"""
  807. for row in range(self.table.rowCount()):
  808. checkbox = self.table.cellWidget(row, 0)
  809. if checkbox:
  810. checkbox.setChecked(False)
  811. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  812. if car_plate:
  813. self.car_checked_state[car_plate] = False
  814. def select_all_scan(self):
  815. """全选扫码"""
  816. for row in range(self.table.rowCount()):
  817. checkbox = self.table.cellWidget(row, 1)
  818. if checkbox:
  819. checkbox.setChecked(True)
  820. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  821. if car_plate:
  822. self.car_scan_state[car_plate] = True
  823. def deselect_all_scan(self):
  824. """取消全选扫码"""
  825. for row in range(self.table.rowCount()):
  826. checkbox = self.table.cellWidget(row, 1)
  827. if checkbox:
  828. checkbox.setChecked(False)
  829. car_plate = self.table.item(row, 3).text() if self.table.item(row, 3) else ''
  830. if car_plate:
  831. self.car_scan_state[car_plate] = False
  832. # ===== 测试发车功能 =====
  833. def on_test_drive(self, car_plate, from_scan=False):
  834. """测试发车按钮点击事件"""
  835. # 如果是扫码任务且监控已停止,不再执行
  836. if from_scan and not self.is_monitoring:
  837. self.add_log(f"⏹️ 监控已停止,取消测试发车: {car_plate}")
  838. return
  839. if not car_plate:
  840. if not from_scan:
  841. self.add_log("❌ 车牌号为空,无法执行测试发车")
  842. return
  843. # 获取可执行文件路径
  844. exe_path = self.get_exe_path_with_plate(car_plate)
  845. if not exe_path:
  846. if not from_scan:
  847. self.add_log(f"❌ 未选择文件夹,无法执行测试发车: {car_plate}")
  848. QMessageBox.warning(self, "警告", "请先选择文件夹路径")
  849. else:
  850. self.add_log(f"❌ 未选择文件夹,跳过: {car_plate}")
  851. # 继续处理下一个
  852. self.scan_timer.start(1000)
  853. return
  854. # 检查文件是否存在
  855. if not os.path.exists(exe_path):
  856. if not from_scan:
  857. self.add_log(f"❌ 可执行文件不存在: {exe_path}")
  858. QMessageBox.warning(self, "错误", f"可执行文件不存在:\n{exe_path}")
  859. else:
  860. self.add_log(f"❌ 可执行文件不存在,跳过: {car_plate}")
  861. # 继续处理下一个
  862. self.scan_timer.start(1000)
  863. return
  864. try:
  865. # 获取exe所在目录作为工作目录
  866. exe_dir = os.path.dirname(exe_path)
  867. if from_scan:
  868. self.add_log(f"🚀 [扫码任务] 执行测试发车: {car_plate}")
  869. else:
  870. self.add_log(f"🚀 执行测试发车: {car_plate}")
  871. self.add_log(f"📂 执行路径: {exe_path}")
  872. self.add_log(f"📁 工作目录: {exe_dir}")
  873. # 使用 ShellExecute 启动
  874. import ctypes
  875. result = ctypes.windll.shell32.ShellExecuteW(
  876. None, # 父窗口句柄
  877. "open", # 操作类型
  878. exe_path, # 要打开的程序路径
  879. "", # 命令行参数
  880. exe_dir, # 工作目录
  881. 1 # 窗口显示状态 (1=正常显示)
  882. )
  883. # ShellExecuteW 返回值 > 32 表示成功
  884. if result > 32:
  885. self.add_log(f"✅ [扫码任务] 测试发车启动成功: {car_plate}")
  886. self.statusBar().showMessage(f"测试发车已启动: {car_plate}")
  887. else:
  888. self.add_log(f"❌ [扫码任务] 启动失败: {car_plate},错误码: {result}")
  889. except Exception as e:
  890. self.add_log(f"❌ [扫码任务] 执行失败 {car_plate}: {str(e)}")
  891. # 注意:这里的定时器会在 process_next_scan 中启动,不需要在这里启动
  892. # ===== 监控 =====
  893. def start_monitoring(self):
  894. load_index = self.load_region_combo.currentIndex()
  895. unload_index = self.unload_region_combo.currentIndex()
  896. if load_index <= 0 and unload_index <= 0:
  897. QMessageBox.warning(self, "警告", "请选择装车区域和/或卸车区域")
  898. return
  899. if not self.session_id:
  900. QMessageBox.warning(self, "警告", "请先登录")
  901. return
  902. if self.send_client_combo.currentIndex() <= 0:
  903. QMessageBox.warning(self, "警告", "请选择装车客户端")
  904. return
  905. if self.receive_client_combo.currentIndex() <= 0:
  906. QMessageBox.warning(self, "警告", "请选择卸车客户端")
  907. return
  908. if not self.ws_client or not self.ws_client.is_running:
  909. QMessageBox.warning(self, "警告", "WebSocket未连接")
  910. return
  911. # 检查是否选择了文件夹
  912. if not self.folder_path_input.text().strip():
  913. reply = QMessageBox.question(
  914. self,
  915. "确认",
  916. "未选择文件夹,将发送空值,是否继续?",
  917. QMessageBox.Yes | QMessageBox.No,
  918. QMessageBox.No
  919. )
  920. if reply == QMessageBox.No:
  921. return
  922. self.is_monitoring = True
  923. self.start_btn.setEnabled(False)
  924. self.stop_btn.setEnabled(True)
  925. self.refresh_region_btn.setEnabled(False)
  926. self.triggered_records = {}
  927. load_region = self.load_region_combo.currentText() if load_index > 0 else "无"
  928. unload_region = self.unload_region_combo.currentText() if unload_index > 0 else "无"
  929. load_client = self.send_client_combo.currentText()
  930. unload_client = self.receive_client_combo.currentText()
  931. folder = self.folder_path_input.text().strip() or "无"
  932. self.add_log("=" * 50)
  933. self.add_log(f"监控启动 - 装车区域: {load_region}, 卸车区域: {unload_region}")
  934. self.add_log(f"装车客户端: {load_client}, 卸车客户端: {unload_client}")
  935. self.add_log(f"文件夹路径: {folder}")
  936. self.add_log("=" * 50)
  937. # 启动两个定时器
  938. self.refresh_timer.start(10000)
  939. self.region_timer.start(10000)
  940. self.statusBar().showMessage("监控运行中...")
  941. def stop_monitoring(self):
  942. self.is_monitoring = False
  943. self.refresh_timer.stop()
  944. self.region_timer.stop()
  945. self.start_btn.setEnabled(True)
  946. self.stop_btn.setEnabled(False)
  947. self.refresh_region_btn.setEnabled(True)
  948. # 停止扫码任务
  949. self.scan_timer.stop()
  950. self.is_processing_scan = False
  951. self.scan_task_queue = []
  952. self.current_scan_plate = None
  953. self.add_log("监控已停止")
  954. self.statusBar().showMessage("监控已停止")
  955. # ===== 区域检测 =====
  956. def check_regions(self):
  957. """检测区域车辆"""
  958. if not self.is_monitoring:
  959. return
  960. load_region_id = self.load_region_combo.currentData() if self.load_region_combo.currentIndex() > 0 else None
  961. unload_region_id = self.unload_region_combo.currentData() if self.unload_region_combo.currentIndex() > 0 else None
  962. if not load_region_id and not unload_region_id:
  963. return
  964. self.region_thread = RegionQueryThread(self.session_id)
  965. self.region_thread.finished.connect(lambda data: self.on_region_check(data, load_region_id, unload_region_id))
  966. self.region_thread.error.connect(lambda err: self.add_log(f"❌ 检测区域失败: {err}"))
  967. self.region_thread.start()
  968. def on_region_check(self, data, load_region_id, unload_region_id):
  969. """处理区域检测结果"""
  970. regions = data.get('list', [])
  971. current_time = datetime.now()
  972. self.add_log(json.dumps(regions))
  973. for region in regions:
  974. region_id = region.get('regionId')
  975. region_name = region.get('regionName', '未知')
  976. car_list = region.get('carList', [])
  977. # 更新车辆数据
  978. self.region_cars[region_id] = car_list
  979. # 检查装车区域
  980. if load_region_id and region_id == load_region_id:
  981. self.check_region_cars(region_name, car_list, 'load', current_time)
  982. # 检查卸车区域
  983. if unload_region_id and region_id == unload_region_id:
  984. self.check_region_cars(region_name, car_list, 'unload', current_time)
  985. self.update_region_status()
  986. def check_region_cars(self, region_name, car_list, task_type, current_time):
  987. for car_id in car_list:
  988. if not car_id:
  989. continue
  990. # 先初始化车牌
  991. car_plate = None
  992. car_state = None
  993. # 根据carId找车牌和状态
  994. for car in self.car_data:
  995. if str(car.get('carId')) == str(car_id):
  996. car_plate = car.get('carPlate')
  997. car_state = car.get('stateCn', '') # 获取状态
  998. break
  999. # 找不到车牌直接跳过
  1000. if not car_plate:
  1001. continue
  1002. # 状态过滤:只处理 "在线-停车" 的车辆
  1003. if "在线-停车" not in car_state:
  1004. continue
  1005. # 核心:启用过滤(必须放这里)
  1006. if not self.car_checked_state.get(car_plate, False):
  1007. continue
  1008. # 初始化记录
  1009. if car_plate not in self.triggered_records:
  1010. self.triggered_records[car_plate] = {'load': None, 'unload': None}
  1011. last_trigger = self.triggered_records[car_plate][task_type]
  1012. if last_trigger is None or (current_time - last_trigger) >= timedelta(minutes=5):
  1013. self.triggered_records[car_plate][task_type] = current_time
  1014. task_name = "装车" if task_type == 'load' else "卸车"
  1015. self.add_log(f"🚛 {car_plate} 在 {region_name} 检测到{task_name}")
  1016. self.execute_task(car_plate, task_type)
  1017. # ===== 执行任务 =====
  1018. def execute_task(self, car_plate, task_type):
  1019. """通过WebSocket执行任务"""
  1020. if not self.ws_client or not self.ws_client.is_running:
  1021. self.add_log("❌ WebSocket未连接,无法执行任务")
  1022. return
  1023. # 获取对应的客户端 userID
  1024. if task_type == 'load':
  1025. user_id = self.send_client_combo.currentData()
  1026. else:
  1027. user_id = self.receive_client_combo.currentData()
  1028. if not user_id:
  1029. self.add_log(f"❌ 未选择{task_type}客户端")
  1030. return
  1031. # 生成带车牌的文件路径: 文件夹路径/车牌.jpg
  1032. value = self.get_value_with_plate(car_plate)
  1033. if not value:
  1034. self.add_log(f"⚠️ 未选择文件夹,发送空值,车牌: {car_plate}")
  1035. else:
  1036. self.add_log(f"📁 生成文件路径: {value}")
  1037. # 发送指令
  1038. route = "发货" if task_type == 'load' else "收货"
  1039. success = self.ws_client.send(route, value, user_id)
  1040. if success:
  1041. self.add_log(f"📤 发送{route}指令到 {user_id},车牌: {car_plate},value: {value}")
  1042. else:
  1043. self.add_log(f"❌ 发送{route}指令失败")
  1044. # ===== 发送指令(手动) =====
  1045. def send_cmd_to_vehicle(self, car_plate, text_msg):
  1046. """发送指令到车辆(保留原有功能)"""
  1047. if not self.session_id or not self.username:
  1048. self.add_log("❌ 未登录,无法发送指令")
  1049. return
  1050. params = {"textMsg": text_msg, "textType": 1, "signBit": "10010000"}
  1051. url = "http://119.91.58.175:9999/gps-web/api/send_cmd.jsp"
  1052. req_params = {
  1053. "userId": self.username,
  1054. "cmdId": "500",
  1055. "cmd": "CmdSendText",
  1056. "params": json.dumps(params, ensure_ascii=False),
  1057. "sessionId": self.session_id,
  1058. "plate": car_plate
  1059. }
  1060. try:
  1061. response = requests.get(url, params=req_params, timeout=10)
  1062. response.encoding = 'utf-8'
  1063. self.add_log(f"📤 发送指令到 {car_plate}: {text_msg}")
  1064. if response.status_code == 200:
  1065. data = response.json()
  1066. code = data.get('rspCode')
  1067. if code == '0':
  1068. self.add_log(f"✅ 指令发送成功")
  1069. else:
  1070. self.add_log(f"⚠️ {data.get('rspDesc', '发送失败')}")
  1071. else:
  1072. self.add_log(f"❌ HTTP {response.status_code}")
  1073. except Exception as e:
  1074. self.add_log(f"❌ {str(e)}")
  1075. # ===== 配置 =====
  1076. def save_config(self):
  1077. config = {
  1078. 'username': self.username_input.text(),
  1079. 'folder_path': self.folder_path_input.text()
  1080. }
  1081. try:
  1082. with open('config.json', 'w', encoding='utf-8') as f:
  1083. json.dump(config, f, indent=4, ensure_ascii=False)
  1084. except:
  1085. pass
  1086. def load_config(self):
  1087. if os.path.exists('config.json'):
  1088. try:
  1089. with open('config.json', 'r', encoding='utf-8') as f:
  1090. config = json.load(f)
  1091. self.username_input.setText(config.get('username', '深金川'))
  1092. folder_path = config.get('folder_path', '')
  1093. if folder_path:
  1094. self.folder_path_input.setText(folder_path)
  1095. self.folder_path = folder_path
  1096. except:
  1097. pass
  1098. # ===== 日志 =====
  1099. def add_log(self, message, level="INFO"):
  1100. """添加日志"""
  1101. timestamp = datetime.now().strftime("%H:%M:%S")
  1102. self.log_output.append(f"[{timestamp}] {message}")
  1103. self.log_output.verticalScrollBar().setValue(
  1104. self.log_output.verticalScrollBar().maximum()
  1105. )
  1106. # ==================== 启动 ====================
  1107. def main():
  1108. app = QApplication(sys.argv)
  1109. window = MainWindow()
  1110. window.show()
  1111. sys.exit(app.exec_())
  1112. if __name__ == '__main__':
  1113. main()