新 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. 端到端链路总览
3. 环节详解
3.1 信号采集与解码
- 音频采集运行在独立子进程
capture_proc(进程边界隔离 CoreAudio 会话退化, macOS 实测:残留客户端会让新会话永久劣化)。数据进UtcRing(按绝对序号slot_id % capacity寻址,杜绝时间戳错位)。 - 解码器输入恒为 12 kHz int16 单声道(
server/core/binding.py强制,其他采样率 拒绝进入 DSP ABI);TX 波形恒为 48 kHz。所有 DSP 调用经全局锁、只在独立 Worker 内执行(OpenMP 线程只在 Fortran 聚合结果,绝不回调 Python)。 - 每 15 s slot,
orchestrator把整槽音频送 Worker 解码(WSJT-X 3.0.2 Improved, profiles 0–4,A8 门控),产出DecodeResult(slot_id / snr / dt / frequency / text 等),经on_decode进入业务层。
3.2 DXCC 实体判定
实体库:仓库内
cty.dat(country-files ADIF 格式),由server/engine/dxcc.py的CtyDatabase解析:=CALL精确前缀优先;(数字)替换(如(23))在加载时展开;- 其余按最长前缀匹配(如
TN→ Niger)。 lookup已索引化(精确表 + 前缀 trie),单次亚微秒。
判定(
main.pyon_decode):entity = get_cty_database().lookup(view["call"]) # (实体名, 洲) view["is_new_dxcc"] = bool(entity) and entity[0] not in worked_dxccworked_dxcc来自dxcc_summary全量统计(qso 表非 void 记录 × cty 查表, 同实体同波段计一次,DXCC Challenge 语义);cache 未建时保守为False(不误触发)。
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 == 本台)不触发
)
3.4 触发与安全门(_auto_call)
- 不需要控制租约(系统级、无人值守,NFR-087);但发射必须过
safety.arm()—— 这是唯一的 TX 门。 arm()在任一互锁 faulted 时抛TxRefused→ 记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)(UC-003 反相时隙);tx_frequency = 伙伴解码频率(UC-003 频率纪律,2026-08-07 修复;旧行为写死 1500 Hz,见 §7 复盘);
- audit 落库:
audit_event(actor=system, operation=auto_call, target=呼号, detail=snr=… new_dxcc)。
3.5 应答发射(TX 链路)
TxDriver(server/engine/tx_driver.py)是时隙奇偶泵:只在 sequencer 的tx_phase对应的奇偶 slot 拉取至多一条消息,编码后交safety.transmit()。- 决策窗口(I9):sequencer 空闲时驱动持续轮询到
TX_DECISION_CUTOFF_SECONDS(5.0 s),点击/自动呼叫一 arm 就发;fit guard 拒绝超过 ~2.4 s 的启动 (12.64 s 波形必须塞进 15 s slot),错过则顺延到下一可用 slot。 - 频率:
encoder.encode(message, sequencer.tx_frequency, slot_id)——应答用伙伴 解码频率(split 行为,对方接收机能配对);CQ 用FrequencyOccupancy选出的 1500 附近空闲频点(UC-004,见 §3.8 注)。 - 安全:
transmit()前复核权限链(faults / armed / PTT 状态 / 聚合 TX 预算); PTT 经 rigctld(串口唯一 owner);watchdog 与 STOP 可随时取消。
3.6 QSO 状态机(sequencer)
应答侧流程(我们是应答者):
- 每条消息 1 发 + 至多 3 重传(NFR-055);伙伴有相关进展消息则重传预算重置。
- 伙伴转呼他人 → anti-QRM 自动停(
PARTNER_LOST);预算耗尽 →RETRY_EXHAUSTED停并保留伙伴上下文;两者都触发on_stop(清除 selected,P7 修复)。 on_message只处理 addressed-to-me 消息;自身回波(mine)被忽略。
3.7 落库与 worked 集刷新
- QSO 完成(收到 RR73/73)→
_ensure_log恰好一次 →on_qso→qso_log.enqueue(asyncio.to_thread落repository.record_qso; 进程崩溃时data/qso-pending.jsonl待办恢复,不丢记录)。 record_qso:qso 表新增行(source='live'、status='completed'、 my/dx call+grid、report 双方、started_utc、freq_hz、band)+ qso_event +dxcc_dirty = True。- 下一 slot 的 on_decode 先查
dxcc_dirty:为真则重建dxcc_summary(~0.2 s, 至多每 slot 一次)→ 该实体进入 worked → 不会再触发(P1 修复:cache 刷新独立于 band-hunt 开关,auto-call 不再重复呼叫刚通联的台)。
3.8 关联功能:自动波段狩猎(band_hunt)
auto_band_hunt开启后,band-hunter 轮询外部/api/band_hunt(pskreporter 侧, HTTP 是唯一跨库边界),rank_bands/decide_switch纯函数按”未通联实体数” 排序,空闲时经 rig 调谐切频 → 把波段切到新 DXCC 最密集的地方 → 自动呼叫闭环 自然接续。双闸门:设置开关 + 冷却(默认 1200 s)。- 实体名归一化(V1.5,2026-08-08):pskreporter 用普通名(
Germany/Malaysia/Turkey),cty.dat 规范名是(Fed. Rep. of Germany/West Malaysia/Asiatic Turkey)——rank_bands直接比对会失配,已通联实体被误判 new(现场:反复切 15 m 追 Germany,而 qso 表已有 64 条 DL/DK)。_CTY_NAME_ALIASES+_canonical_entity_name()归一化后用于 worked 判定;new_entities保留原名显示;未知名称原样通过不误伤。auto-call 的is_new_dxcc不受影响(两侧都用cty.lookup,本就一致)。 - CQ 空闲频点(UC-004,2026-08-07):
FrequencyOccupancy记录最近 120 s 解码 频率(含自身回波),pick_cq_frequency在 1500±300 Hz 螺旋扫描第一个与所有 占用中心距 ≥30 Hz 的整数频点;全占用回退 1500。手动 CQ 与 cq_loop 每次 CQ/re-CQ 重选。
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.log,MRRC_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 增长 = 采集链路退化信号; rms 与 msgs 长时间矛盾(热而零解码)→ AUDIO fault(有 B1 复验窗口自动解除, TX 仍需人工 re-arm,符合 no-recovery-auto-resumes-TX)。
6. 安全设计(不变量)
- 单点 TX 门:所有 TX 意图必经
SafetyController(AD-007);Web/DSP/sequencer 都不能直接 key PTT。auto-call 的 arm 与人工 arm 走同一互锁。 - 互锁矩阵:CAT / audio / DSP / clock 任一 faulted → arm 拒绝、PTT-off; fault 按源 latch,重复上报不刷屏。
- 不自动恢复 TX:fault 清除后仍需人工 re-arm(或新的 auto-call 触发), 恢复流程绝不自行 resume TX(§12/§15)。
- STOP 优先:任何已认证会话可 STOP(无需租约),幂等、PTT-off 非阻塞。
- 不打断 QSO:auto_call_candidate 要求 IDLE + 无选择;进行中的 QSO/人工选择 绝不被自动呼叫打断(AD-012 单 QSO 自动化)。
- 频率纪律(2026-08-07):应答发在伙伴解码频率(split),避免离频呼叫不被 对方自动序列配对;CQ 避开占用频点。
- 不碰 CAT 串口:唯一 owner 是 rigctld(AD-008)。部署期由 restart.sh serial_guard() 强制:启动 rigctld 前 lsof 检测串口持有者,非 rigctld 进程持有即拒绝启动并列出持有者(fail-fast);MRRC_FT8_SKIP_SERIAL_GUARD=1 应急跳过。
7. 现场案例复盘(2026-08-07)
背景:2026-08-07 现场连续暴露三个根因:TN8GD 自动呼叫的频率纪律、CAT 串口争抢、band-hunt 实体名失配。以下按时间复盘。
7.1 TN8GD 频率纪律(UC-003/UC-004)
| 时间(本地) | 事件 |
|---|---|
| 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、串口持有者兜底)。
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 不受影响。
8. 已知限制与后续
- 弱信号:auto-call 不设 SNR 门槛,-21 dB 也触发(NFR 目标是”抓新实体”); 弱信号 QSO 成功率低,可考虑加可选最小 SNR 设置。
- 多流 DX 台:pileup 模式下对方自动化序列可能不配对离频/拥挤呼叫;频率纪律 已修复主要路径,但极端 pileup 仍需人工介入。
- guard/窗口参数:
pick_cq_frequency的 guard=30 Hz、窗口 ±300 Hz、 占用 TTL 120 s 均为模块常量,可按现场拥挤度调整。 - 单一 QSO:AD-012 约束一次只跟一个台;QSO 完成后自动转回 idle(或 cq_loop re-CQ),不做连续多目标追猎。
- 数据源:
cty.dat是仓库内静态副本;实体划分更新需手动替换文件 + 重启 (cache 懒加载单例)。 - 实体名别名表:
_CTY_NAME_ALIASES目前只覆盖 3 个已知失配(Germany/Malaysia/Turkey);pskreporter 若引入新的不一致名称需人工扩充别名表。