stage_one

This commit is contained in:
2025-12-12 18:00:14 +08:00
parent dc4ef9002e
commit b8b9679bc8
55 changed files with 6541 additions and 459 deletions

View File

@ -1,9 +1,12 @@
import socket
import threading
import time
from datetime import datetime
import binascii
from typing import Optional, Callable, Dict, Any, Set
from collections import Counter
from PySide6.QtCore import QObject
from .crc16 import crc16
from .command_hex import command_hex
@ -12,7 +15,8 @@ class rfid_service:
"""
RFID读写器服务
"""
def __init__(self, host='192.168.1.190', port=6000):
# callback_signal=Signal(int,str)
def __init__(self, host='192.168.250.67', port=6000):
"""
初始化RFID控制器
@ -34,9 +38,9 @@ class rfid_service:
# 需要过滤掉的数据(字符串)
self._filter_value = None
self.check_time_seconds = 60.0 # 采集数据时间(秒)
self.check_time_seconds = 5.0 # 采集数据时间(秒)
# 超时设置
self._connect_timeout = 5.0 # 连接超时时间(秒)
self._connect_timeout = 1.0 # 连接超时时间(秒)
self.client_socket = None
self.connected = False
#链接失败次数
@ -349,7 +353,7 @@ class rfid_service:
#endregion
def start_receiver(self, callback: Optional[Callable[[str], None]] = None)->bool:
def start_receiver(self, callback: Optional[Callable[[int,str], None]] = None)->bool:
"""
开始接收RFID推送的数据
@ -439,14 +443,21 @@ class rfid_service:
if data:
loc_str = command_hex.parse_user_data_hex(data)
print(f"收到RFID推送数据: {binascii.hexlify(data).decode()}")
raw_data = binascii.hexlify(data).decode()
print(f"收到RFID推送数据: {raw_data}")
# 保存到文件
with open('rfid_data.log', 'a') as f:
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
f.write(f"[{timestamp}] 解析数据: {loc_str}, 原始数据: {raw_data}\n")
if loc_str:
# 将数据添加到缓冲区
with self._data_lock:
if self._filter_value == loc_str:
continue
self._data_buffer.append(loc_str)
time.sleep(1)
self._pause_receive = True
self._process_collected_data()
@ -471,12 +482,15 @@ class rfid_service:
most_common_string, count = counter.most_common(1)[0]
print(f"出现次数最多的字符串是: '{most_common_string}', 出现次数: {count}")
# 使用出现次数最多的字符串作为结果传递给回调
self._callback(most_common_string)
# 使用出现次数最多的字符串作为结果传递给回调函数
# self.callback_signal.emit(1,most_common_string)
self._callback(1,most_common_string)
else:
# 空缓冲区情况
print("数据缓冲区为空")
self._callback(None)
# 使用None作为结果传递给回调函数
# self.callback_signal.emit(0,'')
self._callback(0,'')
print(f"收集了{len(self._data_buffer)}条RFID数据过滤后数据为{most_common_string}")
except Exception as e:

View File

