新 DXCC 自动呼叫 —— 端到端机制说明

本文档完整阐述 MRRC-FT8 的”新 DXCC 自动呼叫”(auto-call)功能:从空中信号解码, 到实体判定、自动应答、QSO 完成、落库刷新的全链路机制、配置、安全设计与运维观察点。 适用版本:SDD V1.5(2026-08-08,含 UC-003/UC-004 频率纪律、AD-008 串口守卫、band-hunt 实体名归一化)。


1. 功能定位

auto_call_new_dxcc 是服务端的无人值守 DXCC 追猎开关:当解码带内出现一个 尚未通联过的 DXCC 实体的 CQ,且系统空闲(无 QSO、无人为选择、无发射中), 服务端自动按标准 FT8 流程应答并完成一次 QSO,全程无需操作员干预。

目标(NFR-087):DXCC 实体通联自动化。实体已通联后不再触发(worked 集实时刷新), 因此重复通联同一实体不会发生。

1.1 名词

术语 含义
DXCC 实体 cty.dat(country-files ADIF 格式)划分的通联计分单位,如 Niger(TN 前缀)
worked 集 qso 表中所有非 void QSO 的呼号(去 /后缀)对应的已通联实体名集合
is_new_dxcc 解码消息的发起呼号 → 实体名 ∈ 实体表 且 ∉ worked 集
interlock safety 层互锁(CAT/audio/DSP/clock),任一 faulted 时拒绝 arm/TX
tx_phase 发射奇偶(0=偶时隙,1=奇时隙),应答必须发在伙伴消息的相反时隙
tx_frequency TX 音频偏移(Hz);应答跟随伙伴解码频率,CQ 选空闲频点

2. 端到端链路总览

图 1 端到端链路总览:从空中解码到 worked 集刷新 12 kHz int16 单声道(铁律) FT-710 → capture_proc 子进程 → UtcRing(绝对序号) → DSP Worker 每 15 s slot 一批解码 orchestrator.on_decode(main.py) 1. DXCC cache 刷新 dxcc_dirty 时同步重建,~0.2 s 每 slot 至多一次 2. decode_message_view is_new_dxcc = lookup(call) ∈ 实体 ∧ ∉ worked 3 auto_call_candidate 决策(纯函数) 开关开 ∧ 空闲 ∧ 无选择 ∧ is_new_dxcc ∧ is_cq ∧ ¬mine 4 _auto_call:safety.arm()(互锁门)→ sequencer.reply_to() tx_phase = 1 − slot%2 · tx_frequency = 伙伴解码频率(UC-003) 失败 → 记日志跳过,下一 slot 重试 · audit 落 auto_call 5 TX 链路:TxDriver(奇偶泵 + I9 窗口 + fit guard) next_tx_message() → dsp_encode 48 kHz → safety.transmit(PTT 门 + watchdog + 预算) 6 sequencer QSO 状态机(NFR-055:1 发 + 3 重传) REPLYING → REPORT → ROGER_REPORT → ROGERS → SIGNOFF/DONE 7 落库:_ensure_log → on_qso → qso_log.enqueue → record_qso qso 表 source='live' · dxcc_dirty = True 8 下一 slot:DXCC cache 重建 → 实体进 worked → 不再触发 worked 集闭环
图 1 端到端链路总览:从空中解码到 worked 集刷新

3. 环节详解

3.1 信号采集与解码

图 2 信号采集与解码链:进程边界与采样率铁律 进程边界 FT-710 capture_proc 子进程 UtcRing slot_id % capacity DSP Worker 全局锁 · 0–4 · A8 门控 DecodeResult slot_id / snr / dt / frequency / text on_decode (main.py) RX 恒 12 kHz int16 单声道(binding.py 强制) TX 波形恒 48 kHz 每 15 s slot,orchestrator 把整槽音频送 Worker 解码(WSJT-X 3.0.2 Improved) OpenMP 线程只在 Fortran 聚合结果,绝不回调 Python
图 2 信号采集与解码链:进程边界与采样率铁律

3.2 DXCC 实体判定

