Metadata-Version: 2.4
Name: meiyoufrpclient
Version: 1.0.3
Summary: Cloud box frp client (aarch64 frpc bundled)
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: psutil<7,>=5.6.4
Provides-Extra: systemd

# meiyoufrpclient（云盒 frp 客户端）

在蜂窝模块（如 Quectel RM500U）拨号联网后，从控制面 HTTP 拉取 frp 参数，生成本地 `frpc` 配置并启动客户端；同时提供本机状态 HTTP 接口，便于观测联网与配置阶段。

日志：运行目录下按天落盘 `djcloudbox-YYYY-MM-DD.log`（见 `src/meiyoufrpclient/main.py`）。

配置文件默认路径 **`~/deploy/meiyoufrpclient.env`**（见 [PyPI 安装](#pypi-安装)）；也可用 `MEIYOUFRP_ENV` 指定路径。包内提供 `meiyoufrpclient.env.example` 作模板。

## 启动流程简图（中文）

```mermaid
flowchart TD
  A[启动入口 main] --> B[初始化日志 setup_logging]
  B --> C[进入主编排 run_forever]
  C --> D[执行 bootstrap]

  D --> E[读取 client_sn]
  E --> F[初始化云盒状态 cloud_state]
  F --> G[启动本机状态接口 status_api]
  G --> H[DNS 预检查 ensure_dns_configured]

  H --> I[等待网络就绪 wait_network_ready]
  I --> J{ICMP 探测是否可用}
  J -->|可用| K[尝试预取远程配置 _try_fetch_config_as_connectivity]
  J -->|不可用| L[后台拨号 sim_dial]
  L --> M[拨号后 DNS 再检查]
  M --> I

  K --> N{是否已拿到配置}
  N -->|是| O[写入 frpc.toml]
  N -->|否| P[循环拉远程配置 wait_remote_config]
  P --> O

  O --> Q[启动 frpc 子进程]
  Q --> R[进入看门狗循环]

  R --> S[周期性网络检测 check_network_connection]
  S --> T[更新 cloud_state 网络状态]
  T --> T1[触发本机 D-BUS NetworkStatusChanged]
  T1 --> SUB[订阅端如 MeiyouDrone 可转 MQTT]
  T1 --> U[sleep 间隔]
  U --> R
```



## 编排与状态相关环境变量（`FRPCLIENT_*`）

与 `frpc.toml` 中 frp 官方字段区分开，本进程编排与状态服务使用 `FRPCLIENT_*`：


| 变量                                | 默认                          | 作用                                             |
| --------------------------------- | --------------------------- | ---------------------------------------------- |
| `FRPCLIENT_POST_DIAL_WAIT_SEC`    | `3`                         | 拨号发出后等待几秒再做 DNS 复检                             |
| `FRPCLIENT_DIAL_TIMEOUT_SEC`      | `30`                        | 单轮拨号后持续不通网超过该秒数则重拨                             |
| `FRPCLIENT_CONFIG_RETRY_SEC`      | `30`                        | 拉控制面配置失败后的重试间隔                                 |
| `FRPCLIENT_WATCHDOG_INTERVAL_SEC` | `5`                         | 主循环网络看门狗（ICMP）间隔                               |
| `FRPCLIENT_STATUS_HOST`           | `127.0.0.1`                 | 状态 HTTP 绑定地址                                   |
| `FRPCLIENT_STATUS_PORT`           | `18080`                     | 状态 HTTP 端口                                     |
| `FRPCLIENT_NET_FAIL_THRESHOLD`    | `2`                         | 连续 ICMP 探测失败多少次后判定“离线”（用于 D-BUS/状态快照）          |
| `FRPCLIENT_FRPC_BIN`              | （自动探测）                      | `frpc` 可执行文件路径；未设置时依次尝试：本变量 → pip 包内 `frpc_bin/frpc` → `PATH` 中的 `frpc` |
| `FRPCLIENT_NO_DIAL`               | （无）                         | 设为 `1` 时跳过 `sim_dial`（由 systemd 等单独管拨号时使用）     |
| `FRPCLIENT_QUECTEL_CM`            | `quectel-CM`                | 拨号可执行文件路径                                      |
| `FRPCLIENT_QUECTEL_CM_SUDO`       | `1`                         | 设为 `0` 时不通过 sudo 调用拨号程序                        |
| `FRPCLIENT_DIAL_VERBOSE`          | `0`                         | 设为 `1` 时在控制台打印 quectel-CM 详细输出；默认静默写日志         |
| `FRPCLIENT_DIAL_LOG_PATH`         | `quectel-cm-YYYY-MM-DD.log` | `FRPCLIENT_DIAL_VERBOSE=0` 时 quectel-CM 输出文件路径 |


配置文件查找顺序（**启动时自动加载文件中全部键**，不覆盖已存在的环境变量）：

1. `MEIYOUFRP_ENV` 指向的路径  
2. `~/deploy/meiyoufrpclient.env`  
3. 源码/安装目录旁 `deploy/meiyoufrpclient.env`

控制面相关（写在上述 env 文件中）：


| 变量                 | 说明                                   |
| ------------------ | ------------------------------------ |
| `CLOUD_BOX_SN`     | 云盒序列号；常用 **eth0 MAC 大写、无冒号**（例 `0E8F5573101F`） |
| `CLOUD_CONFIG_URL` | 控制面 API，支持 `{CLOUD_BOX_SN}`          |
| `CLOUD_API_SECRET` | 控制面 `api_secret`（请求头 `X-Api-Secret`） |
| `MEIYOUFRP_ENV`    | 可选，显式指定 env 文件绝对路径                    |


状态接口路由：

- `GET /health` → `{"ok": true}`
- `GET /api/v1/net/status` → `cloud_state` 快照，并含 `frp_running`（是否检测到 `frpc` 进程）

## 本机 D-BUS 联网状态信号（可观测联动）

程序会在主循环的网络探测结果变化时，向系统 D-BUS 发出一个联网状态信号，供本机其它脚本订阅联动显示。

- Bus：默认 `system`
- 对象路径：`/org/meiyoufrpclient/Network`
- 信号接口/成员：`org.meiyoufrpclient.Network.NetworkStatusChanged`
- 信号入参顺序：
  - `string client_sn`
  - `boolean connected`
  - `int32 delay_ms`
  - `int32 online_duration_seconds`
  - `string last_seen`
  - `string status`（在线/离线）

调试订阅：

```bash
dbus-monitor --system "type='signal',interface='org.meiyoufrpclient.Network',member='NetworkStatusChanged'"
```

订阅端建议放在其它包中（本机同机部署 **MeiyouDrone** 时，由其 `dbus_subscriber` 订阅 D-BUS 并用业务 MQTT 发布 `cloudbox/{云盒SN}/network/status`），或使用 `dbus-monitor` 调试。

可选开关：

- `FRPCLIENT_DBUS_ENABLE=0`：禁用 D-BUS 广播
- `FRPCLIENT_DBUS_BUS=session|system`：选择 `session` 或 `system` 总线（默认 system）
- `FRPCLIENT_DBUS_OFFLINE_REPEAT_SEC=5`：离线状态下重复广播的最小间隔（秒）；设为 `0` 关闭离线重复广播

## 网卡与探测（`CLOUDBOX_`*）

由 `NetUtil.py` 使用（ICMP 绑网卡顺序：先蜂窝相关接口，再有线）：

- `CLOUDBOX_QUECTEL_IFACE`：蜂窝接口列表，逗号分隔；默认含 `wwan0`，并与自动发现的 `enx`* USB 网卡组合。
- `CLOUDBOX_ETH_IFACE`：有线/无线接口列表，**逗号分隔**（例 `wlan0,eth0`）；默认 `eth0`。
- `DNS_RESOLVECTL_INTERFACE`：若填写了本机不存在的网卡名，启动时会 **WARNING** 提示（不会静默失败）。
- `CLOUDBOX_PING_HOST`：ICMP 目标主机；默认 `223.5.5.5`（IP 直连，避免 DNS 波动影响联网判定）。

若本机 `ping` 不支持 `-I`（常见于 BusyBox），会回退为默认路由 ping；需要按网卡探测时可安装 `iputils-ping`。

联网判定策略：先 ICMP（按网卡绑定 + 默认路由兜底）。在拨号阶段会直接尝试访问控制面配置接口，只要能返回合法 JSON 配置，即视为已联网并继续启动 frpc。

`frpc` 鉴权 token 由控制面 JSON 下发（`authToken` / `auth_token` / `token`）；若未下发 token，客户端会进入 `frp_error`，不会再用本地默认 token 兜底。

## 仓库结构

说明：`src/` 是源码根目录（src layout），实际 Python 包名是 `meiyoufrpclient`。

```
meiyou-frpclient/
├── README.md
├── pyproject.toml
├── requirements.txt
├── deploy/
│   ├── meiyoufrpclient.env
│   ├── meiyoufrpclient.service      # 系统级 unit 示例（需 root）
│   └── djifrpclient.user.service    # 用户级 unit 示例
└── src/
    ├── frpc_bin/
    │   └── frpc                     # aarch64 静态链接 frpc（随 pip 包安装）
    └── meiyoufrpclient/
        ├── main.py
        ├── orchestrator.py
        ├── cloud_state.py
        ├── status_api.py
        ├── remote_config.py
        ├── dns_connectivity.py
        ├── NetUtil.py
        ├── device_utils.py
        ├── frp.py
        └── frpc.toml
```

## 模块职责一览


| 模块                                        | 作用                                                      |
| ----------------------------------------- | ------------------------------------------------------- |
| `src/meiyoufrpclient/main.py`             | 包入口：`meiyoufrpclient` 脚本调用入口                            |
| `src/meiyoufrpclient/orchestrator.py`     | 编排核心：状态服务 → DNS → 拨号 → 拉配置 → 启动 frp                     |
| `src/meiyoufrpclient/cloud_state.py`      | 线程安全快照：`phase`、`net`、`remote_debug`、`last_config_error` |
| `src/meiyoufrpclient/status_api.py`       | 只读 JSON，供现场或其它进程轮询                                      |
| `src/meiyoufrpclient/remote_config.py`    | GET 控制面 JSON；支持 camelCase / snake_case 字段名              |
| `src/meiyoufrpclient/dns_connectivity.py` | DNS 核心逻辑（resolvectl / resolv 兜底 + 自定义 DNS HTTP）         |
| `src/meiyoufrpclient/NetUtil.py`          | ICMP 与多网卡探测                                             |
| `src/meiyoufrpclient/device_utils.py`     | `sim_dial` 等硬件相关调用                                      |
| `src/meiyoufrpclient/frp.py`              | 根据远程 JSON 生成本地配置并以 `subprocess.Popen` 启动 `frpc`         |


## PyPI 安装

PyPI：[https://pypi.org/project/meiyoufrpclient/](https://pypi.org/project/meiyoufrpclient/)

| 项 | 说明 |
| --- | --- |
| Python | **3.8+**（嵌入式 Ubuntu 18.04 可用；建议 `pip>=21`） |
| pip 升级 | `python3 -m pip install -U pip`（3.8 可用 [get-pip 3.8](https://bootstrap.pypa.io/pip/3.8/get-pip.py)） |
| 架构 | wheel 捆绑 **aarch64** `frpc`；x86 请自备并设 `FRPCLIENT_FRPC_BIN` |

国内用户推荐：

```bash
python3 -m pip install -U pip
python3 -m pip install meiyoufrpclient -i https://pypi.tuna.tsinghua.edu.cn/simple
```

官方源：

```bash
pip install meiyoufrpclient
```

安装后自检（无需先 `source` env，会自动读 `~/deploy/meiyoufrpclient.env`）：

```bash
meiyoufrpclient --check-config
```

wheel 内 `frpc` 位于 `site-packages/frpc_bin/frpc`；`frpc.toml` 由运行时写入包目录。env **不会**自动创建，请复制模板：

```bash
mkdir -p ~/deploy
cp "$(python3 -c "import meiyoufrpclient, os; print(os.path.join(os.path.dirname(meiyoufrpclient.__file__), 'meiyoufrpclient.env.example'))")" ~/deploy/meiyoufrpclient.env
# 源码仓库也可: cp deploy/meiyoufrpclient.env ~/deploy/meiyoufrpclient.env
vi ~/deploy/meiyoufrpclient.env
```

**1. 编辑 env 文件**（标准路径 `~/deploy/meiyoufrpclient.env`）：

写入完整示例（按现场修改 SN、secret、网卡名；`FRPCLIENT_FRPC_BIN` 可省略）：

```text
# ---- 必填 ----
CLOUD_BOX_SN=A25WNXXXXXXXXX
CLOUD_CONFIG_URL=https://meiyouyudi.com/uav/other/cloudbox/api/box/{CLOUD_BOX_SN}
CLOUD_API_SECRET=fiwXXXXX

# ---- 主流程 ----
FRPCLIENT_POST_DIAL_WAIT_SEC=31
FRPCLIENT_DIAL_TIMEOUT_SEC=30
FRPCLIENT_CONFIG_RETRY_SEC=30
FRPCLIENT_WATCHDOG_INTERVAL_SEC=5

# ---- 拨号 ----
FRPCLIENT_NO_DIAL=0
FRPCLIENT_QUECTEL_CM=quectel-CM
FRPCLIENT_QUECTEL_CM_SUDO=1
FRPCLIENT_DIAL_VERBOSE=0
FRPCLIENT_DIAL_LOG_PATH=quectel-cm.log

# ---- frpc（可选；默认使用 pip 包内 site-packages/frpc_bin/frpc）----
# FRPCLIENT_FRPC_BIN=/path/to/custom/frpc

# ---- 状态接口 ----
FRPCLIENT_STATUS_HOST=127.0.0.1
FRPCLIENT_STATUS_PORT=18080

# ---- DNS ----
DNS_PROBE_HOST=www.baidu.com
DNS_FALLBACK_NAMESERVERS=223.5.5.5,114.114.114.114
DNS_MANAGE_RESOLV_CONF=1
DNS_RESOLVECTL_INTERFACE=enxb657fb68191c

# ---- 联网探测 ----
CLOUDBOX_QUECTEL_IFACE=enxb657fb68191c,wwan0
CLOUDBOX_ETH_IFACE=wlan0,eth0
CLOUDBOX_PING_HOST=223.5.5.5
```

保存：按 `Esc`，输入 `:wq` 回车。

**2. 前台试运行**（程序会自动加载 `~/deploy/meiyoufrpclient.env`，一般**无需** `source`）：

```bash
meiyoufrpclient --check-config   # 建议先自检
meiyoufrpclient
```

**3. 用户级 systemd 自启（推荐）**

pip 部署时由 systemd 的 `EnvironmentFile` 注入全部变量，**无需** `source`。妙算等设备无 sudo 时使用用户级 unit。

启用前先停掉前台或 nohup 实例，避免端口 18080 冲突：

```bash
pkill -f '\.local/bin/meiyoufrpclient' 2>/dev/null || true
pkill -x frpc 2>/dev/null || true
```

新建 unit 文件（示例可直接拷贝，按现场改 Python 版本路径）：

```bash
mkdir -p ~/.config/systemd/user
vi ~/.config/systemd/user/djifrpclient.service
```

写入：

```ini
[Unit]
Description=meiyoufrpclient (pip install, user service)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=-%h/deploy/meiyoufrpclient.env
ExecStart=%h/.local/bin/meiyoufrpclient
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target
```

保存后启用：

```bash
systemctl --user daemon-reload
systemctl --user enable --now djifrpclient
loginctl enable-linger "$USER"    # 未登录时也保持用户服务运行
```

说明：`%h` 为当前用户 home；env 默认 `%h/deploy/meiyoufrpclient.env`。pip 安装的 unit 示例亦在 `share/meiyoufrpclient/systemd/`。

常用运维：

```bash
systemctl --user status djifrpclient
journalctl --user -u djifrpclient -f
systemctl --user restart djifrpclient    # 改 env 后重启
systemctl --user disable --now djifrpclient
```

**4. 系统级 systemd 自启（需 root，可选）**

有 sudo 的服务器可用系统级 unit，以指定用户（如 `meiyou`）运行 pip 安装的 `meiyoufrpclient`。env 路径为 `~/deploy/meiyoufrpclient.env`（须由运行用户创建）。

```bash
sudo vi /etc/systemd/system/meiyoufrpclient.service
```

写入（将 `meiyou` 与 `/home/meiyou` 改为实际运行用户）：

```ini
[Unit]
Description=meiyoufrpclient (pip install)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=meiyou
Group=meiyou
EnvironmentFile=-/home/meiyou/deploy/meiyoufrpclient.env
ExecStart=/home/meiyou/.local/bin/meiyoufrpclient
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
```

保存后启用：

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now meiyoufrpclient
journalctl -u meiyoufrpclient -f
```

说明：须以目标用户执行 `pip install meiyoufrpclient`；env 放在该用户 `~/deploy/` 下。妙算等无 sudo 设备用上文 §3 用户级 unit 即可。

## Ubuntu 18.04 + aarch64（妙算 / Wheeltec 等）

1. Python 3.8 + pip：`python3 -m pip install -U pip`（必要时用 get-pip 3.8 脚本）  
2. `pip install meiyoufrpclient -i https://pypi.tuna.tsinghua.edu.cn/simple`  
3. `mkdir -p ~/deploy`，从 `meiyoufrpclient.env.example` 复制并填写 SN（eth0 MAC 大写无冒号）  
4. `meiyoufrpclient --check-config` → `meiyoufrpclient` 或配置用户级 systemd  
5. 状态：`curl -sS http://127.0.0.1:18080/api/v1/net/status`

## 启动方式

开发环境运行（不安装，`frpc` 自动使用 `src/frpc_bin/frpc`）：

```bash
PYTHONPATH=src python3 -m meiyoufrpclient.main
```

源码 editable 安装（同样包含 `frpc_bin`）：

```bash
python3.9 -m pip install -e .
meiyoufrpclient
```

源码部署 + 环境文件：

```bash
set -a && source deploy/meiyoufrpclient.env && set +a
python3 -m meiyoufrpclient.main
```

源码目录用户级 systemd：示例见 `deploy/djifrpclient.user.service`；**生产 pip 部署见上文 [PyPI 安装 §3](#pypi-安装)**。

系统级 systemd（需 root）：**见上文 [PyPI 安装 §4](#pypi-安装)**；仓库参考 `deploy/meiyoufrpclient.service`。

查看状态：

```bash
curl -sS http://127.0.0.1:18080/health
curl -sS http://127.0.0.1:18080/api/v1/net/status
```

本机快速自检（与客户端相同的 DNS/ICMP 逻辑）：

```bash
PYTHONPATH=src python3 -m meiyoufrpclient.check_network
PYTHONPATH=src python3 -m meiyoufrpclient.check_network --status
```

## 依赖

见 `pyproject.toml`：Python ≥ 3.8，`psutil>=5.6.4`；wheel 捆绑 aarch64 `frpc`（约 14MB）。变更见 [CHANGELOG.md](CHANGELOG.md)。

联网状态通过 D-BUS 发出，由 D-BUS 等订阅端转发 。