@ -35,8 +35,8 @@ class RelayController:
self.RING: {'open': '00000000000601050000FF00', 'close': '000000000006010500000000'},
self.UPPER_TO_JBL: {'open': '00000000000601050001FF00', 'close': '000000000006010500010000'},
self.UPPER_TO_ZD: {'open': '00000000000601050002FF00', 'close': '000000000006010500020000'},
self.DOOR_LOWER_OPEN: {'open': '00000000000601050006FF00', 'close': '00000000000601050006FF00'},
self.DOOR_LOWER_CLOSE: {'open': '00000000000601050008FF00', 'close': '000000000006010500080000'},
self.DOOR_LOWER_OPEN: {'open': '00000000000601050006FF00', 'close': '000000000006010500060000'},
self.DOOR_LOWER_CLOSE: {'open': '00000000000601050008FF00', 'close':'000000000006010500080000'},
self.DOOR_UPPER_OPEN: {'open': '00000000000601050003FF00', 'close': '000000000006010500030000'},
self.DOOR_UPPER_CLOSE: {'open': '00000000000601050004FF00', 'close': '000000000006010500040000'},
self.BREAK_ARCH_UPPER: {'open': '0000000000060105000AFF00', 'close': '0000000000060105000A0000'},
@ -58,11 +58,14 @@ class RelayController:
self.BREAK_ARCH_UPPER: 3,
self.BREAK_ARCH_LOWER: 4
}
# 添加线程锁,保护对下料斗控制的并发访问
self.door_control_lock = threading.Lock()
def send_command(self, command_hex):
"""发送原始Socket命令"""
if app_set_config.debug_mode:
return None
# if app_set_config.debug_mode:
# return None
try:
byte_data = binascii.unhexlify(command_hex)
@ -102,32 +105,164 @@ class RelayController:
def control_upper_close(self):
"""控制上料斗关"""
# 关闭上料斗出砼门
self.control(self.DOOR_UPPER_OPEN, 'close')
self.control(self.DOOR_UPPER_CLOSE, 'open')
# 异步5秒后关闭
threading.Thread(target=self._close_upper_5s, daemon=True,name="close_upper_5s").start()
threading.Thread(target=self._close_upper_s, daemon=True,name="close_upper_s").start()
def control_upper_close_sync(self,duration=5):
self.control(self.DOOR_UPPER_OPEN, 'close')
self.control(self.DOOR_UPPER_CLOSE, 'open')
time.sleep(duration)
self.control(self.DOOR_UPPER_CLOSE, 'close')
def control_lower_close(self):
"""控制下料斗关"""
# 关闭下料斗出砼门
self.control(self.DOOR_LOWER_CLOSE, 'open')
thread_name = threading.current_thread().name
print(f"[{thread_name}] 尝试控制下料斗关闭")
with self.door_control_lock:
print(f"[{thread_name}] 获得下料斗控制锁,执行关闭操作")
# 关闭下料斗出砼门
self.control(self.DOOR_LOWER_OPEN, 'close')
self.control(self.DOOR_LOWER_CLOSE, 'open')
time.sleep(3)
self.control(self.DOOR_LOWER_CLOSE, 'close')
print(f"[{thread_name}] 下料斗关闭完成,释放控制锁")
# 异步5秒后关闭
threading.Thread(target=self._close_lower_5s, daemon=True,name="close_lower_5s").start()
# threading.Thread(target=self._close_lower_5s, daemon=True,name="close_lower_5s").start()
def control_upper_open_sync(self,duration):
self.control(self.DOOR_UPPER_CLOSE, 'close')
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(duration)
self.control(self.DOOR_UPPER_OPEN, 'close')
def control_upper_close_sync(self,duration):
thread_name = threading.current_thread().name
print(f"[{thread_name}] 尝试执行上料斗同步关闭,实际操作下料斗")
with self.door_control_lock:
print(f"[{thread_name}] 获得下料斗控制锁,执行同步关闭操作")
self.control(self.DOOR_UPPER_OPEN, 'close')
self.control(self.DOOR_UPPER_CLOSE, 'open')
time.sleep(duration)
self.control(self.DOOR_UPPER_CLOSE, 'close')
print(f"[{thread_name}] 同步关闭操作完成,释放控制锁")
def control_upper_open(self):
#关闭信号才能生效
self.control(self.DOOR_UPPER_CLOSE, 'close')
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.2)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.2)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
#保持8秒
time.sleep(8)
#8秒后再开5秒
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
time.sleep(0.1)
self.control(self.DOOR_UPPER_OPEN, 'open')
time.sleep(0.5)
self.control(self.DOOR_UPPER_OPEN, 'close')
def control_ring_open(self):
"""控制下料斗关"""
# 关闭下料斗出砼门
self.control(self.RING, 'open')
# 异步5秒后关闭
threading.Thread(target=self._close_ring, daemon=True,name="close_ring").start()
threading.Thread(target=self._close_ring, daemon=True,name="_close_ring").start()
def _close_upper_5s(self):
time.sleep(5)
def _close_upper_s(self):
time.sleep(16)
self.control(self.DOOR_UPPER_CLOSE, 'close')
print("上料斗关闭完成")
def _close_lower_5s(self):
time.sleep(5)
time.sleep(6)
self.control(self.DOOR_LOWER_CLOSE, 'close')
def _close_ring(self):
time.sleep(3)
self.control(self.RING, 'close')
def control_arch_lower_open(self):
"""控制下料斗关"""
# 关闭下料斗出砼门
self.control(self.BREAK_ARCH_LOWER, 'open')
# 异步5秒后关闭
threading.Thread(target=self._close_break_arch_lower, daemon=True,name="_close_break_arch_lower").start()
def _close_break_arch_lower(self):
time.sleep(3)
self.control(self.BREAK_ARCH_LOWER, 'close')
def control_arch_upper_open(self):
"""控制上料斗关"""
# 关闭下料斗出砼门
self.control(self.BREAK_ARCH_UPPER, 'open')
# 异步5秒后关闭
threading.Thread(target=self._close_break_arch_upper, daemon=True,name="_close_break_arch_upper").start()
def _close_break_arch_upper(self):
time.sleep(3)
self.control(self.BREAK_ARCH_UPPER, 'close')
def close_all(self):
"""关闭所有继电器"""
self.control(self.UPPER_TO_JBL, 'close')
self.control(self.UPPER_TO_ZD, 'close')
self.control(self.BREAK_ARCH_UPPER, 'close')
self.control(self.BREAK_ARCH_LOWER, 'close')
self.control(self.RING, 'close')
self.control(self.DOOR_LOWER_OPEN, 'close')
self.control(self.DOOR_LOWER_CLOSE, 'close')
self.control(self.DOOR_UPPER_OPEN, 'close')
self.control(self.DOOR_UPPER_CLOSE, 'close')

