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
abe22eec
Commit
abe22eec
authored
Mar 17, 2026
by
fenghen777
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(hil): 显式控制变量映射 + 分离HIL/仿真结果按钮 + FMI2 VR去重修复
parent
0fb78cc4
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
224 additions
and
13 deletions
+224
-13
SKILL.md
.agent/skills/eplan-visualizer/SKILL.md
+1
-0
半实物仿真方案.md
docs/半实物仿真方案.md
+200
-1
package-lock.json
package-lock.json
+1
-1
ProjectPanel.jsx
src/components/ProjectPanel/ProjectPanel.jsx
+15
-11
api.js
src/utils/api.js
+1
-0
modelMapping.js
src/utils/modelMapping.js
+5
-0
modelicaExporter.js
src/utils/modelicaExporter.js
+1
-0
No files found.
.agent/skills/eplan-visualizer/SKILL.md
View file @
abe22eec
...
@@ -53,6 +53,7 @@ src/
...
@@ -53,6 +53,7 @@ src/
5.
大改动优先新增类/文件,而非修改现有文件
5.
大改动优先新增类/文件,而非修改现有文件
6.
`docs/`
目录下所有文档
**必须使用中文命名**
6.
`docs/`
目录下所有文档
**必须使用中文命名**
7.
禁止自动提交 Git
7.
禁止自动提交 Git
8.
**禁止编写带有猜测逻辑的代码**
— 参数和变量名必须明确,不得通过关键词匹配、模式猜测等方式推断语义。遇到不确定的地方必须向用户提问确认,而非假设默认行为
## 编译验证
## 编译验证
...
...
docs/半实物仿真方案.md
View file @
abe22eec
...
@@ -83,7 +83,8 @@
...
@@ -83,7 +83,8 @@
3. 解析 modelDescription.xml:
3. 解析 modelDescription.xml:
- 提取 GUID 和 modelIdentifier
- 提取 GUID 和 modelIdentifier
- 块级解析 <ScalarVariable> 发现所有变量
- 块级解析 <ScalarVariable> 发现所有变量
- 按 valueReference 去重,排除 parameter
- 按 `{数据类型}:{VR}` 组合键去重 (FMI2 不同类型 VR 独立)
- 保留实物节点的 parameter 变量 (如 idealSwitch_1.closed)
4. fmi2Instantiate → setupExperiment → enterInit → exitInit
4. fmi2Instantiate → setupExperiment → enterInit → exitInit
5. 启动 ROS2 wall timer (周期 = step_size)
5. 启动 ROS2 wall timer (周期 = step_size)
...
@@ -166,3 +167,201 @@
...
@@ -166,3 +167,201 @@
-
**采样率:**
每 1ms 记录一点,可通过
`sample_rate_`
调整
-
**采样率:**
每 1ms 记录一点,可通过
`sample_rate_`
调整
-
**步长建议:**
100ms 步长完全满足 ROS2 timer 精度要求
-
**步长建议:**
100ms 步长完全满足 ROS2 timer 精度要求
-
**10s/1ms 仿真:**
约 10000 步,实时运行 ~10s,CSV ~1MB
-
**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 规范行为,不应去重。
package-lock.json
View file @
abe22eec
...
@@ -3191,7 +3191,7 @@
...
@@ -3191,7 +3191,7 @@
},
},
"node_modules/recharts"
:
{
"node_modules/recharts"
:
{
"version"
:
"3.8.0"
,
"version"
:
"3.8.0"
,
"resolved"
:
"https://registry.npm
mirror.com
/recharts/-/recharts-3.8.0.tgz"
,
"resolved"
:
"https://registry.npm
js.org
/recharts/-/recharts-3.8.0.tgz"
,
"integrity"
:
"sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="
,
"integrity"
:
"sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="
,
"license"
:
"MIT"
,
"license"
:
"MIT"
,
"workspaces"
:
[
"workspaces"
:
[
...
...
src/components/ProjectPanel/ProjectPanel.jsx
View file @
abe22eec
...
@@ -53,7 +53,7 @@ export default function ProjectPanel() {
...
@@ -53,7 +53,7 @@ export default function ProjectPanel() {
const
[
renameValue
,
setRenameValue
]
=
useState
(
''
);
const
[
renameValue
,
setRenameValue
]
=
useState
(
''
);
const
[
showCompileLog
,
setShowCompileLog
]
=
useState
(
false
);
const
[
showCompileLog
,
setShowCompileLog
]
=
useState
(
false
);
const
[
showExecuteLog
,
setShowExecuteLog
]
=
useState
(
false
);
const
[
showExecuteLog
,
setShowExecuteLog
]
=
useState
(
false
);
const
[
showResults
,
setShowResults
]
=
useState
(
false
);
const
[
showResults
Mode
,
setShowResultsMode
]
=
useState
(
null
);
// null | 'sim' | 'hil'
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
;
...
@@ -65,7 +65,7 @@ export default function ProjectPanel() {
...
@@ -65,7 +65,7 @@ export default function ProjectPanel() {
const
cur
=
activeProject
?.
executeStatus
;
const
cur
=
activeProject
?.
executeStatus
;
prevExecStatusRef
.
current
=
cur
;
prevExecStatusRef
.
current
=
cur
;
if
(
prev
===
'running'
&&
cur
===
'success'
&&
activeProject
?.
executeResult
?.
csvData
)
{
if
(
prev
===
'running'
&&
cur
===
'success'
&&
activeProject
?.
executeResult
?.
csvData
)
{
setShowResults
(
true
);
setShowResults
Mode
(
'sim'
);
}
}
},
[
activeProject
?.
executeStatus
]);
},
[
activeProject
?.
executeStatus
]);
...
@@ -342,7 +342,7 @@ export default function ProjectPanel() {
...
@@ -342,7 +342,7 @@ export default function ProjectPanel() {
{
(
p
.
hilStatus
===
'done'
||
p
.
hilStatus
===
'stopped'
)
&&
p
.
hilResult
?.
csvData
&&
(
{
(
p
.
hilStatus
===
'done'
||
p
.
hilStatus
===
'stopped'
)
&&
p
.
hilResult
?.
csvData
&&
(
<
button
<
button
className=
{
`${styles.simBtn} ${styles.viewResults}`
}
className=
{
`${styles.simBtn} ${styles.viewResults}`
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setShowResults
(
true
);
}
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setShowResults
Mode
(
'hil'
);
}
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
}
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
}
}
>
📊 查看 HIL 结果
</
button
>
>
📊 查看 HIL 结果
</
button
>
)
}
)
}
...
@@ -352,16 +352,16 @@ export default function ProjectPanel() {
...
@@ -352,16 +352,16 @@ export default function ProjectPanel() {
className=
{
`${styles.simBtn}`
}
className=
{
`${styles.simBtn}`
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
,
color
:
'#60a5fa'
}
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
,
color
:
'#60a5fa'
}
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
fetchHilResults
(
p
.
id
);
}
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
fetchHilResults
(
p
.
id
);
}
}
>
📥 获取仿真
结果
</
button
>
>
📲 获取 HIL
结果
</
button
>
)
}
)
}
{
/* 查看
结果 */
}
{
/* 查看普通仿真
结果 */
}
{
p
.
executeStatus
===
'success'
&&
p
.
executeResult
?.
csvData
&&
(
{
p
.
executeStatus
===
'success'
&&
p
.
executeResult
?.
csvData
&&
(
<
button
<
button
className=
{
`${styles.simBtn} ${styles.viewResults}`
}
className=
{
`${styles.simBtn} ${styles.viewResults}`
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setShowResults
(
true
);
}
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
setShowResults
Mode
(
'sim'
);
}
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
}
}
style=
{
{
width
:
'100%'
,
marginBottom
:
4
}
}
>
📊 查看结果
</
button
>
>
📊 查看
仿真
结果
</
button
>
)
}
)
}
{
/* 编译日志 */
}
{
/* 编译日志 */
}
...
@@ -401,11 +401,15 @@ export default function ProjectPanel() {
...
@@ -401,11 +401,15 @@ export default function ProjectPanel() {
</
div
>
</
div
>
{
/* 仿真结果弹窗 */
}
{
/* 仿真结果弹窗 */
}
{
showResults
&&
(
activeProject
?.
executeResult
?.
csvData
||
activeProject
?.
hilResult
?.
csvData
)
&&
(
{
showResults
Mode
&&
(
<
SimResultsModal
<
SimResultsModal
csvData=
{
activeProject
.
executeResult
?.
csvData
||
activeProject
.
hilResult
?.
csvData
}
csvData=
{
modelName=
{
activeProject
.
modelName
||
'Circuit'
}
showResultsMode
===
'hil'
onClose=
{
()
=>
setShowResults
(
false
)
}
?
activeProject
?.
hilResult
?.
csvData
:
activeProject
?.
executeResult
?.
csvData
}
modelName=
{
activeProject
?.
modelName
||
'Circuit'
}
onClose=
{
()
=>
setShowResultsMode
(
null
)
}
/>
/>
)
}
)
}
</
div
>
</
div
>
...
...
src/utils/api.js
View file @
abe22eec
...
@@ -80,6 +80,7 @@ export async function startHilSession({ moCode, modelName, hardwarePorts, stepSi
...
@@ -80,6 +80,7 @@ export async function startHilSession({ moCode, modelName, hardwarePorts, stepSi
sim_mode
:
p
.
simMode
||
'linear'
,
sim_mode
:
p
.
simMode
||
'linear'
,
ports
:
p
.
ports
||
[],
ports
:
p
.
ports
||
[],
fmu_var_prefix
:
p
.
fmuVarPrefix
||
''
,
fmu_var_prefix
:
p
.
fmuVarPrefix
||
''
,
control_var
:
p
.
controlVar
||
''
,
topic
:
p
.
topic
||
''
,
topic
:
p
.
topic
||
''
,
hw_label
:
p
.
hwNodeLabel
||
''
,
hw_label
:
p
.
hwNodeLabel
||
''
,
hw_node_id
:
p
.
hwNodeId
||
''
,
hw_node_id
:
p
.
hwNodeId
||
''
,
...
...
src/utils/modelMapping.js
View file @
abe22eec
...
@@ -16,14 +16,17 @@ const MODEL_MAP = {
...
@@ -16,14 +16,17 @@ const MODEL_MAP = {
resistor
:
{
resistor
:
{
modelName
:
'Resistor'
,
modelName
:
'Resistor'
,
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
controlVar
:
'R'
,
// Resistor.mo: parameter Real R
},
},
capacitor
:
{
capacitor
:
{
modelName
:
'Capacitor'
,
modelName
:
'Capacitor'
,
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
controlVar
:
'C'
,
// Capacitor.mo: parameter Real C
},
},
inductor
:
{
inductor
:
{
modelName
:
'Inductor'
,
modelName
:
'Inductor'
,
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
portMap
:
{
'p1'
:
'p'
,
'p2'
:
'n'
},
controlVar
:
'L'
,
// Inductor.mo: parameter Real L
},
},
diode
:
{
diode
:
{
modelName
:
'Diode'
,
modelName
:
'Diode'
,
...
@@ -32,6 +35,7 @@ const MODEL_MAP = {
...
@@ -32,6 +35,7 @@ const MODEL_MAP = {
voltage_source
:
{
voltage_source
:
{
modelName
:
'VoltageSource'
,
modelName
:
'VoltageSource'
,
portMap
:
{
'p-pos'
:
'p'
,
'p-neg'
:
'n'
},
portMap
:
{
'p-pos'
:
'p'
,
'p-neg'
:
'n'
},
controlVar
:
'V0'
,
// VoltageSource.mo: parameter Real V0
},
},
ground
:
{
ground
:
{
modelName
:
'Ground'
,
modelName
:
'Ground'
,
...
@@ -40,6 +44,7 @@ const MODEL_MAP = {
...
@@ -40,6 +44,7 @@ const MODEL_MAP = {
ideal_switch
:
{
ideal_switch
:
{
modelName
:
'IdealSwitch'
,
modelName
:
'IdealSwitch'
,
portMap
:
{
'p-in'
:
'p'
,
'p-out'
:
'n'
},
portMap
:
{
'p-in'
:
'p'
,
'p-out'
:
'n'
},
controlVar
:
'closed'
,
// IdealSwitch.mo: parameter Boolean closed
},
},
// ===== 电气控制 =====
// ===== 电气控制 =====
...
...
src/utils/modelicaExporter.js
View file @
abe22eec
...
@@ -403,6 +403,7 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') {
...
@@ -403,6 +403,7 @@ export function exportToModelicaHIL(data, modelName = 'Circuit') {
simMode
:
simModeMap
[
type
]
||
'linear'
,
simMode
:
simModeMap
[
type
]
||
'linear'
,
ports
:
portConnectors
,
ports
:
portConnectors
,
fmuVarPrefix
:
instanceName
,
fmuVarPrefix
:
instanceName
,
controlVar
:
mapping
?.
controlVar
?
`
${
instanceName
}
.
${
mapping
.
controlVar
}
`
:
''
,
topic
:
`/
${
toMoId
(
modelName
)}
/
${
instanceName
}
`
,
topic
:
`/
${
toMoId
(
modelName
)}
/
${
instanceName
}
`
,
});
});
}
}
...
...
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