Hardware Validation Checklist
Items that cannot be verified by unit or integration tests alone. Run against a real CFMoto 450 MT/SR/NK + Android device with HCI snoop log enabled.
How to capture live traffic
# Enable on device (Developer Options → Enable Bluetooth HCI snoop log)
adb shell setprop persist.bluetooth.btsnoopenable true
# Run the app, perform the action, then pull the log:
adb pull /data/misc/bluetooth/logs/btsnoop_hci.log
# Open in Wireshark (filter: btle) or decode with:
python3 tools/apk-analysis/scripts/decode-btsnoop.py btsnoop_hci.log
Validation items
1. Keep-alive ACK control code
Uncertainty: KeepAliveManager.notifyAck() must be called from the ResponseRouter
handler for the correct incoming control code. OEM source suggests 0xEC
(KEEP_ALIVE_RESULT) but 0xE7 (LOCK_RESULT) is also plausible for heartbeat ACKs.
Validate:
- Send a keep-alive heartbeat (LOCK_CONTROL
0x67) - Inspect the bike's reply frame — record
frame[2](control code) - Update
KeepAliveManagerJSDoc and the handler registration inCFMoto450Protocol(once wired) to use the confirmed code
Tests to update:
packages/ble-protocol/__tests__/keepalive.test.ts— add "ACK code is0xEC" assertion once confirmeddocs/test-coverage.md— mark keepalive ACK code as confirmed
2. Lock/unlock encrypted payload structure
Uncertainty: lock() and unlock() builders in packages/ble-protocol/src/commands/index.ts
accept a raw encryptedPayload: Uint8Array and wrap it in a Lock protobuf. The exact
AES-256/ECB/PKCS7 encryption using the cloud-supplied key has not been validated end-to-end.
Validate:
- Obtain
VehicleNowInfoResp.encryptInfo.keyfrom the cloud API - Send a lock command and confirm the bike responds with
LOCK_RESULT(0xE7)result=0
Code to update:
packages/ble-protocol/src/cfmoto450.ts/ mobile integration — wire command/heartbeat ACK handlers to app state (recordCommandAcked,recordHeartbeatAck)
2.1 Cloud encryptInfo compatibility with real TBox auth
Uncertainty: We now fetch encryptValue + key from cloud (VehicleNowInfoResp.encryptInfo)
and use them in BLE auth. This must be validated end-to-end against real hardware.
Validate:
- Login via cloud API y consultar
GET /fuel-vehicle/servervehicle/app/vehicle?vehicleId=... - Confirm returned
encryptValueandkeyproduce successful BLE auth (0x5A -> 0x5B -> 0x5C -> 0x5D result=0) - Confirm
ivcan be ignored in this flow (AES/ECB path)
Result to document:
- If auth succeeds: mark cloud-to-BLE key path as hardware-confirmed
- If auth fails: capture HCI + cloud payload and compare with OEM app session
Current status (2026-03-14):
- Burp export
tools/apk-analysis/mitm-logs/full-historyconfirms cloud login/signing, but only in virtual vehicle mode (vehicleId=-1), whereencryptInfois{}. - This item remains pending until a capture with real VIN-linked vehicle is collected.
2.2 MotoPlay channel boundary (projection vs telemetry)
Uncertainty: Need final hardware-level confirmation that MotoPlay traffic is not carrying bike telemetry (RPM/speed/fuel) for MT450.
Validate:
- Run MotoPlay session with navigation active
- Correlate:
- BLE traffic (HCI snoop)
- cloud vehicle telemetry updates
- MotoPlay-related traffic/logs
- Confirm bike telemetry remains cloud/TBox-driven and not MotoPlay-driven
Current status (static APK):
- MotoPlay classes reviewed indicate projection/navigation state, not bike telemetry payloads.
3. DISPLAY_UNITS enum values
Uncertainty: setUnits('metric') encodes Display_Units.METRIC = 1 and
setUnits('imperial') encodes Display_Units.IMPERIAL = 2. These values are from
the protobuf schema but have not been confirmed against live traffic.
Validate:
- Switch units in the OEM app while capturing HCI snoop
- Decode the outgoing
DISPLAY_UNITS(0x69) frame payload with the Meter proto schema - Confirm that
metric=1,imperial=2(or correct values)
Tests to update:
packages/ble-protocol/__tests__/commands.test.ts— add decoded-payload assertion forsetUnitsonce enum values are confirmed
4. 100ms post-connect delay necessity
Uncertainty: CFMoto450Protocol.connect() waits 100ms after transport.connect()
before calling transport.subscribe(). This matches BleModel.java onConnectSuccess().
Some devices may not need this delay; others may need more.
Validate:
- Connect without the delay → observe whether descriptor write fails on GATT
- Try with 50ms / 100ms / 200ms if issues occur
- Document the minimum safe delay on CFMoto 450 MT hardware
5. MTU 185 negotiation acceptance
Uncertainty: requestMtu(185) is confirmed from BleModel.java but the actual
negotiated MTU the TBox returns may differ. The mock transport always returns 185.
Validate:
- Log the return value of
transport.requestMtu(peripheralId, 185)on a real device - If granted MTU < 185, verify that all built frames fit within the negotiated MTU
(largest command payload is
setSpeedLimitat ~12 bytes — well within any reasonable MTU)
6. TD-04 — Provider race condition on Android
Issue: If the app unmounts (or screen rotates) while requestBlePermissions() is
still awaiting the system dialog, runBleInit may call bleService.initialize() after
bleService.destroy() has already run. The service ends up initialized with no cleanup
registered.
Reproduce:
- Cold-launch the app on Android 12+ (permission dialog appears)
- Immediately rotate the device or navigate away
- Check
bleServiceinternal state —protocolshould be null, but may not be
Fix (when confirmed): Add an isMounted ref guard in CFMotoProvider and check it
before calling bleService.initialize().
7. TD-05 — Stale isRecording closure in useRideRecording
Issue: useRideRecording reads isRecording inside a useEffect that only lists
connectionState as a dependency. On the first 'connected' transition isRecording
is correctly false; on subsequent reconnects it may read a stale true and skip
starting a new recording.
Reproduce:
- Connect to bike → trip starts recording
- Disconnect → trip stops
- Reconnect → verify
isRecordingtransitions totrue(new trip starts)
Fix (if confirmed): Add isRecording to the useEffect dependency array, or replace
the useState with a useRef that does not trigger re-render.
8. TD-06 — recordCommandAcked never called (commandHistory ackedAt always null)
Issue: useBikeStore.recordCommandAcked() is defined and tested but never called in
the current codebase. Command history entries always show ackedAt: null.
To complete:
- Confirm which incoming control codes are ACKs for each outgoing command:
LOCK_RESULT(0xE7) → ACK forLOCK_CONTROL(0x67)FIND_CAR_RESULT(0xEA) → ACK forFIND_CAR(0x6A)LIGHT_CONTROL_RESULT(0xEB) → ACK forLIGHT_CONTROL(0x6B)
- Register ResponseRouter handlers for each ACK code in
CFMoto450Protocol - Call
useBikeStore.getState().recordCommandAcked(sentCode)in each handler
Tests to add: Once wired, add integration test asserting that after sending a command
and simulating the ACK frame, the matching commandHistory entry has ackedAt set.