14. Version History
| Version | Date | Author | Changes |
|---|---|---|---|
| SDD V3.03 | 2026-10-18 | pi | 天线诊断台落地为野外工具:判定卡、修剪建议、野外配置与基准,外加真机上挖出的六个缺陷。 ① 页面按用途重做(原来是仪表盘):判定卡给段内最高 SWR 与四档结论(≤1.5 免调 / ≤2.0 天调可修 / ≤3.0 勉强 / >3.0 不建议),并按「谐振频率与波段中心的相对偏差 ≈ 振子长度的相对偏差」直接给修剪量(每 10 m 剪/加多少 cm);曲线带波段窗口、1.5/2.0 判定线、谐振点竖线、光标悬浮读数,点曲线即把设备屏幕上的 marker 移过去。② 野外独立使用:一键把设备配成「1–54 MHz / 401 点、只留一条 S11 的 SWR 迹线(1.0/格、参考 0)、网格数值、marker 在中点」并写 SAVE 槽 4。关键事实(真机实测):迹线格式不在 config 里——saveconfig 甚至固件的「记住关机前状态」都留不住它(改完重启回 LOGMAG),只有 save/recall 槽存的是整套状态(扫描范围+迹线+标记+标定),所以拔下来开机要 RECALL 4。③ 基准配置:docs/nano/field-baseline.json 从真机倒出(可读部分),GET /api/nano/baseline?compare=1 与现场逐项比对,页面列出所有偏离(扫描范围/迹线/标记/测量设置);固件没有查询命令的 config 开关不在其中,页面显示的是本页记下的意图。④ 真机挖出的六个缺陷:(a) marker 命令是 1 基的,传 0 固件不报错也不动——「命令成功但标记没动」;(b) 状态轮询与扫描并发读写同一个 pyserial 句柄会抢帧字节,且忙时 connected 报 False,页面每扫一次就喊一次「设备掉线」(忙 ≠ 掉线);(c) 死句柄装「在线」直到下次扫描才炸,现在设备不应答立刻丢弃、下轮轮询自愈;(d) fitCanvas 把 canvas.height 设成 280×dpr 而 CSS 无高度规则,Retina 屏每刷新一次画布翻倍;(e) server.py 的日志器叫 logger 而 nano 块写了 log.,只有真连上设备时才 NameError(单测全打桩,一次没跑到);(f) disable_all_traces 在 main 上只有调用没有定义。⑤ 手册与文档:新增 docs/nano_vna.md(11 节操作手册:接线、判定卡、标定命令与验证判据、野外独立使用、基准、固件升级三种进 DFU 的方式、排障表),SDD/09 增 §9.11、SDD/11 增四个组件、AGENTS.md/README.md/DOCUMENT_INDEX 同步。⑥ 网站:产品首页新增 Cloud Hub 章节(零入站 / 一呼号一门 / PTT 归本地 / 逐跳验签 + 拓扑图),并按 chinese-prose 量具重写文案(破折号 9.8‰→0、四字格 26→47‰);补 FT8 / SDR / 蓝牙遥控 / 远程协助四张功能卡;拓扑图标注「路由器也可以是 Wi-Fi AP」,三份副本逐字一致,hub 站已发布。⑦ 测试 2657 → 2716(139 模块)。 |
| SDD V3.02 | 2026-10-18 | pi | NanoVNA 第二期:固件升级(AD-030,U3 默认上锁)。 ① 为什么能安全做:reset dfu 走 boardDFUEnter() → __set_MSP(STM32F303xC_SYSTEM_MEMORY),跳进芯片系统存储区的 ROM bootloader;那段代码不可擦写,所以最坏只是 App 区烧一半、重启停在 DFU,再刷即恢复——不存在变砖。 ② 为什么需要人:H/H4 有 MS/SI/ZK 三个变体(MS5351 vs Si5351;ZK 是 2025 年 NE602A 换料板),那是编译期开关,info 只说 Board: NanoVNA-H 4,运行时读不出来。刷错不砖,但时钟/RF 不对,所以:首次显式选、记住在 config.json、刷后自检(仅在用户声明「CH0 开路」时才判 |S11|,否则只记录,避免把接了负载的正常升级报成失败)。 ③ 成功判据是刷后设备自己报的版本(dfu-util 返回 0 不算数):版本不符 → failed,不是 done;设备没进 DFU 就不跑 dfu-util。 ④ 凭证:firmware-state.json 记镜像名、SHA-256、刷前/刷后版本、时间、自检值;镜像从官方 releases 取(api.github.com 的 digest: sha256: 核对,没 digest 就拒绝刷),6 小时缓存,网络挂了退化成缓存而不假装在线。 ⑤ 系统依赖(镜像下次构建生效):packaging/common/packages.txt 加 dfu-util(bookworm 源,134 KB,只依赖 libusb),服务层新增 99-mrrc-nanovna.rules(0483:5740 应用态 + 0483:df11 ROM bootloader → GROUP=dialout)——后者是没有 /dev 节点的裸 USB 设备,缺这条的报错是 LIBUSB_ERROR_ACCESS,看起来像硬件坏了。 ⑥ 测试 2657 → 2691(139 模块):test_nano_flash 27(变体挑选、SHA 不符删半截文件、无 digest 拒刷、sysfs 等待、argv、状态机的每一条失败路径、凭证原子写)+ test_server_nano_routes 7(默认上锁 / 确认字 / 发射中 / 已有任务 / 未知变体 / 状态可读)。 ⑦ 真机:代码在 feat/nano 分支,盒子上的 server.py 与新代码是纯增量关系(5 个 hunk、0 行删除),已推送并在盒子上就地部署;刷 1.2.50 的实机验收待 MRRC_NANO_ALLOW_FLASH=1 打开后跑(回退镜像 1.2.14 在同一渠道里有)。 |
| SDD V3.01 | 2026-10-18 | pi | NanoVNA-H 接进盒子(AD-030):一次扫描拿到复数 S11,天线画像从 20 分钟变成 1 秒。 ① 现场:0483:5740 ChibiOS CDC-ACM → /dev/ttyACM0(CAT 是另一颗 CP2105,不抢口),NanoVNA-H 4 / firmware 1.2.14 / 401 点 / 已带标定;mrrc 已在 dialout,无需改权限。 ② 协议照固件源码核过:scan_bin 掩码位 1/2/4/8/16/32/128,二进制帧 u16 mask + u16 points + 每点 u32 freq + f32×2;data 0 里没有频率(0=S11、1=S21、2..6=标定数组),频率只在 frequencies/scan 的输出里。 ③ 实测速度:101 点 0.32 s / 201 点 0.63 s / 401 点 1.26 s(ASCII 体量 23.9 KB,二进制 4.8 KB)——所以它是仪器不是流,页面按「扫一次」设计。 ④ 真机撞出的三个缺陷(都在传输层):(a) 直接 reset_input_buffer() 就写命令,会在设备吐上一条回复时把新行塞进去,表现为一个字节都不回(401 点连续两次空帧);(b) 用「静默多久」判断帧尾会在分段之间截断;(c) 只去掉回显行的 CRLF 会把 data 0 变成一行「数据」喂给 float()。修法:发命令前与 ch> 同步(in_waiting 先看一眼,避免每条命令白等 0.6 s)、按帧长读满、回显整行丢弃。 ⑤ 交叉校验:同一段扫频下 scan_bin(带标定)与 data 0 逐点一致(最差 Δ 2e-4),并顺带证实 sweep 之后立刻读 data 0 会拿到上一轮数组的竞态——这正是只用 scan_bin 的理由。 ⑥ 页面与接口:/Nano(主站口令;listen/sdr 403)+ status/scan/settings/cal/sweeps 六条路径;SWR 主图 + Smith / |Z| / R+jX、谐振标线、孤立 1.00 标红(沿用 antenna-sweep 的掉读判据)、存盘/叠加/CSV/s1p;tcxo 与破坏性命令不进白名单。 ⑦ 测试 2591 → 2655(138 模块):test_nano_analysis 14、test_nano_vna 30、test_server_nano_routes 21(含四道守卫的顺序与前缀安全)。 ⑧ 未做(第二期):nano/flash.py + dfu-util/udev 进 packaging/common/ + U3 门控(默认上锁)、刷后版本回读与自检、回退。设计与计划:docs/superpowers/specs/2026-10-18-nano-vna-design.md、docs/superpowers/plans/2026-10-18-nano-vna.md。 |
| SDD V3.00 | 2026-10-18 | pi | 独立 SDR 接收机(AD-029):把一颗”插着却用不了”的 RSP1 变成对外发布的 Web 接收器。 ① 现场取证(W103D 192.168.1.217):SDRplay RSP1(1df7:2500)插入后立刻被内核 msi2500 认领,而那条路径实测坏了——S_FREQ 设 7/25/100/433.92 MHz 一律读回 15.000 MHz(tuner range 上沿钳死),连采 604,800 B 有 99.7% 落在 0–32(有效 I/Q 应铺满 0–255)。② SDRconnect 不支持 RSP1:官方 ARM64 包(headless)能跑、WebSocket API 1.0.3 正常应答,但 valid_devices 只有 "IQ File",root 亦然(排除权限);SDRplay 论坛官方答复「RSP1 不会支持(硬件原因)」。③ 正路是 Hardware API 3.15:包内带 arm64/libsdrplay_api.so.3.15 + sdrplay_apiService,装完 GetDevices → numDevs=1 ser='0000000001' hwVer=1、Init=0,2/4/6/7/8/10 MSPS 全部可用(10 MSPS 1.5 s 收 13.6M 样本);同机 CPU 实测 2 MSPS ≈ 0.35 核、8 MSPS ≈ 1.04 核,设备独占(第二个进程 Init 立即失败)。④ 全频段调谐实测(同一根 HF 天线,rms):198 kHz 4361 / 3.5 MHz 1431 / 7.074 MHz 1059 / 14.074 MHz 330 / 28.074 MHz 37 / 50 MHz 以上只有噪底。⑤ DSP 两轮返工:numpy 融合多相 0.075 核/通道且做不出边带选择性(USB 压不掉 LSB);改 C 的两级信道化(fs_in→96 kHz→48 kHz),实测 AM 0.086 / USB 0.151 核/通道,据此把默认通道数从 24 降到 8;WFM 需要第三条管线(先在 480 kHz 鉴频、再抽音频)。⑥ 真机端到端:/sdr 登录 → /WSsdr → 12 秒内 847 个频谱帧 + 572 个 Opus 帧、available=True、abs_freq 与点选一致;同一 cookie 打开 / 是 302(角色隔离)。⑦ 真机抓到的协议缺陷:A1 帧的 seq 曾按 u8 打包(<BBHH),序号过 255 即 struct.error 把 pipe 打死、接收机变不可用——三处统一 <BHBH,并用 seq=65000 的回归钉住。⑧ 范围:盒子(Linux/aarch64)only,桌面包 MRRC_SDR=off;V1 明确不做聊天/spotter/RDS/数字模式/录音,WFM 立体声与”跟随电台频率”留到下一版。⑨ 测试 2481 → 2544(DSP 7 / 绑定 5 / 帧与 pipe 9 / hub 21 / 接线 12 / 前端 8 …)。设计与计划:docs/superpowers/specs/2026-10-18-sdr-receiver-design.md、docs/superpowers/plans/2026-10-18-sdr-receiver.md。 ⑧ 刷机镜像改为对外下载版(同日决策):老的公开镜像要先走首启引导(生成 Web 口令、探测串口/声卡、/setup),操作员判断「太复杂」;于是对外下载的就是预置版(--preflash:Web abcd!1234、root abcd1234、mrrc abcd1234、Asia/Shanghai、zh_CN.UTF-8)。代价写进文档而不是省略:下载页每一份都是同一组公开口令,且镜像 sshd 默认允许 root 口令登录,所以两张下载卡都改成「第一件事是改掉这三个口令」,packaging/box/README.md 里「发货版不该上传下载页」那句作废并换成两条使用约束。产物 MRRC-Modern-1.28.1-w103d.img.gz 1,221,443,860 B / ee1fba1e…,四步验证全绿(代码身份、bluez 已装、input:x:102:mrrc、五个单元 + logrotate、11 份 profile、frpc aarch64、SDRplay API/libsdr_dsp/udev、/tmp 零残留、D-11、chroot 导入含 paramiko 与 sdr_*);服务层新增对 vendor/sdrplay 的硬门禁,厂商 API 由 tools/sdr/fetch_sdrplay_api.sh 取到 vendor/ 且不入库。 |
| SDD V2.99 | 2026-10-17 | pi | 一台要发出去的盒子,先得活得过无人值守的几个月:日志轮转、journald 上限、录音拒写。 ① 现场(W103D 192.168.1.221,eMMC 1.28.1):logs/server-stdout.log 五小时 17 MB,来源是盒子上的 local_speaker.py 落后一版(缺「一条警告一条 streak」),耳机离线时每 20 ms 写一条 —— 但这个缺陷只是扳机:该文件由单元的 StandardOutput=append:$MRRC_HOME/logs/server-stdout.log 写入,而 systemd 不轮转 append: 目标,服务端也不轮转它,于是它是镜像里唯一没有上限的文件(server.log 是 2 MB × 2 代的 RotatingFileHandler、ft8-worker.log 2 MB、support bundle 留 5 份,/tmp 的两个 slot dump 是 tmpfs 且每进程最多 2 个)。6.5 GB 根分区下,这不是「会不会」而是「多久」。② 轮转:服务层写 /etc/logrotate.d/mrrc——maxsize 5M 触发、rotate 3、compress、delaycompress、missingok、notifempty,copytruncate:systemd 持有 fd,rename 会让写入继续落在一个已 unlink 的 inode 上,目录看起来转了、硬盘继续涨——比不轮转更糟,因为它把增长藏起来。③ scheduler 是自己的:mrrc-logrotate.timer(OnBootSec=10min / OnCalendar=daily / Persistent=true)+ oneshot 服务调 /usr/sbin/logrotate --state /var/lib/mrrc/logrotate.state /etc/logrotate.d/mrrc。不押注 cron 没被 mask、也不押注 Debian 的 logrotate.timer 存在——两条都可能无声地不跑,而那正是「有配置、没人执行」这种最贵的形态。logrotate 同时进 packages.txt(timer 要 exec 一个二进制)。④ journald:默认上限是根分区 10%(这台 650 MB),而除服务端以外所有单元都往 journal 写;新增 /etc/systemd/journald.conf.d/50-mrrc-size.conf(SystemMaxUse=128M、SystemKeepFree=256M)。⑤ 录音拒写:新增 MRRC_RECORDINGS_MIN_FREE_MB(默认 200),在 _execute_set_command("recording", true) 里 会话创建之前测 shutil.disk_usage,不够就回一条 error 并返回;测不出(OSError)不算拒绝——诊断失败不该赔上一个功能。刻意不做删除:64 kbps / 16 kHz mono ≈ 29 MB/小时,客户的录音是唯一无界的写入方,但删 QSO 是破坏性的,产品只拒绝、不删。⑥ 更早的同一根因:桌面两个启动器的 tee(launcher_log.py)本来就是 2 MB 上限并在服务答 HTTP 后停写,所以这条只影响 Linux 镜像线。⑦ 测试 2449 → 2460:tests/test_log_rotation.py 6 项(轮转文件名 == 单元的 append: 目标——一侧改名是静默的;copytruncate;大小触发/代数/压缩;自带 timer 且被 enable;packages.txt 含 logrotate;journald 有上限与 keep-free)、tests/test_recorder_api.py +5(够 / 不够且报出 MB / 测不出不拒 / 0 关闭 / 命令层不建会话)。⑨ 远程协助在盒子上接通(同日真机 .217 / v1.25.5 / Cloud Hub ba4eg,两个真缺陷):(a)「开启协助」每次都失败,报 frpc admin API unreachable on 127.0.0.1:0——端口 0 不是网络问题,是这份隧道配置里没有 webServer 段的标记:实例在管理段进入生成器(10-12)之前入网,connect() 不会为已入网的实例重写配置,set_help_proxy() 也不敢擅自铸一个(跑着的 frpc 不认)。新增 cloud_hub.ensure_admin_segment(),在启动隧道之前补一段(frpc 只在启动时读配置,那是唯一安全时机),并由 _cloud_start_tunnel() 调用;set_help_proxy() 无管理段时改报一句能行动的话(「重启一次应用就会自动补上」),不再拿 0 号端口重试 3 秒。(b)「已连接」永远是 0:HelpSession.note_connection() 没有任何调用者——SSH 门只发 on_event,计数住在会话上,两边从未接线,而单测直接调 note_connection() 所以看不见。门现在在会话结束时回调 on_disconnect,build_session() 接到 note_connection();新测试走接线本身(真 paramiko shell → connections == 1,探针 exec 不计数),删掉那行接线立刻变红。真机验收:开窗 → hub ssh -p 18818 交互式登入 id -un = mrrc(不提权)→ connections: 1 → 客户「停止协助」→ hub 端口释放、配置里协助代理摘掉、主入口全程 302 不抖;排障表与验收记录进 docs/remote_help.md。⑩ 同一形状的第二条死接线(更严重):「连续试错 5 次自动关闭」从来没发生过 —— 门的 check() 自己数失败次数,而决定关窗的 HelpSession.note_auth_failure() 没有调用者(真机:连错 3 次,failures 仍是 0,窗口照开)。现在 SshHelpServer.on_auth_failure → note_auth_failure,两条测试钉住(错一次 → failures: 1;第五次 → closed / auth_failures)。开放提醒:OpenSSH 默认一次连接试 3 遍口令,一次 typo 吃掉 3 次预算 —— 支持侧脚本加 -o NumberOfPasswordPrompts=1。⑪ 测试 2460 → 2467(远程协助:接线 2、管理段 3、试错关闭 2)。⑫ 热升级到最新(真机 .217:1.25.5 → 1.28.1 代码 + 重编 DSP)时抓到「版本号不跟着走」:linux/mrrc_update.sh 从来不写 version.txt,于是诊断包 manifest、/api/update/check 的当前版本、☰ 菜单页脚一直说旧版本(「升级了但没变」这类报告根本无从分辨)。现在按 MRRC_UPDATE_VERSION → 源树 CHANGELOG.md 第一条 → 源树 version.txt 三级取值并同时写 version.txt/VERSION(喂一棵已 stage 过的树时只有第三种可用,stage-tree.sh 的 /*.md 排除就是这么来的)。同一轮把 mrrc-logrotate.timer 从日更改成每小时 —— maxsize 只在 logrotate 跑的时候才被看见,日更等于把 5 MB 上限变成「一天能涨多少」。⑬ 测试 2467 → 2468。⑧ 边界:已刷好的那台盒子需就地补装(配置 + 两个 unit + logrotate),镜像下次构建才自带;verify.sh 第 9 项对 302 判红是既有误判(入口 redirect 是健康的),本次未改。 |
| SDD V2.98 | 2026-10-17 | pi | 盒子上的运行期收敛:一条实测的调频、一处白烧的编码、一个能关的 FT8、一台接上的天调。 ① 调速器(mrrc-cpufreq.service,进服务层):这台的负载碎而密(音频 50 Hz、频谱 30 Hz、FT8 时隙),schedutil 不停变频,sugov:0 累计占一核的 17.8%;vmstat 5 s ×3 三次复现,整机忙 27% → 14%,SoC 温度 48.2–48.4 °C 完全不变。盒子市电供电,“降频省电”换不来续航,只赔半个核。② 编码门:RX 循环原来”有人在听就编码 Opus”,而录音 / FT8 tap / 本地扬声器要的都是 PCM;现在只认 audio_rx_clients / 录音 / FT8 tap,纯扬声器盒子不再每 20 ms 白烧一次 numpy + libopus。③ FT8 引擎开关(☰ → 设置):引擎开机常驻后台解码,小盒子上是一个核的 DSP;开关走已有的 PUT /api/ft8/station {"engine_always_on": …}(它本来就落盘并启停 runtime),没有第二条命令通道;实测关 → DSP worker 2 → 1,开 → 恢复。④ ATR1000:192.168.1.63:60001 接上并在学习(Learn: 7050.0kHz, SWR=1.00, LC, L=13, C=46)。⑤ 蓝牙边界(实测两次):BLE 遥控 HID 与 A2DP 输出可用;HFP/SCO 两个方向都不送数据——bluealsa 的 SCO 编解码线程起来了、控制器 LMP 特性位声明支持 SCO(0xBF byte0 bit7=1),但采集 15 秒 0 字节、放音无声 ⇒ 板级语音通道未引出。耳机麦克风因此不可用(与遥控器的厂商私有 BLE 语音是两条不同的死路),本地发射音源需 USB 麦克风。⑥ 不做的一项:无人看频谱时 scope_pipe 实测只占一核 7%(=整机 1.75%),改 pipe 控制协议的收益不匹配风险,停在此处。⑦ 测试 2437 OK(1 skip),release_check 32 ok。 |
| SDD V2.97 | 2026-10-17 | pi | 本地操作台:蓝牙遥控器直接驱动电台(AD-028)。 ① 现场(W103D 192.168.1.221,eMMC 1.28.1;遥控器 CMCC_Voice_Remote 0C:F3:CE:B0:88:75):配对、绑定都成功,但键不出现——btmon 给出 LE Start Encryption → Encryption Change: Disabled,链路明文,遥控器对每个 GATT 读都回 ATT 0x0E,bluez hog 因此建不出 HID 设备;删设备 + 控制器上下电重配后 Encryption: Enabled with AES-CCM,CMCC_Voice_Remote Keyboard 立刻出现,evtest 立刻解出 KEY_1(usage 0x7001e) 等键。② 键位表全部实测:语音=0x7003e/KEY_F5、电源=0x70066/KEY_POWER、方向 0x70052/51/50/4f、确定 0xc0041、首页 0xc0223、返回 0xc0224、菜单 0x70065、设置 0xc00eb、M 0xc00f0、频道± 0x7004b/4e、音量± / KEY_VOLUMEUP/DOWN、数字 0x7001e…0x70027、* 0x70055、# 0x700cc;设置/M/# 的 keycode 同为 240,故键位按扫描码而非键码;电视 键不发蓝牙(只发红外控电视)。③ 电源键 = 关机键(不是崩溃):第一次抓键时盒子”重启”了,日志只有 2 秒——实为 KEY_POWER 被 systemd-logind 拿去关机;服务层改 HandlePowerKey=ignore 后该键归本模块。④ 实现:新模块 local_ctl.py(纯逻辑层:input_event 解码、KeyMap(扫描码优先/键码兜底 + JSON 覆盖)、NumericEntry(kHz 输入 3 s 超时、确定提交)、波段内卷绕与跨波段跳跃、Controller 的 PTT 边沿语义)+ systemd 单元 mrrc-localctl(User=mrrc,无遥控器时空转等待、节点消失自动重开);PTT 释放走三条路(松键 / SIGTERM / 重连前),幂等。⑤ 测试 2369 → 2404(tests/test_local_ctl.py 35 项,无硬件)。⑥ 文档:新增独立指南 docs/W103D_REMOTE_GUIDE.md(配对判据、按键表、kHz 输入、排障、换遥控器改键位表),站点页 w103d-remote.html。⑦ 未做(明确边界):遥控器麦克风(厂商私有 BLE 服务,且服务端 TX 音频只从浏览器来,需新增”本地麦 → TX 队列”);红外(板子注册了 meson-ir 但实测两次 0 脉冲,接收头未焊)。 |
| SDD V2.96 | 2026-10-17 | pi | 「切到 FT8 就再也没解码」:采集停滞的恢复门只认浏览器,而 tap 的时间轴一旦落后就永不回头。 ① 现场(W103D 192.168.1.220,v1.28.1 源码逐字节一致):/api/ft8/engine/stats 给出 slots_started=67, slots_skipped=66, decodes=0,日志里 dmesg 在 13:31:37 记录 usb 1-2: clear tt 1 (9041) error -71 后整棵 USB 树(cp210x×2 / FT4222 / C-Media 音频)重枚举,采集流就此不再出 PCM(113 s),13:33:31 由操作员打开主界面(/WSaudioRX 连上)才触发 RX capture stalled … reopening;同一条 RX tap 日志的 drift=+118.27s 此后再没回到 0。② 缺陷 A(server.py):停滞恢复分支写成 elif audio_rx_clients: —— FT8 页面从不订阅 /WSaudioRX,于是「没人听」就没有任何代码去重开已死的采集流,解码器静默饿死(本次 113 s,但逻辑上无限期)。门改为「有人在用这段音频」(浏览器 / FT8 tap / 录音),并让告警点名三个消费者。③ 缺陷 B(ft8/platform/rx_tap.py):tap 只在第一个块上锚定 epoch,之后时间轴只按已投递样本数前进 —— 采集一停,它就永久落后墙钟;而 UtcRing 的高水位 = 这条时间轴,时隙网格却由墙钟派生,read_slot 因此永远凑不齐窗口(118 s ≫ 3 s delivery_grace),每槽 SKIPPED (no data within grace)。feed() 现在把调用方本来就传进来的块 epoch 与自身时间轴比对,偏差超过 RESYNC_AFTER_S(0.5 s,比链路的 ±20 ms 抖动高两个量级、比 15 s 时隙小得多)就重新锚定并发 WARNING;UtcRing 把这段空洞记成 gap,跨洞的时隙被跳过,而不是拿拼接过的音频去解码。④ 验证:先写两个失败测试(tests/ft8/platform/test_rx_tap.py::test_feed_reanchors_after_a_capture_outage、tests/test_audio.py::test_the_ft8_tap_counts_as_a_listener),修复前分别为「still 120.0s behind」与「no logs of level WARNING」,修复后转绿;unittest 2369 项 OK(1 skip),tests/ft8 + tests/test_audio.py 1113 项通过。真机热修(两文件 + 重启服务):slots_started=15, skipped=3, decodes=11(含 AP 解码与 4 次深路径升级),drift 由 +118.4 s 变为 ±0.05 s,日志出现两次自愈重锚(+3.10 s / +3.44 s)。⑤ 边界:USB 掉线本身是物理层(当天 13:20/13:31/13:44/13:45 共 8 次 1-2 断连,机箱上是 /dev/ttyUSB* + FT4222 + 音频共用的一只 4 口无源 HUB,按键发射时最易触发)—— 软件现在 2 s 内自愈,代价是每次 1–2 个时隙与短暂的 CAT/频谱中断;根治要有源 HUB / 短屏蔽线 / 磁环,与 §13 R11 同源。盒子上跑的是两文件热修,version.txt 仍为 1.28.1(正式发布时随版本号走)。 |
| SDD V2.95 | 2026-10-17 | pi | 发货盒子的两条退路:面板改系统口令(root/mrrc)+ 预置救急 Wi-Fi(rescue/abcd1234,STA)。 ① 系统口令:/settings 新增「系统账号口令」,POST /api/box/system-password → box_ops.set_system_password → root helper 的新动词 set-password(允许表只有 root/mrrc —— 自由用户名会让这个套接字变成「变成任何人」)。口令只进 chpasswd 的 stdin:不进 argv(/proc/<pid>/cmdline 全世界可读)、不进日志、不进状态字典;helper 的日志只有 set-password ok for 'root'。真机(BG7ZHS,1.28.1):面板改 root → 立刻用新口令 SSH 成功 → 改回;短口令 400 拦在 helper 之前。② 救急 Wi-Fi:preflash.sh 在发货通道里写一条 STA profile mrrc-rescue(ssid=rescue、psk=abcd1234、autoconnect=true、priority=-10、retries=0、hidden=true、band=bg,keyfile 0600)。方向是盒子当客户端:MT7663S 做不了 AP(2026-10-11 真机:能开、信标发不出去,tx_packets 60 s +2),所以救急只能是「客户手机开同名热点,盒子自己连上去」;优先级 -10 保证客户自家的网在时它绝不抢。--wifi-only + MRRC_PREFLASH_WIFI_DIR 是单测真跑的入口(keyfile 内容 / 0600 / 口令长度与换行的拒绝都在真跑中断言)。③ 测试 2343 → 2368(preflash 救急 Wi-Fi 10、box_helper set-password 6、box_ops 客户端 5、server 端点 5)。④ 文档:CHANGELOG、packaging/box/README.md §4.5、docs/W103D_USER_GUIDE.md §2 / §8.10 / §8.11。 |
| SDD V2.94 | 2026-10-17 | pi | Wi-Fi 加入不再依赖扫描:写 profile 再 up,于是「AP 只在客户家」也能预置。 ① 现场:代刷盒子给客户时,客户的 AP 在工作台上必然不可见。net_wifi.connect_wifi 一直发 nmcli device wifi connect <SSID> password <PSK>,而这条快捷方式基于扫描:SSID 不在列表里就报「未发现 SSID 为 … 的网络」且不写任何 profile(1.28.1 / nmcli 1.42.4 真机实测,profile 数 5→5)。同一句也解释隐藏 AP:它永远不在扫描列表里。linux/wifi_preseed.py(开机读 /boot/mrrc-wifi.txt)共用这条路径,而它的全部目的就是「看不见的时候连上」。② 修法:connection delete <ssid> → connection add type wifi con-name <ssid> ifname <ifname> ssid <ssid> → connection modify(802-11-wireless.hidden yes / 802-11-wireless.band bg / connection.autoconnect yes / connection.autoconnect-retries 0,有密码时再加 key-mgmt wpa-psk + psk)→ -w 25 connection up <ssid>。hidden yes 对广播 AP 无损(真机实测:同形态 profile 5 秒内连上可见 AP,freq 2422);band bg 钉死 2.4 GHz(MT7663S 连 5 GHz 会关联成功但数据面不通、~100 s 后固件不再应答扫描,见 w103d-box §陷阱);autoconnect-retries 0 = 无限重试,让「AP 以后才出现」成立;-w 25 有界(不在场时 nmcli 会等它自己 90 s 的默认值,而 perform_wifi_switch 之后只有 30 s 的 DHCP 预算)。profile 名仍等于 SSID —— box_ops._roll_back 与 wifi_preseed 都按这个名字写 autoconnect。③ 守卫语义收紧:test_server_box_pages.WifiSecretNeverComesBackTests.test_the_server_never_asks_nm_for_a_secret 原先按字面量禁掉整个 wireless-security.psk,一个合法的写入撞红了它;现在禁的是读的四种形状(--show-secrets、-g/--get-values 查 psk、connection export、读 NM 的 keyfile 目录),并断言「密码只在 join 里写一次」,net_wifi/box_ops/server 三模块内该字面量出现次数 == 1。④ 测试 2327 → 2343(test_net_wifi.ConnectTests 4 → 13;test_box_ops_wifi_switch 的 17 处桩键由 device wifi connect 改为 connection up NewNet,语义不变)。④b 回滚的两种结局(同日第二次现场):box_ops._roll_back() 原先无条件把失败 profile 的 autoconnect 置 no(旧 Hotspot 教训),于是「扫不到的客户 AP」虽被存下却立刻禁用,面板显示「已保存(不自动连接)」,客户现场永远不会连。现按失败类别分开:net_wifi.access_point_absent()(超时 / 未发现 SSID / no network with SSID)→ 保留 autoconnect=yes,提示改成「已保存,这个网络出现在范围内时自动连接」并带上旧网络的下场;凭据被拒(Secrets were required)→ 仍然置 no。状态快照多一个布尔 saved(键集合的守卫随之更新:仍然没有任何能放密码的键),/settings 据此把「已保存」显示成绿色而不是失败红。新增 7 条测试(AccessPointAbsentTests 5 + 回滚两结局 2)。⑤ 真机证据(W103D 1.28.1 / eMMC 固化后):同形态 profile 冷启动 5 秒自连;eMMC 上客户 AP(扫不到)的 con up 报错落在「SSID 找不到」而不是「属性无效」⇒ profile 合法、只缺 AP。边界:修复在源码与已装盒子上(盒子的 /opt/mrrc_modern/net_wifi.py 已就地替换并重启生效),镜像需下次构建才带;AP 侧仍有离线消不掉的不确定性(5 GHz-only、WPA3-only、SSID 抄错),兜底是「插网线 → /settings 重填」。 |
| SDD V2.93 | 2026-10-17 | pi | v1.28.1 发布:hub 云证书不再被孤儿化 + FT8 的 DSP worker 不再自杀;四份产物出自同一修订 66b6930。 ① 本版内容见 CHANGELOG 的 v1.28.1 条目,以及 V2.90(FT8 worker 不再 abort 宿主进程 + 死因落盘)与 V2.88(PortAudio 重初始化不在 RX 流活着时 terminate);云证书那一笔是 2026-10-15 就写好、一直躺在未合并分支上的 de69bdd —— ssl_bootstrap.sign_for() 幂等,已坏的实例点一次「连接」即自愈。② 产物(四份均出自 66b6930;Windows 侧的 VM certutil -hashfile 与 Mac shasum 逐字一致):macOS MRRC-Modern-v1.28.1-arm64.dmg 85,064,796 bytes 1c9ba24e…;Windows MRRC-Modern-v1.28.1-Windows-x64-Setup.exe 79,386,940 bytes cfbeacc7…;W103D MRRC-Modern-1.28.1-w103d.img.gz 1,170,343,847 bytes 749e442a…;rpi64 MRRC-Modern-v1.28.1-rpi64.img.xz 530,820,848 bytes a00ab228…。③ 验证:macOS 的 codesign --verify valid + Designated Requirement、version.txt 与 CFBundleShortVersionString = 1.28.1、包内 ft8/data/cty.dat 与 libwsjt_core.dylib(依赖全 @rpath);Windows 包内 _internal\wsjt_core.dll + FFTW/gfortran 闭包 + _internal\ft8\data\cty.dat、app 根 version.txt = 1.28.1;W103D 与 rpi64 各跑四步产物验证(代码身份逐字节一致、version.txt = 1.28.1、11 份 profile、frpc 是 aarch64 ELF、FTDI 两库 + 符号链接、User=mrrc 与 EnvironmentFile == MRRC_CONFIG_FILE 双指向、chroot 内 deps + libopus + FT8 DSP abi 2 / paths 0x7)。④ 发布期抓到的两个真缺陷:一是 auth-tokens.json.invalidated-<ts> 被烤进盒子镜像(排除规则写的是精确名 auth-tokens.json,而工作树那份已被运行时改名退役;产物验证第 ② 步抓到,规则改 auth-tokens.json* + 真 stager 守卫补该真实文件名 + 冻结清单同步);二是 Windows 门禁第一次收集 tests/test_preflash.py 就 8 红(subprocess.run(["bash", ...]) 在 VM 上没有 bash;按仓库惯例在 setUpClass 按 shutil.which 跳过,BuildWiringTests 保持全平台)。⑤ 测试:本机 2327 项 OK(1 skip);Windows VM 门禁 2324 项 OK(含上面那条 skip);release_check.py 离线 32 ok / 0 failing。⑥ 站点/指南/mac_pack/win_pack 的四份尺寸与 SHA、SDD/README 的版本与基线、12 张图标记与生成页一并同步。 |
| SDD V2.92 | 2026-10-17 | pi | W103D 发货版(预置)镜像:卖家代刷一批、快递给用户,用户开箱即用(AD-027 / NFR-069)。 设计与计划见 docs/superpowers/specs/2026-10-17-w103d-preflash-design.md、docs/superpowers/plans/2026-10-17-w103d-preflash.md。① 现场取证(1.28.1 产物的只读检查,本次最重的发现):env/mrrc.env 存在且不含 MRRC_WEB_PASSWORD/MRRC_SERIAL_PORT,/var/lib/mrrc/ 只有 certs/(无 firstboot-done)。配合代码可判定两件与文档不符的事实:串口/声卡探测在镜像上从未运行过 —— 首启包装器在”env 文件存在”那条分支上带 MRRC_PRESEED=1 调 first_run.main(),而那个键第一行就 return 0;口令也不是首启生成的,是服务启动时 _ensure_strong_password() 生成的;推论:/setup 的免密引导窗口(AD-026 的产物)在镜像上从未打开过 —— 后者不在本次范围,只写进指南与本次条目。② 出厂预置通道(默认关):新脚本 packaging/common/preflash.sh 在 MRRC_PREFLASH=1 时写 MRRC_WEB_PASSWORD(并显式清空 MRRC_AUTO_PASSWORD)、chpasswd 设 root/mrrc 口令、时区 Asia/Shanghai、locale zh_CN.UTF-8;取值可用 MRRC_PREFLASH_* 按批次覆盖。build-image.sh --preflash 只是把开关经既有的 -e 通道传进容器与 chroot;不传开关时镜像逐字不变(口令照旧现生成)。两道闸门:--preflash 默认 0,且 preflash.sh 在开关不为 1 时拒绝写任何东西(都有测试)。不预置型号/串口(D-3「不许烘焙猜测」与 NFR-067 同向)。③ 设置页两个新面板(服务端零改动):Wi-Fi 面板把手输 SSID 变成一等公民(点扫描结果只作带入),隐藏网络与”扫描返回空表”从此有出路,密码框加显示/隐藏、默认仍掩码,空表时给一句提示;新增「公网入口」面板(呼号 → POST /api/cloud/apply → 状态/入口地址,只读 GET /api/cloud/state,查询走 POST /api/cloud/refresh)—— 已保存的 PSK 仍然不回显(NFR-068 / AD-021 不动);已有令牌时按钮变”查询状态”,因为门户对同呼号重复申请是查重拒绝的(store.apply)。顺带把该页 7 处 innerHTML 清空换成 replaceChildren()、扫描行改 createElement,该页不再有 innerHTML(并被守卫钉住)。④ 首启探测自愈:MRRC_PRESEED=1 的语义从”短路返回”改成”不生成口令、其余照常”,于是探测在镜像上真的会跑;needs_first_run(env, port_exists=…) 增加”配置的串口节点此刻在场”判据(POSIX 路径才查文件系统,COM5 读作”在”);firstboot_wrapper 只在配置定型时盖 firstboot-done —— 卖家工作台上那次开机(U 盘、无电台)因此不会把”已完成”克隆给用户,用户插上电台开机时才定型(证书与 headless 默认值仍在盖章之前,没定型的盒子照样能起网页)。同时补齐 Linux 那份相对 macOS 漏掉的两条(口令在探测之前落盘、探测循环前先 print 一行),并把发现结果改成 typed tuple。⑤ 测试 2296 → 2326(+30;三个新文件 test_preflash.py/test_firstboot_wrapper.py 与 test_server_box_pages.py 的 9 条守卫,另加 first_run 系列 11 条),全量绿(唯一红是 release_check 的常驻项与计数同步,均已处理)。⑥ 真构建与产物验证(同日实测):预置版 1,212,223,182 B / 17f3fe79…、对照版(不带开关)1,191,513,586 B / 8f743cf3…;两份 gzip -t 通过且 gzip -dc 的内容哈希等于各自构建树,四步验证全绿(代码身份逐字节、11 份 profile、/tmp 零残留、D-11 单元身份、chroot 导入门禁含 libopus;shadow 复算用 crypt——Debian 12 的 chpasswd 默认写 yescrypt $y$,openssl passwd 复核不了),预置断言与对照断言互为反向(固定口令/Asia/Shanghai/zh_CN.UTF-8 ↔︎ Etc/UTC/en_US/shadow 不符),两版都在产物里真跑首启包装器:无电台 ⇒ 不盖 firstboot-done(下次开机再探)而证书照生成。构建期拓出两个真缺陷:①脚本假定了基底的 NTP 实现(无条件 enable systemd-timesyncd,而基底是 chrony 且已 enable)→ 改为”不挑实现、但没有 NTP 单元就构建失败”(d7af424);②两个会话并发构建同一产物路径,交错写入产出 trailing garbage 的坏包 → 验收门加严为”gzip -t 通过 且 日志 SHA == 文件 SHA”。⑦ 未验边界:真机 A1–A8(三条口令实测登录、/setup 302、手输 SSID、呼号申请与审批、插上电台后重探、timedatectl 同步)本机没盒子,读数待回填;无 AP 兜底仍是硬边界;发货版镜像不上传下载页(它带公开固定口令)。 |
| SDD V2.91 | 2026-10-17 | pi | 静态资源分发:实例补 Cache-Control 与条件请求 304,hub 边缘补 gzip 与 1 小时缓存 —— 「每次加载都慢」的三条根因一次收口。 ① 现场(hub 访问日志 + 逐项实测):实例此前只发 ETag/Last-Modified、不实现条件 GET(带 If-None-Match 仍返回 200 与完整 534,520 B 的 modules/opus_wasm.js),而 nginx 默认只压 text/html ⇒ 首屏 13 个引用共 816 KB 原样过隧道;日志里同一浏览器反复 GET /modules/opus_wasm.js 200 534520。 ② 实例侧(serve_static):文档类(HTML/JSON/sw.js)no-cache、其余静态件 public, max-age=3600(刻意不写 immutable —— ?v= 是手工 bump 的,1 小时是漏 bump 的最长窗口);_not_modified() 按 RFC 7232 先判 If-None-Match(含 W/ 弱校验)再判 If-Modified-Since,命中回 304,ETag 沿用 Starlette 自己的公式(换公式会让已缓存用户全部重下);有扩展名而文件不存在 → 404(/favicon.png 此前拿到 40 KB 的 index.html 被浏览器当图标收下),无扩展名的 SPA 路由仍回落 index.html。 ③ hub 边缘(mrrc_hub V0.37 的同批改动):gzip 覆盖 JS/CSS/JSON/SVG;静态资源 location 用 proxy_cache(1 小时、proxy_cache_valid 200 1h 只缓存 200、键 实例+URI+query,外加 revalidate/lock/background_update/use_stale),map $status 只在 200/304 上发 Cache-Control —— 未登录取静态件拿到的重定向绝不能带 max-age,否则已登录用户会被锁进一小时的登录页循环;proxy_cache_bypass 让硬刷新穿透回源。 ④ 实测:opus_wasm.js 传输 534,520 → 170,402 B(gzip),第二次起由边缘 304 作答、不穿隧道;条件请求、硬刷新、HTML 不进缓存、重定向不落盘逐条实测。 ⑤ 测试 2280 → 2296(新模块 tests/test_static_cache_headers.py 16 项:分类、304 四态、回落规则、鉴权顺序;tests/README.md 计数与新模块小节同步),全套件 macOS 2295 OK / 1 skip。 ⑥ 边界:static/sw.js 仍未被任何页面注册(死代码,却有 6 处测试为它的 CACHE 版本钉值)—— 本次不动,见设计 §7;HTML 的边缘缓存与 ?v= 自动生成同样留作后续。设计:docs/superpowers/specs/2026-10-17-static-asset-delivery-design.md;hub 侧:../mrrc_hub/deploy/verify_hub_cache.sh。 |
| SDD V2.90 | 2026-10-16 | pi | FT8 深路径每时隙杀死 DSP worker:Fortran 的 stop 会终止宿主进程,而死因结构性不可见。 ① 现场(诊断包 20261016-230940-40c1):deep decode path failed; keeping the fast results 紧接 WorkerFault: worker_exit: worker process exited with status None,其后 slot N: 0 message(s) from 0 result(s) —— 每 15 s 一次。② 指纹:mrrc: FT8 REST/WS routes mounted(server.py 模块级那行)每槽重现 —— Windows 的 multiprocessing spawn 会重导入主模块,所以这一行就是「又新建了一个 worker」的计数器。③ 不是解码逻辑错:本机 macOS 用同一份源码 + 重建的 libwsjt_core.dylib 跑 tests/ft8/dsp 深路径 49 passed / 1 xfailed。④ 能终止宿主进程的位点只有三处 Fortran:wsjt_improved.f90:76 的 error stop 'FFTW thread initialization failed'(每进程一次、失败即死,形态最吻合)、patched/four2avar.f90:85 与 vendor/four2a.f90:84 的 stop 'Unsupported request...'、vendor/packjt.f90:940 的 stop(呼号含非法字符)。⑤ 死因被结构性丢弃:windows/launcher.py:370 把 server 的 stderr 并入 stdout 管道,而 launcher_log.StartupTee.stop()(launcher_log.py:82-92)在服务器答 HTTP 时只关文件句柄、pump 继续读却不再落盘 ⇒ worker 的 fd 2 输出(Fortran 死亡信息、未捕获 traceback)不进任何日志,支持包因此永远只能给出 status None。本次 tee 于 23:06:00 停止,FT8 首次深径在其后。⑥ 修法:ft8/core/worker.py 新增 _redirect_worker_output()(worker_main 的 OMP 行之后立即 os.dup2 到有界的 ft8-worker.log,超 2 MB 轮转 .1,失败静默不误事);wsjt_improved.f90 的 error stop 改为 write(0,...) + status = WSJT_E_INTERNAL 返回(绝不 abort 宿主进程),worker 把状态号放进 dsp_error 细节,父进程于是看到 DecodeError: dsp_error (status 4) 而不是进程消失;supervisor._exit_detail() 在 worker_exit 里补上 generation / restart 数,把「崩溃循环」与「一次性崩溃」区分开;support_bundle 新增 ft8-worker / ft8-worker-prev 两个收集角色。⑦ 验证:本机重编 ft8/dsp 后 tests/ft8 pytest 996 passed / 4 skipped / 1 xfailed(含新增 3 项:日志路径、轮转、真实子进程的 fd 2 捕获);tests/test_support_bundle.py 38 项 OK。⑧ 未包含与代价:patched/four2avar.f90 与 vendor/packjt.f90 两处 stop 按仓库「vendor 永不改、patched 需与上游同步」的规矩未动,它们仍会终止 worker —— 靠 ⑥ 的日志在现场指认;改了 ft8/dsp/ 就要重跑四条 DSP 构建路径(本机 macOS 已重编并跑通,Windows / Linux / 树莓派在发版时编)。⑨ 顺带:dist/verify_v1280_vm.ps1(未纳入版本控制)的 FT8 闭包检查写的是双精度 libfftw3-3.dll,实际产物是单精度 libfftw3f-3.dll + libfftw3f_threads-3.dll —— 已就地改正,正确清单见 tools/win_verify_1270.ps1:21。 |
| SDD V2.89 | 2026-10-16 | pi | v1.28.0 四平台发布:电台电源回到菜单(两种意图分开、唤醒有界可取消);出站 HTTPS 只信随包的根证书;FT8 级联的深路径第一次真正跑起来;外加三个只在产物里现形的缺陷。 ① 电台电源(V2.86 的设计落地 + 真机验收):power 命令的守卫提成 _power_off(),新增 _power_rescue()(每轮复用既有 _power_on_radio(),新代码只有预算/取消/广播);协议向后兼容(true | false 不变,新增 "restart" | "cancelRescue");补掉「FT8 正在发射时PS0拦不住」这个真空档;真机(W103D + FT-710,2026-10-16)V1 关机 / V2 第 1 轮醒来 4–8 秒(三次皆然) / V3 取消 / V4 启动窗口内拒绝 / V4b 窗口过后可关 / V7 listen 拒绝 / 单次开机回读通,V5(发射中)与 V6(拔电)未测;2026-07 那条「PS1 唤醒不可靠」不复现,最可能的解释是旧脚本拿过期的 /dev/ttyUSB* 发命令而救援循环等端口回来再写。真机跑出并修掉两个状态同步缺陷(救援成功后没更新 power_on;_power_on_radio 回读确认后没跳过随后的 power_on轮询 —— 单次开机路径本来就跳过,救援路径漏了),各带回归测试。② 出站 HTTPS 只信随包根(V2.87 的策略变更在本版随产物下发):一台 Windows 的证书库里有一份与被服务链条同名同密钥却已过期的副本,OpenSSL 顺着它建链 ⇒ 门户/隧道/上传全挂而浏览器正常;net_tls._bundled_roots() 从零构造上下文、不读 SSL_CERT_FILE/DIR,只在没有可用 bundle 时才回退平台库并告警;tests/test_tls_trust_store.py 用合成 PKI + 真握手离线复刻(受污染库那一侧必须真的以 expired 失败,反向对照在场)。③ FT8 级联截止 2.5 → 4.0 s(V2.85 的落地):预算守卫恒得 2.5−0.48−0.05=1.97 s < 深路径下界 2.3 s ⇒ 现场 263 次 deadline_risk/ 0 次升级,深路径从未运行;新增守卫断言「截止 − 快路径落地 − 余量 ≥ 深路径实测下界」并做反向验证,LatencyHistogram 尾部边界随截止长到 4.0(0.5…2.5 不动),新增只读 GET /api/ft8/engine/stats 与 FT8 页面 30 s 轮询的升级率(浮点,不是百分比整数)。④ 支持流水线六天停摆:cron 命令行里那个裸 --implement(argparseconst="ALL" 为真)排在 --once之前 ⇒ 1728 行 run.log 里没有一行「没有新包」;main() 判定顺序改为 --once 优先、PATH 补回 /usr/bin:/bin:/usr/sbin:/sbin、publish() 发货失败撤回 state 记录下一轮重试,四条守卫测试。⑤ ☰ 菜单页脚印真实版本号:那个 #menu-version 从第一版起写死 v1.0(v1.14–v1.27 一直如此);现在随fullState 下发 support_bundle.detect_version()的值,老服务端不发该字段时不打印 undefined;index.html 改动 ⇒ sw.js 的 CACHE mrrc-v53→v54(四处 pin 同步),8 条测试按号经过的每一段钉住。⑥ 三个只在产物里现形的缺陷(洁净室真跑与构建门禁抓出):ft8/data/cty.dat 不在 PyInstaller 的 datas 里 ⇒ 打包版 DXCC 库解析到不存在的位置、每次首解留下 cty.dat unreadable回溯且国家名/新 DXCC 高亮静默失效(挂载 v1.27.0 的 DMG 复核为既存缺口);stage-tree.sh 只按名字排除 venv/ 与 .venv/ ⇒ 并行会话的 .venv-ml(macOS venv,torch 596 MB)被整棵装进 ARM 镜像,实测暂存树 16,352 文件/619 MB → 430 文件/23 MB(它也是盒子镜像frpc: Cannot write: No space left on device 的直接原因,日志一个字没提空间);三条只在 POSIX 上成立的测试此前从未在 Windows 上被收集(autopilot 的 fcntl 单飞锁、stage-tree 的 bash+rsync)⇒ 按仓库惯例加平台 skip(VM 33 → 36),而不是把锁降级成假锁。⑦ 过程事实(都写进 skill):盒子镜像的 build/w103d/w103d.img是上一版叠过的树,正常发版的上一轮会把它留在原地 ⇒ 下一次发布是在上一版之上再叠(实测根分区只剩 638 MiB,apt-get update 以 GPG error … signature failed 死掉);源码包排除清单增 .venv-ml 与 *.apk/*.MOV(实测 425 MB → 72 MB);发布中途另一条会话在同一棵树落了音频修复(124fb63,按发射后进程消失、电台卡在发射) ⇒ 四个产物必须同源,macOS 与 Windows 因此重建(盒子镜像本就含它,已逐文件核对audio_handler.py/server.py)。⑧ 产物与验证:macOSMRRC-Modern-v1.28.0-arm64.dmg 85,044,185 bytes 8d3564d4…;WindowsMRRC-Modern-v1.28.0-Windows-x64-Setup.exe 79,370,813 bytes cc8cc4fb…;W103DMRRC-Modern-1.28.0-w103d.img.gz 1,205,181,718 bytes c90dbcf1…;树莓派MRRC-Modern-v1.28.0-rpi64.img.xz 519,284,304 bytes 77ae09b7…。unittest 2274 项(macOS,1 skip);Windows 构建门禁 2271 项 OK(37 platform skips) + tlstrustgate通过;release_check.py 离线 32 ok / 0 failing;盒子镜像四步验证全过(代码身份含 audio_handler.py、装机内容 11 profiles/CLI/frpc aarch64 ELF/FTDI/udev、无运行期状态(v1.27.0 曾漏mrrc_modern.env + auth-tokens.json)、无.venv-ml、D-11 双指向、chroot 内libwsjt_core.so abi=2 / decode_paths=0x7与 scipy/soxr 导入);macOS 与 Windows 冻结包洁净室真跑(https /login 200、http 000、/api/health401、FT8 engine started at boot、cty.dat unreadable0、Traceback 0、运行后包体零改动、codesign --verify 有效),并从运行中的冻结包读到 fullState.version = 1.28.0。边界:TX 音频仍需物理 Windows 真机验收(KVM 必现噼啪);V5/V6 两项电源场景与 FT8 升级率的 3 天窗口仍欠现场读数。 |
| SDD V2.88 | 2026-10-16 | pi | 「按发射 → 进程消失 → 电台卡在发射」:PortAudio 重初始化在 RX 采集流仍被读取时 terminate,构成原生访问违例;而放键路径全在进程内,进程一死电台就永远保持发射。 ① 现场(诊断包 20261016-230940-40c1,Windows 11 + FT-710):用户报「手机上按发射,电台持续发射不再停下来,同时 MODERN SERVER 进程消失,只有强行关电台再重启电脑上的程序」。包内 RX open failed ([Errno -9999] Unanticipated host error) — re-initializing PortAudio 与 Configured audio device '麦克风 (USB Audio Device)' not found 说明这台机器正处在 V2.12/V2.37 记录过的 Windows USB 声卡条件(重枚举 / WDM-KS 重复条目 / 配置名锁死)之下;启动时 cloud hub: clearing a stale frpc (pid 14532) 证明上一个进程没跑过 shutdown 清理。② 根因(两条既有路径的交集):server.py 的 ptt:true 先 cat.set_ptt(True)(FT-710 写 TX1;)再开 TX 音频;而 audio_handler.start_tx() 在 TX 打开连续失败后调 _reinit_pyaudio(),后者直接 self._pa.terminate() —— 没有先关闭 _rx_stream。RX 采集读在事件循环线程(_audio_rx_loop → read_rx_chunk),与 Pa_Terminate() 并发 ⇒ use-after-free ⇒ 原生访问违例:进程没有 Python traceback 就直接消失(这正是包内 292 条回溯没有一条来自主进程的原因),而唯一的两条放键路径(socket 释放 / 断开 dead-man)都要求进程存活 ⇒ 电台保持发射。同类竞态还在 stop_rx()(TX→RX 的 restart_rx 每次都走)与 _schedule_restart() 的 os._exit(42)(跳过 lifespan 的 Force-RX)。③ 修法:_reinit_pyaudio() 在惰性创建的 _stream_lock() 内先关闭 _rx_stream/_tx_stream 再 terminate(_rx_running 故意保持 True,交给 _audio_rx_loop 的 3 s 采集停顿看门狗重建 RX);stop_rx() 同锁;read_rx_chunk() 非阻塞取锁(重建期间这一拍返回 None,既不阻塞事件循环也不碰正在关闭的句柄);ptt:true 里 start_tx 抛异常(而非返回 False)也走 unkey;_schedule_restart() 退出前用 run_coroutine_threadsafe(cat.set_ptt(False))(1.5 s 上限)放键。④ 测试:tests/test_audio.py 111 → 115(关流先于 terminate ×2、重建持锁时不读 ×1、stop_rx 置空 ×1),tests/test_server_setup.py 10 → 12(发射中重启先放键、未发射不发多余命令);套件 2267 → 2273 全绿(唯一失败 test_offline_release_check_is_clean 是既存的官网下载卡陈旧,与本次无关)。⑤ 边界:进程被原生调用或外部 kill 时没有任何 Python 兜底能执行 —— 电台侧 TOT 仍是唯一最终保险,答复里已写入。⑥ 未包含:FT8 DSP worker 每时隙 worker_exit(同一包里另一个独立问题)。 |
| SDD V2.87 | 2026-10-16 | pi | 出站 HTTPS 不再采信本机证书库 —— 同一个缺陷的第二张脸,以及它的离线复刻。 ① 现场(同日):应用报 certificate verify failed: certificate has expired,而同一台机器的浏览器打开同一个门户完全正常;门户侧证书链有效、日志里没有来自该实例的任何请求 —— 说明 DNS/TCP/握手/证书交换都成功,是客户端自己拒绝了。② 根因:本机证书库里有与被服务链条同名同密钥、却已过期的副本(Windows 会缓存见过的中间证书,Let’s Encrypt 的中间证书换代重签,缓存那份到期后正是这个形态);OpenSSL 把库里那张放进链里判 expired,并且不会回退到服务器送来的有效副本。③ 旧策略是错的那一半:net_tls.build_context() 只在默认库为空时才装随包 CA(store_size() > 0 提前返回),也就是把「库非空」当成了「库可用」—— 与 2026-10-04 那次(macOS 库为空)是同一个缺陷的两张脸:把信任判定外包给机器。④ 修法:_bundled_roots() 从零构造上下文(不经 create_default_context()、不读 SSL_CERT_FILE/DIR),只信 MRRC_CA_BUNDLE → 随包 vendor/ca/cacert.pem;只有在没有可用 bundle 时才回退平台库并告警(精简安装仍可用)。部署在 TLS 解密环境里的运维用 MRRC_CA_BUNDLE 指自己的 bundle。⑤ 证据(本地可复现):tests/test_tls_trust_store.py 新增合成 PKI + 真握手:受污染库那一侧必须真的以 expired 失败(反向对照,证明测试能变红),而我们构造的上下文必须握手成功;另有策略断言(结果里的根数 = 我们 bundle 的条数,机器库一条都不进来)。测试 11 → 13 项,套件 2228 → 2230 全绿。⑥ 用户侧:未升级的机器仍用同日的 MRRC-CertDoctor.zip(免安装体检/修复工具);答复页那张卡同步说明应用侧已改。⑦ 同步:AGENTS.md 的 net_tls 行、DEPENDENCIES.md、CHANGELOG、tests/README 计数与模块说明、SDD/README V2.87 与站点徽章/示意图标记。 |
| SDD V2.86 | 2026-10-16 | pi | 电台电源回到界面 —— 但换成两个不可混淆的意图,并把「一次尝试」改成「有界可取消的持续尝试」。 ① 背景:power 命令与守卫自 V2.13 起只留给维护脚本(PS0 可靠、PS1 实测不可靠、PS0 落启动窗口会卡死 CAT MCU,需物理断电)。操作员要求把它作为 ☰ 菜单项放回,并明确「重启」与「停机」是两件事。② 语义:重启电台… = 关机 → 持续唤醒(每 30 s 一轮、上限 15 min、可取消、每轮刷新 15 s 保护窗口);关机… = 先取消救援、只关机、永不自发上电。③ 实现:WS 分支的守卫提成 _power_off()(唯一实现),新增 _power_rescue() —— 每轮复用既有 _power_on_radio()(PS1 ≤3 + FA;/CI-V 回读 + 刷窗),新代码只有预算/取消/广播;协议向后兼容(true | false 不变,新增 "restart" | "cancelRescue");新增广播powerRescue(state/attempt/elapsed/maxSeconds);前端新增独立模块static/modules/power_switch.js(菜单两项 + 顶栏PWR 一格,救援期间可点取消),两个受 tooling-guard 保护的 monolith 只加三行。④ 补掉一个真空档:原分支只查 radio.is_transmitting,早于 FT8 存在 —— FT8 正在发射时PS0 本来是拦不住的,现在走 _ft8_transmitting()。⑤ 可观测:每轮与结论各一条日志(含第几轮、耗时)。⑥ 测试:tests/test_power_switch.py9 → 29 项(守卫 3、救援 6、协议接线 3、前端守卫 6、原有 9 + 2 适配)、tests/test_listen_only.py 的禁止项补三个新取值;套件 2210 → 2228 全绿。 ⑦ 真机读数(2026-10-16 当晚,W103D 盒子 + FT-710;干净室实例 + by-id 串口):V1 关机 ✅(power_on=false);V2 重开 ✅ 第 1 轮醒来、4–8 秒(三次运行皆然;日志 Power rescue woke the radio on round 1 after 5 s);V3 救援中取消 ✅(cancelled,未再唤醒、未上电);V4 救援中点关机 ✅ 被拒(「电台正在启动,请 15 秒后再关机」—— 正是防 CAT MCU 卡死那条守卫);V4b 窗口过后关机 ✅;V7 listen 角色 ✅(power 的四个取值全被角色门拒绝);单次开机 ✅(Power-on verified (PS1 attempt 1),FA; 回读路径在真机通);V5/V6 未测(没抓到发射窗口、没有拔电)。结论:2026-07 的「PS1 唤醒不可靠(3 次/36 秒全失败)」在真机上不复现 —— 最可能的解释正是设计时那条待复核的怀疑(旧脚本拿过期的 /dev/ttyUSB* 路径发 PS1,而电台关机后 USB 重新枚举、节点名会变;救援循环改成等端口回来再写)。逃生舱未触发,两项都发布。⑧ 真机跑出的两个真缺陷(已修 + 回归测试):① 救援路径唤醒成功后没有 radio.update(power_on=True) —— 日志说醒了、界面仍显示关;②_power_on_radio 回读确认后没有跳过 power_on 轮询 —— 紧跟其后的 PS; 把已确认的开机读回成关(单次开机路径本来就跳过,救援路径漏了)。⑨ 现场附带事实(设备侧):测试期间电台 USB 重新枚举,CP2105 从 ttyUSB0/1 变成 ttyUSB1/2,固定写 /dev/ttyUSB0 的生产配置当场失效 —— 已把该盒子改成 by-id 稳定路径并复核云入口 401。rescue 状态不持久化(服务端重启即结束);V5/V6 仍未测。 |
| SDD V2.85 | 2026-10-13 | Claude | FT8 级联的深路径在生产里从未运行过 —— 决策截止的算术错误让每一次升级都被预算守卫挡下,功能整体空转。 ① 缺陷(现场取证):DEFAULT_DECISION_CUTOFF_SECONDS = 2.5(I9 决策截止)是照着单条解码路径量的,而级联要求它覆盖「快路径结果落地延迟 + 最深那条路径的耗时」;预算守卫 remaining = cutoff − (clock() − boundary) − margin 于是恒为 2.5 − 0.48 − 0.05 = 1.97 s < 深路径下界 2.3 s ⇒ 每一次通过策略判据的升级都被拒(deadline_risk 累计 263 次、升级 0 次,约 2.5 h 现场窗口),盒子一直只跑快路径 —— 而升级率没有任何出口,所以「深路径没跑」这件事从界面上看不出来。② 修法:截止 2.5 → 4.0 s,并把「快路径落地 +0.48 s」从注释里的散文提成具名常量 FAST_PATH_FINISH_DELAY_SECONDS(它就是守卫减掉的那一项),新增守卫测试断言 cutoff − 快路径落地 − margin ≥ 深路径下界,并做反向验证(把截止改回 2.5 必须让守卫变红)。③ 顺带修掉一处同源的不一致:LatencyHistogram 自称桶对齐决策截止,但最后一个边界停在 2.5 —— 截止抬到 4.0 之后 2.5~4.0 s 的解码全掉进溢出桶、彼此不可分辨;边界只在尾部增长(0.5…2.5 不动,历史分桶语义不变)到 4.0,并用「常量对常量」的测试把两者钉在一起。④ 观测:runtime.cascade_stats 增 escalations/fast_decodes(引擎未起时少键不抛),新增只读 GET /api/ft8/engine/stats(会话门,不占 lease),FT8 页面状态栏 30 s 轮询显示升级率与计数 —— 升级率是判断这次修复有没有生效的唯一读数,修之前恒为 0、修之后必须非 0;用浮点而不是百分比整数(0.4% 显示成 0 就白测了)。⑤ 文档同步:截止的 2.5 逐字写在 SDD/09、SDD/13、级联 spec 与生成的网站上,四处全部改为 4.0 并注明理由与本次改动;SDD/09:104 与 SDD/14 里同名但无关的 2.5(ATR1000 的 meter wait)刻意不动。⑥ 测试:tests/ft8 754 → 770 项(pytest 收集数;跑完 767 passed / 2 skipped / 1 xfailed)—— +截止守卫 2、延迟桶边界 2、cascade_stats 计数器 2、端点 7、页面静态资源 3,另有若干条旧断言按新截止改写(桶标签、守卫算术)。⑦ (2026-10-14 追加,同一分支未发布活)日志自给:ft8/runtime.py 的每时隙行尾补上 [<path> escalated? late?],并每 _COUNTERS_EVERY_SLOTS = 20 槽打一行 decode counters after N slot(s): decodes=… fast=… escalations=… (…%) late=… errors=…。理由来自现场:①~④ 上线后操作员问「多少槽升级了、多少槽掉出 4.0 s 截止线」,而日志答不了 —— 成功的升级在 cascade.py 里一个字都不记(那里只记拒绝)、late 只活在 WebSocket 帧里,能读的数字只有一个要 30 天会话 cookie 的 GET /api/ft8/engine/stats。于是诊断只能拿发布偏移去推,推错两次并当场收回(原生库 Block size banner 不是深路径标记:2119 次里 51/56 个升级后时隙前都有它,含 −0.17 s 的快槽;「偏移 > 2 s ⇒ 升级过」也不成立:空槽同样慢在 +1.44~+3.05 s,且快带本身还在漂)。测试 tests/ft8 收集数 770 → 776(跑完 773 passed / 2 skipped / 1 xfailed):新增 tests/ft8/test_runtime_slot_counters.py 4 项,做过反向验证(回退实现后 4 条全红),并为此在 tests/ft8/test_server_wiring.py 的两处 object.__new__ 构造里补上 _slots_logged(那两处本就不跑 __init__,要自己补齐 _on_batch 读到的状态)。⑧ (2026-10-14 追加,同上)解码的 CPU 时间进协议:decode_ok 帧新增 cpu_seconds,在 Worker 进程内用 time.process_time() 量(父进程量会把音频与频谱线程算进来,而 RUSAGE_CHILDREN 对还活着的子进程不更新);I9 基准把它按「threads × CPU」列出来,矩阵补上生产值 threads=3。理由:wall 时间看不出假并行 —— 深路径的 OpenMP 区把整个符号谱每线程重算一遍,W103D 上 threads 1→3 只省 1.46× 的 wall 却多烧 1.6× 的 CPU(分析 §8.2);补测全矩阵后,wall 曲线在 3 线程触底、CPU 曲线一路爬到 5.2×,12 线程(0.306 s)比单线程(0.289 s)更慢。这是分析 §4 第 0 梯队点名要做的那次测量,现在以可复现基准(tests/ft8/dsp/test_i9_benchmark.py,MRRC_FT8_I9_BENCHMARK=1)常驻仓库。测试 tests/ft8 收集数 776 → 779(跑完 776 passed / 2 skipped / 1 xfailed;平台主套件 pytest tests 全量 2976 passed):新增 worker 上报 CPU 1 项,协议标量严格性参数化 +2(cpu_seconds 拒绝负值与非有限数)。纯测量:解码、时序、TX 无任何行为读取它。⑨ (2026-10-14 追加,同上)qso.db 从暂存表里挡掉 —— 一次就地升级会静默抹掉操作员的 QSO 历史:准备把 ⑦⑧ 部署到盒子时发现的。server.py:3106 把 FT8 数据目录定在 _writable_runtime_dir()/ft8,所以生产机的库就在 $MRRC_HOME/ft8/qso.db,而那正是暂存树里 ft8/ 落地的位置;packaging/common/stage-tree.sh 已经把 atr1000_tuner.json、mem_channels.json 这类运行期文件逐条排除,唯独漏了 qso.db。触发路径不是假想的:mrrc-update 明确支持 MRRC_UPDATE_SRC 指向本地目录(把未发布分支送上盒子的既有做法,本次就是这么用的),而开发者工作树里恰好有一个未提交的 ft8/qso.db(盒子上那份有 48 行 audit_event)。于是那次升级会把工作树的库盖到生产上,且整个过程一个字都不提数据库。修法:排除表加 --exclude "qso.db"(与相邻条目同类、不加锚点,以覆盖 ft8/ 下的位置);tests/test_packaging_parity.py 加 canary 与 test_the_qso_database_is_never_staged(注释里写明为什么会到这一步),冻结表同步记下这次编辑。部署脚本另加第二道防线:升级前后比对 qso.db 的 size/mtime/行数,暂存树里一旦出现 qso.db 就拒绝安装。⑩ (2026-10-14 追加,同上)A53 全矩阵与 Phase A 基线:⑦⑧ 部署到盒子后,第一次在真机上跑完 threads × CPU 曲线(tools/ft8/i9_thread_matrix.py —— 生产盒子上既没有测试树也没有 pytest,因为 stage-tree.sh 排除 tests//tools//dev_tools/,所以基准必须换成「numpy + 已安装 ft8.core.*」的现场孪生,先在开发机上与 pytest 基准逐点对齐再送过去)。结果:wall 在 3 线程触底 2.138 s,而生产配置正是 threads 3;CPU 单调爬到 3.45×,12 线程 4.134 s。两件事由此从估算变成事实:生产线程数已经是最优点(§9.4 那条「4 线程约 1.55×」的 Amdahl 估算作废 —— 实测第 4 个线程 +13% wall / +22% CPU,因为 sync 阶段不可切分);单次深路径 2.138 s 占 DEFAULT_DECISION_CUTOFF_SECONDS = 4.0 的 53%,于是 cycles 1→3 的 ~3× 在 A53 上是 ~6.5 s、超过截止(开发机上只要 0.630 s)—— 买灵敏度只能从这 2.138 s 里省,不能靠加 cycles。基线记在 docs/ft8-w103d-phase-a-baseline.md(含现场安装事实:4.0 截止已生效、qso.db 升级前后逐字节一致,以及「升级率」一节明确留空待操作员读数而不填推测)。一处对账:spec 2026-10-13-ft8-fast-path-recall-port-design.md:21 记的深路径成本 6.71 core-s 被本次在 Worker 进程内量到的 5.284 core-s 取代(老数只能从进程外量,混着音频与频谱线程),连带把「每槽都升级」的占比从 ~11% 修正为 ~9%;spec 正文留待 Phase B 设计时一并更新。顺带修一处测量卫生:原生解码器按 share_dir = '.'(相对进程 cwd)找 ALLCALL7.TXT(ft8/dsp/vendor/prog_args.f90:4),生产靠 systemd 单元的 WorkingDirectory=$MRRC_HOME 命中,而现场脚本是从操作员家目录起的 —— 实测不影响本工作负载的时间(首测 4.7% 的差重复三次后被推翻,故不写进结论),但它会跳过基于呼号库的假解过滤(searchcallsvar.f90:10 因 ldbvalid=.false. 直接返回 lfound=.true.),而唯一的提示是一行 pytest 基准从不打印的警告;tools/ft8/i9_thread_matrix.py 现以 enter_app_root() 在启动时 chdir,tests/ft8/test_i9_field_twin.py 5 项钉住(含「库路径不受 cwd 影响」的控制项)。测试 tests/ft8 收集数 779 → 784(跑完 781 passed / 2 skipped / 1 xfailed;平台主套件 pytest tests 全量 2982 passed)。⑪ (2026-10-14 追加,同上)升级率的第一次现场读数 —— 深路径在生产上开始运行,以及一处定义错误的自纠:④ 的 4.0 s 截止上线后,⑦ 那行计数器头 15 分钟产出三行:20 槽 decodes=20 fast=7 escalations=13 (65.0%) late=0 errors=0 / 40 槽 … (50.0%) / 60 槽 … (35.0%)。三件事由此定下:① 升级率不再恒为 0 —— 修复前是 263 次拒绝 / 0 次成功,现在 60 槽里升级 21 次,「深路径从未运行过」第一次被推翻;② late=0 且 errors=0 三行皆然 ⇒ 没有任何一批解码越过 boundary + 4.0 s,④ 的截止算术在生产上成立(这比升级率本身更硬:它是对那次修复最直接的验收);③ 每行 fast + escalations == decodes ⇒ 没有一次快路径整体失败回落深路径(那种批次两者都不计,取等即有意的 <= 从未被触发)。原先还从「decodes 恰好等于槽数」推出「每槽都产出了一批解码、没有跳槽」—— 该推理已作废(评审指出是循环论证:当时的 N slot(s) 就是 decodes 本身),详见 ⑫。成本随之有了观测点:0.1 + 升级率 × 5.284 ⇒ 0.6%(5%)/ 3.3%(35%)/ 5.9%(65%),连「每槽都升级」的天花板也只有 9%,即观测区间离余量上限还差一半以上。一处定义错误的自纠:025321a 曾把这一行的分母记成「解出来的报文条数」并写进 docs/ft8-w103d-phase-a-baseline.md;读 orchestrator.py:328 后确认 decodes 是每槽一个批次的计数(_slot_ended 里 += 1),不是报文条数 ⇒ 那个百分比是「多少比例的时隙需要补跑深路径」,报文条数不在这一行里(当时 escalations/slots 与 escalations/decodes 同值,但那是因为两者是同一个数字,不是因为证明过每槽都有批次 —— 见 ⑫)。因此「深路径多解了几条报文」这个问题计数器答不了,基线 §5 第 4 条仍然留作未测。升级率本身仍不收敛(三个窗口 65% → 35% → 5%,跨 13 倍,且首个窗口落在重启后 3.4–8.4 min),计划 Task 8 的 3 天窗口因此照旧欠着 —— 在它到手前不得用这三个数中的任何一个给 Phase B 定优先级。纯记录:本次只改文档(docs/ft8-w103d-phase-a-baseline.md §3/§4/§5、SDD/09 §9.9 与本节),无新增测试、无代码改动。 ⑫ (2026-10-14 追加,同上)评审抓出的两处缺陷 —— 一处命名、一处暂存遗漏 —— 以及巡边时发现它已经出过货。 ① 计数器行把批次当成了槽:_slots_logged 只在 _on_batch 里自增,而 _on_batch 只对解出结果的槽调用,所以它恒等于 counters.decodes;把 decodes 印成 N slot(s),⑪ 里那条「decodes 恰好等于槽数 ⇒ 没有跳槽」就成了拿计数器证明它自己(循环论证,已作废)。修法:行首改用 counters.slots_started、行尾新增 skipped= —— 跳槽(数据没在 grace 窗口内到齐,orchestrator.py:261)第一次在日志里可见;触发节奏仍按批次计数(那只是日志节奏,不是操作员读的数)。测试 tests/ft8/test_runtime_slot_counters.py 新增一项,把两个数故意做成不等(引擎 23 槽 / 20 批次)钉住语义;反向验证:回退实现后该行印 after 20 slot(s)、测试变红,恢复即绿。基线 §3 第 3 条加了更正框。② 暂存表还漏了五个运行期条目:auth-tokens.json(server.py:628,30 天会话凭据)、mrrc_modern.env(_config_file_path(),含 MRRC_WEB_PASSWORD 与 MRRC_CLOUD_TOKEN)、recordings.json(server.py:634,录制索引)、ft8/settings.json(server.py:3104,台站设置)、/env/(设备自己的口令文件)—— ⑨ 的 qso.db 只堵了其中一个。触发路径与 ⑨ 相同(MRRC_UPDATE_SRC 指向本地目录的升级会把开发树的运行期状态盖进安装目录),但对 auth-tokens.json 而言后果不是「把操作员踢下线」而是把开发者的会话装进一台能按发射键的机器。修法:排除表 +5 条,并新增 test_runtime_state_is_never_staged —— 它真的跑一遍 stager(临时源 → rsync → 检查目标),因为正则只证明规则写下了、不证明 rsync 认账;对照组 server.py 防「什么都没拷」也通过。反向验证:回退排除表 → 该测试 5 条断言全红。端到端:对带状态的真实开发树(含 auth-tokens.json、mrrc_modern.env、ft8/qso.db)跑一遍 stager,产物里一个状态文件都没有。③ 它已经出过货(现场调查,逐字节核实 —— 线上产物与本地产物尺寸一致,直接扫本地副本):已发布的 v1.27.0 w103d 与 rpi 镜像(10-13 构建)里 mrrc_modern.env 的口令与云令牌各命中 1 次,auth-tokens.json 里那个 operator 角色的会话令牌(2026-10-13 15:46 签发、30 天有效)命中 1 次 —— 任何下载镜像的人都能读到它,并用它登录任何用该镜像新安装的盒子(该令牌库在盒子上会于启动时载入);v1.26.0 的 w103d 与 rpi 镜像、v1.25.5 的 w103d 镜像(均仍在线上)只含 env 文件、不含令牌库(令牌持久化 10-13 10:56 才落地,早于它的构建没有这个文件可漏)——清点过的产物里,macOS DMG 与 Windows 安装包干净(打包清单本就没有这些文件)。Task 7 那次临时 rsync 也没排除 mrrc_modern.env,现场机的 /opt/mrrc_modern/ 下因此也有一份(该路径由 systemd 的 MRRC_CONFIG_FILE 指向 env/mrrc.env,所以在盒子上是惰性的,但密钥确实落到了现场)。源码包(win_pack.md 的 zip 命令)同样漏了这几项。动作(本行随版本发布前必须完成):轮换 MRRC_CLOUD_TOKEN 与 MRRC_WEB_PASSWORD、作废那个会话令牌、重建并重新发布 v1.27.0 的两个镜像、下架旧版本镜像、清掉现场机上那份副本。 |
| SDD V2.84 | 2026-10-13 | pi | v1.27.0 发布:FT8 走进四个平台 —— 原生 DSP 库成为每个产物的组成部分,而不是盒子上手工编出来的那一份;两个「只在冻结包里出现」的真缺陷由洁净室真跑抓出。 ① 缺口:DSP 库此前没有任何产物携带(只有 W103D 上 M5 验收时手编的那份),刷机或升级后 FT8 一点就是「库缺失」。② 两条镜像线在构建期编译(packages.txt 增 gfortran/g++/cmake/pkg-config/libfftw3-dev,mrrc-service-layer.sh 编完即断言 .so 存在——编不出就是构建失败而不是刷出一个点了没反应的镜像;mrrc-update 每次升级按源码摘要重编,且不阻断升级)。③ Boost 头文件 vendor 化:<boost/crc.hpp> 是唯一用途,Debian 只把它装在 138 MB 的 libboost1.74-dev 里;83 个头的包含闭包(720 KB)进 ft8/dsp/vendor-boost/,四平台都不再需要 Boost。④ Windows:MSYS2 MinGW 编 wsjt_core.dll,objdump -p 走出运行期 DLL 闭包(9 个文件 16.7 MB);顺带修两处可移植性缺陷(mingw-w64 根本没有 stpcpy;MinGW 默认不导出任何符号)。⑤ macOS:packaging/macos/ft8_bundle.py 把 MacPorts 的 5 个依赖收进 app 并把 load command 全部改写 @loader_path,打包前硬门禁拒绝任何残留 /opt/local 引用。⑥ 两个真缺陷(都只在冻结包里,都只在洁净室真跑里现形):全新安装没有 <runtime>/ft8/ 目录(sqlite 不会建父目录 → Repository 抛 OperationalError → 整个 FT8 组合失败、静默退回 SSB-only);冻结包缺 multiprocessing.freeze_support()(DSP worker 的 spawn 子进程重新执行 main、argparse 打 usage 退出 2 → spawn_failed)。⑦ Windows-only 测试缺陷:test_store_is_owner_readable_only 断言 POSIX 0600(Windows 只能 0666)——VM 门禁抓出。⑧ 产物与验证:macOS MRRC-Modern-v1.27.0-arm64.dmg 84,882,735 bytes 9d5ec531…;Windows MRRC-Modern-v1.27.0-Windows-x64-Setup.exe 79,257,177 bytes ba956b19…;W103D MRRC-Modern-1.27.0-w103d.img.gz 1,107,381,643 bytes 2e7452f8…;树莓派 MRRC-Modern-v1.27.0-rpi64.img.xz 489,496,576 bytes bb7127c7…。unittest 2200 项(macOS)+ FT8 pytest 744 项;Windows 构建门禁 2197 项 OK(33 platform skips);release_check.py 离线 32 ok / 0 failing;两条镜像线各跑四步产物验证(代码身份 / 装机内容 / D-11 陷阱 / arm64 chroot 真跑——含 ctypes.CDLL 载入 libwsjt_core.so 并核对 abi=2、decode_paths=0x7,以及 Pi 侧 scipy 1.17.1 / soxr 1.1.0 可导入);macOS 与 Windows 冻结包各跑洁净室真跑(隔离 HOME/LOCALAPPDATA + 现场配置 + 空闲端口:https /login 200、明文 000、/api/health 401,且日志出现 FT8 engine started at boot;运行后包体零改动、codesign --verify 仍有效)。 |
| SDD V2.83 | 2026-10-13 | pi | FT8 真机完善(feat/ft8 续):八个缺陷修复 + 引擎常驻后的界面收敛。 ① 解码时隙被跳过(三分之二):RX tap 的时间轴比墙钟恒定滞后 +0.78~0.83 s(宿主缓冲 100 ms + 平台 80 ms 读块 + soxr ~80 ms + 调度),而 orchestrator 的 delivery_grace 是按 ~85 ms 捕获链写的 0.4 s ⇒ 时隙在数据到齐前被放弃——引擎看着很忙、瀑布照常滚、解码列表几乎不动。排除过程全部现场取证:音频吞吐 98%(12.9 块/秒 × 80 ms)、ring 零 gap 零丢弃、漂移探针显示恒定偏移而非累积(排除时钟问题)。修法:tap 改为整数样本索引驱动时间轴(顺带消除 round(epoch*rate) 每块 ±1 样本造成的假间隙)+ grace 1.5 s。 ② 提前解码:FT8 波形只占时隙前 12.64 s,尾部 ~2.4 s 是空白,等它是纯延迟。Orchestrator 新增 early_decode_s(等待起点提前 1.2 s;lateness 仍按名义时隙结束计,5 s 决策窗口不被侵蚀;提前派发后消费剩余时间,避免同一时隙被解码两次——后者是 TDD 红灯抓出的真 bug);UtcRing.read_slot_partial(slot_id, min_samples) 在尾巴未到时零补齐,gap 重叠仍拒绝(不能拿零粉饰间隙)。端到端 17.0 → 16.1 s;保留 1.16 s 给大 dt 信号。 ③ 「发射一次 CQ 后就不解码了」(操作员现场报告):TX 后平台重建接收流(日志 RX audio: reopening capture stream after TX),链路偏移从稳定的 +0.8 s 变成 +1.81 s 且不回落;而 ② 的提前解码把 grace 留给捕获链的预算压成 grace − 1.2(1.5 s 的 grace 只剩 0.3 s)⇒ 每个时隙都在数据到齐前放弃。修法:grace 3.0 s(仍只是上限——轮询在数据落地瞬间派发,链路快时一格不多等),并在代码里写明它与 EARLY_DECODE_S 的耦合关系。 ④ 会话令牌持久化(操作员现场报告「为啥反复要我登录,后边程序没有重启啊」):登录 cookie 有 30 天,但服务端只把 token 放在两个内存 set(_auth_tokens/_listen_tokens),每次进程重启(含升级的例行重启)都清空——盒子没重启,Web 服务重启了,浏览器拿着完全有效的 cookie 被判 401 ⇒ 跳登录页。新增 data/auth-tokens.json(原子写 + 0600 + 与 cookie 同寿 30 天),启动恢复、登出落盘、过期剔除、损坏/缺失容错;_add_auth_token/_drop_auth_token 收拢登录/登出/bootstrap 三处调用点;6 个新测试。实测:登录 → systemctl restart → 同一 cookie 仍 200。 ⑤ QSO 的 TIME_ON 对齐时隙边界(查操作员「解码显示的时间点是开始还是结束」时发现的真 bug):原实现用按下按钮那一刻的任意墙钟(最多偏进时隙 14 s),与 WSJT-X/JTDX 对不上。vendor 权威定义 wsjtx-3.0.2/widgets/mainwindow.cpp:3055 m_dateTimeQSOOn = now.addSecs(-(m_ntx-1)*TRperiod - fmod(now.second(), TRperiod)),已按此对齐(SLOT_SECONDS = 15.0),并更新两处编码旧行为的断言 + 新增 2 个回归(含「时隙内任意偏移都映射到同一 TIME_ON」)。 ⑥ 显示语义:解码行首时间按上游是时隙起点(slotUtc = slot_id × 15,已用日志实证:slot 落盘于 UTC 02:32:01,该时隙 [02:31:45,02:32:00),页面显示 02:31:45)。本轮先按要求加了「完成时刻 0.1 s + 滞后」,后按操作员反馈(行宽不足)把时间移回分隔条:行保持上游紧凑形态(SNR DT FREQ TEXT entity),分隔条显示 ── 21.074 MHz ─ 03:04:00 UTC (+2.0s) ──;后端 decodes 帧新增 timing 块(slot_start/slot_end/dispatched/finished epoch + 两个 after_slot_end 秒数)并逐条挂到 message 上。真机实测滞后 1.7~2.0 s。 ⑦ 引擎常驻后的模式语义收敛:引擎常驻默认开(开机即 RX 监控,TX 仍需手动 arm)。SSB 界面的整套控制锁定(body.ft8-mode-active 的频率/模式/记忆键/波段/调谐/VFO/TX 与空格键闸门)全部删除——引擎常驻后没有「模式切换」可仲裁;安全边界改为按发射瞬间判定:新增 _ft8_transmitting()(= FT8 的 safety.ptt_on),只在 FT8 波形真在空中那 ~13 s 内拒绝手动 PTT 与丢弃浏览器 mic 上行,其余时间完全放行(_ft8_owns_tx 模式级开关一并退场)。FT8 页面新增频率控制(波段下拉 + ±1 kHz + 点击输入任意频率,全部走既有 /api/ft8/radio/band,该端点仍同时重设 DATA-U + 3 kHz)。 ⑧ 测试与文档:tests/ft8 542→551(+9:提前解码 3、ring 部分读取 2、TIME_ON 2、帧 timing 1、传输期门控 1);平台主套件 2176→2182(+6 auth 持久化)。同步 NFR-022、§7.2 的 auth tokens 行、AGENTS.md 的「Auth tokens cleared on server restart」(该句已不成立:令牌现在跨重启保留)、README 的 session-auth 行、tests/README 计数、SDD/README Quick Facts V2.82→V2.83。未做:CQ 循环 UI、QSO 面板细节、桌面客户端 /desktop、FT4、auto-call/band-hunt(UI 置灰标「未启用」);DSP 编译仍未烘焙进镜像。 |
| SDD V2.82 | 2026-10-12 | pi | FT8 工作模式(分支 feat/ft8):把 MRRC-FT8 的引擎植进 mrrc_modern,做成菜单可切的全局模式——SSB ↔︎ FT8 一键切换,声卡、电台控制与 TX 门控全部复用平台既有实现,不引入第二个服务、不新增声卡打开、不引入 hamlib/rigctld。 规格 docs/superpowers/specs/2026-10-12-ft8-work-mode-design.md、计划 docs/superpowers/plans/2026-10-12-ft8-work-mode.md、真机记录 docs/ft8-m5-acceptance.md、M1 基线 docs/ft8-m1-baseline.md。① 移植边界:ft8/{core,engine,dsp} 从 HAM/ft8 verbatim 移植(DSP 源经 tools/ft8/extract_wsjtx_subset.sh 抽取,编译子集 106 文件 + 7 个补丁副本 + vendor-refs/ 原件基线,vendor.sha256 与政策回归随行);平台耦合全部收敛在 ft8/platform/(rx_tap / tx_sink / cat / settings)、ft8/web/、ft8/mode.py 与 server.py 接线处。② 音频:RX 挂平台既有采集流(_audio_rx_loop 的录音 tap 旁),soxr 流式 44.1k→12k(epoch 由输出样本计数驱动,首块缓冲偏差由测试钉出并修正);TX 走 CQPlayer 同一条注入路径(48k float32 → 20ms int16 帧 → feed_tx_audio)。③ TX 互斥是结构性的:_cq_key/_cq_unkey 泛化为 TxSession,CQ 与 FT8 共用;PlatformRig.set_ptt 只走它。④ 模式状态机:全局 work_mode(重启回 SSB),SSB 台况 6 字段快照/恢复,失败回滚,安全顺序(先停引擎再恢复台况)由测试钉住。⑤ 真机验收(W103D + FT-710):20m FT8 六时隙 4 解码;SSB↔︎FT8 往返无 PTT 残留;单时隙解码 2.14s(判据 ≤8s,余量 3.7×);arm64 设备原生编译 2m02s。联调修掉五个真缺陷:SPA 兜底路由吞掉 lifespan 追加的路由(改模块级挂载 + _LateBound 代理)、平台角色串是 operator/listener、_audio_rx_loop 的无客户端省电门让 FT8 永远拿不到音频、tap 缺现场可判读日志、orchestrator 计数未暴露进 state。 ⑥ 部署事故与修复(重要):盒子上的 mrrc-update 是 1.25.5 旧版(rsync 带 --delete),首次升级删掉了 env/、profiles/、recordings/*.mp3 与 Cloud Hub fleet/——由部署前备份全量恢复;另一次手动 rsync 绕过 stage-tree.sh 把 macOS 的 frpc 覆盖了 aarch64 版。现已把更新器换成现行版本(无 --delete)并把部署流程固定为「本地 stage-tree → rsync → mrrc-update」。行动项:≤1.25.5 镜像的现场机升级前必须先替换该脚本或先备份。⑦ 首版非目标:FT4、桌面客户端、band hunter、JTDX 同步、auto-call-new-DXCC、RUMLogNG;DSP 编译尚未烘焙进镜像。⑧ 测试:tests/ft8 全量 653 项(pytest,含 DSP fresh-build 回归);平台主套件 2176 项 unittest 全绿(TX 路径改造零破坏)。 |
| SDD V2.81 | 2026-10-11 | pi | W103D 固化进 eMMC 并全面取证:跑着的 U 盘系统被就地克隆进 28.9 GB eMMC,原厂引导逐字节保留;同轮把流程沉淀成 skill,并记下 verify.sh 两处判读。 ① 备份:armbian-ddbr 全盘 29 GB → 2.2 GB、pigz -t 校验通过;单线程 gzip 实测 10 MiB/s(≈40 分钟),装 pigz 三线程 ≈9 分钟(dd bs=4M \| pigz -p 3,标准 gzip 流,恢复路径不变)。② armbian-install 两个交互答案 307 / 1(不加 -m yes —— 本板要 u-boot-w103d.bin):ampart 重建 Amlogic 分区表(前 117 MiB 保留区),p1 512 MB FAT + p2 28.3 GB ext4,fstab 与 uEnv.txt 同一新 UUID。③ 核验:[0,444) 与 [512,4 MiB) 与原厂引导备份 cmp 逐字节一致,仅 68 B 分区表换成新表(安装器 dd 故意跳过该段)——「原厂引导被保留」是可证明的,不是承诺。④ 首启:根在 /dev/mmcblk2p2、mrrc-modern/w103d-ddns.timer active、/dev/ttyUSB0 与 FTDI 库在、*:8888 双栈、Cloud Hub 入口 302;machine-id 重生成致 IPv6 IID 漂移(…c4c1:… → …e7c0:…),DDNS 开机 90 秒内自动跟随(权威 NS 双查确认),IPv4 不变。⑤ verify.sh 10 过 / 4 红,均与安装无关:第 9 项只认 200/401,而公网入口未登录时返回 302(跳 /login)是正常应答,判定待放宽;第 11 项三项(NM polkit 规则 / avahi-daemon / box-helper 套接字)缺 —— 该镜像构建于 10-11 中午,早于 LAN 引导件落地,属镜像版本差。⑥ 沉淀:写 eMMC 流程成为独立 skill(w103d-emmc:备份提速、两个答案、字节级核验、IPv6 漂移与 verify.sh 判读、三档回滚);docs/W103D_GUIDE.md §17–§18、packaging/box/README.md §10、docs/w103d_pack.md 同步重述,中英站点页重渲。⑦ 定位调整(同日):§17 从「可选」改为「推荐的最终形态」——盒子 + 电源 + 网线、不再需要 U 盘;指南开头、§21 清单、box 操作单同步。未做:镜像重建(把第 11 项三件烤进去)、verify.sh 的 302 白名单。 |
| SDD V2.80 | 2026-10-11 | Kimi | W103D 无线根因日:盒子 WiFi 的一切怪病都来自 linux-firmware 的 2020 版 N9 固件,换成出厂 2021 版后全部消失;顺手修掉「开放热点其实是加密热点」的产品 Bug;AP 模式的真机答案是否定的。 ① 固件根因(一整天的症状清单都是它):镜像里 ophub 基底带的 mt7663_n9_v3.bin 是 linux-firmware 的 2020 版(3.1.1 / 20200604),在 W103D 上表现为渐进式死亡——能连但每 ~30 秒掉线一次(reason 9)、重连扫描要 13 秒(半小时 78 次掉线)、~16 分钟后 MCU mailbox 超时(W103D mailbox write failed: -110)与 SDIO RX 报 invalid RX packet length、扫描楔死(wpa_supplicant「scan trigger already pending」)、最后 probe 失败接口消失;软重启继承挂死态,只有断电能让芯片复位。对照实验排除项:SDIO 降速 200→50 MHz 与剥掉 UHS 能力更糟(50 MHz 高速度模式下连初始化都过不去),省电/蓝牙共存/BSSID 锁定/监管域均非根因。线索来自 ophub issue #3571 的移植记录(pigeon2049 fork,其修复已被上游 6.18.52+ 吸收——我们内核自带 W103D 补丁,缺的只是固件)。修法:出厂 Android 的 WIFI_RAM_CODE_MT7663.bin(7663mp1827 / 20210308)钉版进仓 vendor/mediatek/mt7663_n9_v3.bin(SHA-256 223f73f1…,字节未改),box-overlay.sh 安装到 /lib/firmware/mediatek/ + sha256sum -c 构建期校验 + dpkg-divert 防 linux-firmware 升级覆盖;溯源与许可声明见 packaging/box/vendor-mt7663.md。真机验收:2.4G 下 200 包 0.5% 丢包、平均 6.3 ms、零掉线、零驱动报错(修复前 59% 丢包 / 平均 277 ms / 半小时 78 次掉线);5 GHz 能认证关联但数据面 98% 丢包(该单元 5G 射频边际,不建议用)。② 产品 Bug:「开放热点」是加密的。 nmcli device wifi hotspot 不传 password 时,NM 1.42 自动生成随机 WPA-PSK(真机抓包:key-mgmt: wpa-psk,「nmcli dev wifi show-password」可读出)——设计 D-2 的开放热点从未开放过,这正是「手机搜不到 MRRC-Setup」的第一个原因。修法:net_wifi.start_hotspot() 改为显式建无 802-11-wireless-security 段的 AP profile(connection add … mode ap -- ipv4.method shared,后者原来由一键命令隐式完成,少了它就没有 10.42.0.1/24 与 dnsmasq)+ connection up;测试重写并加回归断言(argv 里不得出现 wifi-sec/key-mgmt/psk)。③ AP 模式真机答案:不发信标,且源码级根因已定位。 修好固件后 NM/wpa_supplicant 全程认为 AP 已激活(mode=AP、COMPLETED、10.42.0.1/24 就位、零报错),但手机与 Mac 均扫不到。源码走查(ophub/linux-6.18.y,mt7663s 克隆 mt7615_ops):mt7615_ops 压根没有 start_ap op(主线亦然——mt7615 家族的 AP 靠 bss_info_changed(BSS_CHANGED_BEACON*) → mt7615_mcu_add_beacon → MCU_UNI_CMD(BSS_INFO_UPDATE)+BCN_CONTENT TLV 驱动),而 mac80211 的 drv_start_ap 在 op 缺失时返回 0(静默成功)——这就是「NM 成功、空中无物」的全部由来;同时 AP 拉起期间 RFCR 的 mailbox 写连续 -110(一次 up 精确 +6 条),且 AP 折腾后驱动楔死只能断电复位。是否可修取决于固件(7663mp1827 是客户端构建,SoftAP 能力未证实)——判定实验是看原厂安卓的热点是否可用,不可用则固件判死刑、问题关闭;可用则属驱动序列问题,向上游(pigeon2049 fork / ophub #3571)提 issue 附带本行证据。热点引导在此之前不可用;盒子上手走有线/HDMI,设计本就不依赖 AP 成立。④ 测试 2097→2099(VendoredMt7663Tests +2:钉版 SHA 与 overlay 安装/校验/转移三件套);test_net_wifi/test_setup_ap/test_server_setup_wizard 的热点桩全部改吃新命令序列。全套 macOS 2099 OK / 1 skip。⑤ 同轮盒子侧实况:首启引导全链路真机验证通过(DHCP 拿地址、服务拉起、/setup 可答),WiFi 修复后盒子稳定挂 CMCC-cnham(ch11)。未做:热点引导页的真机验收随 AP 边界一起冻结;5G 数据面、长期挂机型稳性观察留待现场。 |
| SDD V2.79 | 2026-10-11 | pi | W103D 真机首启:镜像能开机,但四件事是死的 —— RX 音频被切碎、网页起不来、真 FFT 起不来、HDMI 横幅不出现。本次把六处只有真机才暴露的缺陷全部修掉并加守卫(两处是产品代码,四处是镜像构建)。 ① RX 音频(本次最重要的一条,用户报「声音卡顿得厉害」):server.py 的采集循环跑满 49 次/秒(不是 CPU)、但 5 秒只读到 10 个 20 ms 数据块 ⇒ 实际只拿到 4% 的音频;get_read_available() 返回 0、/proc/asound/card1/stream0 的 Capture 变成 Status: Stop、PyAudio 仍报告流是开的、无异常、内核一条日志都没有 —— 溢出之后采集接口静默停摆,客户端听到的是被切碎的碎片,随后彻底安静。用服务自己的 AudioHandler 在服务外复现并做成三组对照:慢读 47% / 激进读(有任何数据就读)100% / 缓冲 400 ms 99.7%,小缓冲 + 20 ms 读法约 60 秒后永久死亡。根因是这台盒子的宿主缓冲只有 1–2 个 20 ms 周期(macOS/CoreAudio 与 WASAPI 给同一份代码大得多的余量,所以台式机上永远看不见)。修法两条:config.RX_HOST_BUFFER_MS(env MRRC_RX_BUFFER_MS,Linux 默认 100 ms、桌面沿用 20 ms;frames_per_buffer 取它、但读门槛仍是一个 20 ms chunk,采样率域 44.1k↔︎48k 与 882↔︎960 桥一行未动)+ server._audio_rx_loop 的静默看门狗(有人在听却连续 RX_STALL_ALERT_S=3 s 读不到 PCM → WARNING + 重开采集流,重开按 RX_STALL_RESTART_MIN_GAP_S=20 s 限流)。真机复测:50.0 fps / 51.7 kbps / 帧间隔 20 ms、稳定 3 分钟以上。 ② 首启证书把服务挡在门外:firstboot_wrapper.py 只 chown 了 env 目录,/var/lib/mrrc/certs 是构建期就属于 mrrc 的目录、而首启以 root 新生成的 server.key 保持 0600 root:root ⇒ uvicorn load_cert_chain 抛 PermissionError,服务每 3 秒重启一次,server-stdout.log 里只有 traceback、网页永远打不开。修法:生成后 chown -R mrrc:mrrc 证书目录 + chmod 600/644(两份 wrapper 同步),并用 sudo -u mrrc head -c 20 server.key 作为现场判定手段。 ③ FT4222 缺 udev 规则:libft4222 直接打开 USB 设备(它没有自己的 /dev 节点),节点是 root:root 0664 ⇒ 服务用户 mrrc 打不开、scope_pipe 每次都 open_failed:error_2 ⇒ spi_init_failed ⇒ 真 FFT 永远起不来,而 CAT 与音频一切正常(最容易误判的半个故障)。A/B 一锤定音:root 跑 spi_ready:attempt_1,mrrc 跑 fatal:cannot_open_device。修法:overlay 写 /etc/udev/rules.d/99-mrrc-ftdi.rules(0403:601c、MODE=0660、GROUP=dialout)。 ④ HDMI 首启横幅从未出现过:单元里写的 StandardOutput=console 不是合法 systemd 语法(journal 明写 Failed to parse output specifier, ignoring: console),设计 D-6 指望的那个「带口令的横幅」对只有 HDMI 的操作员等于不存在;改成 journal+console(box overlay + rpi stage 两处),并加扫描器守卫禁止裸 console。 ⑤ 构建机的硬件猜测被烘进镜像:overlay 把 install.sh 在构建主机上写的 .env 原样搬进镜像 ⇒ 盒子上带着 /dev/cu.SLAB_USBtoUART(那是我 Mac 上的串口)与一份构建期生成的口令,违反设计 D-3「不许预埋看起来可用的猜测」。修法:搬移后 sed 掉 MRRC_SERIAL_PORT 与 MRRC_WEB_PASSWORD,让首启探测与生成接管。 ⑥ dnsmasq.service 在每台新盒子上是一颗红灯:Debian 包装好就 enable,与 systemd-resolved 抢 53 端口;热点并不需要它(NetworkManager 的 shared 模式自带实例)⇒ overlay 改为 systemctl mask dnsmasq.service。 ⑦ 顺带:verify.sh 自己在健康盒子上报了两个假失败(真机首跑 8 passed / 2 failed):第 7 项读 systemctl show -p EnvironmentFile(单数)在 systemd 252 上恒为空串,属性名是 EnvironmentFiles(复数、值带 (ignore_errors=no) 后缀);第 11 项 systemctl list-unit-files | grep -q 在 set -o pipefail下 exit 141(grep 命中即退 ⇒ systemctl 吃 SIGPIPE ⇒ 「找到了」被判成「没找到」,脚本宣布一个正在运行的单元「未安装」)。各自修好并加扫描器守卫(| grep -q一律改为先取输出再匹配;1/11、3/11 同型管道一并改),真机复测 10 passed / 0 failed。 ⑧ 真机确认已经好的部分:镜像与线上产物逐字节相同(server.py/mrrc_radio.py哈希一致)、mrrc-radio list 11 机型可用、首启探测选中 /dev/ttyUSB0(CP2105)、USB Audio (hw:1,0)44.1 kHz 打开、真实频道状态 7.053 MHz/LSB/2.4 kHz/S9、/manage 与 /setup(LAN 免口令 → 302)围栏正确、mrrc-setup-ap有上行时不开热点。 ⑨ 边界:PTT/发射、录音、Cloud Hub 公网接入未测(发射需要现场操作员);测试 2084 → 2096(+12,含 config 2 / audio 5 / rpi 1 / box 4);tests/README.md 计数同步。 |
| SDD V2.78 | 2026-10-10 | pi | 两份 IC-7300 现场上报的根因在电台侧调制输入源:产品侧补上缺失的那一节文档与一条守卫,顺带修掉一个会让启动器直接退出、且在 PYZ 里无法热修的探测缺陷。 ① 现场(同一根因,两种表现):20261010-203328-9f66(v1.25.5,「IC7300 发射对方没有收到声音」,FM 29.6 MHz)与 20261010-205200-6e3c(v1.25.4,「发射功率小/没有」,SSB 7.05 MHz)。软件链路是清的:服务端把 52% 峰值的语音真写进了电台 USB 声卡(TX audio started: [5] 扬声器 (USB Audio CODEC ) (host=MME, default=44100Hz, actual=48000Hz, channels=1) 与 TX session: frames=351 fed=351 decode_fail=0 written=228 write_err=0 queue_drops=110 peak=52%,peak 是解码后 PCM 实测峰值),电台也真的在发射(TX meters active: RF_PWR=60W \| COMP=0 ALC=107 PWR=155 SWR=1)。调制却是零:FM 下调制为零就是死载波,对方听到的正是「有发射、没声音」;SSB 下没有调制就没有射频功率 —— TX meters active: RF_PWR=56W \| COMP=0 ALC=0 PWR=2 SWR=0 里 RF_PWR=56W 是功率设置值、ALC=0/PWR=2 才是实测,即用户说的「功率没有」。Icom 的调制输入源由 SET → Connectors → MOD Input 决定(语义取自仓库内 IC-7300MK2_ENG_CI-V_0.pdf 的 1A 05 项表:00 84 = DATA OFF MOD 管非 DATA 模式(SSB/AM/FM 语音)、00 85 = DATA MOD、00 81 = USB MOD Level),三项不含 USB 时语音发射永远只有载波 —— 不是产品缺陷。② 本次真正要修的是文档缺口:FT-710 的等价要求(MOD SOURCE = USB)在 website/*/guide.html、docs/WINDOWS_INSTALLER_GUIDE.md 的电台侧表与故障表里都写着;Icom 这一条在全仓 grep -rn "DATA OFF MOD" 零命中 —— 撞上它的用户无处可查。现已补入 docs/OPERATION_GUIDE.md §0.6.2(DATA OFF MOD/DATA MOD/USB MOD Level + USB SEND 保持 OFF(MK2 同组含 USB Keying (CW/RTTY):它把 USB 口 DTR/RTS 当 PTT)+ 接收侧输出选择保持 AF)与验证清单第 5 条(ALC 表不动=调制没进来)、docs/WINDOWS_INSTALLER_GUIDE.md(新增 §4b Icom 菜单小节 + 故障表新增一行,并把「no RF modulation」那行按家族区分)、docs/MACOS_INSTALLER_GUIDE.md 故障表、IC-7300_硬件验收清单.md(前置勾选项 + 4.5 期望值 + hw-verify 记录:先分清「电台侧 DATA OFF MOD 未选 USB」与「软件侧 queue_drops/缺 TX audio started」再判定)。③ 新守卫 tests/test_website_links.py::IcomTxModulationDocsTests:把 MOD Input/DATA OFF MOD/DATA MOD/USB MOD Level 四个短语钉在用户会打开的每一页(guide 源、两个生成页、Windows 安装指南、IC-7300 验收清单),并单独钉 macOS 指南的「发射没有声音」行必须点名 MOD Input。变异验证:删掉 docs/OPERATION_GUIDE.md 里的 DATA OFF MOD 即红。website/guide.html + website/zh/guide.html 由唯一写者 build_guide.py 重建(已有测试钉住生成页不得落后于源文档)。④ 顺带抓到的真实缺陷(须发版):launcher_net.answers() 在探测前调 tls_context()(内含 import ssl),这一句在 try 之外;Windows 冻结包现场 launcher.log 是 ImportError: DLL load failed while importing _ssl: 另一个程序正在使用此文件,进程无法访问。(上一个实例仍持有 _ssl.pyd),异常从 launcher.py:109 running_instance_url → launcher_net.py:94 first_answering 一路冒出,启动器以「MRRC Modern 启动失败」退出 —— 而它要拉起的服务本来能正常运行(该实例同期有 62 次成功启动)。修法:上下文创建包进 try/except,失败即当作「该 scheme 没应答」,把选择权交回调用方(这正是本模块存在的理由);3 个新测试(TlsContextFailureTests:https 探测返回 False / served_url 仍落到能应答的 http / first_answering 链不断),变异验证:去掉 try 即 3 个用例全红。launcher_net.py 在 PYZ 里,热修覆盖不到 ⇒ 只能随下一次发版到达用户。⑤ 答复与状态修复:两份上报的答复卡已发布到 <www.vlsc.net/mrrc_modern/answers/>(status=answered、category=电台,FDE 看板记 noise = 已答复闭环;答复内容为电台菜单四步 + USB SEND 提醒 + ALC 自检 + queue_drops 的次要处理)。发布时发现 dist/support_autopilot/state.json 只剩 2 条而答复页上有 5 条:直接 rebuild_page 会静默丢掉发布者产出的 3 条答复(20260926-061400-e4dc/20261001-161306-86db/20261003-103644-11b2)—— 本次从页面把 3 张卡回收进 dist/support_answers/ 并回填 state.json(at 取自卡片日期以保序),重建结果除新增两张卡与「最后更新」戳外与线上页逐字节相同(已用脚本比对)。dist/ 不入库,故这属于本机状态文件的修复。遗留:这 3 条在看板状态里仍缺(发布者的流程只写答复页不写看板),后续回填。⑥ 验收与边界:python3 -m unittest tests.test_website_links tests.test_launcher_net 22 项全绿;dev_tools/site_audit.py 与内联 JS 守卫、release_check.py --offline 31 ok / 1 failing —— 失败项 website-w103d-card(盒子镜像字节/SHA 未上卡)先于本次改动,已用 git stash 对照确认与本次无关,属另一条在途工作流。未发版:MRRC.iss 版本号与任何产物未动,launcher 修复待随下一次发布;站点文案(guide 两页)已在仓库内生成并可独立上站(website/deploy.sh 是整站带 --delete 的发布日动作,本次不跑)。 |
| SDD V2.77 | 2026-10-10 | pi | 补记 AD-026(初始化热点与免口令引导窗口),修正三处过时事实与一处自相矛盾,并补上三条守卫。本次不改一行产品代码、不动任何产物——v1.25.5 的字节与哈希保持不变,这是一次文档记录修订。 ① 主项:AD-026 从未写进第 8 章。 该决策(mrrc-setup-ap.service 在没有可用上行时开一个开放热点;/setup 向导在热点存续期内对来自热点网段的请求免口令开放,成功后切 STA 并关热点)已在代码里(net_wifi.py、linux/setup_ap.py、server.py 的 /setup 与 GET | POST /api/setup/wizard*、_setup_access()、可写键恰好两个、WizardClaim无 password 字段)、已随 v1.25.5 发货(V2.76 行 ⑤)、两张落地页也已在宣传它(AD-001 ... AD-026 与 AD-026 标签),但 git log -S "AD-026" -- SDD/ 零命中——它从未进入任何 SDD 文件。线索在 7afe5f2的提交说明(“AD-026 was in the SDD”):docs/superpowers/plans/2026-10-08-w103d-setup-hotspot-wizard.md 任务 11 的第 2 步(往第 8 章追加 AD)与第 4/5 步(改落地页)只做了后者;而 test_landing_pages_list_every_decision只检查一个方向(第 8 章 → 落地页),落地页多写一条它不管,于是套件一直绿。本行按计划里作者已写好的正文补记该 AD,并把每条实质断言对实现后代码复核(_setup_access 的两条进门路径,以及”listen 令牌必须在这里被拒——中间件对 SETUP_GATE_PATHS的按相等放行必然跳过 listen 403,漏掉它就是权限提升”;net_wifi.scrub;state.json/wizard.json两个唯一写入者;30 分钟闩含”本次开机不再重开”),Status 记三项待真机边界,并保留”热点开放期内同网段可抢先设口令”这条残留风险。② 三条守卫补上单向检查的缺口:§8.16 汇总表必须列全(该表自己缺 AD-020/023/024/025)、落地页不得宣传第 8 章没有的 AD(正是 AD-026 这类)、第 14 章索引必须列全(原缺 AD-017…AD-025 共 9 条)。三条均做变异验证(破坏不变量即红)——本仓吃过两次”空转闸门”的亏,绿着的守卫不算证据。③ 三处过时事实:第 14 章 Design Baseline Notes 的机型列表停在 FT-710 + IC-7300/MK2(git blame 显示该段邻行改到 2026-08-17,只有这两行自 07-06 初版起未动,故属漏改而非有意冻结);Auth 行写作 ft710_auth(代码config.py 是 mrrc_auth,且缺 listen-only 角色与 AD-024 的 query 弃用);Scope 行未提 Yaesu ASCII-CAT 家族没有频谱协议、只走合成 S 表。④ 一处自相矛盾:§3.3 把 ATR-1000 列为”范围之外”,而同文档 §9.8 有功能图、§10 列 ATR1000Service 为 Implemented ⇒ 改为”调谐器内部(固件/UI/算法)在范围外,链路(发现、调谐辅助、LC 学习存储)在范围内”。⑤ 网站:python3 website/build_sdd.py重生成 16 页 + 10 图;改动只落在本次编辑的三章页面,其余逐字节不变(生成确定性)。⑥ 验收:release_check.py离线 32 ok / 0 failing;文档守卫 64 项 OK(较上版 +3)。⑦ 边界与未做:仅文档 + 一处守卫文件;AD-026 的三项真机边界(MT7663S 能否起 AP、切换后手机重连体验、10.42.0.0/24与既有网络冲突)仍待现场,W103D 计划任务 11 的勾选状态未回填;MRRC.iss 这个错误路径仍留在 V2.72 版本行里(已发布的行按规矩不可再改,此处只记录)。 |
| SDD V2.76 | 2026-10-09 | pi | v1.25.5 发布:快捷行回答「现在在哪」;AD-025 的频谱档位与 D-8 的配置写入层一起到达用户。 ① Web 快捷行(static/ft710_ui.js):五个按钮(MODE/BAND/FILTER/ATT/PRE)从「显示按下去会变成什么」改为「显示当前值」,点击仍轮转到下一档 —— 按钮回答「现在在哪」、点击回答「下一档」,与 Android MainScreen.kt:81-82 的 Text("ATT ${state.attenuatorLabel}") 同义。MODE/BAND/FILTER 显示 mode_name / band_name / getFilterLabel(radioState.filter_width, mode_name);ATT/PRE 显示完整档位 OFF / 6dB / 12dB / 18dB 与 OFF / AMP1 / AMP2(此前是三字符缩写 OF/6d/12/18、A1/A2),标签由 capabilities 的 att_steps / preamp_steps 派生(换机型不必改前端:IC-7300 得 OFF/20dB),caps 缺失时回退 FT-710 常量表。renderButtonLabels() 是全文件唯一写这五个按钮文本的函数(8 处调用:连接/全量状态、脏字段更新、五个点击处理器),点击处理器与乐观更新一行未改 —— 改动只有两个渲染块。缓存版本随之抬升(ft710_ui.js?v=34→35、Service Worker mrrc-v47→v48),契约测试 test_quick_row_shows_current_values_not_next 先红后绿(它同时钉住旧助手 _attShortLabel/_preShortLabel 已消失)。② AD-025(频谱带宽档位)到达用户(设计与实测见 V2.75 行):服务端按 socket 存档 spectrumCaps 并在 _spectrum_fanout 分频、Web Settings 出档位选择器;未声明 caps 的连接逐字节等于改动前的流,已装 Android/iOS 不受影响。③ D-8(配置写入层统一)到达用户:env_store.py(容错读 BOM/UTF-8/cp936/latin-1 + 保注释保序合并 + 跨平台文件锁下的原子替换 + 禁写键与安全门禁两道围栏)取代原先七条各写各的 env 写入路径,Cloud Hub 的连接随之走这条通道 —— 修的是「设置页显示已连接、服务端却读不到证书路径与心跳设置」这类静默失配,以及「一条路径丢掉全部注释并重排、一条把盒子的 0640 放宽成 0644」这类只在现场报告里看得见的分歧。④ 新增 /manage 只读页:热点是否在跑(此前答案只活在状态文件与 journal 里)与三个安全闸门(发射门禁、PTT 上限、心跳超时)当前值及其后果;不提供任何网页开关 —— 把不变量做成网页开关,就是把不变量做成事故。⑤ 盒子侧随包提供、桌面不受影响:/setup 向导(免口令只走热点网段:活心跳 + 网段双重判定;先扫描 Wi-Fi,AP→STA 切换先回响应再切、失败必回热点并带原因;设口令复用既有写入通道、只写两个键、不重启服务)、net_wifi 与 mrrc-setup-ap(无上行才开热点、开放热点是有意的、30 分钟自关且本次开机不重开、PSK 只进 argv)。⑥ 边界:W103D 盒子镜像与树莓派镜像本次不发(站上下载卡与链接停在 1.25.4);站点侧同批上线 W103D 指南页(独立成页 + 规格图 + 语言切换修复)、Android 下载卡追平线上 v1.11.30、首页 promo 海报更新。设计与计划见 docs/superpowers/specs/2026-10-09-main-row-current-values-design.md、docs/superpowers/plans/2026-10-09-main-row-current-values.md、docs/superpowers/plans/2026-10-08-config-layer-unification.md。 ⑦ 验收与产物:macOS 套件 2070 项 OK / 1 skip,release_check.py 离线 30 ok / 0 failing,冻结包符号走查(入口 spectrumCaps/_spectrum_fanout/manage.html/setup.html;PYZ env_store/net_wifi/cloud_hub),冻结 macOS server 洁净室(隔离 HOME + 现场配置:https /login 200、明文 000、/api/health 401、证书与 Recording ready 落用户目录、第二实例 exit 1 点名端口、包体零污染、签名仍有效);Windows 门禁 2067 项 OK / 25 skip(比 macOS 多的 6 个 skip 是本轮新加的平台守卫:5 处 POSIX 权限位断言 + 1 处需要 bash 的 box 解析用例 —— 它们此前从未在 Windows 上跑过,是这一轮门禁抓出来的;另有一处 env_store 夹具在 Windows 上写出 CRLF,被「无改动不该写」那条钉住:代码是对的,夹具不是),Windows 洁净室 19 条全过、真 Setup.exe 静默装 12 条全过(167 文件、安装目录 167→167、卸载零残留)。产物:Windows Setup 54,395,338 bytes,SHA-256 1ed3fb8d224a7084db424562ff9ba8f152a2962411c51cd6daa5352ae6d45ae2(VM Get-FileHash == Mac shasum,v1.25.4 为 54,347,067);macOS DMG 62,490,425 bytes,SHA-256 58ad7481ff7b9fc50e11f7a79d2477ecb98127051baac17f2dda2d4fd8043341。 |
| SDD V2.75 | 2026-10-07 | pi | 频谱带宽档位(AD-025):把「省带宽」从一次性的格式决定变成每 socket 可协商的档位,并补上 SDD §9.2.4 里写定却从未上线的 851 B 帧。 实测起点(打包版日志 Session metrics):单 operator uplink spectrum 150.8–151.0 kbps、audio_rx 0.0–51.7 kbps;真频谱只在硬件帧计数前进时发送 ⇒ 实测 11.1 fps,而 30 Hz 只是 tick 率。两个实测事实否掉了任何单一手段:① uvicorn 0.52.1 与浏览器协商 permessage-deflate(本机复现握手回 server_max_window_bits=12; client_max_window_bits=12),30 帧 × 1701 B 的 payload 上线后只剩 13224 B(≈441 B/帧,3.9×),其中 wf2 的 850 个零字节压缩后 ≈1.4 B/帧 ⇒ 对浏览器砍 wf2 几乎不省钱;② Android 带的是 OkHttp 4.12,其 dex 里有 Request header not permitted: 'Sec-WebSocket-Extensions' 守卫 ⇒ 不提供该扩展,1701 B 真的逐个走出电台。故档位 = 形状 × 分频两个正交因子:high(full 1701 B, ÷1) / mid(wf1 851 B, ÷2) / low(wf1 851 B, ÷4) + 服务端内部 listen(full, ÷3,即 V2.59 起的 listener 行为)。payload 轴 151→38→19 kbps(×¼、×⅛);浏览器线上轴 ≈39→20→10 kbps —— 两个轴差近 4 倍,引用数字必须标明口径。短帧由 full[:851] 切片得到,scope_handler 与 11 个 backend 一行未改,也不存在第二条会漂移的组帧路径。兼容性靠服务端闸门而非客户端升级:未发 {"type":"spectrumCaps","profile":…} 的 socket 永远拿 high(逐字节等于改动前的流),因为已装 Android 的 SpectrumFrame.kt:14-15 同时硬判 size != 1701 与 frame[0] != 0x01,是唯一吃不下短帧的客户端;Web 与 iOS 的解析器本来就能吃 851 B。全帧保持 0x01,不得改标 0x02——iOS SpectrumProcessor.swift:57 是 guard version == 0x01,改了会让已装 iOS 静默丢帧。顺带修三处:① S-meter 回退路径原先没有帧计数闸门、每 tick 重造 1701 B(30 Hz ⇒ ~408 kbps),坏消息比好消息贵,现在两条路径共用同一个 _spectrum_fanout,每档字节数一致;② 遥测 kbps 是 payload 口径(deflate 在它下游、Starlette 看不见),Session metrics 行改标 payload≈ 并新增 spectrum frames high=…/mid=…/low=… 分档帧数(字节只给总量,帧数才说得出是哪一档产生的);③ 计量移出发送的 try,否则计数器里的 bug 会被 except Exception 洗成「客户端走了」而把健康 socket 静默踢出 spectrum_clients(这条是本次自查出来的,已用测试钉住)。文档纠错:§9.2.4 的 v1=851 B 从未上线(全仓无代码发 0x02、git log -S 零命中),本次落实它并退役 v2 命名;NFR-003 的 “~1701 B @ ~30fps ≈ 51KB/s” 是把回退态当常态、“~851 bytes/frame fallback” 从未为真(回退走同一个 get_spectrum_binary(),也是 1701 B);§9.2.4 的 “~10 Hz” 只在回退态成立(真频谱 ÷3 是 3.7 Hz),已改为按条件陈述;AD-023 的 408 kbps / 86% 就地加注校正而不删原文(那是当时容量决策的依据,删掉等于掩盖决策是从什么数字做出的);opus_rx.py:64-68 与 docs/IOS_OPUS_INTEGRATION.md:64 声称的运行时 setOpusBitrate 全仓无实现,已标注;AGENTS.md:104 与 docs/PROJECT_MAP.md:39 引用的 tests/test_ws_protocol.py 并不存在(真名 test_server_ws_protocol.py)。spectrum-paths.svg 随之改口径(v1 851 B · v2 1701 B → full 1701 B · wf1 851 B)并同步 website/sdd/diagrams 的逐字节副本。测试 1673 → 1751(+78:test_spectrum_profile.py 17、test_spectrum_profile_server.py 40、test_spectrum_profile_docs.py 15、test_session_metrics.py 6);验收工具 dev_tools/spectrum_profile_probe.py(--no-deflate 切到 Android 轴、--no-caps 验兼容闸门)。边界:本阶段只动服务端 + Web;Android/iOS 不发 caps ⇒ 继续拿 high,行为不变,真正省流量待 P2(Android 走 APK-only 通道)/ P3(iOS);跨仓 mrrc_hub 的 NFR-H007(0.48 Mbps)与 AD-H14(频谱占 86%)建在 30 fps 假设上,输入数据需按 151 kbps / ~72% 修正。app 版本号本次不动(CHANGELOG 顶部仍是 v1.25.4):官网下载卡指向真实产物文件名(MRRC-Modern-v1.25.4-arm64.dmg),在没有构建产物之前改卡片等于给用户 404,app 版本随下一次发布一起动。设计与计划见 docs/superpowers/specs/2026-10-07-spectrum-bandwidth-profiles-design.md 与 docs/superpowers/plans/2026-10-07-spectrum-bandwidth-profiles-p1.md。 |
| SDD V2.74 | 2026-10-06 | pi | W103D 通用电台盒子镜像:把一台 Amlogic 云电脑变成 11 机型可切换的服务端。仅部署层——协议、CAT、音频、PTT 一行未改。 设计与计划见 docs/superpowers/specs/2026-10-06-w103d-radio-box-design.md、docs/superpowers/plans/2026-10-06-w103d-radio-box.md。① 基底用 ophub 预构建镜像并二次构建,不自建 Armbian:W103D 的板级补丁(SDIO 路由到 SD_EMMC_B、MT7663S 传输层、原厂 2021 版 N9 固件)活在 ophub/unifreq 的内核树里(PR #3658/#3659),自建等于长期跟进那套补丁栈;代价是依赖第三方镜像,缓解手段是钉 SHA-256(998d5244…)并只从官方 release 取。基底文件名里四个标记都不可替换:s905l3a-w103d(换通用 s905l3a 则静默丢 WiFi)、bookworm(Python 3.11)、6.18.54(补丁与固件所在系列)。② 两段式:构建期烘焙 + 首启探测。 macOS 的 Docker 特权容器里 loop 挂载并 chroot(Apple Silicon 原生 arm64,不需要 qemu);口令、串口、声卡、自签证书全部留到首启,因为盲猜会产出一个「服务在跑、就是连不上电台」的配置。③ 镜像内已有运行时是实测的,不是估算的:Python 3.11、libopus0、portaudio19-dev、libportaudio2、python3-venv/pip/dev、libasound2-dev、gcc、git、rsync、sudo、NetworkManager/nmcli、mt7663 驱动均在(在镜像 rootfs 的 dpkg 数据库里逐个查过)⇒ apt 增量≈ 0;增量只有代码 175.4 MiB(用 Pi 构建器的排除清单实测)+ venv≈ 90–120 MiB + frpc≈ 10 MiB + FTDI 库≈ 1 MiB,而 rootfs 可用 963.7 MiB ⇒ 不需要扩容(首启后 eMMC 扩到 32 GB)。构建期仍保留 400 MiB 断言,让将来的基底变化表现为构建失败而不是半个 rootfs。④ FT-710 真 FFT 开箱即得,靠一个 ELF 填两个槽。 find_ftdi_libraries() 要求 libft4222 与 libftd2xx 成对同目录存在,而 FTDI 的 Linux 包只发布一个文件:其 ReadMe.txt 明说静态链入 libftd2xx、install4222.sh 也只装一个、包内无任何 libftd2xx.so。实测 aarch64 构建导出 81 个 FT_* 符号(scope_pipe 真正调用的 FT_OpenEx/FT_Close/FT_SetTimeouts/FT_SetLatencyTimer 全在)且 DT_NEEDED 无 libftd2xx ⇒ vendor/ftdi/libftd2xx.so 做成了指向 libft4222.so 的符号链接,走默认目录扫描、零 env。溯源与复现命令见 packaging/box/vendor-ftdi.md;测试断言它是 e_machine=183 的 ELF(已验判据能拒 x86-64 的 62 与 arm-v7 的 40)。⑤ 最隐蔽的陷阱是配置文件的身份(新决策 D-11):Pi 的单元读 EnvironmentFile=/opt/mrrc_modern/env/mrrc.env,而 cloud_hub.connect() 写的是 server.py:_config_file_path()(优先 MRRC_CONFIG_FILE,否则回退 MEM_FILE.parent/mrrc_modern.env),packaging/ 里从未设过后者 ⇒ 在镜像上两条路径不同 ⇒ 它写下的 MRRC_SSL_CERT/MRRC_SSL_KEY/MRRC_REMOTE_SESSION_TX_HEARTBEAT_S 服务端永远读不到,症状是设置页显示「已连接」而上游 TLS 校验失败、PTT 活性闸门从未生效。这也解释了 Cloud Hub 至今只在 Windows/macOS 实例上跑过(那两边的启动器自己设该变量)。本镜像的单元同时钉死两者,verify.sh 第 7 项把相等关系做成断言。⑥ 11 份 profile:每份只写型号(波特率由后端表决定、串口与声卡由首启解析,写第二遍就多一个会错的地方)。mrrc-radio list/show/use 两条易错规则均被测试钉住:换型号且未显式给 --port 时清空 MRRC_SERIAL_PORT(FT-710 是 ttyUSB0/1、IC-7300 是 ttyACM0,留旧值等于无报错的死链路);TX 门禁由 profile 重新断言而非继承,8 个未验证机型显式写 =0,且测试拒绝任何 profile 出现 =1(AD-019 / NFR-067)。mrrc-radio 自身不做串口 I/O(cat-direct-serial-io):复用 linux/first_run.py 既有的探测,且先停服务再探(否则端口被占着问不出东西)。⑦ udev 规则复用既不重写:install.sh:699-724 已装 0403:601c(FT4222H)MODE=0666——正是让 FTDI 那条路能以 mrrc 用户工作的关键——以及 CP210x 的免 sudo 与 ft710-cat 符号链接。记录一个既有局限:FT-710 两个 CP210x 同 VID:PID,该符号链接归属由枚举顺序决定、有歧义但不影响正确性,因为 probe_radio_model() 的发包探测才是 CAT 口的权威判据。⑧ 三处排除清单由测试逐字钉住:Pi 构建器、box overlay、mrrc_update.sh 各带一份 rsync 排除列表,两份构建器分歧的后果是运行期 ImportError;测试遍历三份并点名漂移的那一处(已实测:删任一条即红)。这是执行中发现的漏洞——初版只比两份,第三份无人守。⑨ frpc 钉版 0.71.0(客户端比服务端新会握手失败,且症状是「隧道永远起不来」而非一句版本抱怨),校验和取自 frp 自己的 per-version frp_sha256_checksums.txt;实测:真实 release 装出 14.8 MB 静态链接 AArch64 ELF,篡改校验和与删除条目两种情形都拒且装 0 个文件。为让守卫可测,加了 MRRC_FRP_BASE 覆盖点(默认 URL 仍钉死)——只改版本号会一致地成功,根本测不到守卫。⑩ 性能结论由工具定义而非文档定义:新增 dev_tools/bench_mrrc.py 复现稳态热路径(走真实模块),M2 上 1.53% of one core(rx 0.96 / spectrum 0.37 / tx 0.19 / state 0.01);按 A53@1.8 ≈ 0.57× Pi 4 单核外推 ⇒ W103D ≈ 30%,含真实开销 35–45%,与项目自身 ~15% 的 SLO 交叉吻合。规格 §2.5 原先引用的 1.64% 已改为该工具的输出(同一测量、不同夹具)。⑪ 测试 1638 → 1669(+31;并入 main 后实测 1673 —— v1.25.4 那轮另有 +4):test_box_profiles(+10:注册表与 profile 双向相等、每份只含白名单键、任何 profile 不得出现 =1、8 个未验证显式 =0、硬编码的未验证集合与后端注册表交叉校验、排除清单三处一致、FTDI 是 aarch64 ELF、第二个名字是指向它的符号链接)、test_mrrc_radio(+10:显式端口写入、换型号清空旧端口、同型号不动端口、门禁不继承、未知型号拒绝、缺 profile 拒绝、写入保留他人键、留下备份、dry-run 不写)、test_host_score(+11:候选主机评分,评分函数逐项对照)。⑫ 边界与未做:未发版——镜像本体尚未构建、也未发布,release-artifacts.json 与网站下载卡留给独立变更(设计 §11);真构建需要 Docker 守护进程,硬件验收(U 盘启动、首启探测、FT-710 真 FFT、电台切换、Cloud Hub 公网)全部待真机,计划任务 11 的清单逐项列明,不以「应该可以」代替;docs/RASPBERRY_PI_GUIDE.md 让用户「解出 libft4222.so 与 libftd2xx.so」而 FTDI 的 Linux 包不含后者——这是独立缺陷,本次只记录不改(本设计的符号链接就是绕开它)。⑬ 11 张设计图的标记从 V2.70 提到 V2.73,但没有一张需要重画:已按 release-artifacts.json 里每张图的 require 关键词逐张核对——它们描述的是架构/功能(后端家族、5 条 WebSocket、PTT 层次、录音、0x27 频谱),没有一张描绘部署渠道,而本版只动了部署层。之所以要动标记:仓库的图规则只接受最新 3 个版本(max_versions_behind=2),V2.70 在 V2.72 时仍在窗口内、升到 V2.73 即出界。所以这次盖章的语义是「按 V2.73 复核过、无需改动」;对比 FT-891 那轮只动了一张,因为新增机型确实改了系统上下文(system-context.svg 现为 V2.72)。 ⑭ 本行编号:本行原写作 V2.73,与同日发布的 v1.25.4 那行撞号 —— 那一行已经上线(网站页面、两个安装包内的文档都已盖章 V2.73),不能再动,故本行改记 V2.74,设计图标记停在 V2.73(仍在「最新 3 个版本」窗口内)。本行所述部署层改动本身未变。 |
| SDD V2.73 | 2026-10-06 | pi | v1.25.4 发布:隧道残留的 macOS 那一半 —— 清理代码被一句 if os.name != "nt": return 关在 Windows 之内,从来没跑过;同轮修掉「关停不停隧道子进程」,并揪出第二个空转的测试闸门。 ① 缺陷:v1.25.2 修「每启动一次就多留一个 frpc」时改的是枚举方式(wmic → Get-CimInstance),而 _kill_stale_frpc() 开头那句平台判断(2026-10-02 697d9d2,为「同机上第二个实例不受影响」加的)把整段清理关进了 Windows ⇒ macOS 上启动时立刻返回、什么都没做。② 现场取证(macOS,2026-10-06):一次开机后启动 6 次留下 6 个孤儿 frpc(PPID 全为 1);当晚 21:39 清场后仅剩的干净子进程拿到 [bg1sb] start proxy success,21:40 应用再启动,新子进程从那一刻起每 33 秒打一次 start error: already exists,86 行日志里 67 行是这段重试 —— 故障自我循环:上一次的孤儿挡住下一次启动,而下一次启动又不清理。③ 修法:枚举改走 ps -eo pid=,command=,只认命令行第一个词的 basename 是 frpc 的进程(用编辑器打开同一份 frpc-bg1sb.toml 的 vim,命令行里也有这个路径,不能误杀);终止改走 SIGTERM,让 frpc 自己关掉控制连接、hub 当场释放代理名(Windows 仍只有 taskkill /F);永不抛出 —— ps 快照与 kill 之间进程可能已自己退出(ProcessLookupError),Windows 侧 taskkill 超时抛的是 SubprocessError 而不是 OSError,两种都得接住;调用方是 TunnelProcess.__init__,异常从那里冒出去会把隧道建立连同触发它的那个接口一起带走。④ 第二处:server.py 的 lifespan 关停半段从来没停过 _cloud_tunnel —— frpc 是服务进程的子进程,父进程没了它照样活着、把代理名占给下一次启动;崩溃或被 kill 会跳过这一步,所以两层缺一个故障就会回来。⑤ 测试:CertificateReloadClockTests 那两条一直是空转的(_configured_ssl_cert() 先读配置文件、只在文件里没有该键时才回退 os.environ,而它们只 patch 了环境变量 —— 判的是开发机真实的 mrrc_modern.env),tests/README.md 还把它记成「pre-existing 失败」;现把 MRRC_CONFIG_FILE 钉在临时文件上,只判自己写下的那张证书。同一轮第二条空转闸门是被 Windows 门禁叫醒的:CallSiteGuardTests(v1.25.1 加的,拦「新加的 urlopen 忘了带 context=」)用 assertGreater(scanned, 50) 证明自己「确实在看出货树」,而那个 50 是照开发机 macOS 的 53 个模块挑的下界,按 win_pack.md 的排除表装到构建机上只有 50 —— 同一份代码在 macOS 绿、在出货树上红(50 not greater than 50);被排除的三个文件里一个 urlopen 都没有,是闸门自己标错了。改成内容对照(出货树里必定存在的 server.py/cloud_hub.py/net_tls.py/launcher_net.py 必须在扫描集里)并让 _SKIP_DIRS 与源码包排除表对齐,变异验证两个方向都红(REPO_ROOT 指向空目录 → 'server.py' not found in set();往 session_metrics.py 塞一处裸 urlopen → 点名 session_metrics.py:217)。套件 1642(macOS,1 skip)/ 1639(Windows,19 skip)。⑥ 验收:VM 门禁 1639 项 OK;冻结字节码走查(dev_tools/bundle_check_v1254.py,自带自对照 —— 拿两个不相干的函数比、必须报 DIFF,否则整份走查不可信)server.lifespan 与 cloud_hub 五个函数逐字节一致;洁净室 19 条全过(https /login 200、同端口明文 000、/api/health 401、TLS 1.3、SAN 含 IP Address=127.0.0.1、只绑 127.0.0.1、证书落在用户数据目录、应用目录 163→163、第二实例没起服务);真 Setup.exe 静默装进隔离 /DIR= 12 条全过(165 文件、version.txt=1.25.4、安装后的 Launcher https 200、卸载无残留)。产物:Windows Setup 54,347,067 bytes,SHA-256 7daebeae65dccfb184be6b93553df0967e12f9fd84b9a937e3a7ae4c4c0659ab(VM Get-FileHash == Mac shasum);macOS DMG 62,440,021 bytes,SHA-256 e823b125c6b2bbf442de0f1bd52785ea2a68cb4c6e52a16eee4e0bc8e8ab8df4。这一版同时是 V2.71 与 V2.72 到达用户的那一版(两者只改冻结入口 server.py 与 cloud_hub.py,热修通道覆盖不到),判据不是版本号推断:构建用的源码包 dist/mrrc_modern_src.zip 里的 server.py、cloud_hub.py 与工作树逐字节相同(_claim_tx_owner_for_token、_same_session_uplink_takeover、FT-891 的 backends/yaesu/yaesu_profiles.py 都在包内),构建机上再把安装包内 server 入口与该源码比对,lifespan 的 co_code/co_consts/co_firstlineno 全部一致。⑦ 两个坑记进 win_pack.md:洁净室第一版只设环境变量不管用 —— 配置文件优先于 os.environ,首次运行生成的 mrrc_modern.env 里写着 0.0.0.0:8888,于是 MRRC_WEB_PORT 完全不生效;.ps1 必须存成 UTF-8 带 BOM,PS 5.1 无 BOM 时按 cp936 读,中文行尾字节与换行配成双字节把换行吃掉、下一行被并进注释(症状是 $cfgPath 报 null)。两条旧断言同轮被证伪:走启动器这条路看不到 signed a self-signed certificate(证书是启动器先签好的,windows/launcher.py:293),第二实例的退出码有意为 0(windows/launcher.py:344)。⑧ 未做:V2.71 里记录的「同一晚 18:38–19:04 另一段不同症状(frames=0 且 drops=0)」仍属客户端采集侧、服务端日志无法定性,本次未修也未下结论。 |
| SDD V2.72 | 2026-10-05 | pi | Yaesu FT-891 接入:第 11 个注册键、Yaesu 家族第 5 个 profile(实验性、仅接收);顺带修掉一个会让全部五款 Yaesu 机型首次连接就崩的既存缺陷。 设计与计划见 docs/superpowers/specs/2026-10-05-ft891-model-design.md、docs/superpowers/plans/2026-10-05-ft891-model.md。① 无真机,全程按 AD-019:verified=False、tx_gated=True(发信需 MRRC_ALLOW_UNVERIFIED_TX=1,释放永不设闸)、id_answer=""(可由 NC_RIGID_FT891=135 推出 0135,但按操作者决定推论值不写成期望值)、unverified_meters 五项、profile 里 11 处 TODO(hw-verify)。② 唯一的共享核心改动:FT-891 的滤波宽度写格式与家族不同——SH01NN 而非 SH00NN(Hamlib newcat.c:9658-9663,int on = is_ft891,P2 是 “bandwidth on” 位)。新增 YaesuModelProfile.filter_width_prefix(默认 "SH00"),cat_core.set_filter_width 改读它 ⇒ 对四款已发布机型字节等价(逐机型钉死回归),核心仍然零机型分支;读回侧取响应末两位,两种前缀都能解析,故未改。AD-018 那句「加第 5 个机型只需加一张 profile 表」因此被实测修正为:必要时给表加一个字段,但绝不在核心里写 if model ==。③ 档位表逐档抄录并与 Hamlib 机器比对一致:narrow 组(CW/RTTY/PKT)索引 1–17、voice 组(SSB)索引 1–21(取自 get 侧 newcat.c:10099-10153/10162-10208;Hamlib 自身在索引 21 上 set 侧注释 3000、get 侧返回 3200,ft891.c:245-290 .filters 又列了一个无对应槽位的 SSB 2250 —— 一律以 get 侧为准并记入 provenance)。AM/FM 不进表:newcat.c:8924-8936 只调 newcat_set_narrow、从不写 SH 索引,10217-10228 返回固定宽度(AM 9000 / AM-N 6000 / FM 16000),给它们编索引就是造数据;索引 0 也不进表(其 Hz 取决于 NA 开关,而本仓库 UI 从不发索引 0)。附带收益:前端 widths[pair[0]]=pair[1] 是后写覆盖,家族表里 AM/FM 的索引 1/2 会盖掉 SSB 的标签,FT-891 不复制这个形状(已加断言钉住 voice 21 / narrow 17 各自索引唯一)。④ 三项离线推不出的事实由老产品的 FT-891 现场上报关闭(hub/mrrc/dist/support_answers/20260917-073700-14ef,操作员 bg7zhs,2026-09-17):串口 38400 8N1(data_bits=8/stop_bits=1/parity=None,并由 hub/mrrc/tests/test_rigctld_supervisor.py:82 钉死)——Hamlib ft891.c:143 的 serial_stop_bits = 2 自承是假设值(Assumed since manual makes no mention),不采信,cat_core 的 8N1 不改;那次「连不上」的根因是 rigctld 未自启(老产品 V6.1.14 已修),不是串口帧格式,故 8N1 无反证。无 USB 声卡(上报里的音频设备是主板 Realtek High Definition 一对,而非 Yaesu codec;老产品模板默认按 USB Audio 片段自动选设备,该用户仍落在 Realtek 上)⇒ audio_name_hints=()、audio_rx_rate=audio_tx_rate=48000(外接接口常态,且 48k 端到端在本仓库是 IC-7300 家族的既证免重采样路径)、下拉标签写明「需外接声卡」、NFR-060/NFR-065 同步。S 表偏高 1–1.5 S(≈ 6–9 dB)—— 只记入 provenance 与 §13.3 A10 作为真机核对首项,不拿另一个产品的观测改本仓库的曲线(无原始 raw 值);这也再次说明 Hamlib 自标 /* TBC */ 的 FT891_STR_CAL(与 FTX1_STR_CAL 逐点相同)不可信。⑤ 既存真缺陷(本版最重要的附带修复):YaesuCatController 从未定义 get_preamp / get_attenuator,而 cat_core.initial_state_sync()(:702-703)与 backend.settings_poll_items()(:151-152)都在调用它们 ⇒ 首次连接成功即抛 AttributeError,7 个设置轮询项里 2 个每轮抛。它能存活是因为该家族无任何真机(V2.46 D2),谁也走不到「串口打开之后」,且 initial_state_sync 在 Yaesu 核心上零测试覆盖。修法沿用既有 _get_int(cmd, offset) 形态(写侧是 PA0{v}/RA0{v},故读回偏移 3,返回 RadioState 存的档位索引)。回归测试做成结构性守卫而非一方法一断言:遍历四组 poll items(含 meter 组的 3 元组,按 callable 取用而非假定位置)+ 解析 initial_state_sync 源码,断言每个被引用的 get_*/set_* 在控制器上存在 —— 五款机型共守 13 个方法,同类空洞以后在导入期就红,而不是在现场崩。⑥ block 约束 cat-sh-format 修订:原文案(title 与 message)把 SH00NN 断言成普适事实,接入 FT-891 后对 backends/yaesu/ 不再成立 ⇒ 改为「前缀由 profile 决定,SH00 家族 / SH01 FT-891」,并写清规则真正要挡的是缺位的 3 字符 SH0NN 与「设完不回读 SH0」。severity/patterns/scope/exclude_scope/sdd_ref 逐字段核实未变,并用一次性探针实测硬编码 f"SH0{i:02d}" 仍被拦下、报出新文案(不为放行自己的改动而放松护栏)。⑦ 前端缓存键:sw.js 对 /index.html 是 cache-first 且只在自身字节变化时重装,而 test_static_cache_busting 把 /index.html 列在 SKIP 里 ⇒ 只改 index.html 不 bump CACHE,已安装用户永远看不到新下拉项且无测试会报警。mrrc-v45 → mrrc-v46,四处 pin 同步重钉(test_audio.py:151、test_server_ws_protocol.py:237、test_tx_liveness.py:345、test_ws_token_transport.py:184 —— 计划初版只列了两处,执行前审查用 grep -rn "mrrc-v45" 查出另两处)。sw.js 保持一行 diff:格式化器会把三个 addEventListener 重写成箭头函数,那不属于本次改动。⑧ 测试 1623 → 1638(+15):按模块——test_yaesu_cat_core 41→46(+5:FT-891 侧 3 = SH 前缀由 profile 决定 / 四款机型仍发 SH00 逐机型钉死 / 读回兼容两种前缀;bug 修复侧 2 = 两个 getter 的档位解析与静默容错)、test_yaesu_profiles 23→27(+4:档位表与 Hamlib 逐档一致 / AM-FM 与索引 0 不在表内 / 模式集排除 C4FM-PKTFM-AMN / 静态事实逐条)、test_yaesu_backend 21→25(+4:拍平后索引唯一 + 无 ATU 无 USB 音频的能力上报 + 两个 TX 闸门测试)、test_yaesu_wiring 8→10(+2 结构性守卫)。按归属——FT-891 侧 +11(cat_core 3 + profiles 4 + backend 4)、bug 修复侧 +4(cat_core 2 + wiring 2),合计 +15。验收:全套件仅余既存红灯 test_cloud_endpoints.CertificateReloadClockTests.test_a_certificate_written_after_start_asks_for_a_reload(先于本次改动、在 origin/main 上同样失败,未触碰);MRRC_RADIO_MODEL=ft891 无硬件启动冒烟无 traceback(串口错误被分类、Server will serve UI only 降级、两条 TX 闸门告警点名 _diag_yaesu.py);能力上报实测 has_atu=false/verified=false/tx_gated=true/audio_name_hints=[]/att_steps=[0,12]/bands 11 段无 2m-70cm/voice 21 narrow 17;音频侧印证 D3 —— 空 hints 下仍由通用 USB-audio 层选到外接设备并提示可用 MRRC_AUDIO_RX_DEVICE 纠正,且设备默认 44100 而按请求的 48000 实际打开成功。⑨ 文档同步:SDD 01/02/03/04/05/06/07/08/09/10/11/12/13/15 + README(Quick Facts V2.71→V2.72、未验证机型七→八、注册键十→十一)、站点 website/sdd/ 六个页面、AGENTS.md:47、README.md、DEPENDENCIES.md、tests/README.md(含修正三处先于本次改动就存在的每模块错数:test_yaesu_wiring 6→8、test_backend_factory 22→30、test_server_ws_protocol 82→98,以 origin/main 实测为准)、docs/OPERATION_GUIDE.md。§10.4 的 filter 行由 SH00<NN>;(P1=0, P2=0 fixed)改为 profile 驱动前缀 —— 该行正是 cat-sh-format 的 sdd_ref 指向处。⑩ 边界与未做:未发版 —— 仓库无 Unreleased 段、CHANGELOG 惯例是条目随发版号落地(四款 Yaesu 记在 ## [v1.16.0]),版本号权威在 packaging/windows/MRRC.iss,故 CHANGELOG 与版本号留给发版步骤;11 项 TODO(hw-verify) 全部待 _diag_yaesu.py --model ft891 现场关闭(逐项关闭方法见规格 §10);已知缺口:AM/FM 下前端会落到 voice 列表故标签与电台固定宽度不符(需 NA 建模或前端按模式隐藏滤波控件)、Id 电表因合并标志 has_vd_id_meters=False 一并隐藏、AM 的 25 W 上限未建模(power_max_w 是单值,家族同样如此)、getNextFilter 的硬编码轮换表仍是 FT-710 的(FT-891 下多一个 -- 档)、macos/first_run.py 不自动探测 FT-891(与四款家族机型一致);backends/ft710/ 全程未动。 |
| SDD V2.71 | 2026-10-05 | pi | 发射时“键控了却没有声音”:TX 上行所有权按 token 认领,而一个 token 名下可以有多条 socket。 ① 现场取证(hub 实例 logs/ft710-server.log,2026-10-05):19:04:43 页面重载后出现第 3 条 /WSaudioTX(owner=False),19:05:34 按键打出 TX audio ownership → PTT client、紧接着 TX audio from non-owner client ignored,四次发射全是 frames=0 fed=0 written=0 peak=0% non_owner_drops=190/118/102/236 —— 电台键控了、调制为零(peak 是调制幅度峰值,0% 即纯静默);19:06:32 那条 socket 断开(owner=True)后 _promote_tx_owner() 顶上,立刻 frames=78 written=72 peak=99% 恢复 —— 故障在僵尸 socket 被回收的瞬间自愈。session_metrics 同期报 3 个 socket / 2 个 operator session,直接证明两条 socket 共用一个 token。② 根因:会话令牌是 30 天 Cookie,一个浏览器会话可以同时持有多条 /WSaudioTX(重载留下的半开旧 socket、同一登录的两个标签页);而 _claim_tx_owner_for_token() 扫描 audio_tx_clients(set,迭代顺序任意)取第一个同 token 的 socket —— 认给谁由内存顺序决定,可能正是那条已经不再送音频的旧 socket。最小复现(只读不改产品代码):12 次试验 2 次认错,且每次都照样打印 ownership → PTT client —— 这行日志因此不能当作判据。③ 修法两层:确定性认领 —— _register_tx_socket() 记录每条 socket 的连接序号(_forget_tx_socket() 随断开回收),按键时取该会话最后连接的那条(这才是 “a same-session replacement audio socket supersedes a half-open page-reload socket” 的本意;原先只在旧 socket 恰好是当前 owner 时才成立);流式接管 —— _same_session_uplink_takeover() 在收到第一个语音帧时把上行交给与 owner 同 token 的非 owner socket(同会话即操作者自己的活页面),一帧约 20 ms 内自愈,覆盖“两条 socket 都活着、人在先连的那个标签页操作”这种连接期规则管不到的情形。④ 负例保持不变:异会话既不能靠连接(_assign_tx_owner_on_connect)也不能靠发流抢占上行 —— 把两个麦克风交错塞进同一个播放队列正是单 owner 闸门要防的事;所有权从某条 socket 移走后 dead-man switch 仍然正确(finally 只在断开的正是 owner 时强制 RX)。⑤ 测试 1616 → 1623(+7,全在 TXUplinkOwnershipTests:最后连接者胜出、20 轮不同 set 顺序下的确定性、同会话流式接管、异会话不可抢占、owner 与无令牌 socket 的 no-op 边界、序号表随断开回收且不泄漏、端点接线守卫)。修复前 6 例 AttributeError(helper 不存在)+ 1 例接线断言红,既有 7 例仍绿;修复后 14 例全绿,重跑最小复现 40 次试验 0 次认错。⑥ 边界与未做:同一晚 18:38–19:04 另有一段不同症状(frames=0 且 drops=0,当时两条 socket 都在线却一帧麦克风音频都没送到服务端)属客户端采集侧,服务端日志无法定性,本次未修也未下结论,需浏览器侧计数/告警(sendTXAudioFrame 在 socket 非 OPEN 时是静默 return false,UI 无任何提示)才能取证;本修复只在源码,server.py 是冻结入口脚本(不在热修通道覆盖范围),到达用户须重建安装包。另:套件在 HEAD 上已有一处与本改动无关的既存红灯 test_cloud_endpoints.CertificateReloadClockTests.test_a_certificate_written_after_start_asks_for_a_reload(已用 git stash 在 HEAD 复跑确认为既有失败),本次未修。 |
| SDD V2.70 | 2026-10-05 | pi | v1.25.3 发布:远端会话的 PTT 存活闸门会把操作者自己刚按下的一键放开;同一轮还抓到一次「门禁全绿却冻进旧字节码」的构建。 ① 缺陷:Cloud Hub 打开的存活闸门(V2.63,MRRC_REMOTE_SESSION_TX_HEARTBEAT_S)只应释放不再证明自己活着的发射会话,而心跳 txhb 在按键时发:浏览器先发 ptt:true,再回一条心跳;服务端读循环要等按键处理(CAT 写 + start_tx,几十毫秒)返回才能读到那条心跳。落在这段窗口里的检查看到的是「既是持键者、又已沉默很久」的会话,于是把刚按下的一键当成失联放开。2026-10-04 的 hub 实例现场日志记了 7 次误放开,每次都在按键后 20–70 ms,间隔 9.3–654 秒(恰好等于该操作者上一次按键以来的时间)。② 修法:ptt:true 处理里对已声明能力(发过 txhb)的会话刷新存活时钟(_tx_hb_last[ws] = time.monotonic())再去做 CAT 写 —— 按键成为活着的证明;但按键本身不声明能力,从未发过 txhb 的会话不进 _tx_hb_capable、保持不被门控(V2.63 的负例不变)。阈值、释放路径(单次 fire-and-forget set_ptt(False)、无 verify 循环)与释放提示均未改。③ 构建事故(本版最重要的流程教训):第一个 Windows 包门禁全绿(1612 项 OK)、version.txt = 1.25.3、size/SHA 都与上一版不同,但符号走查发现冻进去的是未修复的 server.py。根因:PyInstaller 的 workpath 缓存(build\pyinstaller)只在文件 (size, mtime) 变化时重分析,而 tar 保留了构建 Mac 的 mtime —— 修复后的 server.py(Oct 4 23:04)比 v1.25.2 的缓存(Oct 5 07:46)旧,于是只有 COLLECT-00.toc 被重写、旧字节码被复用。证据:_execute_set_command 与源码差 10 条指令、其 genexpr 行号 2469 vs 2484(正好是本次修复加的 15 行)、_tx_hb_capable/_tx_hb_last 只在源码里。修法:build.ps1 在调用 PyInstaller 前 Remove-Item build\pyinstaller(build.sh 早已这么做),并加守卫测试(变异验证:删掉该行即变红)。④ 产物(两端均由本轮代码构建,跨主机 SHA-256 一致):Windows MRRC-Modern-v1.25.3-Windows-x64-Setup.exe 54,342,929 bytes 1413521c…(完整 1413521cd4081d922429320106ba44f135d339e4ac805d29937b0f2d667744ec;VM 门禁 1613 项 OK / 19 skip、Python 3.12.4 / PyInstaller 6.21.0 / Inno Setup 6.7.3、version.txt = 1.25.3、mtime 2026-10-05 13:45:21、fleet 13 文件在应用根目录、包内 junk 0、私钥形状 0〔仅 vendor\ca\cacert.pem〕);macOS MRRC-Modern-v1.25.3-arm64.dmg 62,430,310 bytes 87890593…(完整 878905932720268ba6d9dff3ba049b95b260523e8acb0ae33071b68d8ac419c6,MD5 0cade586bf88e5827e57552c9310fb1f;套件 1616 项全绿 / 1 skip、CFBundleShortVersionString = v1.25.3、Contents/Resources/version.txt = 1.25.3、codesign --verify = valid on disk + satisfies its Designated Requirement、DMG 经典布局、垃圾与私钥各 0)。⑤ 验收:macOS 冻结核洁净室(空 HOME + 字段式配置 127.0.0.1:18894)—— https /login 200、同端口明文 000、/api/health 401、签名证书签出并记入日志、Recording ready 指向用户目录、日志 26 行 / 25 distinct、第二实例退出非零并点名端口、bundle 0 文件被改、运行后 codesign 仍 valid;Windows 洁净室(隔离 LOCALAPPDATA + 空闲端口 18896 + 冻结 Launcher)—— 首探 settle(MRRC_FIRST_RUN_DONE=1、22 位口令、MRRC_SERIAL_PORT=COM1)、https /login 200、明文 refused、/api/health 401、TLS 1.3 + SAN 含 127.0.0.1、第二启动器退出、安装目录 163→163;再用真 Setup.exe 静默装进隔离 /DIR=(165 文件、version.txt 正确、安装后的 Launcher /login 200、安装目录 165→165、卸载干净);两端符号走查(server 入口的 _execute_set_command/_tx_liveness_* 与源码逐字节一致、CONTAINS_OP _tx_hb_capable 在;launcher 携带 launcher_net/first_run/ssl_material/acquire_single_instance/_create_launcher_mutex;PYZ 携带 net_tls/cloud_hub/ssl_bootstrap/upgrade_core/config,cloud_hub 携带 _enumerate_frpc/_stale_frpc_pids/_windows_norm/_kill_stale_frpc/_NO_WINDOW)。⑥ 边界:KVM 无声卡且 VM 未接电台(仅 COM1),TX 话音与 CAT/音频设备行为仍须物理机/真机验收;macOS DMG 早于 build.ps1 的缓存清理修复构建,但该修复只影响 Windows 构建路径,DMG 内容不含构建脚本。 |
| SDD V2.69 | 2026-10-04 | pi | Windows 实例的隧道泄漏:回收旧 frpc 的代码依赖 wmic,而 Win11 已经没有它(且失败被 except 静默吞掉)。 现场取证:hub 的 frps 日志一天被同一个名字刷了 7143 行 proxy [ba4eg] already exists(另有 bg9aaa 994、bg6lh-legacy 362)。从 hub 侧反查,刷屏的 4–5 个 run id 的 client login info 全是同一台机器(hostname MRRC、os windows、frpc 0.71.0),上机看到 6 个 frpc.exe 同时在跑:命令行逐字相同(-c …\fleet\frpc-ba4eg.toml)、启动时间跨两天、其中 5 个的父进程早已退出(孤儿),只有最新的那个是 MRRC-Modern-Server.exe 的亲儿子。根因链:TunnelProcess.__init__ → _kill_stale_frpc() → subprocess.run(["wmic", …]);Win11 已移除 wmic(该机实测 Get-Command wmic 什么都找不到)⇒ FileNotFoundError ⇒ except Exception: return ⇒ 每次启动漏一个,1 个赢得注册、其余每 30 s 重试一次,而这台机器自己一点异常都看不到(症状只出现在 hub 的日志里,这也是它能躺两天没人发现的原因)。已修:cloud_hub.py 改用 PowerShell Get-CimInstance Win32_Process(强制 UTF-8 输出、CREATE_NO_WINDOW、非零退出码即抛),路径比较改大小写不敏感 + 两种斜杠(原 str(config_path) in line 是大小写敏感的,而 Windows 路径不是 —— 仅大小写不同的路径会匹配不到),PID 只收 ASCII 数字,查不到就 WARNING 出声;windows/launcher.py 增命名互斥 Local\MRRC-Modern-Launcher(端口探测只看得见”已经启动完成”的服务器,两次启动落在它 0.4 s 窗口内时都会判定端口是空的 —— 那台机器上正好有 2 个 MRRC-Modern-Launcher;内核互斥在进程退出时自动释放,不会像 pid 文件那样把用户锁在外面);两处失败一律”继续启动”而不拒绝启动。测试 1604→1612(+8:_stale_frpc_pids 的大小写/斜杠/不误杀他人实例/失败告警 4 项,互斥 4 项)。边界:修复在源码 —— 1.25.1 安装包不含它(那台机器装的是 1.25.0,而且该 wmic 写法在 HEAD 依然存在),需要下一个版本号重建才到达用户;现场只手工清掉 5 个孤儿(6→1),“下次应用重启会自己回收”这一步仍待现场验证;bg6lh-legacy 那 362 行经查是本机重连时的窗口重叠(同一实例的新旧连接争注册),属正常抖动,未处理。分发判据(核对过代码):cloud_hub.py 在 mrrc_modern_server.spec 的 hiddenimports 里(被 server.py:51 import)⇒ 已编译进包;windows/launcher.py 是冻结入口脚本 ⇒ 两者都不在热修/补丁通道能覆盖的范围(那条通道按定义只改“未编译进去的运行时文件”)—— 重建安装包是下一版唯一的出路,而且 cloud_hub 与 launcher 都需同步带上。v1.25.2 发布(两端均由 0983c2d 的干净 worktree 构建:并行会话在制的 server.py / test_tx_liveness.py 被排除在包外):Windows MRRC-Modern-v1.25.2-Windows-x64-Setup.exe 54,346,505 bytes 328f029b…(VM 门禁 1609 项 OK / 19 skip;mtime 当天、size 与 SHA 均 ≠ 上一版、包内 version.txt = 1.25.2、VM 与 Mac 两侧 SHA 逐字节相同);macOS MRRC-Modern-v1.25.2-arm64.dmg 62,429,438 bytes 8ef9c8ce…(codesign --verify = valid on disk + satisfies its Designated Requirement)。洁净室真跑 12/12 通过(隔离 LOCALAPPDATA + 预置配置在空闲端口 18899 + BROWSER=no-such-browser,跑的是冻结核 Launcher):/login 200(握手证书 CN=localhost,SAN 实测含 IP Address=127.0.0.1 ⇒ 浏览器校验通过)、同端口明文零字节、第二实例退让且只剩 1 个监听、安装目录 163 → 163 文件不变、无 mrrc_modern.env/certs/recordings 落进安装目录、日志 9 行 9 distinct(无重复写)、Recording ready: 指向用户目录、首跑探测 settle。rpi64 镜像未重建。构建源与验收的边界(KVM 无物理声卡 ⇒ TX 话音待真机;VM 无电台 ⇒ CAT/音频未验)同前。文档:本条目、mrrc_hub/SDD/12 §12.8「排障增补」(hub 侧判据与三步取证)。 |
| SDD V2.68 | 2026-10-04 | pi | 两处「只有构建机看不见」的 macOS 打包缺陷:出站 HTTPS 没有信任库、真频谱找不到 FTDI 库。 现场同一台 Mac(192.168.1.150)报了两件看似无关的事:频谱不对(界面一直是一条光滑假曲线)与接入云端报 CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate (_ssl.c:1032);共性都是构建机上有、用户机上没有的东西被当成了常驻假设。① HTTPS:包内 libcrypto.3.dylib 的编译期默认 CA 路径是构建机的 MacPorts 目录 /opt/local/libexec/openssl3/etc/openssl/cert.pem,用户机没有 /opt/local ⇒ 信任库为空;portal 侧完全正常(curl 200、链完整、通配证书有效至 2026-12-29),同机日志另有 3 条 Support bundle upload failed: … unable to get local issuer certificate(那份诊断包为什么没能自动上传、要人手工递的真因)。空信任库在本机复现出逐字相同的报错,指向 /etc/ssl/cert.pem 即 200。新增 net_tls.py:默认 store 为空时依次加载 MRRC_CA_BUNDLE → 随包 vendor/ca/cacert.pem(119 根,Mozilla 派生)→ 平台系统 bundle(/etc/ssl/cert.pem、/etc/ssl/certs/ca-certificates.crt 等);cloud_hub / upgrade_core / 诊断包上传三处外发 HTTPS 统一走 net_tls.urlopen,并以 AST 守卫禁止再出现不带 context 的 urlopen。构建期新增 dev_tools/tls_trust_gate.py(两个 build 脚本都调用):故意把 CA 环境打空,先断言未修法(裸 create_default_context())确实为空(否则闸门只是空转),再要求 net_tls 重建信任库并与 portal / latest.json 真握手;两个 build 脚本在缺 vendor/ca/cacert.pem 时直接失败。② 真频谱:日志 42 次 scope_pipe exited (frames=0, connected=False)、6 次 Spectrum broadcast active: S-meter fallback、first frame received — spectrum active 0 次 ⇒ 自安装起从未工作;硬件在位(ioreg 里 FT4222 与 CP2105、USB Audio 同属电台内部 hub)、库在位(Contents/Resources/vendor/ftdi/macos 两个 arm64 dylib),手工用正确目录运行立刻得到 94 帧 / 11 fps / wf1_max 164–179。根因:macos/launcher.py 只放行绝对路径,随包 macos/default.env 的相对值 vendor/ftdi/macos 被锚到 Contents/MacOS/,而 v1.18.0 为可签名把数据树搬进 Contents/Resources/(只留 MacOS/_internal 链接)⇒ scope_pipe 报 FTDI libraries not found 并退出,父进程把非 STATUS: 的 stderr 只记 DEBUG,现场日志因此只看得到「无输出地退出」。同一函数上方那行 setdefault 用的正是会回退 _internal 的 runtime_path(),只有这一处漏了:相对值改走 runtime_path(*parts),三个回归测试(_internal 解析 / 同级副本优先 / 绝对路径不动)。③ 答复页新增「已知问题卡」:support_answers.KNOWN_ISSUES(无编号、按现象检索,仍过同一套隐私过滤与渲染器),本次两张事故卡已进 website/answers/index.html;render_page 改为「已知问题 → 按编号的答复」两段。④ 套件 1587 → 1604(+17:test_tls_trust_store.py 10、test_macos_launcher.py 3、test_support_answers.py 4);顺带修 support_bundle.detect_version() 的 resource_dir 类型标注。边界:两处修复都在冻结入口/应用代码内,macOS 只能随新版 DMG 到达用户(本轮未构建产物);现场机先用配置项规避(绝对 FTDI 目录 + SSL_CERT_FILE=/etc/ssl/cert.pem)并已复验:真频谱出帧、portal 200。 v1.25.1 发布(两端均由本行代码构建):Windows MRRC-Modern-v1.25.1-Windows-x64-Setup.exe 54,339,580 bytes c801d35e…(VM 门禁 1601 项 OK / 19 skip + 新构建闸门 dev_tools/tls_trust_gate.py 通过;洁净室:https /login 200、同端口明文 refused、/api/update/check 200 取回线上 latest.json、安装目录 0 改动、server.log 18 行/18 distinct);macOS MRRC-Modern-v1.25.1-arm64.dmg 62,421,426 bytes 461a4f98…(套件 1604 全绿;codesign --verify = valid + DR;洁净室:health 401 / login 200 / update-check 200 / MRRC_FTDI_LIB_DIR 经 _internal 解析到真实 vendor 目录 / bundle 0 改动);rpi64 镜像未重建。顺带校正上一版记录的一处哈希漂移:v1.25.0 的 Windows SHA 由 ee6e2a30f2b699a8…(v1.25.0 前缀 + v1.24.6 尾巴的拼接串,README / Windows 指南 / win_pack.md 各一处)改为产物实测的 ee6e2a30618cd1e5…;同批校正 macOS 侧同类漂移:v1.25.0 的 DMG SHA 由 0974703f88187c32… 改为 0974703f387031d8…(与站上通用镜像一致)—— 判据是拿已发布产物重算,不是页面上写的值。 |
| SDD V2.67 | 2026-10-03 | pi | v1.25.0 发布:「装完就能用」—— 首次探测不再卡死、批准后自己接入、局域网直接可达。 现场机 192.168.1.53(主机名 MRRC)装完 1.24.8 后 launcher 开着全黑窗口、无服务无日志,进程却活着并持着 COM3:实测三段根因 —— ① _key() 的电台关键词 cp210x/slab 与真实描述 Silicon Labs Dual CP2105 两者都不命中,候选保持注册表顺序 COM3/COM4/COM5;② COM3 是 Intel AMT SOL:open 正常、却一个字节也写不进去(杀掉持口进程后实测写入超时);③ probe_ft710() 用 pyserial 默认 write_timeout=None 写 AI0; = 永久阻塞,而首个被探测的正是这个口。现在:探测传 write_timeout=1.0、关键词换成硬件真实字串(cp210/silicon labs/ftdi/prolific/ch340/usb vid:pid)、AMT 与 PCI 标识的口不进候选、自动口令在探测前落盘(实测那次留下的配置是原封不动的模板)、探测前先打印要探哪些口。云侧三处:/status 早已 granted 而客户端自提交后再没问过(nginx 日志实证:当天最后一条 /status 来自另一台机器),因为唯一的轮询循环住在设置对话框(close() 即清掉)⇒ 轮询搬到服务端,每 30 秒自查、批准后当场完成登记,并把 autoconnect 状态暴露给界面;cert_reload_required 原先读 os.environ(= 进程启动那一刻)⇒ 登记后入口 502 且没有任何东西会重启,改为「配置现在要求的 vs 进程正在服务的」直接比对(顺带修掉 st_mtime 的 epoch 与 time.monotonic() 混用);basePath() 把裸路由当目录 ⇒ 页在 /login 时所有 API 打到 SPA 回退拿到 HTML,正是那句 Uncaught (in promise) SyntaxError 的真身,并给 api() 加 catch 让它带地址与状态码报错。界面与默认值:「刷新状态」不再与输入框同色(租户原话「这个是灰色的」因此没点,而该面板没有别的出路);Windows 默认侦听 0.0.0.0(与 SDD/12、config.py 的 ::、macOS/Linux 一致;实测 Mac → 192.168.1.53:8888 由 refused 变通)。产物:Windows Setup 54,115,523 bytes SHA-256 ee6e2a30…(VM Get-FileHash == Mac shasum);macOS DMG 62,298,582 bytes SHA-256 0974703f…。验收:套件 1579 OK(macOS,1 skip)/ 1576 OK(Windows,17 skip);两平台洁净室全绿(CN=127.0.0.1 / https 200 / 明文 000 / health 401 / 录制落在用户目录 / 安装目录 0 个文件被改 / 日志 19 行 18 不同 / 第二实例退出非零并点名端口);Windows 构建作废一次(包内 junk 6 个:VM 源码树的历史 .DS_Store/._* 因 tar 只覆盖不删除而残留)。 |
| SDD V2.66 | 2026-10-02 | pi | v1.24.6 发布:「装完黑屏」不止一个原因 —— 启动器改为向服务器问地址,Windows 端口改独占,裸启动不再写安装目录。 现场机(主机名 MRRC,操作员账号 cheen,装的是 04:03 上线的 1.24.5 构建)仍然黑屏。本轮先把远程通道打通(install_remote_access.ps1:OpenSSH Server + RDP,Mac 的 ed25519 公钥进 C:\ProgramData\ssh\administrators_authorized_keys,用 SID 而非本地化组名授权,icacls /inheritance:r 收权),再用发布产物本身取证:C:\Users\cheen\AppData\Local\MRRC-Modern\logs\server.log + 直接跑包内 MRRC-Modern-Server.exe。关键判据:应用是健康的 —— 纯 HTTP /login 200(text/html; charset=utf-8,1726 B),而同一端口做 TLS 握手得到 uvicorn Invalid HTTP request received ⇒ 浏览器拿到的是协议错误空白页,不是 UI/资产/音频/串口故障。① 启动器与服务器各自决定 TLS,是黑屏的结构性成因。 启动器按「自己能不能签出证书」算 https://,服务器按「自己能加载到什么」决定要不要 TLS —— 两次独立决策、一个 socket,不一致时浏览器就被送到没人监听的 scheme。新增 launcher_net.py(探 /api/health;任何 HTTP 响应都算在听,包括 401;TLS 由 URL scheme 决定),served_url() 先问首选再问另一个 scheme,windows/launcher.py 与 macos/launcher.py 共用(macOS 有完全相同的盲 webbrowser.open(url))。切换必须提示,绝不静默 —— 证书坏了是运维需要知道的事,静默换协议会把故障藏起来。② Windows 的 SO_REUSEADDR 不是 POSIX 那个含义:它允许第二个进程绑到正在监听的端口上,于是两个实例同时 Server ready!、由 OS 把连接分给它们,浏览器可能落到没拿到电台那一个。现场日志成对出现(两套栈各自枚举音频设备、各自开 CAT 口,相差 1–10 ms)正是这个画面。改为 Windows 用 SO_EXCLUSIVEADDRUSE(POSIX 保留 SO_REUSEADDR —— 启动器改配置就地重启要靠它),EADDRINUSE/WSAEADDRINUSE 重试约 3 s 后抛错并点名「另一个 MRRC Modern 还在跑」;启动器 spawn 之前先探(两种 scheme 都问),已有实例就复用并说明(选择复用而非杀进程:更不具破坏性,且能避免两个实例抢串口/声卡)。③ 裸启动把可写默认值指向只读的安装目录。 _runtime_dir() 在打包版就是 C:\Program Files\MRRC Modern;启动器会下发 MRRC_MEM_FILE/MRRC_RECORDINGS_DIR,但服务器 exe 自己也是入口(安装菜单里就有 “MRRC Modern Server” 快捷方式,且 _ensure_strong_password 的帮助文本还教用户这么干)⇒ Permission denied: '…\mrrc_modern.env.tmp'(每次启动生成一个新密码且存不下,这就是「换了新包还是登不上」)与 Recording disabled: …\recordings is not writable。新增 _writable_runtime_dir():不可写即回落 default_user_dir()(LOG_DIR/certs 早就这么做 ✓),MEM_FILE/RECORDINGS_DIR/RECORDINGS_INDEX 走它,_config_file_path() 随 MEM_FILE 一起搬走 ⇒ 密码真的落盘,也终于读得到用户自己那份配置。④ 裸启动开始读用户配置:config.load_user_config_into_environ() 在任何常量计算之前跑(为此把 default_user_dir() 上移)。只在 sys.frozen 或显式 MRRC_CONFIG_FILE 时生效,且只 setdefault —— 显式环境变量仍然赢(Pi/systemd/CI 不受影响),MRRC_NO_CONFIG_FILE=1 可整个关掉;这条限制同时保住测试隔离(开发者 home 里那份真实配置不可能改变套件看到的值)。⑤ 文件日志幂等:root logger 是进程级的,冻结包把本模块当 __main__ 与 server 各加载一次,两次 _setup_file_logging() 就是两个 handler 指向同一文件 ⇒ 每行写两遍(上面「成对」的另一半成因)。新增 _already_logging_to() 按已解析路径比对,同一文件只挂一个 handler;换 MRRC_LOG_DIR 仍照常新建。⑥ V2.65 ④ 里「建议但未实施」的常设守卫本轮落地:tests/test_undefined_app_module_names.py —— 纯标准库 AST 检查「应用模块被 name.attr 使用却在文件里从未绑定」,覆盖根模块与 windows/、macos/、backends/。它不是泛化 pyflakes(名字限定在应用模块集合内,避免误报淹没),也故意不引入 pyflakes/ruff —— requirements-build.txt 只有 PyInstaller,加依赖会迫使 VM 重装 venv(windows-installer 陷阱 8),而发版中途不引入新构建依赖。有效性实测:把 9f03868(装进现场那台的 server.py)喂给它 ⇒ 'ssl_bootstrap',喂 HEAD ⇒ 干净;V2.64 的 cloud_hub 同样命中。这是同一类缺陷第二次进发布包,守卫必须在门禁里,不能只靠 review。⑦ 发布号必须是 1.24.6,不能只补文档:修复后的那次 Windows 构建(54,087,234 B 28ac7743…,04:50 上线)已经以 1.24.5 的号在线上,而升级通道比的是版本串 ⇒ 04:03 装了同号旧构建的机器永远拿不到它,点「升级」只会反复重放同一版本。另:server.py/windows/launcher.py 是冻结入口脚本,不在热修通道能覆盖的范围内(mrrc-release 的判据),重建安装包是唯一出路。测试 macOS 1467 → 1533(+66:三个新文件 47 = test_launcher_net.py 13 + test_server_startup_guards.py 25 + test_undefined_app_module_names.py 9;既有文件 +19 = test_windows_launcher.py 7、test_macos_launcher.py 5、test_config.py 7),Windows VM 门禁 1530 项 OK(17 skip)。十三处变异验证(逐条把修复改回坏行为,对应测试必须红):config 装载器停用 ⇒ 3 红;不可写目录不回落 ⇒ 2 红;换回 SO_REUSEADDR ⇒ 1 红;允许重复 handler ⇒ 1 红;启动器改回盲开自己的 scheme ⇒ win 2 / mac 1 / net 1 红;守卫的应用模块集合缩小 ⇒ 2 红;main() 改回 uvicorn.run 的 else 分支 ⇒ 1 红;显式 host 不问平台独占语义 ⇒ 1 红;显式 host 不重试不给提示 ⇒ 1 红;显式 IPv6 不设 V6ONLY ⇒ 1 红;去掉 sys.frozen 分支 ⇒ 2 红;frozen 判断取反 ⇒ 4 红(连两个既有可写性测试一起红 ⇒ 这组测试互相咬合,不是各守一行);frozen 时仍按可写性返回安装目录 ⇒ 2 红。产物(两端均由本轮代码构建,跨主机 SHA-256 一致):Windows MRRC-Modern-v1.24.6-Windows-x64-Setup.exe 54,110,069 bytes ed127697…(完整 ed127697f2b699a87ca7b6655aaa1673a9eca12fc80cf75123c8c7c308fea79;VM 门禁 1530 项 OK、version.txt = 1.24.6、Inno Setup 6.7.3 / PyInstaller 6.21.0 / Python 3.12.4、fleet 13 个文件在应用根目录、包内 macOS 元数据垃圾 0、私钥形状文件 0、VM 与 Mac 两侧 SHA 逐字节相同);macOS MRRC-Modern-v1.24.6-arm64.dmg 62,286,150 bytes 6cb5cc02…(完整 6cb5cc0288187c32d42a6f43e6d300b33db7e223f0919c2c4d4b8370692e6fbb,MD5 1d3b84d27a6969325132b024adfccd24;套件 1533 项全绿、CFBundleShortVersionString = v1.24.6、Contents/Resources/version.txt = 1.24.6、codesign --verify = valid on disk + satisfies its Designated Requirement、Frameworks -> Resources 与 MacOS/_internal -> ../Resources 两条 symlink 在位、NSMicrophoneUsageDescription 在、垃圾与私钥各 0);rpi64 镜像未重建。一共作废了三个构建(都不是编译失败,而是「某一步静默地不对」):b248 把 macOS 元数据打进包(_internal\static\.DS_Store + AppleDouble,源于历次 tar 留在 VM 树上的残渣,而 spec 把 static/、vendor/ 整目录塞进 datas ⇒ 会真发给用户);b249 洁净室暴露显式 host 绕过端口守卫;b250 又因 macOS 洁净室暴露冻结包往签名 bundle 里写状态(Contents/MacOS/recordings/ 里放一个文件即 a sealed resource is missing or invalid)。Successful compile 三次都没说明任何问题。边界与未做:安装菜单里那条 “MRRC Modern Server” 快捷方式保留(裸启动现在已是正确的,但它仍不是该递给普通用户的入口;删它要动 .iss 的 [Icons],与站点清单/文档一致后再做);certs 命名仍不统一 —— config.py 期望 fullchain.pem/localhost.key,ssl_bootstrap.ensure_self_signed 默认 server.crt/server.key(sign_for() 默认才是 fullchain.pem),本版未改(不影响当前自签路径,已在 win_pack.md 记录);KVM 无声卡,TX 话音仍须物理 Windows 机验收;现场机真机复验(洁净安装 + 真实浏览器加载 UI + 两实例检测)在 v1.24.6 装完后执行,结果回填本条目。 |
| SDD V2.65 | 2026-10-02 | pi | v1.24.5 发布:缺证书就自己签一张(不再静默退回 HTTP),并在发布验收里抓住「修复本身在冻结包里是坏的」。 本版起因是现场报障「装完之后黑屏、不启动」:config.py 把证书指向打包目录(普通用户只读)且文件名是开发机上的 radio.vlsc.net.key —— 包里带的是一条在任何人机器上都不存在的路径;server.py 找不到证书就静默降级为纯 HTTP,而启动器打开的是 https:// ⇒ 浏览器报协议错误 ⇒ 界面一片黑。修法:证书目录改为用户可写的运行目录(Windows %LOCALAPPDATA%\MRRC-Modern\certs、macOS ~/Library/Application Support/…、Linux $XDG_DATA_HOME),缺证书时当场自签,纯 HTTP 只留给显式 --no-ssl;cert_reload_required 改按文件身份(路径+时间戳+大小)比较,且「启动时无证书、之后才有」也算需重启。② 发布验收抓到的阻断级缺陷(本版最重要的一条):server.py 调用 ssl_bootstrap.sign_for() 却从未 import ssl_bootstrap ⇒ 冻结包里每次启动都是 NameError,被那段宽泛的 except Exception 吞成一条 error 日志后照样退回纯 HTTP —— 也就是本版要修的黑屏原封不动地留在了发布包里。它通过了所有既有门禁:VM 上 1460 项测试 OK、三个 PyInstaller 目标、iscc Successful compile、version.txt=1.24.5、SHA-256 在 VM/宿主/Mac 三处一致;模块本来就在 mrrc_modern_server.spec 的 hiddenimports 里(在包里,只是没被 import),而启动器自带一份能用的同款逻辑(windows/launcher.py:20),所以装好的应用看起来是健康的。唯一暴露它的手段是拿发布产物本身跑一次洁净室:以干净的 LOCALAPPDATA + 不存在的证书路径启动打包出的 MRRC-Modern-Server.exe,日志一行即定性。③ 修法与守卫:补 import,并把这段逻辑从 main() 里抽成 _resolve_ssl_kwargs()(内联在 main() 时测试根本够不着),钉住四条契约 —— 缺证书必须签出来(除非 --no-ssl,否则绝不返回 {})、通配绑定地址要签给 localhost(签给 0.0.0.0 与启动器要打开的 https://127.0.0.1 不匹配)、既有证书(运维自装或 hub 登记的)绝不替换、--no-ssl 是唯一的纯 HTTP 入口。套件 macOS 1463→1467、Windows 1460→1464。④ 这是同一文件里第二次出现同一类缺陷 —— V2.64 ① 记的正是「server.py 从未导入 cloud_hub」⇒ 打包版按「申请」即 500。两次都是「模块在包里、名字没绑定」,且都被宽 except 或 SPA 兜底掩盖。建议的常设守卫:对入口模块跑一遍未定义名检查(pyflakes/ruff);本次未加,因为 venv 里没有该依赖,而改 requirements-build.txt 会迫使 VM 重装依赖(windows-installer 陷阱 8)—— 发版中途不引入新构建依赖。⑤ 另两处顺带修掉的真实缺陷:config.default_user_dir() 在被清空的环境下会抛(tests/test_config.py 用 patch.dict(clear=True) reload 本模块,Windows 上 Path.home() 抛 RuntimeError: Could not determine home directory,已在 VM 实测复现)⇒ 改为直接解析 LOCALAPPDATA/USERPROFILE、POSIX 分支保留 expanduser、兜底 <repo>/user-data,路径助手不该让启动失败;tests/test_ssl_bootstrap.py 用了 unittest.mock 却没显式 import,只在整套跑时靠别的模块顺带导入才不报(单跑即 AttributeError)。⑥ 产物(均为修复后重建;修复前的 Windows 54,085,618 B 935a7ed9… 与 macOS 62,260,761 B c67ce53d… 已被取代,且从未上传):Windows MRRC-Modern-v1.24.5-Windows-x64-Setup.exe 54,087,234 bytes 28ac7743…(完整 28ac7743da7f5cc75dbecbb7b8ea2d373cb4c0eb2fd1be52871206d9efd80bae);macOS MRRC-Modern-v1.24.5-arm64.dmg 62,267,377 bytes 039106e5…(完整 039106e5b14ec7c9baedff6fe4e50bda4e521e459743e1bb338871b6861f3b2f);rpi64 镜像未重建。⑦ 验证:两端产物各跑一遍洁净室验收 —— 无证书启动 ⇒ 日志 signed a self-signed certificate + SSL enabled with a self-signed certificate just generated + Uvicorn running on https://…,HTTPS /login 200(修复前是 could not create a certificate (name 'ssl_bootstrap' is not defined) + open http://… (NOT https://),纯 HTTP 200);证书 CN/SAN 与绑定名一致;Windows 侧 VM 已装产物的 Server/Launcher 哈希与 dist 逐字节相同,fleet payload 完整(frpc.exe + openssl.exe + 9 个 DLL + install_instance_tunnel.ps1 + openssl.cnf);活实例公网入口 https://bg9aaa.mrrc.vlsc.net/ → 302、/login → 200;release_check.py 离线 29 ok / 0 failing(两张下载卡片的 bytes/SHA 均对真产物校验通过)。⑧ 站点数字曾被全局替换污染(本轮逐区块修正,每个哈希都能归因到真实产物):Windows 卡片挂着 v1.24.4 的 bytes、完整 SHA 竟是「v1.24.4 前 8 位 + v1.24.3 尾巴」拼出的假哈希、zh 的 macOS 卡片挂着 Windows 的字节数、WINDOWS_INSTALLER_GUIDE.md 的 v1.24.5 行挂着 v1.21.0 的 SHA、归档句还指向站上早已不存在的 v1.14.2。边界:KVM 无声卡,TX 话音仍须物理 Windows 机验收;VM 未接电台,CAT/音频设备行为未验证;VM 上已安装的仍是修复前那次构建(启动器路径可用,故 bg9aaa 入口健康),重装可选;观测到 VM 上有两个 frpc 用同一份配置在跑,日志每 ~33 s 刷 proxy [bg9aaa] already exists(两个 Launcher 进程同秒启动)—— 属独立缺陷,本版未修。 |
| SDD V2.64 | 2026-10-02 | pi | v1.24.1 发布:接入云端的三个缺陷一次修完(客户端两处 + portal 一处),并钉掉边缘→hub 的 IPv6 黑洞。 ① 客户端 server.py 从未导入 cloud_hub(refresh 路径还引用了只绑定裸名的 config.WEB_PORT)⇒ 打包版按下「申请」即 500 NameError;单测没拦住,是因为 test_cloud_hub.py 自己就 import 了那个模块 —— 那行 import 恰好是 server 缺的。新增 test_cloud_endpoints.py 直接驱动两个端点,并把 cloud_hub 写进 mrrc_modern_server.spec 的 hiddenimports。② GET /api/cloud/state 被 SPA 兜底吞掉(@app.get("/{path:path}") 注册在云端点之前 ⇒ 200 + index.html)⇒ 设置对话框读不到自己的状态;改为在所有 API 路由之后用 app.add_api_route 注册兜底,并加两条路由顺序守卫(通用那条对任意 /api/ 路由生效)。这一条是「冻结包真跑」抓到的,端点级单测看不见。③ hub 侧 POST /apply 从不交出申请令牌(hub SDD V0.18;其测试直读 store,绕过了唯一真实的取令牌路径)⇒ 应用拿不到令牌、/status 无从开始;修好并部署,公网实测 32 字符令牌 + 200 applied + 错令牌 403。默认入口改回 hub 主路 portal.mrrc.vlsc.net:8899(实测 3/3 通、0.30–1.40 s),海外边缘降为退化路,且只对「没送达」的失败换路(DNS/TCP/TLS/超时/502/504),portal 答复过就绝不重发。边缘→hub 的 IPv6 黑洞(真凶):www.vlsc.net 与 *.mrrc.vlsc.net 加上 AAAA 后,边缘 nginx 两处 proxy_pass 仍是裸主机名(启动时解析一次,而本机 getaddrinfo 把 AAAA 排前面)⇒ 任何一次 reload 都会把边缘路径切到 hub 不可达的 IPv6(实测 0/4 全超时;同机 v4 4/4 通,且边缘自身 v6 正常 ⇒ 坏在 hub 的 v6 入站)。已钉为 hub 的 IPv4 并 reload:复测 portal 路径 8/8 200(修前 3/6 挂死)—— 顺带给 §12.8 那条「边缘间歇无响应」补上了根因与判据。产物:macOS MRRC-Modern-v1.24.1-arm64.dmg 62,257,332 bytes 137de9f2…;Windows MRRC-Modern-v1.24.1-Windows-x64-Setup.exe 54,080,541 bytes 46323320…;发布时的 latest.json 里 previous 应指向 v1.23.3,不指向 v1.24.0(它的接入云端是已知坏版本);本轮按指示只完成构建与产物级验证,未上传、未重生成 latest.json、未部署站点、未打 tag。验证:本机套件 1461 项全绿;VM 门禁 1457 项 OK(10 skip);release_check.py 离线 0 failing;两端产物均走查字节码(server 入口引用 cloud_hub 与 add_api_route,PYZ 内含 cloud_hub)并在冻结包真跑里跑通「申请 → 令牌落盘 → 状态 applied」(macOS 与 Windows 各一遍);version.txt = 1.24.1。边界:批准之后的自动接好(grant → 证书登记 → 隧道)仍待真实租户机复测;旧实例(bg1sb)仍指向上游证书名 radio.vlsc.net;rpi 镜像仍为 v1.24.0(未重建);KVM 无声卡,TX 话音仍须物理 Windows 机验收。 |
| SDD V2.63 | 2026-09-30 | pi | PTT 半开释放:TX 期间会话活性闸门(§15.6 由”计划”转为已实现,opt-in)。 背景:V2.62 记下的缺口——dead-man switch 只在真正断线时触发,而半开连接(NAT 掉表、换 Wi-Fi、静默丢弃)没有断线可观测;唯一的兜底 MRRC_PTT_MAX_TX_SECONDS 出厂关闭。LAN 下这是边缘情况,经 NAT 远程访问时它是主路径(hub AD-H06/NFR-H006)。机制:新增 MRRC_REMOTE_SESSION_TX_HEARTBEAT_S(默认 0 = 关闭,由 Cloud Hub 显式开启);服务端 _tx_liveness_timeout() 决策 + _tx_liveness_watchdog()(tick 0.25 s,检测延迟 ≤ 阈值 + 1 tick,落在 hub 契约的 ≤1.5 s 内);浏览器在 PTT/TUNE 期间每 500 ms 发 {type:"txhb"}(ptt_manager.js;起于 pttStart/tuneStart、止于 pttEnd/tuneEnd/forceRX)。三个”不释放”是故意的负例:①未声明能力的会话永不被门控——发 txhb 本身就是能力声明,因此旧原生客户端与自带 watchdog 的 LAN 客户端不受影响(这是”只改服务端闸门”会踩的坑:老客户端不心跳就会被误杀);②只有持键会话超时才释放,陈旧 Listener 不得掐掉别人的载波(复用 Layer 4 key-owner 仲裁);③刚连上就按键、尚未发过心跳的会话不算超时。释放走既有 fire-and-forget 路径(单次 set_ptt(False) + 清零 TX 表 + 一条说明性提示给控制面),无 verify 循环。缓存版本:modules/ptt_manager.js?v=15、sw mrrc-v39(test_audio.py/test_server_ws_protocol.py/test_ws_token_transport.py 三处 pin 同步重钉)。测试 1414→1435(+21:真跑 async 循环 + 假 cat/radio,覆盖释放、单写、表清零、提示、仲裁、默认关闭、阈值决策边界与源码守卫)。实现期由测试抓到的两处:浮点边界竞态(now-last 恰等于阈值时被 µs 抖动越过,改为”舒适区间 + 明确越界”两条)与我自己数错的调用次数(stopTXHeartbeat() 4 处:3 个释放路径 + start 内部的幂等停止,防重叠按键堆计时器)。文档:AD-007 修订(V2.63 amendment)、§15.6、AGENTS.md env、README env + PTT 段、tests/README 计数与模块小节。未做/边界:默认仍为关闭(提出需求的是 Hub,而误释放会打断真实 QSO,故由 Hub 显式开启);Cloud Hub 侧尚无隧道,TX_ABORT 与 hub §15.5 的 V1–V10 全矩阵实测(尤其 V5 杀 Tunnel 节点、V6 停整个 Hub、V7 深度半开)须在 Hub 试点时在实例上复跑;浏览器实测同样待做。 |
| SDD V2.62 | 2026-09-30 | pi | 远程接入前置项两件:会话遥测(AD-023)+ 会话令牌不再进 URL(AD-024)。 背景:Cloud Hub 设计评审(mrrc_hub/SDD V0.1,取证见该仓 docs/2026-09-30-fleet-hub-design-review.md)把两项列为 MVP 前置 —— ①扇出决策缺实测并发(hub I-H1);②前端把 30 天令牌拼进 ?token=,而实例跑 uvicorn 默认访问日志(含 query string),项目自己早已记录该问题(support_bundle.py 脱敏正则注释点名 “a ?token= inside a URL”),但脱敏只在导出诊断包时发生。① session_metrics.py(AD-023):纯标准库、不引应用模块;按 (role, kind) 计在活 socket、按 token 计会话数(一个人三个 socket 仍是一个人)、生命期与滑动窗口并发峰值、spectrum/audio_rx 两路上行字节。刻意范围:控制面文本帧不计量(<10 kbps,对比频谱 408 kbps);token 只作模块内部不透明键,绝不进入 snapshot()/report()(测试直接断言渲染结果不含令牌串,沿用 AD-021/NFR-068 的隐私边界),因此可安全默认开启并暴露在 GET /api/session_metrics;另有每 MRRC_SESSION_METRICS_INTERVAL_S(默认 300 s,0=关闭)一条 Session metrics: INFO 行,含区间平均 kbps。实现期由测试抓到的三处:双重关闭必须容忍下溢(清理既走正常返回也走异常路径)、窗口内无采样时窗口峰值取当前值(绝不低于当前)、以及我自己写错的峰值用例(开了三个会话没关就断言”只剩一个”)。② 令牌传输(AD-024):_token_from_ws_handshake() / _token_from_request() 统一解析,顺序 Authorization: Bearer(原生客户端)→ mrrc_auth Cookie(浏览器,同源握手自动携带 —— 这推翻了”浏览器 WS 无法带头因此必须放 URL”的旧注释)→ ?token=(弃用,仍可用,每进程一条 WARNING 点名迁移方式)。前端五个构造点全部停拼 token(ft710_main.js ×2、listen.js ×2、modules/atr1000.js ×1 —— 后者是首轮只扫 static/*.js 漏掉的子目录第 6 个端点)。原生 iOS/Android 客户端仍用 ?token=,故保留 query 兜底而非直接删除(删了它们当场断连),但让每次使用都成为可观测事件。缓存版本:ft710_main.js?v=35、listen.js?v=14、modules/atr1000.js?v=3(顺带修掉 index.html 长期钉 v1 而 sw.js 预缓存 v2 的漂移)、sw mrrc-v38;test_server_ws_protocol.py / test_audio.py / test_atr1000_server.py 的 cache-bust 断言同步重钉。测试 1369→1414(+45:会话遥测 27 = 逻辑 20 + 接线守卫 7;令牌传输 18;另修 test_listen_only.py 的 _FakeRequest/_FakeWS 替身缺 headers —— 真实 Starlette 对象必然有,是替身不完整而非产品代码该放宽)。文档:AD-023/AD-024、§10.2 AuthService 行、§12.5.3 遥测小节、AGENTS.md 模块表 + 安全段、README WS 鉴权段 + env 表 + REST 表、tests/README 计数与模块小节、ws-endpoint-auth 约束规则改写(原文要求 ?token=,已过时)。未做/边界:Cookie 仍是 30 天(会话时长属 hub AD-H09,本次不动);原生客户端迁移未实施;?token= 的移除未排期;浏览器侧验收待做 —— 本次对令牌传输的验证是单元级(解析优先级、弃用告警、接线与前端源码守卫),尚未在真实浏览器 + 登录态下确认五个 WS 端点全部连上(预期:页面能加载即说明 Cookie 在,同源握手必然携带;若某端 4001,客户端会回到登录态而不是静默半连)。这一点在 Cloud Hub 试点前必须实测。 |
| SDD V2.61 | 2026-09-25 | Kimi | v1.21.0 发布:iPhone 开机锁屏竞态修复 + 收听页增强(FFT 迹线 / 频率步进 / 公网缓冲 / DNS 后端)。 能力实现:①开机时 iOS 系统级麦克风权限框遮挡页面并释放刚拿到的屏幕锁,无手势的自动重申请在 iOS 上必被拒(开机 ~30 s 仍黑屏、手动点 ☀ 才稳住)——activateAudioSession() 在 finally 用残余激活窗口重申请 + 新增 window focus 兜底 + WakeLockMgr.enable() 幂等化(static/ft710_main.js,?v=33 / sw mrrc-v36);②收听页:瀑布上方 FFT 迹线(网格 + EMA 平滑 + 琥珀迹线)、频率步进钮(1x/5x,10 Hz–25 kHz)、公网抖动缓冲 500/250/1500 ms、公网代理后端改 DNS 名 radio.vlsc.net(每 location resolver 300 s,家中 IPv6 变化自愈,已部署)。产物:macOS MRRC-Modern-v1.21.0-arm64.dmg 55,933,998 bytes df833cdf…(完整哈希 df833cdf93e71df65e1d460466db224b19f32d62b715a761dd7c7dd98997848c);Windows MRRC-Modern-v1.21.0-Windows-x64-Setup.exe 46,014,388 bytes ccb6e26c…(ccb6e26cfea431fdc6a9924b2befbf4c4f086cf3adcf12de7444ee09c626993a);rpi64 镜像仍为 v1.17.0(本次未重建)。发布前验证:本机套件 1361 项全绿(1 skip)、VM 门禁 1358 项 OK(9 skip,平台相关);release_check.py 离线 29 ok / 0 failing;macOS:codesign --verify --deep --strict 通过、Frameworks -> Resources 链接在、version.txt=1.21.0、DMG 经典布局(app + Applications -> /Applications)、冻结服务双栈 HTTPS(IPv4/IPv6 均 401、明文 000)+ 首次密码横幅正常;Windows:version.txt=1.21.0、FTDI DLL / static/listen.js 在包内、本版改动全在前端资产故直接核验包内内容(ft710_main.js 含 focus 事件重申请、listen.html 引用 listen.js?v=13、sw.js = mrrc-v36;server.py 字节码自 v1.20.0 未变)、打包服务实跑 /api/health 401 + /login 200、VM Get-FileHash == 本机 shasum。构建期事故:首趟构建 ssh 在第三个 PyInstaller spec 中途被 ham 断开(Broken pipe),但产物时间戳/测试日志证实门禁与三个 spec + iscc 在断线前已全部完成 —— 再次印证只认产物不认 BUILD_DONE。边界:构建 VM 未接电台,TX 话音仍须物理 Windows 机验收;iOS 真机锁屏竞态修复待现场复核(预期:开机后不再黑屏);树莓派镜像未重建。 |
| SDD V2.60 | 2026-09-25 | pi | v1.20.0 发布:收听专用界面(listen-only 角色 + 独立收听密码)+ iPhone 主控锁屏修复(能力实现见 V2.59 条目)。 产物:macOS MRRC-Modern-v1.20.0-arm64.dmg 55,638,163 bytes a40269b3…(完整哈希 a40269b3ed7a4fa293c7e799b02566b9fd2f9b7f7cad35541e30294328c1eadd);Windows MRRC-Modern-v1.20.0-Windows-x64-Setup.exe 46,015,844 bytes b5b41eac…(b5b41eac8b642aa7b7c28ff6c0096124348929a0c4104aba82de4a1bf2424ed0);rpi64 镜像仍为 v1.17.0(本次未重建)。发布前验证:本机套件 1361 项全绿(1 skip)、VM 上同套件通过构建门禁;release_check.py 离线 29 ok / 0 failing;macOS:codesign --verify = valid on disk + satisfies its Designated Requirement、Contents/_CodeSignature/CodeResources 就位、Frameworks -> Resources 链接在、CFBundleShortVersionString=v1.20.0 与资源树 version.txt=1.20.0、NSMicrophoneUsageDescription 存在、FTDI 两个 dylib 在包内、DMG 为经典安装布局(含 Applications -> /Applications)、冻结服务双栈 HTTPS(IPv4/IPv6 均 401、明文 000)+ 首次密码横幅正常 + /listen 无 token 时 302 到登录页、走查包内代码对象确认本版新符号真的在包里(_listen_tokens/LISTEN_ONLY_MESSAGE/LISTEN_ALLOWED_SET_FIELDS/_listen_spectrum_clients/LISTEN_SPECTRUM_DIVIDER/_spectrum_frame_due);Windows:version.txt=1.20.0、FTDI DLL / static/index.html / static/listen.js / mem_channels.json / opus.dll 均在包内、走查 server 入口得同一组 listen 符号、PYZ 得 config/support_bundle/cq_player/atr1000_client/scope_handler/recorder、打包服务实跑(/api/health 401、/login 200)、VM Get-FileHash == 本机 shasum。发布流程观察:版本一致性测试(test_release_artifacts.RepositoryStateTests)在构建门禁内执行,因此所有版本串必须先于构建改完(尺寸/SHA 等构建后再填,artifact_facts 规则在产物不存在时自动跳过)——先构建会因 14 条版本规则红而停。边界:构建 VM 本次未接电台(只有 COM1,无 USB 音频),且 KVM 等时 OUT 限制使 TX 话音仍须物理 Windows 机验收;listen 角色真机端到端(访客用收听密码听一次真实 QSO)与 iOS 真机锁屏/音频会话修复待现场复核;树莓派镜像未重建。 |
| SDD V2.59 | 2026-09-25 | Kimi | 收听专用界面(listen-only 角色 + 独立收听密码)。 需求:给访客开一个「只能听」的入口 —— 可以调整频率和模式、听音频、看频谱,但绝不能发射、不能改任何设备设置,且密码与操作员密码分开。新增 MRRC_LISTEN_PASSWORD(默认空 = 不启用收听登录):用它登录得到带 listen 角色的 token(_auth_tokens 保持不变,_listen_tokens 标记角色),登录接口返回 role,登录页对 listen 角色跳转到新页面 /listen(static/listen.html + 自包含 static/listen.js:大频率显示、直接输频/频段快捷键、模式按钮、记忆频道调用(memRecall 只写频率+模式)、S 表、频谱瀑布、RX 音频 + 浏览器端音量;复用 rx_worklet_processor.js 与 WASM Opus 解码,不加载两个受 tooling-guard 保护的 monolith JS)。管控全部在服务端(UI 隐藏从来不是执行点):_handle_ws_message 角色门只放行 ping/get/memLoadAll/memRecall 与 set{freq,vfo_a_freq,vfo_b_freq,mode},其余(含 legacy 冒号格式、memSave/memDelete、recording/cq/ptt/tune/全部设备设置)回 LISTEN_ONLY_MESSAGE 错误;/WSaudioTX 与 /WSatr1000 对 listen token 直接 close 4003(拿不到麦克上行与天调继电器命令);auth 中间件对 listen 角色的非 GET /api/*(logout 除外)回 403,REST 只读。口令比对走 hmac.compare_digest(I9 同款棘轮测试钉住);两个密码相同时按 admin 处理(操作员设成一样时期望全控而非受限)。测试 1330→1356(+26:口令匹配 3、登录角色 7、WS 门 7、端点门 5、代理契约 4);sdd_context check 无新增告警,全套绿。公网映射(同日,应操作员要求):https://www.vlsc.net/mrrc_modern/listen 经 www 主机 nginx 直连反向代理到电台机(无 SSH 隧道)。后端先用 IPv6 字面量上线,当晚切换为 DNS 名 radio.vlsc.net:8888(AAAA)——操作员新建 AAAA 记录时旧字面量的前缀已经变了(字面量方案当场失效的活例证);nginx 侧用 proxy_pass 变量 + 每个 location 的 resolver 1.1.1.1 8.8.8.8 valid=300s,家中 IPv6 再变最多 300 s 自动重解析,无需 reload。listen 页面改为前缀感知(URL_BASE 从 location.pathname 推导,直连 /listen 与代理 /mrrc_modern/listen 同一份代码;login 内联 JS 与静态资源全部相对路径),新增 deploy_listen_proxy.sh(幂等 marker 插入,模式同 deploy_support_receiver.sh)。实施期两个真教训:① www 站点既有正则 location(静态资源 \\.(css | js | …)$ 本地规则)会盖过普通前缀匹配,把 /mrrc_modern/modules/*.js 截成本地 404 —— 三个前缀 location 必须 ^~(该配置文件自己的注释也写着这个惯例);② 提取旧块的正则没匹配^~形式导致重复插入、nginx -t直接挂掉 —— 脚本改为删除全部旧块再插入唯一新块,并有结构测试钉住。边界:经代理的登录限流按来源 IP 聚合(电台机看到的是 www 主机地址);收听密码由操作员自管(.env MRRC_LISTEN_PASSWORD)。 |
iOS 收听(同日晚补):iPhone Safari 首次进页有点击「▶ 开始收听」手势激活 AudioContext(自动播放策略),并补上与主 UI 同款的 ScriptProcessor 回退(AudioWorklet 在 iOS 上不可用时 60 ms 预缓冲 / 300 ms 上限,此前回退缺失会导致苹果浏览器完全无声)。iOS 三连坑(现场逐一定位):① Web Audio 挂在 ambient 音频会话上整体哑掉 —— 操作员现场报告「主界面按 PTT 后才有声」,根因即 getUserMedia 把会话切到 PlayAndRecord 的副作用;收听页照此在启动时请求并立即释放麦克风(mic✓ 诊断可见,不录音);② 首版把激活 await 在音频链第一步,而弹窗在内嵌 webview 永不落定 → 音频 WS 永不连接(日志里控制/频谱在、WSaudioRX 缺席),改并行 + 10 s 超时;③ 锁屏断音 —— 移植 legacy mrrc mobile_modern.js的 Screen Wake Lock(手势申请 + visibilitychange 重申请,诊断行 🔒)。页面加底部诊断行(ctx 状态/帧计数/解码电平 lvl/缓冲 ms)与「测试声音」按钮(<audio> 会话激活 + 同链路 880Hz 测试音),把「没声音」一刀切成输出通路/流内容/解码三段。一屏紧凑布局(100dvh、瀑布固定 clamp(90px,16dvh,150px) 高度、波段/模式/记忆芯片横排,参照收发主界面);记忆频道 3×2 网格全显;页脚在线人数 —— 服务端新增 onlineUsers 广播(/WSradio 每次连接/断开推送 len(ctrl_clients),其它 UI 忽略未知类型);收听角色频谱降帧(_spectrum_frame_due每 3 帧发 1 帧,30 Hz→~10 Hz,省约 2/3 频谱流量,操作员要求);页脚网络指示(三通道接收字节速率 ↓KB/s + /WSradio ping/pong RTT ms,每秒刷新,操作员要求);公网抖动缓冲(收听页 RX jitter buffer 由主界面 LAN 取向的 120/300 ms 加大到 500/250/1500 ms,ScriptProcessor 回退 400 ms/1.5 s —— 公网链路 RTT/抖动远超局域网,紧凑缓冲表现为卡顿,收听场景不在意亚秒级延迟,操作员现场反馈);频率步进(移植主界面的预设步进模式:◀/▶ 单步、◀◀/▶▶ 五倍,步进钮循环 10Hz–25kHz,30kHz–75MHz 边界内);FFT 迹线(瀑布上方 42% 区域新增 #fft-canvas,简化版 renderFFTPlot:网格 + EMA(0.3) 平滑 + 琥珀迹线与渐变填充,无频率刻度——收听角色本就不能改 span)。listen.js?v=13。测试 1330→1361(+31:口令 3、登录 7、WS 门 7、端点门 8、代理/页面契约 6)。文档:§9.2 角色说明 + onlineUsers + 降帧、AGENTS.md、README env 表、tests/README、CHANGELOG。 |
|||
| SDD V2.58 | 2026-09-23 | Claude | v1.19.0 发布:ATR-1000 驻波超阈值自动调谐(能力实现见 V2.57 条目)。 产物:macOS MRRC-Modern-v1.19.0-arm64.dmg 55,654,218 bytes 40d8a15b…(完整哈希 40d8a15b739d156db7c015a95b206e98ec247f71fa08ce63a34d5c7a56af3886);Windows MRRC-Modern-v1.19.0-Windows-x64-Setup.exe 46,002,941 bytes 7b9cabca…(7b9cabcaab51ecdcafc29ff10f6ca1f28339e055e4fe4bd825af1d4e96c4cd40);rpi64 镜像仍为 v1.17.0(本次未重建)。发布前验证:套件本机 1330 项全绿、VM 上同套件绿;release_check.py 离线 29 ok / 0 failing;macOS:codesign --verify = valid on disk + satisfies Designated Requirement、Frameworks -> Resources 与 MacOS/_internal -> ../Resources 两条链接就位、CFBundleShortVersionString=1.19.0 + version.txt=1.19.0、NSMicrophoneUsageDescription 存在、FTDI dylib 在包内、DMG 含 Applications 链接、冻结服务双栈 HTTPS(IPv4/IPv6 均 401、明文 000)+ 首次密码横幅正常、走查 PYZ 确认新代码在包内(SWR_RETUNE_THRESHOLD/_check_swr_retune/_finish_auto_tune/auto_aborted);Windows:version.txt=1.19.0、FTDI DLL / static/ / mem_channels.json / static/modules/atr1000.js 均在包内、走查 server 入口得 _on_atr_tune_event、PYZ 得 atr1000_client 与新常量、VM Get-FileHash == 本机 shasum;latest.json 由 dev_tools/make_latest_json.py 从真实产物重生(installer 1.19.0 / previous 1.18.1)。发布期两个现场事故:① 构建 VM 的 guest 卡死 —— libvirt 报 running、端口 22 可连但 sshd 无法创建任何进程(exec request failed on channel 0,ACPI shutdown 也无响应,积屏为黑屏),virsh destroy + start 后恢复(宿主可用内存 6.2 GB,未触发 gotcha 9 的 OOM);② VM 上未安装 git,而 2026-09-19 随 FDE 看板引入的 RepoCleanTests 在 setUp 里直接调 git init → Windows 构建门禁挂 3 项(WinError 2)。修法按仓库“按可用工具跳过”的约定:该类加 setUpClass 无 git 则 SkipTest,tearDown 的 rm -rf 换 shutil.rmtree(POSIX 命令在 Windows 不存在)。边界:ATR-1000 自动调谐的真机验收(FT-710 + 天调 + 大驻波天线)待现场;树莓派镜像未重建;Windows 包在 KVM 上构建,TX 话音仍需物理 Windows 机验收。 |
| SDD V2.57 | 2026-09-23 | Claude | ATR-1000 驻波超阈值自动完整调谐 + QRP 学习门限(对齐兄弟项目 mrrc V5.8.0/V5.8.5)。 现场需求:大驻波天线上发射时天调不会自己动手,操作员必须停下手动按 TUNE;学习库也只在服务端知道 TX 时才更新。新增高驻波守卫(atr1000_client.py,实例状态、无模块全局、无锁):操作者已在发射(实测功率 ≥5 W)且 SWR 严格 >2.0 持续 ≥1.5 s → 向天调发一帧完整调谐 FF 04 01 02(mode=2),30 s 冷却;同一频点连续 3 次无改善则 auto_giveup 放弃,直到频率变化或 SWR 回落;频率变化 >1 kHz 或继电器刚变(1 s 忽略窗)重启连续段;调谐期间(含手动 assist 的 TX2 载波)由既有 _tuning 标志互斥,不重复触发。绝不自行键控电台:守卫只发天调帧,模块内不存在 CAT/PTT 引用(源码护栏测试固化)——触发前提就是操作者自己的发射。调谐后写回:每条清调谐路径(relay stable >5 s / same-relay confirm >1.5 s / TX 结束 / 45 s 超时 / 设备显式 TUNE_STATUS=0)首次挂接 0.8 s 后的比对,仅当 SWR 改善 ≥0.02 且终值 ≤1.8 才 learn(force_update=True) 写回 atr1000_tuner.json;不回滚继电器(语音 QSO 中途回滚到已知差的匹配比调谐结果更糟);连接断开或帧发送失败发 auto_aborted 终态事件(否则前端进行态会永久卡在 ···)。学习门限:入口由「服务端 TX 信号」改为「实测功率 ≥3 W」(QRP 友好;idle 漏读约 1–2 W 不会误学),因此面板直发 PTT / 外部软件发射也会学习(兄弟项目 V5.8.5 根因 2 的同款修复)。前端:复用既有 atrTuneResult 通道(auto=true)+ 6 个 auto 阶段 toast(触发/成功/无改善/超时/已放弃/中断),进行期间 TUNE 按钮显示 ··· 并禁点;sw.js 缓存版本 atr1000.js?v=2。服务端:_on_atr_tune_event 转发到 /WSatr1000;手动 _start_atr_tune_assist() 增加 read_state()["tuning"] 前置检查(关掉轮询 tx_status ≤500 ms 滞后的竞态窗口)。实现期由测试抓到的两个真缺陷:① 守卫原判定 if self._swr_high_freq and abs(...) 在首次见到频率时(_swr_high_freq == 0)不重置连续段,旧连续段会跨 QSY 触发;② LearningBufferTests 把旧门限写死为字面量 4.9/5.0 W,门限降到 3 W 后会静默失效(已改为从 LEARN_MIN_POWER 派生)。套件 1299→1330(+31:守卫 13、比对与 worker 9、学习门限净增 2、服务端 5、前端护栏 2);AGENTS.md/tests/README.md/CHANGELOG.md 同步。边界:真机(FT-710 + ATR-1000 + 大驻波天线)端到端验收待现场执行 —— 重点是发射中被吃掉的一小段体感是否可接受、以及写回后二次访问该频点是否省掉整次调谐;_retune_fail_count 只活在进程内,重启即清零(刻意不持久化)。 |
| SDD V2.56 | 2026-09-19 | Claude | FDE 看板:支持回路的自我迭代成为常设实践(loop practice 4)。 需求:支持回路只有「分析→答复」,分类出的 bug/feature 没有去处,人每次都要手动挑、手动改、手动收尾。新增 support_board.py(数据模型/渲染/护栏,stdlib-only)与 dev_tools/fde_board.py(排期决策 CLI):pi 分析契约加可选 kind(bug/feature/noise)+severity+title 字段(向后兼容,旧输出无字段则按 needs_code_change 回退 bug、否则 noise),每条分析自动入板;分类即决策:noise → 已答复闭环(答复页已回、无代码工作)、bug → 自动排期进后台实施队列、feature → 待决策(操作员 fde_board.py decide 排期/拒绝);未真正答复(need_more_info)的条目不允许闭环,留在待决策跟进;cron 新增 --implement:对已排期条目单飞实施——pi 只读定位后以 unified-diff JSON 契约返回补丁(模型无写权限),护栏拒绝(工具护栏 JS/certs/packaging/>8 文件)→ git apply --check → 仅在独立 fde/<编号>分支应用,全套 unittest 绿才 commit,红则回滚分支;main 永不被无人值守进程触碰;no_action 判定自动进 Rejected。看板页 <www.vlsc.net/mrrc_modern/board/> 与答复页互链,同 redact_public 隐私过滤;cron 行升级为 --once --publish --implement(幂等重装)。事故修复:cron 里 pi 连续两天 SIGABRT(Homebrew 升级 llhttp 打断 bottled node 的 dylib 链接)——run_pi 信号死亡时日志点名成因与修复;测试隔离补 BOARD_STATE_FILE/BOARD_PAGE_PATH(fixture 曾污染真实看板)。套件 1276→1297(+21 看板测试);release_check 0 failing。边界:fde/ 分支的人工合入与真实 bug 端到端(上报→排期→自动实施→合入)待首次真实排期;实施 pi 的探查预算与 diff 质量待真实样本校准。 |
| SDD V2.55 | 2026-09-18 | Claude | PTT 释放→RX 声音恢复延迟优化(现场体感「滞后太长」,实测日志分解根因)。 现场日志证据:每次 restart_rx(reopening capture stream → RX audio started)稳定 262–300 ms(9 样本无一例外),加上 stop_tx 排空 ~70–130 ms、unkey 写可能排在轮询事务后(POLL_TIMEOUT 0.25 s)、以及 TX 期间服务端照发的静音 Opus 帧在前端 jitter buffer 里积压 ~120 ms(unkey 后必须先排空才听到真声音),合计 ~470–870 ms。三项修复(独立提交):① skip_next_poll("tx_status") 提前到 release drain+unkey 之前(poll-stale-guard 本就要求 skip 先于 CAT 写;顺带消除 unkey 后 in-flight 轮询回读 stale PTT=1 翻回 UI 的竞态);② _audio_rx_loop 在 radio.is_transmitting 时跳过 encode+fan-out(读取/录音 tap/静音 watchdog 保留 —— 半双工电台 TX 期间 USB RX 输出是位精确静音,广播它只会在每个客户端 jitter buffer 里堆静音),TX 结束后客户端处于 recovery gate(60 ms)待命,新帧一到立即起播,顺带省 TX 期间编码 CPU;③ start_rx 记录上次成功 open 的 (index, name) hint,TX→RX 重开时跳过 _find_rx_device 全设备列表扫描(≤4 层枚举,是 ~270 ms 的大头),hint 失效(USB 重枚举改名/换索引)自动回退全扫描;重开 settle 50→20 ms(stop_tx 已先睡 20 ms)。保留不动:AD-011 V2.14 的 capture reopen workaround 本身、stop_tx 的语音尾部 drain(≤50 ms + Pa_StopStream 排空)、ptt-release-no-verify(不加 verify 循环)。测试 1271→1276(+5:TX 静音不广播 1、设备 hint 3、settle 1)。边界:端到端延迟改善需真机验收 —— 用日志 TX session: → RX audio started 间隔与浏览器 window.__jitterMs 复核,预期 ~500–700 ms → ~250–350 ms;「TX 期间预重开捕获流」(把 270 ms 藏进发射时间)因 V2.33 流退化风险(playback 打开时 capture 重开可能立即再退化)未实施,如需要可做成 env 开关实验。文档:SDD/14 版本历史、AGENTS.md/tests-README 测试计数。 |
| SDD V2.54 | 2026-09-18 | Claude | v1.18.1 补丁发布:macOS 装机两处致命修复 —— 签名(「已损坏」)与音频输入权限(RX 无声)。 两处都只在安装后暴露,且都不是电台/配置问题。①签名从未成功:Contents/_CodeSignature 始终缺失,codesign --verify 报 code has no resources but signature indicates they must be present,Gatekeeper 给出「已损坏,无法打开」(右键也绕不过去);根因是 codesign 把 Contents/MacOS、Contents/Frameworks 下的数据文件/目录当嵌套代码而拒绝签整个 bundle,而 build.sh 把这次失败写成警告(non-fatal)—— v1.17.0 的 DMG 实测同样坏,即该缺陷自首次发版起一直存在。修法:数据树放 Contents/Resources/(资源位置被封存而非被当代码检查),只保留 Frameworks -> Resources 与 MacOS/_internal -> ../Resources 两条符号链接;MacOS 内除可执行文件外不得有数据(mem_channels.json/version.txt/macos//vendor/ 已移入资源树,启动器新增 runtime_path() 先 app_dir() 再回退 _internal/);签名或校验失败即 exit 1,spctl 报 damaged 也中止(ad-hoc 下 rejected(无 Developer ID)是正常结果)。②音频输入权限缺失 → RX 静音:打包 Info.plist 无任何权限说明键,系统日志 tccd: Refusing authorization request for service kTCCServiceMicrophone … without NSMicrophoneUsageDescription key;macOS 把音频输入(电台 USB CODEC)归入麦克风权限类,缺键时 CoreAudio 照常让捕获流打开成功、却把缓冲填零 —— 日志只有 peak=0.0% 与误导性的”检查电台 AF 增益”。修法:Info.plist 加 NSMicrophoneUsageDescription(构建期断言缺键即中止),近静默警告在 frozen macOS 上改写系统设置路径。实测证据:修复包 codesign --verify = valid on disk + satisfies its Designated Requirement、_CodeSignature/CodeResources 存在、spctl 只报无 Developer ID、DMG 内外双重校验(内部应用 CFBundleShortVersionString=v1.18.1 + 麦克风键 + version.txt=1.18.1)、冻结服务 /api/update/check 自报 current=1.18.0(v1.18.0 包,同时证明更新通道与线上 latest.json 首次端到端打通)、替换安装后 tccd 由 Refusing … 变为 AUTHREQ_PROMPTING。产物:macOS MRRC-Modern-v1.18.1-arm64.dmg 55,643,537 bytes 931b312c…;Windows MRRC-Modern-v1.18.1-Windows-x64-Setup.exe 46,002,543 bytes 19b5cef2…。套件 1267→1271 项(含 10 项新增:启动器资源回退 6、macOS 权限提示 4)全绿;release_check.py 离线 29 ok / 0 failing。顺带修掉 harness 自身的编码缺陷:Windows VM 的 GBK 控制台下 ✓/·/✗ 让 reporter 抛 UnicodeEncodeError(把”规则违规”伪装成 traceback),改为 errors=backslashreplace。边界:麦克风授权需用户首次点「允许」,ad-hoc 签名下授权绑定当次构建 cdhash、今后每次升级都会重新询问(要免除需 Developer ID 签名);Windows 包在 KVM 上构建(无物理声卡,TX 话音仍待真机验收)。、今后每次升级都会重新询问(要免除需 Developer ID 签名);Windows 包在 KVM 上构建(无物理声卡,TX 话音仍待真机验收)。 发布后文档全面同步(同日):docs/MACOS_INSTALLER_GUIDE.md 由 58 行扩写为完整使用指南(下载校验 → 安装 → 两种 Gatekeeper 弹窗的分辨 → 麦克风权限及其「静默失败」症状 → 数据/日志位置 → 升级与回退 → 卸载含 tccutil → 14 行故障排查表 → 隐私边界 → 环境变量);并在 docs/OPERATION_GUIDE.md 纠正错误说法 —— 旧文档称「已损坏」也可用右键绕过,实为不可绕过(签名无效,必须换包),同时补麦克风权限步骤与 4 条 FAQ;README.md 更新两平台下载块(此前停在 v1.16.0 且 SHA 竟是 v1.17.0 的)并新增 macOS 安装节;mac_pack.md 验证步骤按新布局重写(路径、签名/版本/权限三项门禁,并明确不再用 xattr -dr 做冒烟前置 —— 它会掩盖签名是否真的有效,而这正是坏包一路发出的原因);win_pack.md 补包内 version.txt 与脚本入口走查两条硬证据,以及远程触发构建不能用 -NoNewWindow 的实测教训;SECURITY_GUIDE.md 新增桌面端权限与隐私边界;应用内「🐞 遇到问题」页加「先花 30 秒自查」三条(macOS 权限 / 旧包签名 / 电台连接);中英文首页下载卡片旁加首次启动注意事项;引导页已重新生成并部署。注:static/support.html 属应用内资源,随下一次构建到达用户;站点页面已部署并逐页 grep 验证。 |
| SDD V2.53 | 2026-09-17 | Claude | 支持链路第 3 期 slice 1:更新通道(只读检查 + 生成式清单 + 自证状态)。 upgrade_core.py(纯标准库):版本三元组比较(v 前缀/预发布后缀容忍、畸形排最旧)、latest.json 校验(缺字段/版本不一致/非 https/SHA 非 64 位/尺寸非正/previous 不更旧一律 ManifestError)、check() 与 check_quietly()(离线只报原因不抛),以及升级状态 schema:mark_install_started 写 installing + pendingInstall,confirm_boot(新版本) 才写 ok —— 测试直接断言旧版本无法宣称胜利。dev_tools/make_latest_json.py 从真实产物算 size/SHA-256(小于 1 KB 视为不是安装包而拒绝)、installer 版本必须等于 CHANGELOG 顶版本、并用与客户端同一套 parse_manifest 复验后才写盘。GET /api/update/check 走既有鉴权、只读、失败不 500。为什么分两片:下载/安装会修改运行中的机器,半成品升级器比没有升级器更糟,故 slice 2(下载→校验→发射门禁 423→交接→自证)单独一轮交付。套件 1226→1258 项全绿;release_check 29 ok / 0 failing。边界:无真实安装器参与,端到端升级未验证;latest.json 尚未生成(需三个安装包先重建)。 |
| SDD V2.52 | 2026-09-17 | Claude | 支持链路第 2 期:自动分诊与答复页(dev_tools/support_autopilot.py + support_answers.py)。 轮询接收端 → 解包 → 组装摘要(复用第 1 期的体检结论)→ pi(只读 read/grep/find/ls,仓库为 cwd)按固定 JSON 契约产出结论 → 渲染答复卡 → 默认只出草稿,--publish 才更新 website/answers/index.html + git commit + 单文件 rsync(全站部署是交互式的,不能进 cron);need_more_info 即使带 --publish 也不发布。护栏:幂等 state.json、单轮上限 2 条、pi 超时 540 s(env 可调)、半包跳过、单条失败不中断、--inspect 不调模型的预检、--install-cron 写绝对路径 + 显式 PATH。公开页隐私边界:redact_public 过滤邮箱/手机号/设备路径/串口/内网地址/本机与临时路径,用带敏感值的假包做负例测试。 真机冒烟抓到三个 mock 测不出的缺陷:①提示词模板写成 { digest } → str.format 读成键名 " digest ",第一次真调用即 KeyError;②pi 继承 stdin,父进程是管道时它等 EOF 直到超时(cron 传 /dev/null,兄弟仓因此从未暴露)→ 传 DEVNULL;③真摘要下模型在仓库里探查到超时 → 提示词加探查预算(≤5 次检索/≤6 个文件)+ 思考档位可调(默认 medium,实测 4m12s 跑完)。第一张真卡又暴露两处:标题取了 problem.txt 的 markdown 头「# 问题描述」(改为取首个非标题行)、macOS 临时路径 /var/folders/… 进了公开页(补进过滤)。端到端验收:真包 → 摘要 → pi → 契约 → 草稿卡,结论引用 server.py:_rec_enqueue(REC_QUEUE_MAX=200)与 recorder.py:383 的补静音逻辑,附操作员原始证据行;合成包与其接收端副本事后已删除。套件 1224 项全绿(含 21 项答复逻辑、17 项 CLI)。边界:只在合成包上验证过结论质量;cron 连续运行与真实用户包待观察;答复页首次上线需一次站点部署(rsync 只发布该页)。 |
| SDD V2.51 | 2026-09-17 | Claude | 支持诊断包与上报闭环(支持链路 1/4)。 需求:问题上报长期只有现象描述,第一轮往返都花在索取证据上;而打包版根本没有服务端日志文件(logging.basicConfig 只进控制台)。选型对比四条路(浏览器构建/第三方日志服务/复用兄弟项目接收端/服务端构建+白名单脱敏+独立实例),采纳服务端构建:support_bundle.py(纯标准库、无应用依赖、可热修)按 env 键白名单裁剪配置 + 键值正则二次清洗(\b 在 _PASSWORD 上不成立,改用负向后顾;替换计数写进 manifest),收集日志尾部(≤2 MB/行对齐、按 512 KB/128 KB 逐级降级以免超接收端 20 MB 上限)、环境/电台/音频快照(含 host API —— 2026-09-10 的 WDM-KS 重名坑)与浏览器侧上下文,产出 diagnostics/summary.txt 自动体检结论(匹配本仓真实日志行:Server ready!/Configured audio device/RX open failed/Device not configured/S-meter fallback/Recording writer is falling behind/enable TX after)。日志持久化是前置:新增 MRRC_LOG_DIR 轮转 server.log(2 MB×2) + 启动器 tee server-stdout.log(只在启动窗口写文件,之后仅排空管道——退出读循环会让服务端因 64 KB 管道写满而阻塞或 EPIPE);install.sh 的 systemd 与 launchd 重定向改名以免与 handler 双写同名文件。独立接收端:vendor 兄弟项目 262 行标准库服务(三处本地改动:端口 8098、存储 /var/www/support-modern、meta 记 product),deploy_support_receiver.sh 幂等部署 unit + nginx /mrrc_modern/support/,并自检本地/公网 /api/list 无口令必须 401。入口:SPA 菜单纯 <a target="_blank">(ft710_ui.js 只绑定 [data-action],两个受护栏保护的文件零改动)+ static/support.html;/api/support/{bundle,upload,save} 走既有 auth 中间件,单飞构建、保留最近 5 包,断网时「只保存到本地」等价可用。规则引擎抓到实现期三个真缺陷:backend.capabilities 是 @property(写成调用会被 per-block try 吞成 {'error':…},已补驱动真实代码路径的守卫测试)、测试桩类体内自引用自身类名、以及 vendor 的模块名 server 与本仓 server.py 同名(测试改为按路径 importlib 加载)。真机验收:接收端已上线;端到端跑通「生成(2096 B、2 处脱敏、密钥零命中)→ 上传(编号 20260917-085006-1a0d)→ 清单可见且 product=mrrc_modern → 下载 SHA-256 与本地一致」。套件 1103→1186 项全绿;release_check 29 ok / 0 failing(含全部图与生成副本规则)。验收边界:三个安装包与本版镜像未重建(本条目只到源码与网站;version.txt 由三处构建脚本写入,rpi64 旧镜像的 VERSION 仍被识别),因此「从已安装的桌面版点一次生成/上传」与树莓派实机尚未执行;答复页目前是占位页(第 2 期接自动分诊)。文档:AD-021、§5 NFR-068、§10 SupportService、§12.5.1/12.6、§13 R13。 |
| SDD V2.50 | 2026-09-13 | Claude | v1.17.0 发布:一键 CQ 按键(服务端一次性自动化发射)。 三平台产物:macOS MRRC-Modern-v1.17.0-arm64.dmg 56,298,417 bytes 5bbb363d…cdfe38(实际前缀见下);Windows MRRC-Modern-v1.17.0-Windows-x64-Setup.exe 45,970,364 bytes f378ab6d…;rpi64 MRRC-Modern-v1.17.0-rpi64.img.xz 551,282,340 bytes a56e5658…。 发布前验证:套件 1103 项(VM 上 8 skipped)全绿;release_check.py 离线 29 ok / 0 failing;macOS 冻结包无头双栈 HTTPS(IPv4/IPv6 皆 401、明文 000)+ 自动密码横幅 + CQ ready: …(6.1 s, 48 kHz mono, 307 frames),DMG 经典安装布局(含 Applications 链接);Windows 走查 PYZ(cq_player + 8 个 CQ 符号)与 _internal\static\audio\cq.wav(588,566 B),server.exe 启动打印同一条 CQ ready;线上 exe 下载 SHA-256 与本地一致。构建期缺陷(本机全绿、VM 才暴露):测试里 str(path).endswith("static/audio/cq.wav") 是 POSIX 假设,Windows 用反斜杠 → 单个测试失败并拦住构建;改为 Path.parts 断言,并把该类坑写进 win_pack.md 排错表。验收边界:真机(电台 + 监听)的 CQ 端到端验收由现场执行,结果回填本条目。 |
| SDD V2.49 | 2026-09-13 | Claude | 一键 CQ 按键(服务端一次性自动化发射)。 需求:远程呼叫 CQ 每次都要按住 PTT 念一整套呼号,现场反馈要「按一下就能 CQ」,且每次发射必须有明确终点。选型对比三条路(浏览器播放 / 完整 TX 源多路复用器 / 服务端播放+最小侵入),采纳服务端播放:音频从磁盘到声卡不经过网络,键控路径复用已现场验证的 PTT 释放架构,前端只镜像状态。新增 cq_player.py(WAV 解析→48 kHz 单声道归一化→20 ms 分帧→按设备队列深度投喂,队列低于 4 帧才喂下一帧,绝不制造 queue_drops),服务端接线(_cq_key/_cq_unkey 镜像 PTT 链路、CQ 期间麦克风帧丢弃计数、发起端断线/退出立即中止、与 TUNE/PTT 互斥),前端 #btn-cq(绿=呼叫,呼叫中变 STOP)+ cqState 广播(1 Hz 进度)与 fullState.cq。实现期抓到的两个真缺陷:①启动日志对 frames 计数用了 len() → 有效资产的服务器启动直接失败(单元测试全绿也发现不了,真机启动才暴露,已加回归测试);②TX 门禁检查顺序在资产检查之后 → 未验证机型会看到「资产未加载」而不是门禁说明。文档:AD-020、§9.2 协议表、§15 第 8 层防线、§12 运维流程(含「如何替换 CQ 录音」)。 |
| SDD V2.48 | 2026-09-13 | Claude | v1.16.0 发布:Yaesu SDR 机型族 + 发布工程化。 发布 V2.46–V2.47 的工作,并把「文档漂移」变成可执行门禁:release-artifacts.json 登记每个承载版本/尺寸/SHA 的文件,harness/release_check.py 做离线版本一致性检查(--online 校验线上页面与下载、--deep 下载比对 SHA),tests/test_release_artifacts.py 在套件里强制执行;新增 docs/PROJECT_MAP.md 作为「改什么该同步哪些文档」的入口。部署侧新增 prune(sdd/、images/ 删除的页面不再永久残留)与发布完整性审计。文档:网站落地页补齐服务端录音功能卡与三款 Icom 预览机型、指南 intro 由 v1.11.x 升到当前版本并新增 TX 门禁与启动器 FAQ、SDD 落地页同步。测试 1055。产物:macOS MRRC-Modern-v1.16.0-arm64.dmg 56,001,051 bytes / SHA-256 a3b35f4843fc178306e67fc8e2bd732dd05bfecff6bea9a3c3d1b1f508cdfe38;Windows MRRC-Modern-v1.16.0-Windows-x64-Setup.exe 45,532,589 bytes / e453446bfc82072be703279c69edc8c34cc40ff99841e3a0239dc83fc97ceae4;rpi64 MRRC-Modern-v1.16.0-rpi64.img.xz 543,966,776 bytes / c8f3c3fd2e0f9dd050d998c8351a9cba0999f21e6994e4b9fe2caea09fd8dcb8。三件产物均按字节码/镜像内抽查验证(不含 strings 式证据),并逐项核对下载 URL 与 content-length。 |
| SDD V2.47 | 2026-09-13 | Claude | 设计图全面校对 + 图纳入发布索引。 发现 SDD/diagrams/ 里 3 张图根本不是本项目的(system-architecture.svg 写着 “SunMRRC System Architecture / SunSDR2 DX (SDD V3.4)”、tx-gain-staging.svg 的 TX EQ/压缩链、telemetry-flow.svg 的 0x1F00 二进制遥测包——MRRC Modern 都没有这些),且三张均无文档引用;其余图停在 8 月中旬,落后约 20 个 SDD 版本。处理:删除该 3 张(含 website 副本);按当前能力重画 system-context / layer-architecture / logical-architecture / spectrum-paths / audio-chains / subject-area-model / ptt-safety(补 Layer 0 未验证门禁,并从 SDD/15 正式引用,解决孤儿图);light-touch 更新 polling-architecture(V2.43 后不再全量重同步、轮询项来自 RadioBackend);UI 菜单图补 录音 Recordings 与「连接设置…」两行并把标题改为型号无关的 “MRRC Modern 菜单”。图成为受管承载物:每张带 diagram-version 标记(允许滞后 ≤2 版)与所描绘能力的关键词,website/sdd/diagrams/ 与 website/images/ 改由 build_sdd.py/build_guide.py 生成(此前是手工副本,静默漂移的温床),release_check.py 新增 diagrams 规则(标记+关键词+副本一致性)与 tests/test_release_artifacts.py 的 4 项测试;docs/PROJECT_MAP.md 补齐图的来源⁄生成关系与不变量。另修 docs/OPERATION_GUIDE.md 仍在指引下载 v1.13.0 的 DMG(2 处)与首页硬编码的旧测试数 633。 |
| SDD V2.46 | 2026-09-12 | Claude | Yaesu 机型扩展:FTDX10 / FTDX101D / FTDX101MP / FTX-1F(实验性,仅接收)。 新增 profile 驱动的共享 Yaesu ASCII-CAT 核心 backends/yaesu/(AD-018):yaesu_profiles.py(每机型差异的唯一来源 —— 模式寄存器与独立的 CAT 字符表、滤波槽位、频段、衰减/前置、功率格式、S 表曲线、溯源),cat_core.py(移植 FT-710 已验证的传输层:; 帧/AI 前缀过滤/写-only set/PTT 优先级抢占/ENXIO 分类/重连,按 profile 参数化,并含 Hamlib ftx1.c:881 的 VM000; 离开 memory 步骤),backend.py(由 profile 派生能力位/表/poll 项 + TX 门禁 + 只读 ID; 校验)。backends/ft710/ 零改动(AD-018 后果:迁移为三期,真机 A/B)。执行期设计审查抓到 3 个缺陷:①__getattr__ 无法满足 ABCMeta → 四个后端类会保持抽象、实例化直接 TypeError(改为导入时绑定委托 + 清空被冻结的抽象集合 + 守卫测试);②模式必须是整数寄存器(RadioBackend.set_mode(mode_num: int)/RadioState.mode: int),且 FTX-1 的 H/I 不是十六进制字符 —— 原设计 f"MD0{num:X}" 会发出无效命令 MD011(改为显式 mode_codes 查表,set_mode(0x11) → MD0I);③poll 项与 initial_state_sync 必须返回已解析的 RadioState 字段值且键名必须匹配(vfo_a_freq 而非 frequency)。频谱:Yaesu CAT 参考中不存在波形(SS 仅为设置;Hamlib 整个 Yaesu 家族无 scope 代码)→ scope_type="none",由既有 S 表高斯回退覆盖 UI,前端无需改动。双接收(FTDX101D/MP 与 FTX-1F 的固有能力)只记录不实现,留二期。测试:新增 5 个模块 106 例(profile 不变式、传输/命令层、后端门禁与身份、pty 假电台往返、诊断助手、注册表接线),全套 962 例全绿;Hamlib 模拟器对等测试为可选(skipUnless 已构建的 simftdx101 + socat)。执行中还修掉假电台自身的两个缺陷(阻塞 os.read 线程导致事件循环关停挂死 → 改 loop.add_reader;未实现 FA/FB 写)。验证边界:无真机 → TX 默认拒绝、ID; 期望值三台未知(仅记录)、音频采样率按 44.1 kHz 假设(TODO(hw-verify))、表头曲线列入 unverified_meters;_diag_yaesu.py 负责闭合。 |
| SDD V2.45 | 2026-09-12 | Claude | 录音写入端生命周期修复 ——「同一进程里只有第一次录音有声音」。 现场日志(22:26:53):Recording started 之后 6 ms 就出现第一条 Recording writer is falling behind — dropping audio,随后每 ~1.1 s 一条(每 50 块),21.9 秒累计 950+ 次丢弃,最后以 Recording queue full on stop — finishing directly 收尾;产物 15515kHz_20260912_222653.mp3 经 ffmpeg volumedetect 实测 max_volume = -91 dB(数字静音),时长 21.92 s 与真实会话一致 —— 大小和时长都正确、内容全哑。根因(代码证据 + 本机复现,现场日志签名逐行复现):_recording_writer_loop 是每会话一次性的 —— 消费 stop 哨兵、刷盘定稿后即 return(关闭流程正是靠这个语义 await 它),但任务只在 lifespan 里 create_task 一次。于是首次 REC/STOP 之后 writer 永久消失、再无消费者:200 块(≈4 s)的有界队列在毫秒级填满,此后每一块都走 QueueFull 丢弃分支,stop() 再按时间戳把整条时间轴补静音。「第一条丢弃出现在开始后 6 ms」正是铁证:队列在会话开始前就已经是满的(上一次会话留下的残留)。旁证(与「每个进程只有第一份录音有声音」一致,期间服务重启过):recordings/ 中 07050kHz_221811 -4.2 dB、15515kHz_222508 -3.2 dB、15800kHz_215337 -0.4 dB 有真实音频,其余 7 份均为 -91.0 dB(含分析期间旧进程在 22:30:15 又产出的一份,同样全哑)。修复:(1) 新增幂等的 _ensure_rec_writer() —— 任务已结束或为 None 即重建,由音频块入队、录音开始、stop 入队三处调用,活跃会话永远有消费者;任务因异常死亡时会 WARNING「Recording writer failed — restarting it」而不是静默丢弃(正是本次事故中最缺的可观测性);(2) 队列满时的 stop 回退不再在事件循环上直接 session.stop()(那会与 writer 线程的 add_audio 并发写同一时间轴,违反单写者不变式),改为丢弃最旧一块为哨兵腾位,MP3 定稿仍由 writer 线程独享。回归测试 RecorderWriterLivenessTests ×3(同进程第二次 REC 必须真正收到音频;writer 异常死亡必须 WARNING 并重建;满队列时 stop 必须入队而非就地执行),修复前双双为红。测试 909(+3)。文档:AD-017(writer 生命周期与队列语义)、操作指南「丢失 N 块」判定段、AGENTS.md/tests-README 计数。边界:V2.44 的「专用单线程池」改动(14aff85)消除的是另一类风险(录音与串口/音频争抢默认 executor 导致饥饿),与本次生命周期根因无关,两者互补、均保留;真机 8 小时以上连续录制与树莓派 SD 卡长时间写入仍未验证。 |
| SDD V2.44 | 2026-09-12 | Claude | v1.15.0 发布:macOS 已发布,Windows 当晚补发布;并修复“同进程第二次录音全静音”。 发布前抓到并修复的缺陷:MP3 writer 任务其实是每会话一次性的(收尾后返回,关停时靠 await 它),但原先只在进程启动时创建一次 —— 首次 REC/STOP 之后任务已结束,后续每次录音队列无人消费,200 块积压秒级灌满、音频全部被丢弃,stop() 再补出静音,于是得到“长度正确、内容 100% 静音”的 MP3(现场 22:26 Recording dropped 950 block(s) so far)。修复:_ensure_rec_writer() 在每次 add_audio/stop 入队/每次 REC 时幂等重建(发现 writer 已死时告警),队列满改为丢最旧块腾位给 stop 哨兵(不再在事件循环里同步 stop);新增 3 个 writer 存活测试(该步共 909;随后启动器修复 +10,发布产物由 919 项测试把关)。关键教训:两个安装包分别在 22:03 与 22:18 构建,而该修复写于 22:34 —— 已上传的包不含修复;同时我最初用 strings 搜索打包产物里的函数名来判断“包是否含修复”是无效方法(PyInstaller 的 PYZ 是压缩的),改用 PyInstaller.archive.readers.CArchiveReader 提取入口脚本并遍历代码对象后才得到可信结论(macOS 重建包确认含 _ensure_rec_writer)。macOS(已发布):MRRC-Modern-v1.15.0-arm64.dmg(55,976,374 bytes,SHA-256 d66cb2adff34a5f0a4d85c64d20d97d448eed7eb278a58acffdb4852aa55099e)—— 修复后重建,包内 lameenc.cpython-313-darwin.so、Contents/Frameworks 符号链接、前端 v29 与录音面板均已核验,lame.js 已移除。Windows 已发布(补记,同日稍晚):原 v1.15.0 构建(45,491,710 bytes,构建于修复前)在发布窗口内被主动撤下(网站上 MRRC-Modern-Setup.exe 回退至 v1.14.2 归档),未对外公告;ham.vlsc.net(Win11 KVM 宿主)恢复后从同一 commit 重建为 MRRC-Modern-v1.15.0-Windows-x64-Setup.exe(45,494,587 bytes,SHA-256 74d04b84ab7b3d0b85314efff304fccb1494f60d95f07c24c0758bee6f13ee0c,919 项测试 + 3 个 PyInstaller 目标 + Inno Setup 6.7.3 全绿),已替换通用名与版本名两个下载入口并同步本节、installer guide、win_pack.md、README 与网站卡片。验证教训(本次新增):strings/grep 无法证明压缩 PYZ 里的符号是否存在,改用 CArchiveReader + 代码对象遍历 —— MRRC-Modern-Server.exe 的 server 脚本条目确认含 _ensure_rec_writer/_recording_writer_loop/_rec_enqueue_stop,recorder 模块在 PYZ 内且含 RecordingSession/add_audio/close_without_finishing(BUNDLE_CHECK PASS)。宿主不可达本身已登记为 §13 R11 的同源问题。树莓派 rpi64 镜像(同日补发布):MRRC-Modern-v1.15.0-rpi64.img.xz(546,128,812 bytes,SHA-256 c9936a3befb780c17133851330d901d43885c181eb86d68e9eb903f1ee3a0773)由本机 Docker Desktop 原生 aarch64 重建 —— 镜像内首次自带 lameenc.cpython-311-aarch64-linux-gnu.so(v1.14.3 构建于录音功能之前,镜像里根本没有服务端录音),并附带同一个编码坑的 Linux 版本修复:/boot/firmware/mrrc.env 预置文件用 ANSI/GBK 保存后,linux/first_run.py / firstboot_wrapper.py 的严格 UTF-8 读取会让 mrrc-firstboot.service 首次上电即失败;现两者共用 read_env_text() 兜底解码(+7 测试,最终 926 项)。包内抽查:VERSION=1.15.0、_ensure_rec_writer、read_env_text、lameenc aarch64 .so 均在;verify-image.sh、xz -t、debugfs 抽查通过。产物来源:Windows/macOS 由 919 项测试的树构建,Pi 由最终 926 项测试的树构建(该修复只在 linux/ 路径)。已完成的验证边界:mac 906→909→919→926 测试、三个 PyInstaller 目标、ad-hoc 签名、sha 校验;Windows 包内 lameenc.cp312-win_amd64.pyd 与 FTDI DLL、static/、mem_channels.json 均在位(新构建已复核)。仍未验证:长时间连续录制、树莓派上的录制、Windows 真机 TX 音频(KVM 等时 OUT 限制)。 |
| SDD V2.43 | 2026-09-12 | Claude | CAT 掉线恢复成本修复 + 日志静音(现场:USB 串口桥反复重枚举)。 现场日志证据:[Errno 6] Device not configured(macOS ENXIO)+ 紧随其后 No such file or directory: /dev/cu.usbserial-0121DB3A0,且当时的可用端口列表里没有电台的桥 → 不是读超时误判,而是 USB 串口桥物理层消失后重新枚举(当日累计 171 次,分布突发:9/09 112 次、9/11 6 次)。真正的性能问题在恢复路径:watchdog 重连后调用 initial_state_sync() —— FT-710 的 24 条串口查询(FA/FB/VS/MD0/TX/SM0/SH0/AG/PC/PA0/RA0/NB0/NR0/BC/AC/PS/SS01/AN/GT/CO/MS/AO/RI0/RG0,每条 0.25 s 超时 + 20 ms 间隔),实测占住串口锁 17:10:23→17:11:25 ≈ 62 秒(日志在这段时间完全空白),期间用户命令排队、且同步会 mark 全部字段 dirty(连带把 2026-08-15 加的 serial_connected 诊断刷了 84 行)。修复:重连不再跑全量同步 —— 那 24 个字段全部由轮询分层覆盖(IF 0.1 s 频率/模式/S表、VFO 与 TX 0.5 s、设置 2 s、慢遥测 5 s),重连只需恢复 serial_connected 并照旧重跑 scope init(EX040101/EX040200),未读到的字段保留上次已知值而不再被默认值清零(原实现经 RadioState.from_sync_result + 全字段 setattr 会清零 —— 这也是不能部分同步的原因)。日志纪律:reconnect_loop 首次尝试 INFO、后续 DEBUG、掉线超过 RECONNECT_HINT_AFTER_ATTEMPTS(5 次 ≈ 15 s 退避)后只发一条 WARNING 给出物理层提示(USB 线/供电/勿用无源 HUB);connect() 失败仅首次打印 ERROR + 可用端口列表、后续降为 DEBUG;新增 CatController._is_device_gone() 识别 ENXIO/ENODEV/ENOENT(并在读错误处只提示一次);2026-08-15 的 state broadcast contains serial_connected 诊断降为 DEBUG。测试 906(+3)。保留的物理层边界:重枚举本身是硬件/驱动事件(电缆、供电、无源 HUB、与 FT4222 频谱共用同一条 USB),软件侧只能做到快速恢复 + 静默 + 可操作提示,已登记为 §13 R11。 |
| SDD V2.42 | 2026-09-12 | Claude | 服务端 QSO 录音(增量 MP3)—— 替换浏览器录音器,修复回放颤抖。 根因(代码证据):旧录音器把到达的帧顺序拼接、无时间戳,于是网络抖动与 jitter buffer 的补帧被写进文件(queue.length > 50 的丢弃与 WS 停顿都成了时间轴压缩);采样率错配已排除(AudioRX_sampleRate 恒为 48 kHz,Opus 解码亦 48 kHz)。新架构:recorder.py 把 RX(AudioHandler.read_rx_chunk() 的设备域 PCM,44.1k/48k)与 TX(/WSaudioTX 解码出的 48 kHz 麦克风 PCM)放到单一时钟轴上(time.monotonic_ns() 定位、源切换重锚定、50 ms 连续性容差吸收调度抖动、真实停顿补静音),经 44.1k→48k(audio_resample,AD-011 唯一 SRC 桥)与 48k→16k(96 抽头 Hamming 窗 sinc FIR 抗混叠抽取,移植 mrrc)得到 16 kHz 单声道存储域,用 lameenc 增量编码边录边落盘(每块 flush → 崩溃时磁盘上仍有可播放前缀;RAM 恒定,对比 mrrc 的内存缓冲 1 小时省 ~115 MB)。并发与接线:单 writer 任务独占会话(asyncio.to_thread 编码,事件循环零阻塞),有界队列(200 块≈4 s,带丢弃计数)防内存失控;RX 循环的 idle 深睡条件改为「无监听且未在录音」,否则无人收听时 REC 录到空白;TX 侧只在 radio.tx_status == 0 期间录 RX(避免自监听重复)。控制面:复用已鉴权的 /WSradio —— set{field:"recording"} 启停(放在 “Radio not connected” 守卫之前:录音依赖 USB 音频而非 CAT,无 CAT 时文件名显示 00000kHz,与 mrrc 一致),stop 哨兵携带 completion future 以便写盘完成后再广播;recordingState 变更即广播 + 录制中 1 Hz(复用 RX 循环 20 ms tick,不新增任务);fullState 携带同一快照 → 新客户端/刷新即同步。文件面:三条 REST 路由(列表 / Range 的 FileResponse 流(实测 206 + Content-Range,播放器可 seek)/ 删除),名称白名单 ^\d{5}kHz_\d{8}_\d{6}\.mp3$ + containment 校验(I8 教训),正在录制的那份删除返回 409。索引 recordings.json(与 mem_channels.json 同处)合并磁盘:用户自行放入的 mp3 也能显示(时长按大小/码率推算)。前端:REC 按钮改发服务端命令并渲染广播状态(含实时计时,无本地计时器);新增「录音」面板(总占用、频率/日期/时长/大小、单实例 <audio> 播放、下载、删除,录制中禁用删除),全部 createElement/textContent(XSS 门禁)。删除浏览器录音器与 lame.js(530 KB);cache-bust main v29 / ui v31 / sw mrrc-v32。依赖与打包:lameenc>=1.8.0(单编译扩展模块,非包)+ PyInstaller hiddenimport;两个 launcher 增加 MRRC_RECORDINGS_DIR(用户数据目录)。测试 891(+22)。硬件边界:真机 RX tap 的音频连续性、TX 实测音质、mac DMG/Win 安装包内 lameenc 的加载与录制、树莓派 SD 卡长时间录制 —— 均未验证,须在对应平台各录一段实测。文档:SDD §5 NFR-066、§7.2 实体、§8 AD-017、§9 音频链路、§10/§11 Recorder、§12 接口与运维、§13 R10/A8、AGENTS.md/README/tests-README/CHANGELOG、操作指南与树莓派指南、网站指南段落。 |
| SDD V2.41 | 2026-09-12 | Claude | ICOM SDR model support: IC-705 / IC-7610 / IC-7760 behind a profile-driven CI-V core — with no hardware present for any of the three. The Icom side of the backend layer became data-driven: new backends/ic7300/civ_profiles.py holds one CivModelProfile per model (CI-V address, 1a 05 <item> Transceive set-mode item, scope geometry, bands, mode registers, attenuator steps, meter curves, verification flags, data provenance) and civ_codec/civ_controller/civ_scope take those values as optional parameters whose defaults reproduce the previous IC-7300 constants exactly — the three new backends are ~10-line subclasses overriding _profile, backends/__init__.py gained known_models() (the server’s model whitelist and the per-model baud table are now registry-derived, closing the class of bug where a registered backend stays unreachable), and no server.py model branches were added. New radios: IC-705 (0xA4, 475-bin scope, HF+6m+2m/70cm, 10 W), IC-7610 (0x98, 689-bin/15-segment scope, 0-45 dB attenuator in 3 dB steps, dual receiver) and IC-7760 (0xB2, same scope geometry, 200 W). All profiles carry verified=False, list their unverified_meters, and refuse PTT/TUNE until the operator sets MRRC_ALLOW_UNVERIFIED_TX=1 — an unverified radio can receive, but cannot key; a release is never gated (Chapter 15 layering), and the server answers a refused key-up with actionable text instead of silence. RadioCapabilities gained verified/tx_gated/dual_rx/unverified_meters/audio_gain_boost, RadioState gained the dirty-tracked model_mismatch, and the model-ID check (19 00, read-only) logs the observed bytes without inventing a verdict — no profile records an expected ID, because a fabricated expectation would raise false mismatch warnings on working radios. New _diag_civ.py drives the production CivController through identity → read-only probes → scope measurement (which yields the three facts that cannot be settled offline: bin count, amplitude ceiling, segment count) → static meters including raw Vd/Id → an optional 200 ms TX check with PTT read-back and manual recovery steps, then restores state and writes a paste-ready Markdown report naming civ_profiles.py as the correction target. Bug found while implementing the ATT step list: the attenuator data byte is packed BCD, not decimal (the plan’s range(0,46,3) would have sent 0x0F for 15 dB); new encode_bcd_byte/decode_bcd_byte helpers carry the IC-7300MK2 CI-V reference evidence, and the IC-7300’s 20 dB byte stays bit-identical at 0x20. Frontend: connection dialog lists the three models with a 实验性 label, RX playback boost comes from capabilities instead of a model-string comparison, and a badge + one-time notice explains a disabled transmitter (cache-bust main v28 / ui v30 / sw mrrc-v31). Packaging: backends.ic7300.civ_profiles added to the PyInstaller hiddenimports. Suite 820 tests across 43 modules (was 724/39). Hardware boundary: no unit of any of the three models was available, so the 689-bin geometry, the three models’ meter curves (inherited from the IC-7300 and rescaled by rated power), the 48 kHz USB-audio assumption and every 19 00 model ID remain unverified and are listed as assumption A7 / risk R9 in §13; _diag_civ.py is how a field report turns them into facts. Docs: SDD §5 (NFR-032/041/060/065), §8 AD-016 amendment, §9, §11, §13 (A7/R9), §15 (new outermost gate layer), AGENTS.md module table + env vars, README supported-radio table, tests/README counts, IC-7300_硬件验收清单.md pointer. |
| SDD V2.40 | 2026-09-12 | Kimi | rpi64 image rebuilt from HEAD and published as v1.14.3. The first Pi image (v1.14.1) predated the V2.37–V2.39 fixes, so the image was rebuilt from main and released as MRRC-Modern-v1.14.3-rpi64.img.xz (588,677,000 bytes, SHA-256 2894c895685c72d27e6c97d997d51cf2835ddb018e813375c15950da733a2509) — it now carries the radio-settings panel (V2.34), RX recording fixes (V2.31/V2.32), IC-7300 model↔︎baud linkage + audio duplicate recovery (V2.37/V2.38) and the S-meter no-data sentinel (V2.39, already shipped in the v1.14.2 installer pair). Build path inverted: the local Mac + Docker Desktop native aarch64 pipeline (~8 minutes vs 1–3 h under qemu on ham) is now primary; new packaging knowledge recorded in pi_pack.md: Docker Desktop data dir on an external SSD when the internal disk is small (Docker.raw holds the multi-GB work dir), --build-arg BASE_IMAGE=debian:bookworm (the Dockerfile’s bullseye base has an expired security repo), work must stay inside the Docker VM (macOS-volume bind mounts forbid mknod → debootstrap refuses), xz -T4 -6 on 8GB-RAM hosts, and streaming uploads past the www /tmp tmpfs. Verified: chroot gate (deps OK/scope libs OK), xz integrity, debugfs spot-checks (VERSION 1.14.3, both systemd units, password helper, venv python, S-meter sentinel in scope_handler.py), server-side and downloaded SHA-256 match; website EN/ZH Pi card updated to v1.14.3 with real size/SHA and the download enabled (the v1.14.2 release flow had left it disabled as “build in progress”). |
| SDD V2.39 | 2026-09-10 | Claude | IC-7300 S-meter flicker fix — scope “no data” sentinel (hotfix folded into the v1.14.2 installers, rebuilt same day). Field report: the IC-7300’s web S-meter flickered/jumped constantly. Root cause: the IC-7300’s CI-V scope segments carry NO S-meter byte (unlike the FT-710’s FT4222 frame at data[110]), so ScopeHandler.s_meter stayed at its 0-initialized value — a legal S-zero — and _on_scope_frame’s >= 0 gate broadcast it on every ~30 fps waveform, forcing radio.s_meter back to 0 between the 10 Hz CAT poll (15 02) updates that wrote the true reading: the UI flipped between the two continuously. Fix: the “no data” sentinel is now -1, skipped by the existing >= 0 gate; FT-710 per-frame real values and the synthetic fallback (update_from_radio_state seeds the true radio value) are unchanged. Also fixes the pre-existing Optional[callable] type misuse in ScopeHandler (builtins.callable is not a type). 3 new sentinel tests; suite 724 tests. Docs: SDD README Quick Facts, CHANGELOG v1.14.2. Real-radio flicker acceptance on the field rig remains an operator check. Same-day rebuild: the macOS DMG was rebuilt with this fix and redeployed (55,898,038 bytes, SHA-256 88ed7c626e86b8cb2ba03a5ce2419c9313fece0e97eb579da75b8dda6c6bbb75, replacing the V2.38 build); the Windows installer was rebuilt the same night after the jump-host outage cleared (45,435,022 bytes, SHA-256 a7ee16674c0db29a80fbb072301581a3ecf4805e6705382db380ea534862c706, 724 tests on the VM) and redeployed alongside the DMG. |
| SDD V2.38 | 2026-09-10 | Claude | v1.14.2 dual-platform release (macOS DMG + Windows Setup) — IC-7300 baud linkage + audio duplicate recovery. Published the V2.37 fixes in rebuilt installers: MRRC-Modern-v1.14.2-arm64.dmg (55,897,231 bytes, SHA-256 a91875d5d6061c57f59eb5dfef025e6168a55613a32c7365242e10e8c4ef66e2, classic layout with Applications shortcut, ad-hoc signed; Frameworks symlink + FTDI dylibs verified in-bundle) and MRRC-Modern-v1.14.2-Windows-x64-Setup.exe (45,435,022 bytes, SHA-256 a7ee16674c0db29a80fbb072301581a3ecf4805e6705382db380ea534862c706, built on the Win11 KVM VM with Python 3.12.4 / PyInstaller 6.21.0 / Inno Setup 6.7.3; 721 tests, three PyInstaller targets, bundled-file inspection (FTDI FT4222/ftd2xx DLLs, static, mem_channels.json) and cross-host SHA-256 all green). CHANGELOG v1.14.2 entry, Inno MyAppVersion, installer guides, packer playbooks, website EN/ZH pages and regenerated SDD HTML updated. Real-radio acceptance (IC-7300 scope at 115200 with the CI-V baud menu on Auto; audio recovery on the field rig) remains an operator check. |
| SDD V2.37 | 2026-09-10 | Claude | IC-7300 model↔︎baud linkage + audio duplicate recovery (field log 2026-09-10: CI-V scope stalled → S-meter fallback, and RX/TX audio -9999 on every PTT). Root cause one: the legacy installer templates pre-fill MRRC_BAUD_RATE=38400 (the FT-710 value) regardless of model; that stale env value overrode the backend-aware default (115200, V2.24) and starved the IC-7300 CI-V 0x27 scope stream (stall watchdog: no complete waveform for >3s). Root cause two: the connection dialog’s saved device name (“麦克风 (USB Audio CODEC)”, no trailing space) matched ONLY the WDM-KS duplicate of the codec — the MME/WASAPI entries carry an invisible trailing space before “)” — and WDM-KS opens fail with -9999 on the field rig, while all 6 retries re-resolved the same locked index. Fixes: (1) api_setup_save now writes MRRC_BAUD_RATE=default_baud_for(radio_model) (new config.default_baud_for() helper) on every save and logs the radio-menu expectation (CI-V baud = Auto or match); (2) apply_first_run aligns the stored baud with the probed model when unset or still at the legacy template value (explicit custom values preserved); (3) windows/default.env + macos/default.env leave the baud empty (per-model default; both templates’ stale 38400 removed). Audio: _find_rx/_tx_device gain an exclude tier filter — configured-name matches now collect ALL host-API duplicates, prefer a non-WDM-KS entry (same hardware opens fine via MME/DirectSound/WASAPI) and log the alternates; the post-_reinit_pyaudio retry in start_rx/start_tx re-resolves with the failed index excluded, falling through to the codec-hints tier so a WDM-KS-locked name lands on the working MME duplicate (keeps the original index when no other candidate exists — preserves the V2.12 USB-re-enumeration recovery). Device-selection logs now include the host API. pyaudio ImportError fallback is an empty module stub (HAS_PYAUDIO guards unchanged) instead of None so optional-dependency environments keep valid type structure. 28 new tests (config default_baud_for ×4; first-run baud linkage ×4; api_setup_save linkage ×3; host-API preference/exclude tiers ×4; end-to-end WDM-KS→MME recovery for RX and TX ×2; remainder in existing classes); suite 721 tests across 39 modules. Docs: SDD README Quick Facts, AGENTS.md test counts, tests/README counts, CHANGELOG (unreleased). Real-radio acceptance (IC-7300 scope at 115200 with the CI-V baud menu on Auto; audio recovery on the field rig) remains an operator check. |
| SDD V2.36 | 2026-09-09 | Kimi | Raspberry Pi image packaging line (rpi64, frontend/packaging-only). New packaging/rpi/ builds a bootable Raspberry Pi OS Lite 64-bit (Bookworm) image via pi-gen (locked 2026-06-18-raspios-bookworm-arm64): custom stage4 installs runtime deps, deploys the code tree to /opt/mrrc_modern, creates the venv at build time (first boot needs no network), adds the mrrc user (dialout/audio), and wires mrrc-modern.service (systemd, EnvironmentFile, Restart=on-failure) + mrrc-firstboot.service (oneshot: adopt a pre-seeded /boot/firmware/mrrc.env — operator-chosen password — else run linux/first_run.py to generate a password and probe FT-710 ASCII / Icom CI-V 0x19 across /dev/ttyUSB* | ttyACM* with onboard-UART filtering; then ssl_bootstrap cert, headless defaults MRRC_WEB_HOST=::, motd +sudo mrrc-show-password discovery; the web login banner stays loopback-only). FT-710 true spectrum ships without FTDI Linux libs (manual aarch64 drop-in to vendor/ftdi/, documented; S-meter fallback unchanged; IC-7300 unaffected). Build hosts: Docker Desktop (native aarch64) or the ham.vlsc.net host via qemu-user-static (documented fallback used for the first build).linux/first_run.py ported from macos/first_run.py with Linux USB-serial filtering (4 unit tests); test_rpi_packaging.py structural assertions (7 tests → suite 696 green); chroot build gate asserts runtime imports inside the image; post-build verify-image.sh (xz integrity, size bounds, debugfs spot-checks). Docs: pi_pack.md,docs/RASPBERRY_PI_GUIDE.md, website EN/ZH Pi download cards, dual-platform-release skill reference. Real-Pi acceptance (flash, first-boot paths, CAT/audio) remains an operator check. |
| SDD V2.35 | 2026-09-09 | Kimi | v1.14.1 dual-platform release (macOS DMG + Windows Setup) — Radio Settings panel. Published the V2.34 frontend work in rebuilt installers: MRRC-Modern-v1.14.1-arm64.dmg (55,886,045 bytes, SHA-256 c08f2d172f9e1beb44d4bc00381374263cd910bfc74d69f2dd6e77170ffcc8d7) and MRRC-Modern-v1.14.1-Windows-x64-Setup.exe (45,427,335 bytes, SHA-256 b76da0e985e8c1faabdc98df8d1ce92224b5800be721b81947837d931bd6cb71, Win11 KVM VM: 694 tests, three PyInstaller targets, Inno Setup, bundled-file inspection, cross-host SHA-256 all green). CHANGELOG v1.14.1 entry, Inno MyAppVersion, installer guides, packer playbooks, website EN/ZH pages and regenerated SDD HTML updated; AGENTS.md test count corrected (651→694, drifted since V2.19). No CAT/audio/PTT/scope/protocol changes; real-radio slider acceptance remains an operator check. |
| SDD V2.34 | 2026-09-09 | Kimi | Radio settings panel with RF power slider (frontend-only). The ☰ menu “Settings” action now opens a “Radio Settings” modal (showSettingsPanel, ft710_ui.js) instead of scrolling to the hidden DSP sliders block. It exposes the RF PWR slider (5–100; FT-710 = watts via PC, IC-7300 = percent via CI-V level 0x0A pct↔︎raw) reusing the existing rf_power server route — no server changes. input updates the readout only; change (release) sends sendCommand('rf_power', v) so a drag cannot flood the serial port, and renderSliders skips the poll echo while slider-rfpower-p is mid-drag (dataset.dragging guard). The panel is DOM-built (createElement/textContent, no innerHTML) to satisfy the XSS lint gate; the same hardening was applied to showMemoryManager (mem labels are external JSON — closes an injection vector) and the two scope-selector innerHTML='' clears became replaceChildren(). The dormant hidden #dsp-sliders block is left untouched. Cache-bust ft710_ui.js?v=28→29, service worker mrrc-v29→mrrc-v30 (ft710_main.js?v=27 unchanged); cache-bust assertions re-pinned in test_server_ws_protocol.py AND test_audio.py (second sw pin), scope-speed selector assertion updated for the new idiom (+1 new panel test → 694 tests green); JS syntax verified via node --check. Real-radio acceptance (slider → radio power readout, both models) remains an operator check. |
| SDD V2.33 | 2026-09-09 | Kimi | v1.14.0 dual-platform release (macOS DMG + Windows Setup). Published the V2.31/V2.32 RX recording-quality fixes in rebuilt installers: MRRC-Modern-v1.14.0-arm64.dmg (55,885,519 bytes, SHA-256 f977837d521b20b0fa3a2d67d5ea0a15cc6ee060b3d566545388f4d10d13823e, classic DMG layout with Applications shortcut, ad-hoc signed) and MRRC-Modern-v1.14.0-Windows-x64-Setup.exe (45,423,529 bytes, SHA-256 747f1c6b6b9e7bca1cdfeff7a4a498ca3fbdba4b03c044113da17c160f21e48a, built on the Win11 KVM VM with Python 3.12.4 / PyInstaller 6.21.0 / Inno Setup 6.7.3; 693 tests, three PyInstaller targets, bundled-file inspection and cross-host SHA-256 all green). CHANGELOG v1.14.0 entry, Inno MyAppVersion, docs/WINDOWS_INSTALLER_GUIDE.md download table (was stale at v1.12.1), docs/MACOS_INSTALLER_GUIDE.md, packer playbooks (win_pack.md/mac_pack.md), and the download site (EN/ZH index + guide pages, SDD HTML pages regenerated via website/build_sdd.py) all updated; both download URLs verified live (200, matching size, matching SHA-256). No CAT/PTT/scope/protocol changes; real-QSO recording acceptance remains an operator check. |
| SDD V2.32 | 2026-09-07 | Kimi | RX broadcast no longer drops catch-up frames. _audio_rx_loop previously trimmed bursts to the newest AUDIO_RX_MAX_FRAMES_PER_CYCLE = 2 Opus frames whenever the loop fell behind and read_rx_chunk() returned up to 4 chunks at once — the discarded frames were permanent losses, leaving holes in the client jitter buffer (audible flutter) and in browser recordings. The trim is removed: every encoded frame is sent. The burst stays bounded by the read-side cap (min(count, chunk*4) ≈ 80 ms), and the client’s time-based jitter buffer absorbs it. Complements the V2.31 restart_rx() mitigation for the muffled-frame recording issue (the trim produced gaps, not the low-frequency/attenuated frames themselves). Suite 693 tests green. |
| SDD V2.31 | 2026-09-06 | OpenAI | RX recording flutter mitigation. Field analysis of the 2026-09-06 mrrc-qso-* browser recordings showed intermittent 20 ms RX frames whose energy was low-frequency/attenuated, while TX mic frames were clean. The RX read/encode/send code was unchanged since the 2026-08-23 baseline, and offline checks ruled out browser Opus decode, lamejs MP3 encode, chunked 44.1→48 kHz resampling, packet drop, and synthetic event-loop stalls as standalone causes. The remaining field-consistent failure mode is a long-lived PortAudio RX capture stream degraded after TX playback on the same USB codec, previously documented only for Windows. AudioHandler.restart_rx() now reopens the RX capture stream after every TX→RX transition on all platforms, still off the asyncio loop; this preserves the codec/device sample-rate contract and extends the existing full-duplex recovery guard to macOS/CoreAudio-like stacks. Regression tests updated for platform-independent restart behavior. |
| SDD V2.30 | 2026-08-29 | Kimi | v1.12.1 Windows Stable publication + merge with the ox-alpha hardening line (their V2.28). Published the web QSO recorder work (V2.29) after 633/633 tests on the Windows 11 build VM, three successful PyInstaller targets, Inno Setup 6.7.3 compilation, required bundled-file inspection (FTDI DLLs, opus.dll, static assets including lame.js, mem_channels.json), and cross-host SHA-256 verification. Artifact: 45,329,503 bytes, SHA-256 ba5fb7a9fd952e9c92508cf6b159c1d92b9292a3b03925f855f55166d5954e47; both download URLs verified live (200, matching size, matching SHA-256). The release changes only frontend recording behavior; radio-side CAT, 44.1 kHz device-domain audio, PTT, and spectrum behavior are unchanged from v1.12.0. The parallel hardening line (V2.28, ox-alpha: I8/I10/I12 resolved, I9/I11 partial) was merged afterward — both lines had independently claimed SDD V2.28 and cache-bust ft710_main.js?v=26 / sw mrrc-v28, so this line renumbered to V2.29/V2.30 and the merged tree re-bumps cache-bust to ft710_main.js?v=27 / sw mrrc-v29 (the hardening fixes are NOT inside the published v1.12.1 installer; they ship with the next release). Real-QSO recording quality (TX/RX switch edges, sidetone suppression) and physical-radio acceptance remain operator checks. |
| SDD V2.29 | 2026-08-28 | Kimi | Web QSO recorder: RX+TX chronological MP3. The web UI recorder (window.RXRecorder, static/ft710_main.js) previously captured only decoded RX frames; it now records the whole QSO in time order. While PTT is held (txAudioRunning), feedRXRecorderFrame() mutes the RX feed (the radio only returns sidetone/duplex audio during TX, which the UI already dims to 0 via AUDIO_TX_DIM_FACTOR) and local mic frames are fed into the same lamejs mono 48 kHz encoder instead — tapped in all three TX capture paths: AudioWorklet (copied before the zero-copy transfer to tx_opus_worker neuters the buffer, only while recording), ScriptProcessor fallback, and Int16 PCM fallback. Download filename prefix mrrc-rx- → mrrc-qso-; REC button tooltip updated; cache-bust bumped (ft710_main.js?v=26, ft710_ui.js?v=28, service worker mrrc-v28 — numbers later re-bumped in the V2.30 merge after a collision with the parallel hardening line). Frontend-only change; no server/protocol impact, suite 633 tests green (cache-bust guard assertions re-pinned in test_server_ws_protocol.py/test_audio.py, JS syntax verified via node --check). |
| SDD V2.28 | 2026-08-26 | ox-alpha | Open-issue hardening pass: I8 resolved, I9/I11 partially resolved, I10 + new I12 resolved. (1) I8 serve_static path traversal closed: new _resolve_static_path() resolves the STATIC_DIR / path join and rejects anything not is_relative_to the resolved STATIC_DIR — GET /../server.py, absolute request paths (which Path joins wholesale), and symlink escapes now 404 instead of falling through to the SPA fallback; regression tests cover parent traversal, absolute paths, dot-segment variants, and the harmless ....// lookalike. (2) I9 login comparison switched to hmac.compare_digest via _password_matches() (encoding keeps non-ASCII input from raising); _warn_if_default_password() fires a loud startup WARNING when the well-known default is active, wired into lifespan startup; first-login forced change still future work. (3) I10 web subchannel self-heal: subchannelReconnect/subchannelConnected give /WSspectrum, /WSaudioRX, /WSaudioTX independent exponential backoff (1 s→30 s cap), CONNECTING-state guards prevent stacked sockets, 4001 auth expiry stays owned by the control-channel flow, and a dedicated _webClientOff flag suppresses reconnect while the power button has the client OFF (_intentionalClose alone is insufficient: wsRadio.onclose consumes it before the subchannel close events arrive). Cache-bust ft710_main v26 / sw mrrc-v28 (v27 was consumed by the v1.11/v1.12 line). (4) I12 (new) opt-in stuck-keyup watchdog _max_tx_watchdog: after MRRC_PTT_MAX_TX_SECONDS (default 0 = off) of continuous transmit it unkeys fire-and-forget via set_ptt(False), zeroes TX meters, toasts every control client — no verify loop per ch15; covers zombie-but-connected sockets that client watchdogs and the dead-man switch both miss. (5) I11 partial iOS PTT release race fixed in ContentView/RadioViewModel: local pttHeld tracks the gesture (same proven pattern as TUNE’s tuneHeld), release sends ptt:false unconditionally with optimistic local txStatus; device verification pending; watchdog/scenePhase layers remain open. Repo hygiene: stale .bak files removed. Merged-line suite 651 tests across 33 modules (local pass was 623, +18 over its base: test_server_security ×14, test_max_tx_watchdog ×3, subchannel-reconnect contract ×1; union with the v1.11/v1.12 line adds the remainder). Docs: §13.4 issue states, README env table + counts, AGENTS.md counts, tests/README, CHANGELOG. |
| SDD V2.27 | 2026-08-26 | OpenAI | v1.12.0 Windows Stable publication. Published the IC-7300/MK2 runtime-reliability and official CI-V conformance work after 633/633 tests on both macOS and Windows 11, three successful PyInstaller targets, Inno Setup 6.7.3 compilation, required FTDI/Opus/static/seed-data inspection, and cross-host SHA-256 verification. Artifact: 45,339,501 bytes, SHA-256 e7d1e460c408a6da2c0f66f23002d48429fa0b46bfd933305b4f150cbcefade2. The release preserves FT-710 CAT, fixed 44.1 kHz device-domain audio, PTT, and spectrum behavior. Software and installer verification does not constitute physical IC-7300/MK2 USB enumeration, real-radio ACK timing, RF, power-cycle, tuner, or RX/TX audio-quality acceptance. |
| SDD V2.26 | 2026-08-26 | ox-alpha | Full-repo documentation audit + SDD-Guardian harness expansion (docs-only; merged after the IC-7300 V2.24/V2.25 work). Deep audit of server/backends/audio/frontend/mobile/tests surfaced four open issues, recorded in §13.4: I8 serve_static path traversal, I9 non-constant-time password compare plus warning-only weak default, I10 web audio/spectrum subchannels lacking independent reconnect, and I11 the iOS PTT release race. Harness expanded from 17 to 21 rules with warn-level static-path/password checks and info-level audio-reconnect/iOS-PTT guidance; index routing, catalog, skill, lifecycle, environment names, five-endpoint documentation, legacy scope-handler status, and test inventory were synchronized. No runtime behavior changed in this audit commit; current suite remains 633 tests across 31 modules. |
| SDD V2.25 | 2026-08-26 | OpenAI | IC-7300/MK2 official CI-V conformance hardening. Scope startup and diagnostics now enable both display (27 10 01) and waveform-data output (27 11 01); SCROLL-C information is decoded as lower/upper edges rather than center/span. Model identity explicitly selects Transceive item 0071 (IC-7300) or 0089 (MK2), independent of customized CI-V addresses. Unsupported bare 18 power polling is replaced by documented 03 liveness; maintenance 18 01 emits Icom’s baud-dependent FE preamble. IC ALC uses official raw 120 full scale while FT-710 remains raw/255. CI-V scope speed is constrained to FAST/MID/SLOW in capabilities and UI. The SDD Guardian direct-serial rule now recognizes CivController as the Icom transport boundary. Software verification covers official byte vectors and regression behavior; serial/ACK timing, RF, scope cadence, power cycling, and USB audio quality remain hardware acceptance items. Suite 633 tests across 31 modules. |
| SDD V2.24 | 2026-08-26 | OpenAI | IC-7300/MK2 runtime reliability hardening. config.py now selects the serial default by backend (FT-710 38400; IC-7300/MK2 115200) while preserving explicit MRRC_BAUD_RATE and legacy aliases. The CI-V 0x27 demux queue is bounded at 44 segments and drops the oldest on overflow, preventing stale waveform replay after consumer pauses. Spectrum broadcasting runs at 30 Hz and emits real scope payloads only when the frame counter advances. _diag_ic7300_scope.py now reuses the production checksum-free CI-V codec/parser, correctly decodes BCD frequency replies, and disables scope output on exit. Audio processing rates are unchanged; device enumeration and RX/TX open logs now include host API, default rate, actual stream rate, and channels. Added 12 hardware-free regressions in test_ic7300_runtime_reliability.py; suite 617 tests across 30 modules. |
| SDD V2.23 | 2026-08-23 | Kimi | v1.11.0 — User-configurable spectrum and waterfall heights in the web UI menu; power-test flakiness fixed; Windows installer bumped to 1.11.0. Added Spec H and WF H sliders to the off-canvas menu Settings (static/index.html), persisted as ft710_fftHeight / ft710_wfHeight cookies via static/modules/settings_manager.js. static/ft710_ui.js reads these values (with responsive defaults: mobile 22 px FFT / 45 px waterfall, desktop 40 px / 80 px) and applies them to the #fft-canvas and #waterfall-canvas drawing-buffer and CSS heights; the resize handler keeps responsive defaults when no custom value is set and preserves user settings otherwise. Drawing buffers are cleared on height change to avoid scroll artifacts. Cache-bust query strings bumped (ft710.css?v=24, ft710_ui.js?v=26). tests/test_ft710_power.py now mocks time.monotonic() so the boot-window assertion no longer depends on host uptime. packaging/windows/MRRC-Modern.iss updated to 1.11.0; website download links and deploy manifest synced. Suite 605 tests. |
SDD V2.22 | 2026-08-17 | Claude | Launcher config template exposes
SDD V2.21 | 2026-08-17 | Claude | MRRC Modern rebrand + env migration + macOS packaging fix.
SDD V2.20 | 2026-08-17 | Kimi | SDD generalized for multi-radio backend architecture. Updated all core chapters (01–13, 15) and README to describe MRRC Modern /
SDD V2.19 | 2026-08-17 | Kimi | Pluggable multi-radio architecture: Icom IC-7300/IC-7300MK2 backend alongside FT-710 (AD-016).
SDD V2.18 | 2026-08-16 | Kimi | FT710Android native client implemented + web UI compaction (compact canvases, page scroll, RF Gain into menu Settings). New peer subproject
SDD V2.17 | 2026-08-15 | Codex | v1.8.0 Windows Stable publication. Published the commit
SDD V2.16 | 2026-07-31 | Codex | v1.7.8 Windows Stable publication. Published the commit
SDD V2.15 | 2026-07-31 | Codex | Windows TX end-to-end audit and diagnosability hardening. Runtime simulations reconfirmed the 48 kHz browser/Opus contract, exact 960→882/20 ms device conversion, 64 kbps CBR packet flow, and fixed 44.1 kHz Windows PortAudio stream/reinitialization behavior. Fixed a frontend reconnect defect where
MRRC_RADIO_MODEL. Added the radio-model selector (ft710/ic7300/ic7300mk2) with a usage comment to windows/default.env and macos/default.env so the first-run generated config shows how to pick the backend; default remains ft710 (matches config.RADIO_MODEL). Also syncs the already-installed VM instance. Suite 596 tests. |SDD V2.21 | 2026-08-17 | Claude | MRRC Modern rebrand + env migration + macOS packaging fix.
FT710_* environment variables renamed to MRRC_* with automatic legacy fallback in config.py (_env/_env_int/_env_float) so existing deployments keep working; launchers write MRRC_* and read both prefixes. AUTH_COOKIE → mrrc_auth (frontend synced); logger namespaces ft710.* → mrrc.*; server/UI/scripts/website branding cleaned to MRRC Modern (static asset filenames kept). macOS packaging repaired (packaging/macos/build.sh referenced renamed specs and failed) and rebranded to MRRC-Modern-Server/MRRC-Modern-Launcher/MRRC-Modern.app; ft710_launcher.spec → mrrc_modern_launcher.spec. Website redeployed with updated guide/fde/SDD pages. Suite 596 tests. |SDD V2.20 | 2026-08-17 | Kimi | SDD generalized for multi-radio backend architecture. Updated all core chapters (01–13, 15) and README to describe MRRC Modern /
mrrc_modern scope, MRRC_RADIO_MODEL/IC7300_CIV_ADDR env vars, FT-710 vs IC-7300/MK2 operational differences, backend-specific spectrum paths, per-backend audio sample rates, and pluggable backend components. Historical FT-710-Hamlib-Gap-Analysis.md and FT-710-specific AD-014 retained unchanged. Quick Facts bumped to V2.20. |SDD V2.19 | 2026-08-17 | Kimi | Pluggable multi-radio architecture: Icom IC-7300/IC-7300MK2 backend alongside FT-710 (AD-016).
backends/base.py defines RadioBackend ABC (CAT surface), RadioCapabilities, and ScopeProducer protocol with defaulted hooks (bands/ui_modes/filter_tables/poll-item lists/init_scope/create_scope_producer); create_backend(model) factory selected by MRRC_RADIO_MODEL (default ft710). New backends/ic7300/: civ_codec (pure CI-V framing/BCD/0x27 scope segments), civ_controller (async demux over USB CI-V 115200 8N1 addr 0x94, IC7300_CIV_ADDR), civ_scope (475 bins → scale 160→255 → upsample 850 → ScopeHandler), config_ic7300. FT-710 modules moved to backends/ft710/ with root compat shims; fullState gains radioModel/radioDisplayName/capabilities; frontend adapts bands/modes/FIL1-3/ATT-PRE/meters/scope banner; audio device rate + name hints parameterized per backend (IC-7300 48 kHz native, no resample); server mode/band/tune branches backend-aware. 5 new test modules (test_backend_factory, test_config_ic7300, test_civ_codec, test_civ_controller, test_civ_scope); suite 592 tests. IC-7300 hardware-verification TODOs (S-meter top point, TUNE carrier, MK2 transceive) flagged in code comments. Docs: AD-016, AGENTS.md module/env tables, tests/README. |SDD V2.18 | 2026-08-16 | Kimi | FT710Android native client implemented + web UI compaction (compact canvases, page scroll, RF Gain into menu Settings). New peer subproject
FT710Android/ (Kotlin 2.0.21 + Jetpack Compose Material3, minSdk 26, AGP 8.7.3 / Gradle 8.9 / JDK 17, OkHttp 4.12, kotlinx-serialization, DataStore, NDK-built libopus) connects to the same FastAPI server over the 4+1 WS channels (/WSradio JSON control, /WSaudioRX//WSaudioTX tagged audio, /WSspectrum 1701B frames, optional /WSatr1000) and is functionally aligned with the web frontend: frequency/mode/band/filter/gain/DSP/ATU control, RX Opus decode → AudioTrack with jitter buffer, TX AudioRecord → Opus CBR 64 kbps, waterfall+FFT spectrum, S-meter + 5 meters, 6-slot memory channels (server format, label key), DataStore settings + Keystore credentials, self-signed-TLS tolerant client. PTT safety is first-class from day one: pure-Kotlin PTTManager state machine (press only in Idle/Releasing + control channel connected), gesture-finally release, 500 ms watchdog ×3 with onStuckTX, lifecycle onStop forceRelease() — the iOS P0 race classes from docs/IOS_APP_ANALYSIS.md are designed out, not patched. Auth: POST /api/auth/login → token + ft710_auth cookie; 401/429 stops reconnect (login screen). Pure logic (protocol, PTT, spectrum frames, memory channels, RadioState mirror) is JVM-testable: 33 JVM tests green, ./gradlew test assembleDebug lintDebug clean; instrumented Opus round-trip and RF acceptance pending the operator’s real Android device. Design spec + 19-task plan (docs/superpowers/specs|plans/2026-08-16-ft710-android-app*.md) marked implemented. Web UI: waterfall/FFT canvases compacted (mobile 67→45 px / 33→22 px, desktop 120→80 / 60→40 px), page scrolls instead of a fixed viewport (body height:100%/overflow:hidden removed; .app-container height → min-height:100dvh), RF Gain slider moved from the main controls row into the menu Settings (menu section renamed “Scope Display” → “Settings”). Cache bust css v20→v23 / ui v22→v24 (main v23 unchanged), sw.js CACHE ft710-v23→ft710-v24 + ASSETS list synced; pinned cache-bust tests updated in the same commit. Suite 439 tests. Docs: §3.3 scope note, SDD README Quick Facts, website SDD pages (en/zh), CHANGELOG v1.8.1. |SDD V2.17 | 2026-08-15 | Codex | v1.8.0 Windows Stable publication. Published the commit
4ce4d26 Windows x64 installer after 439/439 Windows tests, three successful PyInstaller targets, and Inno Setup compilation; required bundled-file inspection (FTDI DLLs, opus.dll, static, mem_channels.json) and SHA-256 checks passed. Artifact: 36,888,086 bytes, SHA-256 36a48a5f3f325d112937751bddcdebc581039d0484a40c95c1b00fd4bcc170ea. Adds the RF Gain slider (RG 0-255 → 0-100%), stops false ‘radio disconnected’ alarms from transient serial timeouts (serial_connected now flips off only via the CAT watchdog), and hardens switch/stop scripts (case-insensitive pgrep + serial-holder fallback, AD-008). Stable classification is a publication decision; the Win11 KVM had no authoritative physical FT-710 RF speech/noise path, so over-the-air monitoring remains an operator acceptance check rather than claimed build evidence. Website and Windows operator/packager docs synchronized. |SDD V2.16 | 2026-07-31 | Codex | v1.7.8 Windows Stable publication. Published the commit
8629f0c Windows x64 installer after 439/439 Windows tests, three successful PyInstaller targets, Inno Setup compilation, required-file inspection, and silent install/server-listen/auth-boundary/uninstall smoke verification. Artifact: 36,820,041 bytes, SHA-256 c1e474b58f9948206990efbc9f8bdb5b183d03e4f462828b56d2d3f8c0b493bb. The release retains AD-011’s fixed 48 kHz codec → 44.1 kHz FT-710 device bridge on Windows. Stable classification is a publication decision; the Win11 KVM had no authoritative physical FT-710 RF speech/noise path, so over-the-air monitoring remains an operator acceptance check rather than claimed build evidence. Website and Windows operator/packager docs synchronized. |SDD V2.15 | 2026-07-31 | Codex | Windows TX end-to-end audit and diagnosability hardening. Runtime simulations reconfirmed the 48 kHz browser/Opus contract, exact 960→882/20 ms device conversion, 64 kbps CBR packet flow, and fixed 44.1 kHz Windows PortAudio stream/reinitialization behavior. Fixed a frontend reconnect defect where
_intentionalClose was declared const but reassigned, which could abort socket cleanup and leave a stale TX owner. Same-session replacement /WSaudioTX sockets now explicitly supersede the old owner; different sessions cannot steal on connect. The 400 ms server queue now counts oldest-frame drops and emits queue_drops in every TX-session release log, exposing the Windows slow-output/crackle signature directly. Four regressions added; suite 439 tests. Software-chain verification is complete; final RF speech/noise acceptance still requires the physical Win11 + FT-710 path. Docs: §9.4, §15, README, AGENTS, tests/README, CHANGELOG. || SDD V2.14 | 2026-07-28 | Codex | Windows TX restored to the fixed 44.1 kHz FT-710 device domain; V2.9 exception withdrawn. Deep review showed that the same-name WASAPI endpoint’s defaultSampleRate=48000 is the Windows shared-mode audio-engine mix rate, not proof that the FT-710 USB hardware accepts a 48 kHz device stream. The V2.9 KVM pacing experiment is retained as incident evidence but was not an authoritative RF/audio-quality test and must not override AD-011. start_tx() now keeps the selected output device at 44100 Hz with 882 frames per 20 ms on every platform; feed_tx_audio() always converts each decoded 48 kHz/960-sample frame to 44.1 kHz/882 samples (1764 bytes), including after PortAudio reinitialization and device re-enumeration. Removed _wasapi_tx_variant and 48 kHz pass-through. Preserved the Windows TX→RX capture reopen and v1.7.7 PortAudio recovery. Regression coverage locks the selected-device rate, byte budgets, exact SRC frame size, stale-rate immunity, and reinit behavior; suite remains 435 tests. Installer metadata moves to private v1.7.8 test build; actual RF speech/noise acceptance and public release remain pending the operator’s FT-710 test. Docs: AD-011, §9.4, AGENTS, README, Windows installer guide, tests/README, CHANGELOG. |
|---|---|---|---|
| SDD V2.13 | 2026-07-28 | Kimi | Header power switch WITHDRAWN before release — FT-710 CAT power control is too fragile for a remote UI. V2.11 added the button, V2.12 hardened it; day-2 guarded live testing delivered the verdict: with the radio warm, CAT healthy, and all guards active, a single PS0→PS1 cycle failed — 3 verified PS1 attempts over 36 s never woke the radio (PS1 attempt 1/2/3: radio not answering in the server log), and it again needed physical intervention. Combined with the V2.12 MCU-wedge incident, the feature cannot meet the reliability bar for remote operation, so per the user’s standing instruction the UI button is removed (header vfo-indicator restored). Kept: (1) the power WS command for the maintenance scripts (_power_cycle*.py), now with the V2.12 guards (15 s boot window, PS1 retry≤3× + FA read-back verify, PS0 double-send, TX-active refusal) so the script path cannot wedge the MCU either; (2) Tier-3 PS; poll for truthful power_on state; (3) the PortAudio re-init fix — that one is independently load-bearing (every power cycle re-enumerates the sound card). Also learned: the 36 s synchronous verify loop inside the WS handler starves that connection’s keepalive (server closed the client with 1011 ping-timeout) — acceptable on the maintenance path, would have needed a background task for UI use. test_power_switch.py stays as the guard’s regression coverage; suite 435 tests. Docs: §10.4 power row, CAT knowledge base PS warning, CHANGELOG v1.7.7 rewritten. |
| ——— | —— | ——– | ——— |
| SDD V2.12 | 2026-07-27 | Kimi | Power-command hardening + PortAudio re-enumeration survival (live power-cycle test fallout). Reliability testing of the V2.11 header power switch on the real radio produced two field failures. (1) Boot-interrupted PS0 wedges the CAT MCU: a rapid PS0→PS1→PS0 sequence (PS0 landing seconds after PS1, mid-boot) left the radio with serial/audio/scope USB all enumerated but CAT permanently deaf — direct serial probes (FA;/ID;/PS;, both CP2105 channels, all DTR/RTS combos) answered nothing; only a physical power cycle recovers. A timing probe showed the healthy envelope: PS1 20 s after PS0 boots the radio in ~4 s. Guards added to the power set handler: 15 s boot window after PS1 during which PS0 is rejected with a UI error; PS1 retried ≤3× and verified by a real FA; query before power_on flips (a lost fire-and-forget PS1 previously left the radio silently off); PS0 sent twice; power-off refused while transmitting. (2) Audio dies after any radio power cycle: the FT-710’s whole USB hub (sound card included) re-enumerates on power-off/on, invalidating the CoreAudio device IDs cached inside PortAudio at Pa_Initialize time — every TX/RX stream open then failed -9999 paUnanticipatedHostError until a server restart (the “TX audio device unavailable” field report). audio_handler._reinit_pyaudio() (terminate + re-init) is now invoked once on TX/RX open failure, with device index re-resolved by name before retrying; index-locked FT710_AUDIO_RX/TX_DEVICE=<n> configs remain fragile across re-enumeration — name locking recommended. Diagnosis note: py-spy on the wedged server proved the poll loops were alive and querying (all timing out) — the “stall” was radio-side, not a scheduler deadlock. 12 new tests (test_power_switch.py 9 + PortAudioReinitTests 3); suite 435 tests. Docs: §10.4 power row, tests/README counts + module list, CHANGELOG v1.7.7. |
| ——— | —— | ——– | ——— |
| SDD V2.11 | 2026-07-27 | Kimi | Radio power switch in the web UI header (CAT PS), replacing the VFO indicator. The power set-command path already existed end-to-end (cat.set_power → PS0;/PS1;, power_on in RadioState + initial sync, iOS app consuming it) but the web UI had no control. Frontend: the header vfo-indicator span is replaced by a btn-radio-power ⏻ button (amber = on, red = off) — VFO-A/B identity remains visible as the active button in the VFO control row; OFF is guarded by a confirm dialog, ON is instant (the radio’s soft-off keeps the CAT interface alive, so PS1; wakes it — same path as _power_cycle.py). Backend: the power set handler now calls skip_next_poll("power_on") before the CAT write (V1.7 stale-read guard), and PS joins the Tier-3 settings poll (14 queries) so front-panel power changes sync back — no response while off, skipped by the if resp guard. Poll behavior while powered off is unchanged: 5 failed IF cycles → serial_connected=False, first answered poll flips it back. Cache-bust css v19 / main v22 / ui v21 + sw.js ft710-v22; two pinned-version tests updated; suite 423 tests. Docs: §10.4 power row, §9.6 Tier-3 list, README polling table, tests/README counts, CHANGELOG v1.7.7. |
| ——— | —— | ——– | ——— |
| SDD V2.10 | 2026-07-26 | Kimi | HTTPS by default on Windows — self-signed bootstrap (minimal approach). Root cause of “HTTPS can’t be enabled on a fresh install”: windows/launcher.py hardcoded --no-ssl in build_command(), and server.py only enables SSL when cert+key already exist — a fresh install has neither (the Win11 KVM rig only got HTTPS via a bypass ps1 that ran ft710-server.exe directly with FT710_SSL_CERT/KEY). Per user direction this is the minimal fix, not a local-CA/trust-chain design: new ssl_bootstrap.py generates a throwaway self-signed cert on first run (ECDSA P-256, 10-year, SANs = localhost / hostname / hostname.local / 127.0.0.1 / ::1 / detected LAN IPs, idempotent reuse, graceful None when cryptography missing); launcher ssl_material() honours explicit FT710_SSL_CERT/FT710_SSL_KEY first, FT710_SSL=off restores HTTP; build_command(ssl_pair) passes --ssl-cert/--ssl-key instead of the hardcoded --no-ssl; local_url(secure=) picks the scheme; wait_for_server probes with an unverified context (self-signed isn’t trusted by any store). Why HTTPS matters off-localhost: plain HTTP on a LAN address is not a secure context — browsers disable AudioWorklet and getUserMedia (the exact failure seen during remote phone testing). cryptography>=41 uncommented in requirements.txt (PyInstaller bundles it via the launcher spec’s repo-root pathex). server.py unchanged (existing --ssl-cert/--ssl-key + file-exists logic). 12 new tests (test_ssl_bootstrap.py + launcher SSL tests); suite 421 tests. Docs: AGENTS.md module table, tests/README counts, CHANGELOG v1.7.6, WINDOWS_INSTALLER_GUIDE HTTPS note, win_pack.md §2.4 sidecar scripts marked superseded. |
| ——— | —— | ——– | ——— |
| SDD V2.9 | 2026-07-26 | Kimi | Windows TX crackle fix — WASAPI 48 kHz output (Win11 KVM field report: transmitted audio “咔啦咔啦” noise instead of voice). Measured on the rig: the C-Media codec’s MME 44.1 kHz playback path paces ~1.4× slow (50×20 ms stream.write() block 1.36–1.42 s instead of 1.00 s at 44.1/48/32/16/8 kHz; only 22050 Hz paces 1.02), so the TX drain loop falls behind, the 400 ms queue cap drops 24–34 % of voice frames (TX session written/fed 330/432, 195/296 — macOS production shows 96–97 %), chopping transmitted audio into crackle. The codec’s WASAPI entry at its native 48 kHz mix rate paces correctly (0.96). Fix: start_tx() prefers the same-name WASAPI entry on Windows (_wasapi_tx_variant) and opens the stream at that entry’s native rate; feed_tx_audio passes 48 kHz PCM straight through at 48 kHz (no 48→44.1 resample) and pre-buffer/cap/graceful-drain budgets are computed from the actual stream rate (getattr fallbacks keep module constants for 44.1 kHz). macOS unchanged (CoreAudio device domain stays 44.1 kHz); RX capture stays MME 44.1 kHz on Windows (paces fine). v1.7.5 hotfix: the v1.7.4 branch referenced sys.platform without the module import (NameError on every PTT) — new end-to-end StartTxWindowsTests cover start_tx on both platforms. AD-011 amended: device-domain rate is host-API-dependent, not universally 44.1 kHz — this overrides the audio-pyaudio-rate warn rule with runtime evidence. 7 new tests (WindowsWasapiTxTests + StartTxWindowsTests); suite 409 tests. Docs: AD-011, AGENTS.md module table, tests/README counts, CHANGELOG v1.7.4/v1.7.5. |
| ——— | —— | ——– | ——— |
| SDD V2.8 | 2026-07-26 | Kimi | Windows RX audio dies after first PTT (full-duplex wedge) + scope resync that actually works (Win11 KVM field report: audio perfect after server restart, gone after one PTT). RX root cause: on Windows (MME/DirectSound), opening the TX playback stream on the FT-710’s C-Media USB codec silently wedges the RX capture stream — it stays open and error-free but delivers silence; macOS CoreAudio is unaffected (hardware capture verified healthy, max 43% FS, with the server stopped). Fix: AudioHandler.restart_rx() reopens the capture stream on every TX→RX transition, hooked in _broadcast_state on tx_status dirty (covers PTT/TUNE/physical PTT/dead-man), Windows-only no-op elsewhere (macOS must not pay the reopen cost per PTT). Scope root cause: the byte-by-byte sync_stream resync (wfview port) could never succeed on the FT4222 — every 1-byte SingleRead is its own SPI transaction (CS toggles per call), making a contiguous multi-byte sync pattern unobservable; it only consumed the stream and churned recovery into fatal:too_many_reinits after every PTT (V2.7’s TX-pause resume path used it). Replaced with resync_device() (close → 1 s idle-bus settle → reopen — the pattern that empirically realigns, same as a pipe restart), used by both the TX→RX resume and the bad-frame path; MAX_SYNC_ATTEMPTS/sync_attempts_this_device removed. AD-008 amended (Windows full-duplex wedge + restart_rx). 4 new tests (RestartRxTests); suite 402 tests. Docs: AD-008, AGENTS.md module table, tests/README counts, CHANGELOG v1.7.3. |
| ——— | —— | ——– | ——— |
| SDD V2.7 | 2026-07-26 | Kimi | TX-safe spectrum: scope_pipe TX pause + Windows process-tree kill (field log from the Win11 KVM test rig: after every PTT the waterfall showed fallback/garbage for 30–45 s). Root cause: the FT-710 garbles its scope stream during TX (normal radio behavior), but scope_pipe was TX-agnostic — it kept reading, tripped sync_lost → spi_stalled device re-init → more sync failures → fatal:too_many_reinits, then a server-side pipe restart before real FFT frames returned. Second defect surfaced by the same log: on Windows, killing the PyInstaller onefile bootloader (proc.terminate()) orphaned the real worker, which kept the FT4222 open, so the next pipe failed FT_OpenEx with FT_DEVICE_NOT_FOUND for ~10–15 s (matches the long-standing code comment). Fixes: (1) the pipe protocol gains a stdin control channel — the server pushes TX:1/TX:0 on every tx_status dirty broadcast (_notify_scope_pipe_tx, covers PTT/TUNE from any source, re-synced with force=True on pipe start); while TX is active the pipe pauses SPI reads and freezes all sync/stall/reinit counters (tx_pause STATUS), and on TX→RX runs one clean sync_stream re-sync (tx_resume:resync); (2) Windows pipe teardown uses taskkill /PID <pid> /T /F (_terminate_process_tree_sync, POSIX keeps SIGTERM), and the pipe treats stdin EOF (parent died) as a shutdown signal — a second orphan guard alongside the stdout-heartbeat EPIPE path. Frontend: waterfall/FFT canvases show “TX 发射中 — 频谱暂停” while tx_status != 0 instead of stale rows. AD-005 amended (stdin control channel + tree kill), §9.5.1 diagram updated, UC-004 flow unchanged from the client’s perspective. 13 new tests (test_scope_pipe_tx.py: control-line parsing, TX-notify transitions/force/dead-pipe guard, taskkill vs SIGTERM); suite 398 tests. Docs: AD-005, §9.5.1, AGENTS.md module table, tests/README counts, CHANGELOG v1.7.2. |
| ——— | —— | ——– | ——— |
| SDD V2.6 | 2026-07-26 | Kimi | Windows RX/TX audio device lock + setup guide. Field report: Windows install with working CAT/FT4222 spectrum had broken audio — the FT-710’s built-in USB sound card enumerates on Windows under a generic name (no “FT-710”/“YAESU” in it): USB Audio CODEC on some driver/OS builds, USB Audio Device on others (confirmed on the reporter’s machine), possibly localized like 麦克风 (USB Audio Device) or prefixed 2- USB Audio CODEC. AD-008 name matching missed it and the mono/full-duplex heuristics grabbed a laptop mic (RX) or PC speakers (TX). audio_handler.py adds a generic USB-audio name tier (USB_AUDIO_NAME_HINTS, case-insensitive substring) after the FT-710-specific names and before the channel heuristics, for both _find_rx_device and _find_tx_device; multiple matches (per-host-API duplicates open the same hardware) use the first and log a warning pointing at the env-var lock. windows/default.env now pre-locks FT710_AUDIO_RX_DEVICE/FT710_AUDIO_TX_DEVICE=USB Audio — the common substring of both enumeration forms (name is reboot-stable; index is not). docs/WINDOWS_INSTALLER_GUIDE.md gains an “Audio (RX/TX) Setup” section: device identification from the startup PortAudio device list, env-var locking, Windows sound-panel guidance (never make the radio’s USB audio the default playback device — system sounds would modulate the transmitter; optional 16-bit/44.1kHz formats), and the radio-side modulation routing verified against the FT-710 Operation Manual — RADIO SETTING → MODE SSB (and AM/FM/PSK-DATA) → MOD SOURCE = USB (default AUTO selects by transmission method), RPTT SELECT stays OFF since PTT is CAT-keyed. Troubleshooting table gains 4 audio rows. SDD: AD-008 decision/rationale, NFR-065, §12.2 config table, I2 marked Resolved (implemented under the FT710_AUDIO_RX/TX_DEVICE names). 8 new tests (USBCodecDeviceSelectionTests); suite 385 tests. Docs: tests/README counts, AGENTS.md module table, README env examples. |
| ——— | —— | ——– | ——— |
| SDD V2.5 | 2026-07-26 | Codex | Windows installed-app diagnostics and runtime discovery fixes after field log review. Root cause visible in the Windows console: default FT710_SERIAL_PORT=COM3 did not exist (FileNotFoundError), so CAT was offline and scope init could not run; CAT connect failure now logs the currently visible serial ports to satisfy R1 mitigation. Launcher local URL selection now maps IPv6 wildcard FT710_WEB_HOST=:: to http://localhost:<port> instead of probing 127.0.0.1, avoiding a false 15s startup warning when Uvicorn listens on [::]. opus_rx.py now searches packaged Windows opus.dll locations (opus.dll, _internal\opus.dll, vendor\opus\windows\bin\x64\opus.dll), and the Windows build script warns/copies vendor\opus\windows when present. Docs: Windows installer troubleshooting for COM3/CP210x, libopus, and localhost; README test count. Tests: 4 new regression tests; suite 377 tests. |
| ——— | —— | ——– | ——— |
| SDD V2.4 | 2026-07-25 | Kimi | Optional ATR1000 networked antenna-tuner linkage, ported from the sibling mrrc project. Default disabled: FT710_ATR1000_HOST (empty = off) + FT710_ATR1000_PORT (default 60001); when off there is no client task, no network traffic, linkage hooks short-circuit, /WSatr1000 closes with code 4000, and the frontend module stays inert — zero impact on existing installs. New backend modules: atr1000_client.py (asyncio-native WS client to the tuner hardware: binary frames [0xFF,CMD,LEN,DATA], 5s reconnect, 55-min proactive refresh, TX-no-SYNC policy with 15s watchdog, tuning-clear heuristics, 5s-throttled relay writes, LearningBuffer 4-sample stability-window learning, notify_freq/notify_tx sync hooks, on_change callback) and atr1000_tuner.py (TunerStorage LC-learning JSON store ported verbatim: learn gate SWR 1.0–1.8, 1kHz keys ±5kHz nearest, atomic writes; store file atr1000_tuner.json). 3 linkage behaviors: (1) freq change → auto-apply learned LC relays (hooked in _broadcast_state on vfo_a_freq/vfo_b_freq/active_vfo dirty — dirty holds source fields only); (2) TX on/off → notify_tx (device push mode, learning window); (3) server-side tune assist _atr_tune_assist(): TX2 carrier → skip if SWR≤1.6 → snapshot relays → full tune (mode=2) → keep+learn if SWR improved ≥0.02 else rollback relays → carrier always dropped in finally (module constants ATR_TUNE_*: 0.3s settle, 5s min, 45s deadline, 0.8s compare settle, 2.5s meter wait). New 5th WS endpoint /WSatr1000 (?token= auth, JSON both ways): server→client atrState (connected/power/swr/sw/ind/cap/ind_uh/cap_pf/tuning/tx/freq/last_update), atrTuneResult (start/skipped/success/rollback/error), pong; client→server atrTune, ping. fullState gains atr1000Enabled. Frontend: new static/modules/atr1000.js (WS client, exponential-backoff reconnect, meter row, ATR TUNE button) activated only via window.ATR1000.init(msg.atr1000Enabled===true); new hidden-by-default #atr-row in the meters section (power bar, SWR bar, LC L=x C=y relay text, tuning indicator, TUNE button with ··· progress + toast). Radio-internal TUNE (TX2+AC003) untouched. Architecture simplification vs mrrc: its 4-layer chain (browser→Tornado→unix socket→proxy→device) collapsed to a single-process direct async client; its browser-side tune assist moved server-side (avoids mrrc’s documented PTT-delay/main-thread-blocking incident); ATR data deliberately kept out of RadioState (separate channel, same precedent as spectrum); audio path untouched. Cache busting: sw.js ft710-v21, css v18, main v21, ui v20, settings_manager v13, atr1000.js v1. 98 new tests across 3 files (test_atr1000_tuner.py 36, test_atr1000_client.py 50, test_atr1000_server.py 12); suite 372 tests across 21 modules. Docs: AGENTS.md module table + env vars, Ch9 §9.8, Ch10 service row, Ch11 component rows, Ch15 tune-assist note, tests/README counts, README WS/env tables. Follow-up (2026-07-26): lifespan primes the linkage with the current freq/TX at boot (startup state sync predates the client); frozen-app store path — FT710_ATR1000_STORE env overrides the TunerStorage file (Windows launcher points it at the user data dir so learned data stays writable and survives reinstalls), PyInstaller spec hiddenimports both ATR modules; startup log line for the enabled feature. Suite 373 tests. |
| ——— | —— | ——– | ——— |
| SDD V2.3 | 2026-07-25 | Kimi | All frontend settings unified to cookie storage. Previously persistence was split: ft710_user_settings/ft710_afVol/scope display options (ft710_scopeFloor/Ceil/Theme) in localStorage, memory channels inconsistently in sessionStorage (ft710_main.js) and localStorage (ft710_ui.js). settings_manager.js now provides generic setCookie/getCookie helpers (1-year max-age, path=/, SameSite=Lax, URI-encoded) used by every call site; a one-time migration moves the six legacy localStorage keys plus the sessionStorage memory-channel copy into cookies on first load, then removes them. Side benefit: memory channels now survive browser restarts (sessionStorage did not). Mic gain now persists too: it was the one slider saved nowhere (radio-side CAT only, re-polled on every connect) — the slider’s change handler stores ft710_micGain in a cookie and _applySavedMicGain() pushes it back to the radio on every fullState when it differs from the polled value. New device-side mic gain (🎙 Vol): a software GainNode on the browser mic capture graph (all three TX capture paths: AudioWorklet, ScriptProcessor fallback, PCM fallback), slider 0–200 → linear 0–2× (100 = unity), persisted as ft710_micVol — browser-local like 🔊 Vol and independent of the radio’s CAT mic_gain. index.html cache-bust query strings bumped (settings_manager v13, main/ui v20) and sw.js cache ft710-v18→v20. 3 new tests (CookieSettingsPersistenceTests), cache-bust assertions v18→v19; suite 274 tests. Docs: AGENTS.md module table, Ch11 SettingsManager row, tests/README counts. |
| ——— | —— | ——– | ——— |
| SDD V2.2 | 2026-07-25 | Kimi | Windows installer deep-audit fixes (packaging chain reviewed end-to-end; PyInstaller 6.21.0 onedir layout reproduced locally for evidence). P0 — packaged web UI 404: PyInstaller 6 onedir places datas under _internal/, but STATIC_DIR resolved next to the exe, so the installed app served only the inline fallback login page and “Static files not found” (the V1.6.0 “web login OK” verification passed because the fallback page works). server.py gains _resource_dir() (sys._MEIPASS-aware, same pattern scope_libraries.get_resource_roots() already used) for STATIC_DIR; launcher’s starter-channel seeding also checks _internal/mem_channels.json. P1 — scope_pipe orphan: on Windows, proc.terminate() kills only the onefile bootloader; the real child inherited the stdout pipe, blocked forever on write, and held the FT4222 device. scope_pipe.py now emits the len=0 stdout heartbeat its docstring always promised (1 s) — a dead parent closes the pipe, the next write raises EPIPE, and the pipe exits through its normal cleanup. P1 — silent build failures: build.ps1 wrapped all native commands (python, pyinstaller, iscc) in Invoke-Checked ($LASTEXITCODE — $ErrorActionPreference never covered native commands), and no longer ships windows/__pycache__. P2 — launcher self-spawn chain: frozen launcher with a missing ft710-server.exe (e.g. antivirus quarantine) used to “fall back” to [sys.executable, server.py] = spawning itself recursively; it now reports the missing exe and exits 1. P2 — version drift: installer AppVersion 1.6.0 → 1.6.3 (app CHANGELOG v1.6.3); new packaging/windows/requirements-build.txt pins pyinstaller==6.21.0; installer guide updated. Stale cache-bust test assertions corrected (v17→v18). 9 new tests (ResourceDirTests, launcher seed/self-spawn tests, ScopePipeHeartbeatTests, build-script abort assertion); suite 271 tests. Docs: tests/README counts, AGENTS.md scope_pipe row, README test count. |
| ——— | —— | ——– | ——— |
| SDD V2.1 | 2026-07-22 | Kimi | “PTT keys but no voice / no RF power” soft failure fixed (long-uptime classic). Root cause: /WSaudioTX uplink ownership was assigned only at connect time — with multiple clients (browser tab + iOS app, or a zombie socket surviving a network flap), an idle first-connected client kept ownership while the PTT-ing client’s mic frames were silently dropped (ws is not _tx_owner_ws → continue); and once the owner disconnected, every remaining client stayed muted forever (no promotion). Log evidence: repeated Audio TX client connected (2-3 total, owner=False) clusters with frantic TX audio started retries, RM5=0/1 during keyed sessions. Fixes in server.py: (1) _promote_tx_owner() hands the uplink to a remaining client on owner disconnect; (2) _claim_tx_owner_for_token() — on PTT key-up the keying client (matched by auth token, now tracked per socket) claims ownership; (3) non-owner drops are counted and logged rate-limited. TX-path observability: per-session counters (frames received / decoded / decode-fail / non-owner drops in server; written / write_err / PCM peak via AudioHandler.tx_stats()) logged at every PTT release as TX session: … — the next silent failure will identify its own broken link from the log alone; first write error per session is now WARNING-level (was debug). Also reviewed: iOS TUNE button now runs the real tune sequence (TX2+AC003, press-and-hold), iOS RX AVAudioPlayerNode ScheduleBuffer channel-mismatch crash fixed (live output format + mono upmix), iOS header S-value dedup, _poll_if TypeError + serial_connected recovery (V2.0, same day). 5 new tests (TXUplinkOwnershipTests); suite 262 tests. Docs: Ch15 Layer 4 ownership note, tests/README counts. |
| ——— | —— | ——– | ——— |
| SDD V2.0 | 2026-07-21 | Kimi | Spurious “电台未连接” (radio disconnected) fixed. Live log (2026-07-21 22:48): every _poll_if cycle raised TypeError: unsupported operand type(s) for -: 'int' and 'NoneType' in the freq-change logging guard (abs(freq - _last_logged_freq) with _last_logged_freq=None, introduced 2026-07-17). The exception fired before state.update(), so every poll counted as a failure; after 5 consecutive failures serial_connected=False was broadcast and the waterfall showed “电台未连接 — 检查 USB / 电源” while the radio was in fact fully operational (user commands/band changes all OK). Two fixes in poll_scheduler.py: (1) None-guard the first-poll logging condition; (2) recovery path — a successful poll now flips serial_connected back to True (previously only the watchdog’s full reconnect did, so even a genuine transient timeout streak stuck the flag until reconnect). 1 new regression test (IFPollRecoveryTests); suite 257 tests. Docs: tests/README counts. |
| SDD V1.9 | 2026-07-21 | Kimi | Spectrum freeze after USB reconnect fixed. Live log (2026-07-21 22:18): the serial device briefly disappeared (USB re-enumeration), CAT auto-reconnected, but the spectrum never recovered — scope_pipe logged spi_stalled:reinit_1/5 and then silence. Root cause: the connection watchdog’s reconnect path re-synced radio state but never re-ran the scope-init CAT sequence (EX040101/EX040200); a USB re-enumeration resets the FT-710’s scope output, so the radio simply stops sending FFT frames and the FT4222-side SPI re-init cannot help. The waterfall then sits frozen on stale data while serial_connected=true, so the V1.8 “radio disconnected” hint never shows. Fix: PollScheduler gains an on_reconnected async hook invoked by _connection_watchdog after a successful reconnect + state re-sync; server.py wires it to _init_scope_cat. Hook failures are logged and non-fatal. 2 new regression tests (WatchdogReconnectTests); suite 252 tests. Docs: §9.6 Tier-5 line, Ch11 PollScheduler row, tests/README counts. |
| SDD V1.8 | 2026-07-21 | Kimi | Web frontend safety & UX overhaul (post-audit fixes). PTT wiring repaired: the PTT/TUNE buttons previously called handlePTTStart/End directly, bypassing PTTManager — pttActive never became true, so the browser watchdog, pagehide force-RX and unload beacon were dead code. Buttons (and Space-bar PTT) now route through PTTManager.pttStart/pttEnd/tuneStart/tuneEnd; the broken sendBeacon('/WSradio') (a WebSocket-only endpoint) was removed — tab-close force-RX is covered server-side by the last-client-disconnect dead-man switch. TUNE is now press-and-hold (was latch-on-click with no confirmation — an accidental-tap TX path). Keyboard guards: keydown handlers ignore e.repeat (held Space/arrow machine-gunned PTT/CAT) and ignore events from inputs (arrow keys in the frequency editor also tuned the radio). Silent render crash fixed: renderScopeSettings() called renderFreqScale(width) without the required range argument, throwing every update cycle (swallowed by the WS message catch, skipping VFO/PTT renders). Server errors surfaced: error WS messages now show a toast banner instead of console-only. AF slider semantics unified: 🔊 Vol is browser-side playback volume persisted in localStorage (ft710_afVol), decoupled from the CAT af_gain poll that used to fight the user’s setting; stale 2.5× comment corrected (actual 10× boost). Waterfall/FFT click-to-QSY: click maps x→frequency within the current span (8 px drag threshold guards scroll gestures). Desktop layout: ≥768 px gets a 720 px container, 120 px waterfall, 60 px FFT plot and larger controls (was a 480 px phone column everywhere); waterfall re-inits debounced on resize. Professionalism: S-meter label is now relative dB (S9=0) instead of misleading dBm; filter width tables are server-authoritative (fullState.filterTables from config.py) with the legacy hardcoded copies as fallback; lame.js (~500 KB) lazy-loads on first REC click instead of every page load. Asset cache bust to v17 (sw.js, index.html). Tests: lame eager-include test inverted to lazy-load assertion, cache-bust versions updated; 247 tests, failure count unchanged from baseline (1F+2E pre-existing). |
| SDD V1.7 | 2026-07-19 | Kimi | SSB filter switching race fixed (“works sometimes, ignores other clicks”). Root cause: _poll_settings checked skip_next_poll only BEFORE each Tier-3 query; a SH0; response already in flight when the user’s filter command arrived carried the pre-command width and was applied unconditionally at the end of the 13-query cycle (~0.5 s later), snapping the web UI back to the stale filter until the post-skip poll corrected it ~4 s on. Whether a click landed inside that in-flight window decided “works / doesn’t work” — the radio itself applied every SH00<NN>; correctly (live log: all sets ok=True, read-backs match). Fixes: (1) settings poll re-checks _should_skip(field) + _polling_paused() AFTER each query await and discards stale reads — same guard pattern _poll_if/_poll_vfo already had, now covering all 13 Tier-3 fields (filter, gains, preamp, att, NR, NB, AN, tuner, scope, antenna, AGC, meter display); (2) the filter/filter_width WS handler waits 150 ms after SH00<NN>;, reads SH0; back, and broadcasts the radio’s ACTUAL index (logged as Filter read-back: index=N (requested M)) so a silently-rejected index would self-correct in ~0.2 s instead of ~4 s — live verification showed N==M for all SSB indexes 9/13/17/20/23, confirming the width table. 3 new regression tests (test_settings_poll_discards_stale_reads_after_query, test_filter_set_reads_back_actual_width, plus the pre-existing skip-before-set ordering assertion); suite now 223 tests, all passing. Docs: §9.6, AD-009, §10.4 filter row, README polling note, AGENTS.md module table, tests/README counts. |
| SDD V1.6 | 2026-07-18 | Kimi | TX-chain documentation synchronization after end-to-end TX analysis, plus runtime fixes from the “SSB filter no effect” investigation. TX Opus params corrected: AD-004/Ch9.4/Ch11 said 28kbps CBR complexity=3; actual opus_codec.js config is 64kbps CBR complexity=5 (fidelity-priority voice). 44.1kHz device reality: AD-011 rewritten — the FT-710 USB audio interface is 44.1kHz native; audio_resample.py bridges the 48kHz codec domain ↔︎ 44.1kHz device domain (960↔︎882 frame-aligned). Ch9.3/9.4 chain diagrams, Ch4.4 flows, NFR-060, Ch10 TXAudioService, Ch11 component rows and AGENTS.md module table updated; stale docstrings/comments fixed in opus_rx.py (16kHz remnants), tx_opus_worker.js (SAB marked dormant, packet size), tx_capture_worklet.js (header). tx-gain-staging.svg documented as sunmrrc-lineage (its preamp/EQ/tanh gain staging does not exist in this codebase) — retained for reference only. RX zero-audio watchdog: AudioHandler.note_rx_chunk() tracks bit-exact-zero RX runs; ≥20s silent with squelch open → WARNING + new rx_audio_silent state field → status-bar “无声” indicator; “Audio loop” log now includes chunk peak. (Root cause of the filter report: the FT-710 USB audio was streaming digital zeros — wedged codec — so no filter change was ever audible; the watchdog makes this class of failure visible.) WS auth-failure redirect: the server rejects bad-token WS upgrades with HTTP 403 before accept, so browsers see 1006, never 4001 — /WSradio onclose now verifies via /api/auth/check and redirects to /login instead of retrying a stale token forever. TX-owner dead-carrier fix: /WSaudioTX owner disconnect during TX now forces TX0; (previously only stopped audio); Ch15 Layer 4 updated. |
| SDD V1.5 | 2026-07-18 | Kimi | Documentation synchronization after deep SDD review. PTT safety chapters aligned with runtime: the triple TX0 verify loop was removed in V1.2 but Ch1/Ch2/Ch3/Ch4/Ch6/Ch8/Ch13/Ch15/README still described it — all updated to the actual 7-layer model (fire-and-forget TX0; + 500ms TX-status poll + browser watchdog). Ch15 rewritten with layer renumbering and a removal note. Polling naming: “5-tier” corrected to 7-task polling in Ch1/Ch3/Ch4/Ch7 and AGENTS.md. TX audio: Ch4.4 TX flow corrected to 48kHz (was 16kHz, pre-AD-011); tx-gain-staging.svg Opus label likewise. Scope clarified (Ch3.3): FT710Mobile/ iOS app and website/ noted as in-repo but outside this SDD’s scope. Tuner AC discrepancy flagged: gap-analysis §1.1 now notes this codebase’s deliberate AC000/AC001/AC003 mapping contradicts the Hamlib AC010/AC011 claim — pending radio re-verification; cross-reference added in Ch10.4. Frame size fix: Ch1 spectrum v1 frame 850B→851B. New open issues (Ch13.4): I6 multi-client control arbitration, I7 mem_channels.json validation/backup. README Quick Facts bumped to V1.5. |
| SDD V1.4 | 2026-07-12 | Claude | FFT spectrum line plot: new 33px canvas above waterfall renders real-time amplitude-vs-frequency polyline from shared wf1 data. Cyan (#06b6d4) line with 1px width + subtle glow, EMA smoothing (Float32Array buffer, α=0.30, 2× boost), horizontal grid at 25/50/75% amplitude, vertical grid aligned with frequency scale ticks. Shared Floor/Ceil controls with waterfall. Waterfall refinements: VFO red line thinned 1.0→0.2px, frequency label removed (now scale-only). install.sh: added --dev flag for dev dependency installation, --no-audio and --no-scope flags. Documentation: Chinese introduction article with setup guide for non-technical users, project screenshot added to README and website, FFT feature cards on website index and SDD page, comprehensive FAQ. |
| SDD V1.3 | 2026-07-08 | Codex | Runtime/documentation synchronization for CAT control, polling, and website docs. Priority CAT preemption: introduced send_priority_set_command() with _cancel_polls cooperative abort so latency-sensitive TX/TUNE commands preempt queued poll reads/waits. TUNE sequence fix: tune=true now performs TX2 then AC003 (start tuning); tune=false sends AC000 then exits TX. FT-710 CAT corrections: tuner mapping corrected to AC000/AC001/AC003; compressor control kept at PR00/PR01 (Yaesu PDF errata noted). State model expansion: added meter_display (MS), amc_level (AO), rf_gain (RG0), and RI-derived telemetry (hi_swr, recording_status, rx_tx_status, tuner_tuning, scan_status, squelch_open). Polling updates: scheduler now runs 7 tasks (IF, VFO, TX status, TX meters, settings, slow telemetry, connection watchdog), with RM3 included in TX meter polling and poll-side awareness of _cancel_polls. Calibration update: power calibration dead-zone entry removed so low-power modulation reports non-zero values earlier. UI memory recall: selecting a memory channel now applies both frequency and stored mode. Website docs: added dedicated SDD HTML pages (website/sdd.html, website/zh/sdd.html) and linked from landing pages. |
| SDD V1.2 | 2026-07-08 | Claude | Major bug fixes and performance optimization. Freq drift fix: DN; is FT-710 active-VFO step-DOWN (~20 Hz), not DNR — removed from settings poll. Active-VFO tracking: added VS/FB polling (0.5 s), freq web command targets active VFO. TX meter fix: FT-710 RM response is 9 chars RM+id+6 digits with raw 0-255 in first 3 digits (was resp[4:]), added piecewise-linear calibration (Power/SWR/Vd/Id from FT-710.rig). PTT latency: 5→3 queries in IF poll, inter-query pause checks, poll timeout 0.25 s, release verify loop removed, TX_DRAIN 150→50 ms. TX audio: reverted background start_tx (race with queue-clearing caused SSB zero-power). RX audio: removed blocking stderr diagnostic prints (30 fps flush), parallelized WS audio sends via asyncio.gather. CAT robustness: AI1 disabled (interfered with FA/MD0/SM0), AI0 sent before ID query. restart.sh: fixed .env source. Updated gap-analysis doc (DN correction). |
| SDD V1.1 | 2026-07-06 | Claude | Bug fix: TX audio chain unified to 48 kHz (was 16 kHz mic → 48 kHz playback = crackling). Enhanced _find_rx_device() with multi-layer detection (config env var, name match, mono-channel heuristic, full-duplex TX). Enhanced _find_tx_device() with same heuristics. opus_rx.py: TX_RATE 16k→48k, added TX_FRAME_SAMPLES constant. tx_opus_worker.js: FRAME_SIZE 320→960, OpusEncoder 16k→48k. ft710_main.js: getUserMedia + AudioContext sampleRate 16k→48k (both main + fallback). TX Opus encoder optimized for voice: CBR 28kbps, VBR/FEC/DTX/HPF disabled. Added SSL CLI args. Event-based power button (mobile-first connect). Added AD-011 (Unified 48kHz TX audio pipeline). AudioContext gain explicitly set to 1.0. iOS unlock oscillator during user gesture. |
| SDD V1.0 | 2026-07-06 | Claude | Initial SDD baseline for MRRC FT-710 codebase. All 15 chapters written from scratch based on current repository state: server.py, cat_controller.py, audio_handler.py, opus_rx.py, radio_state.py, poll_scheduler.py, scope_handler.py, scope_pipe.py, config.py, static/ (HTML, CSS, JS, modules, worklets, worker). Documents: Full CAT control, FT4222 scope + S-meter fallback, Opus/PCM dual-codec bidirectional audio, AudioWorklet RX playback, TX mic capture, 5-tier polling, dirty-field state broadcasting, session auth, memory channels, PTT safety architecture. |
v1.24.4 — 2026-10-02 — 一个 hub、一个地址、入口就是呼号
- 门户收敛为
https://portal.mrrc.vlsc.net/(根路径);配置里写着:8899、www.vlsc.net/mrrc_portal或带/mrrc_portal的旧写法自动改道(:8899实测从未对公网开放,这正是”申请成功但状态 刷不出来”的根因 —— 申请走了边缘兜底,其余请求直连 8899 全部失败)。 - 实例入口不再带
:9988:与其他服务同在 443。 - 入口标签 = 裸呼号(
bg9aaa而非bg9aaa-mrrc-modern);原因见上。 - 启动前清理本实例残留的 frpc(实测:被杀的应用会留下 frpc 占着同名代理,下一次注册不上)。
- 接入成功后的回执/状态带
cert_reload_required,界面据此提示并给出重启入口 —— TLS 上下文是 进程启动时建好的,新签的证书不重启不会被采用(实测:入口会一直 502)。
Design Baseline Notes
- Radios: Eleven registered models, all superheterodyne transceivers (not SDRs), selected by
MRRC_RADIO_MODEL: Yaesu FT-710 (verified, default); Icom IC-7300 / IC-7300MK2 (verified); Icom IC-705 / IC-7610 / IC-7760 and Yaesu FTDX10 / FTDX101D / FTDX101MP / FTX-1F / FT-891 (profiles written from offline rig data, never checked against hardware — transmit is refused untilMRRC_ALLOW_UNVERIFIED_TX=1, AD-019). The registry inbackends/__init__.pyis the validation source of truth. - Radio Protocols: Direct serial CAT (FT-710) or CI-V (IC-7300/MK2) via pyserial — no Hamlib, no rigctld, no TCI.
- Scope: FT-710 uses the FTDI FT4222 SPI chip for 850-point FFT data; the Icom family uses CI-V 0x27 frames on the same serial port. The Yaesu ASCII-CAT family (FTDX10 / FTDX101D / FTDX101MP / FTX-1F / FT-891) has no scope protocol at all, so it only ever shows the synthetic spectrum. Every backend falls back to the synthetic S-meter Gaussian spectrum when real data is unavailable.
- Audio: Bidirectional through the selected radio’s built-in USB audio interface via PyAudio. FT-710: 44.1kHz native device rate; IC-7300/MK2: 48kHz native. Opus codec (RX 64kbps, TX 64kbps CBR, both 48kHz) with Int16 PCM fallback.
audio_resample.pybridges the 48kHz codec domain ↔︎ 44.1kHz device domain for the FT-710 only (960↔︎882 frame-aligned). - Auth: Shared-password session auth, single role split into operator / listen-only (
MRRC_LISTEN_PASSWORD). Cookiemrrc_auth(30-day); transport is header → cookie → deprecated?token=(AD-024 — no frontend builds a token URL any more, and the query form exists only so already-installed native clients keep working until they ship the header path). - PTT Safety: Client-side PTT watchdog (500ms) + dead-man switch (WS disconnect) + pagehide handler + server-side TX status poll (0.5s). No TX0 verify loop on release (radio obeys TX0 fire-and-forget). TX audio drain=50ms. TUNE is press-and-hold.
- Polling: 7-task adaptive scheduler: IF 0.1s (FA/MD0/SM0), VFO 0.5s (VS/FB), TX status 0.5s, TX meters 0.5s (RM3/4/5/6, TX only), settings 2s, slow 5s, connection watchdog 1s. Poll queries use 0.25s timeout and cooperate with priority-command preemption (
_cancel_polls). Poll loops re-check skip state AFTER each in-flight query response and discard stale reads; filter sets are verified by a 150 msSH0;read-back. After a successful watchdog reconnect the scheduler re-runs the scope-init CAT sequence (EX040101/EX040200) via itson_reconnectedhook — a USB re-enumeration resets the radio’s scope output. - Meters: RM3-8 raw 0-255 values calibrated via piecewise-linear tables from FT-710.rig (Power/SWR/Vd/Id). Frontend displays watts, SWR ratio, volts, amps — not raw numbers.
Key Architecture Decisions
| ID | Topic |
|---|---|
| AD-001 | FastAPI/Uvicorn backend |
| AD-002 | Direct serial CAT (no Hamlib) |
| AD-003 | Dirty-field state broadcasting |
| AD-004 | Tagged dual-codec audio (Opus + PCM) |
| AD-005 | scope_pipe standalone subprocess |
| AD-006 | Dual-mode spectrum (FT4222 + fallback) |
| AD-007 | PTT release safety flow (simplified: no verify loop, 50ms drain) |
| AD-008 | PyAudio FT-710 auto-detection |
| AD-009 | 7-tier adaptive polling with bounded lock occupancy |
| AD-010 | Memory channels as server-side JSON |
| AD-011 | 48 kHz codec / per-backend device-rate bridge |
| AD-012 | Active-VFO tracking (VS poll + FB poll — target active VFO on freq set) |
| AD-013 | Meter calibration tables (piecewise-linear from FT-710.rig) |
| AD-014 | AI mode disabled — active polling; DN=step-down not DNR |
| AD-015 | Priority CAT command preemption (_cancel_polls + send_priority_set_command) |
| AD-016 | Pluggable radio backend architecture (FT-710 + IC-7300 via RadioBackend/Capabilities) |
| AD-017 | Server-side QSO recording (incremental MP3, 16 kHz storage domain) |
| AD-018 | Profile-driven Yaesu ASCII-CAT core (FTDX10/FTDX101D/MP/FTX-1F/FT-891); the verified FT-710 path stays separate |
| AD-019 | Unverified models ship receive-only (transmit gate + read-only identity check + per-table provenance) |
| AD-020 | One-touch CQ (server-side one-shot automated transmission reusing the PTT safety chain) |
| AD-021 | Support diagnostics bundle (server-built + allow-list redaction + dedicated receiver) |
| AD-022 | Upgrade channel (generated manifest + self-proving state + transmit gate) |
| AD-023 | Remote-session telemetry (listener concurrency + uplink, counts without identifiers) |
| AD-024 | Session token transport (cookie first, header for native clients, query deprecated) |
| AD-025 | Spectrum bandwidth tiers (shape × divider, gated by spectrumCaps negotiation) |
| AD-026 | Setup hotspot and the password-less onboarding window (opened by network path, not a flag) |
| AD-027 | Factory-preset (ship-ready) box image: --preflash, off by default; fixed credentials + system half baked in; Wi-Fi and the public entry configured from /settings |
| AD-028 | Local control surface: Bluetooth HID remote → evdev → /WSradio (local_ctl.py, mrrc-localctl); HID-usage keymap; PTT release on every path; HandlePowerKey=ignore |
| AD-029 | Independent SDR receiver (public web receiver): sdr_pipe.py owns the device, sdr_dsp.c is the two-stage channelizer, sdr_hub.py arbitrates the single tuner; own password role, 8 demodulators, 50 listeners |
| AD-030 | Serial instrument (NanoVNA-H): nano/vna.py owns the port, scan_bin binary frames validated by exact length, nano/analysis.py derives SWR/Z/resonance; four guards and the device sweep restored after every scan; upgrade via ROM DFU, locked by MRRC_NANO_ALLOW_FLASH |
This document follows IBM Team Solution Design (TeamSD) methodology v2.3.2. Document ID: SDD-MRRC-MODERN-2026-001