Files
virtual_simulation_midware/midware/drivers/renode_agent.py
2026-06-16 15:40:19 +08:00

528 lines
18 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.
"""
2026/4/10
TMY
在connect_renode_19.py的基础上添加单例模式并添加总线类型映射
"""
# midware/drivers/renode_agent.py
import threading
import binascii
import subprocess
import time
import threading
import re
import statistics
from typing import Optional, Callable, List, Dict
from loguru import logger
# ======================
# 这里放你原来 connect_renode_19.py 的全部代码
# 只加 2 个东西:单例 + 总线类型映射
# ======================
# from . import DRIVER_MAPPING
# from midware.config.base_config import DEVICE_CONFIG_DICT
BUS_TYPE_MAP = {
0x11: "UART",
0x22: "CAN",
0x33: "1553B",
0x44: "NET",
0x55: "AD",
0x66: "OC",
}
# 合法的总线类型第一个字节(字符串形式,方便判断)
LEGAL_BUS_PREFIX = {"11", "22", "33", "44", "55", "66"}
class RenodeAgent:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self,
renode_path="renode",
script_path=None,
max_queue=50, # 指令队列最大长度(限流)
reconnect_retries=5, # 自动重连次数
socket_timeout=3):
if hasattr(self, "inited"):
return
self.inited = True
# ========= 你的原有代码 =========
# self.process = ...
# self.async_queue = ...
# self.start_renode()
# self.start_async_listener()
# 基础配置
self.renode_path = renode_path
self.script_path = script_path
self.process: Optional[subprocess.Popen] = None
self.lock = threading.Lock()
# 输出与异步
self.output_buffer: List[str] = []
self.async_running = False
self.async_callback: Optional[Callable[[str], None]] = None
# 限流队列
self.max_queue = max_queue
self.cmd_queue: List[str] = []
self.busy = False
# 自动重连
self.reconnect_retries = reconnect_retries
self.connected = False
# 时延统计
self.latency_stats: Dict[str, List[float]] = {
"send": [], "process": [], "total": []
}
# ====================== 超强过滤规则 ======================
self.filter_re = re.compile(
r"\(.*?\)" # 过滤 (machine) (TestPlatform) 等
r"|\d{2}:\d{2}:\d{2}\.\d+" # 过滤时间 20:15:26.1234
r"|renode>|\$|->" # 过滤提示符
r"|\x1b\[[0-9;]*m" # 过滤颜色码
r"|^\s*$" # 过滤空行
)
self.last_send_t =0.0
self.send_min_gap = 0.01 #控制下发间隔
'''
# 你原来的函数,完全不变
def send_sync(self, cmd: str, timeout=1.0):
# 你的原有逻辑
pass
def enqueue_cmd(self, cmd: str):
# 你的原有逻辑
pass
def start_async_listener(self, callback):
# 你的原有异步监听
pass
'''
def start(self) -> bool:
try:
args = [
self.renode_path,
"--console",
#"--disable-ansi-colors",
"-e", "set echo off; set line-editing off; set raw on"
]
if self.script_path:
args.extend(["-e", f"include @{self.script_path}"])
#args.extend(["-e", f"{self.script_path}"])
self.process = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
universal_newlines=True,
creationflags=subprocess.CREATE_NEW_CONSOLE #LWB
)
self.async_running = True
self.connected = True
threading.Thread(target=self._read_thread, daemon=True).start()
threading.Thread(target=self._queue_worker, daemon=True).start()
time.sleep(1)
self.clear_buffers()
print("✅ 仿真平台 启动成功")
sysbus_cmd = "logLevel 3" #3级-只显示error
response = renode.send_sync(sysbus_cmd)
return True
except Exception as e:
print(f"❌ 启动失败: {e}")
self.connected = False
return self._try_reconnect()
# -------------------------------------------------------------------------
# 后台读取线程(永远实时读取,永不堆积)
# -------------------------------------------------------------------------
def _read_thread(self):
while self.async_running and self.process.poll() is None:
try:
line = self.process.stdout.readline()
if not line:
time.sleep(0.001)
continue
cleaned = self.filter_line(line)
if not cleaned:
continue
with self.lock:
self.output_buffer.append(cleaned)
if self.async_callback:
self.async_callback(cleaned)
except Exception:
break
self.connected = False
print("⚠️ Renode 已断开")
self._try_reconnect()
# -------------------------------------------------------------------------
# 过滤垃圾输出
# -------------------------------------------------------------------------
def filter_line(self, line: str) -> Optional[str]:
res = self.filter_re.sub("", line).strip()
return res if res else None
# -------------------------------------------------------------------------
# 同步发送(带时延测量 + 限流),
# -------------------------------------------------------------------------
def send_sync(
self,
cmd: str,
timeout_ms: int = 20
) -> tuple[Optional[str], float, float, float]:
if not self.connected or not self.process:
return None, 0, 0, 0
# 限流:队列满则等待
while len(self.cmd_queue) >= self.max_queue:
time.sleep(0.001)
# self.clear_buffers() #同步发送不再清空全局输出缓冲区,缓冲区只有一部读取线程消费 2026/6/3
t0 = time.perf_counter()
# 发送
try:
self.process.stdin.write(f"{cmd}\n")
self.process.stdin.flush()
except:
return None, 0, 0, 0
t1 = time.perf_counter()
# 等待结果
timeout = time.time() + timeout_ms / 1000
while time.time() < timeout:
with self.lock:
if self.output_buffer:
res = "\n".join(self.output_buffer)
self.output_buffer.clear()
t2 = time.perf_counter()
send_lat = (t1 - t0) * 1000
proc_lat = (t2 - t1) * 1000
total_lat = (t2 - t0) * 1000
self.latency_stats["send"].append(send_lat)
self.latency_stats["process"].append(proc_lat)
self.latency_stats["total"].append(total_lat)
return res, send_lat, proc_lat, total_lat
time.sleep(0.001)
return None, 0, 0, 0
'''
# -------------------------------------------------------------------------
# 同步发送(有时延测量 + 限流,不等待回复),
# 2026/6/4
# -------------------------------------------------------------------------
def send_sync(
self,
cmd: str,
timeout_ms: int = 20
) -> tuple[Optional[str], float, float, float]:
if not self.connected or not self.process:
return None,0,0,0
#队列限流等待
while len(self.cmd_queue) >=self.max_queue:
time.sleep(0.001)
t0 = time.perf_counter()
try:
self.process.stdin.write(f"{cmd}\n")
self.process.stdin.flush()
except:
return None,0,0,0
t1 = time.perf_counter()
#【关键改动不在阻塞轮询收应答直接退出所有应答给dispacher消费】
send_lat = (t1 - t0)*1000
return None,send_lat, 0 , send_lat
'''
# -------------------------------------------------------------------------
# 异步监听
# -------------------------------------------------------------------------
def start_async_listener(self, callback: Callable[[str], None]):
self.async_callback = callback
def clear_buffers(self):
with self.lock:
self.output_buffer.clear()
# -------------------------------------------------------------------------
# 指令队列限流(后台异步发送)
# -------------------------------------------------------------------------
def enqueue_cmd(self, cmd: str):
if len(self.cmd_queue) < self.max_queue:
self.cmd_queue.append(cmd)
'''
def _queue_worker(self): #老版本,停用 2026、6、4
while self.async_running:
if not self.connected or self.busy or not self.cmd_queue:
time.sleep(0.001)
continue
self.busy = True
cmd = self.cmd_queue.pop(0)
self.send_sync(cmd, timeout_ms=1) #150->1
self.busy = False
'''
'''
def _queue_worker(self): #新版本2026、6、4
while self.async_running:
if not self.connected or not self.cmd_queue:
time.sleep(0.001)
continue
cmd = self.cmd_queue.pop(0)
self.send_sync(cmd) #150->1
logger.info(f" _queue_worker向管道发送数据{cmd}")
'''
def _queue_worker(self): #新版本2026、6、4
while self.async_running:
now = time.time()
#时间没有到让出CPU
if now - self.last_send_t < self.send_min_gap:
time.sleep(0.0005)
continue
if not self.connected or not self.cmd_queue:
time.sleep(0.001)
continue
cmd = self.cmd_queue.pop(0)
self.send_sync(cmd) #150->1
logger.info(f" _queue_worker向管道发送数据{cmd}")
# -------------------------------------------------------------------------
# 自动重连
# -------------------------------------------------------------------------
def _try_reconnect(self) -> bool:
print(f"🔌 尝试自动重连 ({self.reconnect_retries} 次)")
for i in range(self.reconnect_retries):
self.stop()
time.sleep(1)
if self.start():
print(f"✅ 第 {i+1} 次重连成功")
return True
time.sleep(1)
print("❌ 重连全部失败")
return False
# -------------------------------------------------------------------------
# 时延统计
# -------------------------------------------------------------------------
def get_latency_report(self) -> Dict[str, float]:
def stats(arr):
if not arr: return 0,0,0
return round(statistics.mean(arr),2), round(max(arr),2), round(min(arr),2)
return {
"send_avg,max,min": stats(self.latency_stats["send"]),
"process_avg,max,min": stats(self.latency_stats["process"]),
"total_avg,max,min": stats(self.latency_stats["total"]),
}
def clear_latency(self):
for k in self.latency_stats:
self.latency_stats[k].clear()
# -------------------------------------------------------------------------
# 停止
# -------------------------------------------------------------------------
def stop(self):
self.async_running = False
self.connected = False
try:
if self.process:
self.process.stdin.write("quit\n")
self.process.stdin.flush()
time.sleep(0.2)
self.process.terminate()
except:
pass
self.process = None
print("🔌 Renode 已停止")
# midware/drivers/renode_agent.py
# 第三步:异步数据分发(完全不影响你现有架构)
#在 renode_agent.py 里加全局分发回调,直接把数据投递到你现有的驱动 recv
# def renode_async_data_dispatcher(line: str):
# try:
# # 1. 转成字节
# byte_data = binascii.unhexlify(line.strip())
# print(byte_data)
# # 2. 解析头部 3 字节
# if len(byte_data) < 3:
# return
# bus_type_id = byte_data[0]
# machine_type = byte_data[1]
# machine_id = byte_data[2]
# payload = byte_data[3:]
# # 3. 映射协议类型
# protocol = BUS_TYPE_MAP.get(bus_type_id)
# if not protocol:
# return
# # 4. 找到对应设备(根据单机编号)
# dev_name = None
# for name, cfg in DEVICE_CONFIG_DICT.items():
# if cfg.get("协议类型") == protocol and cfg.get("单机编号") == machine_id:
# dev_name = name
# break
# if not dev_name:
# return
# # 5. 把数据 **直接放入你现有的总线接收队列**
# from midware.core.bus_udp_decoupler import bus_recv_queues
# q = bus_recv_queues[protocol]
# q.put({
# "dev_name": dev_name,
# "raw_data": payload,
# "protocol": protocol,
# })
# except Exception:
# pass
def renode_async_data_dispatcher(line: str):
try:
# -----------------------
# 🔥 延迟导入(放在函数内部,不会循环引用!)
# -----------------------
from midware.core.bus_udp_decoupler import bus_recv_queues
from midware.config.base_config import DEVICE_CONFIG_DICT
# print(f"异步监听到的数据{line}")
logger.info(
f"异步监听到的数据{line}"
)
# -----------------------
# 0.1. 基础过滤
# -----------------------
line = line.strip()
if len(line) < 6: # 至少3字节 = 6个十六进制字符
return
# -----------------------
# 0.2. 【关键】判断前2个字符是否是合法总线类型
# -----------------------
first_byte_str = line[:2]
if first_byte_str not in LEGAL_BUS_PREFIX:
# 不是我们要的总线数据 → 直接丢弃
return
# -----------------------
# 0.3. 判断整个字符串是否是合法十六进制(安全转换)
# -----------------------
def is_hex(s):
try:
int(s, 16)
return True
except:
return False
if not is_hex(line):
return
# 1. 把 Renode 返回的字符串转成字节,首先判断首字符是否为
byte_data = bytes.fromhex(line.strip())
if len(byte_data) < 3:
return
# 2. 解析 3 字节头部
bus_type_id = byte_data[0]
machine_type = byte_data[1]
machine_id = byte_data[2]
payload = byte_data[3:]
# 3. 映射协议名
protocol = BUS_TYPE_MAP.get(bus_type_id)
if not protocol:
return
# 4. 根据 协议类型 + 单机编号 → 找到设备名
dev_name = None
for name, cfg in DEVICE_CONFIG_DICT.items():
if (cfg.get("protocol") == protocol and
cfg.get("number") == machine_id):
dev_name = name
break
if not dev_name:
return
# 5. 放入你现有的接收队列(完全兼容你原来架构)
q = bus_recv_queues[protocol]
try:
q.put_nowait({
"dev_name": dev_name,
"raw_data": payload,
"protocol": protocol,
})
except:
pass
except Exception:
import traceback
traceback.print_exc()
# 在 renode_agent.py 末尾加这个函数
def start_renode_uart_redirect():
from midware.config.base_config import DEVICE_CONFIG_DICT
renode = RenodeAgent() # 单例
for dev_name, cfg in DEVICE_CONFIG_DICT.items():
tcp_local = cfg.get("tcp_local_port", 1)
tcp_bus = cfg.get("tcp_bus_port", 1)
if tcp_local != 1 and tcp_bus != 1:
term_name = f"term_{dev_name}"
cmd1 = f'emulation CreateServerSocketTerminal {tcp_bus} "{term_name}"'#临时注释2026/6/2
cmd2 = f'connector Connect sysbus.{dev_name} {term_name}'
print(f"[Renode] 配置UART重定向: {cmd1}")
renode.send_sync(cmd1, timeout_ms=2)
time.sleep(0.1)
print(f"[Renode] 连接外设: {cmd2}")
renode.send_sync(cmd2, timeout_ms=2)
time.sleep(0.2)
print("[Renode] UART重定向配置完成")
# 全局单例,全局唯一
renode = RenodeAgent(
renode_path = r"E:\simulation\1_simulation\卫星仿真平台_后端.exe", #r"E:\codes\1_simulation\卫星仿真平台_后端.exe", #r"C:\Users\PingCe\Desktop\1_simulation\卫星仿真平台_后端.exe", # Windows可写绝对路径如 C:/Renode/renode.exe
script_path = r'E:\simulation\1_simulation\generated.resc',#r'E:\codes\1_simulation\generated_771_ADC1.resc', #r'include @C:\Users\PingCe\Desktop\1_simulation\generated_771_ADC1.resc',
max_queue=25, # 最多同时排队5条指令防止压爆
reconnect_retries=5
)