樣本:SonoiQ-2.2.15_260113 (1).apk(299 MB)· SHA-256867c136a…60a2c1生成:2026-09-03 · 授權:離線樣本靜態分析 · 21 節合併全景
🗺️ App 全景圖 — The Whole Picture
flowchart TB
subgraph HW["🔬 探頭硬件 Probe Hardware"]
FPGA[["Efinix Trion T55 FPGA<br>波束形成 · 掃描轉換"]]
MCU[["TI CC3220/CC3235 WiFi MCU<br>AP 模式 192.168.254.1"]]
USB[["Cypress FX USB<br>VID 0x04B4 · EP6 bulk"]]
AUTH[("授權存儲<br>EEPROM / SD block 2-12<br>B·C·PW·DICOM·Vet 位")]
PIEZO[壓電陣元 16ch/32ch]
end
subgraph XPORT["📡 傳輸層"]
USBIF[UsbProbeManager<br>FPGA 地址映射 0x20000/0x40000/0x60000]
WLANIF[WlanProbe / CmdParser<br>0xFE 頭命令碼 · UDP 8081]
end
subgraph NATIVE["🧠 Native 引擎 (arm64)"]
DOP[libdoppler 15.4MB<br>DSP · DSC · DICOM 棧 · hash 授權]
SOCK[libHSocketIf · libh264rtp]
AIENG[PyTorch 76MB · TFLite · Vosk · OpenCV · FFmpeg]
end
subgraph JAVA["☕ Java 核心"]
PUMP[kernel.MainHandler<br>JavaGetMessage 輪詢泵]
CORE[Ultrasys 單例<br>ImagePlayer · Cine · MITI]
AISVC[ai.* 16 診斷類<br>膀胱·心臟·甲狀腺·乳腺…]
DICJ[JDicom* 服務<br>Storage·Worklist·MPPS·Print]
MEAS[preset · measurement<br>OB_table 177 表]
end
subgraph UI["🖥️ UI 層"]
SPLASH[SplashActivity 登錄<br>Admin/12345678 MD5]
MODELS[18 OEM 機型<br>LandQ6 · PortQ61 · Lianmed…]
VET[人/獸醫模式 0-6 級]
end
subgraph EXT["🌐 外部世界"]
PACS[["醫院 PACS / DICOM"]]
HIS[["HIS 系統<br>SHA-1 簽名 API"]]
SDK[["第三方 App<br>ThirdPartyManageActivity"]]
end
subgraph DATA["📦 資產"]
PID[["PID03.zip<br>77 探頭型號 + 13 AI 模型"]]
FW[["fw/board v2/v3<br>FPGA + CC32xx 固件"]]
end
PIEZO --> FPGA
FPGA --> USB
USB --> USBIF
MCU --> WLANIF
USBIF --> DOP
WLANIF --> DOP
DOP <--> SOCK
DOP --> PUMP
PUMP --> CORE
CORE --> AISVC
AISVC --> AIENG
CORE --> DICJ
CORE --> MEAS
CORE --> UI
SPLASH --> MODELS
MODELS --> VET
DICJ --> PACS
AUTH -.-> DOP
HIS -.->|HttpProtocol_hle| DICJ
SDK -.->|exported| SPLASH
PID -.-> DOP
FW -.-> FPGA
執行摘要 — 靜態分析報告
SonoiQ 2.2.15 — 靜態逆向分析報告
| 項目 | 內容 |
|---|---|
| 樣本 | SonoiQ-2.2.15_260113 (1).apk |
| SHA-256 | 867c136abf74211da7eaa511f0d5a1de022064421d7b2a5410d105f32060a2c1 |
| 大小 | 299,234,800 bytes(解壓 482 MB,2,182 檔案) |
| 包名 | com.SonoiQ.uhandprobe |
| 標籤 | SonoiQ,launcher com.Com.handprobe.SplashActivity |
| 授權 | 離線樣本靜態分析(case sono-iq,auth=granted,network_profile=offline) |
| 工具 | jadx 1.5.6、apktool 3.0.3、OpenJDK 17(tool-index 路徑) |
| 日期 | 2026-09-03 |
1. 樣本定性
手持超聲波診斷設備的 Android 宿主 App(OEM/ODM 白牌方案)。連接 USB / Wi-Fi 超聲探頭,提供 B/M/Color/PW 成像、AI 輔助診斷、DICOM、報告打印等完整臨床工作流。
高置信度依據(Evidence 鏈):
- E-01:
assets/pid/含 FPGA bitstream ×6(FPGA_ch16*.rbf,Intel/Altera RBF)與探頭描述庫PID03.zip(140.8 MB) - E-02:
lib/arm64-v8a/libdoppler.so(15.4 MB)+HPidIf.GetProbeElementNumber()等探頭 API - E-03:
handprobe.application.ai.*16 個診斷類(BladderMeas、CardiacAutoEF、ThyroidDiagnose…) - E-04:DICOM 全家桶(
JDicomServer/Storage/Worklist/MPPS/Print) - E-05:
MyApplication內 18 個OEM_*_ID常量(ESAOTE、BD、ECCOSCAN、SONOSTAR…)
2. 技術構成
dex(Java 層)
classes.dex9.5 MB /classes2.dex2 KB /classes3.dex4.5 MB- jadx 1.5.6 處理 6,076 個 class,18 個反編譯錯誤(0.3%)
- 錯誤分佈:第三方庫 9 檔(mockito、apache-poi、okhttp3 Http2Connection、android.support GridLayoutManager/Flow、zxing Code128Reader),自有代碼僅 1 檔
handprobe/components/file/fileselect/utils/FileUtils.java(type-inference overflow,單一超大 method,非混淆/加固) - 主要第三方:xUtils(ORM)、OkHttp 3、OpenCV、PyTorch、TFLite、Vosk、zxing(條碼)、mp4parser、JNA(
libjnidispatch)
native(lib/arm64-v8a,僅此 ABI)
| 分類 | 庫 |
|---|---|
| 超聲核心 | libdoppler(PID/波束/前端/DSC/DICOM/系統控制) |
| 傳輸 | libHSocketIf(USB/串口/GL 視圖)、libh264rtp |
| 影像 | libopencv_java4、libhopencvif_lib、libfpgargb_lib |
| AI | libpytorch_jni(76 MB)、libtensorflowlite_jni+GPU、libvosk |
| 視頻/文檔 | FFmpeg 56 系列(avcodec/avformat/avfilter/avutil/swscale/swresample/postproc)、libx264、libmodpdfium/libjniPdfium |
| 其他 | libjnidispatch(JNA)、libc++_shared、libfbjni、libmodft2/libmodpng |
資產
assets/pid/147.9 MB:PID03.zip(探頭描述庫)、FPGA bitstream ×6、BMP/PMP/VMP/dPMP.bin(模式參數表)、Tintmap.bin(色調映射)、gsl3670.fw(觸摸屏固件)、FileCopy.bat/FileCopyAll.bat(開發殘留)assets/vosk/139.2 MB:中文vosk-model-small-cn-0.22+ 英文vosk-model-small-en-us-0.15assets/OB_table/9.6 MB:177 個產科參考表(CHINA、CHITTY、ASUM、BERTAGNOLI…)assets/user/:說明書 PDF(英/俄/中)assets/fw/board/:板卡固件 4 MBassets/BodyMark/:261 個體表標記
3. 架構
探頭(FPGA) ──USB/WiFi──> UsbProbeManager/WlanProbe ──> jnitojava ──> libdoppler 等 native
│ JavaGetMessage 輪詢
kernel.MainHandler(主線程廣播)──> Ultrasys 單例 ──> UI (Activities/Fragments)
├──> AI (PyTorch/TFLite)
└──> DICOM (native 棧)
詳細:../docs/architecture.md(含 Mermaid 圖)。
4. 安全觀察(靜態)
| # | 觀察 | 嚴重度 | 依據 |
|---|---|---|---|
| S-01 | android:allowBackup="false" ✅ |
— | Manifest 34 行 |
| S-02 | ThirdPartyManageActivity exported + 回傳第三方結果 |
低 | Manifest 50 行;僅設備廠商集成場景 |
| S-03 | 兩 Activity 聲明 sharedUserId="android.uid.system"(需系統簽名) |
信息 | Manifest 48-49 行 |
| S-04 | android.permission.INSTALL_PACKAGES 聲明(非系統簽名無法使用) |
低 | Manifest 11 行 |
| S-05 | 崩潰處理器重啟 Launcher 並 AppUtil.exitApp(1)(設備級行為,無隱蔽持久化) |
信息 | MyApplication.handleException |
| S-06 | FileCopy.bat 開發腳本殘留(無憑證/密鑰內容) |
信息 | assets/pid/ |
| S-07 | 網絡:OkHttp 標準 TLS;未見自定義 CertificatePinner(Java 層) | 信息 | okhttp3 反編譯未見 pin 邏輯 |
| S-08 | 未發現:加固殼、dex 動態加載、反射調用敏感 API、遠程 code exec 模式 | ✅ | jadx 全量掃描 |
| S-09 | USB 影像流無加密:「EncImg」= BC 影像縮寫;幀首 byte 為方向標記(負值→Reserse 反轉) |
信息 | UsbProbeManager.ReadEncImg→ReadBCImage;mImgBuf[0]<0 分支 |
| S-10 | 功能授權存於探頭 EEPROM(B/PW/Color/DICOM/人獸醫 + OEM ID 匹配);vendor request 24/81 寫授權通道 | 中(商業敏感) | HSUsbProbe EEPROM 地址表 + WriteAuthCode |
| S-11 | HSUsbProbeCh16.VerifyAuth() 直接 return true(ch16 探頭無認證) |
低 | HSUsbProbeCh16.java 全文 |
| S-12 | libHSocketIf.InputUtils 可注入系統級鼠標/鍵盤事件 |
信息(OEM 設備特性) | Java_jni_InputUtils_* 8 導出 |
| S-13 | 未見 Java/native 層明文 AES/DES/RC4 標識(strings 證據) | ✅ | libdoppler 53K strings 掃描 |
| S-14 | ThirdPartyManageActivity exported 無保護:任意 App 可啟動、注入病患 PHI 入 DB、經 SAVEIMG_PATH/IMG_PATH 外洩影像/PDF/絕對路徑 | 高 | docs/deepdive/thirdparty-sdk-interface.md |
| S-15 | HIS API:SHA-1+Base64 簽名、ts 硬編碼、TLS 憑證驗證完全關閉(中間人可攔截 HIS 憑證) |
中 | HttpProtocol_hle.java:95-123 |
| S-16 | WLAN 探頭協議無 checksum(CmdHandler.varify() 空 stub),僅靠 0xFF 終止符 |
低 | docs/deepdive/wlan-command-codes.md |
未做動態驗證(離線範圍內):上述結論均為靜態推斷,動態行為(Frida 驗證網絡/SSL、探頭協議)不在本次授權。
5. 深度分析進度(2026-09-03 更新)
三線深入已完成靜態部分:
- ✅ native 深挖(
docs/native-analysis.md):rabin2 重建 381 個 JNI 導出全表、關鍵函數地址、DICOM 棧證實、私有 UID root1.2.156.162458、hash 函數族 - ✅ USB 協議重構(
docs/usb-protocol.md):VID 0x04B4(Cypress)、EP6 bulk、FPGA 地址映射(B@0x20000/H264@0x40000/T@0x60000)、影像方向標記、EEPROM 功能授權體系 - ⏸ 動態分析(Frida):腳本已備妥(
frida/hooks.js,10 個 hook section),阻塞於無設備連接(adb devices為空)
6. 待設備就緒後(需授權設備)
frida -U -f com.SonoiQ.uhandprobe -l frida/hooks.js --no-pause驗證:USB 流捕獲、消息泵語義、VerifyAuth 行為- 硬件在環:探頭 USB 流量抓取(Wireshark usbmon / Frida)驗證協議重構
- libdoppler 算法深挖(
JavaGetMessage消息表、CPreProc)——IDA/Ghidra(本機 IDA Pro 未裝,Ghidra 可用)
附錄 A:重現命令
cd /Volumes/End-Gsme/untra-sound/sonoiq-reverse
jadx -d jadx_out --show-bad-code "…SonoiQ-2.2.15_260113 (1).apk"
apktool d -f -o apktool_out "…SonoiQ-2.2.15_260113 (1).apk"
shasum -a 256 "…SonoiQ-2.2.15_260113 (1).apk"
附錄 B:資產清單統計腳本
import re, collections
rows = []
with open('_apk_listing.txt', encoding='latin-1') as f:
for line in f:
m = re.match(r'\s*(\d+)\s+\d{2}-\d{2}-\d{4}\s+\d{2}:\d{2}\s+(.+)$', line.strip())
if m: rows.append((int(m.group(1)), m.group(2)))
c = collections.defaultdict(lambda: [0, 0])
for s, n in rows:
k = '/'.join(n.split('/')[:2]) + '/'
c[k][0] += 1; c[k][1] += s
for k, (cnt, sz) in sorted(c.items(), key=lambda x: -x[1][1]):
print(f"{k:30s} {cnt:5d} files {sz/1e6:8.1f} MB")
授權範圍
Scope — SonoiQ 2.2.15 Static Analysis
| Field | Value |
|---|---|
| Case ID | sono-iq |
| Sample | /Volumes/End-Gsme/untra-sound/SonoiQ-2.2.15_260113 (1).apk |
| SHA-256 | 867c136abf74211da7eaa511f0d5a1de022064421d7b2a5410d105f32060a2c1 |
| Package | com.SonoiQ.uhandprobe |
| auth.status | granted |
| network_profile | offline |
| Authorization | Offline static analysis of a locally owned sample. No network targets, no dynamic instrumentation against any device, no repackaging/distribution. |
| Created | 2026-09-03 (case-init.sh, reverse-skill router) |
| Router | skills/scripts/master-route.sh → PRIMARY apk-reverse/ (R1) |
Permitted actions (executed)
jadxstatic decompilation →jadx_out/apktool dunpack →apktool_out/- Manifest / assets / smali inspection
- Code wiki + markdown documentation generation
git init+ commits of analysis artifacts
Out of scope
- Any dynamic hooking (Frida) on a device
- Repackaging, signing, or installing a modified APK
- Any network interaction with the vendor or third parties
專案總覽
SonoiQ 2.2.15 — Android APK 靜態逆向分析項目
手持式超聲波診斷設備 App 的反編譯代碼庫 + Code Wiki。 樣本:SonoiQ-2.2.15_260113 (1).apk(299 MB,SHA-256867c136a…60a2c1) 授權範圍:離線靜態分析(詳見 scope.md)
SonoiQ 是什麼
SonoiQ 是手持超聲波探頭(handheld ultrasound probe)的 Android 宿主 App(package com.SonoiQ.uhandprobe)。它連接 USB 或 Wi-Fi 超聲探頭(內含 FPGA 波束形成器),在 Android 設備上實現完整的超聲成像工作流:
- 成像模式:B / M / Color Doppler / PW(脈衝多普勒)等,含 DSC(數字掃描轉換)、cine 回放
- AI 輔助診斷:膀胱體積、心臟 EF、肺部 B 線、甲狀腺、多囊卵巢(PCOS)、神經、皮膚、靜脈曲張等(PyTorch + TensorFlow Lite 推理)
- 語音控制:Vosk 中/英文離線語音識別
- 臨床工作流:患者管理、測量、報告、DICOM(Storage / Worklist / MPPS / Print)、PDF 報告、打印
- OEM 白牌:
MyApplication內有 18 個 OEM ID(ESAOTE、BD、ECCOSCAN、SONOSTAR…),同一代碼庫派生多個品牌機型 UI(LandQ6 / PortQ61 / Lianmed / ccgc / Anor 等) - 獸醫模式:
HAnimalMng/AnimalExam+def_vet資源開關
規模一覽
| 指標 | 數值 |
|---|---|
| APK 大小 | 299 MB(解壓 482 MB,2,182 檔案) |
| dex | 3 個(classes.dex 9.5 MB + classes2/3) |
| Java 檔(jadx) | 6,025(自有代碼 ~1,050,其餘第三方庫) |
| smali 檔 | 11,997(12 萬行級) |
| native libs | 28 個 .so,149.6 MB,僅 arm64-v8a |
| 最大資產 | assets/pid/PID03.zip 140.8 MB(探頭描述庫) |
Key Concepts
- Probe / PID — 探頭識別與參數庫(
libdoppler提供HPidIf/HBeParamIf/HFeParamIf,setPidFilesPath()指向assets/pid/PID03.zip) - jnitojava 消息泵 — C++ native 經
JavaGetMessage(long[])輪詢向 Java 層推送what/arg1/arg2消息(kernel.MainHandler) - Ultrasys — 中央單例,統籌 DSC、影像播放、cine、preset、MITI(機械/熱指數)
- ConnectType — 7 種探頭連接:SIMU / WLAN / WUSB / USB / UWLAN / RGBIF / BROADCAST
- ExamMode / Preset — 檢查模式與掃描參數預設體系(
assets/presetPara/、assets/SonoiQ6*/) - FPGA bitstream —
assets/pid/FPGA_ch16*.rbf(Intel/Altera RBF)在探頭連接時編程
Entry Points
| 檔案 | 角色 |
|---|---|
com.Com.handprobe.SplashActivity |
LAUNCHER 入口:登錄、DICOM 服務啟動、機型路由 |
com.Com.application.MyApplication |
Application:3 個 SQLite DB、crash handler、TaskManager、OEM 常數 |
com.Com.handprobe.BaseActivity |
主 Activity 基類(mUltrasysApp 宿主) |
kernel.MainHandler |
C→Java 消息泵(單例,主線程) |
handprobe.application.ultrasys.Ultrasys |
超聲核心單例 |
High-Level Architecture
Java UI 層(Activities/Fragments)→ Ultrasys 核心(Java)→ JNI(jni.*)→ native 引擎(libdoppler/libHSocketIf/OpenCV/PyTorch/TFLite)→ 探頭(USB/Wi-Fi,FPGA 採集)。詳見 docs/architecture.md。
Module Map
| Module | Purpose |
|---|---|
| ultrasys | 超聲核心:DSC、影像、cine、MITI |
| kernel-jni | C↔Java 消息橋 + native 接口 |
| com-ui | UI Activities / OEM 機型派生 |
| components | DICOM、檔案、顯示、輸入等組件 |
| ai | AI 輔助診斷 + 語音 |
| network-probe | 探頭連接(USB/WLAN)+ DICOM 網絡 |
| preset-measurement | 預設參數、檢查模式、測量 |
| data-assets | 資產、FPGA 固件、DB、OEM 配置 |
| native-analysis | libdoppler/libHSocketIf JNI 面、符號、strings |
| usb-protocol | USB 影像流協議、FPGA 地址映射、EEPROM 授權體系 |
| feature-auth | 功能授權體系完整解碼(hash 算法、漏洞) |
| additional-findings | 硬件身份、77 探頭型號、13 AI 模型、40+ 檢查模式 |
| deepdive/ | 7 項深挖:消息泵反匯編、WLAN 命令碼、ThirdParty SDK、DispMode、INF 格式、CC32xx 固件、Efinix bitstream |
| frida/hooks.js | Frida 動態 hook 套件(10 個 section,待設備) |
Getting Started
工具鏈重現步驟見 docs/getting-started.md。
目錄結構
sonoiq-reverse/
├── jadx_out/sources/ # jadx Java 反編譯(6,025 檔)
├── apktool_out/ # apktool 解包:smali + AndroidManifest.xml
│ ├── smali* # 12K smali 檔(3 dex)
│ └── AndroidManifest.xml
├── docs/ # Code Wiki(架構、模組、圖)
├── reports/ # 靜態分析報告
├── scope.md # 授權範圍
└── README.md
系統架構 Architecture
Architecture — SonoiQ
系統形態
SonoiQ 是典型的醫療超聲嵌入式宿主架構:超聲探頭(硬體,含 FPGA 波束形成器與前端模擬電路)通過 USB 或 Wi-Fi 連接到 Android 主機。App 分四層:
- UI 層(
com.Com.Activities +handprobe.application.gui.Fragments)— 掃描界面、測量、菜單 - 核心層(
handprobe.application.*)—Ultrasys單例統籌影像管線;preset/measurement/patient/cine/network 子系統 - JNI 橋(
jni.、kernel.)—libdoppler/libHSocketIf的 Java 接口 + C→Java 消息泵 - Native 引擎(28 個
.so)— 超聲信號處理(doppler)、影像處理(OpenCV)、AI 推理(PyTorch/TFLite)、語音(Vosk)、視頻(FFmpeg/x264)
數據入口是探頭(RF/影像流經 FPGA 採集),出口是屏幕顯示、DICOM 存儲/打印、PDF 報告與本地檔案。
Components
- Probe hardware(FPGA) — 探頭內固件由
assets/pid/FPGA_ch16*.rbf(Intel/Altera bitstream)+PID03.zip探頭描述庫定義;App 側通過HPidIf.setPidFilesPath()/InitPidSys()初始化 - UsbProbeManager / WlanProbe — 傳輸層:USB bulk 讀寫(
UsbWriteDwords/UsbReadDwords/WUsbReadImg)或 Wi-Fi 探頭(handprobe.application.wlan.*) - Ultrasys — Java 核心單例:
DscCtrl(數字掃描轉換)、FeUltrasysCtrl(前端控制)、ImagePlayer、CinePlayer、PresetServer、MITI 熱指數 - kernel.MainHandler — native 消息泵:
jnitojava.JavaGetMessage(long[4])輪詢 → 廣播給註冊的 Doppler Handler - DICOM services —
JDicomServer/Storage/Worklist/MPPS/Print(handprobe.components.dicom.*) - AI engines —
handprobe.application.ai.*(診斷算法)+handprobe.components.ai.{pytorch,tensorflow}(推理封裝) - Storage — xUtils SQLite:
UserInfo/sonoiq.db、OperatorAuth/sonoiq.db、PatientInfo/sonoiq.db+ 外部存儲檔案系統(PodFilesSys)
System Diagram
flowchart TD
Probe[["Ultrasound Probe<br>FPGA beamformer"]] -->|"USB bulk / WiFi"| Xport[UsbProbeManager / WlanProbe]
Xport -->|jnitojava| Native[["Native engines<br>libdoppler · libHSocketIf<br>OpenCV · PyTorch · TFLite<br>Vosk · FFmpeg"]]
Native -->|"JavaGetMessage(what,arg1,arg2)"| Pump[kernel.MainHandler]
Pump -->|broadcast| Core[Ultrasys singleton]
Core --> DSC[DscCtrl]
Core --> Img[ImagePlayer / DispSurface]
Core --> Cine[CinePlayer / CinePartitionBuffer]
Core --> MITI[MITIData]
Img --> UI[Activities / Fragments<br>com.Com.handprobe · gui.*]
UI --> Dicom[DICOM Server/Storage/Worklist/MPPS/Print]
Dicom --> Net{{"PACS / Printer<br>(external)"}}
UI --> AI[ai.* diagnosis]
AI --> Native
UI --> DB[("SQLite ×3<br>xUtils")]
Core --> Preset[PresetServer / ExamMode]
Preset --> Assets[["assets/<br>PID03.zip · presetPara · BodyMark"]]
Data Flow
- 採集 — 探頭 FPGA 採集 RF/影像,App 經
UsbProbeManager.Read()/ReadEncImg()拉取(jnitojava.WUsbReadImg) - 處理 — native 完成波束形成/掃描轉換/多普勒處理;結果經
JavaGetMessage上拋 - 顯示 —
Ultrasys分發 →ImagePlayer/DispSurface(OpenGL,HGLImageJNIViewLib)渲染到 UI - AI — 用戶觸發測量 →
ai.*調用 PyTorch/TFLite 模型 → 結果疊加顯示 - 存儲/輸出 — 圖像/報告寫本地(
PodFilesSys)、經 DICOM 上傳 PACS、打印或輸出 PDF
Key Design Decisions
- 單一 arm64-v8a ABI — 現代設備專用,壓縮體積(探頭 App 一般跑在專用平板)
- 探頭即數據庫 — 探頭參數(元素數、半徑、頻率)由
PID03.zip描述,native 側查詢(GetProbeElementNumber()等),App 換探頭即可切換參數 - 7 種連接類型共存 —
ConnectType(SIMU/WLAN/WUSB/USB/UWLAN/RGBIF/BROADCAST)由HSysctrlIf.SetProbeConnType()同步到 native - OEM 由常數+資源派生 — 同一 dex 經
MyApplication.OEM_*_ID與def_vet等資源開關切換品牌/人獸醫模式,無需分叉代碼 - C→Java 採用輪詢而非直接 JNIEnv 回調 —
JavaGetMessage阻塞輪詢 + 主線程 Handler 廣播,避免 JNI 跨線程回調複雜性 - 資源全面混淆 —
res/檔名短隨機化(如xY.png、9_.jpg),但assets/保持明文目錄結構
模組 — ultrasys 超聲核心
Module: ultrasys — 超聲核心
handprobe.application.ultrasys(+ 少量 handprobe.components.ultrasys)是整個 App 的影像核心。Ultrasys 是中央單例,上游接 native 引擎(經 jni/jnitojava),下游餵 UI 層。
Responsibilities
- 統籌成像管線:B / M / Color / PW 模式切換與參數分發
- DSC(數字掃描轉換):
algm.DscCtrl把探頭極坐標數據轉為屏幕直角坐標 - 影像顯示與回放:
ImagePlayer/TwoImagePlayer/CinePlayer/MBCinePlayer - MITI(機械指數 MI / 熱指數 TI):
MITIData+MITIObservable實時計算與顯示 - 前端控制:
algm.FeUltrasysCtrl(頻率/深度/焦點) - 探頭模式管理:
probe.ExamMode.HExamType、probe.ConnectType
Key Files
Ultrasys.java— 中央單例(Ultrasys.Instance()),持有 DscCtrl / FeUltrasysCtrl / ImagePlayer / CinePlayer / PresetServer / MITIData 引用ParaCtrl.java— 參數控制(從 UI 參數變化同步到 native)HFuncId.java— 功能 ID 常量(按鍵/菜單/功能碼)algm/DscCtrl.java— 掃描轉換控制(SetBFrameCorrCoef/SetCFrameCorrCoef)image/—DispMode、DispSurface、ImageDefine、ImagePlayer(Base)、TwoImagePlayerparameter/—ExamTypesId、FuncidMap、InspectionObjectTypeprobe/—ConnectType(7 種連接)、ExamMode/、podsys/(探頭系統)、PidInterfacecine/— cine 錄製
Public API(Java 側關鍵入口)
Ultrasys.Instance()— 取單例Ultrasys.setMITIData(float[])— native 回調更新熱指數(由jnitojava.UpdateMITI調用)Ultrasys.GetPattern(int, int)— 取模式數據Ultrasys.GetProbeConType()— 探頭連接類型ConnectType.SetConnType(int)— 切換連接類型並同步HSysctrlIf.SetProbeConnType()
Internal Structure
algm/(algorithm)— 與 native 配對的算法控制:DSC、前端image/— 顯示鏈:player → surface → 模式定義parameter/— 檢查類型/功能碼映射probe/— 探頭體系:連接、模式、pod 系統(poddef 定義)cine/— cine 環形緩衝與錄製
Dependencies
- Used by:
com.Com.handprobe.BaseActivity(宿主)、gui.fragment.ImageDisplayFragment、ai.* - Uses:
jni.(HBeParamIf/HFeParamIf/HPidIf/HDscClassIf/HSysctrlIf/HPresetIf)、kernel.、handprobe.application.usb.、handprobe.application.wlan.、components.ultrasys.*(FrameLineData、CinePartitionBuffer、B/C/M/PW 掃描參數類)
Notable Patterns / Gotchas
Ultrasys大量字段是 public 的直接暴露(mDscCtrl等),跨包直接操作——無嚴格封裝- 掃描參數按模式分族:
BScanPara/CScanPara/MScanPara/PWScanPara+ 對應*DspPara FrameLineData.SimDataBuffer表明有模擬數據路徑(SIMU 連接模式,用於無探頭演示/培訓)- MITI 數據由 native 側計算後上拋(
UpdateMITI(float[])),Java 只負責顯示
模組 — kernel + jni C↔Java 橋
Module: kernel + jni — C↔Java 橋
kernel(6 檔)與 jni(14 檔)是 Java 層與 native 引擎(libdoppler、libHSocketIf、libh264rtp、libhopencvif_lib、libfpgargb_lib)的接口。
Responsibilities
- C→Java 消息推送:native 產生的
what/arg1/arg2消息泵入主線程 - Java→C 調用:探頭參數查詢、系統控制、DICOM、DSC、預設、影像
- 串口/USB Socket 訪問(
libHSocketIf) - FPGA RGB 輸出(
libfpgargb_lib)、H.264 視頻解碼(libh264rtp)、OpenCV 封裝(libhopencvif_lib)
Key Files
kernel/MainHandler.java— 主線程單例 Handler:起一個輪詢線程不停調jnitojava.JavaGetMessage(long[4]),有消息就廣播給mDopplerMsgHandlerVector所有註冊 Handlerkernel/DopplerHandler.java— Doppler 專用 Handler 封裝kernel/HObserver.java/HObservable.java— 觀察者模式基類(UI 訂閱影像/參數事件)kernel/FocusResMng.java、kernel/WinRect.java— 焦點/窗口矩形輔助handprobe/application/usb/jnitojava.java— native 回調 Java 的固定簽名入口(JavaGetMessage、SetJniEvn、UsbWriteDwords、UsbReadDwords、WUsbReadImg、UpdateMITI、GetPattern、CineDataCapture…)
Native 庫加載映射(System.loadLibrary 實測)
| Java 類 | 庫 |
|---|---|
HPidIf HBeParamIf HFeParamIf HDicomIf HDscClassIf HPresetIf HSysctrlIf |
libdoppler(核心) |
SerialPort MeasFormulaParser InputUtils HGLImageJNIViewLib |
libHSocketIf(USB/串口/GL) |
HH264PlayerIf |
libh264rtp |
HOpencvIf |
libhopencvif_lib |
fpgargb_lib |
libfpgargb_lib |
IDReader.DecodeWlt |
DecodeWlt(中國身份證讀卡) |
Public API 精選
HPidIf— 探頭查詢:GetProbeId(String)、GetProbeName(int)、GetProbeElementNumber()、GetProbeRadius()、GetProbeType(int)、InitPidSys()、setPidFilesPath(String)HSysctrlIf— 系統控制:SetProbeConnType(int)等(190+ 行,最大接口之一)HGLImageJNIViewLib— OpenGL 影像視圖SerialPort— 串口讀寫MeasFormulaParser— 測量公式解析(native)
Internal Structure
兩條消息路徑:
native → jnitojava.JavaGetMessage (輪詢) → kernel.MainHandler (主線程廣播) → 註冊 Handler
Java → jni.* static native → libdoppler / libHSocketIf
Dependencies
- Used by:
handprobe.application.(幾乎所有核心包)、com.Com.handprobe. - Uses: Android
System.loadLibrary、libc++_shared(native 依賴)
Notable Patterns / Gotchas
- 輪詢而非回調:
JavaGetMessage在後台線程忙輪詢(Tools.Sleep(20)),避免 JNIEnv 跨線程 attach 複雜度 - 消息為固定 4×long 佈局(
[0]=what, [1]=arg1, [2]=arg2),擴展性有限 - 多個 JNI 類載入同一個
libdoppler——native 側按類名分派(靜態註冊) jnitojava的方法名固定為 native 側硬編碼的回調點(改 Java 方法名會斷鏈)
模組 — com.Com UI 層 / OEM 機型
Module: com.Com — UI 層 / OEM 機型
com.Com(67 檔)是 Android UI 層:入口 Activity、主 Activity 基類、以及按 OEM 機型派生的整套界面。
Responsibilities
- App 啟動與登錄(
SplashActivity) - 主掃描界面骨架(
BaseActivity) - OEM 機型界面派生(Land / Port / Lianmed / ccgc / Anor)
- 第三方 App 集成入口(
ThirdPartyManageActivity,exported) - 崩潰處理與桌面返回
Key Files
application/MyApplication.java— Application:初始化 xUtils、3 個 SQLite(UserInfo / OperatorAuth / PatientInfo)、crash handler(記錄後重啟 Launcher)、TaskManager後台線程、時間/日期系統設置handprobe/SplashActivity.java— LAUNCHER:用戶登錄(spinner 選用戶 + 密碼 + 自動登錄 + 緊急按鈕)、DICOM 服務啟動、機型路由handprobe/BaseActivity.java— 主 Activity 基類:持有mUltrasysApp,消息處理(AMSG_CONNECT_CHANGE等)handprobe/LandFeature/LandActivity.java— 橫屏機型handprobe/LandQ6Feature/— Q6 橫屏系列:LandQ6Activity、LandQ6MidActivity、LandQ62Activity、LandQ6BMVActivity、Q62SonoStarActivity(SonoStar OEM)、SSKeydataHandle、HKPKeyboardAPP(硬體鍵盤)handprobe/PortQ61Feature/PortQ61Activity.java— 便攜 Q61handprobe/LianmedFeature/LianmedActivity.java— Lianmed OEMhandprobe/ccgc/LandCCGCActivity.java— ccgc OEMhandprobe/AnorActivity.java+UI/AnorTopFuncFrag.java/AnorBottomFuncFrag.java— Anor 機型handprobe/PortActivity.java— 便攜基礎版handprobe/ThirdPartyManageActivity.java— 第三方 App 調起接口(授權結果回傳)handprobe/FsActivity.java、CrashActivity.java、MyInstalledReceiver.java— 輔助
OEM 白牌機制
MyApplication 定義 18 個 OEM ID 常量(實測源碼):
OEM_YOUKEY_ID=1 OEM_ESAOTE_ID=2 OEM_INTEGRA_ID=3 OEM_BMV_ID=4
OEM_VIS_ID=6 OEM_D8NOTEBOOTK_ID=7 OEM_SMART_ID=8 OEM_ECCOSCAN_ID=9
OEM_CCOSON_ID=10 OEM_IVET_ID=11 OEM_HBKQ_ID=12 OEM_ECHONECT_ID=13
OEM_FASVET_ID=14 OEM_DRAMINSKIVET_ID=15 OEM_SONOSTAR_ID=16 OEM_OPU_ID=17
OEM_MADINA_ID=18 OEM_BD_ID=19
機型行為差異由資源開關控制,例如獸醫模式:R.string.def_vet 為 "on" 時 GetPatientMng() 返回 HAnimalMng、getExamByID() 查 AnimalExam。
Dependencies
- Used by: manifest(
SplashActivity為 LAUNCHER;BaseActivity/LandActivity/… 全部exported=false除SplashActivity與ThirdPartyManageActivity) - Uses:
handprobe.application.、handprobe.components.、kernel.、jni.、UI.*
Notable Patterns / Gotchas
- 全部主界面
screenOrientation="landscape"且configChanges全開(旋轉不重建) ThirdPartyManageActivity是唯一顯式 exported 的 Activity(Theme.Translucent),支持第三方 App 嵌入超聲功能——MyApplication.returnThirdParty()回傳結果LandQ62Activity/LandQ6Activity帶sharedUserId="android.uid.system"(需系統簽名安裝——典型 OEM 預裝設備)- Crash handler 會
finish()所有 Activity 並啟動com.android.launcher3,醫療設備場景下防止卡死在崩潰畫面
模組 — components 可重用組件
Module: components — 可重用組件
handprobe.components(319 檔)是與超聲成像解耦的可重用子系統:DICOM、檔案、顯示、輸入、串口、音頻、網絡等。
Responsibilities
按子包:
| 子包 | 職責 |
|---|---|
dicom/ |
DICOM 服務全家桶:Server、Storage、Worklist、MPPS、Print、Local + JDicom* JNI 包裝 + UnitConverter |
file/ |
檔案系統抽象:PodFilesSys(探頭數據目錄)、AssetsFile、檔案選擇器 fileselect.FileManage |
display/ |
第二顯示器支持(SecondDislay) |
input/ |
鍵盤/軌跡球/觸摸輸入 |
serialport/ |
串口設備(掃描槍/身份證等外設) |
usb/ |
USB 設備:HS5pRGBIf(RGB 輸出機型) |
audio/ |
音頻:AudioPlayer + voice/vosk/VoskSpeechRecognizer(語音輸入) |
opencv/ |
OpenCV Java 封裝 |
network/ |
網絡工具 |
wifi/ |
Wi-Fi 管理 |
BroadCast/ |
廣播接收 |
PatientInfo/ UserInfo/ |
患者/用戶數據模型與 DB(Exam、AnimalExam、User、LoginInfo) |
task/ |
TaskManager 後台任務線程 |
permission/ |
權限申請封裝 |
ultrasys/ |
成像共享部件:FrameLineData(幀線數據 + FileManager)、cine/(CinePartitionBuffer、CinePlayer、MBCinePlayer、ICineIterator、MCineRollBuffer)、parameter/(BScanPara/CScanPara/MScanPara/PWScanPara/MITIData…) |
utils/ |
AppUtil、FileOperate、LanguageUtil、LogUtil、QlogUtil、ScreenUtil、ToastUtil |
widget/ |
自定義控件(HEditText、HArrayAdapter…) |
Key Files
dicom/JDicomServer.java— DICOM 服務 JNI 入口(nativelibdoppler內建 DICOM 棧)file/PodFilesSys.java— 探頭數據目錄管理(App 外置存儲佈局)file/fileselect/fileselect/FileManage.java— manifest 註冊的檔案管理 ActivityPatientInfo/db/Exam.java、AnimalExam.java— xUtils ORM 患者檢查表ultrasys/cine/MCineRollBuffer.java— cine 環形緩衝(錄製回放)ultrasys/parameter/MITIData.java— MI/TI 熱指數數據 + Observable
Dependencies
- Used by:
handprobe.application.、com.Com. - Uses:
org.xutils(ORM)、jni.(JDicom)、org.opencv、Vosk(org.vosk)
Notable Patterns / Gotchas
- DICOM 棧實際在 native(
JDicom*全是 native 方法),Java 側只是參數對象(DicomPresetPara、DicomMPPSServicePara)與生命周期管理 - 患者數據庫同時支持人(
Exam)與動物(AnimalExam)兩套表,獸醫模式在查詢層切換 MCineRollBuffer是內存敏感的環形緩衝——掃描過程全幀緩存於 RAM(largeHeap="true"為此開)- 語音識別走
org.vosk(離線),模型在assets/vosk/(中/英 small 模型,~139 MB)
模組 — AI 輔助診斷與語音
Module: ai — AI 輔助診斷與語音
handprobe.application.ai(算法/診斷)+ handprobe.components.ai(推理引擎封裝)實現了完整的 AI 輔助診斷功能集。
Responsibilities
- 診斷測量:單次觸發 → 模型推理 → 輪廓/數值疊加顯示
- 推理引擎封裝:PyTorch(
libpytorch_jni)與 TensorFlow Lite(libtensorflowlite_jni+ GPU delegate) - 離線語音控制:Vosk 中/英文識別,語音指令操作掃描界面
診斷功能清單(application/ai 實測)
| 類 | 功能 |
|---|---|
BladderMeas |
膀胱體積測量 |
CardiacAutoEF |
心臟射血分數(EF)自動計算 |
LungBLineDetector |
肺部 B 線檢測(新冠肺炎/肺水腫場景) |
ObHcMeas |
產科頭圍(HC)測量 |
PcosDiagnose |
多囊卵巢綜合徵診斷 |
PwAutoPositionCarotid(0) |
頸動脈 PW 自動取樣門定位 |
ThyroidDiagnose |
甲狀腺診斷 |
NerveSegment |
神經分割(麻醉導引) |
SkinSegment |
皮膚層分割 |
VaricosityMeas |
靜脈曲張測量 |
AssistDiagnose |
通用輔助診斷 |
AiFuncMng / AiMeasBase |
AI 功能管理 / 測量基類 |
UltraSpeechRecognizer |
語音識別入口(包裝 Vosk) |
Key Files
application/ai/AiFuncMng.java— AI 功能註冊/調度application/ai/UltraSpeechRecognizer.java— 語音指令識別(+components/audio/voice/vosk/VoskSpeechRecognizer.java實際 Vosk 調用)components/ai/AiModelBase.java— 模型基類(加載/推理生命周期)components/ai/MaskProcessor.java+Mask3Diam/MaskInnerDiameter/MaskUtils/LineSegment— 分割掩膜後處理(直徑/輪廓擬合)components/ai/pytorch/、components/ai/tensorflow/— 引擎適配components/ai/ImageUtils.java/AiData.java— 影像預處理
推理鏈
UI 觸發 → AiFuncMng → AiMeasBase 子類(ROI 選擇、預處理)
→ MaskProcessor/ImageUtils → pytorch / tensorflow 引擎
→ libpytorch_jni / libtensorflowlite_jni
→ Mask 後處理(擬合直徑/面積)→ 數值疊加顯示
Dependencies
- Used by:
gui.fragment.ImageDisplayFragment(影像界面集成) - Uses:
org.pytorch、org.tensorflow、org.vosk、ultrasys(取當前幀)
Notable Patterns / Gotchas
- 模型體積:
libpytorch_jni.so達 76 MB(打包了 PyTorch 運行時) - TFLite 帶 GPU delegate(
libtensorflowlite_gpu_jni.so),OpenCL 加速 - Vosk 模型在
assets/vosk/:中文vosk-model-small-cn-0.22、英文vosk-model-small-en-us-0.15(含 graph/HCLr/Gr.fst、am/final.mdl、ivector) - AI 測量在 UI 線程外異步跑,結果以 Observer 回調刷新
模組 — 探頭連接與網絡
Module: network-probe — 探頭連接與網絡
探頭連接(USB / Wi-Fi)與 DICOM 網絡是 SonoiQ 的兩條外部通路。
探頭連接(handprobe.application.usb + wlan)
7 種連接類型(ultrasys.probe.ConnectType):
| 常量 | 值 | 含義 |
|---|---|---|
NONE_CONNECT_TYPE |
0 | 未連接 |
SIMU_CONNECT_TYPE |
1 | 模擬(無探頭演示) |
WLAN_CONNECT_TYPE |
2 | Wi-Fi 探頭 |
WUSB_CONNECT_TYPE |
3 | 無線 USB |
USB_CONNECT_TYPE |
4 | USB 有線 |
UWLAN_CONNECT_TYPE |
5 | 另一種無線模式 |
RGBIF_CONNECT_TYPE |
6 | RGB 接口機(HS5pRGBIf) |
BROADCAST_CONNECT_TYPE |
7 | 廣播 |
切換時 ConnectType.SetConnType() 同步 HSysctrlIf.SetProbeConnType(int) 並刷新 UI 搜索按鈕狀態、發送 AMSG_CONNECT_CHANGE。
Key Files
usb/UsbProbeManager.java— USB bulk 傳輸單例(Write/Read/ReadEncImg/GetImgPack)usb/jnitojava.java— native 回調入口(見 kernel-jni 模組)wlan/protocol/— 探頭通信協議(Tools、JsonParam)wlan/wlanprobe/WlanProbe.java+ProbeWifiInfo.java— Wi-Fi 探頭發現/連接wlan/wifi/— Wi-Fi 管理
DICOM 網絡(handprobe.components.dicom)
全部 DICOM 邏輯在 native libdoppler 內(JDicom* 類為 JNI 包裝):
JDicomServer— 本地 DICOM 服務JDicomStorageService— 存儲(SCU 上傳 PACS)JDicomWorklistService— 工作列表(Modality Worklist)JDicomMPPSService— 檢查狀態回報(MPPS)JDicomPrintService— DICOM 打印JDicomLocal/JDicomService— 本地/服務基類- 參數:
handprobe.application.preset.DicomPresetPara、DicomMPPSServicePara
其他網絡
application/network/— 通用網絡(激活/升級類)components/network/— 網絡工具- Manifest 權限:INTERNET、CHANGE_WIFI_STATE、ACCESS_WIFI_STATE、CHANGE_NETWORK_STATE(無 BLUETOOTH 實際使用——聲明遺留)
Dependencies
- Used by:
Ultrasys(連接狀態)、SplashActivity(啟動 DICOM 服務)、BaseActivity(offWifi) - Uses:
jni.HSysctrlIf、kernel.*
Notable Patterns / Gotchas
MyApplication.onActivityDestroyed會在 BaseActivity 銷毀時mMainActivity.offWifi()——離開主界面即斷 Wi-Fi 探頭ThirdPartyManageActivity回傳時ProbeWifiInfo.DisconnectWifi()強制斷開,防止第三方 App 期間佔用探頭ReadEncImg表明 USB 影像流是加密/編碼傳輸(WUsbReadImg),細節在 native 側
模組 — 預設與測量
Module: preset-measurement — 預設與測量
掃描參數預設(preset)與臨床測量(measurement)是超聲設備的兩大業務核心。
Preset(handprobe.application.preset)
PresetServer.java— 預設服務(加載/保存/分發)PresetData.java/PresetParams.java— 預設數據模型;MITIObservable熱指數觀察PresetFile.java/PresetFileManipulation.java/SystemPresetFile.java— 預設檔案持久化ExamMode/+ExamModeAliases.java— 檢查模式(腹部/心臟/產科/血管…)與別名ExamToPid.java— 檢查模式 → 探頭 PID 映射ExamImg/ExamImgParaPerMode.java— 每模式影像參數(含ProbeParameter嵌套)DicomPresetPara.java/JDicomPresetPara.java/DicomMPPSServicePara.java/NetworkPresetData.java— DICOM/網絡配置DisableProbeBtnPreset.java— OEM 禁用按鈕配置
預設資產:assets/presetPara/(16 檔)、assets/SonoiQ6/、assets/SonoiQ6Vet/、assets/SonoiQVet/、assets/SonoiQ/(型號默認預設)。
Measurement(handprobe.application.measurement)
MeasManager.java— 測量總管MeasureLib.java/VetMeasureLib.java— 人/獸醫測量庫(MeasurePackage/、VetMeasurePackage/)MeasurePreset.java/MeasOBCalcPreset.java— 測量預設MeasAutoCalc.java/MeasVolCalc.java— 自動計算/體積計算MeasResultMng.java— 結果管理(報表輸出)ExpressionParser.java— 測量公式解析(Java 側;native 版見jni.MeasFormulaParser)MeasureView/、measurebase/、measMenu/— 測量 UIMEASID.java— 測量 ID 常量obInfo/— 產科信息:OBInfoMng、OBTableUtil、TestOBTable(GA 孕周/生長曲線查表)PWAutoCalc/— PW 多普勒自動計算
產科表資產
assets/OB_table/ 177 檔(9.6 MB),按標準命名:CHINA、CHITTY(DERIVED)、CHITTY(O-O)、CFEF、BERTAGNOLI、ASUM(澳洲超聲醫學會)等——各國/各學會胎兒生物測量參考表。
Dependencies
- Used by:
Ultrasys(MITI、PresetServer)、gui.*(測量 UI)、報告系統 - Uses:
jni.HPresetIf/HPidIf(native 側預設/探頭查詢)、xutils(JSON 序列化)
Notable Patterns / Gotchas
- 預設檔案在 native 與 Java 兩側都有概念:Java 管持久化/UI,native(
HPresetIf)管實際掃描參數生效 VetMeasureLib與MeasureLib平行——獸醫測量套件獨立維護- 產科孕周計算依賴
OB_table參考表,跨地區合規需正確選表
模組 — 資產、固件與數據庫
Module: data-assets — 資產、固件與數據庫
APK 解壓 482 MB 中,assets/(298 MB)承載了探頭描述庫、FPGA bitstream、AI/語音模型與臨床參考表。
資產目錄實測統計
| 路徑 | 大小 | 內容 |
|---|---|---|
assets/pid/ |
147.9 MB | 探頭描述庫與 FPGA 固件(見下) |
assets/vosk/ |
139.2 MB | Vosk 中/英文離線語音模型 |
assets/OB_table/ |
9.6 MB | 177 個產科參考表 |
assets/user/ |
7.1 MB | 說明書 PDF(English / Russian / Chinese) |
assets/fw/board/ |
4.0 MB | 板卡固件 |
assets/SonoiQ6*/SonoiQVet/SonoiQ/ |
~1.3 MB | 各型號默認預設配置 |
assets/presetPara/ |
0.4 MB | 預設參數 |
assets/DigTplXmlEs/En/Ch |
0.8 MB | 報告模板 XML(西/英/中) |
assets/BodyMark/ |
0.1 MB | 261 個體表標記 PNG(腹部/心臟/產科/血管…) |
assets/CommentMenu/ |
0.1 MB | 註釋菜單 |
assets/dicom/ |
0.1 MB | DICOM 配置 |
assets/reports/ |
— | 報告模板 |
assets/pid/ — 探頭固件核心
PID03.zip 140.8 MB 探頭描述庫(HPidIf.setPidFilesPath 目標)
FPGA_ch16.rbf 718 KB Intel/Altera FPGA bitstream v1
FPGA_ch16_2/3/4/6.rbf 718 KB×4 同族 bitstream
FPGA_ch16_5.rbf 3.5 MB (最大版本)
BMP.bin / PMP.bin / VMP.bin / dPMP.bin B/M/PW 模式參數表
Tintmap.bin 色調映射表
gsl3670.fw 觸摸屏控制器固件(Silead GSL3670)
GlobalVersion.txt 全局版本號
FileCopy.bat / FileCopyAll.bat (Windows 工具殘留——開發者拷貝腳本)
Native 庫(lib/arm64-v8a,149.6 MB)
| 庫 | 大小 | 用途 |
|---|---|---|
libpytorch_jni.so |
76.1 MB | PyTorch 推理 |
libopencv_java4.so |
19.9 MB | OpenCV |
libdoppler.so |
15.4 MB | 超聲核心(PID/參數/DICOM/DSC/系統控制) |
libvosk.so |
8.9 MB | 語音識別 |
libavcodec-56.so 等 FFmpeg 全家 |
~12 MB | 視頻編解碼(cine 導出) |
libmodpdfium.so / libjniPdfium.so |
6.1 MB | PDF 渲染(報告) |
libtensorflowlite_jni.so + GPU |
7.3 MB | TFLite |
libh264rtp.so |
108 KB | H.264 RTP |
libhopencvif_lib.so |
3.2 MB | OpenCV JNI 封裝 |
libfpgargb_lib.so |
14 KB | FPGA RGB 輸出 |
libHSocketIf.so |
14 KB | USB/串口 Socket |
libjnidispatch.so |
158 KB | JNA |
數據庫(運行時創建,非 APK 內)
| DB | 位置 | 用途 |
|---|---|---|
sonoiq.db |
UserInfo/database/ |
用戶與登錄 |
sonoiq.db |
OperatorAuth/database/ |
操作者授權 |
sonoiq.db |
外部存儲(FileStorageMng) |
患者/檢查記錄(xUtils ORM) |
Notable Patterns / Gotchas
PID03.zip佔 APK 近半——單一探頭描述庫壓縮包(zip 內未壓縮的位圖/參數表)FileCopy.bat/FileCopyAll.bat隨 assets 打包是明顯的開發者疏忽(含拷貝命令,可用於推測出廠燒錄流程)- FPGA bitstream 同族多版本(
_2/_3/_4/_5/_6)暗示現場可切換 FPGA 配置 - 資源目錄
res/全面混淆(短隨機檔名),assets/未混淆——敏感內容(PID/固件/模板)都在 assets
USB 探頭協議
USB 探頭協議(靜態重構)
來源:UsbProbeManager、HUsbProbeInterface、HSUsbProbe(jadx)+libdoppler/libHSocketIf/libh264rtp導出符號。純靜態推斷,未經硬件驗證。
1. USB 設備層
| 項 | 值 | 證據 |
|---|---|---|
| VID | 0x04B4(1204) |
HUsbProbeInterface.PROBE_VID |
| PID | 0x1004(4100) |
HUsbProbeInterface.PROBE_PID |
| VID 含義 | Cypress Semiconductor | 0x04B4 為 Cypress 註冊 VID → FX2/FX3 系 USB 控制器 |
| 控制傳輸 | EP0,setup 8 B / buffer 64 B | USB_EP0_BUF_LEN=64、USB_SETUPDATA_LEN=8 |
| 數據傳輸 | EP6 bulk,FIFO 2,048 B | USB_EP6_FIFO_SIZE=2048(Cypress FX3 特徵) |
| 設備枚舉 | 動態掃描(不硬編碼 VID/PID 過濾),分三路 | ScanHUsbProbe/ScanHSUsbProbe/ScanHRProbe |
探頭驅動類譜系(handprobe.components.usb):
HUsbProbe (基類)
├── HSUsbProbe ── HSUsbProbeCh16 (FPGA ch16 系列,mProbeType=4)
├── HRUsbProbe (R 系列)
├── HRTLUsbProbe (Realtek 方案)
└── HCyRTLUsbProbe (Cypress/Realtek 組合?)
2. FPGA 地址空間(影像流讀寫直接定址)
| 常量 | 值 | 用途 |
|---|---|---|
FPGA_BIMG_ADDR |
131,072(0x20000) | B 模式影像 |
FPGA_CIMG_ADDR |
131,072 | C(Color)影像 |
FPGA_H264_ADDR |
262,144(0x40000) | H.264 編碼流 |
FPGA_TIMG_ADDR |
393,216(0x60000) | T/M 模式影像 |
影像幀尺寸:B/C 各 131,072 B(128 KB);M 8,192 B;PW 2,048 B;T 10,240 B。
RF 採集模式(mRFGather)讀 2,097,152 B(2 MB 全緩衝 mImgBuf)。
3. 讀取循環(USB_ImgReadThread,priority=10)
loop:
CheckImgUpload() → bitmask
bit0 (1): B 影像就緒
bit1 (2): M/PW 就緒
bit2 (4): C 影像就緒
ConType 3 (WUSB):
ReadBCImage → HH264PlayerIf.WUsbSignalBCImg (H.264 流解碼路徑)
ConType 4 (USB):
probeType==6: GetImgPack → HH264PlayerIf.RUsbSignalImgReady
else:
Read(0x20000, mImgBuf, RF?2MB:128KB)
mImgBuf[0] < 0 → HDscClassIf.Reserse(buf, 131072) ← 方向標記!
(SIMU 模式: simdat++ 填 ramp 假數據)
DispMode 分支:
5 → Read(0x60000, mTImgBuf, 2048) → PW 隊列 → HH264PlayerIf.PackD
9 → Read(0x60000, mMImgBufCache, 10240) → 4×2048 分包 → HH264PlayerIf.PackM
8/55 → Read(0x20000, mCImgBuf, 131072) → 拼入 mImgBuf[131072..262144]
「EncImg」真相
jnitojava.WUsbReadImg → UsbProbeManager.ReadEncImg → mUsbInterface.ReadBCImage()。
「Enc」= BC(B+C 合成)影像的縮寫,不是加密。安全結論:USB 影像流為明文位元組流;幀首 byte 最高位是影像方向(正/反)標記(負值 → HDscClassIf.Reserse 整幀反轉),無加密層。
4. 寫入路徑(FPGA 寄存器/參數)
Write(addr, data, len)→ 隊列UWlanWriteQueueElem{addr,data,len}→ 寫線程:- USB(ConType 4):直接
mUsbInterface.Write - UWLAN(ConType 5):
WlanProbe.mICmdHandler.UWriteFPGA(addr,data,len);TCP_PARA時走UWriteFPGA_TCP(TCP 通道傳 FPGA 寄存器寫) - WLAN 讀取:
UReadFPGA(addr, out, len)/UReadFPGA_TCP,1.5 s 信號量超時
5. 探頭授權體系(EEPROM 功能位)
HSUsbProbe 定義探頭 EEPROM 授權地址表:
| EEPROM 地址 | 內容 |
|---|---|
| 32,764 | 探頭序列號(EEPROM_SN_ADDR) |
| 28,160 | 探頭 SN(64 B 讀取) |
| 28,192 | 板卡 SN |
| 28,312 | OEM ID(App 側 CheckOEMId(GetProbeOemId()) 強制匹配) |
| 28,224 | DICOM 功能授權 |
| 28,240 | PW 多普勒授權 |
| 28,256 | Color 授權 |
| 28,288 | B 模式授權 |
| 28,308 | 人/獸醫模式授權 |
VerifyAuth():讀 EEPROM SN + 探頭 SN(首 byte 合法性檢查!=-1 && !=204)→ 拼接SN-XXXX標識 → 授權WriteAuthCode()/ vendor requestVR_USB_SET_AUTH=24→ 寫回授權(廠商售後解鎖通道)HSUsbProbeCh16.VerifyAuth()直接return true(ch16 探頭無 SN 認證——可能 ch16 系無 EEPROM 或授權移至 App 側)UpdateDicomAuth():DICOM 授權經 USB 通道更新
商業模式解讀
功能授權(B/C/PW/DICOM/人獸醫)儲存於探頭硬件 EEPROM,而非 App:換 App/設備不能解鎖高階功能;廠商經 vendor request 寫碼解鎖。OEM ID 鎖死探頭與白牌 App 的匹配(探頭不能跨品牌用)。
6. Wi-Fi 探頭協議(handprobe.application.wlan)
- 命令框架:
CmdParser/CmdInfo/BaseCmdHandler→CmdHandler/WlanCmdHandler - 傳輸:
WlanHost/WlanClient/WlanProbe(多主機GetMultiHost()) - 序列化:
protocol.Tools(Hex/ASCII 轉換、StrToHex_r、IntToByte_o——大小端雙向) - 雙通道:UDP 命令 + 可選
TCP_PARA(TCP 參數通道) JsonParam標籤:Tag_probe_id等 JSON 配置項
7. 未解(需硬件/動態)
CheckImgUpload的具體實現(寄存器 or EP 中斷)- H.264 流格式(
WUsbSignalBCImg輸入的碼流封裝) - vendor request 完整碼表(除 24/81 外)
- WLAN 命令碼錶(
CmdInfo枚舉值)
Native 分析
Native Analysis — libdoppler / libHSocketIf(靜態)
工具:rabin2 6.2.0 + strings(macOS)。樣本為 stripped aarch64 ELF,但 JNI 導出符號完整保留,無需 IDA 即可重建完整 JNI 面。
libdoppler.so(15.4 MB,超聲核心)
- ELF aarch64,dynamically linked,stripped(BuildID
157510dc…43b0) - 導出符號 35,618 個,其中 381 個 JNI 導出(
Java_*),按類分佈:
| JNI 類 | 導出數 | 職責 |
|---|---|---|
jni.HSysctrlIf |
91 | 掃描系統控制(增益/焦點/TGC/頻率/功率) |
jni.HBeParamIf |
77 | 波束形成參數(B 模式) |
jni.HDscClassIf |
69 | 掃描轉換/影像處理 |
jni.HFeParamIf |
54 | 前端參數 |
jni.HPidIf |
34 | 探頭描述(PID) |
jni.HDicomIf |
33 | DICOM 棧 |
jni.HPresetIf |
8 | 預設 |
jnitojava |
2 | JavaGetMessage(@0x0061dd28, 244B)+ SetJniEvn |
jni.MeasFormulaParser |
1 | 測量公式(tonibolan) |
關鍵符號(地址實測)
Java_handprobe_application_usb_jnitojava_JavaGetMessage 0x0061dd28 244 B
Java_jni_HDscClassIf_ImgBufToDispBuf 0x0062464c 804 B
Java_jni_HDscClassIf_CPreProc 0x006229d8 1536 B
Java_jni_HDscClassIf_PwPostProcessAndSaveCine 0x0062342c 592 B
Java_jni_HDscClassIf_SearchPwMaxOfArea 0x006262c8 332 B
Java_jni_HDscClassIf_hash_1JSHash_1forHuman 0x006251dc 136 B
Java_jni_HDicomIf_GenPatientID 0x006616a0 228 B
Java_jni_HDicomIf_StorageServiceCommit 0x00660774 116 B
Java_jni_MeasFormulaParser_tonibolan 0x007be2d4 496 B
Strings 證據
- DICOM 棧完整內嵌:數百個 DICOM tag 名(
AcquisitionContextDescription、CurveDataDescriptor…)+ 標準 UID(1.2.840.10008.*) - 私有 UID root:
1.2.156.162458.1.1.0.1(1.2.156.162458為廠商私有註冊) - 字符串 hash 全家桶:
JSHash/FNVHash/DJBHash/DEKHash/BPHash/BKDRHash(HDscClassIf.hash_*)——經典 hash 表 lookup(資源名/配置 key 匹配),暗示 native 側有大量字符串驅動的配置表 - 未發現明文字符串級別的 AES/DES/RC4 標識(crypto grep 命中的全為 DICOM tag 名)
libHSocketIf.so(14 KB)
- 導出:
Java_jni_SerialPort_open/close/config/constructFileDescriptor(1.2 KB 的 open 是主體)+Java_jni_InputUtils_*(8 個:moveMouse/clickMouse/wheelMouse/AbsMouse/PressMoveMouse/sendKeyCodeUp/Down/open/close) SerialPort_open:"Opening serial port %s with flags 0x%x"→/dev/tty*串口 +Ljava/io/FileDescriptor回 JavaInputUtils是設備級輸入注入(鼠標/鍵盤事件注入系統)——OEM 專用設備(如軌跡球面板)場景- 編譯鏈:Android clang 9.0.9(based on LLVM 9.0.9svn)
其他 native 庫要點
| 庫 | 觀察 |
|---|---|
libpytorch_jni.so(76 MB) |
PyTorch 運行時全打包,AI 推理主引擎 |
libtensorflowlite_jni.so + _gpu_jni |
TFLite CPU + GPU(OpenCL)雙後端 |
libvosk.so(8.9 MB) |
Kaldi 系語音識別 |
libh264rtp.so |
HH264PlayerIf:USB 影像流 H.264 解碼(WUsbSignalBCImg/RUsbSignalImgReady/PackD/PackM) |
libfpgargb_lib.so |
fpgargb_lib:FPGA RGB 輸出 + bindToCpu(7)(LandQ6 系列綁定 CPU7)+ byteswap(RBF 位元組序轉換) |
| FFmpeg 56 系列 | cine 導出視頻編碼(x264 在內) |
libmodpdfium/libjniPdfium |
PDF 渲染(說明書/報告) |
未解部分(需 IDA/Ghidra 動態或深挖)
JavaGetMessage內部消息協議(what/arg1/arg2 的語義表)- 波束形成/掃描轉換核心算法(
CPreProc1.5 KB 等大函數) hash_*forHuman系列用途(hash 表 key 生成)- DICOM 棧的具體實現(自研 vs 內嵌第三方——tag 名集合顯示是完整自研/集成棧)
功能授權體系
Feature Authorization System — 功能授權體系完整解碼
靜態重構(jadx + native 符號)。授權算法分佈:**驗證邏輯 Java 側,hash 算法 native 側(HDscClassIf.hash_*)**。
1. 總覽:三種探頭硬件,三種授權存儲
| 探頭類 | 硬件 | 授權存儲 | 驗證方式 |
|---|---|---|---|
HSUsbProbe(S 系列 USB) |
Cypress USB | EEPROM 地址表 | hash(SN) == EEPROM 授權碼 |
HRUsbProbe(R 系列 RGB 機) |
RGB 接口 + SD | SD 卡 block 2-12 | hash(SN) == SD 授權碼 |
HRTLUsbProbe(Realtek 系) |
Realtek | 寄存器(ProbeReg(4) 單 byte) |
bitmask 直讀 |
HUsbProbe(基類)/ HSUsbProbeCh16 |
— | — | VerifyAuth() 硬編碼 return true |
2. 授權算法(HRUsbProbe / HSUsbProbe 通用)
授權碼 = 對探頭 SN 字符串計算 hash(native libdoppler 內實現):
| 功能 | hash | 存儲位置(R: SD block / S: EEPROM addr) |
|---|---|---|
| B 模式 | DJBHash |
block 6 / EEPROM 28,288 |
| Color | BKDRHash |
block 4 / EEPROM 28,256 |
| PW 多普勒 | FNVHash |
block 3 / EEPROM 28,240 |
| DICOM | BPHash |
block 2 / EEPROM 28,224 |
| 人/獸醫切換 | JSHash |
block 8 / EEPROM 28,308 |
| 使用時間/計次 | DEKHash |
block 12(隨機數+計數器) |
全部係經典公開 hash 算法(DJB2/BKDR/FNV-1/BP/JS 均為標準教科書算法)。SN 格式:"。
3. 關鍵漏洞/後門(靜態證據)
3.1 SN 清零自動全授權(HRUsbProbe.VerifyAuth)
boolean z = b == 0 && bArr3[1] == 0; // SN 頭兩字節為 0 = 未初始化
int iReadAuthCode = ReadAuthCode(4); // Color 授權碼
if (iReadAuthCode == 0 && z) {
WriteAuthCode(4, iHash_BKDRHash_forColor2); // ← 自動寫入正確授權碼!
}
Color/DICOM/PW 三項都有同樣邏輯:授權位為 0 且 SN 未初始化 → 自動寫入 hash(SN) 全功能授權。(推測:出廠自檢/新 SN 燒錄流程,但可被利用——清 SN 頭 2 字節 + 清授權位 = 全解鎖)
3.2 VerifyAuth 硬編碼 true
HUsbProbe.VerifyAuth()(基類):return trueHSUsbProbeCh16.VerifyAuth():return true(ch16 型號完全無 SN 認證)- 但功能位仍由 EEPROM hash 驗證(
HSUsbProbe.VerifyAuth內 680-706 行)——ch16 跳過嘅係 SN 格式檢查,唔係功能位
3.3 萬能 OEM ID = 255
public Boolean GetOEMAuth(int probeOemId) {
if (probeOemId != 255) { ... if (probeOemId != appOemId) return false; return true; }
return true; // ← 255 對任何 App 一律通過
}
另有 IsCheckOEMID(appOemId) 開關:App 側可配置完全跳過 OEM 驗證(此時 appOemId 視為 1)。
3.4 授權寫入通道公開
- vendor request
VR_USB_SET_AUTH=24:WriteAuthCode()明文寫入,無簽名無加密 WriteEEprom(81, addr, code, 4):S 系列寫通道WriteSD(block, ...):R 系列寫通道- 31-byte bulk 命令格式已解碼:
{GET_EEPROM_REG3, 'S','B','C', 24,24,24,24, len32, 0x80,0,10,40,0, addr32, 0, len16, 0...}
4. 隱藏功能清單
| 功能 | 證據 | 狀態 |
|---|---|---|
| 使用時間/計次授權(租賃模式) | Readrandomandnum(12)、SetUseTimeNum、hash_DEKHash_forUseTime;Ultrasys.mUseTimeAuthCode |
檢查點在 native(Java 層只有 set/get) |
| 獸醫等級 0-6 | ReadVetId → bArr[0]-48(ASCII '0'-'6')→ SetVet(i) + WLAN msg 100;RTL 用寄存器 bit3-5 |
7 級獸醫模式 |
| NoExpand(影像擴展鎖) | RTL mNoExpand = (b & 128)>>7 + WLAN msg -95 |
硬件位 |
| FPGA 數據寬度 8/16 bit | SendMsg 16/17 → SetDataWidth(8/16) |
RGB 機型 |
| FPGA 電源/配置控制 | SendMsg 18-21 → SetFPGANConfig/CheckFPGANStatus/CheckFPGANConfigDone/SetFPGAPower |
RGB 機型 |
| RF 原始數據採集 | mRFGather → 讀 2,097,152 B 原始 RF |
工程/研究模式 |
| 多主機模式 | WlanProbe.GetMultiHost() == 2 |
一探頭多設備(教學?) |
| FPGA 現場升級 | 外部 FPGA_ch16_5.rbf 覆蓋 assets 版 + byteswap |
升級通道 |
| 模擬模式 | IsSUsbSimOpen() → ramp 假數據 |
無探頭演示 |
| 第三方 App 集成 | ThirdPartyManageActivity(exported)+ returnThirdParty |
SDK 模式 |
| DICOM 獨立授權更新 | UpdateDicomAuth() 經 USB |
售後 DICOM 開通 |
5. 安全評估
- 保護強度:授權碼演算法為公開經典 hash + SN 明文 → 可完全離線計算。真正的防護只係「物理訪問探頭存儲」嘅門檻
- 無簽名、無非對稱加密、無挑戰應答 → 無防重放、無防偽造
- UseTime 計時授權若在 native 檢查,需進一步動態驗證(Frida hook
SetUseTimeAuthCode後觀察 native 行為) - 商業影響:EEPROM/SD 讀寫器 + SN 讀取 → 全功能解鎖;OEM ID 255 或關閉 IsCheckOEMID → 跨品牌通用
JNI API Reference
API Reference — JNI / Native Surface
SonoiQ 的「公共 API」本質上是 jni.* 包暴露的 native 接口。以下按功能分組(全部實測自 jadx 反編譯)。
探頭與 PID — jni.HPidIf(→ libdoppler)
public static native boolean InitPidSys();
public static native boolean setPidFilesPath(String path); // assets/pid/PID03.zip
public static native int GetProbeId(String str);
public static native String GetProbeName(int i);
public static native int GetProbeType(int i);
public static native int GetProbeElementNumber(); // 陣元數
public static native float GetProbeRadius(); // 凸陣半徑
public static native float GetProbeElementDistance(); // 陣元間距
public static native int GetProbePowerType();
public static native float GetFreqValue(int i, int i2); // 頻率
public static native float GetPwFreq(int i);
public static native float GetFocusDepth(int i);
public static native int GetBSSR(int i, int i2); // B 模式採樣率
public static native int GetMaxScanArea();
public static native float GetDeflectAngle(int i, int i2); // 偏轉角
public static native float GetCardiacExpand();
public static native float GetExpandAngle();
public static native int GetDblPlanFlag();
掃描系統控制 — jni.HSysctrlIf(→ libdoppler)
public static native void DopplerAppInit();
public static native void DoStartScan();
public static native void DoStopScan();
public static native void Freeze();
public static native void CalcScanFrame();
public static native void RFGather(); // RF 採集
public static native float GetFrameRate();
public static native void ParaChangeStart(int i); // 參數批量變更 begin/end
public static native void ParaChangeEnd(int i);
public static native void ParaSave(short s, short s2, String str);
public static native void ImgPresetInit();
public static native int ReadFPGAParam(int i); // FPGA 參數讀取
public static native void SetAPowerLevel(int i); // 聲功率
public static native void SetBFocus(int i);
public static native void SetBFreq(int i);
public static native void SetBGainLevel(int i);
public static native void SetBDynamicLevel(int i);
public static native void SetBDTgc(int[] iArr); // TGC 曲線
public static native void SetBExpand(boolean z);
public static native void SetBImgEnhanceLevel(int i);
public static native void SetBSpaceCompSwitch(boolean z);
public static native void SetBFrameCorrLevel(int i);
public static native void SetAdAdjust(int i);
// …(B/C/M/PW 全套 setter,190 行級)
掃描轉換與影像處理 — jni.HDscClassIf(→ libdoppler)
public static native void BCJoint(byte[], byte[], int, int[], int[], int[], int);
public static native boolean BEnhance(byte[], byte[], int, int, int, int);
public static native boolean BFrameCorr(byte[], byte[], int, int, int, int);
public static native boolean BreathNoiseDepress(byte[], int, int, int); // 呼吸噪聲抑制
public static native boolean CFDenoise(byte[], byte[], int, int, int, int);
public static native boolean CFrameCorrelation(byte[], byte[], byte[], int, int, int, int, int);
public static native boolean ColorMap(byte[], int[], int[]); // 色調映射
public static native void CancelDscTableCalc(boolean z);
public static native int ClipImg(byte[], byte[], int, int, int, int, int, int);
DICOM — jni.HDicomIf(→ libdoppler,native DICOM 棧)
public static native void Init();
public static native void AddStorageServer(int, String, String, String, int);
public static native void RemoveStorageServer(String);
public static native void ResetStorageServerList();
public static native void AddWorkListServer(int, String, String, String);
public static native void RequestWorklist(int, String, String, String, String);
public static native void AddMppsServer(int, String, String, String);
public static native boolean SendMPPS(String aet, String host, String port, String..., int...);
public static native void CommitPrepare(byte[], int, int, int, byte[], int, boolean);
public static native int GenPatientID(byte[]);
public static native void GetUID(HExamInfo examInfo);
public static native void SetHospitalName(String);
public static native void SetDeviceInfo(String, String, String, String);
public static native void SetDateAndTime(String, String);
public static native void AddDefaultServer(String);
USB / 消息橋 — handprobe.application.usb.jnitojava(native 回調入口)
public static native int JavaGetMessage(long[] out); // out[0]=what out[1]=arg1 out[2]=arg2
public static native void SetJniEvn();
public static boolean UsbWriteDwords(int, byte[], int); // → UsbProbeManager.Write
public static boolean UsbReadDwords(int, byte[], int); // → UsbProbeManager.Read
public static int WUsbReadImg(byte[], int); // → ReadEncImg(加密影像)
public static int UsbGetImgPack(byte[], int[]); // → GetImgPack
public static void UpdateMITI(float[]); // → Ultrasys.setMITIData
public static int GetPattern(int, int); // → Ultrasys.GetPattern
public static void CineDataCapture(); // → AppProc.DicomCineDataCapture
其他
jni.SerialPort(→libHSocketIf)— 串口 open/close/read/writejni.HH264PlayerIf(→libh264rtp)— H.264 解碼播放jni.HOpencvIf(→libhopencvif_lib)— OpenCV 操作封裝jni.fpgargb_lib(→libfpgargb_lib)— FPGA RGB 輸出jni.HGLImageJNIViewLib(→libHSocketIf)— OpenGL 影像視圖jni.MeasFormulaParser(→libHSocketIf)— 測量公式解析jni.InputUtils(→libHSocketIf)— 輸入工具jni.HPresetIf/HBeParamIf/HFeParamIf(→libdoppler)— 預設/波束參數/前端參數
附加發現
Additional Findings — 硬件生態與臨床功能全景
第二輪深挖(PID03.zip 內容、固件、HIS API、FPGA bitstream 元數據、hash 反匯編)。全部靜態證據。
1. 硬件身份鎖定
| 發現 | 證據 | ||
|---|---|---|---|
| FPGA = Efinix Trion T55F324(非 Intel/Altera) | FPGA_ch16_5.rbf 明文頭:Family: Trion / Device: T55F324 / Width: 16 / Mode: passive |
||
| FPGA 項目名 YK2100 | RBF 頭:Project: D:\Project\FPGA\YK2100_FPGA_T55_new\03_pnr(開發者 Windows 路徑殘留) |
||
| 品牌 = Youkey | OEM_YOUKEY_ID = 1 + YK2100 項目名 → SonoiQ 原始品牌 Youkey,其餘 17 OEM 為白牌 |
||
| 板卡 FPGA 固件 | `fw/board/v2 | v3/fpga/16 | 32/UFPGA.bin`(raw bitstream,0xFF 開頭無頭) |
| WiFi MCU = TI CC3220R / CC3235S | `fw/board/v2 | v3/mcu/cc32{20r,35s}/mcuimg2.bin;頭 0030 0020` = Cortex-M4 向量表(SP=0x20003000) |
|
| 固件版本 | fpga/16 v3 = 191204001.3.18;PID Global = P25122602_004(2025-12-26 構建) |
||
| RBF 工具鏈 | Version: 2022.1.226、Generated: Wed Jul 31 11:07:04 2024(Quartus/Efinity 2022.1) |
2. 探頭型號全集(PID03.zip,77 個型號目錄 + Global + AIModel,6,294 檔 344 MB)
| 類別 | 型號例 | 用途 |
|---|---|---|
| 凸陣 C | 03C5-2Ds、04C5-2Ds、05C5-2Vs、06C15s、07C15s、08C15s、C5-2Ds… | 腹部/產科 |
| 微凸高頻 | 05C60h、07C50h、07C60h、08C50h、08C60h(h 後綴) | 高頻微凸 |
| 線陣 L | L10-4Fs、L11-4Ds、L12-4Hs、06L40s、07L25h、07L40s、08L40h… | 淺表/血管/肌骨 |
| 相控陣 P | P4-2Fs、04P4-2Ds、05P4-2Ds、07P20s、08P4s | 心臟 |
| 腔內 E | E10-4Ds、E10-4Ks、06E10s、08E10s | 婦科/泌尿 |
| 複合 | 05L25E10 | 特殊 |
| Global | BGrayMap / DGrayMap / DXNode / ApodizeCurveLib / CMMParaLib | 全局灰階映射+切趾庫 |
每型號 pattern0/Normal/ 含全套 DSP 參數(.INF 201KB 主描述 + ApodizePara、BlindArea、CodingLut、CosLutPara、DRPara、DSFilterPara、EmitFocusPara…)——波束形成/DSC 完整參數集,換型號=換參數集。
3. AI 模型全集(PID03/AIModel/,13 個 .pt,~165 MB)
| 模型 | 大小 | 對應功能 |
|---|---|---|
| ResUNet_Bladder.pt | 23.9 MB | 膀胱分割 |
| UNet_Sample400_SegClass_breast_lite.pt | 23.9 MB | 乳腺分割(ExamTypesId.BREAST_EXAM 有入口) |
| UNet_Sample180_256x192_Carotid_recognition_v&l.pt | 17.7 MB | 頸動脈 |
| skin_segment.pt | 17.5 MB | 皮膚分割 |
| cardiac_ef.pt | 13.8 MB | 心臟 EF |
| lung_bline_detector.pt | 13.4 MB | 肺 B 線 |
| UNet_ob_hc.pt | 7.9 MB | 產科頭圍 |
| UNet_Sample7000_256x192_Thyroid.pt | 7.9 MB | 甲狀腺分割 |
| UNet_Varicosity.pt | 7.9 MB | 靜脈曲張 |
| ovary_pcos_classifier.pt | 6.5 MB | 多囊卵巢分類 |
| mobilenet_v3_classifier_3_n190_128x96_quantized.pt | 4.6 MB | MobileNetV3 量化分類器 |
| Thyroid_classifier_3_n7000_256x192_lite.pt | 4.6 MB | 甲狀腺分類 |
模型架構命名暴露訓練細節(n190/n7000 = 訓練樣本數、256x192 = 輸入尺寸、v&l = 血管+縱向)。
4. 臨床檢查模式全集(ExamTypesId,40+ 模式)
腹部(成人/兒科/新生兒/穿透)、心臟(成人/兒科/新生兒/胎兒)、產科(OB1/OB23)、婦科、乳腺、頸動脈、IMT(內中膜厚度)、眼科、前列腺、睪丸、泌尿、腎臟、肌骨、骨科、神經、甲狀腺、外周動脈/靜脈、上肢動脈/靜脈、經顱 TCI、急診套件(FAST/OB/ABD/血管/淺表)。
5. HIS 集成 API(HttpProtocol_hle)
- 用途:醫院資訊系統對接(
basicUserInfo用戶驗證、addCheckItem回傳檢查項) - 認證:
appKey+appSecret+ts+sign;sign = SHA-1(MessageDigest.getInstance("SHA"))+ Base64 - 弱點:
- SHA-1 已過時(碰撞可行)
ts=168236588304硬編碼(2023-04-25,非實時時間戳)- HTTPS 證書驗證完全關閉(
checkServerTrusted空實現 +hostnameVerifier恆 true)——中間人可攔截 HIS 憑證 - 配置:用戶可在
NetWorkPresetFrag設置服務器/帳號/密碼(http/https 可選)
6. Hash 算法反匯編確認(libdoppler, arm64)
| 功能 | 算法 | 反匯編證據 |
|---|---|---|
| B 模式 | DJB2 | mov w21, 0x1505(=5381)+ add w10, w21, w21, lsl 5(×33)+ add w21, w10, c |
| Color | BKDR(乘加) | mul w11, w21, w10 + add w21, w11, c |
| PW | FNV-1(乘異或) | mul w12, w21, w10 + eor w21, w12, w11 |
授權碼 = 對 SN 字符串計算以上公開算法,可完全離線重現(Java 一行代碼)。
7. 深挖完成狀態(2026-09-03,7 項全部完成 → docs/deepdive/)
- ✅
java-getmessage-semantics.md— 消息泵反匯編:元素{int32 what; int32 arg1; int64 arg2}、std::deque、GetLongArrayElements(0x5e0)/ReleaseLongArrayElements(0x620)、Java 側 arg2 截斷 int32 - ✅
wlan-command-codes.md— 命令碼全集:0xFE 頭 + cmdid + dir + 2B LE 長度 + 0xFF 尾;D_ 設備碼 128-202 / W_ 參數碼 0-127;端口 8081/8082/8083/8093;hex-ASCII vs raw 編碼;無 checksum - ✅
thirdparty-sdk-interface.md— SDK 接口:7 extras key、5 錯誤碼、SAVEIMG_PATH/IMG_PATH 雙通道、PHI 注入/外洩風險(高) - ✅
dispmode-values.md— DispMode 全集:0=B 1=M 5=PW 8=BC 9=BM 16/17 雙幅 50/51 多幅 55=BPower 57/58/59=PW 疊加變體;值兼任硬件 scan-mode ID - ✅
pid03-inf-format.md— INF 格式:GBK XML 標籤 + 二進制 payload 混合,65 個標籤(發射/接收/波束形成/DSC/PW/MI-TI 全套 DSP 參數結構) - ✅
cc32xx-firmware.md— CC32xx:run-from-SRAM 影像(SP=0x20003000,reset 在 SRAM)、serial flash 檔(probeid.txt/uhandprobe.bin/sleep.bin)、AP 命令、v2/v3 差異 49% - ✅
efinix-bitstream.md— Efinix:_5 係完整未壓縮 T55 bitstream(3,459,240 B = 27.67Mbit,AN006 max 27.675Mbit 吻合);5 個 718,569 B raw RBF 係 5 個不同板版本(~4.8:1 壓縮/截斷);fpgargb_lib.byteswap證實為 16-bit 字序反轉
深挖 1 — 消息泵反匯編
JavaGetMessage 消息泵反匯編 (C→Java Message Pump Disassembly)
對象:libdoppler.soJava_handprobe_application_usb_jnitojava_JavaGetMessage(0x0061dd28,244 B,arm64) 方法:r2 線性反匯編(避免 full-analysis 卡死),結合 Java 側kernel.MainHandler交叉驗證。
一、完整解碼(高置信度)
函數主體 = 從全局單例的消息隊列 pop 一個 16-byte 元素,轉換寫入 Java long[4]:
// 語義等價 C 偽代碼
int Java_jnitojava_JavaGetMessage(JNIEnv* env, jclass, jlongArray out) {
G* g = s_szInstanceNumber; // 0xe2e000 全局單例指針
if (g->queue_ready == 0) // [x22+0x28] == 0
return 0; // 隊列未初始化
jlong* buf = env->GetLongArrayElements(out, NULL); // vtable[0x5e0]
Msg m = g->queue.pop_front(); // 16-byte 元素: d8(2×int32) + x23(int64)
buf[0] = (jlong)(int32_t)m.what; // sshll v0.2d, v8.2s → 符號擴展
buf[1] = (jlong)(int32_t)m.arg1; // 同一 SSHLL 指令寫兩路
buf[2] = (jlong)m.arg2; // x23 原始 int64 直接存 [x21+16]
buf[3] 未寫(Java 側只用 0-2)
env->ReleaseLongArrayElements(out, buf, 0); // vtable[0x620], mode=0
return 1;
}
關鍵指令證據
| 地址 | 指令 | 含義 |
|---|---|---|
| 0x61dd40 | adrp x22, reloc.s_szInstanceNumber |
全局單例(隊列宿主) |
| 0x61dd48-4c | ldr x8,[x22,0x28]; cbz x8, 0x61de00 |
隊列空/未初始化 → return 0 |
| 0x61dd60 | ldr x8,[x8,0x5e0]; blr x8 |
GetLongArrayElements(JNI vtable 0x5e0) |
| 0x61dd6c-94 | ldr q0,[x22,0x20] + lsr x10,x9,5 + and x10,x10,0x7fff...f8 |
std::deque map 索引(x>>5 & ~7 每 8 元素一 block) |
| 0x61dd98-9c | ldr d8,[x9]; ldr x23,[x9,8] |
元素兩字段:d8=2×int32、x23=int64 |
| 0x61ddb0 | bl operator_delete |
pop 後析構元素 |
| 0x61ddd0 | sshll v0.2d, v8.2s, 0 |
int32→int64 符號擴展(what/arg1 為 signed) |
| 0x61ddd4-98 | str q0,[x21]; str x23,[x21,0x10] |
寫 buf[0]=what、buf[1]=arg1、buf[2]=arg2 |
| 0x61ddec | ldr x8,[x8,0x620]; blr x8 |
ReleaseLongArrayElements(mode=0) |
| 0x61ddf8 | mov w0, 1 |
有消息返回 1 |
Java 側消費(kernel/MainHandler.java:36-45,已知)
if (jnitojava.JavaGetMessage(jArr) != 0) {
message.what = (int) jArr[0];
message.arg1 = (int) jArr[1];
message.arg2 = (int) jArr[2]; // ← 注意:int64 截斷為 int32!
// 廣播給 mDopplerMsgHandlerVector 所有註冊 Handler
}
二、結論
- 消息元素:C 側
{int32 what; int32 arg1; int64 arg2}(16 B,deque 隊列) - 隊列:std::deque(每 block 8 元素,由
lsr 5+&~7證明) - 全局單例:
s_szInstanceNumber(0xe2e000)——隊列 + 就緒標誌 + 容器頭 - 返回值:0=無消息/未初始化,1=成功取出一條
- arg2 類型不對稱:native 傳 int64,Java 截斷成 int32(
(int) jArr[2])——arg2 實際有效位 ≤ 32 - what/arg1 為 signed int32 語義(符號擴展證據)
三、缺口(無法靜態確定)
- 消息生產者:邊個函數 push 入呢個 deque?(需
xrefs to s_szInstanceNumber或對 0xe2e000 引用做反向分析——stripped binary 下成本高) - what 值語義表:消息碼枚舉喺 native,Java 側只係轉發。可以從 Java 側
handleMessage的case反向歸納(BaseActivity的AMSG_*常量是 UI 側消息,未必同源) - 隊列元素構造器未定位(
operator_new16B 分配點可用axt 0x61dd98追)
四、對 Frida 的意義
- Hook
JavaGetMessage的 onLeave + 讀 out 數組(Frida 可以讀 Java 數組)即可獲得what/arg1/arg2全流——無需理解 native 隊列 - 或 Hook
MainHandler.handleMessage(Java 側)更簡單:所有消息必經此處 - 若要篡改消息(例如屏蔽凍結狀態),直接改
jArr內容即可
深挖 2 — WLAN 命令碼
WLAN 探頭命令代碼總表 (WLAN Probe Command-Code Reference)
來源:jadx_out/sources/handprobe/application/wlan/(jadx 反編譯 Java 協定層)。 檔案簡稱對照:protocol/CmdInfo.java、protocol/CmdParser.java、protocol/BaseCmdHandler.java、protocol/CmdHandler.java、protocol/Tools.java、wlanprobe/WlanProbe.java、wlanprobe/WlanHost.java、wlanprobe/WlanClient.java、wlanprobe/WlanCmdHandler.java、wlanprobe/JsonParam.java、wifi/HWifiReceiver.java。
一、封包框架 (Packet Framing)
1.1 傳送端(App → 探頭)— BaseCmdHandler.excute() (BaseCmdHandler.java:52-90)
| 偏移 | 欄位 | 說明 |
|---|---|---|
[0] |
0xFE |
W_HEAD = -2 封包頭 (BaseCmdHandler.java:35) |
[1] |
cmdid | 命令代碼(1 byte,& 0xFF) |
[2] |
dir | 方向:0=SET、1=GET、2=DAT、3=PACK |
[3..4] |
len | 資料長度(little-endian,len % 256 / len / 256) |
[5..] |
payload | 見下方編碼規則 |
| 末位 | 0xFF |
W_END = -1 封包尾 (BaseCmdHandler.java:34) |
編碼規則 (payload encoding):
- dir ∈ {0 (SET), 1 (GET)}:payload 為 hex-ASCII 編碼(每個原始 byte → 兩個 ASCII 十六進位字元),長度
= 2 × len。回傳總長= 2×len + 6(BaseCmdHandler.java:64-87)。 - dir ∈ {2 (DAT), 3 (PACK)}:payload 為 原始二進位,長度
= len。並在結尾把 dir 欄位[2]清零 (BaseCmdHandler.java:80-89)。 - dir == 1 (GET):長度欄位強制
[3]=1(BaseCmdHandler.java:61-63)。
1.2 接收端(探頭 → App,WLAN UDP)— CmdHandler.PackProcThreadInit (CmdHandler.java:245-280)
注意:接收端「沒有」0xFE 封包頭,與傳送端不對稱。
| 偏移 | 欄位 |
|---|---|
[0] |
cmdid |
[1] |
dir |
[2..3] |
len(little-endian,mRecvData[2] + (mRecvData[3] << 8)) |
[4..] |
data:dir==3 時原始複製;其餘 hex-ASCII 解碼 (Tools.StrToHex) |
長度上限檢查:s <= 8189 && s != 0 (CmdHandler.java:247)。
1.3 接收端(探頭 → App,USB 通道)— CmdHandler.WUsbProcThreadInit (CmdHandler.java:494-521)
USB 通道以 0xFE 為同步字節掃描,框架為 [0xFE][cmdid][dir][len_lo][len_hi][data(hex-ASCII)],且資料為 hex-ASCII(每 byte 兩個字元),下一個封包偏移 = 5 + 2×len (CmdHandler.java:519)。
1.4 TCP 通道框架(UWLAN,ConType==5)
TCP 在 UDP 框架前再加 2-byte little-endian 長度前綴:[len_lo][len_hi][0xFE][cmdid][dir][len_lo][len_hi][data]。證據:UReadFPGA_TCP / UWriteFPGA_TCP (CmdHandler.java:913-1031) 中 mData[0]=len_lo, mData[1]=len_hi, mData[2]=0xFE, mData[3]=cmdid。
1.5 校驗 (Checksum)
無 checksum。varify(DatagramPacket) 為空實作,恆回傳 true (CmdHandler.java:65-67)。完整性僅靠 0xFF 結束字節與長度欄位。
二、通道、埠號與位址 (Channels, Ports & Addresses) — BaseCmdHandler.java:25-35
| 常數 | 值 | 用途 |
|---|---|---|
REMOTE_IP |
192.168.254.1 |
探頭預設位址 |
REMOTE_HOST_2_IP |
192.168.254.199 |
第二主機位址 |
REMOTE_BROADCASTER_IP |
192.168.254.255 |
廣播位址 |
REMOTE_PORT |
8081 |
命令 UDP/TCP 埠 |
LOCAL_PORT |
8082 |
本機命令接收埠 |
LOCAL_DAT_PORT |
8083 |
本機資料接收埠(FPGA 讀寫回應) |
WLAN_PORT |
8093 |
主機發現/廣播埠 |
連線型態 (ConnectType.java:10-17):0=無、1=SIMU、2=WLAN(SSID)、3=WUSB、4=USB、5=UWLAN(+SSID+)、6=RGBIF、7=BROADCAST。UWLAN (5) 走 TCP,其餘走 UDP。
三、方向與目標代碼 (Direction & Target Codes)
3.1 方向 CmdInfo.CmdDir (CmdInfo.java:6-14)
| 常數 | 值 | 意義 |
|---|---|---|
W_SET |
0 | 寫入/設定參數 |
W_GET |
1 | 讀取/請求參數 |
W_DAT |
2 | 資料(二進位) |
W_PACK |
3 | 封包/批量資料(二進位) |
3.2 目標 CmdHandler 常數 (CmdHandler.java:41-44)
BROADCASTER_TARGET=255、DEVICE_TARGET=0、HOST_1_TARGET=1、HOST_2_TARGET=2。在 CmdHandler.excute(i, i2, i3, bArr, i4) 中 i4 即目標:2 → wlanSendToHost2()、255 → wlanBroadcaster()、其餘 → wlanSend() + wusbSend() (CmdHandler.java:87-102)。
四、命令代碼總表 (Command-Code Table)
代碼以十進位表示;(byte)負值對應無號值:例如-95 = 0xA1 = 161。 方向縮寫:→P = App→Probe、←P = Probe→App、⇄ = 雙向。
4.1 裝置類命令 D_*(Device,128–202)
| 代碼 | 常數名稱 | 方向 | 意義 | 證據 |
|---|---|---|---|---|
| 128 | D_SET_AP_SSID_NAME | →P | 設定 AP SSID 名稱 | CmdInfo.java:59 |
| 129 | D_SET_AP_SSID_PWD | →P | 設定 AP SSID 密碼 | CmdInfo.java:60 |
| 130 | D_SET_BULE_LED | →P | 設定藍色 LED(藍芽指示燈) | CmdInfo.java:61 |
| 131 | D_SET_WHITE_LED | →P | 設定白色 LED | CmdInfo.java:72 |
| 132 | D_SET_MCUIMG_CONTENT | →P | 寫入 MCU 韌體內容 | CmdInfo.java:66 |
| 133 | D_SET_MCUIMG_CONTENT_END | →P | MCU 韌體內容結束 | CmdInfo.java:67 |
| 134 | D_SET_MCUIMG_SIZE | →P | MCU 韌體大小 | CmdInfo.java:68 |
| 135 | D_SET_IMX51_IMG_CONTENT | →P | 寫入 i.MX51 映像內容 | CmdInfo.java:63 |
| 136 | D_SET_IMX51_IMG_CONTENT_END | →P | i.MX51 映像內容結束 | CmdInfo.java:64 |
| 137 | D_SET_IMX51_IMG_SIZE | →P | i.MX51 映像大小 | CmdInfo.java:65 |
| 138 | D_SET_PID_CONTENT | →P | 寫入 PID(產品 ID)內容 | CmdInfo.java:69 |
| 139 | D_SET_PID_RETURN_CONTENT | ←P | PID 回傳內容 | CmdInfo.java:70 |
| 140 | D_SET_PID_RETURN_FINISH | ←P | PID 回傳完成 | CmdInfo.java:71 |
| 141 | D_PROBE_FIRMWARE_VERSION | ⇄(GET) | 探頭韌體版本 | CmdInfo.java:51 |
| 142 | D_WIFI_FIRMWARE_VERSION | ⇄(GET) | Wi-Fi 韌體版本 | CmdInfo.java:83 |
| 143 | D_HOST_CHECK | ⇄ | 主機偵測 | CmdInfo.java:44 |
| 144 | D_WIFI_CHANNEL | ⇄(GET) | Wi-Fi 通道 | CmdInfo.java:82 |
| 145 | D_PROBE_PID_VERSION | ⇄(GET) | 探頭 PID 版本 | CmdInfo.java:54 |
| 146 | D_PROBE_FPGA_VERSION | ⇄(GET) | 探頭 FPGA 版本 | CmdInfo.java:52 |
| 147 | D_UPROBE_FPGA_ADDR | →P | FPGA 暫存器位址 | CmdInfo.java:74 |
| 148 | D_UPROBE_FPGA_DAT | ⇄ | FPGA 暫存器資料讀/寫 | CmdInfo.java:75 |
| 149 | D_PROBE_SN | ⇄(GET) | 探頭序號 | CmdInfo.java:55 |
| 150 | D_UPROBE_PRODUCTID | ⇄(GET) | 產品 ID(配對握手) | CmdInfo.java:77 |
| 151 | D_UPROBE_PROBEID | ⇄(GET) | 探頭 ID | CmdInfo.java:76 |
| 152 | D_SET_CHARGE | →P | 設定充電狀態 | CmdInfo.java:62 |
| 153 | D_ENABLE_SECOND_HOST | ⇄ | 啟用第二主機 | CmdInfo.java:40 |
| 154 | D_IP_ADDR | ⇄ | IP 位址 | CmdInfo.java:46 |
| 155 | D_BOARD_VERSION | ⇄(GET) | 電路板版本 | CmdInfo.java:34 |
| 156 | D_BOARD_SN | ⇄(GET) | 電路板序號 | CmdInfo.java:32 |
| 157 | D_UPROBE_BOARDID | ⇄(GET) | 探頭電路板 ID | CmdInfo.java:73 |
| 158 | D_DICOM_AUTH | →P | DICOM 授權 | CmdInfo.java:37 |
| 159 | D_PW_AUTH | →P | PW 授權 | CmdInfo.java:56 |
| 160 | D_COLOR_AUTH | →P | 彩色(Colour)授權 | CmdInfo.java:36 |
| 161 | D_PRJ_OPTION | ⇄(GET) | 專案選項旗標 | CmdInfo.java:50 |
| 162 | D_LIANYIN_DATA | ←P | 聯影(Lianmed)資料 | CmdInfo.java:47 |
| 163 | D_WRITE_FLASH_FILE | →P | 寫入 Flash 檔案 | CmdInfo.java:84 |
| 164 | D_READ_FLASH_FILE | ⇄ | 讀取 Flash 檔案 | CmdInfo.java:57 |
| 165 | D_GET_MCUIMG_CONTENT | ⇄ | 取得 MCU 映像內容 | CmdInfo.java:41 |
| 166 | D_GET_MCUIMG_CONTENT_END | ⇄ | MCU 映像內容結束 | CmdInfo.java:42 |
| 167 | D_GET_MCUIMG_SIZE | ⇄ | 取得 MCU 映像大小 | CmdInfo.java:43 |
| 168 | D_HUMAN_VET_AUTH | →P | 人醫/獸醫授權 | CmdInfo.java:45 |
| 169 | D_DUAL_ACTIVE | →P | 雙探頭啟動切換 | CmdInfo.java:38 |
| 170 | D_POWERSAVE_TIME | ⇄(GET) | 省電時間 | CmdInfo.java:49 |
| 171 | D_OEM_ID | ⇄ | OEM ID | CmdInfo.java:48 |
| 172 | D_USETIME_AUTH | →P | 使用期限授權 | CmdInfo.java:79 |
| 173 | D_USETIME_TIME | ⇄(GET) | 使用期限剩餘時間 | CmdInfo.java:81 |
| 192 | D_BOARD_TEMPERATURE | ←P | 電路板溫度 | CmdInfo.java:33 |
| 193 | D_BATTERY_LEVEL | ←P | 電池電量 | CmdInfo.java:31 |
| 194 | D_ADC_PROBE_ID | ←P | ADC 探頭 ID | CmdInfo.java:29 |
| 195 | D_BUTTON_ID | ←P | 探頭按鍵 | CmdInfo.java:35 |
| 196 | D_4DPOS_ID | ←P | 4D 定位 | CmdInfo.java:28 |
| 197 | D_PROBE_ID | ←P | 探頭 ID | CmdInfo.java:53 |
| 198 | D_USB_INSERT_ID | ←P | USB 插入事件 | CmdInfo.java:78 |
| 199 | D_AP_CONNECT_ID | ←P | AP 連線事件 | CmdInfo.java:30 |
| 200 | D_DUAL_PROBES_ID | ←P | 雙探頭 ID | CmdInfo.java:39 |
| 201 | D_USETIME_ID | ⇄ | 使用期限 ID | CmdInfo.java:80 |
| 202 | D_SEND_HOST_2 | →P | 轉送給第二主機 | CmdInfo.java:58 |
4.2 影像參數命令 W_*(App 命令,0–127)
| 代碼 | 常數名稱 | 意義 | 證據 |
|---|---|---|---|
| 0 | W_PROBEID_BEGIN | 探頭 ID 起始(0) | CmdInfo.java:155 |
| 1 | W_PROBEID_CMD | 探頭 ID | CmdInfo.java:156 |
| 2 | W_FREEZE_CMD | 凍結/解凍 | CmdInfo.java:127 |
| 3 | W_POLLING_CMD | 輪詢/心跳 | CmdInfo.java:143 |
| 4 | W_DEPTH_CMD | 深度 | CmdInfo.java:120 |
| 5 | W_TGC_CMD | TGC | CmdInfo.java:189 |
| 6 | W_B_GAIN_CMD | B 增益 | CmdInfo.java:95 |
| 7 | W_B_FREQ_CMD | B 頻率 | CmdInfo.java:94 |
| 8 | W_B_TSI_CMD | B TSI | CmdInfo.java:101 |
| 9 | W_B_POWER_CMD | B 功率 | CmdInfo.java:98 |
| 10 | W_B_DYNAMIC_CMD | B 動態範圍 | CmdInfo.java:89 |
| 11 | W_B_EXPAND_CMD | B 展開 | CmdInfo.java:91 |
| 12 | W_B_FOCUSPOS_CMD | B 焦點位置 | CmdInfo.java:92 |
| 13 | W_B_ENHANCE_CMD | B 增強 | CmdInfo.java:90 |
| 14 | W_B_SPACECOMP_CMD | B 空間複合 | CmdInfo.java:99 |
| 15 | W_B_STEER_CMD | B 偏轉 | CmdInfo.java:100 |
| 16 | W_B_FRAMECORR_CMD | B 幀相關 | CmdInfo.java:93 |
| 17 | W_B_POSTEFFECT_CMD | B 後處理 | CmdInfo.java:97 |
| 18 | W_B_LR_CMD | B 左右翻轉 | CmdInfo.java:96 |
| 19 | W_B_UD_CMD | B 上下翻轉 | CmdInfo.java:102 |
| 20 | W_B_COLORMAP_CMD | B 彩色圖譜 | CmdInfo.java:88 |
| 21 | W_THI_GAIN_CMD | THI 增益 | CmdInfo.java:191 |
| 22 | W_THI_DYNAMIC_CMD | THI 動態範圍 | CmdInfo.java:190 |
| 23 | W_THI_POSTEFFECT_CMD | THI 後處理 | CmdInfo.java:192 |
| 24 | W_FRAMECORR_CMD | 幀相關 | CmdInfo.java:126 |
| 25 | W_C_GAIN_CMD | C 增益 | CmdInfo.java:113 |
| 26 | W_C_SCALE_CMD | C 量程 | CmdInfo.java:116 |
| 27 | W_C_SPEED_REVERSE_CMD | C 速度反向 | CmdInfo.java:117 |
| 28 | W_C_FREQ_CMD | C 頻率 | CmdInfo.java:112 |
| 29 | W_C_WALLFILTER_CMD | C 壁濾波 | CmdInfo.java:119 |
| 30 | W_C_BASELINE_CMD | C 基線 | CmdInfo.java:106 |
| 31 | W_C_FRAMECORR_CMD | C 幀相關 | CmdInfo.java:111 |
| 32 | W_C_PRIORY_CMD | C 優先權 | CmdInfo.java:114 |
| 33 | W_C_PSEUDOCOLOR_CMD | C 偽彩 | CmdInfo.java:115 |
| 34 | W_C_STEER_CMD | C 偏轉 | CmdInfo.java:118 |
| 35 | W_C_DISP_LINE_RANGE_CMD | C 顯示線範圍 | CmdInfo.java:109 |
| 36 | W_C_DISP_POINT_RANGE_CMD | C 顯示點範圍 | CmdInfo.java:110 |
| 37 | W_POWER_GAIN_CMD | Power 增益 | CmdInfo.java:147 |
| 38 | W_POWER_SCALE_CMD | Power 量程 | CmdInfo.java:150 |
| 39 | W_POWER_SPEED_REVERSE_CMD | Power 速度反向 | CmdInfo.java:151 |
| 40 | W_POWER_WALLFILTER_CMD | Power 壁濾波 | CmdInfo.java:152 |
| 41 | W_POWER_FRAMECORR_CMD | Power 幀相關 | CmdInfo.java:146 |
| 42 | W_POWER_PRIORY_CMD | Power 優先權 | CmdInfo.java:148 |
| 43 | W_POWER_PSEUDOCOLOR_CMD | Power 偽彩 | CmdInfo.java:149 |
| 44 | W_POWER_DYNAMIC_CMD | Power 動態範圍 | CmdInfo.java:145 |
| 45 | W_M_GAIN_CMD | M 增益 | CmdInfo.java:134 |
| 46 | W_M_SPEED_CMD | M 速度 | CmdInfo.java:138 |
| 47 | W_M_DYNAMIC_CMD | M 動態範圍 | CmdInfo.java:133 |
| 48 | W_M_COLORMAP_CMD | M 彩色圖譜 | CmdInfo.java:132 |
| 49 | W_M_POSTEFFECT_CMD | M 後處理 | CmdInfo.java:137 |
| 50 | W_M_LINECORR_CMD | M 線校正 | CmdInfo.java:135 |
| 51 | W_M_TIME_MARK_CMD | M 時間標記 | CmdInfo.java:139 |
| 52 | W_M_LINENUM_CMD | M 線數 | CmdInfo.java:136 |
| 53 | W_PW_BASELINE_CMD | PW 基線 | CmdInfo.java:164 |
| 54 | W_PW_SPEED_CMD | PW 速度 | CmdInfo.java:181 |
| 55 | W_PW_QUICKANGLE_CMD | PW 快速角度 | CmdInfo.java:178 |
| 56 | W_PW_ANGLE_CMD | PW 角度 | CmdInfo.java:162 |
| 57 | W_PW_COLORMAP_SWITCH_CMD | PW 彩色圖譜切換 | CmdInfo.java:167 |
| 58 | W_PW_TIMEMARK_CMD | PW 時間標記 | CmdInfo.java:183 |
| 59 | W_PW_REVERSE_CMD | PW 反向 | CmdInfo.java:179 |
| 60 | W_PW_COLORMAP_CMD | PW 彩色圖譜 | CmdInfo.java:166 |
| 61 | W_PW_VOLUME_CMD | PW 音量 | CmdInfo.java:185 |
| 62 | W_PW_DYNAMIC_CMD | PW 動態範圍 | CmdInfo.java:169 |
| 63 | W_PW_AUTOCALC_CMD | PW 自動計算 | CmdInfo.java:163 |
| 64 | W_PW_TRACERANGE_CMD | PW 追蹤範圍 | CmdInfo.java:184 |
| 65 | W_PW_STEER_CMD | PW 偏轉 | CmdInfo.java:182 |
| 66 | W_PW_FREQ_CMD | PW 頻率 | CmdInfo.java:171 |
| 67 | W_PW_SCALE_CMD | PW 量程 | CmdInfo.java:180 |
| 68 | W_PW_GAIN_CMD | PW 增益 | CmdInfo.java:172 |
| 69 | W_PW_FILTER_CMD | PW 濾波 | CmdInfo.java:170 |
| 70 | W_PW_GATESIZE_CMD | PW 取樣門大小 | CmdInfo.java:174 |
| 71 | W_PW_GATEPOS_CMD | PW 取樣門位置 | CmdInfo.java:173 |
| 72 | W_PW_LINENUM_CMD | PW 線數 | CmdInfo.java:175 |
| 73 | W_PW_QOPT_CMD | PW Q 優化 | CmdInfo.java:177 |
| 74 | W_PW_POSTEFFECT_CMD | PW 後處理 | CmdInfo.java:176 |
| 75 | W_PW_ACSPWR_CMD | PW 聲功率 | CmdInfo.java:161 |
| 76 | W_EXAMMODE_CMD | 檢查模式 | CmdInfo.java:124 |
| 77 | W_B_CMD | 切換至 B 模式 | CmdInfo.java:87 |
| 78 | W_C_CMD | 切換至 C 模式 | CmdInfo.java:107 |
| 79 | W_M_CMD | 切換至 M 模式 | CmdInfo.java:131 |
| 80 | W_POWER_CMD | 切換至 Power 模式 | CmdInfo.java:144 |
| 81 | W_PW_CMD | 切換至 PW 模式 | CmdInfo.java:165 |
| 82 | W_UPDATE_CMD | 更新 | CmdInfo.java:194 |
| 83 | W_ALL_PARAM_CMD | 讀取全部參數 | CmdInfo.java:85 |
| 84 | W_NOFPGADSC_CMD | 無 FPGA DSC | CmdInfo.java:141 |
| 85 | W_SAVE_PARAM_CMD | 儲存參數 | CmdInfo.java:186 |
| 86 | W_EXAMIMGPARA_CMD | 檢查影像參數 | CmdInfo.java:123 |
| 87 | W_UIMSG_CMD | UI 訊息 | CmdInfo.java:193 |
| 88 | W_C_DISP_LINEANDDOT_CMD | C 顯示線與點 | CmdInfo.java:108 |
| 89 | W_PWB_B_CMD | PW/B 切換 | CmdInfo.java:158 |
| 90 | W_PWC_C_CMD | PW/C 切換 | CmdInfo.java:159 |
| 91 | W_PWC_POWER_CMD | PW/Power 切換 | CmdInfo.java:160 |
| 92 | W_PWBC_PW_CMD | PW 切換 | CmdInfo.java:157 |
| 93 | W_UPDATE_IMGMODE_CMD | 更新影像模式 | CmdInfo.java:195 |
| 94 | W_MITI_CMD | MITI | CmdInfo.java:129 |
| 95 | W_IMGQUALITY_CMD | 影像品質 | CmdInfo.java:128 |
| 96 | W_ENCMODE_CMD | 編碼模式 | CmdInfo.java:122 |
| 97 | W_CONNECT_TYPE_CMD | 連線型態 | CmdInfo.java:104 |
| 98 | W_DICOM_SWITCH_CMD | DICOM 開關 | CmdInfo.java:121 |
| 99 | W_PROBEDID_MAPFLAG_CMD | 探頭 DID 映射旗標 | CmdInfo.java:154 |
| 100 | W_VET_CMD | 獸醫(Vet)模式 | CmdInfo.java:196 |
| 101 | W_EXAMMODE_ID_CMD | 檢查模式 ID | CmdInfo.java:125 |
| 102 | W_BFGATHER_CMD | B 幀收集 | CmdInfo.java:86 |
| 103 | W_SEARCH_HOST_CMD | 搜尋主機(發現廣播) | CmdInfo.java:187 |
| 104 | W_CONNECT_HOST_CMD | 連線主機 | CmdInfo.java:103 |
| 105 | W_C_APOWER_CMD | C 聲功率 | CmdInfo.java:105 |
| 106 | W_M_APOWER_CMD | M 聲功率 | CmdInfo.java:130 |
| 107 | W_START_PLAY | 開始播放 | CmdInfo.java:188 |
| 108 | W_PRE_FRAME | 上一幀 | CmdInfo.java:153 |
| 109 | W_NEXT_FRAME | 下一幀 | CmdInfo.java:140 |
| 110 | W_PW_CW_CMD | PW/CW | CmdInfo.java:168 |
| 127 | W_NONE_CMD | 空命令(回應/ack) | CmdInfo.java:142 |
4.3 其它 ID 群組
| 群組 | 值 | 意義 | 證據 |
|---|---|---|---|
BtnId.W_BTN_1..10_ID |
1–10 | 探頭按鍵編號 | CmdInfo.java:203-212 |
BtnAction.W_BTN_DOWN/HOLD/LONG_PRESS/UP |
1/2/3/4 | 按鍵動作 | CmdInfo.java:218-226 |
PollingID.P_IMAGE_RUN_CMD |
128 | 影像執行輪詢 | CmdInfo.java:231 |
PollingID.USB_CONNECTED_CMD |
129 | USB 已連線 | CmdInfo.java:232 |
PollingID.USB_DISCONNECTED_CMD |
130 | USB 已斷線 | CmdInfo.java:233 |
PollingID.PROBE_CONTINUE_TEST_CMD |
131 | 探頭連續測試 | CmdInfo.java:230 |
PollingID.WLAN_CONNECTED_CMD |
132 | WLAN 已連線 | CmdInfo.java:234 |
PollingID.WLAN_DISCONNECT_CMD |
133 | WLAN 已斷線 | CmdInfo.java:235 |
PollingID.IMAGE_SYNC_CMD |
134 | 影像同步 | CmdInfo.java:229 |
AppMsgId.WUSB_SET_PROBE_ID |
240 | 設定探頭 ID | CmdInfo.java:242 |
五、Java 端處理器映射 (Handler → Code Mapping)
核心接收處理器為 WlanProbe.mHandler.handleMessage() (WlanProbe.java:712-1332)。收到 what==1 訊息後輪詢 CmdHandler.mNetDataQueue,依 cmdElem.cmdid 分派:
| 代碼 | 無號值 | 處理動作 | 證據 |
|---|---|---|---|
| -115 | 141 | mProbeFwVerObservable(韌體版本) |
WlanProbe.java:742-745 |
| -114 | 142 | mWifiFwVerObservable(Wi-Fi 韌體版本) |
WlanProbe.java:746-749 |
| -107 | 149 | mProbeSnObservable(序號) |
WlanProbe.java:750-754 |
| -106 | 150 | m_ProductIdStr / mProductIdObservable(產品 ID) |
WlanProbe.java:1324-1326 |
| -101 | 155 | mProbeBoardVerObservable(板版本) |
WlanProbe.java:773-776 |
| -100 | 156 | mBoardSnObservable(板序號) |
WlanProbe.java:777-780 |
| -95 | 161 | SetNoExpand(Data[0]&1)(專案選項旗標) |
WlanProbe.java:1315-1319 |
| -94 | 162 | LianmedSendPacket()(聯影資料) |
WlanProbe.java:1313 |
| -112 | 144 | mCurChannel(Wi-Fi 通道) |
WlanProbe.java:784-789 |
| -111 | 145 | mProbePidVerObservable(PID 版本) |
WlanProbe.java:790-794 |
| -110 | 146 | mProbeFpgaVerObservable(FPGA 版本) |
WlanProbe.java:795-799 |
| -103 | 153 | mSeHost(啟用第二主機) |
WlanProbe.java:803-811 |
| -86 | 170 | mProbePowerSaveTime(省電時間) |
WlanProbe.java:812-816 |
| -83 | 173 | mUseTime(使用剩餘時間,≤5 提示過期) |
WlanProbe.java:817-829 |
| -64 | 192 | mTemperature / mTempObservable(溫度) |
WlanProbe.java:1189-1199 |
| -63 | 193 | mBattLevel / mBattLevelObservable(電池) |
WlanProbe.java:1201-1212 |
| -62 | 194 | mProbeId = CalProbeId()(ADC 探頭 ID) |
WlanProbe.java:1213-1236 |
| -59 | 197 | mProbeId = Data[0]-48(探頭 ID) |
WlanProbe.java:830-846 |
| -56 | 200 | mDualProbeId0/1、mDualActive(雙探頭) |
WlanProbe.java:847-895 |
| -16 | 240 | mProbeId(設定探頭 ID) |
WlanProbe.java:896-903 |
| 2 | 2 | Freeze()/UnFreeze()(凍結) |
WlanProbe.java:757-766 |
| 4 | 4 | SetDepth()(深度) |
WlanProbe.java:1321 |
| 6 | 6 | SetBGain() |
WlanProbe.java:769-770 |
| 7 | 7 | SetBFreq() |
WlanProbe.java:771-772, 904-905 |
| 8 | 8 | SetTsi() |
WlanProbe.java:907-908 |
| 9 | 9 | SetAPower() |
WlanProbe.java:910-911 |
| 10 | 10 | SetBDynamic() |
WlanProbe.java:913-914 |
| 11 | 11 | SetBExpand() |
WlanProbe.java:916-917 |
| 12 | 12 | SetBFocus() |
WlanProbe.java:919-920 |
| 13 | 13 | SetBEnhance() |
WlanProbe.java:922-923 |
| 14 | 14 | SetBFrameComp() |
WlanProbe.java:925-926 |
| 15 | 15 | SetBSteer() |
WlanProbe.java:928-929 |
| 16 | 16 | SetBFrameCorre() |
WlanProbe.java:931-932 |
| 17 | 17 | SetBGrayMap() |
WlanProbe.java:934-935 |
| 18 | 18 | setBLR() |
WlanProbe.java:937-938 |
| 19 | 19 | setBUD() |
WlanProbe.java:940-941 |
| 20 | 20 | SetBColorMap() |
WlanProbe.java:943-944 |
| 25–36 | 25–36 | SetCGain/Scale/Invert/Freq/WallFilter/…/Steer/LineRange/DotRange |
WlanProbe.java:1239-1278 |
| 43 | 43 | SetPowerColorMap() |
WlanProbe.java:946-947 |
| 45–47 | 45–47 | SetMGain/MSpeed/MDynamic |
WlanProbe.java:1281-1289 |
| 49 | 49 | SetMGrayMap() |
WlanProbe.java:949-950 |
| 52–54 | 52–54 | SetMDispLine/PwBaseLine/PwSpeed |
WlanProbe.java:1292-1301 |
| 56 | 56 | SetPwAngle() |
WlanProbe.java:952-956 |
| 59 | 59 | SetPwReserse() |
WlanProbe.java:957-958 |
| 61–73 | 61–73 | SetPwVolume/Dynamic/Steer/Freq/Scale/Gain/WallFilter/GateSize/GatePos/DispLine |
WlanProbe.java:960-999 |
| 75 | 75 | SetAPower() |
WlanProbe.java:1000-1001 |
| 76 | 76 | SetExamType() |
WlanProbe.java:1003-1005 |
| 77 | 77 | SetDispMode(0) |
WlanProbe.java:1006-1009 |
| 78 | 78 | SetDispMode(16/8) |
WlanProbe.java:1010-1016 |
| 79 | 79 | SetDispMode(9) |
WlanProbe.java:1017-1019 |
| 80 | 80 | SetDispMode(16/55) |
WlanProbe.java:1020-1026 |
| 82 | 82 | SetDispMode(5)(更新) |
WlanProbe.java:1027-1039 |
| 86 | 86 | SetPresetParam() |
WlanProbe.java:1040-1042 |
| 89–92 | 89–92 | SetDispMode(57/58/59/5) |
WlanProbe.java:1043-1054 |
| 94 | 94 | setMITISData() |
WlanProbe.java:1055-1057 |
| 96 | 96 | mEncModeObservable(編碼模式) |
WlanProbe.java:1058-1063 |
| 98 | 98 | SetDicomAuth/PwAuth/ColorAuth/HumanVetAuth/OEMId |
WlanProbe.java:1064-1105 |
| 100 | 100 | mVet / mProbeDisabled(獸醫模式) |
WlanProbe.java:1106-1162 |
| 101 | 101 | SetExamType() |
WlanProbe.java:1163-1165 |
| 103 | 103 | UpdatetoHostList()(主機發現) |
WlanProbe.java:1166-1170 |
| 104 | 104 | WlanHost.setConnect()(主機連線) |
WlanProbe.java:1171-1186 |
六、多主機握手 (Multi-Host Handshake)
- 偵測第二主機:
TcpSocketConnect()檢查本機 IP 末位== 199時呼叫SetMultiHost(2)(CmdHandler.java:156-160)。 - 啟用訊號:收到代碼 153 (
D_ENABLE_SECOND_HOST) 設定mSeHost(WlanProbe.java:803-811)。 - 轉送:
MultiHostSendCmd()以目標2呼叫excute(..., 2)→wlanSendToHost2()(WlanProbe.java:1444-1477;CmdHandler.java:91-92, 688-702)。wlanSendToHost2把封包重新包裝為cmdid=-54(=202D_SEND_HOST_2)後送至REMOTE_PORT(CmdHandler.java:690-701)。 - 主機角色:
WlanClient.SetWlanRole(0/1/2)= NONE/CLIENT/HOST (WlanClient.java:26-28)。
七、主機發現與連線握手(WLAN_PORT 8093)
- 主機廣播:
WlanHost.UpateParam()以 dir=3 (PACK) 廣播代碼 103 (W_SEARCH_HOST_CMD),payload 為 JSON 參數(JsonParam:sn/ip/name/id/ex_name/ex_id/img_mode/w/h/shape/depth/scale/angle/rang)(WlanHost.java:144-188;JsonParam.java:8-22)。 - 客戶端接收:
WlanClient.StartBroadCastRecv()監聽 8093,代碼 103 →UpdatetoHostList()(WlanClient.java:156-189, 203-207)。 - 連線:客戶端送代碼 104 (
W_CONNECT_HOST_CMD) 帶本機 IP;主機回代碼 104 設WlanHost.setConnect()(WlanClient.java:66-102;WlanProbe.java:1166-1186)。 - 斷線:代碼 104 payload 為單字節
"0"(WlanClient.java:71)。 - 心跳:代碼 3 (
W_POLLING_CMD) 用作 keepalive (WlanHost.java:180)。
八、配對握手 (Pairing Handshake) — CmdHandler.StartWlanPair() (CmdHandler.java:325-456)
- 以 UDP (LOCAL_DAT_PORT 8083) 收探頭回應,超時 8 秒 (
StartWlanPairTimer)。 - 送出代碼 150 (
D_UPROBE_PRODUCTID) 請求產品 ID (CmdHandler.java:384)。 - 收到回應後回傳封包:
0xFE, 150, 2, 1, 0, VR_GET_EEPROM_REG3(=85), 0xFF(CmdHandler.java:424-431;HSUsbProbe.java:49)。
九、FPGA 暫存器讀寫 (UReadFPGA/UWriteFPGA)
- 位址:代碼 147 (
D_UPROBE_FPGA_ADDR) 設 4-byte little-endian 位址 (CmdHandler.java:747-754)。 - 資料:代碼 148 (
D_UPROBE_FPGA_DAT) 讀/寫資料;dir=1 讀、dir=0 寫 (CmdHandler.java:760, 847)。 - 大批量資料以 1400-byte 區塊分片(
mData[3]=120, mData[4]=5表示 1400)(CmdHandler.java:767-769)。 - TCP 版 (
UReadFPGA_TCP/UWriteFPGA_TCP) 加 2-byte 長度前綴 (CmdHandler.java:913-1031)。
十、已知缺口 (Known Gaps)
- 未定義代碼區間:D_* 表在 174–191 之間無常數定義(
D_BOARD_TEMPERATURE=192起才接續);190、191亦無定義。 - 無 checksum:
varify()為空實作,封包完整性僅靠0xFF結束字節。 - 傳送/接收框架不對稱:傳送端含
0xFE頭,WLAN UDP 接收端卻從[0]起讀 cmdid(無0xFE頭),僅 USB 通道與 TCP 通道掃描0xFE。 - 方向語意待確認:多數 D_* GET 代碼的「確切回傳方向」未在反編譯碼中明示,僅由
SendGetCmdToProbe呼叫點推斷(Ultrasys.java:2765-2799)。 - 代碼 190/191、174-189 語意未知:反編譯碼中無對應常數或處理器。
W_UPDATE_CMD(82)、W_UIMSG_CMD(87)、W_NOFPGADSC_CMD(84)、W_BFGATHER_CMD(102)、W_PROBEDID_MAPFLAG_CMD(99) 等:僅有常數定義,未見明確的 Java 端接收處理器。HSUsbProbe暫存器碼(VR_* 常數,HSUsbProbe.java:35-62)屬 USB 協定層,與 WLAN 命令碼為不同命名空間,此處僅列出與配對握手相關者。
深挖 3 — ThirdParty SDK
ThirdPartyManageActivity — 第三方 SDK 整合介面 (Third-Party SDK Integration Interface)
來源(jadx 反編譯): -com.Com.handprobe.ThirdPartyManageActivity.java(exported activity,AndroidManifest.xml 第 50 行) -com.Com.application.MyApplication.java(returnThirdParty/getThirdPartyExtras/mIsThirdPartyEntered) -handprobe.application.app.AppProc.java(影像輸出 + 回傳結果觸發) -com.Com.application.ReportFragment.java(PDF 輸出) -com.Com.handprobe.BaseActivity.java、LandFeature.LandActivity.java(病患注入、exit 回傳)
一、定位與角色 (Role)
ThirdPartyManageActivity 是一個 exported="true"、無 intent-filter、無權限保護 的透明 Activity(@android:style/Theme.Translucent.NoTitleBar)。它的作用是扮演一個 「轉送閘道 (relay)」:第三方 App 以 explicit component 呼叫它,它把第三方帶來的 extras 轉送給主 App(BaseActivity/LandActivity 或 SplashActivity),並在主 App 完成工作後以 setResult() 把結果送回第三方,再把第三方 App 帶回前景。
它本身不做任何超音波工作,邏輯極短(約 130 行),真正的輸出邏輯分散在 AppProc(影像)與 ReportFragment(報告 PDF)。
二、啟動協定 (Launch Protocol)
2.1 Component
Manifest(第 50 行)只宣告了 activity,沒有 :
<activity android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
android:exported="true"
android:name="com.Com.handprobe.ThirdPartyManageActivity"
android:screenOrientation="sensor"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
因此沒有 action / category 可以靠 implicit intent 觸發。第三方必須用 explicit component 啟動:
- 套件名:
com.SonoiQ.uhandprobe - 元件:
com.SonoiQ.uhandprobe/com.Com.handprobe.ThirdPartyManageActivity
Intent i = new Intent();
i.setComponent(new ComponentName("com.SonoiQ.uhandprobe",
"com.Com.handprobe.ThirdPartyManageActivity"));
i.putExtra("APP_NAME", "MyApp");
i.putExtra("SAVEIMG_PATH", "/sdcard/myapp/images/");
i.putExtra("PATIENTID", "P001");
i.putExtra("NAME", "John Doe");
i.putExtra("GENDER", 1);
i.putExtra("AGE", 35);
i.putExtra("EXPORT_MODE", "jpeg");
startActivityForResult(i, REQ_CODE);
已確認:exported="true"且無 intent-filter(Manifest 第 50 行)。 推斷(高度確信):第三方需以startActivityForResult+ explicit component 啟動,因為代碼依賴getCallingActivity()/getCallingPackage()與setResult()的回傳機制——這些 API 只有在以startActivityForResult啟動、且在同一 task 返回棧的條件下才有非 null 值。
三、Intent Extras 協定 (Intent Extras Protocol)
3.1 輸入鍵 (Input Keys)
ThirdPartyManageActivity.onCreate 讀取以下鍵,存入 public static Map mArgsMap(HashMap,全行程共享):
| Key | 型別 | 讀取位置 | 語意 |
|---|---|---|---|
APP_NAME |
String | getString |
第三方 App 名稱(記錄用途,未見後續消費) |
SAVEIMG_PATH |
String | getString |
第三方指定輸出目錄;App 會把超音波影像 PNG 與報告 PDF 直接檔案複製 到這個目錄 |
PATIENTID |
String | getString |
病患 ID;空字串 → 不注入病患;非空 → 建 HPatient 並注入 |
NAME |
String | getString |
病患姓名(setFirstName) |
GENDER |
int | getInt |
病患性別(setGender,無 default → 缺省為 0) |
AGE |
int | getInt |
病患年齡(setAge(age, 0),缺省為 0) |
EXPORT_MODE |
String | AppProc(非 onCreate) |
回傳結果格式:"jpeg"/"jpg"/"webp"/其他 → 決定 IMG_PATH 指向的檔案格式 |
EXPORT_MODE 與輸出鍵 IMG_PATH 是 ThirdPartyManageActivity 的 public static 常數("EXPORT_MODE"、"IMG_PATH"),但不在 onCreate 讀取——它們在 extras 被轉送後由 AppProc 消費。
3.2 轉送 (Forwarding)
Bundle extras = getIntent().getExtras();
if (extras != null) {
intent.putExtras(extras); // 原封不動轉送給 MainActivity/SplashActivity
// ...解析並填入 mArgsMap、建 mThirdPartPatient...
startActivity(intent);
}
extras 被 putExtras 轉送到主 Activity,且 getThirdPartyExtras() 回傳的其實就是 同一個 bundle 物件(mTPManageActivity.getIntent().getExtras())。這導致回傳結果與輸入共用同一 bundle 記憶體(見第四節)。
已確認:mArgsMap為public static Map,在onCreate每次clear()後重填。全行程共享 → 若主 App 已存在、第三方再次進入,靜態 map 會被新呼叫覆寫。 推斷:APP_NAME僅作記錄;PATIENTID為必填鍵——若 extras 存在但缺PATIENTID,mArgsMap.get("PATIENTID").equals("")會拋NullPointerException。
四、生命週期 (Lifecycle)
4.1 Activity 本身
ThirdPartyManageActivity 是透明、瞬時的:
onCreate:KeepFullScreen→ 決定轉送目標 → 解析 extras。
- extras 存在 →
startActivity(intent)轉送給主 App,return(不 finish,留在返回棧當結果中繼)。 - extras 為 null → 直接回傳
ERROR_Intent_has_no_extras並finish()。
onNewIntent:僅setIntent(intent)(支援 singleTop/singleTask 重入更新 extras)。onActivityDestroyed(Application 生命週期回調):mTPManageActivity = null、mIsThirdPartyEntered = false。
4.2 Application 層狀態追蹤 (MyApplication)
MyApplication 用 ActivityLifecycleCallbacks 追蹤第三方狀態:
onActivityCreated/onActivityStarted(命中ThirdPartyManageActivity)→ 記錄mTPManageActivity、mIsThirdPartyEntered = true;並在onActivityCreated記錄mCalledByComponentName = new ComponentName(getCallingPackage(), getCallingActivity().getClassName())。onActivityDestroyed→ 清除狀態、mIsThirdPartyEntered = false。
4.3 病患注入 (Patient Injection)
第三方提供的 PATIENTID/NAME/GENDER/AGE 在 LandActivity.UpdatePatientThird() 中生效(需 WRITE_EXTERNAL_STORAGE 權限,ISGetPremission):
if (mTPManageActivity.isPatientUpdate() && ISGetPremission) {
HPatient p = mTPManageActivity.updatePatient(); // 只回傳一次(mPatientUpdate 會被清為 false)
if (p == null) AddNewPatient();
else AddNewPatient(p.getId()) ... setFirstName/Gender/Age/Birthday;
// SetCurPatient + NewExam + SaveToDb
}
updatePatient()有一次性語意:讀取後把mPatientUpdate設回false。
五、回傳結果機制 (Return-Result Mechanism)
5.1 進入點
有兩個重載:
ThirdPartyManageActivity.returnThirdParty(Intent intent, Bundle bundle)— 實際執行setResult+finish+ 喚回第三方。MyApplication.returnThirdParty(Bundle bundle)— 包裝:ProbeWifiInfo.DisconnectWifi()→ 主 ActivitymoveTaskToBack(true)+setIntent(null)→ 委派給mTPManageActivity.returnThirdParty。
5.2 setResult 邏輯 (確認自反編譯)
public void returnThirdParty(Intent intent, Bundle bundle) {
if (bundle != null) {
if (bundle.getString(IMG_PATH) != null) {
intent.putExtras(bundle);
setResult(-1, intent); // RESULT_OK —— 成功
} else if (bundle.getInt(ERROR) <= 0 && bundle.getString(ERROR) == null) {
bundle.putString(ERROR, "Not save image.");
bundle.putInt(ERROR, 3);
intent.putExtras(bundle);
setResult(0, intent); // RESULT_CANCELED —— 未存影像
} else {
intent.putExtras(bundle);
setResult(0, intent); // RESULT_CANCELED —— 已有錯誤碼
}
} else {
Bundle b = new Bundle();
b.putString("error", "Unknown error.");
b.putInt(ERROR, 1);
intent.putExtras(b);
setResult(0, intent); // RESULT_CANCELED —— 未知錯誤
}
finish();
setIntent(null);
if (mCalledByComponentName != null) {
intent.setComponent(mCalledByComponentName);
startActivity(intent); // 把第三方 App 帶回前景
}
}
注意第三個分支的 bug 語意:bundle.getString(ERROR) == null但bundle.getInt(ERROR) > 0(例如只放了 int 錯誤碼、沒放字串)會落入最後分支,直接setResult(0)原樣回傳 bundle。 注意錯誤字串用了兩個不同 key:正常路徑用ERROR("ERROR"),null-bundle 分支誤用"error"(小寫)——已確認是源碼不一致。
5.3 觸發回傳的時機 (Call Sites)
| 觸發點 | 位置 | 行為 |
|---|---|---|
影像存檔完成且 EXPORT_MODE 存在 |
AppProc ~312–333 |
依 EXPORT_MODE 轉檔(jpeg/jpg/webp)或沿用 PNG,設 IMG_PATH → returnThirdParty |
功能鍵「返回」 (FUNCID_Return_to_Lian, case 205) |
AppProc ~824 |
非 Lianmed 時 returnThirdParty(getThirdPartyExtras()) |
連續兩次 back (exit()) |
BaseActivity.exit ~866 |
getThirdPartyExtras() != null → returnThirdParty |
| 全域未捕捉例外 (crash) | MyApplication uncaught handler ~153–158 |
設 ERROR="App crashed." + code 4 → returnThirdParty |
getThirdPartyExtras()回傳 null 的條件:mTPManageActivity == null || !mIsThirdPartyEntered || getIntent() == null。因此回傳只在第三方確曾進入時發生。
六、錯誤碼 (Error Codes)
ThirdPartyManageActivity 定義的常數(已確認):
| 常數 | 值 | 字串 | 觸發條件 |
|---|---|---|---|
ERROR_Reserved |
0 | (保留) | — |
ERROR_Unknown |
1 | "Unknown error."(用 key "error") |
bundle 為 null |
ERROR_Intent_has_no_extras |
2 | "Intent has no extras." |
啟動時 getExtras() == null |
ERROR_Not_save_image |
3 | "Not save image." |
回傳時無 IMG_PATH 且無錯誤碼 |
ERROR_App_crashed |
4 | "App crashed." |
全域 uncaught exception handler |
回傳的 result code:
-1(RESULT_OK):僅當IMG_PATH存在(成功)。0(RESULT_CANCELED):所有錯誤路徑。
錯誤碼與字串都放在回傳 Intent 的 extras 裡,key 皆為 ERROR(值 "ERROR")——唯 null-bundle 分支誤用 "error"(見 5.2 註記)。
七、第三方能拿到什麼 (What the Third Party Gets Back)
有 兩條獨立輸出通道,互補:
7.1 SAVEIMG_PATH(檔案複製通道,被動持續輸出)
- 影像:
AppProc每次存 PNG 影像後,FileUtils.fileCopyByChannel複製一份到SAVEIMG_PATH + mFilename + ".png"(FileDefinition.PNG_SUFFIX)。 - 報告 PDF:
ReportFragment產生 PDF 後複製到SAVEIMG_PATH + createReportName(mExam) + ".pdf"。
第三方指定的是目錄,App 直接寫入多個檔案,與 Intent 回傳無關。
7.2 EXPORT_MODE + IMG_PATH(Intent 回傳通道,主動單次輸出)
- 第三方設
EXPORT_MODE後,App 存檔時依格式轉檔並把 單一檔案的絕對路徑 寫入IMG_PATH,再setResult(-1)。 - 格式對應:
"jpeg"/"jpg"→ JPEG"webp"→ WEBP- 其他(或不設,但走了回傳路徑)→ 沿用 PNG(
file2.getAbsolutePath()) - 第三方在
onActivityResult中讀:
if (resultCode == RESULT_OK && data != null) {
String path = data.getStringExtra("IMG_PATH"); // 絕對路徑
}
結論:第三方拿到的不是圖片位元組,而是檔案絕對路徑(IMG_PATH),以及透過 SAVEIMG_PATH 目錄被動收到的 PNG 影像 + PDF 報告副本。
已確認:IMG_PATH存的是絕對路徑(file.getAbsolutePath());setResult(-1)只在IMG_PATH != null時發生。 推斷:若第三方未設EXPORT_MODE,App 不會在存檔時主動回傳;IMG_PATH永遠為 null,回傳(如按返回鍵或 exit)會落入「Not save image」錯誤分支。若要拿到影像路徑,EXPORT_MODE是必要輸入。
八、安全性分析 (Security Notes)
ThirdPartyManageActivity 是 SonoiQ 上 攻擊面最大的元件:exported、無 intent-filter 限制、無權限、無簽章/白名單驗證。
- 任意第三方可啟動:
exported="true"且無任何android:permission保護;任何同裝置 App 都能用 explicit component 觸發。 - 無呼叫者身分驗證:
getCallingActivity()/getCallingPackage()僅用於「事後把第三方帶回前景」,從不驗證呼叫者簽章、套件名或授權。 - 病患 PHI 注入:呼叫者可控
PATIENTID/NAME/GENDER/AGE,透過UpdatePatientThird()直接寫入 SonoiQ 的病患資料庫(SaveToDb)——資料完整性風險(可植入偽病患紀錄)。 - 資料外洩 (exfiltration) 通道:
SAVEIMG_PATH由第三方指定,App 會把含病患資訊的超音波影像與報告 PDF 複製到第三方可控目錄(App 有WRITE_EXTERNAL_STORAGE+requestLegacyExternalStorage="true",可寫共享外部儲存),第三方再讀取該目錄即可取走 PHI。IMG_PATH直接回傳 App 內部檔案的絕對路徑(路徑洩漏 + 若檔案在可讀共用區則等同內容洩漏)。
- 靜態可變狀態:
mArgsMap為public static、無同步;多第三方/多實例共用,存在跨呼叫污染與競態。 - crash 回報洩漏:全域 uncaught handler 會把
"App crashed."(code 4)連同原本 extras 回傳給第三方,並AppUtil.exitApp(1)強制結束 App——第三方可用惡意 extras 反覆觸發 crash(例如缺PATIENTID的 NPE,見 3.2 推斷)。 - 喚回第三方(foreground re-entry):回傳後 App 會
startActivity(mCalledByComponentName),等於把控制權交還給第三方;若第三方本身就是惡意 App,此機制使它得以在主 App 結束後立即重新取得前景。
相較於其他 activity,LandQ6Activity/LandQ62Activity帶有android:sharedUserId="android.uid.system"(第 48–49 行,本身也是可疑的越權標記),但ThirdPartyManageActivity不帶 sharedUserId,運行於 App 一般 UID。
九、已確認 vs 推斷 (Confirmed vs Inferred)
已確認(直接源自反編譯源碼/Manifest):
ThirdPartyManageActivityexported、無 intent-filter、透明主題。- 所有 input key(
APP_NAME/SAVEIMG_PATH/PATIENTID/NAME/GENDER/AGE/EXPORT_MODE)與 output key(IMG_PATH/ERROR)。 - 錯誤碼常數 0–4 與對應字串。
setResult的完整分支邏輯與 result code(-1/0)。mArgsMap為public static,onCreate 中clear()重填。- 兩條輸出通道(
SAVEIMG_PATH檔案複製、EXPORT_MODE→IMG_PATH回傳)的具體行為。 - 病患注入流程(
UpdatePatientThird→AddNewPatient→SaveToDb)。
推斷(邏輯推導,未直接實測):
- 第三方需以 explicit component +
startActivityForResult啟動(因無 intent-filter,且代碼依賴getCallingActivity/setResult)。 PATIENTID為實質必填——缺省會 NPE(mArgsMap.get("PATIENTID").equals(""))。GENDER/AGE缺省為 0(getInt無 default)。- 不設
EXPORT_MODE時不會有主動回傳、IMG_PATH恒為 null。 APP_NAME僅作記錄用途,未見下游消費。
深挖 4 — DispMode 值
SonoiQ DispMode 特殊值解讀 (Special DispMode Values)
反編譯來源:jadx_out/sources/(JADX 反編譯的classes.dex)。 核心類別:handprobe/application/ultrasys/image/DispMode.java、ImageDefine.java、Ultrasys.java、handprobe/application/usb/UsbProbeManager.java、handprobe/application/ultrasys/ParaCtrl.java。
一、常數定義總表 (Constants Definition)
DispMode.java:7-20 定義了完整的模式常數。模式值並非連續,中間留有空位(保留給硬體
scan mode 對應的 ID),因此值本身即帶有語意(見第五節)。
| 值 | 常數名 (Constant) | 字面含意 | 中文含意 |
|---|---|---|---|
| 0 | B_MODE |
Brightness (2D grayscale) | B 模式(灰階二維) |
| 1 | M_MODE |
Motion | M 模式(時間-運動曲線) |
| 5 | PW_MODE |
Pulsed-Wave Doppler | PW 脈衝波都卜勒 |
| 8 | BC_MODE |
B + Color | B + 彩色都卜勒(C)複合 |
| 9 | BM_MODE |
B + M | B + M 模式複合 |
| 16 | TWO_BC_MODE |
Dual B + Color | 雙幅 B+彩色 |
| 17 | TWO_B_POWER |
Dual B + Power | 雙幅 B+能量都卜勒 |
| 50 | BB_MODE |
B + B (bi-plane) | 雙 B(雙平面) |
| 51 | BBBB_MODE |
B+B+B+B (4-view) | 四 B(四分割) |
| 55 | BPOWER_MODE |
B + Power Doppler | B + 能量都卜勒複合 |
| 57 | PWB_B_MODE |
PW over B | PW 頻譜疊加於 B 圖 |
| 58 | PWC_C_MODE |
PW over Color | PW 頻譜疊加於彩色圖 |
| 59 | PWC_POWER_MODE |
PW over Power | PW 頻譜疊加於能量圖 |
二、模式 → 意義 → 資料路徑對照表 (Mode → Meaning → Data Path)
資料路徑的硬體事實 (FPGA 位址與尺寸)
UsbProbeManager.java:42-66:
BIMG_SIZE = 131072 // B 影像一幀大小
CIMG_SIZE = 131072 // C(彩色)影像一幀大小
FPGA_BIMG_ADDR = 131072 // B 影像在 FPGA 的記憶體位址
FPGA_CIMG_ADDR = 131072 // C 影像位址(與 B 同值,分時/分區使用)
FPGA_H264_ADDR = 262144
FPGA_TIMG_ADDR = 393216 // "T 影像":PW 頻譜 / M 軌跡資料區
MIMG_SIZE = 8192 // M 影像
MIMG_PACK_SIZE = 2048 // M 每包 2048B
PWIMG_SIZE = 2048 // PW 頻譜每幀 2048B
TIMG_SIZE = 10240 // T 影像(M)總量 10240B
USB 讀取分派邏輯 UsbProbeManager.java:256-305(InitThread 內的 ImgReadUSBThread):
CheckImgUpload()回傳 bit0=1 → 讀 B 影像131072bytes(UsbProbeManager.java:240-241)- bit1=1 → 依
GetDispMode()分派讀取第二路資料(下表)。
對照表
| 值 | 意義 | USB 資料路徑 (evidence) | Cine page size (DispMode.SetCine) |
SetScanMode (Ultrasys.SetDispMode) |
||
|---|---|---|---|---|---|---|
| 0 | B 灰階 | bit0:FPGA_BIMG_ADDR 讀 131072B → mImgBuf(UsbProbeManager.java:240-241) |
131072(雙平面 262144)DispMode.java:110-119 |
SetScanMode(0) Ultrasys.java:1435 |
||
| 5 | 純 PW 都卜勒 | bit1:FPGA_TIMG_ADDR 讀 2048B → mTImgBuf → mUsbPwDataQueue → PackD(UsbProbeManager.java:258-270, 372-394) |
換 PW 模擬 buffer,不設 mLastMode DispMode.java:106-108 |
SetScanMode(5) Ultrasys.java:1430 |
||
| 8 | B+彩色 (BC) | bit1:FPGA_CIMG_ADDR(131072) 讀 131072B → mCImgBuf → 拷入 mImgBuf[131072..](UsbProbeManager.java:271-278) |
C_PAGE_SIZE(見下)DispMode.java:88-97 |
SetScanMode(8) Ultrasys.java:1422 |
||
| 9 | B+M (BM) | bit1:FPGA_TIMG_ADDR 讀 10240B → mMImgBufCache,拆 4×2048 → mTImgBuf → PackM(UsbProbeManager.java:279-296, 387-394) |
131072(BM 模擬 buffer)DispMode.java:77-86 |
SetScanMode(9) Ultrasys.java:1390 |
||
| 16 | 雙幅 B+彩色 | 同 8(C 影像路徑) | 未在 SetCine 明確處理(落入空 switch) |
SetScanMode(8) |
||
| 17 | 雙幅 B+能量 | 同 8(C 影像路徑) | 未在 SetCine 明確處理 |
SetScanMode(8) |
||
| 50 | 雙 B | bit0 B 影像(雙平面各自讀取) | 未在 SetCine 明確處理 |
SetScanMode(8) |
||
| 51 | 四 B | bit0 B 影像(四分割) | 未在 SetCine 明確處理 |
SetScanMode(8) |
||
| 55 | B+能量 (BPower) | bit1:同 8,FPGA_CIMG_ADDR 讀 131072B(UsbProbeManager.java:271-278,`== 55 \ |
\ | == 8`) | C_PAGE_SIZE DispMode.java:99-104 |
SetScanMode(8) Ultrasys.java:1426 |
| 57 | PW 疊加 B | bit0 B 影像;bit1 無對應讀取分支(見第七節缺口) | 未處理(空 switch) | SetScanMode(0) Ultrasys.java:1400 |
||
| 58 | PW 疊加彩色 | bit1:FPGA_CIMG_ADDR 讀 131072B(UsbProbeManager.java:297-304,`== 58 \ |
\ | == 59`) | 未處理(空 switch) | SetScanMode(8) Ultrasys.java:1405 |
| 59 | PW 疊加能量 | bit1:同 58(C 影像路徑) | 未處理(空 switch) | SetScanMode(55) Ultrasys.java:1410 |
C_PAGE_SIZE定義於Ultrasys.java:342:C_PAGE_SIZE = (ImageDefine.C_M 256) + 262144 = (512256)+262144 = 393216。 巧合的是FPGA_TIMG_ADDR = 393216與C_PAGE_SIZE同值——B+彩色模式整頁 (C 131072 + 其餘 262144)恰等於 T 影像在 FPGA 記憶體中的起始偏移。
三、特殊值 5 / 8 / 9 的語意(單一複合模式)
- 5 (PW):純脈衝波都卜勒。唯一走「頻譜」專用 2048B 資料路徑的模式,
UsbProbeManager.UReadMDImg 用 HH264PlayerIf.PackD 封裝(UsbProbeManager.java:381)。
進入 PW 前若溫度過高會攔截(Ultrasys.java:1362)。SetCine 不更新 mLastMode,
表示 PW 是「暫態」疊加,退出後回上一模式(DispMode.java:106-108;ParaCtrl.java:721-761 的
Pw_lastMode 往返邏輯證實此點)。
- 8 (BC):B + 彩色都卜勒。C 影像以第二路(bit1)上傳,
SetMode時會
ResetDropFrameCount()(DispMode.java:58-60),並以 C_PAGE_SIZE 建立 cine buffer。
- 9 (BM):B + M。M 軌跡資料同樣來自
FPGA_TIMG_ADDR,但量為 10240B(=5 幀×2048),
由獨立執行緒拆包 4×2048(UsbProbeManager.java:283-296),顯示時用 PackM。
四、複合/變體模式為何存在 55 / 57 / 58 / 59
55 = BPOWER(B + 能量都卜勒)
能量都卜勒(Power Doppler)與彩色(Velocity)共用同一 C 影像資料路徑與同一個
SetScanMode(8),差別在 DSP 參數(ParaCtrl.java:699-719 用 UsePowerPara() vs
UseColorPara())。所以 55 是 8 的「能量參數變體」。
57 / 58 / 59 = PW 疊加模式(PW over B / C / Power)
這三個值是「PW 頻譜 + 底圖」的複合顯示模式,證據:
- 命名:
PWB_B_MODE(PW+B)、PWC_C_MODE(PW+C)、PWC_POWER_MODE(PW+Power)。 - UI 觸發(
WlanProbe.java:1043-1051,遠端命令 89/90/91 → 57/58/59)。 - 模式往返邏輯(
ParaCtrl.java:679-682, 702-706, 736-760):
- 在 B(0) 或 BM(9) 下按 PW → 進 57;在 BC(8) 下按 PW → 進 58;在 BPower(55) 下按 PW → 進 59。
- 在 57/58/59 下再按 PW → 回到純 5;再按一次 → 回上一複合模式。
- 底圖對應(
Ultrasys.java:1397-1410的SetScanMode):
- 57 →
SetScanMode(0)(底圖為 B) - 58 →
SetScanMode(8)(底圖為彩色) - 59 →
SetScanMode(55)(底圖為能量,掃描模式 55 = 能量)
- SampleWinView 繪製(
SampleWinView.java:322-331):57 只聚焦 PW sample gate;
58/59 同時繪製彩色 sample box(mCurCSample)與 PW gate(mCurPWSample)——
即「彩色取樣框 + PW 取樣門」並存的複合畫面。
結論:55 是「能量都卜勒」的獨立 DispMode;而 57/58/59 是「PW 疊加」的複合模式,
其底圖分別為 B / 彩色 / 能量。三者的存在是為了讓 PW 頻譜能疊在任一種底圖上,
同時保留一個獨立的值給純能量模式(55)。
五、為何值不連續(55、57、58、59 的來歷)
這些值同時扮演「App 層 DispMode ID」與「硬體 scan mode ID」雙重角色。關鍵證據是
Ultrasys.SetDispMode 尾段(Ultrasys.java:1378-1436)把 DispMode 翻譯成
HSysctrlIf.SetScanMode(...) 的硬體掃描模式編號,其對應為:
| DispMode | → SetScanMode | 硬體掃描語意(推測) |
|---|---|---|
| 0 | 0 | B 掃描 |
| 5 | 5 | 純 PW 掃描 |
| 8/16/17/50/51/55 | 8 | 彩色/能量流 2D 掃描(power 由 DSP 參數決定) |
| 9 | 9 | B+M 掃描 |
| 57 | 0 | B + PW 交錯掃描 |
| 58 | 8 | 彩色 + PW 交錯掃描 |
| 59 | 55 | 能量 + PW 交錯掃描 |
HSysctrlIf.SetScanMode 為 JNI native 方法(jni/HSysctrlIf.java:171),其實際
語意在 native 層,jadx 輸出中不可見。但可推論:硬體 scan-mode 編號空間(0/5/8/9/55)是
App 層 DispMode 值的一個子集,55 是硬體「能量」掃描模式 ID,因此 DispMode 55 與 59
直接沿用了硬體編號;57/58/59 是「PW 交錯」的變體編號(底圖 + PW),55/58/59 這組值
正是「能量/彩色/B」三種底圖的 PW 疊加版本編碼。
六、getFuncid 對照(功能鍵 ID 映射)
DispMode.getFuncid()(DispMode.java:24-52)把模式映射到「功能 ID」(41–45),
供硬體功能鍵使用(LandActivity.java:786 傳給 getProbebtnIndex):
| DispMode | funcid | 功能 |
|---|---|---|
| 0, 50, 51 | 41 | B |
| 8, 16, 17, 58 | 42 | Color (C) |
| 55, 59 | 43 | Power |
| 5 | 44 | PW |
| 1, 9 | 45 | M |
這進一步佐證:58 與 8 同屬彩色功能、59 與 55 同屬能量功能、57 未列出(落入 41 = B)。
七、已知缺口與未解之謎 (Gaps)
M_MODE = 1疑似未使用:全 codebase 未找到SetDispMode(1)呼叫;純 M 模式
在此裝置上實際以 9 (BM) 呈現。1 是定義但未啟用的遺留常數。
- 57 的 PW 頻譜資料路徑不明:USB 讀取分派(
UsbProbeManager.java:256-305)中,
57 不在 bit1 的分派鏈(只有 5 / 55‖8 / 9 / 58‖59),故 57 模式下 PW 頻譜如何
從 FPGA 上傳未在 Java 層交代——可能由 native HH264PlayerIf 或他處處理,需進一步追蹤。
- SetScanMode 的精確硬體語意未知:
HSysctrlIf為 JNI,掃描模式 0/5/8/9/55 對應的
實際聲學掃描序列(尤其 55 = 能量 與 8 = 彩色 在 FPGA 內的差異)無法從 Java 層證實。
- DispMode 55(BPower)為何走
SetScanMode(8)而 59 走SetScanMode(55):
推測為「能量參數可由 DSP 在 scan mode 8 上實現,但 PW+能量交錯需要專屬 scan mode 55」,
但此為推論,未在反編譯碼中直接證實。
- 16/17/50/51(雙幅/四幅)的資料路徑:這些模式在 USB 層只走 bit0(B 影像),
彩色部分(16/17)如何與 B 交錯上傳未有獨立分支,推測由 SetScanMode(8) 的
硬體交錯輸出在同一 B buffer 中完成。
深挖 5 — INF 格式
PID03 .INF 格式解析 (Probe Description .INF Format)
對象:PID03.zip內每探頭型號的pattern0/Normal/<型號>.INF(例:03C5-2Ds.INF,201,837 B) 方法:GBK 解碼 + xxd + 標籤提取。
一、格式定性
自定義「XML 標籤 + 二進制 payload」混合格式:以 GBK 編碼中文 XML 標籤分段,標籤之間是原生二進制數據(float 數組 / 字節表),非標準 XML(無轉義、payload 不可打印)。
<探头一般信息> [0x16 header: 型號名(12B) + 版本(2B) + float 參數... + 日期戳 "20251126-094503"]
</探头一般信息>
<探头图标> ... </探头图标>
<图谱索引> ... </图谱索引>
<频率列表> float[] ... </频率列表>
<声速列表> float[] ... </声速列表>
...
<MI基准参数> ... </MI基准参数>
<TI基准参数> ... </TI基准参数>
開頭實證(hexdump):3c cc bd cd b7 d2 bb b0 ... 3e = GBK <探头一般信息>,0x74 處 探头一般信息> 閉合。
二、65 個標籤全集(GBK 解碼提取,去噪後按功能分組)
| 分組 | 標籤 |
|---|---|
| 基本信息 | 探头一般信息(型號、版本、日期戳 20251126-094503)、探头图标 |
| 幾何/聲學 | 图谱索引、扫描密度、频率列表、声速列表、偏转角度、穿刺 |
| 發射鏈 | B/CD 发射聚焦参数、B/CD 发射波形、发射芯片控制参数 |
| 接收/波束形成 | 变迹相关参数、宽带合成截位数、B/CD 亮度补偿参数、盲区补偿参数、B/CD 模式 GrossTGC、接收芯片控制参数、B/CD 旋转角度表 |
| DSC/降採樣 | B/C/D 降采样滤波器参数、截位控制、IQ复合参数、加权求和参数、焦点复合参数、C矩阵乘法相关参数 |
| 影像後處理 | 增益范围参数、帧相关及线相关参数、对数压缩参数、B图像增强参数(+App)、背景抑制参数、KeyHole参数、彩色编码表、彩色增强参数、C能量对数压缩参数、NDR平滑阈值、速度方差能量帧相关参数 |
| 模式控制 | 扫描时序、B/C/BD 模式扫描控制、M 模式参数、D型成像参数 |
| PW 多普勒 | PW多普勒增益调节、PW壁滤波参数、PW谱平均参数、PW谱压缩参数、PW声音处理参数、PW音频增益系数、PW谱勾边参数、D型勾边平滑参数、PW一键优化参数 |
| 安全指數 | MI基准参数、TI基准参数(機械/熱指數基準——合規必需) |
(少數標籤為二進制數據被 GBK 誤解碼的噪聲,如 #=%吞=潦!,屬正常現象)
三、與 .bin 檔的對應
pattern0/Normal/ 內大量同名 .bin 正是 INF 內各段 payload 的獨立分離版(供 native 按需載入):
03C5-2DsApodizePara.bin ← <变迹相关参数>
03C5-2DsBFTrunctbit.bin ← <宽带合成截位数>
03C5-2DsBlindArea.bin ← <盲区补偿参数>
03C5-2DsB_ImgEnhanceApp.bin ← <B图像增强参数App>(36 KB,最大)
03C5-2DsDSFilterPara_B.bin ← <B降采样滤波器参数>(737 KB,超大 FIR 系數集)
03C5-2DsCosLutPara_B.bin ← 餘弦查找表(98 KB)
03C5-2DsDRPara.bin ← 動態範圍參數
03C5-2DsEmitFocusPara_B.bin ← <B发射聚焦参数>
...
四、結構推斷(已驗證部分)
- 頭部 0x16 =
<探头一般信息>開標籤 + 0x66 00(0x66=102 = payload 長度?)+ 型號 ASCII(12B 對齊) - 型號名重複出現:主型號
03C5-2Ds+ 別名C5-2V/C5-2D(對應 GetProbeName 返回的不同名稱) - 日期戳格式
YYYYMMDD-HHMMSS - 尾部以
結束 + 0xFF padding
五、未解
- 標籤與 payload 之間的長度前綴規則(部分段有 2B/4B 長度頭,部分靠標籤閉合定位)
探头图标段格式(圖標位圖)- native 解析器(
HPidIf.InitPidSys→setPidFilesPath)的完整解析邏輯在 libdoppler 內
深挖 6 — CC32xx 固件
CC32xx MCU 固件靜態分析 (TI CC3220R/CC3235S Firmware)
對象:assets/fw/board/v2|v3/mcu/cc32{20r,35s}/mcuimg2.bin 方法:純離線 xxd/python3/strings(無 TI 工具鏈)。
一、影像身份
| 檔 | 大小 | SP(向量表[0]) | Reset(向量表[1]) | vec3 |
|---|---|---|---|---|
| v2/cc3220r | 140,376 B | 0x20003000 | 0x2001bd99 | 0x2001d82d |
| v2/cc3235s | 136,463 B | 0x20003000 | 0x2001bb19 | 0x2001d58d |
| v3/cc3220r | 140,376 B | 0x20003000 | 0x2001bb61 | 0x2001d64d |
| v3/cc3235s | 137,288 B | 0x20003000 | 0x2001bd81 | 0x2001d8a5 |
- Cortex-M4 向量表確認(SP=0x20003000)
- Reset 向量落喺 SRAM(0x2001_xxxx)而非 flash(0x0100_0000) → 典型 TI SimpleLink「run-from-SRAM」影像:代碼由 serial flash 載入 SRAM 執行(CC32xx mcuimg 架構特徵)
- 首 8 字節無 TI OTA 檔頭 magic(直接係向量表)→ mcuimg2.bin 係裸影像(非 OTA 封裝)
二、可讀字符串(v3/cc3235s,strings -n 5)
file error
Fprobeid.txt ← 檔案操作:探頭 ID 檔
Fuhandprobe.bin ← 檔案操作:主固件影像(命名同 App 包 com.SonoiQ.uhandprobe 呼應)
Fwlan_ap_start -s " ← WLAN AP 啟動命令(-s <ssid>):探頭 MCU 開 AP 模式
uhandprobe_sleep.bin ← 睡眠模式影像
F前綴 = SimpleLink 檔案 API 名稱殘留(sl_FsOpen 等);串列顯示 MCU 的 serial flash 檔案系統佈局:probeid.txt(探頭 ID)、uhandprobe.bin(升級影像)、uhandprobe_sleep.bin(睡眠影像)wlan_ap_start -s ":MCU 提供 AP 模式啟動能力(Wi-Fi 探頭的直連模式——App 連探頭 AP 192.168.254.1 的對應側)- 文本密度極低(大部分為二進制)→ 影像可能經 bit-reversal 或部分打包,裸 strings 不可靠
三、版本差異
- v2→v3(cc3235s):49%(66,838/136,463)字節不同——實質性固件升級
- 板級版本對應
fw/board/v2vsv3(App 內GlobalVersion P25122602_004對應 2025-12-26 構建)
四、推斷(未驗證)
- CC3220R / CC3235S 是同一設計的兩代 WiFi MCU(CC3220 2.4GHz、CC3235 雙頻+BLE 系),固件按板 v2/v3 分別打包 → 無線探頭有兩個硬件代次
uhandprobe.bin與mcuimg2.bin的關係:mcuimg2 是開機引導影像(run-from-SRAM),uhandprobe.bin可能是放在 serial flash 的完整應用影像,開機後載入
五、完整解包需求(未做,需工具)
- TI ImageCreator / Uniflash:正式解析 SimpleLink 影像、serial flash 檔案系統
- 若影像 bit-reversed(TI CC32xx 歷史上有位序變換),需先做 bit 反轉再 disasm
- 完整逆向 MCU 協議(AP 握手、探頭控制命令)需 Ghidra(Cortex-M4, little-endian, 無 OS 或 TI-RTOS)
深挖 7 — Efinix Bitstream
Efinix Trion T55 FPGA Bitstream — 靜態分析 (Static Analysis)
分析對象:SonoiQ APK 內嵌之 Efinix Trion T55F324 FPGA 位元流檔案。 方法:純離線 (xxd / strings / python3),未執行任何 FPGA 工具鏈。 範圍:RBF 標頭/註解格式、原始位元流特性、版本差異、Java 端處理流程、變體假說、真實逆向之需求。
1. 檔案總覽 (File Overview)
1.1 探頭 FPGA (Probe FPGA) — assets/pid/FPGA_ch16*.rbf
| 檔案 | 大小 (bytes) | MD5 (前 12) | 起頭 |
|---|---|---|---|
| FPGA_ch16.rbf | 718,569 | 2775ed70c3b6 | FF×32 … |
| FPGA_ch16_2.rbf | 718,569 | 4cdf4c761b52 | FF×32 … |
| FPGA_ch16_3.rbf | 718,569 | 8c99e843a91c | FF×32 … |
| FPGA_ch16_4.rbf | 718,569 | 7a91e00ba4ba | FF×32 … |
| FPGA_ch16_5.rbf | 3,459,240 | — | 明文註解標頭 (plaintext annotation header) |
| FPGA_ch16_6.rbf | 718,569 | 22f86d1bef8b | FF×32 … |
1.2 主板 FPGA (Board FPGA) — assets/fw/board/{v2,v3}/fpga/{16,32}/UFPGA.bin
| 檔案 | 大小 (bytes) | MD5 (前 12) | 起頭 |
|---|---|---|---|
| v2/16/UFPGA.bin | 847,677 | 354c9e368f4b | FF×32 … |
| v2/32/UFPGA.bin | 871,678 | eefd60449eb0 | FF×32 … |
| v3/16/UFPGA.bin | 848,325 | ec865c02f962 | FF×32 … |
| v3/32/UFPGA.bin | 869,410 | 663efd0a08a6 | FF×32 … |
全部 10 個位元流檔案中,僅FPGA_ch16_5.rbf帶有明文標頭;其餘皆為無標頭原始位元流 (raw bitstream),直接以0xFF開頭。
2. FPGA_ch16_5.rbf — 完整註解位元流 (Full Annotated Bitstream)
2.1 註解標頭格式 (Annotation Header Format)
FPGA_ch16_5.rbf 的前 256 bytes 為純文字註解標頭,欄位如下(以 \n 分隔,其餘以 \n 填充至 256 bytes):
Version: 2022.1.226
Generated: Wed Jul 31 11:07:04 2024
Project: D:\Project\FPGA\YK2100_FPGA_T55_new\03_pnr
Family: Trion
Device: T55F324
Width: 16
Mode: passive
PADDED_BITS: 0
\n \n \n … (padding to 256 bytes)
欄位解讀:
| 欄位 | 值 | 意義 |
|---|---|---|
Version |
2022.1.226 |
Efinity IDE 版本(2022.1, build 226) |
Generated |
Wed Jul 31 11:07:04 2024 |
編譯時間戳 |
Project |
D:\Project\FPGA\YK2100_FPGA_T55_new\03_pnr |
原始 PnR 專案路徑(洩漏 Windows 建置機路徑) |
Family |
Trion |
FPGA 家族 |
Device |
T55F324 |
T55, 324-ball FBGA |
Width |
16 |
組態資料匯流排寬度 = 16 bit(平行組態) |
Mode |
passive |
被動組態模式 |
PADDED_BITS |
0 |
尾端未填補位元數 = 0 |
標頭固定 256 bytes(第 256 byte 即二進位載荷起點,值 0x16)。這份標頭是 Efinity 輸出 RBF 時可選附加的 metadata(AN 006 指出此標頭最多可增加 ~1 KB)。
2.2 原始位元流起始位置 (Raw Stream Boundary)
與其他 5 個原始 RBF(以 0xFF×32 開頭)不同,_5 的二進位載荷不含 0xFF 前導序列 (preamble):
- 標頭結束 / 載荷起點:offset 256,首 8 bytes =
16 8a 22 36 f0 33 29 35。 - 全檔
0xFF僅 105 個(最長連續 1 byte,位於 offset 2691);不存在0xFF長連串,故「搜尋第一個0xFFrun」找不到傳統的 raw-stream 邊界。 @1@1@標記位於 offset 836(即0x40 0x31 0x40 0x31 0x40),嵌於稀疏 nibble 資料區內,屬位元流內部 framing/annotation 標記,非 raw 資料起點。- 載荷可粗分兩段:
- offset 256 ~ 約 2600:註解/框架區 (annotation/framing),僅 119 種相異 byte 值,含大量
0x00、0x04/0x05/0x08/0x0c/0x0d及 ASCII0 1 4 5 < @,entropy ≈ 4.30。 - offset 約 2600 → 檔尾:密集位元流資料區,256 種相異 byte 值全出現,entropy ≈ 3.45,
0x00佔約 58%,其餘以單一位元置位的 byte (0x01/0x02/0x04/0x08/0x10/0x20/0x40/0x80) 為主——與第 3 節之原始 RBF 統計特徵一致,屬同一種編碼。
2.3 位元流大小對照 (Bitstream Size vs. T55 Spec)
依 Efinix AN 006「Configuring Trion FPGAs」 Table 1:
T55 最大支援組態位元數(單一影像, single image)= 27,675,040 bits
換算與實測:
| 量 | 值 |
|---|---|
| 27,675,040 bits ÷ 8 | 3,459,380 bytes |
| FPGA_ch16_5.rbf 實測 | 3,459,240 bytes(= 27,673,920 bits) |
| 差值 | −140 bytes(−1,120 bits) |
| 標頭扣除後載荷 | 3,458,984 bytes(27,671,872 bits) |
→ _5 檔大小與 T55 單一影像最大組態位元數幾乎完全吻合(差距 140 bytes,可由「標頭填充 + PADDED_BITS: 0 不填尾端」解釋)。_5 即 T55 的完整未壓縮位元流。 反觀 718,569-byte 的原始 RBF 僅為其 ~20.8%(約 4.81:1)——見第 3.3 節。
3. 原始 RBF 位元流 (Raw RBF Bitstreams)
FPGA_ch16.rbf、_2、_3、_4、_6 五個檔大小完全相同(718,569 bytes)、無標頭、0xFF 起頭。
3.1 前導 / 結尾結構 (Preamble / Trailer)
以 FPGA_ch16.rbf 為例:
- 前導:
0xFF× 32(idle/sync 填充),其後接 sync/命令字0x6a。 - 共同前綴 (common prefix):五個檔共享 42 bytes 前綴:
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff (32 × 0xFF)
6a f7 f7 f7 f7 f7 f7 f3 fb f2 (sync + 9 bytes)
- 結尾:檔尾
0xFF填充 59 bytes。 - 內部長連串:offset 75 處有 255-byte
0xFFrun、offset 346 處有 288-byte0x00run;全檔 ≥16-byte 的相同-byte run 高達 1,567 段——呈結構化區塊(sync/pad/data),非壓縮流樣貌。 - byte 統計:
0x00佔 57.3%(411,790/718,569),其次為單一位元置位值0x10/0x20/0x08/0x40/0x02/0x04/0x01/0x80;256 種 byte 值全出現,entropy ≈ 3.29。
3.2 五個變體差異 (Variant Differences)
兩兩 byte 比對(vs. FPGA_ch16.rbf,718,569 bytes):
| 檔案 | 首個差異 offset | 差異 byte 數 | 差異比例 |
|---|---|---|---|
| _2 | 45 | 364,395 | 50.7% |
| _3 | 42 | 365,685 | 50.9% |
| _4 | 44 | 365,322 | 50.8% |
| _6 | 42 | 365,539 | 50.9% |
→ 五檔共享 42-byte 前綴後,約半數位元組互異。這不是「僅 channel 設定」的微小差異,而是五份實質不同、但同規模(同 T55、同編碼)的完整位元流,對應不同硬體主板版本(見第 5.2 節對應表)。
3.3 大小之謎:718,569 bytes vs 27.68 M bits
T55 完整位元流需 27,675,040 bits(3.46 MB),但原始 RBF 僅 718,569 bytes(5,748,552 bits,~20.8%)。
假說(未經 Efinity 專案設定確認):
- 位元流壓縮 (bitstream compression):Efinix 支援位元流壓縮(Efinity Programmer Guide 記載 Titanium 預設壓縮至約一半;Trion 亦具壓縮選項)。壓縮由 FPGA 內部組態控制器解壓,故檔案無標準壓縮 magic、呈稀疏/區塊結構。
- 或為截斷/最小化位元流(去除尾端填充與閒置 frame)。
- 前導
0x6a(bit-reversed 為0x56,見第 4 節 UFPGA)為 sync 字,屬壓縮或標準位元流皆可能之結構。
兩者何者成立需對照 Efinity 專案(YK2100_FPGA_T55_new)的COMPRESSION設定,靜態層面無法定論——已誠實標記為未定。
4. UFPGA.bin — 主板 FPGA 位元流 (Board FPGA Bitstreams)
4.1 大小 (Sizes)
| 主板版 | 16 通道 | 32 通道 |
|---|---|---|
| v2 | 847,677 B | 871,678 B(+24,001) |
| v3 | 848,325 B(+648) | 869,410 B |
- 32 通道比 16 通道約大 ~24 KB(v2: +24,001;v3: +21,085)——額外通道之邏輯使然。
- v3 與 v2 差異甚小(16ch: +648 B;32ch: −2,268 B)——主板小改版。
- 四檔兩兩差異達 69.6 萬~78.5 萬 bytes(幾乎全檔不同),共同前綴僅 41–42 bytes。
4.2 位元反轉關係 (Bit-Reversal Relationship)
UFPGA.bin 與探頭 RBF 存在逐 byte 位元反轉 (bit-reversal) 對應:
FPGA_ch16.rbf 前導: FF…FF 6a f7 f7 f7 f7 f7 f7 f3 fb …
UFPGA(v2/16) 前導: FF…FF 56 ef ef ef ef ef ef cf df …
bitrev(56 ef…) = 6a f7 f7 f7 f7 f7 f7 f3 fb … ← 與 ch16 前 41 bytes 完全一致
0x6a↔0x56、0xf7↔0xef 恰為位元反轉對。意即主板 FPGA 位元流與探頭 FPGA 位元流共享同一 sync/前導字,但以相反位元序 (bit order) 儲存(典型為 SPI LSB-first vs MSB-first 載入差異)。注意兩者為不同檔案、不同 FPGA(尺寸不同),僅前導字序相反。
5. Java 程式碼處理流程 (Java Handling)
5.1 載入 (Loading) — HUsbProbeInterface 建構子
// HUsbProbeInterface(Context context) — 節錄
this.mFPGARbfBuf = LoadFPGAFile(ReadBinAssetsFiles.mFPGA_ch16_RbfFileName); // ch16.rbf
this.mFPGARbfBuf2 = LoadFPGAFile(ReadBinAssetsFiles.mFPGA_ch16_2_RbfFileName); // _2
this.mFPGARbfBuf3 = LoadFPGAFile(ReadBinAssetsFiles.mFPGA_ch16_3_RbfFileName); // _3
this.mFPGARbfBuf4 = LoadFPGAFile(ReadBinAssetsFiles.mFPGA_ch16_4_RbfFileName); // _4
this.mFPGARbfBuf5 = LoadFPGAFile(ReadBinAssetsFiles.mFPGA_ch16_5_RbfFileName); // _5
String str = MyApplication.App().getExternalFilesDir() + "/FPGA_ch16_5.rbf";
if (new File(str).exists()) {
this.mFPGARbfBuf5 = FileOperate.readFileToByteArray(str); // ★ 外部檔案覆寫
}
fpgargb_lib.byteswap(mFPGARbfBuf5.length, mFPGARbfBuf5); // ★ 僅對 _5 做 byteswap
ReadBinAssetsFiles只宣告ch16、_2、_3、_4、_5五個檔名常數——FPGA_ch16_6.rbf未被任何程式碼引用(孤兒檔,遺留資產)。- 外部檔案覆寫路徑 (external-file override):
getExternalFilesDir()/FPGA_ch16_5.rbf若存在即取代 asset 版本——即 board v2110 的 FPGA 位元流可現場升級 (field upgrade)。 byteswap僅施加於_5,其餘四檔原樣載入、不做變換。
5.2 主板版本對應 (Board-Version → Bitstream Mapping) — SetCurProbe
if (BoardVersion() == 1) buf = ch16.rbf;
else if (BoardVersion() == 3) buf = ch16_3.rbf;
else if (BoardVersion() == 4) buf = ch16_4.rbf;
else if (BoardVersion() == 5) buf = ch16_4.rbf; // v4/v5 共用
else if (BoardVersion() == 2110) buf = ch16_5.rbf; // ★ byteswap + 可外部覆寫
else buf = ch16_2.rbf; // 預設
5.3 byteswap 原生實作 — fpgargb_lib.byteswap(int, byte[])
System.loadLibrary("fpgargb_lib")(lib/arm64-v8a/libfpgargb_lib.so)。逆向反組譯 Java_jni_fpgargb_1lib_byteswap(objdump, ARM64):
GetByteArrayElements→ 取得陣列指標。- 逐對交換相鄰 byte:
byte[i] ↔ byte[i+1](i = 0, 2, 4, …),即 16-bit word 位元組序交換 (16-bit endian swap);NEONld2/st2對 32-byte 區塊向量化。 ReleaseByteArrayElements,回傳true。
→ 名稱雖叫 byteswap,實為每 16-bit word 內高低位元組對調。這與 _5 標頭 Width: 16(16-bit 平行組態)吻合:Efinity 輸出之 byte 序與 16-bit 平行匯流排載入所需 byte 序相反,故載入前對調(參照 AN 006 Table 12:16-bit bus 每週期載入 Byte0(高)+Byte1(低))。此變換不作用於其他 board 版本,顯示舊板(1/2/3/4/5)採不同載入模式(如序列 SPI,不需 byte 對調),僅 board v2110 採 16-bit passive 平行載入。
6. 六個變體之假說 (Hypotheses on the 6 Variants)
依第 5 節程式碼對應與檔案特性:
| 檔案 | 大小 | 對應主板版 | 推測角色 |
|---|---|---|---|
| ch16.rbf | 718,569 | BoardVersion 1 | v1 主板(最舊) |
| ch16_2.rbf | 718,569 | 預設 (else) | 通用/未識別主板 |
| ch16_3.rbf | 718,569 | BoardVersion 3 | v3 主板 |
| ch16_4.rbf | 718,569 | BoardVersion 4、5 | v4/v5 主板(共用) |
| ch16_5.rbf | 3,459,240 | BoardVersion 2110 | 新版主板,16-bit passive 平行組態、完整未壓縮位元流、可現場升級 |
| ch16_6.rbf | 718,569 | 無引用 | 遺留/棄用資產 |
關鍵結論:六檔皆為 T55F324(16 通道探頭) 位元流,但屬不同硬件主板改版的組態,非「不同 channel 數」之變體(channel 差異反映在 fw/board/*/fpga/{16,32} 的 UFPGA.bin,而非 pid/ 內的 ch16 系列)。_5 為唯一由 Efinity 原始輸出之完整註解位元流(帶標頭、需 byte 對調、可外部覆寫),其餘五檔為預先處理過之無標頭位元流。
7. 真實位元流逆向需要什麼 (What Real Bitstream Reversal Requires)
誠實評估:靜態層面無法還原邏輯設計。
- 無公開格式規範:Efinix 的 RBF/bitstream 內部格式(frame 佈局、CRAM 位址對應、routing mux 編碼)未公開;公開文件(AN 006、Efinity Programmer/Software User Guide)僅說明組態介面、位元流總大小與打包方式,不揭露位元流內容語意。目前無第三方格式規格。
- 需要 Efinity 工具鏈:真正的逆向路徑是取得/安裝 Efinity IDE,用其
dbedit(資料庫編輯器)或把位元流載回工具、以對應 project(標頭洩漏之路徑D:\Project\FPGA\YK2100_FPGA_T55_new\03_pnr)重新生成並 diff,才能建立「位元流 bit ↔ 邏輯 cell/routing」的對照。此工具鏈為閉源、需授權。 - 黑箱差分 (black-box diff) 為可行中間路徑:既然有 5 個同規模、半數位元組相異的原始位元流(不同主板版)+ 1 個完整位元流,可對齊其 42-byte 前綴後做逐 frame 差分,推測哪些區段對應「主板版本相依」的 I/O/routing 區塊。但無 CRAM 佈局規格下,仍無法將位元對應至具體 LUT/FF。
_5的 byteswap 是唯一已知轉換:16-bit word 位元組對調(第 5.3 節)是唯一可驗證的位元流轉換;其餘(UFPGA 的位元反轉、718KB 的壓縮/截斷)僅為統計觀察與假說。
附錄:關鍵原始十六進位 (Key Hex)
FPGA_ch16.rbf 前 64B: FF×32 6a f7 f7 f7 f7 f7 f7 f3 fb f2 f9 fb fa f1 f8 f9
fd f9 f8 f8 fa fb fd f9 ff fe ff fb fc fc f9 fd …
FPGA_ch16_5.rbf 標頭(0..255): 純文字 + \n 填充;載荷起點 256 = 16 8a 22 36 f0 33 29 35 ec 64 be c0 …
FPGA_ch16_5.rbf @1@1@: offset 836 (0x40 31 40 31 40)
UFPGA v2/16 前 64B: FF×32 56 ef ef ef ef ef ef cf df cf 1f df cf 0f 1f 9f …
Class Diagram
Core Types(基於 jadx 反編譯實測)
classDiagram
class MyApplication {
+static final int[] OEM_*_ID (18個)
+DbManager.DaoConfig userdaoConfig
+DbManager.DaoConfig operatorAuthdaoConfig
+DbManager.DaoConfig daoConfig
+TaskManager mTaskManager
+HPatientMng GetPatientMng()
+Exam getActiveExam()
+void handleException(Throwable)
}
class SplashActivity {
+List~User~ allUser
+void initDicomServices()
+void routeToModelActivity()
}
class BaseActivity {
+UltrasysApp mUltrasysApp
+void handleMessage(int, Object)
+void offWifi()
}
class Ultrasys {
<<singleton>>
+DscCtrl mDscCtrl
+FeUltrasysCtrl mFeUltrasysCtrl
+ImagePlayer mImagePlayer
+CinePlayer mCinePlayer
+PresetServer mPresetServer
+MITIData mMITIData
+static Ultrasys Instance()
+void setMITIData(float[])
+int GetProbeConType()
}
class MainHandler {
<<singleton>>
+Vector~Handler~ mDopplerMsgHandlerVector
+void InitCtoJavaMessageHandler()
+void RegistryDopplerHandler(Handler)
}
class jnitojava {
+static native int JavaGetMessage(long[])
+static boolean UsbWriteDwords(int, byte[], int)
+static boolean UsbReadDwords(int, byte[], int)
+static int WUsbReadImg(byte[], int)
+static void UpdateMITI(float[])
}
class HPidIf {
+static native boolean InitPidSys()
+static native boolean setPidFilesPath(String)
+static native int GetProbeId(String)
+static native int GetProbeElementNumber()
+static native float GetProbeRadius()
}
class ConnectType {
<<singleton>>
+const SIMU/WLAN/WUSB/USB/UWLAN/RGBIF/BROADCAST
+void SetConnType(int)
}
class UsbProbeManager {
<<singleton>>
+void Write(int, byte[], int)
+void Read(int, byte[], int)
+int ReadEncImg(byte[], int)
+int GetImgPack(byte[], int[])
}
class AiFuncMng {
+void trigger(AiMeasBase)
}
class AiMeasBase {
<<abstract>>
+void run()
}
class VoskSpeechRecognizer {
+void recognize()
}
class Exam {
+int examState
}
class AnimalExam {
+int examState
}
MyApplication --> TaskManager : owns
MyApplication --> Exam : queries
MyApplication --> AnimalExam : queries (vet)
BaseActivity --> Ultrasys : hosts
SplashActivity --> BaseActivity : routes
Ultrasys --> MainHandler : subscribes
MainHandler --> jnitojava : polls
jnitojava --> UsbProbeManager : delegates I/O
Ultrasys --> ConnectType : reads
ConnectType --> HPidIf : native sync
AiFuncMng --> AiMeasBase : dispatches
AiFuncMng --> VoskSpeechRecognizer : voice control
Notes
Ultrasys內部持有DscCtrl/FeUltrasysCtrl/ImagePlayer/CinePlayer/PresetServer等,但通過 public 字段暴露(非 getter),跨包直接訪問jnitojava是 native 側硬編碼的回調入口類——方法簽名即 ABIMainHandler是唯一的 C→Java 消息出口(輪詢 + 主線程廣播)- AI 類之間用
AiMeasBase抽象:每個診斷功能一個子類,AiFuncMng統一調度
Sequence Diagrams
Workflow 1: App 啟動 → 掃描界面
sequenceDiagram
participant User
participant Splash as SplashActivity
participant App as MyApplication
participant Dicom as JDicomServer
participant Main as BaseActivity
participant Ultrasys
User->>Splash: 啟動 App
Splash->>App: onCreate(DB / TaskManager / crash handler)
Splash->>Splash: 顯示登錄(用戶列表 + 密碼)
User->>Splash: 登錄 / 緊急模式
Splash->>Dicom: 啟動 DICOM 服務(Storage/Worklist/MPPS/Print)
Splash->>Main: 路由到機型 Activity(LandQ6/PortQ61/…)
Main->>Ultrasys: Instance() 初始化成像管線
Main-->>User: 掃描界面就緒
Walkthrough
- 啟動 —
SplashActivity(manifest LAUNCHER) - DB 初始化 —
MyApplication.onCreate():xUtils 三庫 + WAL - 登錄 — 用戶選擇、密碼、自動登錄、緊急按鈕
- DICOM —
JDicomServer等 native 服務啟動 - 路由 — 按 OEM 資源開關跳
LandActivity/LandQ6Activity/PortQ61Activity/LianmedActivity/LandCCGCActivity/PortActivity
Workflow 2: 探頭連接與影像流
sequenceDiagram
participant Probe as FPGA Probe
participant Usb as UsbProbeManager
participant Jni as jnitojava
participant Pump as MainHandler
participant Core as Ultrasys
participant GL as HGLImageJNIViewLib
Probe->>Usb: USB bulk 數據(加密影像包)
Usb->>Jni: WUsbReadImg / GetImgPack
Jni->>Core: JavaGetMessage 輪詢消息
Core->>Pump: 廣播 what/arg1/arg2
Pump->>Core: Handler 回調(主線程)
Core->>GL: 渲染(OpenGL)
GL-->>User: 畫面更新
Walkthrough
- 連接 —
ConnectType.SetConnType(USB_CONNECT_TYPE)→HSysctrlIf.SetProbeConnType(4) - PID 初始化 —
HPidIf.setPidFilesPath(assets/pid/PID03.zip)→InitPidSys() - 數據拉取 —
UsbProbeManager.Read()/ReadEncImg() - 消息泵 —
MainHandler.InitCtoJavaMessageHandler()輪詢jnitojava.JavaGetMessage(long[4]) - 顯示 —
Ultrasys→ImagePlayer/DispSurface(OpenGL)
Workflow 3: AI 測量(膀胱體積)
sequenceDiagram
participant User
participant UI as ImageDisplayFragment
participant Mng as AiFuncMng
participant Meas as BladderMeas
participant Pt as org.pytorch
participant So as libpytorch_jni.so
participant Mask as MaskProcessor
User->>UI: 點擊膀胱測量
UI->>Mng: trigger(BladderMeas)
Mng->>Meas: 異步執行
Meas->>Pt: 前向推理(當前幀)
Pt->>So: JNI
So-->>Pt: 分割掩膜
Pt-->>Mask: 後處理(直徑/面積擬合)
Mask-->>Meas: 體積結果
Meas-->>UI: Observer 回調 → 疊加顯示
Workflow 4: DICOM 存儲
sequenceDiagram
participant User
participant UI as ReportFragment
participant Dicom as JDicomStorageService
participant Dopp as libdoppler (native DICOM)
participant PACS as PACS Server
User->>UI: 檢查結束 → 存儲
UI->>Dicom: 發送圖像
Dicom->>Dopp: native 調用
Dopp->>PACS: C-STORE(DICOM 協議)
PACS-->>Dopp: ACK
Dopp-->>Dicom: 結果
Dicom-->>UI: 狀態提示
Getting Started
Getting Started — 重現與擴展分析
Prerequisites
| 工具 | 版本(實測) | 路徑 |
|---|---|---|
| jadx | 1.5.6 | /opt/homebrew/bin/jadx |
| apktool | 3.0.3 | /opt/homebrew/bin/apktool |
| Java (JDK) | OpenJDK 17.0.20.1 | /usr/bin/java |
| git | — | 系統自帶 |
| Python 3 | 3.9+ | 系統自帶(統計腳本用) |
工具路徑以 reverse-skill tool-index.md 為準(本機已裝,無需 bootstrap)。
重現反編譯
APK="/Volumes/End-Gsme/untra-sound/SonoiQ-2.2.15_260113 (1).apk"
cd /Volumes/End-Gsme/untra-sound/sonoiq-reverse
# Java 反編譯(~3 dex,6025 檔)
jadx -d jadx_out --show-bad-code "$APK"
# 解包(smali + manifest + 資源)
apktool d -f -o apktool_out "$APK"
驗證樣本一致:
shasum -a 256 "$APK"
# 867c136abf74211da7eaa511f0d5a1de022064421d7b2a5410d105f32060a2c1
資產清單統計
unzip -l "$APK" > _apk_listing.txt
# 再跑 Python 分類腳本(見 reports/static-analysis-report.md 附錄)
常用探索命令
# 找 native 加載點
grep -rn "System.loadLibrary" jadx_out/sources/handprobe jadx_out/sources/jni \
jadx_out/sources/kernel jadx_out/sources/com/Com --include="*.java"
# 找關鍵字(加密/簽名/證書/網絡)
grep -rln "encrypt\|decrypt\|AES\|RSA\|certificate\|trust" \
jadx_out/sources/handprobe jadx_out/sources/com/Com --include="*.java"
# 搜 smali(patch 前)
grep -rn "invoke-static.*CheckSign\|checkSign" apktool_out/smali*/ --include="*.smali"
已知限制
- 僅 arm64-v8a ABI——無 armeabi-v7a 模擬器支持
res/檔名全面混淆;assets/明文- 核心算法在 native(
libdoppler15.4 MB)——Java 層只有 JNI 簽名;深入需 IDA/Ghidra(reverse-skillida-reverse/radare2路由) LandQ6Activity/LandQ62Activity帶sharedUserId="android.uid.system"——重打包需系統簽名
Where to Go Next
- 架構:architecture.md
- 模組:README.md#module-map
- JNI API:api.md
- 靜態分析報告:../reports/static-analysis-report.md