# 機台網路訊號上報規格（RSRP / RSSI / SINR）

> 目的：讓後台（star-cloud）能讀取現場機台的行動網路品質，用於監控與低訊號告警。
> 狀態：規格定案，待實作。最後更新：2026-06-18。
> 設計原則：**全程 additive**，沿用既有 heartbeat / machine_logs 架構，不破壞線上 main（晟崴／中國醫）既有 API 與資料結構。

---

## 0. 全鏈路（沿用現有路徑，無新通道）

```
App (每 60s heartbeat)
  → MQTT  machine/{serial}/heartbeat        ← payload 加巢狀 network 物件
  → Go Gateway  RPUSH redis: mqtt_incoming_jobs
  → Laravel  php artisan mqtt:listen (BLPOP)
  → Job  ProcessHeartbeat.php                ← 解析 network + 寫入
  → DB   machines(快照欄位) + machine_logs(時序，仿溫度)
  → Admin  index.blade.php network 分頁(仿 temperatureAjax 圖表)
```

已驗證事實（原始碼對帳）：
- App：`MqttManager` 每 60s 發 `machine/{serial}/heartbeat`；payload 為 Gson 序列化的 `HeartbeatPayload`（snake_case，目前僅 `firmware_version`/`temperature`）。
- App：`FontendActivity.getMobileSignalStrength()` 目前只呼叫 `CellSignalStrengthLte.getDbm()`，無 5G、無 RSSI/SINR。
- App：`app/build.gradle` defaultConfig `minSdk 21`、`targetSdk 34`、`compileSdk 35`；**注意 flavor S9XYU 的 `minSdk 28`**，S12XYU 繼承 21。兩者皆低於訊號 getter 所需 API。
- 後台：`mqtt-gateway/` Go → `ListenMqttQueue.php`(BLPOP) → `ProcessHeartbeat::dispatch`，`type=heartbeat` 路由確認。
- 後台：`ProcessHeartbeat.php` 目前更新 `last_heartbeat_at/status/temperature/firmware_version`。
- 後台：machines 表「無」任何訊號欄位。
- 後台：**溫度曲線無專用時序表**，是讀 `machine_logs` 用 `message='Temperature reported: :temp°C'` + `context['temp']` 過濾（`MachineController.php` `temperatureAjax()`），且 `ProcessHeartbeat` 只在溫度變化時才寫 log → 內建降頻。

---

## 1. App 端

### 1.1 HeartbeatPayload 擴充
在 `HeartbeatPayload`（`structure/mqtt/HeartbeatPayload.java`）加一個**巢狀** `network` 物件（不要平鋪，確保舊解析零影響）：

```json
{
  "firmware_version": "1.2.3",
  "temperature": 25,
  "network": {
    "type": "cellular",        // cellular | wifi | ethernet
    "tech": "LTE",             // LTE | NR | null
    "rsrp": -95,               // dBm，行動網路主要覆蓋指標
    "rssi": -65,               // dBm
    "sinr": 12,                // dB
    "rsrq": -10                // dB
  }
}
```
- WiFi / 有線：`type` 對應，行動欄位給 null。
- 機台未送 `network`（舊版）→ 後台視為 null，不寫訊號 log。

### 1.2 訊號讀取（抽成 util，與 UI icon 共用）
把 `getMobileSignalStrength()` 邏輯抽成可重用方法，補齊三項並加防護：

```java
// LTE：getRsrp/getRssi 需 API 29+；getRssnr(=SINR) 需 API 32+
CellSignalStrengthLte lte = ((CellInfoLte) cellInfo).getCellSignalStrength();
int rsrp = (SDK_INT >= 29) ? lte.getRsrp()   : lte.getDbm();
int rssi = (SDK_INT >= 29) ? lte.getRssi()   : Integer.MAX_VALUE;
int sinr = (SDK_INT >= 32) ? lte.getRssnr()  : Integer.MAX_VALUE;
int rsrq = (SDK_INT >= 29) ? lte.getRsrq()   : Integer.MAX_VALUE;

// 5G NR：需判斷 CellInfoNr / CellSignalStrengthNr（API 31+）
//   getSsRsrp() / getSsRsrq() / getSsSinr()，tech 標記為 "NR"
```

