为什么要开放协议?

因为业余无线电的精神,在于"看得懂、玩得转"。我们开放这份语音数据包格式,核心期望是让大家能开发出兼容网络——不只读懂协议,而是能够基于它造出自己的设备、客户端、授权模式和网络,让网络由参与者共同演化。另外,随着阶段的发展,FMO也进入了第二阶段:协议开放期。

为什么会有厂家标识(Vendor)?

我们维护厂家标识(vendor),更多是为了让大家都遵守同一种语音编码规则。 一份共同的线格式,是不同设备、不同服务器之间能够互通的前提。它是一份"共同语言",不是一道准入壁垒。

如果你是设备开发者

  • 我们希望你有自己的主张:

    FMO 开发者不会替你解决网络兼容问题。我们已经公布了所有设计思想和文档,当前阶段,您完全可以参考这些,搭建您自己的网络。当然这里会有一些门槛,这些需要您自己设计,而不是CCCV。

  • 请务必尊重每个服务器:

    你应自己发行一个信任根与互联网络,能否被接纳,取决于每一个服务器管理员们,而不是我这种设备的开发者。 因为我们从没声明过,也从没要求过大家建立服务器,大家只是拿到了设备,看了我们的文档,觉得理念相符,就建立了服务器。另外,管理员们从来都不是 FMO 项目开发者的的私有资产,他们不是客户,也不是员工,他们有权、有能力、也有自己的想法。大家只是因为共同的理念在一起,而不是因为我们要求他们在一起。

    如果你的设备做得优秀,思想先进,相信每一位网络管理员都会欢迎它,届时互联经由管理员们自然就实现了。

    当然,你可以开发一个运行于自己的服务器端专有设备,这比发行一个根要轻松一些,我们认为这是良性的,干净的设计。

    但如果只是想做一个"虚拟客户端",借 FMO 的私钥接入网络,我们不推荐这种做法;若你仍选择它,请对自己的所有行为负责 参考文章:关于提取设备私钥的温和提醒。

    我们真心希望你的设备,能够以良好的信任,优秀的设计,赢得所有管理员的欢迎,当他们把您的网络合并进他们的的服务器时,相信你的喜悦会远胜于“只是做了一个虚拟客户端”。

开放格式,不意味着开放一切

动手之前,请了解几点期望与边界:

  • 身份仍受 PKI 系统约束:

    即使你有能力开发兼容语音协议设备,但不意味着你可以畅通无阻的行走在FMO的各个服务器之间,目前FMO网络里真实的身份要求仍然是强制的。你应当自己去赢得管理员们的信任,发行自己的信任根,或让管理员们接受你的服务器理念,或和他们沟通,或让他们自然接受你的设计。总之做好自己的审核,让他们主动接纳你吧。

  • 帧结构由 FMO 项目方主导:

    本协议定义的帧结构不接受外部 PR 变更申请,演进建议请通过 FMO 项目方渠道提出,保证全网互操作性。

  • 兜底机制始终在场:

    爱好者们的服务器可能会部署 FAS 审计自动拦截身份冒用,这是FMO的规则,您可以设计自己的身份方案,但请不要试图绕过 FAS 审计,这是对服务器们的尊重,也是对自己的负责,另外服务器管理员拥有自主管理的黑名单;如果出现了严重的违规行为,我们也会对严重越界行为的用户启动数字证书吊销(CRL)。

  • 参与者的责任:

    如果你是服务器管理员,想桥接外部源,请对你的服务器输入源负责——接入你网络的音频与身份,是你需要把关的第一道门,如果你的服务器出现了问题,第一责任人依然是你自己。

我们最终期望的,是一个平行的网络:

不是一棵中心的树,而是一片由热心的管理员们各自搭建、又彼此融合的网络。管理者有自己的主张,设备有自己的设计,大家在同一个语音编码规则下自由互通。

我们欢迎每一个热爱钻研的人。你探索、你学习、你为自己的行为负责——这正是 FMO 想传递的业余无线电精神。



本文定义语音数据格式,供实现互通的设备/服务器参考。所有多字节字段均为小端序。文档以一条语音消息从外到内逐层展开:消息包 → 传输帧 → 编码语音帧 → 编码载荷。

1. 协议总览

