Add OVERWATCH v0.1.0 — full detection engine + polish

Phase 1 (BLE), Phase 2 (WiFi BSSID/SSID), Phase 3 (DeFlock map proximity),
Phase 4 (Waze live POLICE alerts), and Phase 5 polish all wired through one
DetectionStore. Confidence engine scores 0-100; UI maps to 4-tier circle.

Polish:
  - Stylized two-line app title: [DЯΣΛMMΛKΣЯ] // 0VΣЯW4TCH
  - Modal bottom sheet for source drill-down (tap circle)
  - Settings screen: per-source toggles, proximity sliders, theme select
  - SharedPreferences-backed Settings with StateFlow exposure
  - DetectionService respects per-source toggles at start time
  - Scanners read proximity overrides via supplier lambdas

README documents all sources, architecture, build steps, permissions, and
the legal disclaimer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
KaraZajac
2026-04-28 21:10:57 -04:00
parent 1e195605df
commit 87b1f0b887
45 changed files with 3283 additions and 0 deletions
@@ -0,0 +1,241 @@
package org.soulstone.overwatch.fusion
/**
* Confidence scoring — port of flock-detection's algorithm with weights from the OVERWATCH plan.
*
* One [BleObservation] (a single ScanResult) → one score. Multi-method bonus and RSSI bonuses
* apply within a single observation. Cross-source corroboration is handled at the [DetectionStore]
* level (multiple sources hitting the same area push the global max upward).
*/
object ConfidenceEngine {
// Single-method base weights (BLE)
const val W_BLE_OUI = 40
const val W_BLE_OUI_AXON = 80
const val W_BLE_NAME = 45
const val W_BLE_NAME_PENGUIN_NUMERIC = 15
const val W_BLE_MFG_XUNTONG = 60
const val W_BLE_TN_SERIAL_BONUS = 20 // added on top of mfg
const val W_BLE_RAVEN_UUID = 70
const val W_BLE_RAVEN_UUID_MULTI = 90 // 3+ UUIDs
// Single-method base weights (WiFi — wired in Phase 2)
const val W_WIFI_OUI = 40
const val W_WIFI_SSID_GENERIC = 50
const val W_WIFI_SSID_FLOCK_FMT = 65
// Map / Waze (Phase 3 + 4)
const val W_DEFLOCK_NEAR = 60 // <= 200m
const val W_DEFLOCK_VERY_NEAR = 85 // <= 50m
const val W_WAZE_POLICE = 55
// Bonuses
const val B_MULTI_METHOD = 20
const val B_STRONG_RSSI = 10 // > -50 dBm
const val B_STATIONARY = 15 // RSSI rise-peak-fall
/** What we observed about one BLE device on a single scan callback. */
data class BleObservation(
val mac: String,
val rssi: Int,
val deviceName: String?,
val advertisedUuids: List<java.util.UUID>?,
val manufacturerCompanyId: Int?,
val manufacturerPayload: ByteArray?,
val isStationary: Boolean = false
)
/** What we observed about one WiFi AP on a single scan result. */
data class WifiObservation(
val bssid: String,
val ssid: String?,
val rssi: Int,
val isStationary: Boolean = false
)
/** A DeFlock map ALPR observed within proximity threshold. */
data class DeflockObservation(
val osmId: Long,
val distanceMeters: Float,
val operator: String?,
val manufacturer: String?
)
/** A Waze POLICE alert observed within proximity + freshness thresholds. */
data class WazeObservation(
val uuid: String,
val distanceMeters: Float,
val ageMs: Long,
val confidence: Int, // raw 0-5
val reliability: Int, // raw 0-10
val subtype: String?
)
data class Scored(
val score: Int,
val methods: String,
val label: String,
/** True if the BLE OUI specifically matched Axon (drives the "Axon body cam" labeling). */
val isAxon: Boolean
)
fun scoreBle(obs: BleObservation): Scored {
var score = 0
val methods = StringBuilder()
var methodCount = 0
var ouiHit = false
var nameHit = false
var mfgHit = false
var ravenHit = false
var isAxon = false
// OUI prefix
if (org.soulstone.overwatch.data.targets.BleOuis.isAxon(obs.mac)) {
score += W_BLE_OUI_AXON
methods.append("axon_oui ")
ouiHit = true; isAxon = true
} else if (org.soulstone.overwatch.data.targets.BleOuis.matches(obs.mac)) {
score += W_BLE_OUI
methods.append("oui ")
ouiHit = true
}
if (ouiHit) methodCount++
// Device name patterns
if (org.soulstone.overwatch.data.targets.Patterns.bleNameMatch(obs.deviceName)) {
score += W_BLE_NAME
methods.append("name ")
nameHit = true
} else if (org.soulstone.overwatch.data.targets.Patterns.isPenguinNumeric(obs.deviceName)) {
score += W_BLE_NAME_PENGUIN_NUMERIC
methods.append("penguin_num ")
nameHit = true
}
if (nameHit) methodCount++
// Manufacturer-data signature
if (obs.manufacturerCompanyId == org.soulstone.overwatch.data.targets.Manufacturers.XUNTONG_COMPANY_ID) {
score += W_BLE_MFG_XUNTONG
methods.append("mfg_0x09C8 ")
mfgHit = true
if (org.soulstone.overwatch.data.targets.Manufacturers.hasTnSerial(obs.manufacturerPayload)) {
score += W_BLE_TN_SERIAL_BONUS
methods.append("tn_serial ")
}
}
if (mfgHit) methodCount++
// Raven service UUIDs
val ravenCount = org.soulstone.overwatch.data.targets.RavenUuids.countMatches(obs.advertisedUuids)
if (ravenCount > 0) {
if (ravenCount >= 3) {
score += W_BLE_RAVEN_UUID_MULTI
methods.append("raven_multi ")
} else {
score += W_BLE_RAVEN_UUID
methods.append("raven_uuid ")
}
ravenHit = true
methodCount++
}
// Multi-method corroboration bonus
if (methodCount >= 2) {
score += B_MULTI_METHOD
methods.append("multi ")
}
// Strong RSSI (very close)
if (obs.rssi > -50) {
score += B_STRONG_RSSI
methods.append("strong_rssi ")
}
// Stationary RSSI trend
if (obs.isStationary) {
score += B_STATIONARY
methods.append("stationary ")
}
score = score.coerceAtMost(100)
val label = when {
isAxon -> "Axon body cam (${obs.mac})"
ravenHit -> "Raven gunshot detector (${obs.mac})"
!obs.deviceName.isNullOrBlank() -> "${obs.deviceName} (${obs.mac})"
else -> "Surveillance BLE (${obs.mac})"
}
return Scored(score, methods.toString().trim(), label, isAxon)
}
fun scoreWaze(obs: WazeObservation): Scored {
// Plan baseline: 55 for any POLICE alert ≤500m & <10min old.
// Caller is responsible for applying the proximity + age gate before scoring.
var score = W_WAZE_POLICE
// Lightweight crowd-trust nudge: high reliability & high confidence each add a few points,
// capped well under the multi-method bonus so a corroborating BLE/WiFi hit still dominates.
if (obs.reliability >= 7) score += 5
if (obs.confidence >= 4) score += 5
score = score.coerceAtMost(100)
val methods = "waze_police rel=${obs.reliability} conf=${obs.confidence}"
val ageMin = (obs.ageMs / 60_000L).toInt()
val sub = obs.subtype?.let { " ($it)" } ?: ""
val label = "Police report$sub @ ${obs.distanceMeters.toInt()}m, ${ageMin}min ago"
return Scored(score, methods, label, isAxon = false)
}
fun scoreDeflock(obs: DeflockObservation): Scored {
val score = if (obs.distanceMeters <= 50f) W_DEFLOCK_VERY_NEAR else W_DEFLOCK_NEAR
val rangeTag = if (obs.distanceMeters <= 50f) "deflock<=50m" else "deflock<=200m"
val descriptor = listOfNotNull(obs.manufacturer, obs.operator)
.joinToString(" / ").ifBlank { "ALPR" }
val label = "%s @ %dm (osm:%d)".format(descriptor, obs.distanceMeters.toInt(), obs.osmId)
return Scored(score, rangeTag, label, isAxon = false)
}
fun scoreWifi(obs: WifiObservation): Scored {
var score = 0
val methods = StringBuilder()
var methodCount = 0
val ouiHit = org.soulstone.overwatch.data.targets.WifiOuis.matches(obs.bssid)
if (ouiHit) {
score += W_WIFI_OUI
methods.append("oui ")
methodCount++
}
var ssidHit = false
if (org.soulstone.overwatch.data.targets.Patterns.ssidFlockFormat(obs.ssid)) {
score += W_WIFI_SSID_FLOCK_FMT
methods.append("ssid_flock ")
ssidHit = true
} else if (org.soulstone.overwatch.data.targets.Patterns.ssidGenericMatch(obs.ssid)) {
score += W_WIFI_SSID_GENERIC
methods.append("ssid_generic ")
ssidHit = true
}
if (ssidHit) methodCount++
if (methodCount >= 2) {
score += B_MULTI_METHOD
methods.append("multi ")
}
if (obs.rssi > -50) {
score += B_STRONG_RSSI
methods.append("strong_rssi ")
}
if (obs.isStationary) {
score += B_STATIONARY
methods.append("stationary ")
}
score = score.coerceAtMost(100)
val label = if (!obs.ssid.isNullOrBlank()) "${obs.ssid} (${obs.bssid})"
else "Surveillance WiFi (${obs.bssid})"
return Scored(score, methods.toString().trim(), label, isAxon = false)
}
}
@@ -0,0 +1,24 @@
package org.soulstone.overwatch.fusion
/**
* One observation from one source at one moment.
*
* @param source which scanner produced this
* @param key stable per-device identifier (MAC for BLE/WiFi, OSM id for DeFlock, uuid for Waze)
* @param label short human-readable description shown in the drill-down ("Axon body cam", "FS-1A2B")
* @param score 0-100 confidence assigned by the engine
* @param matchedMethods space-separated short tags for what triggered ("axon_oui mfg_0x09C8 tn_serial")
* @param rssi signal strength if applicable (BLE/WiFi); null for map/Waze sources
* @param timestampMs wall-clock millis when this event was produced
*/
data class DetectionEvent(
val source: DetectionSource,
val key: String,
val label: String,
val score: Int,
val matchedMethods: String,
val rssi: Int? = null,
val timestampMs: Long = System.currentTimeMillis()
) {
val level: ThreatLevel get() = ThreatLevel.fromScore(score)
}
@@ -0,0 +1,62 @@
package org.soulstone.overwatch.fusion
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* In-memory hub for detection events.
*
* - Keeps the most recent event per (source, key) — newer overwrites older.
* - Drops events older than [retentionMs] (default 5 min, mirrors flock-detection's dedup window).
* - Exposes [threatLevel] = the worst tier across all live events.
* - No persistence: per the user's spec, no detection-history DB.
*/
class DetectionStore(
private val retentionMs: Long = 5 * 60 * 1000L,
private val nowMs: () -> Long = System::currentTimeMillis
) {
private val _events = MutableStateFlow<List<DetectionEvent>>(emptyList())
val events: StateFlow<List<DetectionEvent>> = _events.asStateFlow()
private val _threatLevel = MutableStateFlow(ThreatLevel.GREEN)
val threatLevel: StateFlow<ThreatLevel> = _threatLevel.asStateFlow()
private val _maxScore = MutableStateFlow(0)
val maxScore: StateFlow<Int> = _maxScore.asStateFlow()
@Synchronized
fun submit(event: DetectionEvent) {
val cutoff = nowMs() - retentionMs
val merged = (_events.value + event)
.filter { it.timestampMs >= cutoff }
.groupBy { it.source to it.key }
.map { (_, list) -> list.maxByOrNull { it.timestampMs }!! }
.sortedByDescending { it.score }
_events.value = merged
recompute(merged)
}
@Synchronized
fun clear() {
_events.value = emptyList()
_threatLevel.value = ThreatLevel.GREEN
_maxScore.value = 0
}
@Synchronized
fun pruneExpired() {
val cutoff = nowMs() - retentionMs
val live = _events.value.filter { it.timestampMs >= cutoff }
if (live.size != _events.value.size) {
_events.value = live
recompute(live)
}
}
private fun recompute(live: List<DetectionEvent>) {
val max = live.maxOfOrNull { it.score } ?: 0
_maxScore.value = max
_threatLevel.value = ThreatLevel.fromScore(max)
}
}
@@ -0,0 +1,41 @@
package org.soulstone.overwatch.fusion
/**
* Tracks RSSI samples per device to detect a stationary signature.
*
* Ported from flock-detection (rssi_track_update / rssi_track_is_stationary):
* a fixed-installation camera produces a rise → peak → fall pattern as the
* observer walks past it. A handheld emitter (phone, etc.) does not.
*
* Algorithm:
* - Keep up to [windowSize] recent samples per key.
* - Find peak index. Stationary if peak is NOT at the edge AND
* range (peak - min(first, last)) >= [minRangeDb].
*/
class RssiTracker(
private val windowSize: Int = 15,
private val minRangeDb: Int = 6
) {
private val samples: MutableMap<String, ArrayDeque<Int>> = mutableMapOf()
@Synchronized
fun update(key: String, rssi: Int) {
val deque = samples.getOrPut(key) { ArrayDeque() }
deque.addLast(rssi)
while (deque.size > windowSize) deque.removeFirst()
}
@Synchronized
fun isStationary(key: String): Boolean {
val s = samples[key] ?: return false
if (s.size < 3) return false
val list = s.toList()
val peakIdx = list.indices.maxByOrNull { list[it] } ?: return false
if (peakIdx == 0 || peakIdx == list.lastIndex) return false
val edgeMin = minOf(list.first(), list.last())
return (list[peakIdx] - edgeMin) >= minRangeDb
}
@Synchronized
fun clear() = samples.clear()
}
@@ -0,0 +1,24 @@
package org.soulstone.overwatch.fusion
/**
* 4-tier threat classification. Maps directly to the green/yellow/orange/red UI circle.
* Thresholds ported from flock-detection's CONFIDENCE_* constants.
*/
enum class ThreatLevel(val minScore: Int) {
GREEN(0), // < 40 — nothing credible
YELLOW(40), // 40-69 — single weak indicator
ORANGE(70), // 70-84 — high confidence
RED(85); // 85+ — certain
companion object {
fun fromScore(score: Int): ThreatLevel = when {
score >= RED.minScore -> RED
score >= ORANGE.minScore -> ORANGE
score >= YELLOW.minScore -> YELLOW
else -> GREEN
}
}
}
/** Logical signal channel — used in the drill-down UI. */
enum class DetectionSource { BLE, WIFI, DEFLOCK, WAZE }