998 lines
40 KiB
Python
998 lines
40 KiB
Python
|
|
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)
|