Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
E
EPlanVisualizer
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
嵇洲
EPlanVisualizer
Commits
6f2731f4
Commit
6f2731f4
authored
Mar 17, 2026
by
fenghen777
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 仿真时长参数支持 + 按钮布局优化 + 智能状态重置
parent
abe22eec
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
80 additions
and
405 deletions
+80
-405
半实物仿真方案.md
docs/半实物仿真方案.md
+0
-367
接口文档.md
docs/接口文档.md
+11
-1
ProjectPanel.jsx
src/components/ProjectPanel/ProjectPanel.jsx
+55
-31
useProjectStore.js
src/hooks/useProjectStore.js
+12
-4
api.js
src/utils/api.js
+2
-2
No files found.
docs/半实物仿真方案.md
deleted
100644 → 0
View file @
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 规范行为,不应去重。
docs/接口文档.md
View file @
6f2731f4
...
@@ -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 |
...
...
src/components/ProjectPanel/ProjectPanel.jsx
View file @
6f2731f4
...
@@ -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=
{
h
wCount
===
0
||
p
.
hilStatus
===
'starting'
||
p
.
hilStatus
===
'running'
}
disabled=
{
h
ilDisabled
}
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'
?
(
...
...
src/hooks/useProjectStore.js
View file @
6f2731f4
...
@@ -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
);
...
...
src/utils/api.js
View file @
6f2731f4
...
@@ -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
}),
});
});
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment