Files
Feeding_control_system/upper_plc.py
2025-12-12 18:00:14 +08:00

405 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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