一、前言
最近在进行 AFSIM、DIS、Open-DIS 以及 Wireshark 联调时,需要验证一个比较具体的问题:
如何使用 Python + Open-DIS 构造一个坦克 Entity State PDU,并在 DIS 数据中同时携带坦克炮塔和主炮的部件参数?
前面已经实现了普通 Entity State PDU 的发送,但是在 Wireshark 中只能看到坦克实体本身的信息,例如:
- Entity ID
- Entity Type
- Position
- Velocity
- Orientation
- Marking
而无法进一步验证坦克的:
- 炮塔方位角
- 主炮俯仰角
- 炮塔与主炮之间的附着关系
DIS 的 Entity State PDU 支持可变长度的 Articulation Parameters,可以用来描述实体上的可动部件。Open-DIS 的实现中,EntityStatePdu 维护一个 articulation parameter 列表,序列化时会自动根据列表长度写入参数数量。
本文记录整个实现过程。
二、DIS中的坦克部件如何表示
在 DIS 中,坦克本身是一个 Entity,而炮塔、主炮等可动部件通过 Articulated Parts 进行描述。
可以简单理解成:
text
Tank Entity
│
├── Turret
│ └── Azimuth
│
└── Gun
└── Elevation
例如:
text
坦克
├── 炮塔
│ └── 方位角 45°
│
└── 主炮
└── 俯仰角 5°
对应到 DIS 的 Articulated Parts:
text
ArticulatedParts
├── changeIndicator
├── partAttachedTo
├── parameterType
└── parameterValue
Open-DIS Python 当前 dis7.py 中的 ArticulatedParts 正是按照这几个字段进行序列化的,其中 parameterType 是 32 位枚举,parameterValue 是 64 bit 字段。
三、parameterType 如何确定炮塔和主炮
这里是整个程序最关键的地方。
DIS 的标准示例中,坦克炮塔方位角使用:
text
4096 + 11 = 4107
其中:
text
4096 = Primary Turret
11 = Azimuth
因此:
text
4107 = Primary Turret Azimuth
主炮俯仰角使用:
text
4416 + 13 = 4429
其中:
text
4416 = Primary Gun
13 = Elevation
因此:
text
4429 = Primary Gun Elevation
DIS 官方枚举示例明确给出了这一计算方式:炮塔方位角为 4107,主炮俯仰角为 4429。
因此本文使用:
python
parameterType=4107
表示:
text
1号主炮塔方位角
使用:
python
parameterType=4429
表示:
text
1号主炮俯仰角
四、partAttachedTo 到底是什么意思
这个字段一开始比较容易理解错误。
它不是简单意义上的"部件编号",而是:
当前可动部件附着在哪一个可动部件上。
DIS 标准规定:
text
partAttachedTo = 0
表示:
该部件直接附着在实体本体上。
而 DIS 官方坦克示例中:
text
炮塔 → 坦克
主炮 → 炮塔
因此:
text
炮塔:
partAttachedTo = 0
主炮:
partAttachedTo = 1
这里的 1 表示炮塔这个 articulation part。
DIS 官方示例明确说明,炮塔直接连接坦克,因此 ID - Part Attached to = 0;主炮连接到炮塔,因此 ID - Part Attached to = 1。
因此可以理解为:
text
Tank
│
partAttachedTo = 0
│
Turret
│
Attachment ID = 1
│
Gun
partAttachedTo = 1
需要注意:
partAttachedTo = 1是 DIS 坦克炮塔示例中的部件附着标识,并不是说 DIS 永远规定"炮塔编号就是1"。
五、changeIndicator 是干什么的
changeIndicator 用于表示可动部件参数的变化。
Open-DIS 对该字段的定义是:
text
indicate the change of any parameter for any articulated part.
Starts at zero, incremented for each change
即:
text
从0开始
参数发生变化时递增
例如炮塔初始状态:
text
角度 = 45°
changeIndicator = 0
炮塔发生变化:
text
角度 = 50°
changeIndicator = 1
继续变化:
text
角度 = 60°
changeIndicator = 2
因此:
text
PDU 1:
45° CI=0
PDU 2:
45° CI=0
PDU 3:
50° CI=1
PDU 4:
60° CI=2
它不是 PDU 的发送序号。
DIS 官方坦克示例也特别指出,不同 Articulation Parameter 记录的 Change Indicator 是相互独立的,可以不同。
六、为什么 parameterValue 需要特殊处理
本文使用的 Open-DIS Python 版本中:
python
parameterValue
最终需要按照 DIS 的 64 bit Parameter Value 进行序列化。
当前 Open-DIS Python 的 ArticulatedParts.serialize() 中:
python
outputStream.write_long(self.parameterValue)
也就是说,最终写入的是一个 64 bit 整数形式的数据。
但是对于炮塔角度这样的参数,DIS 参数值本质上是一个浮点值。
因此这里需要将:
text
float32
放入:
text
64 bit Parameter Value
的高 32 位,低 32 位补 0。
本文采用如下方法:
python
def encode(value):
b = struct.pack(">f", float(value)) + b"\0\0\0\0"
n = struct.unpack(">Q", b)[0]
return n - (1 << 64) if n >= (1 << 63) else n
这样,例如:
python
encode(math.radians(45))
就可以得到 Open-DIS write_long() 可以接受的 64 bit 整数。
七、完整代码
下面是本次测试使用的完整程序。
python
# -*- coding: utf-8 -*-
import socket
import time
import math
import struct
from io import BytesIO
from opendis.stream import DataOutputStream
from opendis.dis7 import EntityStatePdu, EntityIdentifier, EntityType, ArticulatedParts
# ============================================================
# UDP和发送频率
# ============================================================
IP = "127.0.0.1"
PORT = 3001
INTERVAL = 0.1 # 10 Hz
# ============================================================
# 将DIS部件参数编码为64 bit Parameter Value
# 高32位:IEEE754 float32,低32位:0
# 当前Open-DIS的write_long()要求传入int64
# ============================================================
def encode(value):
b = struct.pack(">f", float(value)) + b"\0\0\0\0"
n = struct.unpack(">Q", b)[0]
return n - (1 << 64) if n >= (1 << 63) else n
# ============================================================
# Entity ID:Site=1、Application=1、Entity=1001
# ============================================================
eid = EntityIdentifier()
eid.simulationAddress.site = 1
eid.simulationAddress.application = 1
eid.entityNumber = 1001
# ============================================================
# 坦克实体类型
# ============================================================
etype = EntityType()
etype.entityKind = 1
etype.domain = 1
etype.country = 225
etype.category = 1
etype.subcategory = 1
etype.specific = 1
etype.extra = 0
# ============================================================
# 创建Entity State PDU
# ============================================================
pdu = EntityStatePdu(
entityID=eid,
forceId=1,
entityType=etype
)
pdu.exerciseID = 1
# 坦克位置
pdu.entityLocation.x = 1000.0
pdu.entityLocation.y = 2000.0
pdu.entityLocation.z = 100.0
# 坦克速度
pdu.entityLinearVelocity.x = 10.0
pdu.entityLinearVelocity.y = 0.0
pdu.entityLinearVelocity.z = 0.0
# 坦克姿态:DIS使用弧度
pdu.entityOrientation.psi = math.radians(30)
pdu.entityOrientation.theta = 0.0
pdu.entityOrientation.phi = 0.0
pdu.marking.setString("TANK-1001")
# ============================================================
# 炮塔:4107 = Primary Turret Azimuth
# Part Attached To = 0,直接连接坦克实体
# ============================================================
turret = ArticulatedParts(
changeIndicator=0,
partAttachedTo=0,
parameterType=4107,
parameterValue=encode(math.radians(45))
)
# ============================================================
# 主炮:4429 = Primary Gun Elevation
# Part Attached To = 1,连接到炮塔
# ============================================================
gun = ArticulatedParts(
changeIndicator=0,
partAttachedTo=1,
parameterType=4429,
parameterValue=encode(math.radians(5))
)
# ============================================================
# 当前Open-DIS Python版本使用variableParameters
# 两个参数分别表示炮塔和主炮
# ============================================================
pdu.variableParameters = [turret, gun]
# ============================================================
# 序列化PDU
# ============================================================
buf = BytesIO()
pdu.serialize(DataOutputStream(buf))
data = buf.getvalue()
# ============================================================
# UDP循环发送
# ============================================================
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print("PDU size:", len(data), "bytes")
print("Articulation Parameters:", len(pdu.variableParameters))
print("Sending to {}:{} ...".format(IP, PORT))
try:
while True:
sock.sendto(data, (IP, PORT))
print("Send:", len(data), "bytes")
time.sleep(INTERVAL)
except KeyboardInterrupt:
print("Stopped.")
finally:
sock.close()
---
# 八、程序的数据结构
程序最终构造出来的逻辑结构可以表示为:
```text
Entity State PDU
│
├── Entity ID
│ ├── Site = 1
│ ├── Application = 1
│ └── Entity = 1001
│
├── Entity Type
│ ├── Entity Kind
│ ├── Domain
│ ├── Country
│ ├── Category
│ ├── Subcategory
│ ├── Specific
│ └── Extra
│
├── Location
│
├── Velocity
│
├── Orientation
│
├── Marking
│
└── Variable Parameters
│
├── Articulated Part #1
│ ├── Change Indicator = 0
│ ├── Part Attached To = 0
│ ├── Parameter Type = 4107
│ └── Parameter Value = 45°
│
└── Articulated Part #2
├── Change Indicator = 0
├── Part Attached To = 1
├── Parameter Type = 4429
└── Parameter Value = 5°
Open-DIS 的 Entity State PDU 中,Articulation Parameters 本身就是一个可变长度列表,因此这里放入两个 ArticulatedParts 对象后,PDU 会携带两个部件参数。
九、两个部件之间的关系
最终可以理解成:
text
Tank
Entity ID=1/1/1001
│
│
partAttachedTo=0
│
┌─────▼─────┐
│ Turret │
│ │
│ Parameter │
│ 4107 │
│ 45° │
└─────┬─────┘
│
partAttachedTo=1
│
┌─────▼─────┐
│ Gun │
│ │
│ Parameter │
│ 4429 │
│ 5° │
└───────────┘
其中:
text
4107 = Primary Turret Azimuth
4429 = Primary Gun Elevation
DIS 官方示例采用的也是这种坦克炮塔---主炮层级关系。
十、Wireshark中应该重点检查什么
程序发送以后,可以使用 Wireshark 抓取:
text
UDP
→ 3001
然后查看:
text
DIS
└── Entity State PDU
首先检查:
text
Entity ID
应该能够看到:
text
Site ID = 1
Application ID = 1
Entity ID = 1001
然后重点检查:
text
Articulation Parameters
数量应该为:
text
2
因为我们添加了:
python
pdu.variableParameters = [
turret,
gun
]
Open-DIS 在序列化 Entity State PDU 时,会根据 articulation parameter 列表的实际长度写入参数数量。