Files
dtm-py-all/dtmgtApp.py

998 lines
40 KiB
Python
Raw Normal View History

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) # 设置SIGINTCtrl + 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)