GitHub - cfbird/HT203U-Thermal: USB-C thermal camera, with **real temperature readout** — not just the colorized video the stock/generic UVC apps show · GitHub
/" data-turbo-transient="true" />
Skip to content
Search/
Sign in<br>Sign upAppearance settings
You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.
Dismiss alert
{{ message }}
cfbird
HT203U-Thermal
Public
Notifications<br>You must be signed in to change notification settings
Fork
Star
main
BranchesTags
Go to file
CodeOpen more actions menu
Folders and files<br>NameNameLast commit message<br>Last commit date<br>Latest commit
History<br>19 Commits<br>19 Commits
.github/workflows
.github/workflows
app
app
.gitignore
.gitignore
LICENSE
LICENSE
README.md
README.md
build.gradle.kts
build.gradle.kts
gradle.properties
gradle.properties
settings.gradle.kts
settings.gradle.kts
View all files
Repository files navigation
HT203U Thermal
Native Android app for the Vevor/Hti HT-203U USB-C thermal camera with real, calibrated temperature readout — built by reverse-engineering the module live, since the stock apps only show colorized video. Under 1 MB, no native code, no third-party camera libraries.
The big finding
Despite the Hti branding, the HT-203U is a rebranded HIKMICRO Mini2Plus core (USB 2bdf:0102, product string "HikCamera"). It is not an Xtherm/InfiRay device, so none of the published HT-301/T2S+ protocols (zoom-command 0x8004, Xtherm calibration metadata) apply. Everything below was reverse-engineered from the device itself.
Device layout (as discovered)
USB descriptors:
IF#0: VideoControl — input terminal (id 2), processing unit (id 5), extension unit id 10, 15 controls, GUID {A29E7641-DE04-47E3-8B2B-F4341AFF003B} (vendor controls: FFC/shutter etc. — mapping in progress), interrupt EP 0x83
IF#1: VideoStreaming — YUY2 uncompressed (7+ frame descriptors), MJPEG, H.264; bulk endpoint 0x81 (maxPkt 512), alt setting 0
The interesting mode is uncompressed 256×392 @ 25 fps (200,704 B/frame), a stacked frame:
Rows<br>Content
0–191<br>Raw thermal counts , u16 LE (uncompensated; drifts with FPA until FFC)
192–195<br>Config/state block (contains sensor dims 256/192, FPA-tracking values; partially decoded)
196–387<br>Camera-rendered display image as YUY2 (chroma byte pinned to 0x80)
388–391<br>Telemetry block : magic ffff eeee, then device-computed max/min/avg raw values
The raw block is identified at runtime by chroma signature (the display block has every odd byte = 0x80).
Temperature calibration
Two-point empirical fit (2026-07-26), references measured with a kitchen thermometer:
Ice-water slurry, 3.9 °C → raw 4405
Rolling boil, ~100 °C → raw 6979
T(°C) = (raw − 4405) × 0.0373349 + 3.9 (≈ 26.8 counts/°C)
Independent validation on anchors not used in the fit: palm skin → 32.5 °C (expected 32–34), indoor ambient → 25.2 °C (July, plausible). Caveat: absolute offset can drift a few °C across power cycles and during warm-up because FFC (shutter flat-field correction) can't be triggered yet — that lives in the vendor XU and is the current reverse-engineering target.
Why the usual Android UVC libraries fail
libuvc-based libraries (saki/UVCCamera forks, UVCAndroid) fail negotiation on this device with UVC_ERROR_INVALID_MODE (-51) for every mode. This app therefore implements UVC-over-bulk in pure Kotlin (BulkUvc.kt) on the standard UsbManager API:
Claim the VS interface, PROBE/COMMIT via controlTransfer (probe response: dwMaxVideoFrameSize=200704, dwMaxPayloadTransferSize=12288, bmFramingInfo=0x03)
Read the bulk endpoint, reassemble payloads by header FID/EOF bits
App features
Live thermal display, ironbow palette, auto-exposure, min/max/center markers
Calibrated °C/°F readout for min/center/max (+ raw center count in the status line)
Photo : 4× PNG to Pictures/HT203U + raw u16 frame to Download/HT203U (.raw, 256×192 LE — great for offline analysis)
Video : H.264 MP4 recording of the live view to Movies/HT203U
Rotate (90° steps, persisted) + free screen orientation
Debug tooling: Probe XU (read-only walk of the 15 vendor controls), Dump meta (hex of config/telemetry rows + frame stats), Log (in-app trace + logcat, share/copy)
Mode auto-cycling if a stream mode delivers no frames
Runtime CAMERA permission (Android 10+ silently denies USB access to video-class devices without it) + standard USB permission dialog (GrapheneOS-friendly)
Getting the APK
Every push to main builds via GitHub Actions and publishes to the rolling release:
https://github.com/cfbird/HT203U-Thermal/releases/download/latest/app-debug.apk
Or build locally: Gradle 8.9, JDK 17, Android SDK 34 — gradle assembleDebug.
GrapheneOS notes
Allow USB peripherals: Settings → USB-C port ("Allow new USB peripherals when unlocked")
Grant Camera permission on first launch, then accept the USB dialog...