AFSIM 11篇 外部控制 External Control:用代码实时操控仿真
🧭 系列导航(共 15 篇):
入门篇:01 AFSIM 是什么? · 02 从零搭建环境 · 03 跑通第一个仿真 · 04 核心概念扫盲
基础篇:05 SDL 入门 · 06 Platform 详解 · 07 传感器与跟踪 · 08 武器与交战 · 09 机动与航线
进阶篇:10 通信与处理器 · [11 External Control] · 12 TCP 客户端实战 · 13 Wizard 可视化
高级篇:14 C++ 插件开发 · 15 调试与最佳实践
上一篇:10 通信与处理器 | 下一篇:12 TCP 客户端实战
11 外部控制 External Control:用代码实时操控仿真
前面几篇,我们的"控制"都写死在 SDL 场景文件里:航线是固定的、任务触发条件是预设的。但真实项目里,指挥决策往往由外部系统实时给出——比如一个 Java 后端根据最新态势算出"让 UAV 飞到这儿""现在开火"。AFSIM 怎么让外部程序在仿真运行中读写状态、下发指令?答案就是本篇主角:wsf_external_control 插件(外部控制插件)。
一、wsf_external_control 插件在做什么
一句话类比:wsf_external_control 就像在仿真引擎里开了一扇"后门"——它把仿真内部的状态(平台位置、track 列表、武器状态)暴露出去,同时接收外部程序发来的指令并执行。没有它,仿真是一个封闭的黑盒;有了它,你的 Java/Python 代码就成了仿真的一位"远程指挥官"。
从机制上说,这个插件做两件事:
- 对外提供读取接口:外部客户端连上来后,可以周期性拉取平台状态、track 列表等。这里的 track 列表正是 第 07 篇《传感器与跟踪》 里提到的 WsfTrackList——插件把它序列化后通过 TCP 发给客户端,客户端就能拿到 GetTrackCount()、GetTrackEntry() 看到的那些 track。
- 对外接收控制指令:客户端发来 COMMAND|key=value 形式的文本指令(如 FIRE / MOVE_TO,详见本篇第三节),插件解析后,向仿真事件队列(event queue)注入相应事件(如 FireWeaponEvent),仿真时钟驱动下一刻去执行。
要强调一点:AFSIM 是第 04 篇提到的离散事件仿真(Discrete Event Simulation),时钟由事件驱动推进。所以外部指令并不是"立刻改内存",而是作为事件入队,由仿真在对应时刻处理,这保证了因果与一致性。
这也解释了为什么外部控制不能"想改就改"——你在 12:00:00 这一刻发来的 FlyToEvent,仿真只会把它安排到当前事件时间之后的某个时刻执行;如果你连续狂发一百条指令,它们会按到达顺序依次排队,而不是覆盖式地瞬移平台。理解这一点,你就不会困惑"为什么我下了指令,飞机没有立刻转向"。顺带一提,插件对外暴露的状态也是某一仿真时刻的快照,不是实时视频流;客户端拉取得越频繁,看到的态势越新,但也会占用更多带宽与 CPU。
二、三个关键事件类
插件把外部指令翻译成具体的仿真事件,常用的有三个(命名空间 wsf::external):
- FlyToEvent:让某平台飞向指定位置。例如让 UAV_01 改变航线去某坐标点。对应 第 09 篇《机动与航线》 的机动,但这里是运行时动态指定的。
- PlatformCommandEvent:更通用的平台命令事件,可以携带任意高层指令(改变任务、切换模式等),由平台的 processor 解读。
- FireWeaponEvent:下令开火。触发 第 08 篇《武器与交战》 的武器发射流程,对指定目标投弹/发射。
从 C++ 插件视角,注入事件大致是这样(完整插件工程见 第 14 篇《自定义 C++ 插件开发》):
cpp
// 命名空间:wsf::external
void ExternalControl::IssueFlyTo(const std::string& plat, double x, double y, double z) {
FlyToEvent* ev = new FlyToEvent(plat, WsfVec3(x, y, z));
GetSimulation()->QueueEvent(ev); // 入队,由仿真时钟驱动执行
}
void ExternalControl::IssueFire(const std::string& plat, const std::string& target) {
FireWeaponEvent* ev = new FireWeaponEvent(plat, target);
GetSimulation()->QueueEvent(ev);
}
三、通信机制:插件当 TCP Server
wsf_external_control 插件在仿真启动时,会在本机起一个 TCP server,监听 127.0.0.1:31000(环回地址,仅本机可连,安全且低延迟)。外部程序作为 TCP client 连上来之后,双方就建立了双向收发通道。
🔍 安装实证|真实线协议是"管道分隔纯文本",不是 JSON:插件源码 swdev\src\wsf_plugins\wsf_external_control\README.md 明确写明——命令格式为 COMMAND|key=value|key=value|...,每条以换行结束;响应为 OK|... / ERROR|... / DATA|...。支持的具体命令:
| 命令 | 参数 | 含义 |
|---|---|---|
| FIRE | platform, weapon, target, qty | 平台发射武器 |
| MOVE_TO | platform, lat, lon, alt, speed | 设置平台目的地 |
| SET_SPEED | platform, speed | 设置平台速度 |
| SET_ALTITUDE | platform, altitude | 设置平台高度 |
| SET_HEADING | platform, heading | 设置平台航向 |
| GET_STATUS | platform | 查询平台状态 |
| LIST_PLATFORMS | (无) | 列出所有平台 |
也就是说,本篇前文说的"客户端发 fly_to / fire_weapon 这类 JSON"是概念性示意——真实插件并不吃 JSON,而是吃 COMMAND|key=value。插件收到 FIRE|platform=UAV_01|weapon=fox3|target=Enemy_Ship 后,内部再把它翻译成 第 08 篇 的 FireWeaponEvent 等引擎事件入队执行。第 12 篇 的客户端代码会改用这套真实格式重写。
- 客户端 → 插件:发送 COMMAND|key=value|... 文本指令(如 FIRE、MOVE_TO)。
- 插件 → 客户端:周期性推送平台状态、track 快照(响应以 DATA|... 形式),或回 OK / ERROR 确认。
之所以选 TCP 而非 UDP,是因为指令不能丢——"开火"这种命令丢了可不是闹着玩的,TCP 的重传与有序性正好满足可靠性要求。
四、真实项目是怎么用的
作者正在维护一套 AFSIM 仿真控制系统,后端是 Java Spring Boot,其中 ExternalControlService 这个服务类在应用启动时就会去连 127.0.0.1:31000。连上后,它一边定时拉取 UAV_01(红方 RECON_STRIKE_UAV)和 Enemy_Ship(蓝方 ENEMY_DDG)的 track 与状态,一边根据业务规则下发指令。举个实战场景:
系统发现 Enemy_Ship 进入 UAV_01 的打击范围,于是由 ExternalControlService 向插件发送一条 FlyToEvent 让无人机抵近,再发一条 FireWeaponEvent 下令打击。整个"感知—决策—行动"闭环由 Java 后端驱动,而非写死在 SDL 里。
这正是外部控制最迷人的地方:把仿真变成可被业务系统实时驱动的"数字靶场"。下一篇 第 12 篇《Java/Python 接入 AFSIM:TCP 客户端开发实战》 我会把这套 Java 客户端的代码完整贴出来。
补充一个工程细节:在真实项目里,ExternalControlService 与插件之间并非"连上就完事"。由于仿真可能重启、插件进程可能短暂不可用,我们在客户端实现了断线重连与心跳机制——每隔几秒发一个轻量 ping,若连续多次无回应就主动断开并按指数退避重连,重连成功后重新拉取 WAV_01 与 Enemy_Ship 的当前 track,避免决策基于过期态势。另外,所有下发的指令我们都会本地落日志,记录"时刻 + 指令内容 + 仿真回执",这样既方便复盘交战过程,也便于在 第 15 篇《调试、性能优化与工程化最佳实践》 里做回归测试。这种"连接管理 + 日志留痕"的小心机,是外部控制从 Demo 走向生产系统的关键一步。
五、外部控制时序
把"连接→拉取 track→下发指令→仿真响应"画成时序图,脉络就清晰了:把"连接→拉取 track→下发指令→仿真响应"画成时序图,脉络就清晰了:
小结
- wsf_external_control 插件是仿真对外的"后门",让外部程序在运行中读写状态、下发指令。
- 它本质是 TCP server,监听 127.0.0.1:31000,外部客户端连接后双向收发,指令可靠不丢。
- 外部指令被翻译成 FlyToEvent / PlatformCommandEvent / FireWeaponEvent 等事件,按离散事件仿真机制入队执行(呼应 第 04 篇)。
- 拉取的 track 来自 WsfTrackList(见 第 07 篇),可在外部系统里复现战场态势。
- 真实项目用 Java Spring Boot 的 ExternalControlService 连该端口,实时操控 UAV_01 与 Enemy_Ship;想自己写插件请参考 第 14 篇。!
下期预告
原理讲完了,下一篇直接上代码:《12 Java/Python 接入 AFSIM:TCP 客户端开发实战》 我会给出可对照的 Java TCP 客户端、JSON 指令封装、Spring Boot 集成方式,以及一段 Python 等价实现和常见坑。想亲手把仿真"握"在自己代码里的同学,下期千万别错过,点赞收藏关注走起!
前面几篇,我们的"控制"都写死在 SDL 场景文件里:航线是固定的、任务触发条件是预设的。但真实项目里,指挥决策往往由外部系统实时给出——比如一个 Java 后端根据最新态势算出"让 UAV 飞到这儿""现在开火"。AFSIM 怎么让外部程序在仿真运行中读写状态、下发指令?答案就是本篇主角:wsf_external_control 插件(外部控制插件)。
浙公网安备 33010602011771号