Commit 6f2731f4 authored by fenghen777's avatar fenghen777

feat: 仿真时长参数支持 + 按钮布局优化 + 智能状态重置

parent abe22eec
# 半实物仿真 (HIL) 技术方案
## 概述
半实物仿真 (Hardware-in-the-Loop) 将 Modelica 电路模型编译为 FMU (Functional Mock-up Unit),
通过 ROS2 节点架构实现仿真模型与实物/模拟硬件的实时交互。
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 前端 (React) │
│ ┌────────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ 画布: 标记实物 │→│ 导出 Modelica │→│ 调用 /api/hil/start │ │
│ │ isHardware=true │ │ + 半实物配置 │ │ duration/stepSize │ │
│ └────────────────┘ └──────────────┘ └────────────────────┘ │
│ ↕ 轮询 status │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 结果展示: SimResultsModal (CSV 表格 + 图表) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▼ HTTP
┌─────────────────────────────────────────────────────────────────┐
│ 后端 (Drogon C++) │
│ ┌────────────┐ ┌───────────┐ ┌──────────────────────────┐ │
│ │ HilCtrl │→│ HilService │→│ 1. CompileModel (omc) │ │
│ │ start/stop │ │ 会话管理 │ │ 2. ExportFMU (buildFMU) │ │
│ │ status │ │ │ │ 3. LaunchRos2Node (fork) │ │
│ │ results │ │ │ └──────────────────────────┘ │
│ └────────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
▼ fork + ros2 run
┌─────────────────────────────────────────────────────────────────┐
│ ROS2 节点 (Humble) │
│ │
│ ┌──────────────────┐ ROS2 Topic ┌─────────────────────┐ │
│ │ fmu_bridge_node │ ←─────────────→ │ hardware_sim_node │ │
│ │ │ /hil/X/fmu_out │ (每个实物节点一个) │ │
│ │ ┌─FMU─────────┐ │ /hil/X/hw_out │ │ │
│ │ │ dlopen .so │ │ │ 模式: switch/ │ │
│ │ │ doStep() │ │ │ resistor/capacitor │ │
│ │ │ getReal() │ │ │ /motor/linear... │ │
│ │ │ setReal() │ │ └─────────────────────┘ │
│ │ └─────────────┘ │ │
│ │ CSV 内存缓冲 │ │
│ │ → 完成后写文件 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## 流程详解
### 1. 前端导出
用户在画布中将节点标记为「实物」(`isHardware: true`),设置仿真时长后点击启动。
`exportToModelicaHIL()` 生成:
- 完整的 Modelica `.mo` 代码
- `hardwarePorts` 数组:每个实物节点的 Modelica 实例名、模式、端口列表
### 2. 后端编译与 FMU 导出
`HilService::StartSession()` 流程:
| 步骤 | 操作 | 状态 |
|------|------|------|
| 1 | 写 `.mo` 文件 → `omc compile_hil.mos` 检查语法 | `compiling` |
| 2 | `omc export_fmu.mos` → 生成 FMI 2.0 Co-Simulation `.fmu` | `exporting_fmu` |
| 3 | fork 启动 `fmu_bridge_node` + N 个 `hardware_sim_node` | `launching_ros2` |
| 4 | 仿真运行中 | `running` |
| 5 | 仿真完成 / 手动停止 | `done` / `stopped` |
### 3. FMU Bridge 节点
`FmuBridgeNode.cpp` 是核心仿真驱动:
```
启动流程:
1. 解压 .fmu (unzip) → 找 binaries/linux64/*.so
2. dlopen() 加载共享库 → 获取 FMI2 函数指针
3. 解析 modelDescription.xml:
- 提取 GUID 和 modelIdentifier
- 块级解析 <ScalarVariable> 发现所有变量
- 按 `{数据类型}:{VR}` 组合键去重 (FMI2 不同类型 VR 独立)
- 保留实物节点的 parameter 变量 (如 idealSwitch_1.closed)
4. fmi2Instantiate → setupExperiment → enterInit → exitInit
5. 启动 ROS2 wall timer (周期 = step_size)
仿真循环 (每步):
├ 检查 g_stop_requested (SIGTERM)
├ 检查 sim_time + 0.5*step >= duration (完成)
├ doStep(sim_time, step_size)
├ getReal() 读取所有输出变量
├ 发布到 ROS2 话题
└ 追加到 csv_rows_ 内存缓冲
结束:
├ WriteCSV() → 一次性写入文件
├ 发布 /hil/done 消息
└ rclcpp::shutdown()
```
**SIGTERM 处理:** 收到停止信号时立即写入已采集数据再退出。
### 4. Hardware Sim 节点
`HardwareSimNode.cpp` 模拟实物设备行为:
| 模式 | 行为 |
|------|------|
| `resistor` | V_in → I_out = V/R |
| `capacitor` | V_in → I_out = C*dV/dt |
| `inductor` | I_in → V_out = L*dI/dt |
| `switch` | 周期开关 |
| `voltage_src` | 恒定电压输出 |
| `motor` | V_in → 速度 (含惯性) |
| `linear` | output = gain * input (默认) |
### 5. 结果获取
仿真完成后 CSV 数据通过 `GET /api/hil/results?session_id=xxx` 返回。
前端每 3 秒轮询状态,`done` 后自动获取结果并弹出图表视图。
## API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/hil/start` | 启动 HIL 会话 |
| GET | `/api/hil/status?session_id=` | 查询状态 |
| POST | `/api/hil/stop` | 停止会话 |
| GET | `/api/hil/results?session_id=` | 获取 CSV 结果 |
详细请求/响应格式见 [接口文档.md](接口文档.md)
## 文件清单
### 前端
| 文件 | 职责 |
|------|------|
| `utils/modelicaExporter.js` | `exportToModelicaHIL()` 导出模型 + 实物配置 |
| `utils/api.js` | `startHilSession`, `stopHilSession`, `getHilResults`, `getHilStatus` |
| `hooks/useProjectStore.js` | `startHil`, `stopHil`, `fetchHilResults` + 轮询逻辑 |
| `components/ProjectPanel/ProjectPanel.jsx` | 仿真时长输入、启动/停止按钮、结果查看 |
### 后端
| 文件 | 职责 |
|------|------|
| `controllers/HilCtrl.h/cpp` | HTTP 端点: start, status, stop, results |
| `services/HilService.h/cpp` | 编译、FMU 导出、ROS2 进程管理、会话状态 |
### ROS2 节点
| 文件 | 职责 |
|------|------|
| `ros2_nodes/src/FmuBridgeNode.cpp` | FMU 加载/步进/数据采集/CSV 输出 |
| `ros2_nodes/src/HardwareSimNode.cpp` | 多模式实物设备模拟 |
| `ros2_nodes/src/fmi2*.h` | FMI 2.0 标准头文件 |
| `ros2_nodes/CMakeLists.txt` | 编译配置 (链接 dl) |
## 性能特性
- **数据缓冲:** CSV 数据在内存 `vector<string>` 中累积,仿真结束一次性写入文件
- **采样率:** 每 1ms 记录一点,可通过 `sample_rate_` 调整
- **步长建议:** 100ms 步长完全满足 ROS2 timer 精度要求
- **10s/1ms 仿真:** 约 10000 步,实时运行 ~10s,CSV ~1MB
## FmuBridgeNode 实现细节
### FMU 加载流程
```
1. unzip .fmu → /tmp/fmu_extract_xxx/
├── binaries/linux64/*.so (FMU 共享库)
├── modelDescription.xml (变量/GUID 描述)
└── resources/ (FMU 资源)
2. 解析 modelDescription.xml (纯字符串解析, 无外部 XML 依赖)
├── <fmiModelDescription guid="xxx"> → GUID
├── <CoSimulation modelIdentifier="xxx"> → 库名
└── <ScalarVariable name="..." valueReference="..." causality="...">
└── <Real/Boolean/Integer> → 数据类型
3. dlopen(binaries/linux64/xxx.so) → dlsym() 获取函数指针:
fn_instantiate_ fmi2Instantiate
fn_setup_exp_ fmi2SetupExperiment
fn_enter_init_ fmi2EnterInitializationMode
fn_exit_init_ fmi2ExitInitializationMode
fn_do_step_ fmi2DoStep
fn_get_real_ fmi2GetReal
fn_set_real_ fmi2SetReal
fn_get_boolean_ fmi2GetBoolean
fn_set_boolean_ fmi2SetBoolean
fn_terminate_ fmi2Terminate
fn_free_instance_ fmi2FreeInstance
```
### 硬件变量匹配机制
FmuBridgeNode 通过 `hw_var_names` 参数(逗号分隔的实物节点 Modelica 实例名)匹配 FMU 变量:
```
hw_var_names = "idealSwitch_1,resistor_1"
hw_control_vars = "idealSwitch_1.closed,resistor_1.R"
FMU 变量 匹配结果
──────────────────────────────── ──────────
idealSwitch_1.closed ✓ 控制输入 (hw_control_vars 显式指定)
idealSwitch_1.v ✓ 输出 (hw_prefix 匹配, 非控制变量)
idealSwitch_1.i ✓ 输出
resistor_1.R ✓ 控制输入 (hw_control_vars 显式指定)
resistor_1.v ✓ 输出
capacitor_1.p.v ✗ 不匹配任何 hw_prefix
```
**控制变量识别规则(显式映射,无猜测):**
控制变量名由前端 `modelMapping.js``controlVar` 字段定义,全链路传递到 FmuBridgeNode:
```
modelMapping.js modelicaExporter.js api.js HilService.cpp FmuBridgeNode.cpp
controlVar: 'closed' → controlVar: 'x_1.closed' → control_var → hw_control_vars → 精确匹配 VR
```
| 组件类型 | controlVar | 数据类型 | 来源 |
|----------|------------|----------|------|
| ideal_switch | closed | Boolean | IdealSwitch.mo |
| resistor | R | Real | Resistor.mo |
| capacitor | C | Real | Capacitor.mo |
| inductor | L | Real | Inductor.mo |
| voltage_source | V0 | Real | VoltageSource.mo |
### Boolean 变量支持
FMU 中的 Boolean 变量(如开关的 `closed` 参数)通过 `HwLink.input_types` 记录数据类型,
使用 `fmi2SetBoolean/GetBoolean` 操作。硬件节点发送的 Float64 值通过阈值 0.5 转换:
`hw_value > 0.5 → fmi2True, 否则 fmi2False`
> **注意:** FMI2 中不同数据类型的 valueReference 是独立命名空间,
> 即 Real 的 `vr=0` 和 Boolean 的 `vr=0` 是不同的变量。
> FmuBridgeNode 使用 `{type}:{vr}` 组合键去重,并在 `HwLink` 中存储每个变量的类型,
> 避免按 VR 扫描时的跨类型混淆。
### ROS2 话题命名
每个实物节点对应两个话题:
| 话题 | 方向 | 说明 |
|------|------|------|
| `/hil/{instanceName}/fmu_out` | FmuBridge → HardwareSimNode | FMU 输出值发给硬件节点 |
| `/hil/{instanceName}/hw_out` | HardwareSimNode → FmuBridge | 硬件节点响应写回 FMU |
### ROS2 参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `fmu_path` | string | "" | .fmu 文件路径 |
| `step_size` | double | 0.001 | 仿真步长 (秒) |
| `duration` | double | 10.0 | 仿真时长 (秒) |
| `csv_output` | string | "" | CSV 输出文件路径 |
| `hw_var_names` | string | "" | 逗号分隔的实物节点实例名前缀 |
| `hw_control_vars` | string | "" | 逗号分隔的控制变量全名 (由前端提供) |
## Switch 演示场景
### 演示目标
将前端画布中的 `switch` 节点标记为半实物,ROS2 `HardwareSimNode` 以固定周期(100ms 开 / 100ms 关)切换开关状态,验证从硬件控制信号到 FMU 仿真结果的完整链路。
### 数据流
```
HardwareSimNode (switch 模式) FmuBridgeNode FMU
┌──────────────────────┐ hw_out ┌───────────────────┐ ┌───────┐
│ 计时 → 100ms翻转 │ ──────────→ │ setBoolean(control)│ ───────→ │ │
│ output: 1.0 / 0.0 │ │ doStep() │ │ 求解 │
│ │ fmu_out │ getReal(v, i...) │ ←─────── │ │
│ │ ←────────── │ → CSV 记录 │ │ │
└──────────────────────┘ └───────────────────┘ └───────┘
```
### 关键配置
`HilService` 启动 `hardware_sim_node` 时自动配置:
```cpp
// 当 sim_mode == "switch" 时
params.push_back({"period_ms", "100.0"}); // 半周期 100ms
```
**效果:** 开关以 200ms 为全周期(100ms 闭合 + 100ms 断开)反复切换。
### 预期结果
仿真 CSV 中可观察到:
- `idealSwitch_1.closed` 变量在 0/1 之间以 200ms 周期交替
- 电路中的电压/电流随开关状态变化呈方波响应
## 调试记录
### 问题 1: FMU Bridge 启动即崩溃
**现象:** `results` 接口返回 404,`fmu_bridge.log` 显示 `RCLInvalidROSArgsError`
**原因:** ROS2 不接受空值参数 (`-p hw_control_vars:=`)。当 `hw_control_vars` 为空字符串时,
`LaunchRos2Node` 构造了非法的命令行参数。
**修复:** `LaunchRos2Node` 中跳过空值参数:
```cpp
for (const auto& [key, val] : params) {
if (!val.empty()) {
cmd += " -p " + key + ":=" + val;
}
}
```
### 问题 2: 控制变量未被识别 (inputs=0)
**现象:** 日志显示 `HW 节点 [idealSwitch_1]: inputs=0 outputs=4`,开关从未被切换。
**原因(三层叠加):**
1. **`api.js` 遗漏字段:** `hardwarePorts` 映射时没有包含 `control_var` 字段,
后端从未收到控制变量名。
2. **VR 跨类型去重错误:** `idealSwitch_1.closed` (Boolean, vr=0) 与 `ground_1.p.i` (Real, vr=0)
在 FMI2 中属于不同类型的独立命名空间,但代码使用了全局 `seen_vrs` 按 VR 编号去重,
导致 Boolean 变量被 Real 变量「占位」后跳过。
3. **StepCallback 类型判断脆弱:** 在仿真循环中通过 VR 扫描 `variables_` 判断 Boolean/Real 类型,
同样因跨类型 VR 重叠导致误判。
**修复:**
- `api.js` 新增 `control_var: p.controlVar || ''` 字段映射
- VR 去重改为 `{type}:{vr}` 组合键
- `HwLink` 新增 `input_types`/`output_types` 向量,直接存储数据类型
### 问题 3: 控制变量被 parameter 过滤规则跳过
**现象:** `idealSwitch_1.closed``causality="parameter"`,被 `parameter` 跳过规则过滤。
**原因:** 旧代码无条件跳过所有 `causality == "parameter"` 的变量。
但自定义 Modelica 组件的控制参数(如 `closed`, `R`, `C`)在 FMU 中就是 parameter。
**修复:** 仅跳过非实物节点的 parameter:
```cpp
if (var.causality == "parameter" && !var.is_hw_related) continue;
```
### FMI2 VR 命名空间说明
FMI2 标准中 `valueReference` 按数据类型独立编号:
```
Real vr=0 ground_1.p.i ← Real 的 vr=0
Real vr=1 ground_1.p.v
Real vr=2 idealSwitch_1.i ← 多个 Real 变量共享 vr=2 (Modelica 别名优化)
Real vr=2 idealSwitch_1.p.i
Real vr=2 resistor_1.n.i
Boolean vr=0 idealSwitch_1.closed ← Boolean 的 vr=0, 与 Real vr=0 无冲突
```
同类型的 VR 重复是 OpenModelica 编译器的别名优化(物理上同一个值),去重是正确的。
跨类型的 VR 重复是 FMI2 规范行为,不应去重。
...@@ -84,9 +84,17 @@ ...@@ -84,9 +84,17 @@
**请求:** **请求:**
```json ```json
{ "model_name": "Circuit" } {
"model_name": "Circuit",
"stop_time": 2.0
}
``` ```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| model_name | string | ✅ | 模型名称 |
| stop_time | number | - | 仿真终止时间(秒),默认 1.0 |
**成功响应** (`code: 0`): **成功响应** (`code: 0`):
```json ```json
{ {
...@@ -138,6 +146,7 @@ ...@@ -138,6 +146,7 @@
"sim_mode": "switch", "sim_mode": "switch",
"ports": ["p", "n"], "ports": ["p", "n"],
"fmu_var_prefix": "idealSwitch_1", "fmu_var_prefix": "idealSwitch_1",
"control_var": "idealSwitch_1.closed",
"topic": "/Circuit_1/idealSwitch_1", "topic": "/Circuit_1/idealSwitch_1",
"hw_label": "开关", "hw_label": "开关",
"hw_node_id": "new-device-3" "hw_node_id": "new-device-3"
...@@ -162,6 +171,7 @@ ...@@ -162,6 +171,7 @@
| hardware_ports[].topic | string | - | ROS2 话题前缀 | | hardware_ports[].topic | string | - | ROS2 话题前缀 |
| hardware_ports[].hw_label | string | - | 节点显示名称 | | hardware_ports[].hw_label | string | - | 节点显示名称 |
| hardware_ports[].hw_node_id | string | - | 前端节点 ID | | hardware_ports[].hw_node_id | string | - | 前端节点 ID |
| hardware_ports[].control_var | string | - | 控制变量全名,如 `idealSwitch_1.closed` |
| step_size | number | - | 仿真步长(秒),默认 0.001 | | step_size | number | - | 仿真步长(秒),默认 0.001 |
| duration | number | - | 仿真时长(秒),默认 10.0 | | duration | number | - | 仿真时长(秒),默认 10.0 |
......
...@@ -54,6 +54,7 @@ export default function ProjectPanel() { ...@@ -54,6 +54,7 @@ export default function ProjectPanel() {
const [showCompileLog, setShowCompileLog] = useState(false); const [showCompileLog, setShowCompileLog] = useState(false);
const [showExecuteLog, setShowExecuteLog] = useState(false); const [showExecuteLog, setShowExecuteLog] = useState(false);
const [showResultsMode, setShowResultsMode] = useState(null); // null | 'sim' | 'hil' const [showResultsMode, setShowResultsMode] = useState(null); // null | 'sim' | 'hil'
const [simDuration, setSimDuration] = useState(1);
const [hilDuration, setHilDuration] = useState(10); const [hilDuration, setHilDuration] = useState(10);
const activeProject = projects.find(p => p.id === activeProjectId) || null; const activeProject = projects.find(p => p.id === activeProjectId) || null;
...@@ -228,52 +229,75 @@ export default function ProjectPanel() { ...@@ -228,52 +229,75 @@ export default function ProjectPanel() {
/> />
</div> </div>
{/* 操作按钮 */} {/* 编译按钮 */}
<div className={styles.simBtnRow}> <button
<button className={`${styles.simBtn} ${styles.compile}`}
className={`${styles.simBtn} ${styles.compile}`} onClick={(e) => { e.stopPropagation(); compileProject(p.id); }}
onClick={(e) => { e.stopPropagation(); compileProject(p.id); }} disabled={p.compileStatus === 'compiling'}
disabled={p.compileStatus === 'compiling'} style={{ width: '100%', marginBottom: 6 }}
> >
{p.compileStatus === 'compiling' ? ( {p.compileStatus === 'compiling' ? (
<><span className={styles.spinner}></span> 编译中…</> <><span className={styles.spinner}></span> 编译中…</>
) : ( ) : (
<>▶ 编译模型</> <>▶ 编译模型</>
)} )}
</button> </button>
<button
className={`${styles.simBtn} ${styles.execute}`} {/* 普通仿真: 时长 + 执行按钮 */}
onClick={(e) => { e.stopPropagation(); executeProject(p.id); }} <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
disabled={p.compileStatus !== 'success' || p.executeStatus === 'running'} <label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>仿真时长:</label>
title={p.compileStatus !== 'success' ? '请先编译成功' : '下发等效设备执行'} <input
> type="number"
{p.executeStatus === 'running' ? ( min="0.1" max="3600" step="0.1"
<><span className={styles.spinner}></span> 下发中…</> value={simDuration}
) : ( onChange={(e) => setSimDuration(e.target.value === '' ? '' : Number(e.target.value))}
<>⏵ 下发等效设备执行</> onBlur={() => { if (!simDuration || simDuration <= 0) setSimDuration(1); }}
)} onClick={(e) => e.stopPropagation()}
</button> disabled={p.compileStatus !== 'success'}
style={{
flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none',
opacity: p.compileStatus !== 'success' ? 0.4 : 1,
}}
/>
<span style={{ fontSize: 11, color: '#666' }}></span>
</div> </div>
<button
className={`${styles.simBtn} ${styles.execute}`}
onClick={(e) => { e.stopPropagation(); executeProject(p.id, { stopTime: simDuration }); }}
disabled={p.compileStatus !== 'success' || p.executeStatus === 'running'}
title={p.compileStatus !== 'success' ? '请先编译成功' : '下发等效设备执行'}
style={{ width: '100%', marginBottom: 6 }}
>
{p.executeStatus === 'running' ? (
<><span className={styles.spinner}></span> 下发中…</>
) : (
<>⏵ 下发等效设备执行</>
)}
</button>
{/* 半实物仿真按钮 */} {/* 半实物仿真: 时长 + HIL 按钮 */}
{(() => { {(() => {
const hwCount = (p.nodes || []).filter(n => n.data?.isHardware).length; const hwCount = (p.nodes || []).filter(n => n.data?.isHardware).length;
const hilDisabled = hwCount === 0 || p.compileStatus !== 'success' || p.hilStatus === 'starting' || p.hilStatus === 'running';
return ( return (
<> <>
{/* 仿真时长输入 */} {hwCount > 0 && (
{hwCount > 0 && p.hilStatus !== 'running' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>仿真时长:</label> <label style={{ fontSize: 11, color: '#888', whiteSpace: 'nowrap' }}>HIL时长:</label>
<input <input
type="number" type="number"
min="1" max="3600" step="1" min="1" max="3600" step="1"
value={hilDuration} value={hilDuration}
onChange={(e) => setHilDuration(Number(e.target.value) || 10)} onChange={(e) => setHilDuration(Number(e.target.value) || 10)}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
disabled={hilDisabled}
style={{ style={{
flex: 1, padding: '3px 6px', fontSize: 11, flex: 1, padding: '3px 6px', fontSize: 11,
background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)',
borderRadius: 4, color: '#ccc', outline: 'none', borderRadius: 4, color: '#ccc', outline: 'none',
opacity: hilDisabled ? 0.4 : 1,
}} }}
/> />
<span style={{ fontSize: 11, color: '#666' }}></span> <span style={{ fontSize: 11, color: '#666' }}></span>
...@@ -287,8 +311,8 @@ export default function ProjectPanel() { ...@@ -287,8 +311,8 @@ export default function ProjectPanel() {
borderColor: hwCount > 0 ? 'rgba(251,146,60,0.3)' : undefined, borderColor: hwCount > 0 ? 'rgba(251,146,60,0.3)' : undefined,
color: hwCount > 0 ? '#fb923c' : undefined, color: hwCount > 0 ? '#fb923c' : undefined,
}} }}
disabled={hwCount === 0 || p.hilStatus === 'starting' || p.hilStatus === 'running'} disabled={hilDisabled}
title={hwCount === 0 ? '请先在属性面板中将节点标记为实物' : `${hwCount} 个实物节点`} title={p.compileStatus !== 'success' ? '请先编译成功' : hwCount === 0 ? '请先在属性面板中将节点标记为实物' : `${hwCount} 个实物节点`}
onClick={(e) => { e.stopPropagation(); startHil(p.id, { duration: hilDuration }); }} onClick={(e) => { e.stopPropagation(); startHil(p.id, { duration: hilDuration }); }}
> >
{p.hilStatus === 'starting' ? ( {p.hilStatus === 'starting' ? (
......
...@@ -53,9 +53,18 @@ const useProjectStore = create((set, get) => ({ ...@@ -53,9 +53,18 @@ const useProjectStore = create((set, get) => ({
if (activeProjectId) { if (activeProjectId) {
// 更新已有项目 // 更新已有项目
const prev = projects.find(p => p.id === activeProjectId);
// 仅当节点/连线发生变化时重置编译/执行/HIL 状态
const nodesChanged = JSON.stringify(prev?.nodes) !== JSON.stringify(nodes)
|| JSON.stringify(prev?.edges) !== JSON.stringify(edges);
const resetFields = nodesChanged ? {
compileStatus: 'none', compileResult: null,
executeStatus: 'none', executeResult: null,
hilStatus: 'none', hilResult: null,
} : {};
const updated = projects.map(p => const updated = projects.map(p =>
p.id === activeProjectId p.id === activeProjectId
? { ...p, nodes, edges, updatedAt: now, name: name || p.name } ? { ...p, nodes, edges, updatedAt: now, name: name || p.name, ...resetFields }
: p : p
); );
persistProjects(updated); persistProjects(updated);
...@@ -211,7 +220,7 @@ const useProjectStore = create((set, get) => ({ ...@@ -211,7 +220,7 @@ const useProjectStore = create((set, get) => ({
}, },
/** 执行仿真 — 调用后端 API(仅编译成功后可执行) */ /** 执行仿真 — 调用后端 API(仅编译成功后可执行) */
executeProject: async (id) => { executeProject: async (id, { stopTime = 1.0 } = {}) => {
const { projects } = get(); const { projects } = get();
const project = projects.find(p => p.id === id); const project = projects.find(p => p.id === id);
if (!project || project.compileStatus !== 'success') return; if (!project || project.compileStatus !== 'success') return;
...@@ -228,7 +237,7 @@ const useProjectStore = create((set, get) => ({ ...@@ -228,7 +237,7 @@ const useProjectStore = create((set, get) => ({
try { try {
// 响应格式: { code, message, data: { sim_log, csv_data } } // 响应格式: { code, message, data: { sim_log, csv_data } }
const resp = await executeModel(project.modelName || 'Circuit'); const resp = await executeModel(project.modelName || 'Circuit', stopTime);
const isOk = resp.code === 0; const isOk = resp.code === 0;
setStatus({ setStatus({
executeStatus: isOk ? 'success' : 'error', executeStatus: isOk ? 'success' : 'error',
...@@ -251,7 +260,6 @@ const useProjectStore = create((set, get) => ({ ...@@ -251,7 +260,6 @@ const useProjectStore = create((set, get) => ({
/** 启动半实物仿真 */ /** 启动半实物仿真 */
startHil: async (id, { duration = 10, stepSize = 0.001 } = {}) => { startHil: async (id, { duration = 10, stepSize = 0.001 } = {}) => {
get().saveProject();
const { projects } = get(); const { projects } = get();
const project = projects.find(p => p.id === id); const project = projects.find(p => p.id === id);
......
...@@ -39,10 +39,10 @@ export async function compileModel(moCode, modelName) { ...@@ -39,10 +39,10 @@ export async function compileModel(moCode, modelName) {
* @param {string} modelName - 已编译的模型名称 * @param {string} modelName - 已编译的模型名称
* @returns {Promise<{code, message, data: {sim_log, model_name, csv_data?}}>} * @returns {Promise<{code, message, data: {sim_log, model_name, csv_data?}}>}
*/ */
export async function executeModel(modelName) { export async function executeModel(modelName, stopTime = 1.0) {
return request('/execute', { return request('/execute', {
method: 'POST', method: 'POST',
body: JSON.stringify({ model_name: modelName }), body: JSON.stringify({ model_name: modelName, stop_time: stopTime }),
}); });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment