📹 第七章 ONVIF协议原理解析
📄 一句话概括

PTZ 最反直觉的地方:ONVIF 不告诉你「转多少度」,只说「转到 -1 到 1 之间的某个值」。 而 y 轴的负方向是「下」——这是数学坐标系,不是屏幕坐标系。

📝 本节和前面几节的关系

上一节讲事件,这一节讲「能动的那部分」。 PTZ 是 07.02 里唯一「发出去就立刻有物理后果」的一组——前面几节都是读或改配置, 这一节会让云台真的转起来。

一、GetNodes 告诉你能做什么

项 本例的值 含义
MaximumNumberOfPresets 32 预置位数量上限
HomeSupported true 支不支持「回原位」

预置位是个很实用的功能:把「看大门口」存成预置位 1,以后一句话就转过去。 它的上限是设备能力的一部分,有的设备只给 8 个,有的给 32 个(本例 32)。 做「自动巡航」时这个数就是循环次数的上限。 第二行的 HomeSupported 常被忽略:支持的话可以 GotoHomePosition 一键回原位, 不支持就只能自己记一个位置再 AbsoluteMove 回去。 后者的问题是「原来在哪」这件事设备不记得——如果你的程序也不知道,那就回不去了。

二、绝对位置和连续速度,两套空间

支持哪些坐标空间,也是 GetNodes 的一部分:

空间类型 URI 尾段 含义
AbsolutePanTiltPositionSpace PositionGenericSpace 转到哪
ContinuousPanTiltVelocitySpace VelocityGenericSpace 以多快转

两种都能用,看你要什么:要精确定位就用绝对位置,要手感(像摇杆)就用速度。 GenericSpace 这个词的意思是「通用空间」——它的范围就是 -1 到 1,不映射到任何实际角度。 所以设备不告诉你「-1 对应多少度」,这是规范故意的:不同镜头的视野不一样,没法统一。 一个 2.8 毫米广角和一个 12 毫米长焦,同样的 -1 到 1 覆盖的角度差好几倍。 所以「转到东边 30 度」这种需求在 ONVIF 里无法直接表达, 只能靠「先转到某个已知位置、再按比例推」。

三、ContinuousMove 的参数和那个负号

连续移动的请求长这样:

<tptz:ContinuousMove>
  <tptz:ProfileToken>Profile_1</tptz:ProfileToken>
  <tptz:Velocity>
    <tt:PanTilt x="0.5" y="-0.2"/>
    <tt:Zoom x="0"/>
  </tptz:Velocity>
</tptz:ContinuousMove>
参数 范围 含义
PanTilt x -1 ~ 1 左右:负是左,正是右
PanTilt y -1 ~ 1 上下:负是下,正是上
Zoom x -1 ~ 1 变倍:负是广角,正是长焦

第二行那个「负是下」容易记反:这是数学坐标系的习惯(y 向上为正), 不是屏幕坐标(y 向下为正)。 所以「想往下看」要发 y=-0.5,不是 +0.5。 这个反直觉的设定,是 PTZ 调试里最常见的错误来源。 它的表现是「方向反了」而不是「报错」,所以调试时很容易怀疑是设备装反了, 实际上是符号写反了。 建议第一次接设备时,四个方向各发一次,记在纸上。

四、发了不停,要自己叫停

ContinuousMove 返回成功不等于云台会自己停:它一直转到收到 Stop 或者撞到限位。

场景 会发生什么 怎么防
客户端正常发 Stop 停住 —
客户端崩了 一直转,撞限位才停 设 Timeout
网络断了 同上 设 Timeout

「发了不停」是真实故障:客户端崩了、网络断了,云台就一直转,撞到限位才停。 所以工程做法是两条:① 移动时同时发一个「超时自动停」;② 客户端异常退出时兜底发 Stop。 第三列那个 Timeout 是 ContinuousMove 的可选参数,给了它就自动停,不给就一直转。 「不给」是最常见的错——因为不报错、调试时也看不出问题,只有在异常退出时才暴露。 这和第七章前面那些「静默失败」是一类东西,特征都是「跑得通,但出事时才发现少了一层保护」。