**必做防護（否則低版機台崩潰 / 5G 機台誤報無訊號）：**
1. 所有 API 29+/32+ getter 一律包 `Build.VERSION.SDK_INT` 判斷；取不到的值以 null 上報（`Integer.MAX_VALUE` 視為 null）。
2. **版本防護用 inline guard 即可（agy 詰問後共識，原判已撤回）**：
   - 本專案 minSdk 21 = ART，採惰性驗證；對「**既有類別的新方法**」（如 `CellSignalStrengthLte.getRsrp()`，類別自 API 17 即存在）只要包 `Build.VERSION.SDK_INT` inline guard 就安全，**不會** class-load `VerifyError`/啟動崩潰。這也是本檔既有慣例（[FontendActivity.java:709](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/ui/FontendActivity.java#L709) 的 `getSignalStrength()` API29、:1474 的 TIRAMISU API33 皆 inline 出貨）。Wrapper/Helper class 只是可選風格，非正確性必需。
   - **唯一例外**：引用 API 29+/30+ 才有的**新類別「型別」**（`CellInfoNr`/`CellSignalStrengthNr`）時，少數客製 ROM 仍可能 `NoClassDefFoundError`。故 **5G 分支整段務必在 `SDK_INT` guard 內**（見下點）。
3. **5G NSA fallback（agy 查核）**：現場 5G 多為 **NSA（非獨立組網）**，控制面錨定在 LTE，`getAllCellInfo()` 主小區仍是 `CellInfoLte`。所以**不能只靠 `CellInfoNr`**；邏輯應為「先取 `CellInfoNr`（SA 5G，整段包在 API 31+ guard 內），取不到則 fallback `CellInfoLte`」，否則 NSA 機台拿不到訊號。
4. **權限：App 用 root 自我授權（定案，適配「打包 APK 直裝機台、無 adb、無人值守」佈署模式）**：
   - 現況：`checkPermissionByAccessFineLocation()` 在 Android M+ 直接 `return true`，**沒做執行期動態申請**；App 非平台簽章/系統 App（debug key、無 `sharedUserId`）。但 `getMobileSignalStrength()` 有 `try-catch(SecurityException)` → 未授權也只回 -1，**不會崩**。
   - **關鍵：機台是 rooted**，App 已用 `su` 做乙太網路/OTA/RS232（見 [EthernetTetherHelper.java:10](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/worker/EthernetTetherHelper.java#L10)、[OtaManager.java:529](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/tools/OtaManager.java#L529)）。因此**不需 adb、不需人工、不需跳視窗**。
   - **做法**：App 啟動時（讀訊號前）以 root 自我授權，照抄 `EthernetTetherHelper` 的 `exec("su")` 模式：
     ```
     su -c "pm grant <context.getPackageName()> android.permission.ACCESS_FINE_LOCATION"
     ```
     建議封裝成 `LocationPermissionHelper.ensureGrantedViaRoot()`，失敗靜默（訊號降為 -1，不影響其他功能）。授權具持久性，授一次即可，每次開機保險再跑無妨。
   - 備註：cellular 分支只在 active transport==CELLULAR 才跑（現場多走 WiFi/有線），故行動訊號上報主要在用 SIM 的機台才有資料。

### 1.3 commit 注意
本分支 `feat/general-cart-and-payment` 正在改 `MqttManager`；`HeartbeatPayload` 擴充與訊號讀取請**獨立小 commit**，避免與購物車／支付改動纏在一起。

---

## 2. 後台 star-cloud

### 2.1 採用既有慣例：寫 machine_logs，不另建表
初版**不新建 `machine_network_logs` 表**，沿用溫度同一條管道（已驗證可行）：

`ProcessHeartbeat::handle()` 在解析 `network` 後（整段包在 `if (isset($payload['network']))`，舊機台不進）：

1. **更新 machines 快照欄位**（列表頁即時格數用）。**防 null 覆蓋（agy 查核）**：僅在 `network.type === 'cellular'` 時才寫 `last_rsrp/last_rssi/last_sinr`；切到 WiFi/有線時行動欄位為 null，若無條件覆蓋會把先前的 LTE 快照洗掉。
2. **寫 machine_logs 時序**，但**降頻**（見 §3 閾值）：
   - type = `network`
   - message = `'Signal reported'`
   - context = `{ "rsrp": -95, "rssi": -65, "sinr": 12, "type": "cellular", "tech": "LTE" }`
3. **低訊號告警**（見 §3）：達門檻時另寫一筆 `level=warning`/`error` 的 log，複用 `MachineLog` 既有 Discord hook。

> 若日後需密集採樣再升級為獨立 `machine_network_logs` 表（仿 `machine_logs` append-only、`const UPDATED_AT = null`、複合索引 `(machine_id, recorded_at)`）。初版不需要。

### 2.2 machines 主表新增 nullable 快照欄位
新 migration（仿 `add_status_to_machines_table`），全部 nullable（additive、不破壞既有 query）：

```php
$table->string('network_type')->nullable();   // cellular/wifi/ethernet
$table->smallInteger('last_rsrp')->nullable();
$table->smallInteger('last_rssi')->nullable();
$table->smallInteger('last_sinr')->nullable();
$table->timestamp('last_network_at')->nullable();
```
同步加入 `Machine` model `$fillable`。

### 2.3 Admin 介面（仿溫度圖）
- 新增 `signalAjax()`，複製 `temperatureAjax()` 改成過濾 `message='Signal reported'`、輸出 `context['rsrp']`（及 sinr）成時間序列。
- 前端 `admin/machines/index.blade.php`：
  - 列表頁狀態欄旁加「訊號」格，用 `last_rsrp` 換算格數（與 App icon 邏輯一致）。
  - 詳情面板新增 `network` 分頁，複製溫度圖 Alpine.js 邏輯，打 `signalAjax` 畫 RSRP / SINR 折線。

---

## 3. 閾值定義（本規格核心數值）

### 3.1 指標對照（業界通用）
| 指標 | 單位 | 優 | 良 | 中 | 差 | 極差 |
|---|---|---|---|---|---|---|
| **RSRP**（覆蓋，主指標） | dBm | ≥ -80 | -80~-90 | -90~-100 | -100~-110 | < -110 |
| **RSSI**（總功率，參考） | dBm | ≥ -65 | -65~-75 | -75~-85 | -85~-95 | < -95 |
| **SINR**（訊噪比，越大越好） | dB | ≥ 20 | 13~20 | 0~13 | < 0 | — |

販賣機只做小流量（心跳/交易/同步），重點是**穩定不斷線**，非高速。**-110 dBm 為危險線，-100 以上視為健康。**

### 3.2 寫 log 降頻閾值
- **RSRP 變化 ≥ 5 dB** 才寫一筆（RSRP 自然抖動約 ±3 dB，故用 5 dB 去噪）。
- **保底**：即使無明顯變化，**最多每 30 分鐘補寫一筆**（曲線不斷、可確認機台仍在線且穩定）。
- 判斷依據：比對 `machines.last_rsrp` 與 `machines.last_network_at`。

### 3.3 低訊號告警閾值（Discord）
- **RSRP < -110 dBm 且連續 3 次心跳（約 3 分鐘）皆低於** → 告警（避免瞬間抖動誤報）。
- 輔助：**SINR < 0 dB 持續** → 一併告警（干擾大、易斷）。
- **加 RSRQ（agy 建議）**：RSRP 看似正常（如 -95）但 **RSRQ < -15 dB** 代表多重路徑反射／同頻干擾嚴重，仍會交易超時 → RSRQ < -15 dB 持續也告警。
- 分級建議：
  - RSRP -100 ~ -110：`warning`（觀察）
  - RSRP < -110：`error`（派人檢查天線／機台位置）

### 3.4 ⚠️ 告警盲點與修正（agy 查核，最致命）
**問題**：RSRP < -110 時機台往往已斷網，根本**發不出**那筆低訊號 heartbeat，後台「連續 3 次 < -110」的條件永遠湊不齊 → 訊號最差的機台反而不會告警。

**修正**：低訊號告警**必須與既有「離線偵測機制」結合**（比對 `last_heartbeat_at` 逾時的排程）。判定離線時主動檢查該機台 `machines.last_rsrp`：
- 若最後一筆 RSRP 已 < -105 dBm → 告警訊息註記「機台離線，最後上報訊號極差 (RSRP: -108dBm)，疑似訊號不佳斷線」。
- 否則照原離線告警處理。

如此「訊號差→斷線→離線告警帶出最後訊號」與「訊號差但仍在線→低訊號告警」兩條路徑互補,涵蓋完整。

---

## 4. 相容性檢查（對照後台慣例）
- ✅ heartbeat payload 用巢狀 `network`，舊欄位零變動；舊機台不送 → null。
- ✅ machines 全用 nullable 欄位，既有 query／API 回應結構不破壞。
- ✅ 沿用 machine_logs + 新增 admin endpoint，不碰 B 系列既有 endpoint。
- ⚠️ 註：「nullable／不破壞回應結構／B 系列不可刪」是後台**實務慣例**（migration/fillable 習慣），非 `api-rules.md` 明文 MUST 條款；仍應遵循。

---

## 5. 實作清單（給後台同事 / 後續排程）
**App 端**
- [ ] **權限自我授權**：新增 `LocationPermissionHelper.ensureGrantedViaRoot()`，啟動時以 `su -c "pm grant <pkg> ACCESS_FINE_LOCATION"` 靜默授權（照抄 `EthernetTetherHelper`，失敗不崩）（§1.2-4）
- [ ] 訊號讀取：補 RSSI/SINR/RSRQ + inline `Build.VERSION.SDK_INT` 防護（符合本庫慣例，毋須 Wrapper class）+ `CellInfoNr`→`CellInfoLte` fallback、5G 分支整段在 API31+ guard 內（§1.2-2/3）
- [ ] `HeartbeatPayload` 加巢狀 `network`（含對應 class，測 Gson JSON 結構；null 不序列化已驗證安全）
- [ ] 獨立 commit（勿混入購物車／支付）

**後台 star-cloud**
- [ ] migration：machines 加 5 個 nullable 快照欄位 + 更新 `Machine` fillable
- [ ] `ProcessHeartbeat`：解析 `network` → **僅 cellular 才更新快照（防 null 覆蓋）** + 依 §3.2 降頻寫 machine_logs + 依 §3.3 告警
- [ ] **離線偵測排程**：判離線時帶出 `last_rsrp` 註記「疑似訊號不佳斷線」（§3.4 告警盲點修正）
- [ ] `signalAjax()` endpoint（仿 `temperatureAjax`）
- [ ] `index.blade.php`：列表訊號格 + 詳情 network 圖表分頁

---

## 6. 查核紀錄
- 2026-06-18 初稿：內建 Agent 子代理對帳 App + star-cloud 原始碼，A1–A7 事實成立。
- 2026-06-18 agy 交叉比對（Antigravity 獨立讀兩 repo）：A1–A7 全部複核成立；提出 5 項補充——告警斷網盲點(§3.4)、權限缺口(§1.2-4)、VerifyError(§1.2-2)、5G NSA fallback(§1.2-3)、WiFi null 覆蓋保護(§2.1)、RSRQ 干擾告警(§3.3)；更正 minSdk 因 flavor 而異(S9XYU=28)。
- 2026-06-18 與 agy **交叉詰問**（非照單全收，回原始碼查證後辯論）：
  - 爭點1「權限 🔴 致命」→ **agy 修正、降級**：有 try-catch 不會崩、kiosk 多半預授、cellular 分支少觸發 → 改列「需現場驗證的穩健性缺口」(§1.2-4)。
  - 爭點2「必須 Wrapper class 否則啟動崩」→ **agy 認錯、撤回**：minSdk21=ART 惰性驗證，inline `SDK_INT` guard 足夠且為本庫慣例(:709)；唯引用 5G 新「型別」需整段 guard(§1.2-2)。
  - 5G NSA fallback / WiFi null 覆蓋 / 告警綁離線偵測三點，我覆核後同意，維持。
