407 lines
15 KiB
Python
407 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到达搅拌楼
|
||
|
||
#37振捣室,32在途中,96,98在搅拌楼
|
||
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("服务已安全停止") |