import argparse import asyncio import queue import multiprocessing import signal import time import keyboard import requests import serial.tools.list_ports import sqlite3, datetime import eel import tkinter as tk import threading, os, sys, platform import serial import logging # from flask_socketio import SocketIO, emit # 改为sockets 来实现websocket from flask import Flask, request, jsonify, g, Response, send_from_directory, json from flask_cors import CORS, cross_origin import websockets import uuid # sudo systemctl restart nginx #centos 下重启nginx # gunicorn -b 0.0.0.0:5050 -w 3 -error-log ./dtmgt.log dtmgtApp:app # gunicorn -b 0.0.0.0:5050 -w 3 --error-log ./dtmgt.log --preload dtmgtApp:app # 在linux 下不使用gunicorn启动此程序,nohup python(39) dtmgtApp.py --modbusServer=xxxxx --modbusServerPort=xxxxx # 本地运行 python dtmgtApp.py --startModbusServer # pyinstaller --add-data "src;dst" xxxx.py # 打包成1个可执行文件 exe # pyinstaller --onefile --add-data "web;web" dtmgtApp.py # pyinstaller dtmgtApp.spec # 包含pyQT5的打包 20251208 更新 from pymodbus.client import ModbusSerialClient as ModbusClient # import dtMachineService as machineService # 延迟到 main() 函数中导入,以便先设置 MOCK_MODE from modbus_server import ModbusServer from multiprocessing import Process, Event from api_route import app, access_table_data, dutDirectionMode, set_machine_service from utils import print_with_timestamp initialized = False dbServer_aloned = False dtm_machines = {} dtMachineService = None machineService = None # 将在 main() 函数中导入 webview_thread = None modbus_server_obj = None webSocket_server = None websocket_connected_clients = set() # 定义全局对象 ws_message_send_queue = queue.Queue() # 全局线程安全的消息队列 ws_dispatcher_thread = None flask_server_thread = None keyboard_input_thread = None main_exit_flag = False flask_stop_event = None socketio_thread = None insert_result_thread = None # 设置 Werkzeug 日志记录器(Flask 默认的开发服务器)等级为 ERROR logging.getLogger('werkzeug').setLevel(logging.ERROR) # 连接SQLite数据库 try: dbConn = sqlite3.connect('db\dtmgtDb.db') dbCursor = dbConn.cursor() # 创建表 dbCursor.execute('''CREATE TABLE IF NOT EXISTS TEST (ID INT PRIMARY KEY NOT NULL, NAME TEXT NOT NULL, RESULT TEXT NOT NULL);''') except sqlite3.Error as e: print(f"Database error: {e}") # 或者你可以选择抛出异常,让程序停止运行 # raise e # socket IO name_space = "/ws" """ # 改为sockets 来实现websocket @socketio.on('connect', namespace=name_space) # 处理前端定时或者主动来查询试验机器数据和状态的请求 @cross_origin() def ws_connect(auth ={}): print('socket client connected', auth) return None @socketio.on('disconnect', namespace=name_space) # 处理前端定时或者主动来查询试验机器数据和状态的请求 @cross_origin() def ws_disconnect(): print('socket client disconnected') """ def handle_get_machines_value(param): # print('socket io received message: ' + param) try: param = json.loads(param) except: pass if 'machines' in param: if dtMachineService: current_time = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3] # print(f'{current_time} :getMachinesValue begin', param['machines']) result = dtMachineService.get_machines_value(param['machines']) # emit('get_machines_data', {"status": "success", "data": result}) current_time = datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3] # print(f'{current_time} :getMachinesValue end', param['machines']) return {"status": "success", "data": result} else: return {"status": "error", "msg": "machine service is not available"} # emit('get_machines_data', {"status": "error", "msg": "machine service is not available"}) else: # emit('get_machines_data', {"status": "error", "msg": "machines in param is invalid"}) return {"status": "error", "msg": "machines in param is invalid"} """ { "requestId": "f876asd98g7fds987da", "module": "dz_drop", "sfcNo":"Fxxxxxxxxx+7+350", "buckSn":"CJJ412435132B1078", "time": "2024-03-27 12:20:53", "deviceCode": "SUN-LAB1-xxx-xxx", "deviceCode": "微跌落设备", "operatorId": "2309120078", "data": "{"example_field1":"2024-03-27 12:20:53","example_field2":[0.232,0.231,0.43,0.545,0.643]}" } """ """ 下列参数data 示例: {'SN': '543009860', 'description': '常规跌落操作,样品需放正', 'dropCycles': 12, 'dropDirection': 0, 'dropHeight': 8900, 'dropItem': '正面', 'dtMachine': 'DTM-STA-00002', 'dutSN': '543009860', 'endTime': '2024-07-16 10:11:28', 'id': 111, 'key': 'DTM-TR-00001', 'name': '二型电池组件', 'phase': 'PRJ-PHASE-01', 'project': 'GV9206', 'startTime': '2024-06-14 23:15:46', 'station': '01', 'stationAssigned': '{"dtMachine":"DTM-STA-00002","station":"01"}', 'status': 'cancel', 'tester': '234'} """ """ buckSn:样品编码 project:项目代号 phase:样品阶段 deviceCode:设备编码 channel:工作位(通道) operatorId:操作员 cyclesTotal:样品总测试次数 orientation:跌落方向 direction:跌落方向编号 cycles:跌落次数 height:跌落高度 config:样品描述 startTime:测试开始时间 endTime:测试结束时间 """ def generate_request_id(): return uuid.uuid4().hex def remove_item_from_send_queque(key, value): # 从队列中获取元素,直到队列为空 global ws_message_send_queue to_be_emerge = [] while not ws_message_send_queue.empty(): try: item = ws_message_send_queue.get_nowait() # 检查 machine_sn 是否匹配 if item.get(key) != value: # 如果不匹配,将元素放回队列 ws_message_send_queue.put(item) except queue.Empty: # 队列为空,跳出循环 break def post_data_to_lmis(station, data): if data is None: return headers = {'content-type': 'application/json'} post_json = {"requestId": generate_request_id(), "module": "dz_drop", "buckSn": data.get("SN"), "deviceCode": data.get("dtMachine"), "operatorId": data.get("tester"), "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "deviceName": data.get("deviceName"), "deviceStatus": data.get("deviceStatus")} item_json = {"project": data.get("project"), "phase": data.get("phase"), "channel": data.get("station"), "startTime": data.get("startTime"), "endTime": data.get("endTime"), "orientation": data.get("dropItem"), "direction": data.get("dropDirection"), "cycles": data.get("dropCycles"), "cyclesTotal": data.get("cyclesTotal"), "height": data.get("dropHeight"), "config": data.get("description")} post_json['data'] = json.dumps(item_json); print("post data to lmis--", data, post_json) retry_count = 0 url = "http://lims.sunwoda.com/gateway/api/rel/upload/commonUploadData" # url = "http://106.52.71.204:3005/RkUpdate/OtaUpdater/dropItem" while retry_count < 5: try: response = requests.post(url, data=json.dumps(post_json), headers=headers) # 检查响应状态码 """ 上传失败异常处理 若上传失败,如网络异常、接口调用不同、未知错误等原因,最终导致数据上传失败,则使用同一个 requestId 进行重传,最多重试5次,每次间隔1分钟。 若重试过程中,收到 1501 状态码,则表示数据已上传成功,停止重传,结束请求。 """ if response.status_code == 200: # 解析 JSON 响应内容 response_json = response.json() # 获取 code 和 message code = response_json.get('code') message = response_json.get('message') print("resp", response.status_code, 'code=', code, 'message=', message) # cyx if code == 200: print((f"{post_json.get('deviceCode')} {item_json.get('channel')} 上传 " f"{post_json.get('buckSn')} {item_json.get('cycles')} finished 操作成功"), message) if code == 1501: print("数据上传成功", message) if code == 200 or code == 1501: break # 成功后跳出循环,结束线程 else: retry_count += 1 time.sleep(60) # 间隔1分钟 else: # raise Exception("接口响应状态码错误") retry_count += 1 time.sleep(60) # 间隔1分钟 except Exception as e: print(f"上传失败,原因:{e},开始第{retry_count + 1}次重试") retry_count += 1 time.sleep(60) # 间隔1分钟 else: print("重试上传5次后仍失败,结束请求") # ---------------------------------------------------------------- # 处理LMIS 数据上传 from concurrent.futures import ThreadPoolExecutor # 全局配置 UPLOAD_MAX_RETRIES = 5 UPLOAD_RETRY_INTERVAL = 60 # 秒 UPLOAD_THREAD_POOL_SIZE = 20 # 根据网络延迟调整 UPLOAD_LIMS_URL = "http://lims.sunwoda.com/gateway/api/rel/upload/commonUploadData" class LMISUploadSystem: def __init__(self): self.retry_queue = queue.PriorityQueue() # (重试时间, data, retry_count) self.executor = ThreadPoolExecutor(max_workers=UPLOAD_THREAD_POOL_SIZE) self._start_retry_monitor() self.session = requests.Session() # 复用HTTP连接 def _start_retry_monitor(self): """启动重试监控线程""" def monitor(): while True: now = time.time() try: # 检查是否有需要重试的任务 if not self.retry_queue.empty() and self.retry_queue.queue[0][0] <= now: next_time, data, retry_count = self.retry_queue.get() self.executor.submit(self._upload_data, data, retry_count) time.sleep(0.5) # 降低CPU占用 except Exception as e: print(f"重试监控异常: {e}") threading.Thread(target=monitor, daemon=True).start() def post_data(self, data): """提交数据到上传系统""" self.executor.submit(self._upload_data, data, 0) def _upload_data(self, data, retry_count): """执行实际的上传操作""" try: if data is None: return # 构建请求数据 item_json = { "project": data.get("dutProject", ""), # 使用表字段名 "phase": data.get("dutPhase", ""), # 使用表字段名 "channel": data.get("station_no", ""), # 使用表字段名 "startTime": data.get("startTime", ""), "endTime": data.get("endTime", ""), "orientation": data.get("dropItem", ""), "direction": data.get("dropDirection", 0), "cycles": data.get("dropCycles", 0), "cyclesTotal": data.get("stationDropCycles", 0), # 使用表字段名 "height": data.get("dropHeight_int", 0), # 使用表字段名 "config": data.get("description", "") } post_json = { "requestId": str(uuid.uuid4()), "module": "dz_drop", "buckSn": data.get("SN", ""), "deviceCode": data.get("machine_sn", ""), # 使用表字段名 "operatorId": data.get("tester", ""), "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "deviceName": data.get("deviceName", ""), "deviceStatus": data.get("status", ""), # 使用表字段名 "data": json.dumps(item_json) } headers = {'content-type': 'application/json'} print(f"上传数据到LMIS: {data['SN']} (尝试 {retry_count + 1})") # 使用连接池发送请求 response = self.session.post( UPLOAD_LIMS_URL, data=json.dumps(post_json), headers=headers, timeout=10 ) # 处理响应 if response.status_code == 200: response_json = response.json() code = response_json.get('code') message = response_json.get('message') if code in (200, 1501): print(f"上传成功: {data['SN']} | 代码: {code}, 消息: {message}") return # 成功结束 # 处理失败情况 raise Exception(f"上传失败: 状态码={response.status_code}, 响应码={code}, 消息={message}") except Exception as e: print(f"上传异常: {e}") if retry_count < UPLOAD_MAX_RETRIES: next_retry = time.time() + UPLOAD_RETRY_INTERVAL self.retry_queue.put((next_retry, data, retry_count + 1)) else: print(f"放弃上传: {data.get('SN')} | 已重试{UPLOAD_MAX_RETRIES}次") # 初始化上传系统 lmis_upload_system = LMISUploadSystem() # 定义一个线程安全的队列 result_to_localDB_queue = queue.Queue() """ SN TEXT, -- 测试样品序列号 status TEXT, -- 状态 startTime TEXT, -- 跌落开始时间 endTime TEXT, -- 跌落结束时间 tester TEXT, -- 测试员 description TEXT, -- 说明 id INTEGER PRIMARY KEY AUTOINCREMENT, dropDirection INTEGER, -- 跌落面(方向) dropHeight_int INTEGER, -- 跌落高度(mm) dropSpeed INTEGER, -- 跌落速度 stationDropCycles INTEGER, -- 工位跌落次数 dropCycles INTEGER, -- 样品累计已完成跌落次数 itemCurrentCycles INTEGER, -- 样品此方向跌落次数 machine_sn TEXT, -- 设备编码 station_no TEXT, -- 设备工位(通道) dutProject TEXT, -- 样品项目 dutPhase TEXT, -- 样品阶段 dutProjectType TEXT, -- 样品项目类型 dutWeeks INTEGER, -- 测试周数 dutWorkOrder TEXT -- 测试工单 """ def run_insert_result(): while True: try: insert_records = [] while not ws_message_send_queue.empty(): # 尝试从队列中获取元素,如果队列为空,则阻塞等待 result_row = result_to_localDB_queue.get() insert_records.append(result_row) if len(insert_records) > 0: # print(f"insert req results {insert_records}") print(f"insert req results {len(insert_records)}") # 执行插入数据库的操作 if dtMachineService: dtMachineService.insertTableRecords('TestReq', insert_records) # 标记任务完成 except queue.Empty: # 如果队列在超时时间内为空,则继续循环 continue time.sleep(2) def cb_insert_result(test_req_result): """回调函数 - 处理测试结果""" # 1. 放入结果队列供本地数据库写入 result_to_localDB_queue.put_nowait(test_req_result) # 2. 提交到LMIS上传系统 lmis_upload_system.post_data(test_req_result) def cb_station_result_change(station, data): if data is None: return json_data = {"status": "success", "command": "station_result_change", "data": data} data["command"] = "station_result_change" global ws_message_send_queue if ws_message_send_queue is not None: ws_message_send_queue.put(json_data) else: print("Message queue is not initialized yet!") def cb_machine_data_change(machine_sn_list, data): # 通知和station 无关的machine data,目前重点是connectState if data is None: return json_data = {"status": "success", "command": "machine_data_change", "data": data} data["command"] = "machine_data_change" if len(machine_sn_list) == 1: # 更新单台机器的数据 remove_item_from_send_queque('machine_sn', machine_sn_list[0]) json_data['machine_sn'] = machine_sn_list[0] global ws_message_send_queue if ws_message_send_queue is not None: ws_message_send_queue.put(json_data) else: print("Message queue is not initialized yet!") def run_flask_server(flask_stop_event=None): app.run(host='0.0.0.0', port=5050, threaded=True, use_reloader=False) # WebSocket 消息分发器:从队列中取出消息并发送给所有客户端 async def ws_message_send_dispatcher(): while True: # 创建一个字典来存储以 SN 为键的最新记录 latest_machines = {} # 从队列中取出所有元素 while not ws_message_send_queue.empty(): item = ws_message_send_queue.get() if item.get('command') == 'machine_data_change': # 假设每个元素都有一个 'data' 键,且 'data' 是一个字典,其中包含 'machines' 键 machines = item.get('data', {}).get('machines', []) for machine in machines: # 使用 SN 作为键来存储最新的记录 sn = machine.get('SN') # 更新字典中的记录,始终保留最新的记录 latest_machines[sn] = machine else: ws_message_send_queue.put(item) # 将字典中的值转换成一个列表 combined_machines = list(latest_machines.values()) if len(combined_machines) > 0: combine_json_data = {"status": "success", "command": "machine_data_change", "data": {'status': 'success', 'machines': combined_machines}} ws_message_send_queue.put(combine_json_data) # 如果队列中有消息,则获取消息 if not ws_message_send_queue.empty(): message = ws_message_send_queue.get() # 将消息发送给所有已连接的客户端 disconnected_clients = [] # print("ws send",len(websocket_connected_clients)) # cyx for ws in websocket_connected_clients: try: await ws.send(json.dumps(message)) except websockets.exceptions.ConnectionClosed: disconnected_clients.append(ws) # 移除断开的客户端 if disconnected_clients: for ws in disconnected_clients: try: websocket_connected_clients.discard(ws) print(f"websocket client disconnected {len(websocket_connected_clients)}") finally: pass if dtMachineService: all_machines_sn = dtMachineService.get_all_machine_sn() all_machines_data = dtMachineService.get_machines_value(all_machines_sn) await asyncio.sleep(0.1) # 防止占用过高 CPU async def websocket_server_handle(ws, path): print(f"websocket client connected {path} ") try: websocket_connected_clients.add(ws) finally: pass async for message in ws: # print("ws", message) try: msg_json = json.loads(message) if msg_json: # print("ws",msg_json) if "command" in msg_json and msg_json["command"] == "get_machines_data" and \ "data" in msg_json: result = handle_get_machines_value(msg_json['data']) result['command'] = 'get_machines_data' # print(result) # 将字典转换为 JSON 字符串 await ws.send(json.dumps(result)) except websockets.exceptions.ConnectionClosed: # 连接关闭时从集合中移除 try: websocket_connected_clients.remove(ws) finally: pass # print(f'Unexpected error: {e}') # await websocket.send(json.dumps({"message": "Received your message: " + data['message']})) def run_websockets(): global webSocket_server loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) webSocket_server = websockets.serve(websocket_server_handle, 'localhost', 5080) loop.run_until_complete(webSocket_server) # 在事件循环中启动消息分发器 loop.create_task(ws_message_send_dispatcher()) try: loop.run_forever() except KeyboardInterrupt: print("Caught keyboard interrupt. stop web sockets server") webSocket_server.close() loop.run_until_complete(webSocket_server.wait_closed()) def stop_flask_server(): requests.post('http://127.0.0.1:5050/shutdown') # 将端口号改为你的 Flask 服务器的端口号 @eel.expose def close_window(): print("Eel window closed via JavaScript.") exit_program() # This will shut down the servers and exit the program def on_webveiw_closed(): print("webview window closed via JavaScript.") exit_program() # This will shut down the servers and exit the program def run_eel(): eel.init('web') # 告诉 Eel 查找 'web' 文件夹 eel.start('dtm_entry.html', size=(3000, 2000), position=(0, 0), suppress_error=True) class H5Api: # def close_window(self): # print("Eel window closed via JavaScript.") # exit_program() # This will shut down the servers and exit the program pass # 定义 Flask-SocketIO 运行函数 def run_flask_socketio(): # socketio.run(app, host='0.0.0.0', port=5080) pass def exit_program(sig=None, frame=None): global main_exit_flag, flask_stop_event, modbus_server_obj, webview_thread, socketio_thread, flask_server_thread global webSocket_server, insert_result_thread, dtMachineService # 显示关闭提示窗口(在开始清理资源之前) closing_window = None try: ui_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'UI') if ui_path not in sys.path: sys.path.insert(0, ui_path) from ui_utils.splash_window import ClosingWindow closing_window = ClosingWindow(title="系统关闭中", message="跌落试验管理系统正在关闭,请稍候...") closing_window.show() except Exception as e: print(f"显示关闭提示窗口失败: {e}") print("="*60) print("Received SIGINT or window close, exiting...") print("="*60) main_exit_flag = True try: # 0. 停止 dtMachineService 中的 PLC 设备轮询线程 print("0. 停止 PLC 设备轮询线程...") if dtMachineService and hasattr(dtMachineService, 'dtMachineStationData'): # 停止所有机器中的 PLC 设备轮询线程 for machine in dtMachineService.dtMachineStationData: virt_plc_device = machine.get('virt_plc_device') if virt_plc_device: # 如果 PLC 设备有 plc_client 属性,则调用其 stop_polling if hasattr(virt_plc_device, 'plc_client') and virt_plc_device.plc_client: if hasattr(virt_plc_device.plc_client, 'stop_polling'): try: virt_plc_device.plc_client.stop_polling() print(f" 已停止 PLC 设备 {machine.get('SN', 'unknown')} 的轮询线程") except Exception as e: print(f" 停止 PLC 设备 {machine.get('SN', 'unknown')} 轮询失败: {e}") print(" PLC 设备轮询线程已停止") # 等待一下确保线程完全停止 time.sleep(2) print(" 等待 PLC 线程完全停止...") except Exception as error: print(f" 停止 PLC 设备轮询失败: {error}") try: # 1. 停止 Flask 服务器 print("1. 停止 Flask 服务器...") stop_flask_server() if flask_server_thread and flask_server_thread.is_alive(): flask_server_thread.join(timeout=2) print(" Flask 服务器已停止") except Exception as error: print(f" 停止 Flask 服务器失败: {error}") try: # 2. 停止 WebSocket 服务器 print("2. 停止 WebSocket 服务器...") if webSocket_server: try: # websockets.serve 返回的是一个 Server 对象,使用 close() 方法 if hasattr(webSocket_server, 'close'): webSocket_server.close() elif hasattr(webSocket_server, 'ws_server'): # 如果是包装器对象 webSocket_server.ws_server.close() except Exception as e: # WebSocket 服务器可能已经关闭,忽略错误 print(f" WebSocket 关闭日志: {e}") if socketio_thread and socketio_thread.is_alive(): socketio_thread.join(timeout=2) print(" WebSocket 服务器已停止") except Exception as error: print(f" 停止 WebSocket 服务器失败: {error}") try: # 3. 停止 Modbus 服务器及其 PLC 设备 print("3. 停止 Modbus 服务器...") if modbus_server_obj: # 停止 Modbus 服务器中的 PLC 设备轮询 if hasattr(modbus_server_obj, 'plc_devices'): for device_info in modbus_server_obj.plc_devices: device = device_info.get('device') if device and hasattr(device, 'stop_polling'): try: device.stop_polling() print(f" 已停止 Modbus Server PLC 设备 {device_info.get('com', 'unknown')} 的轮询") except Exception as e: print(f" 停止 Modbus Server PLC 设备轮询失败: {e}") # 终止 Modbus 服务器进程 modbus_server_obj.terminate() modbus_server_obj.join(timeout=3) print(" Modbus 服务器已停止") except Exception as error: print(f" 停止 Modbus 服务器失败: {error}") try: # 4. 停止数据插入线程 print("4. 停止数据插入线程...") if insert_result_thread and insert_result_thread.is_alive(): insert_result_thread.join(timeout=2) print(" 数据插入线程已停止") except Exception as error: print(f" 停止数据插入线程失败: {error}") try: # 5. 清理 dtMachineService print("5. 清理 dtMachineService...") if dtMachineService: # 如果有清理方法,调用它 if hasattr(dtMachineService, 'cleanup'): dtMachineService.cleanup() print(" dtMachineService 已清理") except Exception as error: print(f" 清理 dtMachineService 失败: {error}") print("="*60) print("所有服务已停止,程序退出") print("="*60) # 关闭提示窗口 if closing_window: try: closing_window.close() except Exception as e: print(f"关闭提示窗口失败: {e}") # 强制退出 os._exit(0) def modbus_client_test(): # 创建 Modbus 客户端实例 client = ModbusTcpClient('127.0.0.1', port=5020) # 连接到 Modbus 服务器 connection = client.connect() if connection: print("Modbus client connected successfully") # 在这里执行你的 Modbus 请求 # 例如,读取线圈状态 response = client.read_coils(13, 5) if response.isError(): print("Modbus request failed:", response) else: print("Coil states:", response.bits) response = client.read_holding_registers(185, 2, 1) if response.isError(): print("Modbus request failed:", response) else: print("Holding Registers is :", response.registers) response = client.write_registers(78, [81, 88, 89, 100]) if response.isError(): print("Modbus request failed:", response) else: print("write Registers is :", response.isError()) else: print("Modbus client connection failed") # 关闭 Modbus 客户端连接 client.close() """ post_json = {"requestId": generate_request_id(), "module": "dz_drop", "buckSn": data.get("SN"), "deviceCode": data.get("dtMachine"), "operatorId": data.get("tester"), "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} item_json = {"project": data.get("project"), "phase": data.get("phase"), "channel": data.get("station"), "startTime": data.get("startTime"), "endTime": data.get("endTime"), "orientation": data.get("dropItem"), "direction": data.get("dropDirection"), "cycles": data.get("dropCycles"), "cyclesTotal": data.get("cyclesTotal"), "height": data.get("dropHeight"), "config": data.get("description")} """ def create_lims_mock(): lims_mock = {} lims_mock['SN'] = "202407229087" lims_mock['dtMachine'] = "DTM-STA-00003" lims_mock['tester'] = "65432" lims_mock['project'] = "DTM-PRJ-00006" lims_mock['station'] = "01" lims_mock['phase'] = "D Sample TEST" lims_mock['startTime'] = "2024-06-14 23:15:46" lims_mock['endTime'] = "2024-07-13 15:55:45" lims_mock['dropItem'] = "正面" lims_mock['dropDirection'] = 0 lims_mock['dropCycles'] = 800 lims_mock['cyclesTotal'] = 2356 lims_mock['dropHeight'] = 5673 lims_mock['dropDirection'] = "" return lims_mock def main(args): global initialized, modbus_server_obj, webview_thread, socketio_thread, flask_server_thread, main_exit_flag global flask_stop_event, dtMachineService, insert_result_thread # 先显示启动提示窗口(如果需要UI) splash = None if not args.no_ui: # 立即显示启动提示窗口 ui_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'UI') if ui_path not in sys.path: sys.path.insert(0, ui_path) from ui_utils.splash_window import SplashWindow splash = SplashWindow(title="系统启动中", message="跌落试验管理系统正在加载,请稍候...") splash.show() print_with_timestamp("启动提示窗口已显示") # 设置 MOCK_MODE(必须在导入 dtMachineService 之前) if hasattr(args, 'mock') and args.mock: import modbus_plc modbus_plc.MOCK_MODE = True print_with_timestamp("启用 PLC 模拟模式", color='yellow') else: print_with_timestamp("使用真实 PLC 设备", color='green') # 在设置 MOCK_MODE 之后导入 dtMachineService global machineService import dtMachineService as machineService comm_config = { "baudrate": 9600, "stopbits": serial.STOPBITS_ONE, "parity": serial.PARITY_EVEN, "bytesize": serial.SEVENBITS } drop_register = { '01': {"height": 'D0160', "cycles": "D0200", "cyclesFinished": "D0202", "start": 'M0016', "stop": "M0008"}, '02': {"height": 'D0162', "cycles": "D0204", "cyclesFinished": "D0206", "start": 'M0017', "stop": "M0018"} } signal.signal(signal.SIGINT, exit_program) # 设置SIGINT(Ctrl + C)信号处理器 print_with_timestamp("init dtMachineService") dtMachineService = machineService.DtmMachineService(args.modbusServer, args.modbusServerPort, access_table_data, app, dutDirectionMode, cb_station_result_change, cb_machine_data_change, cb_insert_result) initialized = True set_machine_service(dtMachineService) if args.startModbusServer: # 创建并启动Modbus Server的线程 machines_config = dtMachineService.get_machine_com_config() print_with_timestamp(f"main start modbus server ") # cyx # 创建 ModbusServer 实例 # 创建同步事件 modbus_ready_event = Event() modbus_server_obj = ModbusServer(host=args.modbusServer, port=args.modbusServerPort, machines_config=machines_config, mock_mode=args.mock) # 启动 ModbusServer 进程 modbus_server_obj.start() # 等待 ModbusServer 完全启动 wait_result = modbus_ready_event.wait(timeout=4) print_with_timestamp(f"Modbus server started in a separate process. {wait_result}") initialized = True print_with_timestamp("modbus client connect to server") # cyx dtMachineService.dtm_virt_machine_client_connect() # if not wait_result: print_with_timestamp(f"等待MODBUS SERVER 启动超时", color= 'red') # 创建并启动Flask的线程 print_with_timestamp("begin init flask server") flask_stop_event = threading.Event() flask_server_thread = threading.Thread(target=run_flask_server, args=(flask_stop_event,)) flask_server_thread.daemon = True flask_server_thread.start() print_with_timestamp("init flask server over") # 创建线程并启动 socketio_thread = threading.Thread(target=run_websockets) socketio_thread.daemon = True socketio_thread.start() # 启动写入从PLC得到测试结果的thread insert_result_thread = threading.Thread(target=run_insert_result) insert_result_thread.daemon = True insert_result_thread.start() # 启动PyQt5前端界面 if not args.no_ui: print_with_timestamp("开始启动PyQt5 UI...") print_with_timestamp("正在启动界面,请稍候......") # 设置API地址(在导入UI模块之前) ui_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'UI') if ui_path not in sys.path: sys.path.insert(0, ui_path) import config as ui_config ui_config.HTTP_API_BASE_URL = f"http://127.0.0.1:{args.port or 5050}" ui_config.WEBSOCKET_URL = f"ws://127.0.0.1:5080" # 导入 UI 模块 from dtmgtUI import main as ui_main, set_on_exit_callback # 关闭启动提示窗口(在主界面启动前关闭) if splash: splash.close() print_with_timestamp("启动提示窗口已关闭") # 等待tkinter窗口完全关闭 time.sleep(0.3) # 设置退出回调函数(保持原有逻辑) def on_ui_exit(): print_with_timestamp("UI 窗口关闭,开始退出程序...") # 直接调用 exit_program,不使用额外的线程或提示窗口 exit_program() set_on_exit_callback(on_ui_exit) # 启动 UI(这会阻塞直到窗口关闭) print_with_timestamp("UI 启动中...") exit_code = ui_main() print_with_timestamp(f"UI 退出,退出码: {exit_code}") # 检查退出码 if exit_code == -1: # 用户取消登录或登录失败,exit_program 已经在 on_ui_exit 中被调用 print_with_timestamp("用户取消登录,程序已退出") else: # 正常关闭 UI 窗口,退出程序 exit_program() else: # 如果不启动UI,则保持后台运行 while not main_exit_flag: if flask_server_thread: flask_server_thread.join(1) elif socketio_thread: socketio_thread.join(1) else: pass class startArgs: def __init__(self, startModbusServer=False, modbusServer="127.0.0.1", modbusServerPort=5020): self.startModbusServer = startModbusServer self.modbusServer = modbusServer self.modbusServerPort = modbusServerPort def check_simulator_api_availability(): """ 检测FastAPI API服务是否可用 """ url = "http://127.0.0.1:8088/gateway/api/rel/upload/getconnect" try: # 发送GET请求 response = requests.get(url, timeout=5) # 检查响应状态码 if response.status_code == 200: # 检查响应内容 data = response.json() if data.get("status") == "ok" and "LMIS 模拟接收数据服务已运行" in data.get("message", ""): print("✅ API服务正常运行") print(f"响应内容: {data}") return True else: print("⚠️ API服务响应内容不符合预期") print(f"响应内容: {data}") return False else: print(f"❌ API服务返回异常状态码: {response.status_code}") return False except requests.exceptions.ConnectionError: print("❌ 无法连接到API服务 - 连接被拒绝") print("请检查服务是否已启动,以及端口号是否正确") return False except requests.exceptions.Timeout: print("❌ 连接API服务超时") return False except requests.exceptions.RequestException as e: print(f"❌ 请求异常: {e}") return False if __name__ == "__main__": multiprocessing.freeze_support() # 添加 freeze_support 以支持 Windows 的多进程 # 解析参数(需要先解析以获取 --debug-console 参数) parser = argparse.ArgumentParser(description="DTM 跌落试验管理系统") parser.add_argument('--startModbusServer', default=True, help="启动 Modbus Server", action='store_true') parser.add_argument('--modbusServer', default="127.0.0.1", help="Modbus Server 地址") parser.add_argument('--modbusServerPort', default=5020, help="Modbus Server 端口") parser.add_argument('--no-ui', action='store_true', help="不启动PyQt5前端界面") parser.add_argument('--port', type=int, default=5050, help="HTTP服务端口") parser.add_argument('--mock', action='store_true', help="启用 PLC 模拟模式") parser.add_argument('--check-simulator', action='store_true', help="检查 LMIS 模拟器 API 可用性") parser.add_argument('--debug-console', action='store_true', help="打包后仍然显示控制台窗口(调试用)") args = parser.parse_args() # 检测是否是打包后的可执行文件(没有console窗口) # 如果是PyInstaller打包的程序,且没有 --debug-console 参数,则重定向stdout/stderr到日志文件 if getattr(sys, 'frozen', False) and not args.debug_console: # 运行在打包后的可执行文件中,且未指定 debug-console log_dir = os.path.join(os.path.dirname(sys.executable), 'logs') if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, f'dtmgt_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.log') # 重定向stdout和stderr到日志文件 sys.stdout = open(log_file, 'w', encoding='utf-8', buffering=1) sys.stderr = sys.stdout print(f"=== DTM试验管理系统启动 (日志文件: {log_file}) ===") else: # 开发模式或调试模式,直接输出到控制台 if getattr(sys, 'frozen', False): print("=== DTM试验管理系统启动 (调试模式 - 控制台输出) ===") else: print("=== DTM试验管理系统启动 (开发模式) ===") # 检查是否是 multiprocessing 子进程 # 检查模拟器 API 可用性(仅在指定 --check-simulator 时) if hasattr(args, 'check_simulator') and args.check_simulator and check_simulator_api_availability(): UPLOAD_LIMS_URL = "http://127.0.0.1:8088/gateway/api/rel/upload/commonUploadData" if any(arg.startswith("--multiprocessing-fork") for arg in sys.argv): # 如果是子进程,直接调用主逻辑,跳过 argparse pass else: # 如果是主进程,正常调用主逻辑 main(args)