Files
gateway_plc/gateway/cache_manager.py

882 lines
38 KiB
Python
Raw Normal View History

2025-08-13 18:13:09 +08:00
import threading
import time
import logging
from snap7.util import *
import struct
2025-08-13 18:13:09 +08:00
class CacheManager:
"""PLC数据缓存管理器"""
2025-08-13 18:13:09 +08:00
def __init__(self, config, plc_manager, app=None):
"""
初始化缓存管理器
2025-08-13 18:13:09 +08:00
Args:
config: 配置对象
plc_manager: PLC管理器实例
app: 主应用程序引用用于配置重载
"""
self.plc_manager = plc_manager
self.config = config
self.app = app
self.cache = {}
self.refresh_interval = 1 # 1秒刷新一次
self.running = False
self.lock = threading.Lock()
self.thread = None
self.last_update = {} # 区域级最后更新时间
self.plc_last_connected = {} # PLC级最后连接时间
self.plc_connection_status = {} # PLC连接状态
self.logger = logging.getLogger("CacheManager")
self.init_cache()
2025-09-29 14:41:04 +08:00
2025-08-13 18:13:09 +08:00
def init_cache(self):
"""初始化缓存结构"""
for plc in self.config["plcs"]:
plc_name = plc["name"]
self.cache[plc_name] = {}
self.last_update[plc_name] = {}
self.plc_last_connected[plc_name] = 0 # 初始化为0未连接
self.plc_connection_status[plc_name] = "never_connected"
2025-08-13 18:13:09 +08:00
for area in plc["areas"]:
name = area["name"]
# 确保初始状态为断开
self.cache[plc_name][name] = {
"data": bytearray(area["size"]),
"db_number": area["db_number"],
"offset": area["offset"],
"size": area["size"],
"type": area["type"],
"structure": area.get("structure", []),
"status": "disconnected" # 初始状态为断开
}
self.last_update[plc_name][name] = 0
2025-08-13 18:13:09 +08:00
def refresh_cache(self):
"""后台线程:定期刷新缓存"""
while self.running:
start_time = time.time()
2025-08-13 18:13:09 +08:00
try:
for plc in self.config["plcs"]:
plc_name = plc["name"]
refresh_interval = plc.get("refresh_interval", 0.5)
2025-08-13 18:13:09 +08:00
client = self.plc_manager.get_plc(plc_name)
2025-08-13 18:13:09 +08:00
# 检查PLC连接状态
plc_connected = client.connected
2025-08-13 18:13:09 +08:00
# 更新PLC连接状态
with self.lock:
if plc_connected:
self.plc_last_connected[plc_name] = time.time()
self.plc_connection_status[plc_name] = "connected"
else:
if self.plc_last_connected[plc_name] == 0:
self.plc_connection_status[plc_name] = "never_connected"
else:
self.plc_connection_status[plc_name] = "disconnected"
2025-08-13 18:13:09 +08:00
# 刷新所有可读区域
for area in plc["areas"]:
if area["type"] in ["read", "read_write"]:
name = area["name"]
try:
data = client.read_db(area["db_number"], area["offset"], area["size"])
2025-08-13 18:13:09 +08:00
# 更新区域状态基于PLC连接状态和读取结果
with self.lock:
if plc_connected and data and len(data) == area["size"]:
self.cache[plc_name][name]["data"] = bytearray(data)
self.cache[plc_name][name]["status"] = "connected"
self.last_update[plc_name][name] = time.time()
else:
self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
# 如果之前有数据,保留旧数据但标记状态
if self.last_update[plc_name][name] > 0:
self.logger.info(f"PLC {plc_name} area {name} disconnected but keeping last valid data")
except Exception as e:
with self.lock:
self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name]
self.logger.warning(f"Error updating status for {plc_name}/{name}: {e}")
"""计算刷新一个PLC的时间"""
# 计算实际执行时间
execution_time = time.time() - start_time
#计算需要睡眠的时间确保总等于refresh_time
sleep_time = max(0, refresh_interval - execution_time)
time.sleep(sleep_time)
# 记录实际刷新间隔
self.logger.debug(f"plc_name: {plc_name},"
f"Cache refresh completed.Execution time: {execution_time:.3f}s,"
f"Sleep time: {sleep_time:.3f}s,"
f"Total interval: {execution_time + sleep_time:.3f}s")
2025-08-13 18:13:09 +08:00
time.sleep(self.refresh_interval)
except Exception as e:
self.logger.error(f"Error in refresh_cache: {e}")
time.sleep(self.refresh_interval)
2025-08-13 18:13:09 +08:00
def start(self):
"""启动缓存刷新线程"""
if self.running:
return
2025-08-13 18:13:09 +08:00
self.running = True
self.thread = threading.Thread(
target=self.refresh_cache,
name="CacheRefreshThread",
daemon=True
)
self.thread.start()
self.logger.info("Cache manager started")
2025-08-13 18:13:09 +08:00
def stop(self):
"""停止缓存刷新线程"""
if not self.running:
return
2025-08-13 18:13:09 +08:00
self.running = False
if self.thread:
# 等待线程结束,但设置超时防止卡死
self.thread.join(timeout=2.0)
if self.thread.is_alive():
self.logger.warning("Cache refresh thread did not terminate gracefully")
self.thread = None
self.logger.info("Cache manager stopped")
2025-08-13 18:13:09 +08:00
def get_plc_connection_status(self, plc_name):
"""获取PLC连接状态"""
with self.lock:
return self.plc_connection_status.get(plc_name, "unknown")
2025-08-13 18:13:09 +08:00
def get_last_update_time(self, plc_name, area_name):
"""获取区域数据最后更新时间"""
with self.lock:
return self.last_update.get(plc_name, {}).get(area_name, 0)
2025-08-13 18:13:09 +08:00
def get_summary(self):
"""获取缓存摘要信息"""
summary = {}
with self.lock:
for plc_name, areas in self.cache.items():
summary[plc_name] = {}
for area_name, area in areas.items():
last_update = self.last_update[plc_name][area_name]
plc_status = self.plc_connection_status.get(plc_name, "unknown")
2025-08-13 18:13:09 +08:00
# 区域状态应与PLC连接状态一致除非有有效数据
area_status = area["status"]
if plc_status == "never_connected":
area_status = "never_connected"
elif plc_status == "disconnected" and self.last_update[plc_name][area_name] == 0:
area_status = "disconnected"
2025-08-13 18:13:09 +08:00
summary[plc_name][area_name] = {
"status": area_status,
"plc_connection_status": plc_status,
"last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
"size": area["size"],
"type": area["type"]
}
return summary
2025-08-13 18:13:09 +08:00
def get_area_status(self, plc_name, area_name):
"""获取区域状态"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return {"status": "not_found", "message": "PLC or area not found"}
2025-08-13 18:13:09 +08:00
plc_status = self.plc_connection_status.get(plc_name, "unknown")
last_update = self.last_update.get(plc_name, {}).get(area_name, 0)
2025-08-13 18:13:09 +08:00
# 区域状态应与PLC连接状态一致除非有有效数据
area_status = area["status"]
if plc_status == "never_connected":
area_status = "never_connected"
elif plc_status == "disconnected" and last_update == 0:
area_status = "disconnected"
2025-08-13 18:13:09 +08:00
return {
"status": area_status,
"plc_connection_status": plc_status,
"last_update": last_update,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never",
"size": area["size"],
"type": area["type"]
}
2025-08-13 18:13:09 +08:00
def read_area(self, plc_name, area_name, offset, length):
"""单个区域读取"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
print("read area :",area)
2025-08-13 18:13:09 +08:00
if not area:
return None, "Area not found", "unknown", 0
if offset + length > area["size"]:
return None, "Offset out of bounds", "unknown", 0
2025-08-13 18:13:09 +08:00
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
2025-08-13 18:13:09 +08:00
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return None, f"PLC not connected (status: {plc_status})", plc_status, 0
2025-08-13 18:13:09 +08:00
try:
data = client.read_db(area["db_number"], area["offset"] + offset, length)
# 验证数据有效性
if data and len(data) == length:
# 更新缓存中的这部分数据
for i in range(length):
area["data"][offset + i] = data[i]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected"
2025-08-13 18:13:09 +08:00
return data, None, plc_status, update_time
else:
area["status"] = plc_status
return None, "Invalid data returned", plc_status, 0
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
return None, f"Read failed: {str(e)}", plc_status, 0
def read_area_bool(self, plc_name, area_name, offset, length):
"""单个区域读取"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return None, "Area not found", "unknown", 0
if offset + length > area["size"]:
return None, "Offset out of bounds", "unknown", 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return None, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
data = client.read_db_bool(area["db_number"], area["offset"] + offset, length)
# 验证数据有效性
if all(isinstance(val, bool) for val in data.values()):
# 按字典键顺序更新多个值
for i, val in data.items():
area["data"][offset + i] = val # 确保offset+i不越界
# area["data"][offset] = data.values
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected"
return data, None, plc_status, update_time
else:
area["status"] = plc_status
return None, "Invalid data returned", plc_status, 0
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
return None, f"Read failed: {str(e)}", plc_status, 0
2025-08-13 18:13:09 +08:00
def write_area(self, plc_name, area_name, offset, data):
"""单个区域写入"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return False, "Area not found", "unknown", 0
2025-08-13 18:13:09 +08:00
if area["type"] not in ["write", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Area is read-only", plc_status, 0
2025-08-13 18:13:09 +08:00
if offset + len(data) > area["size"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Offset out of bounds", plc_status, 0
2025-08-13 18:13:09 +08:00
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
2025-08-13 18:13:09 +08:00
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
2025-08-13 18:13:09 +08:00
try:
success = client.write_db(area["db_number"], area["offset"] + offset, data)
if success:
# 更新缓存中的这部分数据
for i in range(len(data)):
area["data"][offset + i] = data[i]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
2025-08-13 18:13:09 +08:00
return True, None, plc_status, update_time
else:
area["status"] = plc_status
return False, "Write failed", plc_status, 0
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
return False, f"Write failed: {str(e)}", plc_status, 0
def batch_write_area(self, plc_name, area_name, offset, data):
"""单个区域写入"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return False, "Area not found", "unknown", 0
if area["type"] not in ["write", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Area is read-only", plc_status, 0
if offset + len(data) > area["size"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Offset out of bounds", plc_status, 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
for i, byte in enumerate(data):
byte_data = bytes([byte])
current_offset = offset + (i * 2)
byte_value = byte_data[0]
value = bytearray(2)
if isinstance(byte_value, int):
set_int(value, 0, byte_value)
data = value
success = client.batch_write_db(area["db_number"], current_offset, data)
if success:
# 更新缓存中的这部分数据
for j in range(len(data)):
area["data"][offset + j] = data[j]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
else:
area["status"] = plc_status
return False, "Write failed", plc_status, 0
return True, None, plc_status, update_time
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
return False, f"Write failed: {str(e)}", plc_status, 0
def batch_write_bool_area(self, plc_name, area_name, offset, data):
"""单个区域写入"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return False, "Area not found", "unknown", 0
if area["type"] not in ["write", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Area is read-only", plc_status, 0
if offset + len(data) > area["size"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Offset out of bounds", plc_status, 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
value = bytearray(offset + 1)
for bit, bit_value in enumerate(data):
set_bool(value, offset, bit, bit_value)
data = value
success = client.batch_write_db_bool(area["db_number"], offset, data)
if success:
# 更新缓存中的这部分数据
for j in range(len(data)):
area["data"][offset + j] = data[j]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
else:
area["status"] = plc_status
return False, "Write failed", plc_status, 0
return True, None, plc_status, update_time
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
return False, f"Write failed: {str(e)}", plc_status, 0
def write_area_bool(self, plc_name, area_name, offset, data):
"""单个区域写入"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return False, "Area not found", "unknown", 0
if area["type"] not in ["write", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Area is read-only", plc_status, 0
if offset + len(data) > area["size"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Offset out of bounds", plc_status, 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
success = client.write_db_bool(area["db_number"], area["offset"] + offset, data)
if success:
# 更新缓存中的这部分数据
for i in range(len(data)):
area["data"][offset + i] = data[i]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
return True, None, plc_status, update_time
else:
area["status"] = plc_status
return False, "Write failed", plc_status, 0
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
return False, f"Write failed: {str(e)}", plc_status, 0
2025-08-13 18:13:09 +08:00
def batch_read(self, requests):
"""批量读取"""
results = []
for req in requests:
plc_name = req["plc_name"]
area_name = req["area_name"]
offset = req.get("offset", 0)
length = req.get("length", None)
# 获取PLC连接状态
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": 0,
"last_update_formatted": "N/A",
"message": f"PLC not connected (status: {plc_status})"
})
continue
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": 0,
"last_update_formatted": "N/A",
"message": "Area not found"
})
continue
# 如果未指定length读取整个区域
if length is None:
length = area["size"] - offset
data, error, _, update_time = self.read_area(plc_name, area_name, offset, length)
if error:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never",
"message": error
})
else:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "success",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)),
"offset": offset,
"length": length,
"data": list(data)
})
return results
def batch_write(self, requests):
"""批量写入"""
results = []
for req in requests:
plc_name = req["plc_name"]
area_name = req["area_name"]
offset = req["offset"]
data = bytes(req["data"])
# 获取PLC连接状态
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": 0,
"last_update_formatted": "N/A",
"message": f"PLC not connected (status: {plc_status})",
"offset": offset
})
continue
success, error, _, update_time = self.batch_write_area(plc_name, area_name, offset, data)
if error:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never",
"message": error,
"offset": offset
})
else:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "success",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)),
"offset": offset,
"length": len(data)
})
return results
def batch_write_bool(self, requests):
"""批量写入"""
results = []
for req in requests:
plc_name = req["plc_name"]
area_name = req["area_name"]
offset = req["offset"]
data = bytes(req["data"])
# 获取PLC连接状态
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": 0,
"last_update_formatted": "N/A",
"message": f"PLC not connected (status: {plc_status})",
"offset": offset
})
continue
success, error, _, update_time = self.batch_write_bool_area(plc_name, area_name, offset, data)
if error:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "error",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never",
"message": error,
"offset": offset
})
else:
results.append({
"plc_name": plc_name,
"area_name": area_name,
"status": "success",
"plc_connection_status": plc_status,
"last_update": update_time,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)),
"offset": offset,
"length": len(data)
})
return results
def read_generic(self, plc_name, area_name, offset, data_type, count=1):
"""通用读取接口"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return None, "Area not found", "unknown", 0
if area["type"] not in ["read", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return None, "Area is read-only", plc_status, 0
array_name = f"{plc_name}_{area_name}"
2025-09-29 14:41:04 +08:00
# 计算实际DB偏移
db_offset = area["offset"] + offset
# 确保在区域内
if data_type == 'bool':
required_size = (offset + count + 7) // 8
elif data_type in ['int', 'word']:
required_size = 2 * count
elif data_type in ['dint', 'dword', 'real']:
required_size = 4 * count
else: # byte
required_size = count
if db_offset + required_size > area["size"] or db_offset < 0:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return None, "Offset out of bounds", plc_status, 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return None, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
# 使用Snap7Client的read_generic方法
result = client.read_generic(area["db_number"], db_offset, data_type, array_name, count)
if result is None:
return None, "Read failed", plc_status, 0
# 对于bool类型需要特殊处理缓存
if data_type == 'bool':
for i in range(count):
byte_offset = offset // 8 + i // 8
bit_offset = (offset % 8) + (i % 8)
if bit_offset >= 8:
byte_offset += 1
bit_offset -= 8
# 读取当前字节值
current_byte = area["data"][byte_offset]
if result[i]:
# 设置位为1
new_byte = current_byte | (1 << bit_offset)
else:
# 设置位为0
new_byte = current_byte & ~(1 << bit_offset)
area["data"][byte_offset] = new_byte
else:
# 对于其他类型,直接更新缓存
if not isinstance(result, list):
result = [result]
if data_type == 'byte':
item_size = 1
elif data_type in ['int', 'word']:
item_size = 2
else: # dint, dword, real
item_size = 4
for i, val in enumerate(result):
item_offset = offset + i * item_size
if data_type == 'byte':
area["data"][item_offset] = val & 0xFF
elif data_type in ['int', 'word']:
# 2字节数据
packed = struct.pack(">h" if data_type == "int" else ">H", val)
for j in range(2):
area["data"][item_offset + j] = packed[j]
elif data_type in ['dint', 'dword', 'real']:
# 4字节数据
packed = struct.pack(
">l" if data_type == "dint" else
">I" if data_type == "dword" else
">f",
val
)
for j in range(4):
area["data"][item_offset + j] = packed[j]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected"
return result, None, plc_status, update_time
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}")
return None, f"Read failed: {str(e)}", plc_status, 0
def write_generic(self, plc_name, area_name, offset, data_type, value):
"""通用写入接口"""
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return False, "Area not found", "unknown", 0
if area["type"] not in ["write", "read_write"]:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Area is read-only", plc_status, 0
# 计算实际DB偏移
db_offset = area["offset"] + offset
# 确保在区域内
if data_type == 'bool':
# 确定存储这些布尔值需要多少字节。
required_size = (offset + (len(value) if isinstance(value, list) else 1) + 7) // 8
elif data_type in ['int', 'word']:
required_size = 2 * (len(value) if isinstance(value, list) else 1)
elif data_type in ['dint', 'dword', 'real']:
required_size = 4 * (len(value) if isinstance(value, list) else 1)
else: # byte
required_size = len(value) if isinstance(value, list) else 1
if db_offset + required_size > area["size"] or db_offset < 0:
plc_status = self.plc_connection_status.get(plc_name, "unknown")
return False, "Offset out of bounds", plc_status, 0
client = self.plc_manager.get_plc(plc_name)
plc_status = self.plc_connection_status.get(plc_name, "unknown")
# 如果PLC未连接直接返回错误
if plc_status != "connected":
return False, f"PLC not connected (status: {plc_status})", plc_status, 0
try:
# 使用Snap7Client的write_generic方法
success = client.write_generic(area["db_number"], db_offset, data_type, value)
if success:
# 根据数据类型更新缓存
if data_type == 'bool':
# 处理bool写入
if not isinstance(value, list):
value = [value]
for i, val in enumerate(value):
byte_offset = offset // 8 + i // 8
bit_offset = (offset % 8) + (i % 8)
if bit_offset >= 8:
byte_offset += 1
bit_offset -= 8
# 读取当前字节值
current_byte = area["data"][byte_offset]
if val:
# 设置位为1
new_byte = current_byte | (1 << bit_offset)
else:
# 设置位为0
new_byte = current_byte & ~(1 << bit_offset)
area["data"][byte_offset] = new_byte
elif data_type == 'byte':
# 处理byte写入
if not isinstance(value, list):
value = [value]
for i, val in enumerate(value):
area["data"][offset + i] = val & 0xFF
elif data_type in ['int', 'word']:
# 处理int/word写入
if not isinstance(value, list):
value = [value]
for i, val in enumerate(value):
# 2字节数据
packed = struct.pack(">h" if data_type == "int" else ">H", val)
for j in range(2):
area["data"][offset + i * 2 + j] = packed[j]
elif data_type in ['dint', 'dword', 'real']:
# 处理dint/dword/real写入
if not isinstance(value, list):
value = [value]
for i, val in enumerate(value):
# 4字节数据
packed = struct.pack(
">l" if data_type == "dint" else
">I" if data_type == "dword" else
">f",
val
)
for j in range(4):
area["data"][offset + i * 4 + j] = packed[j]
update_time = time.time()
self.last_update[plc_name][area_name] = update_time
area["status"] = "connected (last write)"
return True, None, plc_status, update_time
else:
area["status"] = plc_status
return False, "Write failed", plc_status, 0
except Exception as e:
area["status"] = plc_status
self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}")
return False, f"Write failed: {str(e)}", plc_status, 0
2025-08-13 18:13:09 +08:00
def get_parsed_data(self, plc_name, area_name):
"""获取解析后的数据"""
from data_parser import parse_data
2025-08-13 18:13:09 +08:00
with self.lock:
area = self.cache.get(plc_name, {}).get(area_name)
if not area:
return {"error": "Area not found"}
2025-08-13 18:13:09 +08:00
plc_status = self.plc_connection_status.get(plc_name, "unknown")
last_update = self.last_update.get(plc_name, {}).get(area_name, 0)
2025-08-13 18:13:09 +08:00
# 区域状态应与PLC连接状态一致除非有有效数据
area_status = area["status"]
if plc_status == "never_connected":
area_status = "never_connected"
elif plc_status == "disconnected" and last_update == 0:
area_status = "disconnected"
2025-08-13 18:13:09 +08:00
structure = area.get("structure", [])
if structure:
parsed = parse_data(area["data"], structure)
parsed["status"] = area_status
parsed["plc_connection_status"] = plc_status
parsed["last_update"] = last_update
parsed["last_update_formatted"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never"
return parsed
else:
return {
"raw_data": list(area["data"]),
"status": area_status,
"plc_connection_status": plc_status,
"last_update": last_update,
"last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never"
2025-09-29 14:41:04 +08:00
}