图 3 DXCC 实体判定:cty.dat 三规则 lookup 与 worked 集比较 cty.dat (country-files ADIF 格式,仓库内静态副本) CtyDatabase 解析 =CALL 精确前缀优先 (数字) 替换(如 (23))加载时展开 最长前缀匹配(TN → Niger) lookup 已索引化:精确表 + 前缀 trie 单次亚微秒 lookup("TN8GD") 实体 = Niger ∈ worked 集? worked 集 = dxcc_summary(qso 非 void 记录 × cty 查表) 不触发 (已通联) is_new_dxcc = true 进入候选判定(图 4) cache 未建时保守为 False(不误触发)
图 3 DXCC 实体判定:cty.dat 三规则 lookup 与 worked 集比较

3.3 决策门(auto_call_candidate 纯函数)

每 slot 按解码消息顺序检查,至多命中第一个候选:

return (
    auto_call_enabled            # 后端开关 setting_meta.auto_call_new_dxcc == True
    and not has_selection        # 无人工选择(P7 修复后 QSO 结束会清 selected)
    and sequencer_state == QSOState.IDLE.value   # 无 QSO、无 CQ 循环
    and bool(view.get("is_new_dxcc"))
    and bool(view.get("is_cq"))
    and not view.get("mine")     # 自身发射回波(from_call == 本台)不触发
)
图 4 决策门 auto_call_candidate:六条件 AND,每 slot 至多命中第一个 每 slot 按解码消息顺序检查,至多命中第一个候选 auto_call_enabled — setting_meta.auto_call_new_dxcc ¬ has_selection — QSO 结束清 selected(P7) sequencer_state == IDLE — 无 QSO、无 CQ 循环 is_new_dxcc is_cq ¬ mine — 自身发射回波不触发 全部满足 AND 六条件同时为真 命中本 slot 第一个候选 → _auto_call
图 4 决策门 auto_call_candidate:六条件 AND,每 slot 至多命中第一个

