[FIX] 修復遠端 OTA 更新失敗並依環境區分 APK 版本後綴

1. 新增 DOWNLOAD_WITHOUT_NOTIFICATION 權限:OtaManager 改用 DownloadManager 後以 VISIBILITY_HIDDEN 靜默下載,缺此權限會在 enqueue 時丟出「Invalid value for visibility: 2」導致每次遠端更新一啟動就失敗。
2. versionName 後綴依 build type 動態切換:release → _R(正式)、debug → _D(測試),現場可一眼辨識 APK 來源;versionCode 不含後綴、兩者相同不影響 OTA 比版。
3. compiler-apk workflow 的後台 keyin 列印由寫死 _R 改為 _D,與實際 debug 檔名一致。
4. verPatch 升至 13。
5. 新增 docs/network-signal-reporting-spec.md 網路訊號上報規格文件。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-18 16:16:47 +08:00
parent c7da4d1a12
commit 8d764ba388
4 changed files with 193 additions and 3 deletions

View File

@ -105,7 +105,7 @@ if [ -n "$APK_SOURCE" ]; then
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_D"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
echo "=================================================="

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 2
def verPatch = 11
def verPatch = 13
@ -76,9 +76,12 @@ android {
verMajor = 10 // 10
}
// release Rdebug D APK
def envSuffix = (buildType == "release") ? "R" : "D"
// Variant
def calculatedVersionCode = verMajor * 10000 + calculatedMinor * 100 + calculatedPatch
def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_R"
def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_${envSuffix}"
variant.outputs.each { output ->

View File

@ -14,6 +14,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- OTA 以 DownloadManager VISIBILITY_HIDDEN 靜默下載,需此權限否則 enqueue 會丟 Invalid value for visibility: 2 -->
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<application
android:name=".MyApp"

View File

@ -0,0 +1,185 @@
# 機台網路訊號上報規格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
// LTEgetRsrp/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 / CellSignalStrengthNrAPI 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 fallbackagy 查核)**:現場 5G 多為 **NSA非獨立組網**,控制面錨定在 LTE`getAllCellInfo()` 主小區仍是 `CellInfoLte`。所以**不能只靠 `CellInfoNr`**;邏輯應為「先取 `CellInfoNr`SA 5G整段包在 API 31+ guard 內),取不到則 fallback `CellInfoLte`」,否則 NSA 機台拿不到訊號。
4. **權限:真機需驗證 + 穩健性缺口agy 詰問後降級,非確證致命 bug**`checkPermissionByAccessFineLocation()` 在 Android M+ 直接 `return true`**沒做執行期動態申請**,且全專案無 `requestPermissions`、非平台簽章。但 `getMobileSignalStrength()``try-catch(SecurityException)` → 即使未授權也只是回 -1**不會崩**。
- 這些是**專用 kiosk 機**`ACCESS_FINE_LOCATION` 多半在佈署時用 `adb shell pm grant` 或 provisioning 預授;且 cellular 分支只在 active transport==CELLULAR 時才跑(現場多走 WiFi/有線),故是否真壞**未經現場驗證**。
- **行動**:實作訊號上報前,先在目標機台確認權限已授(`adb shell dumpsys package <pkg> | grep ACCESS_FINE_LOCATION`);若未授,選擇「佈署腳本 `pm grant`」或「App 內補執行期申請」其一即可。不列為阻斷性 bug但**不確認就上線,行動網路訊號會全空**。
### 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`),全部 nullableadditive、不破壞既有 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 持續** 一併告警干擾大易斷
- **加 RSRQagy 建議)**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 欄位,既有 queryAPI 回應結構不破壞。
- ✅ 沿用 machine_logs + 新增 admin endpoint不碰 B 系列既有 endpoint。
- ⚠️ 註「nullable不破壞回應結構B 系列不可刪」是後台**實務慣例**migration/fillable 習慣),非 `api-rules.md` 明文 MUST 條款;仍應遵循。
---
## 5. 實作清單(給後台同事 / 後續排程)
**App 端**
- [ ] **先驗權限**:在目標機台確認 `ACCESS_FINE_LOCATION` 已授(`adb shell dumpsys package <pkg> | grep FINE_LOCATION`);未授則 `pm grant` 或補執行期申請§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**
- [ ] migrationmachines 加 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 原始碼A1A7 事實成立。
- 2026-06-18 agy 交叉比對Antigravity 獨立讀兩 repoA1A7 全部複核成立;提出 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 覆蓋 / 告警綁離線偵測三點,我覆核後同意,維持。