405 lines
15 KiB
Python
405 lines
15 KiB
Python
|
|
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("服务已安全停止")
|