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

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