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("服务已安全停止")