User Story #74 » androidApp-62-capture-uwb-rssi-into-signalquality.patch
| androidApp/src/main/java/com/aether/mofe/platform/AndroidMofePlatformProvider.kt | ||
|---|---|---|
|
// every bearing over ~3° as "out of range", so self-yaw never anchored.
|
||
|
azimuth = r.azimuthDeg?.toDouble()?.let { Math.toRadians(it) },
|
||
|
elevation = r.elevationDeg?.toDouble()?.let { Math.toRadians(it) },
|
||
|
signalQuality = 1.0,
|
||
|
rssi = 0.0,
|
||
|
// Per-link signal strength captured from the UWB ranging callback (#62).
|
||
|
// Previously both fields were dead constants (signalQuality≡1.0, rssi≡0.0),
|
||
|
// so MeasurementPreFilter's signal-quality gate (raw.signalQuality < 0.5)
|
||
|
// never fired and weak long-range / NLOS links were treated as pristine.
|
||
|
//
|
||
|
// Android UWB reports RSSI in dBm (when the stack exposes it), not a
|
||
|
// normalized [0,1] quality. Thread the real dBm into `rssi`, and DERIVE
|
||
|
// `signalQuality` from it with the SAME linear map the engine already applies
|
||
|
// to RSSI in MeasurementPreFilter.computeRawConfidence — ((rssi + 100)/50)
|
||
|
// clamped to [0,1] — so a link at ≤ -75 dBm crosses the 0.5 gate. When the
|
||
|
// stack reports no RSSI (r.rssiDbm == null) we fall back to the previous dead
|
||
|
// defaults, leaving behavior unchanged for hardware/versions that expose none.
|
||
|
// NOTE: the -100..-50 dBm scale is the engine's existing assumption; real UWB
|
||
|
// RSSI may need on-device recalibration of that map (see #62 follow-up).
|
||
|
signalQuality = r.rssiDbm?.let { ((it + 100.0) / 50.0).coerceIn(0.0, 1.0) } ?: 1.0,
|
||
|
rssi = r.rssiDbm?.toDouble() ?: 0.0,
|
||
|
)
|
||
|
}
|
||
| androidApp/src/main/java/com/aether/mofe/platform/AndroidUwbService.kt | ||
|---|---|---|
|
import android.util.Base64
|
||
|
import android.util.Log
|
||
|
import androidx.core.uwb.RangingParameters
|
||
|
import androidx.core.uwb.RangingPosition
|
||
|
import androidx.core.uwb.RangingResult
|
||
|
import androidx.core.uwb.UwbAddress
|
||
|
import androidx.core.uwb.UwbClientSessionScope
|
||
| ... | ... | |
|
azimuthDeg = pos.azimuth?.value,
|
||
|
elevationDeg = pos.elevation?.value,
|
||
|
timestampMs = System.currentTimeMillis(),
|
||
|
// Capture per-link RSSI off this range sample (#62) so the
|
||
|
// engine can see weak links. Best-effort / null when the
|
||
|
// stack does not report it — see extractRssiDbm.
|
||
|
rssiDbm = extractRssiDbm(pos),
|
||
|
)
|
||
|
)
|
||
|
}
|
||
| ... | ... | |
|
private fun b64(bytes: ByteArray): String = Base64.encodeToString(bytes, Base64.NO_WRAP)
|
||
|
private fun hex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) }
|
||
|
/**
|
||
|
* Read per-link RSSI (dBm) off a UWB [RangingPosition] DEFENSIVELY (#62).
|
||
|
*
|
||
|
* WHY REFLECTION rather than a direct `position.rssiDbm`: receive-signal strength
|
||
|
* is not a stable part of the AndroidX UWB surface across the versions and vendor
|
||
|
* stacks this app runs against — `RangingPosition.rssiDbm` was added partway along
|
||
|
* the androidx.core.uwb 1.0.0 alpha→beta line, and some OEM builds still back it
|
||
|
* with the framework RSSI_UNKNOWN sentinel. A direct property reference would fail
|
||
|
* to compile/link where it is absent, and this Android module cannot be built in
|
||
|
* the offline dev sandbox to catch that. Reflection keeps the capture best-effort:
|
||
|
* real dBm when the stack reports it, null (→ the engine keeps its prior default)
|
||
|
* when it does not. First-path / CIR is not exposed on the AndroidX surface at all,
|
||
|
* so it is deliberately not attempted here.
|
||
|
*
|
||
|
* @return a plausible negative dBm reading, or null when unavailable / unknown.
|
||
|
*/
|
||
|
private fun extractRssiDbm(position: RangingPosition): Int? = runCatching {
|
||
|
val raw = (position.javaClass.getMethod("getRssiDbm").invoke(position) as? Number)?.toInt()
|
||
|
// Valid UWB RSSI is a negative dBm (~ -30 … -100). Reject the framework
|
||
|
// RSSI_UNKNOWN sentinel (-128) and any non-negative "unset" reading.
|
||
|
if (raw != null && raw in -127..-1) raw else null
|
||
|
}.getOrNull()
|
||
|
companion object {
|
||
|
// Fixed UWB channel so both peers agree without an OOB exchange. Channel 9 + BPRF
|
||
|
// preambles 9–12 are broadly supported (Pixel + Galaxy). UWB_PREAMBLE is the legacy
|
||
| ... | ... | |
|
val azimuthDeg: Float?,
|
||
|
val elevationDeg: Float?,
|
||
|
val timestampMs: Long,
|
||
|
/** Per-link receive signal strength in dBm from the UWB ranging callback, or
|
||
|
* null when the platform/hardware does not report it (read defensively in
|
||
|
* AndroidUwbService.extractRssiDbm). Threaded to the engine's rssi /
|
||
|
* signalQuality fields in AndroidUwbRangingServiceAdapter. Defaulted so the
|
||
|
* non-radio emitExternalRange path stays source-compatible. (#62) */
|
||
|
val rssiDbm: Int? = null,
|
||
|
)
|
||
|
enum class SessionStatus {
|
||