五、动手 Demo:云台,坐标是归一化的

Demo 先问能力(能存几个预置位、支持哪些坐标空间), 再发一次连续移动、一次停止,最后查状态。 本机没有云台,所以「动没动」验不了,只能验「报文被接受」。

# ① 能力
_a, b, _n = S.call(port, "ptz", "GetNodes")
node = ET.fromstring(b).find(".//{%s}PTZNode" % S.NS_TPTZ)
print(node.find("{%s}MaximumNumberOfPresets" % S.NS_TT).text)
for sp in node.iter("{%s}URI" % S.NS_TT):
    print(" ", sp.text.split("/")[-1])

# ② 连续移动(y 负是下)
move = ("<tptz:ContinuousMove>"
        "<tptz:ProfileToken>Profile_1</tptz:ProfileToken>"
        "<tptz:Velocity><tt:PanTilt x=\"0.5\" y=\"-0.2\"/>"
        "<tt:Zoom x=\"0\"/></tptz:Velocity>"
        "</tptz:ContinuousMove>")
_a, b2, _n = S.call(port, "ptz", "ContinuousMove", move)
print("move fault:", b2.count("Fault"))

# ③ 停,再查状态
stop = ("<tptz:Stop><tptz:ProfileToken>Profile_1</tptz:ProfileToken>"
        "<tptz:PanTilt>true</tptz:PanTilt>"
        "<tptz:Zoom>true</tptz:Zoom></tptz:Stop>")
_a, b3, _n = S.call(port, "ptz", "Stop", stop)
print("stop fault:", b3.count("Fault"))

_a, b4, _n = S.call(port, "ptz", "GetStatus",
                   "<tptz:GetStatus><tptz:ProfileToken>Profile_1"
                   "</tptz:ProfileToken></tptz:GetStatus>")
st = ET.fromstring(b4).find(".//{%s}PTZStatus" % S.NS_TT)
pt = st.find("{%s}Position/{%s}PanTilt" % (S.NS_TT, S.NS_TT))
print(pt.get("x"), pt.get("y"))

实跑输出:

--------------------------------------------------------------------
云台,坐标是归一化的
--------------------------------------------------------------------
**PTZ 最反直觉的地方:ONVIF 不告诉你「转多少度」,只说「转到 -1 到 1 之间的某个值」。**
  这一节把坐标空间、速度、状态三个东西看清楚。

**`GetNodes` 告诉你能做什么:**

  项                          值                        含义
  --------------------------------------------------------------------------------------
  `MaximumNumberOfPresets`    32                        **预置位数量上限**
  `HomeSupported`             true                      支不支持「回原位」

  **预置位是个很实用的功能**:  **把「看大门口」存成预置位 1,以后一句话就转过去。**
  **它的上限是设备能力的一部分**,  **有的设备只给 8 个,有的给 32 个**(本例 32)。
  **做「自动巡航」时这个数就是循环次数的上限。**

**支持哪些坐标空间:**

  空间类型                          URI 尾段
  --------------------------------------------------------------------------
  AbsolutePanTiltPositionSpace      `PositionGenericSpace`
  ContinuousPanTiltVelocitySpace    `VelocityGenericSpace`

  **`PositionSpace` 是「转到哪」,`VelocitySpace` 是「以多快转」。**
  **两种都能用,看你要什么**:  **要精确定位就用绝对位置,要手感(像摇杆)就用速度。**
  **`GenericSpace` 这个词的意思是「通用空间」**——
  **它的范围就是 -1 到 1,不映射到任何实际角度。**
  **所以设备不告诉你「-1 对应多少度」**,  **这是规范故意的:不同镜头的视野不一样,没法统一。**