3.4 触发与安全门(_auto_call

图 5 触发与安全门:safety.arm 互锁矩阵与 skip/重试 互锁矩阵(任一 faulted → arm 拒绝、PTT-off) CAT audio DSP clock 候选命中(图 4)→ _auto_call safety.arm() 唯一的 TX 门 任一互锁 faulted → TxRefused log: auto_call skipped: interlock open (<call>) 跳过本 slot,下一 slot 重试 不崩溃、不风暴 arm 成功 → 记录选中消息(call/grid/snr/slot) sequencer.reply_to(...) tx_phase = 0 if slot_id is None else 1 − (slot_id % 2) tx_frequency = 伙伴解码频率(UC-003) 旧行为写死 1500 Hz(见 §7 复盘) 系统级、无人值守:不需要控制租约(NFR-087) audit_event:actor=system · operation=auto_call detail=snr=… new_dxcc · target=呼号
图 5 触发与安全门:safety.arm 互锁矩阵与 skip/重试

3.5 应答发射(TX 链路)

图 6 应答发射 TX 链:TxDriver 时隙泵 → safety.transmit → rigctld TxDriver 时隙奇偶泵 I9 决策窗口 5.0 s fit guard 12.64 s ≤ 15 s slot dsp_encode 48 kHz 波形 safety.transmit PTT 门 + watchdog rigctld 串口唯一 owner FT-710 PTT 错过 → deferring 下一 slot TxDriver:只在 sequencer 的 tx_phase 对应奇偶 slot 拉取至多一条,编码后交 safety.transmit() I9 决策窗口:sequencer 空闲时持续轮询到 5.0 s 截止,点击/自动呼叫一 arm 就发 fit guard:12.64 s 波形 + 0.2 s 启动必须塞进 15 s slot,错过则顺延下一可用 slot dsp_encode:应答用伙伴解码频率(split,对方接收机能配对);CQ 用 FrequencyOccupancy 空闲频点 safety.transmit:复核 faults / armed / PTT 状态 / 聚合 TX 预算;watchdog 与 STOP 可随时取消 PTT 经 rigctld(串口唯一 owner,AD-008)
图 6 应答发射 TX 链:TxDriver 时隙泵 → safety.transmit → rigctld

3.6 QSO 状态机(sequencer)

应答侧流程(我们是应答者):

图 7 QSO 状态机(应答侧):REPLYING → DONE 与中断出口 应答侧(我们是应答者)· 每条消息 1 发 + 至多 3 重传(NFR-055) REPLYING Tx1 <dx> <my> <my-grid> (先发本台呼号+网格) 对方报告到达 REPORT Tx2 <dx> <my> -snr ROGER_REPORT Tx3 <dx> <my> R-snr 对方 R 报告到达 ROGERS Tx4 <dx> <my> RR73 对方 RR73/73 到达 SIGNOFF → DONE Tx5 <dx> <my> 73 → 落库 on_message 只处理 addressed-to-me;自身回波(mine)忽略 PARTNER_LOST 伙伴转呼他人 → anti-QRM 自动停 RETRY_EXHAUSTED 预算耗尽 → 停并保留伙伴上下文 on_stop:清除 selected(P7)
图 7 QSO 状态机(应答侧):REPLYING → DONE 与中断出口

3.7 落库与 worked 集刷新

图 8 落库与 worked 集刷新:dxcc_dirty 闭环 收到 RR73/73 _ensure_log(恰好一次) on_qso qso_log.enqueue 注:asyncio.to_thread record_qso qso 表 source='live' · status='completed' my/dx call+grid · 双方 report · freq_hz/band qso_event · dxcc_dirty = True 下一 slot:on_decodedxcc_dirty → 重建 dxcc_summary (~0.2 s,每 slot 至多一次) 实体 ∈ worked → 不再触发 进程崩溃 data/qso-pending.jsonl 待办恢复(不丢记录) P1 修复:cache 刷新独立于 band-hunt 开关
图 8 落库与 worked 集刷新:dxcc_dirty 闭环

3.8 关联功能:自动波段狩猎(band_hunt)

图 9 band-hunt 闭环:双闸门 → 轮询 → 归一化 → 排序切频 → 自动呼叫 ① 双闸门 auto_band_hunt ∧ 冷却 1200 s ② 轮询 /api/band_hunt pskreporter · HTTP 唯一跨库边界 ③ 实体名归一化(V1.5) _CTY_NAME_ALIASES ④ rank/decide_switch 按未通联实体数降序 ⑤ 空闲时 rig 调谐切频 经 rigctld 切到目标波段 ⑥ 新波段解码 auto-call 闭环自然接续 把波段切到新 DXCC 最密集的地方 → 自动呼叫闭环接续 CQ 空闲频点(UC-004):占用环 120 s TTL · 1500±300 Hz 螺旋 · guard 30 Hz · 全占用回退 1500
图 9 band-hunt 闭环:双闸门 → 轮询 → 归一化 → 排序切频 → 自动呼叫

4. 配置与开关

途径 默认 说明
auto_call_new_dxcc Web UI FT8 tab / PUT /api/v1/settings 关闭 后端持久化(setting_meta 表);boot 时后端值覆盖前端默认
auto_band_hunt 同上 关闭 波段狩猎第二闸门
MRRC_FT8_BAND_HUNT_URL 环境变量 空 = 关闭 pskreporter /api/band_hunt 地址
MRRC_FT8_BAND_HUNT_COOLDOWN 环境变量 1200 s 切波段冷却
MRRC_FT8_LOG_LEVEL 环境变量 DEBUG(restart.sh) 观察 auto_call 日志
MRRC_FT8_SKIP_SERIAL_GUARD 环境变量 未设置(守卫生效) 应急跳过 restart.sh 串口占用守卫(AD-008)

注意:开关在 setting_meta 表;服务端开关从未开启时功能完全静默(历史 教训:2026-08-05 现场 root cause 之一)。开启后可在 /state 快照与 audit_event 表确认。


5. 运维观察点

5.1 日志(/tmp/mrrc-ft8.err.logMRRC_FT8_LOG_LEVEL=DEBUG

日志行 含义
slot N: M msgs, ... + slot N msg: snr=… dt=… f=… <text> 每槽解码明细
auto_call: <call> snr=… slot=… 自动呼叫已触发(arm 成功、已入 sequencer)
auto_call skipped: interlock open (<call>) 互锁 faulted/PTT 不确定,跳过本槽(下槽重试)
safety fault: audio: degraded capture session… + safety cleared: audio 音频退化互锁 锁/解
safety arm: TX armed by operator arm 成功(含 auto_call 内部调用,措辞沿用人工路径)
tx window slot N: … deferring fit guard 顺延
band_hunt tick: auto=… seq=… selected=… armed=… band-hunt 状态

5.2 数据库(mrrc-ft8.db,SQLite)

观察
qso source='live' 行 = 本服务完成;status/completed_epoch
audit_event operation='auto_call'(actor=system)每次触发一行
decode_event 7 天保留;消息/SNR 历史
setting_meta 开关持久化
-- 最近自动呼叫记录
SELECT * FROM audit_event WHERE operation = 'auto_call' ORDER BY epoch DESC LIMIT 10;
-- 最近 live QSO
SELECT * FROM qso WHERE source = 'live' ORDER BY id DESC LIMIT 10;

5.3 频点占用健康

ring slot N: … gaps=1 …caprestarts 增长 = 采集链路退化信号; rmsmsgs 长时间矛盾(热而零解码)→ AUDIO fault(有 B1 复验窗口自动解除, TX 仍需人工 re-arm,符合 no-recovery-auto-resumes-TX)。


6. 安全设计(不变量)

  1. 单点 TX 门:所有 TX 意图必经 SafetyController(AD-007);Web/DSP/sequencer 都不能直接 key PTT。auto-call 的 arm 与人工 arm 走同一互锁。
  2. 互锁矩阵:CAT / audio / DSP / clock 任一 faulted → arm 拒绝、PTT-off; fault 按源 latch,重复上报不刷屏。
  3. 不自动恢复 TX:fault 清除后仍需人工 re-arm(或新的 auto-call 触发), 恢复流程绝不自行 resume TX(§12/§15)。
  4. STOP 优先:任何已认证会话可 STOP(无需租约),幂等、PTT-off 非阻塞。
  5. 不打断 QSO:auto_call_candidate 要求 IDLE + 无选择;进行中的 QSO/人工选择 绝不被自动呼叫打断(AD-012 单 QSO 自动化)。
  6. 频率纪律(2026-08-07):应答发在伙伴解码频率(split),避免离频呼叫不被 对方自动序列配对;CQ 避开占用频点。
  7. 不碰 CAT 串口:唯一 owner 是 rigctld(AD-008)。部署期由 restart.sh serial_guard() 强制:启动 rigctld 前 lsof 检测串口持有者,非 rigctld 进程持有即拒绝启动并列出持有者(fail-fast);MRRC_FT8_SKIP_SERIAL_GUARD=1 应急跳过。
图 10 安全不变量:单点 TX 门、STOP 优先、不自动恢复 Web UI / 人工操作 auto-call(system) CQ 循环 SafetyController 唯一 TX 门(AD-007) arm(): 互锁 CAT/audio/DSP/clock transmit(): faults/armed/PTT/预算 + watchdog PTT rigctld FT-710 STOP 任何已认证会话(无需租约) 幂等 · PTT-off 非阻塞 fault 清除后仍需人工 re-arm —— no-recovery-auto-resumes-TX
图 10 安全不变量:单点 TX 门、STOP 优先、不自动恢复

7. 现场案例复盘(2026-08-07)

背景:2026-08-07 现场连续暴露三个根因:TN8GD 自动呼叫的频率纪律、CAT 串口争抢、band-hunt 实体名失配。以下按时间复盘。

7.1 TN8GD 频率纪律(UC-003/UC-004)

图 11 TN8GD 频率根因:应答必须发在伙伴解码频率 修复前(2026-08-07 现场) TN8GD CQ @843 本台应答 @1500 自身回波 @1500 ✗ 未配对 — 重传 4 次无应答,RETRY_EXHAUSTED UC-003 修复后 CQ @843 应答 @843 ✓ split 配对 — 对方接收机能配对 应答必须发在伙伴解码频率 0 500 843 1500 2000 2500 3000 TX 音频频率(Hz)— 旧行为写死 1500 Hz,与 TN8GD 的 CQ(f=843 Hz)离频
图 11 TN8GD 频率根因:应答必须发在伙伴解码频率
时间(本地) 事件
06:07–07:16 TN8GD 的 CQ 被 auto-call 候选命中 61 次,全部 skipped: interlock open —— 05:27 起音频退化 fault 锁互锁 3.5 h(08:55:47 才清除),TX 被安全系统正确拦截;引擎每 30 s 坚持重试,机制正常
09:46:15 互锁解除后第一个可用 slot,auto_call: TN8GD snr=-21 slot=119071144 立即触发(无需人工),发出 4 次 12.64 s 应答波形
09:46:30–09:48:00 解码到 3 次 TN8GD BG1SB ON80(f=1500)——解析为 from=BG1SB,即自身发射回波,非 TN8GD 应答;全程无 BG1SB TN8GD … 消息
~09:48:22 无伙伴报告,重传预算耗尽 → RETRY_EXHAUSTED → QSO 丢弃未落库
09:53–09:55 TN8GD 转去通联 JA4AQS / VU2KPH

根因:TX 音频频率写死 1500 Hz,而 TN8GD 的 CQ 在 f=843 Hz——应答发在错误 音频偏移,对方自动化 DX 台未配对。对照:成功 QSO(BD6KDG@1600、UT5ZC@1401) 均因对方人工在自己频率上应答;UA4LDP 同样只有 1500 回波、同样未完成。

修复(UC-003)Sequencer.tx_frequency 记录伙伴解码频率(reply_to(…, tx_frequency=…)),TxDriver 编码读 sequencer.tx_frequency;auto-call / select / reply 全链路透传 freq;旧客户端缺 freq 回退 1500。同批完成 UC-004 (CQ 选空闲频点)。

7.2 CAT 串口争抢与 serial_guard(AD-008,V1.4)

18:01 一台手动 nohup 的旧 mrrc_ft710 server.py(MacPorts Python)直接 open /dev/cu.usbserial-0121DB3A0,与 rigctld 共持 → 字节争抢 → 18:00–22:20 rig 轮询 ~90% 超时(wrong reply/Rig busy 数千条),band_hunt 切频 5 次失败;停掉残留进程(PID 56041)立即恢复。修复:restart.sh serial_guard()(见 §6 不变量 7);旧项目 switch.sh/stop.sh 同步加固(大小写不敏感 pgrep -if、串口持有者兜底)。

图 12 CAT 串口争抢与 serial_guard 守卫 FT-710 CAT 串口 /dev/cu.usbserial-0121DB3A0 rigctld 合法 owner(AD-008) mrrc_ft710 server.py 残留进程(PID 56041) ⚡ 字节争抢 18:00–22:20 rig 轮询 ~90% 超时(4 h)· wrong reply / Rig busy 数千条 band_hunt 切频 5 次失败 · 停掉残留进程后立即恢复 部署期守卫(V1.4 · AD-008) restart.sh serial_guard() lsof -t "$RIG_DEVICE" 检测持有者 非 rigctld 持有 → fail-fast,拒绝启动 列出持有者(PID + command) MRRC_FT8_SKIP_SERIAL_GUARD=1 应急跳过
图 12 CAT 串口争抢与 serial_guard 守卫

7.3 band-hunt 实体名失配与归一化(V1.5)

12 h 复盘——band_hunt 反复切 15 m 追 “Germany”,但 Germany 早已通联(qso 表 64 条 DL/DK);根因:worked 过滤直接拿 pskreporter 普通名对 cty 规范名失配(42 个实测实体名中仅 3 个不一致);修复 _CTY_NAME_ALIASES + _canonical_entity_name();auto-call 的 is_new_dxcc 不受影响。

图 13 实体名归一化:pskreporter → cty 规范名 rank_bands 的 worked 判定先过别名表归一化(V1.5) Germany Malaysia Turkey _CTY_NAME_ALIASES _canonical_entity_name() pskreporter 名 → cty 规范名 未知名称原样返回 Fed. Rep. of Germany West Malaysia Asiatic Turkey worked 判定 ✓ Germany 不再判 new(qso 表 64 条 DL/DK) 42 个实测实体名中仅这 3 个不一致 · auto-call 的 is_new_dxcc 不受影响(两侧都用 cty.lookup)
图 13 实体名归一化:pskreporter → cty 规范名

8. 已知限制与后续