为什么要开放协议?

因为业余无线电的精神,在于"看得懂、玩得转"。我们开放这份语音数据包格式,核心期望是让大家能开发出兼容网络——不只读懂协议,而是能够基于它造出自己的设备、客户端、授权模式和网络,让网络由参与者共同演化。另外,随着阶段的发展,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 Hz16-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)或冒用已登记正式 ID0x3000 起及注册表中已有条目)。
  • 正式区注册按申请顺序分配,要求提供硬件实拍照片(含手写日期+呼号纸条)等诚意证明。
  • 所有 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/帧)。
  • 接收端按 frameNumindex 将消息包拆回逐个传输帧,依序解码播放;聚合超时机制保证语音不因攒包而额外延迟。

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_pcmstep_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 增量),deltastep>>3 基线起累加,predictorstep_index 限幅(-32768..32767、0..88)后更新。

    • 解码:由 4-bit 码重建 deltastep>>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 的 streamBeginUTCUID 字段):

情形 裁决
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 的规则实现,即可获得全网一致的行为。