消息包(≤1400B)
├─ 消息头(固定 64B)
└─ 传输帧 × N(N 个编码语音帧聚合)
     ├─ 传输帧头(8B)
     └─ 编码语音帧
          ├─ 编码语音帧头(8B)
          └─ 编码载荷
               ├─ OPUS 帧   (变长,约 40-60B / 40ms)
               └─ RADPCM 帧 (固定 328B / 80ms)
发送端:模拟音频 → 采样 → 编码(OPUS/RADPCM)→ 编码语音帧 → 传输帧 → 消息包 → 网络发布
接收端:网络接收 → 解析消息包 → 拆分传输帧 → 解析编码语音帧 → 解码 → 模拟音频输出

公共音频参数:采样率 8000 Hz,16-bit 有符号,单声道。

2. 消息包

2.1 语义

一条消息包是语音数据的基本传输单元,携带同一发送者的一段连续语音。包头为固定 64B,其后为 1..N 个传输帧(见 §3)。

2.2 消息头(64B)

业务头 45B + 扩展区 19B:

偏移 大小 字段 说明
0 2 version 包版本,当前为 1
2 4 vendor 数据包厂家标识(0=FMO),见 §2.3
6 4 UID 发送者用户 ID
10 12 callsign 呼号,不足补 0
22 4 streamBeginUTC 语音流起始时间(UTC ms)
26 4 timestamp 本包生成时间(UTC ms)
30 4 len 整个消息包长度(含头部)
34 2 frameNum 本包内传输帧个数
36 4 checkSum CRC32,仅覆盖帧区
40 1 smeter S 表强度
41 4 srvUID 当前连接服务器 UID
45 19 reserved 扩展区,总头长固定为 64B

2.3 厂家标识(vendor)

vendor 字段标识数据包的生产厂家,用于设备识别与溯源。发放区间:

区间 类别 获取方式
0x0000 ~ 0x0FFF 保留区(FMO 项目方) 不开放,他人不得使用
0x1000 ~ 0x1FFF 实验区 自由取用,不登记、不审核
0x2000 ~ 0x2FFF 软件区 自由取用,不登记、不审核
0x3000 ~ 0xFFFFFFFF 正式区 经 fmo-raw-vendor-id 仓库 PR 申请审核后登记
  • 软件实现(PC/Web/SDK 等无硬件载体)请使用软件区;实验/原型项目请使用实验区,无需申请;自由取用不构成 FMO 的 ID 分配,不具权威,ID 冲突由 FMO 项目方裁决。
  • 严禁使用保留区(0x0000 ~ 0x0FFF)或冒用已登记正式 ID(0x3000 起及注册表中已有条目)。
  • 正式区注册按申请顺序分配,要求提供硬件实拍照片(含手写日期+呼号纸条)等诚意证明。
  • 所有 vendor ID 由 FMO 项目方分配,厂商自行分配的 ID 无效。
  • vendor 仅用于设备识别/溯源,不参与鉴权(鉴权依赖证书体系)。

声明:厂家编码分配仅为开放登记,不构成我方对申请厂家及其产品的任何认可、背书或保证。我方不担保注册厂家的设备体验、协议实现正确性、安全性或与网络的互通质量,相关责任由厂家自行承担;对严重违反协议或损害通联网络的厂家,我方保留拒绝注册或撤销编码的权利。

3. 传输帧

3.1 承载语义

一个传输帧承载一个编码语音帧,即一次编码周期的输出,对应固定时长的语音:OPUS 帧为 40ms、RADPCM 帧为 80ms。传输帧是消息包与编码语音帧之间的传输层单元,第一个传输帧紧贴消息头之后。

3.2 帧结构(8B 头)

偏移 大小 字段 说明
0 2 index 帧序号,从 1 开始递增
2 2 len 含本帧头在内的长度
4 4 reserved 保留
8 - data 编码语音帧载荷

3.3 聚合语义

  • 发送端把连续产生的编码语音帧依次追加为传输帧;当追加后超过 MTU = 1400B,或聚合时长达到 250ms 时,结束当前消息包并发布。
  • 因此一个消息包通常含 ≤6 个 OPUS 帧(40ms/帧)或 ≤3 个 RADPCM 帧(80ms/帧)。
  • 接收端按 frameNum 与 index 将消息包拆回逐个传输帧,依序解码播放;聚合超时机制保证语音不因攒包而额外延迟。

