三一平台.py 59 KB

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