View File

@ -0,0 +1,184 @@
# hardware/transmitter.py
from pymodbus.exceptions import ModbusException
import socket
from config.ini_manager import ini_manager
from config.settings import app_set_config
class TransmitterController:
def __init__(self, relay_controller):
self.relay_controller = relay_controller
# 变送器配置
self.config = {
1: { # 上料斗
'slave_id': 1,
'weight_register': 0x01,
'register_count': 2
},
2: { # 下料斗
'slave_id': 2,
'weight_register': 0x01,
'register_count': 2
}
}
# 备份 modbus 读取数据)
def read_data_bak(self, transmitter_id):
"""读取变送器数据"""
try:
if transmitter_id not in self.config:
print(f"无效变送器ID: {transmitter_id}")
return None
config = self.config[transmitter_id]
if not self.relay_controller.modbus_client.connect():
print("无法连接网络继电器Modbus服务")
return None
result = self.relay_controller.modbus_client.read_holding_registers(
address=config['weight_register'],
count=config['register_count'],
slave=config['slave_id']
)
if isinstance(result, Exception):
print(f"读取变送器 {transmitter_id} 失败: {result}")
return None
# 根据图片示例,正确解析数据
if config['register_count'] == 2:
# 获取原始字节数组
raw_data = result.registers
# 组合成32位整数
weight = (raw_data[0] << 16) + raw_data[1]
weight = weight / 1000.0 # 单位转换为千克
elif config['register_count'] == 1:
weight = float(result.registers[0])
else:
print(f"不支持的寄存器数量: {config['register_count']}")
return None
print(f"变送器 {transmitter_id} 读取重量: {weight}kg")
return weight
except ModbusException as e:
print(f"Modbus通信错误: {e}")
return None
except Exception as e:
print(f"数据解析错误: {e}")
return None
finally:
self.relay_controller.modbus_client.close()
# 直接读取 变送器返回的数据并解析
def read_data(self, transmitter_id):
"""
Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗
return: 读取成功返回重量 weight: int, 失败返回 None
"""
TIMEOUT = 2 # 超时时间为 2秒
BUFFER_SIZE= 1024
IP = None
PORT = None
weight = 0
if transmitter_id == 1:
# 上料斗变送器的信息:
IP = ini_manager.upper_transmitter_ip
PORT = ini_manager.upper_transmitter_port
elif transmitter_id == 2:
# 下料斗变送器的信息:
IP = ini_manager.lower_transmitter_ip
PORT = ini_manager.lower_transmitter_port
if not IP or not PORT:
print(f"未配置变送器 {transmitter_id} 的IP或PORT")
return 0
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.settimeout(TIMEOUT)
s.connect((IP, PORT))
# print(f"连接上料斗变送器 {IP}:{PORT} 成功")
# 接收数据变送器主动推送recv即可获取数据
data = s.recv(BUFFER_SIZE)
if data:
# print(f"收到原始数据:{data}")
# 提取出完整的一个数据包 (\r\n结尾)
packet = self.get_latest_valid_packet(data)
if not packet:
print("未获取到有效数据包!!")
return None
# 解析重量
weight = self.parse_weight(packet)
else:
print("未收到设备数据")
except ConnectionRefusedError:
print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)")
except socket.timeout:
print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据")
except Exception as e:
print(f"读取异常:{e}")
# 成功返回重量int失败返回None
return weight
def get_latest_valid_packet(self, raw_data):
"""
解决TCP粘包:
从原始数据中,筛选所有有效包,返回最新的一个有效包
有效包标准: 1. 能UTF-8解码 2. 按逗号拆分≥3个字段 3. 第三个字段含数字(重量)
"""
DELIMITER = b'\r\n'
# 1. 按分隔符拆分,过滤空包
packets = [p for p in raw_data.split(DELIMITER) if p]
if not packets:
return None
valid_packets = []
for packet in packets:
try:
# 过滤无效ASCII字符只保留可见字符
valid_chars = [c for c in packet if 32 <= c <= 126]
filtered_packet = bytes(valid_chars)
# 2. 验证解码
data_str = filtered_packet.decode('utf-8').strip()
# 3. 验证字段数量
parts = data_str.split(',')
if len(parts) < 3:
continue
# 4. 验证重量字段含数字
weight_part = parts[2].strip()
if not any(char.isdigit() for char in weight_part):
continue
# 满足所有条件,加入有效包列表
valid_packets.append(packet)
except (UnicodeDecodeError, IndexError):
# 解码失败或字段异常,跳过该包
continue
# 返回最后一个有效包最新无有效包则返回None
return valid_packets[-1] if valid_packets else None
def parse_weight(self, packet_data):
"""解析重量函数:提取重量数值(如从 b'ST,NT,+0000175\r\n' 中提取 175)"""
try:
data_str = packet_data.decode('utf-8').strip()
parts = data_str.split(',')
# 确保有完整的数据包,三个字段
if len(parts) < 3:
print(f"parse_weight: 包格式错误(字段不足):{data_str}")
return None
weight_part = parts[2].strip()
return int(''.join(filter(str.isdigit, weight_part)))
except (IndexError, ValueError, UnicodeDecodeError) as e:
# print(f"数据解析失败:{e},原始数据包:{packet_data}")
return None