4. 编码语音帧

4.1 语义

编码语音帧是一次编码的输出,独立携带 compressMode 声明编码类型,并自带长度,接收端可据此与解码器解耦。

4.2 帧结构(8B 头)

偏移 大小 字段 说明
0 1 compressMode 编码类型,见下表
1 2 len 含头在内的整个编码语音帧长度
3 5 reserved 保留(编码参数扩展位)
8 - raw 编码载荷起始

4.3 编码类型(compressMode)

值 编码
0 PCM(预留,不用于网络传输)
1 OPUS
2 RADPCM

5. 编码载荷(raw)

5.1 OPUS 帧

OPUS 编码器的直接输出,无额外包头,长度变长(编码参数见 §6.1)。

5.2 RADPCM 帧(固定 328B)

8B 帧头 + 320B 数据区:

偏移 大小 字段 说明
0 2 frame_index 帧序号(0 起,uint16_t 范围内循环)
2 2 recover_pcm 编码开始时预测器值
4 1 step_index 编码开始时步长索引(0-88)
5 1 reserved 保留
6 2 adpcmBytes 数据区有效字节数(=320)
8 320 数据区 每字节 2 个 4-bit 样本:高 nibble 为第 1 个,低 nibble 为第 2 个;共 640 样本(80ms)

兼容性注记:早期实现的 adpcmBytes 为 8-bit 字段(线值因宽度截断实际为 64,且帧头含未初始化填充字节)。新实现按 16-bit 读写,并兼容旧格式包(以其低字节判定)。建议新实现直接使用 16-bit 语义。

recover_pcm 与 step_index 组成帧恢复点:丢包后解码器据此重置 IMA 编解码状态(见 §6.2)。

6. 编码规范

6.1 OPUS

参数 值
采样率 / 通道 8000 Hz / 1
应用模式 VoIP(OPUS_APPLICATION_VOIP)
比特率 自动(OPUS_AUTO)
复杂度 / 信号 4 / 语音(OPUS_SIGNAL_VOICE)
VBR / VBR 约束 启用
帧长 40ms(320 样本 / 640B PCM)
最大带宽 超宽带(受 8kHz 采样率限制,实际 ≤4kHz)

自动比特率推导:

bitrate = 60 × Fs / frame_size + Fs × channels
        = 60 × 8000 / 320 + 8000 × 1
        = 1500 + 8000 = 9500 bps(约 1.19 KB/s)→ 每帧约 47.5B,VBR 下典型 40-60B

6.2 RADPCM

  • 每帧 640 样本(80ms,1280B PCM)→ 320B ADPCM,压缩比 4:1(不计帧头)。
  • 直流抑制(编码器侧):y[n] = x[n] - x[n-1] + R·y[n-1],R = 0.999(Q15 32735),截止约 1.3Hz。
  • 丢包恢复(解码器维护 last_frame):
1. last_frame == 0xFFFF(首帧)→ 用帧恢复点重置状态
2. frame_index == 0             → 视为新语音序列开始,重置状态
3. frame_index != last_frame+1  → 检测到丢包,用帧恢复点重置状态
4. 其余                        → 连续帧,正常解码
  • IMA 核心算法:

    • 89 级步长表 IMA_STEP_TABLE:
    7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34,
    37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143,
    157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494,
    544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552,
    1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428,
    4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487,
    12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086,
    29794, 32767
    
    • 16 项索引表 IMA_INDEX_TABLE = {-1,-1,-1,-1, 2, 4, 6, 8, -1,-1,-1,-1, 2, 4, 6, 8}。

    • 编码:diff = pcm - predictor,按 step = STEP_TABLE[step_index] 逐级比较生成 4-bit 码(bit3 符号,bit0-2 增量),delta 自 step>>3 基线起累加,predictor、step_index 限幅(-32768..32767、0..88)后更新。

    • 解码:由 4-bit 码重建 delta(step>>3 基线 + 各 bit 增量),更新 predictor 后输出。

说明:发送/接收端可另做音频滤波等增强处理,不影响上述线格式与互通。

7. 尺寸速查

项 字节数
编码语音帧头 8
传输帧头 8
消息包头 64
OPUS 帧载荷 约 40-60(变长)
RADPCM 帧载荷 328(固定)

