# ARCH 热点侧 —— 完整技术说明与代码架构

> 文档版本：2026-09-23 · 对应固件 **1.0.24**
> 适用对象：接手 ARCH 热点盒子（树莓派 + SX1255/MMDVM 射频板）开发与运维的人
> 配套备份：`03-配置备份\ARCH热点固件-1.0.24-完整备份-20260923\`

---

## 第 0 章　一句话说清这套东西

ARCH 热点侧 = **一台树莓派 + 一块 SX1255 多通道射频板**，上面跑一套自研 Web 控制台，
把 DMR / C4FM(YSF) / D-STAR / P25 / NXDN / FM(FMO) / POCSAG 这 7 种制式
**同时**接到 46002 多模融合网络里，用户用浏览器就能配置全部参数。

它**不是** Pi-Star 的换皮：射频前端换成了自研的 `MMDVM-Multi`（一次采一段宽带 I/Q，软件分出 N 个信道），
Web 端是整个重写的，服务器列表、身份、语音链路都指向 ARCH 自己的 93 服务器 + 北京公网入口。

---

## 第 1 章　系统边界：热点侧 vs 服务器侧

必须先把边界分清，改错地方会白忙。

| 归属 | 在哪里 | 负责什么 | 代码位置 |
| --- | --- | --- | --- |
| **热点侧**（本文档） | 用户家的树莓派 | 射频收发、各模式网关、Web 控制台、FMO 桥、本地录音/播报触发 | 本备份 `source\` |
| **服务器侧** | 93 / 92 / 北京 | FreeDMR 主站、XLXD、YSF/P25/NXDN 反射器、TETRA Brew、MQTT、录音归档、Web 管理平台、官网 | `07-当前工程工作区\...\work\`、93 上 `/opt/*` |
| **镜像侧** | Cloudflare Pages（`arch.hljrsa.cn`） | 发布安装包、数据镜像（DMR ID / 各模式服务器列表）、官网、反馈页 | `server-mirror\` + 93 `/opt/arch-site` |
| **手机 APP** | 用户手机 | 音频 + 通话界面 | 独立一条线，不在本文范围 |

**一句话**：热点侧只负责"把电台和网络接起来 + 把界面做出来"，所有"全球列表/谁在线/录音"之类都在服务器和镜像侧。

---

## 第 2 章　两条发布线（内测版 / 公开版）

同一套源码，打包成两个版本，**互相独立升级**：

| | 内测版 internal | 公开版 public |
| --- | --- | --- |
| 频道名 | `internal` | `public` |
| 下载地址 | `https://arch.hljrsa.cn/downloads/arch-hotspot/internal/` | `.../public/` |
| 集群功能（TETRA / TDR Link / FlowStation） | **保留** | **彻底移除** |
| 免责声明 | 有 | 有（更详细） |
| 是否对外公开 | 否，仅自用/内测 | 是，官网公开下载 |
| 当前版本 | 1.0.24 | 1.0.24 |

**公开版是怎么"彻底移除"集群的**：不是运行时开关，而是在打包时对源码做**语义级裁剪**——
删掉集群页面、删掉集群后端路由、删掉 `tetra_*.py`、`web-tetra.sh`、`tetra-bs.service` 等文件，
再对全树做一次正则清洗（任何含 `TETRA` / `TDR Link` / `FlowStation` / `Bluestation` 的整行都删除）。
打包后会用关键字全树校验，必须命中 0 个文件才算通过（见第 11 章）。

> ⚠️ 以后改公共部分（射频、Web、升级逻辑）时**两边都要改**；
> 只改集群相关的东西时**只动内测版**，公开版会因为裁剪规则自动不含。

---

## 第 3 章　发布包结构（解包后长什么样）

包内目录结构 = 设备上的绝对路径，安装脚本就是"照抄"到 `/`。

```
arch-hotspot-<通道>-<版本>/
├── install.sh                     ← 主安装脚本（见第 4 章）
├── arch-hotspot-update            ← 设备上的在线升级命令
├── build_upstream.sh              ← 编译上游 MMDVM 各组件
├── VERSION / CHANNEL / LICENSE / DISCLAIMER.md
├── opt/
│   ├── mmdvm-web/                 ← ★ Web 控制台（自研核心）
│   │   ├── app.py                 ← 主程序：Flask + 前端 HTML/CSS/JS 全在一个文件里
│   │   ├── fmo_page.py            ← FMO 模拟互联页面
│   │   ├── console_new.py         ← 新版控制台页面骨架
│   │   ├── rfmode_page.py         ← 射频模式页面
│   │   ├── rfmode_js.py / rfmode_section.py / rfmode2_js.py / rfmode2_section.py
│   │   ├── multi2_js.py / multi2_section.py   ← 多信道矩阵
│   │   ├── tetra_page.py / tetra_adv_backend.py / tetra_adv_spec.py   ← 集群页（仅内测）
│   │   └── rfmode_standalone.py
│   └── fmo-bridge/                ← ★ FMO 模拟互联桥（自研）
│       ├── fmo_bridge.py          ← 主程序（RF↔网络音频、MQTT、PCM 编解码）
│       ├── fmo_voice.py / fmo_dsp.py / fmo_fm.py / fmo_packet.py
│       ├── fmo_proto.py / fmo_reg.py / fmo_presence.py
│       ├── fmo_app_uplink_relay.py
│       └── config.template.json
├── usr/local/bin/                 ← 运维脚本（被 Web 调用）
│   ├── web-manage.sh              ← Web 的"总调度"（状态/切换/保存/日志/备份/更新）
│   ├── web-multi.sh / web-fmo.sh / web-tetra.sh
│   ├── channel-mode.sh            ← 单通道模式切换
│   ├── update-database.sh         ← 从 ARCH 镜像更新数据（本版新增 dmrhosts）
│   ├── update-hosts.sh            ← 全量主机库更新（镜像优先，Pi-Star 兜底）
│   ├── update-dstar.sh
│   ├── arch-data-bootstrap.sh     ← 首次开机自动拉数据
│   ├── arch-fw-online.sh          ← 在线固件升级
│   ├── sys-manage.sh              ← 系统设置（品牌/主题/密码/WiFi）
│   ├── arch-sync-fm-qso.py
│   ├── backup.sh / diagnose.sh / DMRIDUpdate.sh / dmrid-update-wrapper.sh
│   └── hw-detect.sh
├── usr/local/libexec/             ← Python 工具（真正干活的地方）
│   ├── mmdvm-save.py              ← ★ 所有"保存配置"都走这里
│   ├── channel-setup.py           ← ★ 通道规划落盘 + 模式互斥校验
│   ├── hosts-build.py             ← ★ 把上游 txt 转成 Web 用的 JSON（含 ARCH 置顶）
│   ├── mmdvm-status.py            ← ★ 状态与通话记录（Web 每次刷新都调它）
│   ├── fmo-api.py                 ← FMO 桥的 API 后端
│   ├── arch-channel-limit.py      ← 硬件通道上限判定
│   ├── tetra-api.py               ← 集群 API（仅内测）
│   └── ini-edit.py
└── etc/
    ├── mmdvm/                     ← 运行期状态与规划
    │   ├── channels.json          ← ★ 多通道规划（通道数、每路模式、频点）
    │   ├── frequency.json         ← 射频中心频率
    │   ├── active-mode.conf       ← 当前生效模式/通道数
    │   ├── install-state.conf     ← 安装状态 + 各上游组件 commit 号
    │   ├── bm-masters.json        ← 指向 DMR_Hosts.json 的软链接
    │   ├── fm-tailtone.json / fw-url
    ├── MMDVM-Multi.ini            ← 多通道前端（宽带采样）
    ├── MMDVM-IQ.ini / MMDVM-IQ-{1..11}.ini   ← 每路 SDR 调制解调器
    ├── MMDVM-Host.ini / MMDVM-Host-{1..11}.ini ← 每路协议栈（模式、呼号、频点）
    ├── DMRGateway.ini             ← DMR ↔ BM/FreeDMR 网关
    ├── YSFGateway.ini / P25Gateway.ini / NXDNGateway.ini
    ├── ircddbgateway              ← D-Star 网关（反射器、模块）
    ├── nginx/sites-available/mmdvm-web   ← 80 端口反代到 127.0.0.1:18080
    └── systemd/system/            ← 全部服务单元（见第 6 章）
```

**代码量参考**：源码树 103 个文件 ≈ 1.0 MB，其中 `opt/mmdvm-web/app.py` 一个文件就 ≈ 470 KB
（Flask 后端 + 整个前端 HTML/CSS/JS 都塞在里面，是历史原因，改的时候务必用锚点式精确替换）。

---

## 第 4 章　安装流程（从一行命令到能用）

用户在树莓派上执行：

```bash
curl -kfsSL https://arch.hljrsa.cn/downloads/arch-hotspot/public/install.sh | sudo bash
```

### 4.1 第一层：云端引导脚本 `install.sh`

```bash
BASE="https://arch.hljrsa.cn/downloads/arch-hotspot/<通道>"
下载 arch-hotspot-<通道>-<版本>.tar.gz     ← 带 curl -k 回退（时钟没同步时证书会 not yet valid）
sha256sum -c                              ← 校验哈希，不符就中止
tar 解包 → 找到包目录 → 执行包内 install.sh
```

### 4.2 第二层：包内 `install.sh`

1. 检查 root、记录是否首次安装（看 `/etc/arch-hotspot-release` 是否存在）
2. `apt-get install` 基础依赖（python3 / nginx / network-manager / git …）
3. 装 Python 依赖：优先用 apt 的 python3-flask/gunicorn/…，失败则用**离线轮子**
   `deps/arch-python-wheels.tar.gz`
4. 调 `build_upstream.sh` 编译上游组件（见 4.3）
5. 建 `mmdvm-web` 系统用户与目录，把包内 `opt/` `usr/local/` `etc/` 覆盖到系统
6. **首次安装**才覆盖 `etc/*.ini` 与 `ircddbgateway`；**升级**用 `cp -n`（不覆盖用户配置）
7. `hosts-build.py` 重建主机库、`channel-setup.py apply` 应用通道规划
8. 装 `arch-hotspot-update`，写 `/etc/arch-hotspot-release`（记录通道/版本/更新源）
9. 写 sudoers：`/etc/sudoers.d/arch-web`（见第 10 章）
10. `systemctl enable --now` 各服务，启动 `arch-data-bootstrap`（首启拉数据）
11. 根据 `DMRGateway.ini` 里有没有 ID 决定要不要起 DMR 网关
12. 按 `channels.json` 逐路 enable `mmdvm-iq@N` 和 `mmdvmhost@N`

### 4.3 上游组件怎么来的：`build_upstream.sh`

- 依赖包：`git build-essential cmake autoconf … libliquid-dev libsoapysdr-dev nlohmann-json3-dev mosquitto libwxgtk3.2-dev`
- 源码：从镜像下载 `deps/arch-upstream-src.tar.gz`（**不放 GitHub，国内直连镜像**），解到 `/opt/src-mmdvm`
- **三处本地补丁（很重要，别丢）**：
  1. `MMDVM-IQ/IO.cpp` —— 双工 DMR 模式下原来只在发射时喂全时隙解码器，
     导致**本机手台在空闲热点上发射时收不到**；改成始终喂 `dmrRX`。
  2. `MMDVM-Multi/Constants.h` —— `PFB_FILTER_DELAY 48 → 24`（降延迟）。
  3. `sxxcvr/SoapySX` —— 去掉对 git 的依赖，写死版本号，保证离线可编。
- 编译顺序：MMDVM-Multi → MMDVM-IQ → MMDVM-Host → DMRGateway → YSFGateway → P25Gateway → NXDNGateway → ircddbgatewayd → SoapySX 模块
- 已存在就跳过（`FORCE_MMDVM_IQ=1` 可强制重编）

> 注意：`/usr/local/bin` 里那几个大二进制是**上游编译产物**，不属于本备份范围（本备份只收 `*.sh` / `*.py`）。

---

## 第 5 章　升级机制

设备上执行：

```bash
sudo arch-hotspot-update
```

逻辑（`updater_template.sh`）：

1. 读 `/etc/arch-hotspot-release` 拿到 `CHANNEL` / `VERSION` / `UPDATE_BASE`
2. 拉 `$UPDATE_BASE/$CHANNEL/current.json`（带 `curl -k` 回退）
3. 版本相同 → 直接退出
4. 下载新包 → **用 current.json 里的 sha256 校验** → 解包 → 执行包内 `install.sh`
5. `install.sh` 检测到不是首次安装，**用户配置不会被覆盖**

`current.json` 长这样：

```json
{ "channel": "public", "version": "1.0.24",
  "file": "arch-hotspot-public-1.0.24.tar.gz",
  "sha256": "95801d01…" }
```

---

## 第 6 章　运行时架构

### 6.1 进程与服务

| systemd 单元 | 作用 | 数量 |
| --- | --- | --- |
| `mmdvm-multi.service` | 宽带 I/Q 采样 + 数字信道化（自研前端） | 1 |
| `mmdvm-iq@N.service` | 第 N 路软件调制解调器（MMDVM-IQ） | 按通道数 1–7 |
| `mmdvmhost@N.service` | 第 N 路协议栈（DMR/D-Star/YSF/P25/NXDN/FM/POCSAG） | 按通道数 1–7 |
| `dmrgateway.service` | DMR ↔ BrandMeister/FreeDMR | 0/1 |
| `ysfgateway.service` / `p25gateway.service` / `nxdngateway.service` | 各模式网络网关 | 各 0/1 |
| `ircddbgatewayd.service` | D-Star（DCS/XRF/REF/XLX）网关 | 0/1 |
| `fmo-bridge.service` | FMO 模拟互联桥（FM 房间 + MQTT + APP 中继） | 1 |
| `mmdvm-web.service` | Web 控制台（gunicorn，`--workers 1 --threads 6`，监听 127.0.0.1:18080） | 1 |
| `nginx.service` | 80 端口反代到 18080，对外提供 Web | 1 |
| `mosquitto.service` | 本机 MQTT（FMO/RAW 主题） | 1 |
| `arch-data-bootstrap.service/timer` | 定时/首启更新 DMR ID 与各模式服务器列表 | 1 |
| `arch-fw-online.service`（可选） | 在线固件升级 | — |
| `tetra-bs.service`（仅内测） | TETRA Brew 基站 | 0/1 |

### 6.2 数据流（接收方向示例：网络上有人讲话）

```
46002 网络 → 93 服务器 → 北京公网入口
        ↓ UDP
   YSFGateway / P25Gateway / NXDNGateway / DMRGateway / ircddbgatewayd
        ↓ 本机 UDP（每个模式一个端口段）
   MMDVM-Host（第 N 路，含该路模式）→ 音频帧
        ↓ UDP（MMDVM 协议）
   MMDVM-IQ（第 N 路）→ 基带 I/Q
        ↓
   MMDVM-Multi → SX1255 射频 → 手台听到

FM/FMO 分支：
   MQTT(FMO/RAW) → fmo-bridge → 声码器 → ALSA / 射频
```

### 6.3 端口速查

| 端口 | 用途 |
| --- | --- |
| 80 | Web 控制台（nginx → 18080） |
| 18080 | gunicorn 内部端口（仅 127.0.0.1） |
| 1883 | 本机 MQTT（mosquitto） |
| 62030 | FreeDMR（服务器侧） |
| 62031 / 62032 | DMRGateway ↔ MMDVM-Host |
| 42000 / 41000 / 41400 | YSF / P25 / NXDN 反射器 |
| 30051 | D-Star（DCS/XLX） |

---

## 第 7 章　Web 控制台架构

### 7.1 三层

```
浏览器
  │ HTTP
nginx (80)  ──反向代理──▶  gunicorn (127.0.0.1:18080)
                              │
                              ▼
                        /opt/mmdvm-web/app.py（Flask）
                              │ subprocess + sudo
                              ▼
   /usr/local/bin/web-manage.sh   ← 总调度
   /usr/local/libexec/mmdvm-save.py / mmdvm-status.py / hosts-build.py / fmo-api.py
                              │
                              ▼
                    /etc/*.ini、/etc/mmdvm/*.json、systemctl
```

**设计要点**：Flask 本身不碰系统文件，全部通过 `sudo` 调用白名单里的脚本；
脚本都是"可单独在命令行跑"的，方便排障（出问题时先手工跑脚本，再看 Web 是不是也有问题）。

### 7.2 API 一览（app.py 里注册的路由）

| 路由 | 作用 |
| --- | --- |
| `/` | 整个控制台页面（HTML/CSS/JS 由 `render_template_string` 一次性吐出） |
| `/api/status` `/api/ports` | 系统状态、服务端口 |
| `/api/qso` | **通话记录**（Live QSO 看板，0.5 秒轮询 + 0.6 秒折叠缓存） |
| `/api/config/<模式>` | 读某模式配置（走 `web-manage.sh get-config`） |
| `/api/save/<模式>` | 保存某模式配置（走 `mmdvm-save.py`） |
| `/api/switch/<模式>` | 单通道切换模式 |
| `/api/bm-list` `/api/ysf-hosts` `/api/p25-hosts` `/api/nxdn-hosts` `/api/dstar-reflectors` `/api/fcs-hosts` | 各类服务器/反射器列表 |
| `/api/hosts-status` `/api/hosts-rebuild` | 主机库状态与重建 |
| `/api/update-db/<类型>` | 手动触发数据更新（dmrid / dmrhosts / hosts / …） |
| `/api/logs/<服务>` | 服务日志尾部 |
| `/api/diagnose` `/api/backup` `/api/bm-test/<host>` | 诊断 / 备份 / BM 连通性测试 |
| `/api/sys/info` `/api/sys/net` `/api/sys/wifi` `/api/sys/action` `/api/sys/fw/upload` | 系统设置（品牌、WiFi、固件上传…） |
| `/api/restart/<服务>` | 重启服务（白名单） |

### 7.3 前端为什么"看不见文件"

`app.py` 用 `render_template_string` 把 HTML/CSS/JS 当字符串吐出来，
所以**改前端 = 改 app.py**（或用同目录的 `*_page.py` / `*_js.py` 模块拼装）。
改的时候**不要按行号**，用锚点字符串精确替换——文件太大，行号很容易漂。

---

## 第 8 章　关键脚本逐个说

### 8.1 `mmdvm-save.py` —— 所有"保存"的落点

| 函数 | 写入目标 |
| --- | --- |
| `save_rf` | `MMDVM-Host*.ini`（呼号、频点、双工、功率） |
| `save_dmr` | DMR 宿主 + `DMRGateway.ini`；**ESSID 090123 逻辑**：`7 位 DMR ID + 2 位 ESSID(01–99)`，只叠加到 DMR，不污染其它模式 |
| `save_ysf` / `save_p25` / `save_nxdn` / `save_dstar` / `save_fm` / `save_pocsag` | 各自的 .ini |
| `set_identity` / `sync_host_identity_and_frequencies` | 呼号/ID/频点全通道同步 |
| `update_plan_identity` | 把呼号、DMR ID 写回 `channels.json` |
| `restart_channels` | 改完自动重启对应服务 |

**注意**：`save_nxdn` 会同时写 `NXDN` 通道的 `General/Id` 与 `NXDNGateway.ini` 的 `Id`
（历史上这里被写成"NXDN 只用呼号、清掉 ID"，导致 NXDN 上不了网，已修）。

### 8.2 `channel-setup.py` —— 通道规划与模式互斥

- `hardware_limit()`：调 `arch-channel-limit.py` 拿本机上限，返回 1–7
- `apply(plan)`：把 `channels.json` 展开成 `MMDVM-Multi.ini` + 每路 `MMDVM-IQ-N.ini` / `MMDVM-Host-N.ini`
- **模式互斥校验**：只统计"当前运行中的通道"，**未启用的历史通道不算占用**
  （历史 bug：单通道时想从 DMR 切 D-Star 被误报"其它通道占用"）
- 频点网格：`grid_step()` 按采样率算信道间隔

### 8.3 `arch-channel-limit.py` —— 硬件通道上限

| 设备 | 上限 |
| --- | --- |
| 树莓派 2 / Zero 2 | 2 |
| 树莓派 3（含 CM3） | 4 |
| 树莓派 4（内存 < 2 GB） | 4 |
| 树莓派 4（≥ 2 GB）/ 树莓派 5 | 7 |
| 其它设备 | 按 CPU 核数 / 内存判定：≤2 核或 <1.2 GB → 2；≤4 核或 <2.2 GB → 4；否则 7 |

Web 里"保存并应用"时会弹窗提示建议路数与免责声明。

### 8.4 `hosts-build.py` —— 服务器列表生成（含 ARCH 置顶）

输入：`/usr/local/etc/hosts/` 下的上游 txt/json（来自 ARCH 镜像）
输出：`DMR_Hosts.json`、`YSFHosts.json`、`NXDNHosts.json`、`P25Hosts.json`、`DSTAR_Reflectors.json`、`hosts-index.json`

**ARCH 置顶规则**（每次重建都强制生效，防止被上游数据覆盖）：

| 模式 | 置顶条目 |
| --- | --- |
| DMR | `ARCH_46002_China` · `46002 二区多模融合` · `srv2.hljrsa.cn:62030` |
| YSF | `CN-46002` · `二区多模融合 / 哈尔滨` · `ysf.hljrsa.cn:42000`（重复项自动去重） |
| P25 | `46002 二区多模融合` · `39.96.117.224:41000` |
| NXDN | `46002 二区多模融合` · `39.96.117.224:41400` |
| D-STAR | `DCSHLJ / XRFHLJ / REFHLJ / XLXHLJ` · 均带 `46002二区多模融合` |

### 8.5 `mmdvm-status.py` —— 状态与通话记录

两种调用方式：
- 直接跑 → 输出 `=== SERVICES === / PROFILE / SYSTEM / NETWORK / RF FREQ / QSO ===`（Web 首页用）
- `mmdvm-status.py qso` → 只输出通话记录（Live QSO 用）
  - **一次 `journalctl -u … -o json -n 600` 批量取全部通道 + fmo-bridge**（不要改回逐通道多次调用，会拖慢 Pi）

**"通话中"判定**（各地模式"开始行"写法完全不同，这是踩过的坑）：

| 模式 | 开始行 |
| --- | --- |
| DMR | `DMR Slot n, received (RF|network) voice header from X to Y` |
| D-Star | `D-Star, received (RF|network) header from X to Y` |
| YSF | 网络侧无 header，用 `YSF, received network data from X to DG-ID n at X` |
| P25 | `P25, received network transmission from X to TG n`（射频侧 `received RF voice transmission`） |
| NXDN | `NXDN, received network transmission from X to TG n` |
| FM | 由 `fmo_bridge._qso_log()` 合成，**开始行不带时长、结束行带 `N seconds`** |

### 8.6 `web-manage.sh` —— Web 的总调度

`status / ports / switch / get-config / save-XXX / logs / backup / diagnose / update-db / bm-list / bm-test / restart / qso`

- `get-config DMR` 会**先去 `channels.json` 找 DMR 实际在哪一路**，再读那一路的 ini
  （历史 bug：DMR 不在第 1 路时配置页读到空）
- `switch` 单通道时直接**替换**当前通道模式，多通道时只找运行中的空闲位

### 8.7 数据更新：`update-hosts.sh` / `update-database.sh`

- 镜像优先：`https://arch.hljrsa.cn/downloads/arch-hotspot/data/`，失败再退回 Pi-Star 官方
- `update-hosts.sh`：全量（DMR ID + DMR/YSF/P25/NXDN/D-STAR 主机库）→ 调 `hosts-build.py`
- `update-database.sh <类型>`：`dmrid | dmrhosts | ysfhosts | p25hosts | nxdnhosts | dstarhosts | hosts | all`
  - `dmrhosts` 是 1.0.22 新加的（原来根本没有 DMR 列表更新通道）
- `arch-data-bootstrap.sh`：开机自动跑一次，缺什么补什么

### 8.8 `fmo-bridge/` —— 模拟互联（FM 房间）

- `fmo_bridge.py`：主循环。上行（RF→网络）、下行（网络→RF）、MQTT 收发、PCM 转码、尾音、录音触发
- `fmo_voice.py` / `fmo_dsp.py` / `fmo_fm.py` / `fmo_packet.py`：语音与调制相关
- `fmo_reg.py` / `fmo_presence.py`：注册与在线状态上报
- `fmo_app_uplink_relay.py`：手机 APP 上行中继
- **`_qso_log()`**：往 journal 写"看起来像 MMDVM-Host"的通联行，供 Web 通话看板解析
  （开始行**必须不带** `N seconds`，否则前端会当成已结束、不显示红色高亮）
- 证书在 `/opt/fmo-bridge/pki/`（本设备私有，备份里含）

---

## 第 9 章　配置文件索引（`/etc`）

| 文件 | 作用 | 改它会影响 |
| --- | --- | --- |
| `mmdvm/channels.json` | **通道规划总表**：通道数、每路模式与频点 | 一切（改完要 apply） |
| `mmdvm/frequency.json` | 射频中心频率、间隔、采样率 | 频率类页面显示 |
| `mmdvm/active-mode.conf` | 当前生效模式串与通道数 | 首页模式显示 |
| `mmdvm/install-state.conf` | 安装状态 + 各上游组件 commit（溯源用） | 升级判定 |
| `MMDVM-Multi.ini` | 宽带前端参数 | 所有信道 |
| `MMDVM-IQ-N.ini` | 第 N 路 SDR 增益/反相 | 单路射频质量 |
| `MMDVM-Host-N.ini` | 第 N 路协议栈（模式开关、呼号、ID、频点、色码/RAN/NAC/DG-ID） | 单路模式 |
| `DMRGateway.ini` | BM/FreeDMR 主站、端口、密码、Id | DMR 上网 |
| `YSFGateway.ini` / `P25Gateway.ini` / `NXDNGateway.ini` | 各模式网络服务器与 ID | 各模式上网 |
| `ircddbgateway` | D-Star 呼号、`reflector1`、`atStartup1`、DPlus/DExtra 开关 | D-Star 连接 |
| `systemd/system/*.service` | 服务定义 | 启动行为 |
| `nginx/sites-available/mmdvm-web` | 80→18080 反代 | Web 访问 |

> `ircddbgateway` 里的 `reflector1` 决定开机自动链到哪个反射器。
> 出厂模板是**空的**（`reflector1=` / `atStartup1=0`），不会被写死到某个公共反射器。

---

## 第 10 章　权限模型

- Web 进程以 `mmdvm-web` 系统用户运行（无登录 shell）
- 需要 root 的动作，靠 `/etc/sudoers.d/arch-web` 白名单：

```
mmdvm-web ALL=(root) NOPASSWD: /usr/local/bin/web-manage.sh *, /usr/local/bin/web-fmo.sh *,
                               /usr/local/bin/web-multi.sh *, /usr/local/bin/sys-manage.sh *,
                               /usr/local/bin/web-tetra.sh *, /usr/local/libexec/mmdvm-save.py *,
                               /usr/local/libexec/hosts-build.py *
```

**新增需要 root 的功能时，必须同时往这个白名单里加**，否则 Web 上点了没反应（历史坑）。

---

## 第 11 章　构建与发版

### 11.1 打包

```powershell
python <工作区>\artifacts\arch-hotspot-release\make_release.py
```

脚本做的事：

1. 把 `source\` 复制成 `out\internal\` 和 `out\public\`
2. **public**：执行 `public_app()` 等裁剪函数，删掉集群页面/路由/文件，再全树删掉含
   `TETRA|TDR Link|FlowStation|Bluestation` 的行；删掉 `tetra-api.py`、`web-tetra.sh`、
   `tetra-bs.service` 等
3. 两版都执行 **`sanitize()` 脱敏**：

| 原文 | 替换成 |
| --- | --- |
| `<呼号>` | `NOCALL` |
| `<DMR ID>` … | `000000000` |
| `<密码 / token>` | `CHANGE_ME` |
| `nocall.org.cn` | `example.com` |
| `192.168.1.10` / `192.168.1.20` | `192.168.1.10` / `192.168.1.20` |
<div class="author">作者：BG2GA · <a href="https://arch.hljrsa.cn/disclaimer/">免责声明</a></div>

4. 写 `VERSION`（脚本顶部常量 `VERSION = '1.0.24'`，**发版只改这一行**）、`CHANNEL`、`LICENSE`、`DISCLAIMER.md`
5. 从模板生成 `install.sh`、`arch-hotspot-update`、`build_upstream.sh`
6. 所有 `.sh` / `.py` 强制 LF 换行（避免 Windows 上 CRLF 把脚本搞坏）
7. 打 tar.gz + 生成 `.sha256`，并产出 `web\<通道>\current.json` 与云端引导 `install.sh`

### 11.2 发版检查清单

- [ ] `VERSION` 已改
- [ ] 公开包全树 `TETRA|TDR Link|FlowStation` 命中 = **0 个文件**
- [ ] 两个包都没有 `<呼号>` / `<DMR ID>` / `<密码>` / 内网 IP（除页脚作者行）
- [ ] 解包后 `sha256sum -c` 通过、所有 `.sh` 过 `bash -n`、所有 `.py` 过 `python3 -m compileall`
- [ ] 上传 93 `/opt/arch-site/downloads/arch-hotspot/{internal,public}/`（含 `current.json`、`install.sh`）
- [ ] 部署 Cloudflare Pages：`bash /opt/dmr-admin/work/deploy-arch-site.sh`
- [ ] 公网下载回来校验 SHA-256
- [ ] 写 `ARCH-维护记录.md`，更新本备份目录

### 11.3 数据镜像（服务器侧，每 4 小时）

`arch-hotspot-mirror.py` + `arch-hotspot-mirror.timer`（00:15 / 04:15 / 08:15 / 12:15 / 16:15 / 20:15，随机延迟 5 分钟）
抓取全球数据 → 应用 `apply_arch_overlay()` 把 ARCH 自建服务器置顶并写注释 → 部署到 Cloudflare。
设备端 `update-database.sh` 永远从这个镜像取。

---

## 第 12 章　已知坑与修复索引（务必先读）

| 现象 | 根因 | 修法 |
| --- | --- | --- |
| 装了官方 Lite 系统后 `curl` 报 `certificate is not yet valid` | 树莓派时钟未同步，证书时间被判为"未生效" | 引导脚本与升级脚本都带 `curl -k` 回退 |
| 新装设备 DMR 上不了 BM，提示呼号必须与 BM 注册一致 | 打包时把呼号/ID 脱敏成 `NOCALL`/`0000000`，用户没填 | 属正常现象，用户在页面填自己的呼号与 ID |
| 打包后 DMR 2 时隙不能用 / 本地手台发射不解码 | `MMDVM-IQ/IO.cpp` 双工模式下只在发射时喂全时隙解码器 | 已在 `build_upstream.sh` 打补丁（始终喂 `dmrRX`） |
| 单通道切模式被拦"其它通道占用" | 前端与 `channel-setup.py` 把未启用的历史通道也算占用 | 只统计运行中的通道 |
| DMR 配置页读不到值 | `get-config` 只读第 1 路 | 按 `channels.json` 找实际 DMR 通道 |
| 勾选 ESSID 保存后消失 | 后端 `save_dmr` 忽略 ESSID 参数 | 合成 9 位 ID，只写 DMR |
| YSF/P25/NXDN 的网络语音不显示"通话中"、不变红 | 这些模式网络侧**没有 header 行**，前端只认 `header` | 增加 `received network transmission` / `received network data from` / `received rf voice transmission` 关键字与判定 |
| FM 通道也不变红 | 桥的开始行带了 `0.1 seconds`，被判成已结束 | 开始行不带时长 |
| 通话看板刷新慢 / Pi CPU 高 | 每次轮询都要起一个 journalctl + python；8 次调用 | 一次批量 journalctl + 0.6 秒折叠缓存 |
| DMR 列表里没有 ARCH 46002 | `hosts-build.py` 没有置顶逻辑、`update-database.sh` 没有 DMR 更新通道 | 新增 `_ARCH_*` 常量与 `dmrhosts` 子命令 |
| 页面脚本报错、白屏 | 前端全塞在 `app.py` 里，改错一处就整体挂 | 用锚点精确替换；出问题看页面底部的"页面脚本错误"提示条 |
| 录不了音 / 房间列表不全 | 房间来源是 `channels.json`，没启用对应通道就不会出现 | 先开通道，再看录音计划 |

---

## 第 13 章　排障速查

```bash
# 服务全景
systemctl list-units --type=service --no-pager | grep -E 'mmdvm|gateway|fmo|arch-|nginx|mosquitto'

# 某一路的协议日志
sudo journalctl -u mmdvmhost@1.service -f

# Web 后端
sudo journalctl -u mmdvm-web.service -n 100 --no-pager

# 通道规划
cat /etc/mmdvm/channels.json

# 手工跑一遍脚本（比 Web 好排错）
sudo /usr/local/libexec/mmdvm-status.py
sudo /usr/local/libexec/mmdvm-status.py qso | head
sudo /usr/local/bin/web-manage.sh get-config DMR
sudo /usr/local/libexec/hosts-build.py -v

# 数据更新
sudo /usr/local/bin/update-database.sh hosts
sudo /usr/local/bin/update-database.sh dmrhosts
```

**改任何东西之前**：
1. 先备份：`sudo cp -a <文件> <文件>.bak.<说明>-$(date +%Y%m%d-%H%M%S)`
2. 先看日志/先手工验证，别猜
3. 改完立刻 `python3 -m py_compile` / `bash -n` 自检
4. 写进 `ARCH-维护记录.md`

---

## 第 14 章　备份与回滚

| 要备份什么 | 放哪 |
| --- | --- |
| 热点侧全部源码 | `03-配置备份\ARCH热点固件-<版本>-完整备份-<日期>\source\` |
| 两个成品包 + 哈希 + current.json | 同目录 `release\` |
| 打包脚本 | 同目录 `build\` |
| 服务器镜像脚本 | 同目录 `server-mirror\` |
| 实机现场配置 | 同目录 `device-15-live\` |
| 日常开发源码 | `07-当前工程工作区\ARCH-业余玩无线电融合中枢\artifacts\arch-hotspot-release\`（**打包用的就是这份**） |

回滚：
- 单文件 → 设备上 `cp -a xxx.bak.<时间> xxx` 后重启对应服务
- 整包 → 把旧版 tar 传到设备，`tar xzf` 后 `sudo bash install.sh`
- 数据 → `sudo /usr/local/bin/update-database.sh hosts` 重新拉