View File

@ -3,12 +3,19 @@ from pymodbus.exceptions import ModbusException
import socket
from config.ini_manager import ini_manager
from config.settings import app_set_config
import time
class TransmitterController:
def __init__(self, relay_controller):
self.relay_controller = relay_controller
self.test_upper_weight=5043
self.test_lower_weight=1256
self.is_start_upper=False
self.is_start_lower=False
self.start_time_upper=None
self.start_time_lower=None
# 变送器配置
self.config = {
1: { # 上料斗
@ -73,7 +80,8 @@ class TransmitterController:
self.relay_controller.modbus_client.close()
# 直接读取 变送器返回的数据并解析
def read_data(self, transmitter_id):
def read_data_normal(self, transmitter_id):
"""
Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗
return: 读取成功返回重量 weight: int, 失败返回 None
@ -82,9 +90,7 @@ class TransmitterController:
BUFFER_SIZE= 1024
IP = None
PORT = None
weight = 0
weight = 0
if transmitter_id == 1:
# 上料斗变送器的信息:
IP = ini_manager.upper_transmitter_ip
@ -97,8 +103,86 @@ class TransmitterController:
if not IP or not PORT:
print(f"未配置变送器 {transmitter_id} 的IP或PORT")
return 0
if app_set_config.debug_mode:
print(f"调试模式,未读数据({transmitter_id},IP: {IP}:{PORT})")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.settimeout(TIMEOUT)
s.connect((IP, PORT))
# print(f"连接上料斗变送器 {IP}:{PORT} 成功")
# 接收数据变送器主动推送recv即可获取数据
data = s.recv(BUFFER_SIZE)
if data:
# print(f"收到原始数据:{data}")
# 提取出完整的一个数据包 (\r\n结尾)
packet = self.get_latest_valid_packet(data)
if not packet:
print("未获取到有效数据包!!")
return None
# 解析重量
weight = self.parse_weight(packet)
else:
print("未收到设备数据")
except ConnectionRefusedError:
print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)")
except socket.timeout:
print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据")
except Exception as e:
print(f"读取异常:{e}")
# 成功返回重量int失败返回None
return weight
def read_data(self, transmitter_id):
"""
测试用模拟读取变送器数据mock
Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗
return: 读取成功返回重量 weight: int, 失败返回 None
"""
TIMEOUT = 2 # 超时时间为 2秒
BUFFER_SIZE= 1024
IP = None
PORT = None
weight = 0
if transmitter_id == 1:
# 上料斗变送器的信息:
IP = ini_manager.upper_transmitter_ip
PORT = ini_manager.upper_transmitter_port
if self.is_start_upper:
if self.start_time_upper is None:
self.start_time_upper=time.time()
weight=self.test_upper_weight-50*(time.time()-self.start_time_upper)
if weight<0:
weight=0
else:
weight=self.test_upper_weight
self.start_time_upper=None
return weight
elif transmitter_id == 2:
# 下料斗变送器的信息:
IP = ini_manager.lower_transmitter_ip
PORT = ini_manager.lower_transmitter_port
if self.is_start_lower:
if self.start_time_lower is None:
self.start_time_lower=time.time()
weight=self.test_lower_weight-50*(time.time()-self.start_time_lower)
else:
weight=self.test_lower_weight
self.start_time_lower=None
return weight
if not IP or not PORT:
print(f"未配置变送器 {transmitter_id} 的IP或PORT")
return 0
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.settimeout(TIMEOUT)

405
hardware/upper_plc.py Normal file
View File

@ -0,0 +1,405 @@
import socket
import struct
import time
import threading
import logging
from enum import Enum
from typing import Optional, Callable
class FinsServiceStatus(Enum):
"""服务状态枚举"""
DISCONNECTED = "未连接"
CONNECTING = "连接中"
CONNECTED = "已连接"
POLLING = "轮询中"
ERROR = "错误"
STOPPED = "已停止"
class FinsPoolFullError(Exception):
"""连接池已满异常"""
pass
class OmronFinsPollingService:
"""欧姆龙FINS协议数据轮询服务严格三指令版本"""
def __init__(self, plc_ip: str, plc_port: int = 9600):
"""
初始化FINS服务
Args:
plc_ip: PLC IP地址
plc_port: PLC端口默认9600
"""
self.plc_ip = plc_ip
self.plc_port = plc_port
# 服务状态
self._status = FinsServiceStatus.DISCONNECTED
self._socket: Optional[socket.socket] = None
# 轮询控制
self._polling_thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
self._polling_interval = 1.0
# 回调函数
self._data_callbacks = []
self._status_callbacks = []
# 最新数据
self._latest_data: Optional[int] = None
self._last_update_time: Optional[float] = None
# 配置日志
self._setup_logging()
def _setup_logging(self):
"""配置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger("FinsPollingService")
def _update_status(self, new_status: FinsServiceStatus, message: str = ""):
"""更新状态并触发回调"""
old_status = self._status
if old_status != new_status:
self._status = new_status
self.logger.info(f"状态变更: {old_status.value} -> {new_status.value} {message}")
for callback in self._status_callbacks:
try:
callback(old_status, new_status, message)
except Exception as e:
self.logger.error(f"状态回调执行失败: {e}")
def _send_and_receive(self, data: bytes, expected_length: int = 1024) -> bytes:
"""发送数据并接收响应"""
if not self._socket:
raise ConnectionError("Socket未连接")
# print("_send_and_receive发送:",data)
self._socket.send(data)
response = self._socket.recv(expected_length)
return response
def _check_handshake_response(self, response: bytes):
"""检查握手响应"""
if len(response) < 24:
raise ConnectionError("握手响应数据不完整")
# 检查响应头
if response[0:4] != b'FINS':
raise ConnectionError("无效的FINS响应头")
# 检查命令代码
command_code = struct.unpack('>I', response[8:12])[0]
if command_code != 0x01:
raise ConnectionError(f"握手命令代码错误: 0x{command_code:08X}")
# 检查错误代码
error_code = struct.unpack('>I', response[12:16])[0]
if error_code == 0x20:
raise FinsPoolFullError("FINS连接池已满")
elif error_code != 0x00:
raise ConnectionError(f"握手错误代码: 0x{error_code:08X}")
self.logger.info("握手成功")
def _check_query_response(self, response: bytes) -> int:
"""检查查询响应并返回数据"""
if len(response) < 30:
raise ConnectionError("查询响应数据不完整")
# 检查响应头
if response[0:4] != b'FINS':
raise ConnectionError("无效的FINS响应头")
# 检查命令代码
command_code = struct.unpack('>I', response[8:12])[0]
if command_code != 0x02:
raise ConnectionError(f"查询命令代码错误: 0x{command_code:08X}")
# 检查错误代码
error_code = struct.unpack('>I', response[12:16])[0]
if error_code != 0x00:
raise ConnectionError(f"查询错误代码: 0x{error_code:08X}")
# 提取数据字节(最后一个字节)
data_byte = response[-1]
return data_byte
def _check_logout_response(self, response: bytes):
"""检查注销响应"""
if len(response) < 16:
raise ConnectionError("注销响应数据不完整")
# 检查响应头
if response[0:4] != b'FINS':
raise ConnectionError("无效的FINS响应头")
# 检查命令代码
command_code = struct.unpack('>I', response[8:12])[0]
if command_code != 0x03:
raise ConnectionError(f"注销命令代码错误: 0x{command_code:08X}")
# 检查错误代码
error_code = struct.unpack('>I', response[12:16])[0]
if error_code != 0x02:
raise ConnectionError(f"注销错误代码: 0x{error_code:08X}")
self.logger.info("注销成功")
def connect(self) -> bool:
"""
连接到PLC并完成握手
Returns:
bool: 连接是否成功
"""
if self._status == FinsServiceStatus.CONNECTED:
self.logger.warning("已经连接到PLC")
return True
self._update_status(FinsServiceStatus.CONNECTING, "开始连接PLC")
try:
# 创建socket连接
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10.0)
self._socket.connect((self.plc_ip, self.plc_port))
self.logger.info(f"TCP连接已建立: {self.plc_ip}:{self.plc_port}")
# 指令1: 握手
# 46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC
handshake_cmd = bytes.fromhex("46 49 4E 53 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 DC")
self.logger.debug("发送握手指令")
response = self._send_and_receive(handshake_cmd, 24)
# 检查握手响应
self._check_handshake_response(response)
self._update_status(FinsServiceStatus.CONNECTED, "握手成功")
return True
except FinsPoolFullError:
self._update_status(FinsServiceStatus.ERROR, "连接池已满")
raise
except Exception as e:
self._update_status(FinsServiceStatus.ERROR, f"连接失败: {e}")
if self._socket:
self._socket.close()
self._socket = None
raise
def query_data(self) -> Optional[int]:
"""
查询PLC数据
Returns:
int: 数据值(0-255)
"""
if self._status != FinsServiceStatus.POLLING:
raise ConnectionError("未连接到PLC无法查询")
try:
# 指令2: 查询
# 46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01
query_cmd = bytes.fromhex("46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 30 00 E9 00 00 DC 00 00 01 01 B0 00 00 00 00 01")
self.logger.debug("发送查询指令")
response = self._send_and_receive(query_cmd, 1024)
# 检查查询响应并提取数据
data_byte = self._check_query_response(response)
self._latest_data = data_byte
self._last_update_time = time.time()
# 触发数据回调
binary_str = bin(data_byte)
for callback in self._data_callbacks:
try:
callback(data_byte, binary_str)
except Exception as e:
self.logger.error(f"数据回调执行失败: {e}")
self.logger.debug(f"查询成功: 数据=0x{data_byte:02X} ({binary_str})")
return data_byte
except Exception as e:
self.logger.error(f"查询失败: {e}")
raise
def disconnect(self):
"""断开连接"""
if self._socket:
try:
# 指令3: 注销
# 46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 DC E9
logout_cmd = bytes.fromhex("46 49 4E 53 00 00 00 10 00 00 00 02 00 00 00 00 00 00 00 DC 00 00 00 E9")
self.logger.debug("发送注销指令")
response = self._send_and_receive(logout_cmd, 24)
# 检查注销响应
self._check_logout_response(response)
except Exception as e:
self.logger.error(f"注销过程中出错: {e}")
finally:
self._socket.close()
self._socket = None
self._update_status(FinsServiceStatus.DISCONNECTED, "连接已关闭")
def _polling_loop(self):
"""轮询循环"""
self.logger.info("数据轮询循环启动")
while not self._stop_event.is_set():
try:
if self._status == FinsServiceStatus.CONNECTED:
self._update_status(FinsServiceStatus.POLLING, "正在查询数据")
self.query_data()
self._update_status(FinsServiceStatus.CONNECTED, "查询完成")
else:
# 尝试重新连接
try:
self.connect()
except FinsPoolFullError:
self.logger.error("连接池已满,等待后重试...")
time.sleep(5)
except Exception as e:
self.logger.error(f"连接失败: {e}, 等待后重试...")
time.sleep(2)
except Exception as e:
self.logger.error(f"轮询查询失败: {e}")
self._update_status(FinsServiceStatus.ERROR, f"查询错误: {e}")
# 查询失败不影响连接状态保持CONNECTED状态
self._update_status(FinsServiceStatus.CONNECTED, "准备下一次查询")
time.sleep(1)
# 等待轮询间隔
self._stop_event.wait(self._polling_interval)
self.logger.info("数据轮询循环停止")
def start_polling(self, interval: float = 1.0):
"""
启动数据轮询服务
Args:
interval: 轮询间隔(秒)
"""
if self._polling_thread and self._polling_thread.is_alive():
self.logger.warning("轮询服务已在运行")
return
self._polling_interval = interval
self._stop_event.clear()
# 先建立连接
try:
self.connect()
except Exception as e:
self.logger.error(f"初始连接失败: {e}")
# 继续启动轮询,轮询循环会尝试重连
# 启动轮询线程
self._polling_thread = threading.Thread(target=self._polling_loop, daemon=True)
self._polling_thread.start()
self.logger.info(f"数据轮询服务已启动,间隔: {interval}")
def stop_polling(self):
"""停止数据轮询服务"""
self.logger.info("正在停止数据轮询服务...")
self._stop_event.set()
if self._polling_thread and self._polling_thread.is_alive():
self._polling_thread.join(timeout=5.0)
self.disconnect()
self._update_status(FinsServiceStatus.STOPPED, "服务已停止")
self.logger.info("数据轮询服务已停止")
# === 公共接口 ===
def get_service_status(self) -> dict:
"""获取服务状态"""
return {
'status': self._status.value,
'is_connected': self._status == FinsServiceStatus.CONNECTED,
'is_polling': self._polling_thread and self._polling_thread.is_alive(),
'latest_data': self._latest_data,
'latest_data_binary': bin(self._latest_data) if self._latest_data is not None else None,
'last_update_time': self._last_update_time,
'plc_ip': self.plc_ip,
'plc_port': self.plc_port,
'polling_interval': self._polling_interval
}
def get_latest_data(self) -> Optional[int]:
"""获取最新数据"""
return self._latest_data
def get_latest_data_binary(self) -> Optional[str]:
"""获取最新数据的二进制表示"""
return bin(self._latest_data) if self._latest_data is not None else None
# === 回调注册接口 ===
def register_data_callback(self, callback: Callable[[int, str], None]):
"""注册数据更新回调"""
self._data_callbacks.append(callback)
def register_status_callback(self, callback: Callable[[FinsServiceStatus, FinsServiceStatus, str], None]):
"""注册状态变化回调"""
self._status_callbacks.append(callback)
def __enter__(self):
"""上下文管理器入口"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""上下文管理器出口,确保资源释放"""
self.stop_polling()
# 使用示例和测试
if __name__ == "__main__":
def on_data_update(data: int, binary: str):
#4即将振捣室5振捣室 64即将搅拌楼 66到达搅拌楼
print(f"[数据回调] 数值: 0x{data:02X} | 十进制: {data:3d} | 二进制: {binary}")
def on_status_change(old_status: FinsServiceStatus, new_status: FinsServiceStatus, message: str):
print(f"[状态回调] {old_status.value} -> {new_status.value} : {message}")
# 创建服务实例
service = OmronFinsPollingService("192.168.250.233") # 替换为实际PLC IP
# 注册回调
service.register_data_callback(on_data_update)
service.register_status_callback(on_status_change)
print("欧姆龙FINS数据轮询服务")
print("=" * 50)
try:
# 启动轮询服务每2秒查询一次
service.start_polling(interval=2.0)
# 主循环,定期显示服务状态
counter = 0
while True:
status = service.get_service_status()
counter += 1
time.sleep(1)
except KeyboardInterrupt:
print("\n\n接收到Ctrl+C正在停止服务...")
finally:
# 确保服务正确停止
service.stop_polling()
print("服务已安全停止")