**连续移动(`ContinuousMove`)的几个参数:**

  `<tptz:ContinuousMove>`
  `  <tptz:ProfileToken>Profile_1</tptz:ProfileToken>`
  `  <tptz:Velocity><tt:PanTilt x="0.5" y="-0.2"/>`
  `    <tt:Zoom x="0"/></tptz:Velocity>`
  `</tptz:ContinuousMove>`

  参数            范围                含义
  ----------------------------------------------------------------------------
  `PanTilt x`     -1 ~ 1              **左右:负是左,正是右**
  `PanTilt y`     -1 ~ 1              **上下:负是下,正是上**
  `Zoom x`        -1 ~ 1              **变倍:负是广角,正是长焦**

  **第二行那个「负是下」容易记反**:
  **这是数学坐标系的习惯(y 向上为正)**,  **不是屏幕坐标(y 向下为正)。**
  **所以「想往下看」要发 `y=-0.5`,不是 `+0.5`。**
  **这个反直觉的设定,是 PTZ 调试里最常见的错误来源。**

**连续移动不停,要自己叫停:**

  **`ContinuousMove` 返回成功(是),`Stop` 返回成功(是)**

  **「发了不停」是真实故障**:
  **客户端崩了、网络断了,云台就一直转,撞到限位才停。**
  **所以工程做法是两条**:
  **① 移动时同时发一个「超时自动停」;② 客户端异常退出时兜底发 `Stop`。**
  **ONVIF 的 `ContinuousMove` 有个可选的 `Timeout` 参数**,
  **给了它就自动停,不给就一直转。**  **不给是最常见的错。**

**状态查询(`GetStatus`)返回现在在哪:**

  字段                      值
  --------------------------------------------------------
  `PanTilt`                 x=0.25 y=-0.1
  `Zoom`                    x=1.0
  `UtcTime`                 2024-05-01T12:00:00Z

  **第三行那个时间是 UTC**——
  **设备报的是绝对时刻,不是「相对某个起点」。**
  **所以连续两次 `GetStatus` 的时间差就是真实间隔**,
  **这一点比很多私有协议强(那些只给个计数器)。**
  **不过位置还是归一化的**:
  **`x=0.25` 到底指向哪,只有设备自己知道。**
  **要做「转到某个预置位」,用预置位 Token,不要用坐标。**

GetStatus 返回的位置是我在模拟设备里写死的(x=0.25 y=-0.1), 它不会因为前面那次 ContinuousMove 而改变——因为模拟设备根本没有电机。 这一点必须说清,否则会误以为「移动之后状态会跟着变」。 三次调用都是「返回成功」,这只能说明「设备接受了报文」,不能说明「云台真的动了」。 真设备上要验「动没动」,只能看画面或者 GetStatus 的位置变化。 Stop 的两个布尔参数(PanTilt/Zoom)决定停哪个轴,都写 true 就是全停。 只停一个轴的场景也有(比如只停变倍、继续转), 但那样容易出「转到一半停住」的怪状态,一般不建议。

30 秒实验

  1. 把 y 从 -0.2 改成 0.2,想想画面上会往上还是往下。
  2. 把 Zoom x 改成 -1,看模拟设备是接受还是回 Fault。
  3. 给 ContinuousMove 加一个 Timeout 参数(PT5S),看模拟设备认不认。

常见误区

  1. 以为坐标是角度。 它是归一化的 -1 到 1,不映射到任何实际角度,因为不同镜头视野不同。
  2. 以为 y 正是「下」。 y 负才是下,这是数学坐标系,不是屏幕坐标系。
  3. ContinuousMove 不带 Timeout。 云台会一直转,客户端崩了就撞限位,必须设超时。
自测三问(点开对答案) 1. 为什么 ONVIF 不告诉你「-1 对应多少度」? —— 因为**不同镜头的视野不一样**,**同样 -1 到 1 覆盖的角度差好几倍**,没法统一。 2. 「往下看」应该发 `y=0.5` 还是 `y=-0.5`? —— **`y=-0.5`。** PTZ 用数学坐标系,**y 负是下**。 3. `ContinuousMove` 最容易被漏掉的参数是什么?为什么危险? —— **`Timeout`。** 不带的话云台会一直转,**客户端崩溃或断网时就撞限位**。

下一节讲视频分析:规则和模块是两回事,以及「能画框」为什么不是 ONVIF 的基本能力。

相关笔记