RADPCM 帧独占一条消息时总尺寸:64 + 8 + 8 + 328 = 408B / 80ms;OPUS 约 130B / 40ms。实际多帧聚合共享消息头,单帧摊薄后更小。

8. PTT冲突控制

8.1 设计原则

对讲机通信本质上是半双工的:同一信道在任意时刻只应有一个发送者,否则语音会互相覆盖(混叠)。FMO 把这种约束从射频链路延伸到了网络链路,通过三层机制保证多设备、多服务器场景下通联的公平与有序:

层次 机制 作用范围
信道感知 占用检测 设备 ↔ 本地对讲机
方向互斥 单一数据方向通道 设备内部收发
网络仲裁 路由抢占裁决 设备 ↔ 设备 / 跨服务器

目标是:谁先开口谁优先,但任何一方都不能无限期独占信道;被更高优先级打断的一方应当明确感知到"台被抢断"。

8.2 信道占用检测

  • 信道被占用时,设备不键控发射,即使本地有下行语音需要转发到 RF;
  • 同时向用户提示"通道繁忙,请停止发射"并播放忙音,以固定间隔重复提示,避免刷屏。
本地发射请求 → 信道占用? ──是──→ 抑制 PTT,提示"通道繁忙"
                    │
                    └─否──→ 键控 PTT,开始发射

该机制属于物理层的冲突避免,防止设备在他人占台时强行插入射频信道。

8.3 半双工通道互斥

设备内部维持单一数据方向,同一时刻只允许「本机→网络」或「网络→本机」之一进行:

  • 当前方向仍有数据在途时,反向请求被拒绝;
  • 当前方向超过 1s 无数据,才允许切换到反方向。
数据方向  ⇄  RADIO_TO_NETWORK / NETWORK_TO_RADIO 二选一
切换条件:反向有数据 && 当前方向空闲超过 1s

该机制在设备侧杜绝了自身收发混叠,等效于把对讲机的半双工特性平移到网络链路。

8.4 网络路由仲裁

核心思想:任意时刻全网只有一个"当前讲话者",由各设备对路由的竞争结果决定。路由携带两个属性:占用者的身份(UID,见 §2.2)与占用窗口。窗口为 1500ms,窗口内路由不可被轻易切换,除非后者拥有更高优先级。

路由仲裁规则(依据消息头 §2.2 的 streamBeginUTC 与 UID 字段):

情形 裁决
同 UID 续占,刷新占用窗口
当前路由已超时(1500ms) 新包占用
新包 streamBeginUTC 更早(差值 ≤ 2s) 抢占
新包 streamBeginUTC 相同且 UID 更小 抢占
其余情况 拒绝
1. UID 相同             → 续占,刷新窗口
2. 路由超时             → 新包占用
3. streamBeginUTC 更早(≤2s) → 抢占
4. streamBeginUTC 相同 && UID 更小 → 抢占
5. 否则                 → 拒绝

两点说明:

  • 2s 上限:只有"起始时间更早"(而非更晚)的流才允许抢占,且差值不得超过 2s。这防止因网络传输延迟,使一条"出生更晚"的流反而以更大时差抢断当前讲话者,造成播放乱序。
  • 被抢占方行为:当设备发现自己正占用的路由被他人抢占,应立即停止上行,并向用户提示"通道繁忙"——讲话被打断的用户能够明确感知"我的台被抢了"。

仲裁完全基于消息头既有字段,不引入任何协议扩展;所有互通设备/服务器必须按同一套规则裁决,才能保证全网对"当前讲话者"的认知一致。

8.5 上行时长限制

为防止单一方长时间独占信道,单次连续上行设有时长上限:

  • 默认 60s;
  • 可配置为 0(不限)、30、60、90、120s;
  • 达到上限后强制停止上行,并提示用户。

该限制保证语音信道在多人通联中能够流转,属于"防独占"的兜底手段。

8.6 字段关联

PTT 冲突控制复用的消息头字段(见 §2.2):

字段 用途
UID 路由占用者身份、仲裁判定与平局裁决
streamBeginUTC 语音流起始时间,仲裁的时序依据

实现互通时无需为冲突控制扩展任何新字段,仅需在设备/服务器侧按 §8.2 ~ §8.5 的规则实现,即可获得全网一致的行为。