Open-DIS Python发送DIS实体状态PDU:实现坦克炮塔与主炮部件参数

一、前言

最近在进行 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 列表的实际长度写入参数数量。

相关推荐
2601_962283881 小时前
Python 开发框架:Django、Flask和FastAPI
python·django·flask·fastapi·web开发
魏 无羡1 小时前
springboot 拦截器
java·spring boot·后端
2601_962299881 小时前
排序,然后再使用
python·排序·列表·关键函数·装饰-排序-去装饰
步行cgn1 小时前
Spring Boot 绑定嵌套 Bean 详解
java·spring boot·后端
Looooking1 小时前
Python 之 Flask 的全局变量 g
后端·python·flask
SatanII1 小时前
OpenStack核心组件详解|Keystone+Glance+Nova+Cinder原理与实操笔记
网络·笔记·openstack
吹什么轩2 小时前
linux网络:UDP原理
linux·网络·udp
棣廷2 小时前
初识Python异常处理、模块与包
python