- 录屏: 高DPI缩放导致奇数尺寸触发libx264编码失败,宽高取偶; ffmpeg stderr写入日志 - 压力板: 新增 pressure_manager_small.py 原厂家小压力板,与现有大压力板并存 - 打包: 新增 build_backend.ps1,更新 requirements_build.txt / config.ini - 前端: Electron 本地化配置
238 lines
9.3 KiB
Python
238 lines
9.3 KiB
Python
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
import multiprocessing.shared_memory as shared_memory
|
||
import time
|
||
import zlib
|
||
from matplotlib.animation import FuncAnimation
|
||
import logging
|
||
|
||
# 本文件用于演示如何从厂家提供的共享内存中持续读取压力矩阵,
|
||
# 并实时可视化为热力图。代码仅做读取与显示,不负责写入共享内存。
|
||
|
||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||
|
||
|
||
class SharedMemoryContext:
|
||
"""共享内存上下文管理器。
|
||
|
||
作用:
|
||
- 进入 with 语句时连接共享内存。
|
||
- 退出 with 语句时自动关闭连接,避免句柄泄漏。
|
||
"""
|
||
def __init__(self, name):
|
||
self.name = name
|
||
self.shm = None
|
||
|
||
def __enter__(self):
|
||
# 按名字连接已存在的共享内存(由厂家驱动/进程创建)
|
||
self.shm = shared_memory.SharedMemory(name=self.name)
|
||
logging.info(f"Connected to shared memory: {self.name}")
|
||
return self.shm
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
# 只关闭连接,不 unlink(不能销毁由外部创建的共享内存)
|
||
if self.shm is not None:
|
||
self.shm.close()
|
||
logging.info(f"Closed shared memory: {self.name}")
|
||
|
||
|
||
|
||
|
||
|
||
def read_shared_memory_stream(name="x2_pressure"):
|
||
"""生成器函数:持续从共享内存读取数据流。
|
||
|
||
共享内存协议(按当前厂家示例):
|
||
- 前 72 字节:头部(元数据,当前示例未解析)
|
||
- 后续数据区:288 * 64 个 float32(4 字节),总计 73728 字节
|
||
- 数据矩阵形状:reshape 为 (288, 64)
|
||
|
||
Yields:
|
||
tuple(counter, event_tick, datas)
|
||
- counter: 帧序号(从 0 开始)
|
||
- event_tick: 相对启动时间(毫秒)
|
||
- datas: ndarray,shape=(288,64),dtype=float32
|
||
"""
|
||
counter = 0 # 帧计数器
|
||
start_tick = int(time.time() * 1000) # 记录开始时间戳
|
||
while True:
|
||
try:
|
||
# 每次循环使用 with 连接共享内存,确保异常时也能释放句柄
|
||
with SharedMemoryContext(name) as shm:
|
||
# 建立连接后,持续读取同一块共享内存
|
||
while True:
|
||
# 以 uint8 视图读取原始字节流(零拷贝)
|
||
buffer = np.frombuffer(shm.buf, dtype=np.uint8)
|
||
# 验证缓冲区长度是否满足“头部 + 数据区”最小要求
|
||
if buffer.nbytes < 72 + 288 * 64 * 4:
|
||
logging.warning("Data size is insufficient. Waiting for more data...")
|
||
time.sleep(0.1)
|
||
continue
|
||
|
||
# 跳过 72 字节头部,仅解析数据区并转换为 float32 矩阵
|
||
header = bytes(buffer[:72])
|
||
datas = buffer[72:72 + 288 * 64 * 4].view(np.float32).reshape((288, 64))
|
||
event_tick = int(time.time() * 1000) - start_tick # 计算相对时间戳
|
||
# 通过生成器把一帧数据交给上层消费者(可视化或业务处理)
|
||
yield counter, event_tick, datas, header
|
||
counter += 1
|
||
|
||
except FileNotFoundError:
|
||
# 共享内存尚未创建:通常是写端程序未启动
|
||
logging.warning("Waiting for shared memory...")
|
||
time.sleep(1)
|
||
except KeyboardInterrupt:
|
||
# 手动中断(Ctrl+C)时优雅退出
|
||
break
|
||
|
||
|
||
|
||
|
||
|
||
def visualize_data_stream(
|
||
data_stream,
|
||
update_interval=50,
|
||
low_percentile=5,
|
||
high_percentile=98,
|
||
gamma=1.0,
|
||
ema_alpha=0.1,
|
||
):
|
||
"""实时可视化数据流。
|
||
|
||
Args:
|
||
data_stream: 来自 read_shared_memory_stream 的生成器
|
||
update_interval: 动画刷新间隔(毫秒)
|
||
low_percentile: 低端分位数(用于抑制噪声地板)
|
||
high_percentile: 高端分位数(用于抑制少量极值,提升主体对比度)
|
||
gamma: 非线性增强系数,>1 时高压区域颜色更“深”更突出
|
||
ema_alpha: 分位数平滑系数,越小越稳定、越大越灵敏
|
||
"""
|
||
|
||
fig, ax = plt.subplots()
|
||
# 初始化一个 64x64 的空图,后续每帧覆盖更新
|
||
init_data = np.zeros((64, 64))
|
||
im = ax.imshow(init_data, cmap="jet", aspect='equal') #,interpolation='bicubic'
|
||
plt.colorbar(im) # 添加颜色条
|
||
|
||
# 使用“分位数 + 指数平滑”的动态范围,兼顾灵敏度与稳定性
|
||
vmin, vmax = None, None
|
||
last_crc = None
|
||
last_change_ts = time.time()
|
||
last_diag_ts = 0.0
|
||
frame_counter = 0
|
||
fps_window_start = time.time()
|
||
prev_u64 = {}
|
||
prev_u32 = {}
|
||
|
||
def probe_header_fields(header: bytes):
|
||
"""解析72字节头部的候选计数器字段并返回变化摘要。"""
|
||
candidates_u64 = {}
|
||
candidates_u32 = {}
|
||
for off in range(0, min(len(header), 72) - 7, 8):
|
||
candidates_u64[off] = int.from_bytes(header[off:off + 8], byteorder="little", signed=False)
|
||
for off in range(0, min(len(header), 72) - 3, 4):
|
||
candidates_u32[off] = int.from_bytes(header[off:off + 4], byteorder="little", signed=False)
|
||
|
||
changed_u64 = []
|
||
changed_u32 = []
|
||
for off, val in candidates_u64.items():
|
||
prev = prev_u64.get(off)
|
||
if prev is not None and val != prev:
|
||
delta = val - prev
|
||
changed_u64.append((off, val, delta))
|
||
prev_u64[off] = val
|
||
for off, val in candidates_u32.items():
|
||
prev = prev_u32.get(off)
|
||
if prev is not None and val != prev:
|
||
delta = val - prev
|
||
changed_u32.append((off, val, delta))
|
||
prev_u32[off] = val
|
||
return changed_u64, changed_u32
|
||
|
||
|
||
|
||
def update(frame):
|
||
nonlocal vmin, vmax, last_crc, last_change_ts, last_diag_ts, frame_counter, fps_window_start
|
||
try:
|
||
counter, event_tick, data, header = next(data_stream) # 从生成器获取最新数据
|
||
# 示例展示策略:取前 64 行并转置,得到 64x64 画面
|
||
# 说明:这只是可视化截取方式,不代表业务计算必须这样切片
|
||
data = data[:64, :].T.astype(np.float64) # 取前 64 行,再转置 → 64×64
|
||
|
||
# 1) 使用分位数而不是绝对 min/max,避免少量尖峰值“拉扁”整体颜色层次
|
||
p_low = np.percentile(data, low_percentile)
|
||
p_high = np.percentile(data, high_percentile)
|
||
if p_high <= p_low:
|
||
p_low, p_high = float(np.min(data)), float(np.max(data))
|
||
if p_high <= p_low:
|
||
p_high = p_low + 1e-9
|
||
|
||
# 2) 用 EMA 平滑动态范围,减少每帧抖动导致的闪烁
|
||
if vmin is None or vmax is None:
|
||
vmin, vmax = p_low, p_high
|
||
else:
|
||
vmin = (1 - ema_alpha) * vmin + ema_alpha * p_low
|
||
vmax = (1 - ema_alpha) * vmax + ema_alpha * p_high
|
||
if vmax <= vmin:
|
||
vmax = vmin + 1e-9
|
||
|
||
# 3) 归一化后做 gamma 增强:gamma>1 可让高压区域更快进入深色高亮区
|
||
norm = np.clip((data - vmin) / (vmax - vmin), 0.0, 1.0)
|
||
enhanced = np.power(norm, gamma)
|
||
|
||
# 颜色范围固定在 [0,1],增强后高值会更“深”更突出
|
||
im.set_data(enhanced)
|
||
im.set_clim(vmin=0.0, vmax=1.0)
|
||
|
||
# 每秒打印一次“帧新鲜度诊断”,用于判断写端是否在持续更新
|
||
crc = int(zlib.crc32(np.ascontiguousarray(data).tobytes()))
|
||
now = time.time()
|
||
if last_crc is None or crc != last_crc:
|
||
last_crc = crc
|
||
last_change_ts = now
|
||
|
||
frame_counter += 1
|
||
elapsed = now - fps_window_start
|
||
fps = (frame_counter / elapsed) if elapsed > 0 else 0.0
|
||
if now - last_diag_ts >= 1.0:
|
||
frame_age_ms = int(max(0.0, now - last_change_ts) * 1000.0)
|
||
changed_u64, changed_u32 = probe_header_fields(header)
|
||
u64_msg = "none"
|
||
u32_msg = "none"
|
||
if changed_u64:
|
||
# 取前3个变化字段,格式 off:value(delta)
|
||
u64_msg = ", ".join([f"{off}:{val}({delta:+d})" for off, val, delta in changed_u64[:3]])
|
||
if changed_u32:
|
||
u32_msg = ", ".join([f"{off}:{val}({delta:+d})" for off, val, delta in changed_u32[:3]])
|
||
logging.info(
|
||
"VIS诊断 frame_age_ms=%d crc=%s fps=%.1f tick_ms=%d | hdr_u64_changed=%s | hdr_u32_changed=%s",
|
||
frame_age_ms,
|
||
crc,
|
||
fps,
|
||
event_tick,
|
||
u64_msg,
|
||
u32_msg
|
||
)
|
||
last_diag_ts = now
|
||
frame_counter = 0
|
||
fps_window_start = now
|
||
return [im]
|
||
except StopIteration:
|
||
plt.close()
|
||
|
||
ani = FuncAnimation(fig, update, interval=update_interval, blit=True, cache_frame_data=False)
|
||
plt.show()
|
||
|
||
|
||
|
||
|
||
|
||
if __name__ == "__main__":
|
||
|
||
# 读取内存数据
|
||
data_stream = read_shared_memory_stream()
|
||
|
||
# 启动可视化(20ms 间隔约等于 50Hz 刷新)
|
||
visualize_data_stream(data_stream, update_interval=20) # 20Hz更新
|
||
|