12. Operational Model (ART 0522)
12.1 Runtime Topology
Client Browser
→ http://host:8888 (or https:// with reverse proxy)
→ WS endpoints on same host/port
MRRC Host
→ python3 server.py
→ Uvicorn on 0.0.0.0:8888 (configurable via MRRC_WEB_HOST / MRRC_WEB_PORT)
→ Backend selected by MRRC_RADIO_MODEL (ft710 / ic7300 / ic7300mk2)
→ FT-710: Serial CAT via USB Enhanced COM Port (configurable via MRRC_SERIAL_PORT)
→ IC-7300/MK2: CI-V via USB serial port (configurable via MRRC_SERIAL_PORT, backend default 115200 8N1)
→ FT-710 FT4222 SPI: internal FTDI chip (via scope_pipe subprocess)
→ IC-7300/MK2 CI-V 0x27 spectrum: on the same CI-V serial port
→ USB Audio: supported radio USB audio device (auto-detected by PyAudio)
→ PID file: .ft710-server.pid
→ Logs: stdout/stderr (redirected to logs/ by start.sh)
Yaesu FT-710
→ USB connection to host
→ Enhanced COM Port (38400 baud, 8N1)
→ Standard COM Port (115200 baud, for SCU-LAN10 scope models — optional)
→ FT4222 SPI chip (internal)
→ USB Audio interface
Icom IC-7300 / IC-7300MK2
→ USB connection to host
→ Radio menu: CI-V USB Port = Unlink from [REMOTE]
→ USB CI-V serial port (explicit 115200 baud, not Auto; IC-7300 default 0x94, MK2 default 0xB6)
→ 0x27 spectrum data on the same CI-V port (display + data-output switches ON)
→ 48kHz native USB Audio interface
Yaesu FTDX10 / FTDX101D / FTDX101MP / FTX-1F / FT-891 (experimental)
→ USB connection to host
→ USB serial bridge (same ASCII-CAT protocol as the FT-710; 38400 8N1)
→ No scope interface: the Yaesu scope waveform is undocumented, so the
server keeps broadcasting the S-meter synthesiser instead
→ USB Audio interface (sample rate per profile, family default 44.1kHz,
TODO(hw-verify))
First connection is receive-only for an unverified model (IC-705/IC-7610/ IC-7760 and all five Yaesu models): the startup log reports Radio model <key> is NOT hardware-verified — transmit is DISABLED, the connection dialog labels the model 实验性/仅接收, and set_ptt(True)/ set_tune(True) are refused until the operator sets MRRC_ALLOW_UNVERIFIED_TX=1 and restarts. PTT releases are never gated, and the model identity check (ID;, read-only) only logs.
12.2 Configuration
| Name | Default | Purpose |
|---|---|---|
MRRC_RADIO_MODEL |
ft710 |
Backend selection (registry-validated): ft710, ic7300, ic7300mk2, ic705, ic7610, ic7760, ftdx10, ftdx101d, ftdx101mp, ftx1, ft891 |
IC7300_CIV_ADDR |
0x94 |
IC-7300 CI-V radio address (hex) |
IC7300MK2_CIV_ADDR |
0xB6 |
IC-7300MK2 CI-V radio address (hex) |
MRRC_SERIAL_PORT |
/dev/cu.SLAB_USBtoUART |
Radio serial port (FT-710 Enhanced COM Port or IC-7300 CI-V port) |
MRRC_BAUD_RATE |
backend default | CAT/CI-V baud: FT-710 and the five Yaesu models 38400; Icom models 115200; an explicit value overrides the backend default |
MRRC_WEB_PORT |
8888 |
Uvicorn listen port |
MRRC_WEB_PASSWORD |
changeme_please_use_strong_password! |
Web login password |
MRRC_WEB_HOST |
:: |
Bind address |
MRRC_FTDI_LIB_DIR |
(auto) | Directory containing FTDI libraries |
MRRC_FT4222_CLK_DIV |
6 |
SPI clock divider (1=fastest, 9=slowest; CLK_DIV_64 default) |
MRRC_SCOPE_PORT |
(optional) | Scope serial port (Standard COM Port, SCU-LAN10) |
MRRC_SCOPE_BAUD |
115200 |
Scope serial baud rate |
MRRC_MEM_FILE |
mem_channels.json |
Memory channel store (Windows launcher uses %LOCALAPPDATA%) |
MRRC_AUDIO_RX_DEVICE |
(auto) | Audio input device — index or name substring; Windows package pre-locks USB Audio (FT-710 built-in sound card) |
MRRC_AUDIO_TX_DEVICE |
(auto) | Audio output device — index or name substring; Windows package pre-locks USB Audio |
.ft710-server.pid |
runtime | Process ID for start/stop scripts |
12.3 Startup Modes
| Mode | Command | Behavior |
|---|---|---|
| Foreground | python server.py |
Direct console output; Ctrl-C to stop |
| Background | ./start.sh |
Starts in background, logs to logs/, writes PID file |
| Stop | ./stop.sh |
Reads PID file, sends SIGTERM, cleans up PID |
| FT-710 mode | MRRC_RADIO_MODEL=ft710 python server.py |
Default Yaesu FT-710 backend |
| IC-7300 mode | MRRC_RADIO_MODEL=ic7300 MRRC_SERIAL_PORT=/dev/cu.usbserial-... MRRC_BAUD_RATE=115200 python server.py |
Icom IC-7300 backend (CI-V 115200 8N1) |
| IC-7300MK2 mode | MRRC_RADIO_MODEL=ic7300mk2 MRRC_SERIAL_PORT=/dev/cu.usbserial-... MRRC_BAUD_RATE=115200 python server.py |
Icom IC-7300MK2 backend (default CI-V address 0xB6) |
| Custom port | MRRC_WEB_PORT=8889 python server.py |
Override listen port |
| Custom serial | MRRC_SERIAL_PORT=/dev/ttyUSB0 python server.py |
Override serial port |
| Custom password | MRRC_WEB_PASSWORD=mysecret python server.py |
Override login password |
12.4 Connection Matrix
| Source | Target | Protocol | Port/Path | Description |
|---|---|---|---|---|
| Browser | Server | HTTP | $MRRC_WEB_PORT |
Static UI |
| Browser | Server | WS | /WSradio |
Control (JSON) |
| Browser | Server | WS | /WSaudioRX |
RX audio (binary tagged) |
| Browser | Server | WS | /WSaudioTX |
TX mic uplink (binary tagged + text) |
| Browser | Server | WS | /WSspectrum |
Spectrum waterfall (binary) |
| Browser | Server | WS | /WSradio |
recording set command + recordingState broadcasts (recording control rides the control channel) |
| Browser | Server | HTTP | /api/recordings |
Recording list + total usage |
| Browser | Server | HTTP | /api/recordings/{name} |
Recording stream (Range → seeking; inline player + download) |
| Browser | Server | HTTP | /api/recordings/{name} (DELETE) |
Delete a recording (409 while it is being recorded) |
| Browser | Server | HTTP | /api/status |
Full radio state |
| Browser | Server | HTTP | /api/mem_channels |
Memory channels |
| Browser | Server | HTTP | /api/auth/login |
Login |
| Browser | Server | HTTP | /api/auth/logout |
Logout |
| Server | FT-710 | Serial | USB Enhanced COM | CAT commands |
| Server | FT-710 | SPI | Internal FT4222 | Scope data |
| Server | IC-7300/MK2 | Serial | USB CI-V port | CI-V commands (115200 8N1) |
| Server | IC-7300/MK2 | Serial | Same CI-V port | 0x27 spectrum data |
| FT-710 | Server | USB Audio | USB Audio IN | RX audio |
| Server | FT-710 | USB Audio | USB Audio OUT | TX audio |
| IC-7300/MK2 | Server | USB Audio | USB Audio IN | 48kHz RX audio |
| Server | IC-7300/MK2 | USB Audio | USB Audio OUT | 48kHz TX audio |
12.5 Operational Procedures
| Procedure | Steps |
|---|---|
| Start service | ./start.sh; check logs/ for startup messages |
| Stop service | ./stop.sh |
| Verify radio connection | Server log shows backend-specific connect message (FT-710 ID or IC-7300 CI-V ID) |
| Verify scope | FT-710: “scope_pipe: first frame received — spectrum active”; IC-7300: first confirm Unlink from [REMOTE] + explicit 115200, then check “CI-V scope: first complete waveform — spectrum active”; real frames broadcast at up to 30 Hz without duplicates |
| Verify RX audio | Open browser; listen for radio audio; check “RX …K” and startup log fields host, default, actual, channels (IC-7300/MK2: actual=48000Hz) |
| Verify TX audio | Key PTT; speak; confirm on monitoring receiver and check TX open log (actual=48000Hz on IC-7300/MK2) |
| Verify PTT safety | Release PTT; confirm radio returns to RX; check log for backend-specific unkey command |
| Verify CQ key | Press CQ: the button shows STOP, cqState broadcasts calling with frames_sent rising at 50/s, and the radio keys for the length of the recording (~6 s) then drops back to RX. Log: CQ: keyed the radio … CQ complete after 307/307 frames … CQ session: written=… write_err=0 queue_drops=0. Pressing CQ again mid-call shows aborted/aborted_by_user and unkeys immediately. |
| Replace the CQ recording | Put your own 16-bit WAV anywhere readable and start with MRRC_CQ_FILE=/path/to/cq.wav python server.py (mono or stereo, any sample rate; normalised to 48 kHz mono once at startup). Check the startup line CQ ready: <path> (…); if the file is missing, corrupt or longer than 30 s the log says CQ key disabled: <reason> and the button stays inert. |
| Change backend | MRRC_RADIO_MODEL=ic7300 python server.py |
| Change IC-7300 CI-V address | IC7300_CIV_ADDR=0x94 python server.py |
| Change password | MRRC_WEB_PASSWORD=newpass python server.py |
| Change serial port | MRRC_SERIAL_PORT=/dev/ttyUSB0 ./start.sh |
| View server status | curl -k https://localhost:8888/api/status (with auth cookie; -k = the first-run self-signed certificate) |
12.5.1 支持链路(诊断包与接收端)
- 部署接收端(维护者,幂等):
./deploy_support_receiver.sh。它会创建 systemd unitsupport-receiver-modern(端口 8098)、存储目录/var/www/support-modern(www-data,不在 docroot)、 口令文件/etc/mrrc-modern-support.env(0600 root)、nginx 路径/mrrc_modern/support/, 并打印两条自检:本地与公网的/api/list无口令时必须 401。 - 读一个诊断包:先看
diagnostics/summary.txt(启动次数 vsTraceback、数据新鲜度、 音频/串口/频谱事实、TX 门禁、录音写盘告警),再按需看logs/、state/config-redacted.env、diagnostics/env.json。manifest.json记有每个文件的 sha256、脱敏计数与告警。 - 收包:
https://www.vlsc.net/mrrc_modern/support/api/list(Basic 口令见维护者的口令文件), 编号形如20260917-072530-ab12。 - 排查未复现的问题:包内
warnings里”日志可能过旧”意味着这不是现场;summary.txt会显式写出。 - 答复怎么产生:
dev_tools/support_autopilot.py(crontab 每 10 分钟,--once --publish)轮询接收端 → 解包 →pi只读分析(提示词带探查预算:≤5 次检索 / ≤6 个文件)→ 按 JSON 契约渲染答复卡 → 仅当status为answered/needs_fix时更新website/answers/index.html、commit 并 rsync 单文件上线。 人工预检用--inspect <编号>(不调模型);--status看已处理清单;--force <编号>重跑。 草稿永远留在dist/support_answers/,即使发布失败也不丢结论。
12.5.2 更新通道(slice 1:只读检查)
- 发布侧:
python3 dev_tools/make_latest_json.py --installer <版本> <URL> <本地安装包> [--previous …]—— size/SHA-256 从文件本身算出,版本必须等于 CHANGELOG 顶版本,previous必须更旧;缺任一条件拒绝生成。 产物website/downloads/latest.json随站点部署。 - 客户端侧:
GET /api/update/check(需登录)返回{available, current, latest, url, sha256, size, mandatory, notes}; 清单拉取失败只返回{available: false, reason},不会让服务端返回 500。 - 成功判据(slice 2):
<data dir>/updates/state.json的lastResult.status == ok,由新版本启动时写入;installing只表示已拉起安装器。 - 门禁:发射或录音期间升级请求返回 423(slice 2 在交接前复查)。
12.5.3 远程会话遥测(AD-023)
GET /api/session_metrics 返回实测并发与上行(仅计数与字节,无标识):listeners/operators 的 会话数与 socket 数、生命期峰值与滑动窗口峰值、sockets_by_kind,以及 spectrum/audio_rx 两路上行 的累计字节。每 MRRC_SESSION_METRICS_INTERVAL_S(默认 300 s)另输出一条 Session metrics: INFO 行, 含该区间的平均 kbps —— 它就是 Cloud Hub 判断”要不要做 RX 扇出”的输入(mrrc_hub/SDD AD-H12 / I-H1)。
MRRC_SESSION_METRICS_INTERVAL_S=0 关闭周期行,端点照常可用;MRRC_SESSION_METRICS_WINDOW_S (默认 3600 s)决定窗口峰值的宽度。
12.6 Logs and Artifacts
| Artifact | Purpose |
|---|---|
logs/ directory |
Server stdout/stderr when background-started |
MRRC_LOG_DIR/server.log |
Rotating server log (2 MB × 2, UTF-8): the canonical log, written in every launch mode. Default MRRC_LOG_DIR = the install dir’s logs/; the desktop launchers point it at the user data directory (Program Files and /Applications are not writable) |
MRRC_LOG_DIR/server-stdout.log |
Launcher tee of the child’s stdout/stderr, rebuilt at each launch and closed once the server answers HTTP: the only witness of a server that dies before its own logging exists. It keeps draining afterwards so a chatty server can never block on a full pipe. On systemd (install.sh) and launchd the same name is used for the process redirect — never server.log, which the in-process handler owns |
support-out/ |
Built diagnostics bundles, newest 5 (MRRC_LOG_DIR’s sibling); support-export/ holds the copies made by 「只保存到本地」 |
version.txt |
App version shipped inside each artifact (macOS Contents/MacOS/, Windows install dir, rpi64 /opt/mrrc_modern/); the bundle manifest and the future one-click upgrade read it |
.ft710-server.pid |
Running process PID |
mem_channels.json |
Persisted memory channels |
config.py |
Protocol-neutral constants + shared UI mode tables |
backends/ft710/config_ft710.py |
FT-710-specific mode/band/filter/S-meter calibration tables |
backends/ic7300/config_ic7300.py |
IC-7300/MK2-specific mode/band/filter/tables |
lib/ |
FTDI libraries (libft4222.dylib, libftd2xx.dylib, ftd2xx.cfg) — used only by FT-710 backend |
12.7 Operational Risks
| Risk | Mitigation |
|---|---|
| Wrong serial port | Server logs warning; check ls /dev/cu.* or ls /dev/ttyUSB* |
| FT4222 not working (FT-710) | Falls back to S-meter synthetic spectrum; check D2XX config |
| CI-V 0x27 not arriving (IC-7300) | Verify CI-V USB Port = Unlink from [REMOTE], explicit 115200 (not Auto), and the model-specific CI-V address; run _diag_ic7300_scope.py; initialization enables both 27 10 display and 27 11 data output; the bounded queue drops oldest data rather than replaying stale frames |
| Audio not working | Check PyAudio device list in logs; verify the selected radio’s USB audio appears |
| Port already in use | ./stop.sh first; check for stale processes |
| Stale JS cached | Service worker bypasses JS/HTML; version query strings |
| Stuck TX | Multiple safety layers (see Ch. 15); server forces RX on WS disconnect |
12.9 Cloud Hub 模式运维要点(2026-09-30)
实例可被置于云端入口之后运行(呼号子域或路径前缀),此时下列参数与退化需要有意识地被管理:
| 项 | 说明 |
|---|---|
| 路径前缀 | 由 FT710Settings.basePath / .url 驱动,服务端与前端(含 TX 编码 Worker)均已前缀安全;由 tests/test_path_prefix.py 守 |
| 令牌传输 | 不进 URL(AD-024):cookie/Bearer 优先,query 形式仅兼容并告警 |
| PTT 活性闸门 | MRRC_REMOTE_SESSION_TX_HEARTBEAT_S(默认 0 = 关闭)。Hub 模式下建议 3–5 秒:客户端失联即释放 PTT,与 MRRC_PTT_MAX_TX_SECONDS 构成两条独立防线 |
| 会话遥测 | GET /api/session_metrics 与周期日志;用于判断 Listener 并发与上行占用(支撑 AD-H12 的扇出决策) |
| 登录限流退化 | 经隧道/反代时所有登录共享一个来源 IP(5 次失败 / 300 秒全局桶)。运维须知:误锁影响全部用户,不是单个来源 |
Hub 端对应事实见 ../../mrrc_hub/SDD/12-operational-model.md §12.8。
12.10 W103D 镜像(Amlogic 盒子)
把一个 ZTE 云电脑 W103D(Amlogic S905L3A / G12A,4×Cortex-A53@1.8,2GB/32GB,100M 网口,MT7663 WiFi+BT) 变成通用电台服务端。设计与构建手册: docs/superpowers/specs/2026-10-06-w103d-radio-box-design.md、docs/w103d_pack.md; 操作单:packaging/box/README.md。
12.10.1 两段式部署
构建期(macOS + Docker --privileged,Apple Silicon 原生 arm64,不需要 qemu)loop 挂载 ophub 的 Armbian 镜像并 chroot 铺 overlay;运行期首启只做必须碰硬件的探测。
| 阶段 | 做的事 |
|---|---|
| 构建期 | mrrc 用户 + dialout,audio 组;/opt/mrrc_modern(代码 + install.sh 建的 venv);预置 aarch64 FTDI 库;frpc;11 份 profile;两个 systemd 单元 |
| 首启 | mrrc-firstboot → 探测串口与声卡 → 生成 web 口令 → 自签 HTTPS 证书 → 启服务 |
为什么不把串口/口令也烤进去:盲猜会产出一个「看起来能用但连不上电台」的配置,而这类错误在现场表现为”服务在跑、就是不通”。
12.10.2 运行形态
| 项 | 值 |
|---|---|
| 安装位置 | /opt/mrrc_modern(不是仓库目录) |
| 服务用户 | User=mrrc / Group=mrrc,属于 dialout(串口)与 audio(USB 声卡) |
| 两个 env 路径必须同一文件 | EnvironmentFile=/opt/mrrc_modern/env/mrrc.env 且 Environment=MRRC_CONFIG_FILE=/opt/mrrc_modern/env/mrrc.env |
| 内核 | 只走 armbian-update,保持 w103d 变体(MT7663S 依赖专属补丁 + 固件 overlay) |
为什么两个 env 路径必须钉成同一个:cloud_hub.connect() 把证书路径与 MRRC_REMOTE_SESSION_TX_HEARTBEAT_S 写进 server.py:_config_file_path(), 该函数优先读 MRRC_CONFIG_FILE、否则回退到 MEM_FILE.parent/mrrc_modern.env;而 systemd 读的是 EnvironmentFile。 两者分裂时症状是静默的:设置页显示「已连接」,而上游 TLS 校验失败、PTT 活性闸门从未生效。 这也解释了 Cloud Hub 为何至今只在 Windows/macOS 实例上跑过——那两边的启动器会自己设 MRRC_CONFIG_FILE。 packaging/box/verify.sh 第 7 项把这个相等关系做成断言,防止它再次漂移。
12.10.3 电台切换
mrrc-radio list | show | use <model> [--port DEV],型号集合由 backends.known_models() 校验, 每型号一份 packaging/box/profiles/<model>.env——只写型号,因为波特率由后端自己的表决定、串口与声卡由首启解析。
两条容易做错且已被测试钉住的规则:
- 换型号且未显式给
--port时清空MRRC_SERIAL_PORT——FT-710 是 ttyUSB0/1、IC-7300 是 ttyACM0,留着旧值等于一条没有报错的死链路。 - TX 门禁由 profile 重新断言而不是继承——8 个未验证机型(
ic705ic7610ic7760ftdx10ftdx101dftdx101mpftx1ft891)的 profile 显式写MRRC_ALLOW_UNVERIFIED_TX=0(AD-019 / NFR-067), 且任何 profile 出现=1都会被测试拦下:镜像的首次连接若能在未经测量的表上按下发射键,正是 AD-019 要防的那件事。
mrrc-radio 自身不做串口 I/O(cat-direct-serial-io):--port 取操作者给的答案,否则调用 linux/first_run.py 既有的 detect_serial_ports() / probe_radio_model()(先停服务,否则端口被占着、探测问不出东西)。
mrrc-update 是就地升级路径(拉代码 → pip → 重启),无回滚:upgrade_core.py 的自动升级切片尚未开工, 在这里另造一套状态机只会得到一套没被测过的状态机。
12.10.4 与树莓派镜像的关系
| 项 | Pi 镜像(packaging/rpi/) |
W103D 镜像(packaging/box/) |
|---|---|---|
| 产出方式 | pi-gen 造镜像 | 在 ophub 预构建镜像上二次构建 |
| 预置目录 | /opt/mrrc_modern |
同 |
| 预置来源路径 | /boot/firmware/mrrc.env |
/boot/mrrc.env(Armbian 把 boot 分区挂在 /boot) |
| 绑定地址默认 | :: |
0.0.0.0(AGENTS.md 的侦听地址约定) |
| 复制排除清单 | 权威副本 | 必须与它逐字一致,由 tests/test_box_profiles.py::ExclusionParityTests 强制 |
排除清单现在存在于三处(Pi 构建器、box overlay、mrrc_update.sh),测试逐字比对三份。 两份构建器对「什么该进 /opt/mrrc_modern」产生分歧的后果是运行期 ImportError,不是构建错误——所以这条应由测试守而不是由记性守。
不自建 Armbian 镜像是刻意的取舍:W103D 的板级补丁(SDIO 路由到 SD_EMMC_B、MT7663S 传输层、原厂 N9 固件) 活在 ophub/unifreq 的内核树里,自建等于长期跟进那套补丁栈。缓解手段是钉基底镜像的 SHA-256 并只从官方 release 取。
镜像本体尚未发布:MRRC-Modern-<ver>-w103d.img.gz 的版本规则、release-artifacts.json 条目与网站下载卡 留给独立的发展步骤(设计文档 §11)。