diff --git a/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt b/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt index 3f5d685..2e9f7c4 100644 --- a/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt +++ b/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt @@ -20,8 +20,7 @@ import okhttp3.Request import java.util.concurrent.TimeUnit class BeeApiClient( - private var primaryUrl: String = "http://192.168.0.10:5000", - private var altUrl: String = "http://192.168.0.155:5000" + private var apiUrl: String = "http://192.168.0.10:5000" ) { companion object { private const val TAG = "VarroaBeeAPI" @@ -31,22 +30,14 @@ class BeeApiClient( private var fastClient = buildFastClient(null) private val gson = Gson() - // Track which URL is currently active (null = unknown/offline) - private var activeUrl: String? = null - // Connection state var isConnected: Boolean = false private set - fun updateUrls(primary: String, alt: String) { - val oldPrimary = primaryUrl - val oldAlt = altUrl - primaryUrl = primary.trimEnd('/') - altUrl = alt.trimEnd('/') - Log.d(TAG, "URLs updated from [$oldPrimary, $oldAlt] to [$primaryUrl, $altUrl]") - // Reset active URL when settings change - activeUrl = null - Log.d(TAG, "Active URL reset due to configuration change") + fun updateUrl(url: String) { + val oldUrl = apiUrl + apiUrl = url.trimEnd('/') + Log.d(TAG, "URL updated from $oldUrl to $apiUrl") } /** @@ -103,9 +94,8 @@ class BeeApiClient( } catch (e: Exception) { null } } - private suspend fun getRaw(path: String, useActiveUrl: Boolean = true): ApiResult = withContext(Dispatchers.IO) { - val baseUrl = if (useActiveUrl && activeUrl != null) activeUrl!! else primaryUrl - val fullUrl = "$baseUrl$path" + private suspend fun getRaw(path: String): ApiResult = withContext(Dispatchers.IO) { + val fullUrl = "$apiUrl$path" Log.d(TAG, "HTTP GET request to: $fullUrl") try { @@ -114,7 +104,6 @@ class BeeApiClient( val body = resp.body?.string() ?: "" if (resp.isSuccessful) { isConnected = true - activeUrl = baseUrl Log.d(TAG, "HTTP ${resp.code} OK - response length: ${body.length} chars") ApiResult.Success(body) } else { @@ -129,9 +118,8 @@ class BeeApiClient( } private suspend fun getBytes(path: String): ApiResult = withContext(Dispatchers.IO) { - val baseUrl = activeUrl ?: primaryUrl try { - val req = Request.Builder().url("$baseUrl$path").get().build() + val req = Request.Builder().url("$apiUrl$path").get().build() client.newCall(req).execute().use { resp -> val bytes = resp.body?.bytes() ?: ByteArray(0) if (resp.isSuccessful && bytes.isNotEmpty()) { @@ -147,109 +135,38 @@ class BeeApiClient( } /** - * Try to ping the Bee at the given URL with a short timeout. - * Returns the URL if successful, null otherwise. - */ - private suspend fun tryPing(url: String): String? = withContext(Dispatchers.IO) { - Log.d(TAG, "Ping attempt to: $url/api/1/info") - try { - val req = Request.Builder().url("$url/api/1/info").get().build() - fastClient.newCall(req).execute().use { resp -> - if (resp.isSuccessful) { - Log.d(TAG, "Ping successful: $url (HTTP ${resp.code})") - url - } else { - Log.d(TAG, "Ping failed: $url (HTTP ${resp.code})") - null - } - } - } catch (e: Exception) { - Log.d(TAG, "Ping exception: $url - ${e.message}") - null - } - } - - /** - * Try both IPs in parallel with short timeouts. Return the first one that responds. - * If we already have an active URL, try it first (fast path). - */ - suspend fun discoverActiveUrl(): String? = withContext(Dispatchers.IO) { - // Fast path: if we have an active URL, try it first - if (activeUrl != null) { - Log.d(TAG, "Fast path - testing existing active URL: $activeUrl") - val result = tryPing(activeUrl!!) - if (result != null) { - isConnected = true - Log.i(TAG, "Active URL still works: $activeUrl") - return@withContext result - } - // Active URL failed, clear it and try both - Log.w(TAG, "Active URL $activeUrl failed - falling back to discovery") - activeUrl = null - } - - // Try both URLs in parallel - Log.d(TAG, "Starting parallel discovery: [$primaryUrl, $altUrl]") - val primaryDeferred = async { tryPing(primaryUrl) } - val altDeferred = async { tryPing(altUrl) } - - // Wait for first success with timeout - val result = withTimeoutOrNull(4000L) { - val primary = primaryDeferred.await() - if (primary != null) { - Log.d(TAG, "Primary URL responded first, cancelling alt") - altDeferred.cancel() - return@withTimeoutOrNull primary - } - Log.d(TAG, "Primary URL failed, waiting for alt") - altDeferred.await() - } - - if (result != null) { - activeUrl = result - isConnected = true - Log.i(TAG, "Discovery successful - active URL: $result") - } else { - isConnected = false - Log.w(TAG, "Discovery failed - no URLs responded within 4s timeout") - } - result - } - - /** - * Check if Bee is reachable (with discovery fallback). + * Check if Bee is reachable. * Updates internal connection state. */ suspend fun ping(): Boolean { - val url = discoverActiveUrl() - return url != null + return when (getRaw("/api/1/info")) { + is ApiResult.Success -> { + isConnected = true + Log.i(TAG, "Ping successful") + true + } + is ApiResult.Error -> { + isConnected = false + Log.w(TAG, "Ping failed") + false + } + } } /** - * Get the currently active URL (or null if offline). + * Get the currently configured URL. */ - fun getActiveUrl(): String? = activeUrl + fun getApiUrl(): String = apiUrl /** * Force offline state (for exponential backoff scenarios). */ fun setOffline() { isConnected = false - activeUrl = null } suspend fun getLandmarks(): ApiResult> = withContext(Dispatchers.IO) { - Log.d(TAG, "getLandmarks() called - activeUrl: $activeUrl") - - // If no active URL, try discovery first - if (activeUrl == null) { - Log.d(TAG, "No active URL - starting discovery...") - if (discoverActiveUrl() == null) { - isConnected = false - Log.e(TAG, "Discovery failed - marking Bee as offline") - return@withContext ApiResult.Error("Bee offline") - } - } + Log.d(TAG, "getLandmarks() called") when (val r = getRaw("/api/1/landmarks/last/200")) { is ApiResult.Success -> try { @@ -265,13 +182,12 @@ class BeeApiClient( } is ApiResult.Error -> { Log.e(TAG, "getLandmarks() failed: ${r.message} (code: ${r.code})") - // Connection failed, clear active URL for next attempt + // Connection failed, mark as offline if (r.message.contains("timeout", ignoreCase = true) || r.message.contains("connect", ignoreCase = true) || r.message.contains("refused", ignoreCase = true) || r.message.contains("unreachable", ignoreCase = true)) { - Log.d(TAG, "Network error detected - clearing active URL") - activeUrl = null + Log.d(TAG, "Network error detected - marking as offline") isConnected = false } r @@ -280,10 +196,6 @@ class BeeApiClient( } suspend fun getGnss(): ApiResult = withContext(Dispatchers.IO) { - if (activeUrl == null && discoverActiveUrl() == null) { - return@withContext ApiResult.Error("Bee offline") - } - when (val r = getRaw("/api/1/gnssConcise/latestValid")) { is ApiResult.Success -> try { ApiResult.Success(gson.fromJson(r.data, GnssData::class.java)) @@ -295,10 +207,6 @@ class BeeApiClient( } suspend fun getDeviceInfo(): ApiResult = withContext(Dispatchers.IO) { - if (activeUrl == null && discoverActiveUrl() == null) { - return@withContext ApiResult.Error("Bee offline") - } - when (val r = getRaw("/api/1/info")) { is ApiResult.Success -> try { ApiResult.Success(gson.fromJson(r.data, BeeDeviceInfo::class.java)) @@ -314,9 +222,6 @@ class BeeApiClient( * The caller is responsible for trying fallback endpoints. */ suspend fun getCameraFrame(endpoint: String): ApiResult { - if (activeUrl == null && discoverActiveUrl() == null) { - return ApiResult.Error("Bee offline") - } return getBytes(endpoint) } @@ -324,10 +229,6 @@ class BeeApiClient( * Try multiple camera endpoints in order, return first success. */ suspend fun getCameraFrameAuto(configured: String): Pair> { - if (activeUrl == null && discoverActiveUrl() == null) { - return configured to ApiResult.Error("Bee offline") - } - val candidates = listOf( configured, "/api/1/camera/frame", diff --git a/app/src/main/java/com/adamaps/varroa/data/Models.kt b/app/src/main/java/com/adamaps/varroa/data/Models.kt index 0d17789..2790353 100644 --- a/app/src/main/java/com/adamaps/varroa/data/Models.kt +++ b/app/src/main/java/com/adamaps/varroa/data/Models.kt @@ -16,7 +16,8 @@ data class BeeDetection( @SerializedName("width") val width: Double? = null, @SerializedName("height") val height: Double? = null, @SerializedName("pos_confidence") val posConfidence: Double? = null, - @SerializedName("azimuth") val azimuth: Double? = null + @SerializedName("azimuth") val azimuth: Double? = null, + @SerializedName("device_id") val deviceId: String? = null ) { /** Dedup key */ fun dedupKey(): String = "${id}_${ts}" diff --git a/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt b/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt index fbb3438..c84d965 100644 --- a/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt +++ b/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt @@ -15,7 +15,6 @@ private val Context.dataStore: DataStore by preferencesDataStore(na data class VarroaSettings( val beeApiUrl: String = "http://192.168.0.10:5000", - val beeAltApiUrl: String = "http://192.168.0.155:5000", val adamapsApiUrl: String = "https://api.adamaps.org", val adamapsApiKey: String = "***REMOVED***", val pollIntervalSeconds: Int = 30, @@ -28,7 +27,6 @@ class SettingsDataStore(private val context: Context) { companion object { private val KEY_BEE_URL = stringPreferencesKey("bee_api_url") - private val KEY_BEE_ALT_URL = stringPreferencesKey("bee_alt_api_url") private val KEY_ADAMAPS_URL = stringPreferencesKey("adamaps_api_url") private val KEY_ADAMAPS_KEY = stringPreferencesKey("adamaps_api_key") private val KEY_POLL_INTERVAL = intPreferencesKey("poll_interval_seconds") @@ -40,7 +38,6 @@ class SettingsDataStore(private val context: Context) { val settings: Flow = context.dataStore.data.map { prefs -> VarroaSettings( beeApiUrl = prefs[KEY_BEE_URL] ?: "http://192.168.0.10:5000", - beeAltApiUrl = prefs[KEY_BEE_ALT_URL] ?: "http://192.168.0.155:5000", adamapsApiUrl = prefs[KEY_ADAMAPS_URL] ?: "https://api.adamaps.org", adamapsApiKey = prefs[KEY_ADAMAPS_KEY] ?: "***REMOVED***", pollIntervalSeconds = prefs[KEY_POLL_INTERVAL] ?: 30, @@ -53,7 +50,6 @@ class SettingsDataStore(private val context: Context) { suspend fun save(s: VarroaSettings) { context.dataStore.edit { prefs -> prefs[KEY_BEE_URL] = s.beeApiUrl - prefs[KEY_BEE_ALT_URL] = s.beeAltApiUrl prefs[KEY_ADAMAPS_URL] = s.adamapsApiUrl prefs[KEY_ADAMAPS_KEY] = s.adamapsApiKey prefs[KEY_POLL_INTERVAL] = s.pollIntervalSeconds diff --git a/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt b/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt index e68e8a3..cf2f2ad 100644 --- a/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt +++ b/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt @@ -138,10 +138,10 @@ class BeeCollectorService : LifecycleService() { lifecycleScope.launch { Log.d(TAG, "Loading settings from DataStore...") val settings = settingsStore.settings.first() - Log.d(TAG, "Settings loaded - beeApiUrl: ${settings.beeApiUrl}, altUrl: ${settings.beeAltApiUrl}, pollInterval: ${settings.pollIntervalSeconds}s") + Log.d(TAG, "Settings loaded - beeApiUrl: ${settings.beeApiUrl}, pollInterval: ${settings.pollIntervalSeconds}s") - beeClient.updateUrls(settings.beeApiUrl, settings.beeAltApiUrl) - Log.d(TAG, "BeeApiClient URLs updated") + beeClient.updateUrl(settings.beeApiUrl) + Log.d(TAG, "BeeApiClient URL updated") // Bind to Bee network (unvalidated WiFi) Log.d(TAG, "Attempting to bind to Bee network...") @@ -234,13 +234,9 @@ class BeeCollectorService : LifecycleService() { _lastError.value = null Log.i(TAG, "Successfully connected to Bee - received ${result.data.size} detections") - val activeUrl = beeClient.getActiveUrl() - _beeStatus.value = if (activeUrl?.contains("155") == true) { - "Connected (home WiFi)" - } else { - "Connected (AP mode)" - } - Log.d(TAG, "Active URL: $activeUrl, Status: ${_beeStatus.value}") + val apiUrl = beeClient.getApiUrl() + _beeStatus.value = "Connected" + Log.d(TAG, "API URL: $apiUrl, Status: ${_beeStatus.value}") // Filter for new detections val newDetections = result.data.filter { d -> @@ -287,7 +283,24 @@ class BeeCollectorService : LifecycleService() { } private suspend fun storeDetections(detections: List) { - val deviceId = _currentDeviceId.value + // Extract device_id from first detection if available, otherwise use cached value + val deviceIdFromDetection = detections.firstOrNull()?.deviceId + val deviceId = if (!deviceIdFromDetection.isNullOrBlank()) { + // Update cached device ID if found in detection + if (deviceIdFromDetection != _currentDeviceId.value) { + Log.i(TAG, "Device ID found in landmark data: $deviceIdFromDetection") + _currentDeviceId.value = deviceIdFromDetection + } + deviceIdFromDetection + } else { + // Fallback to cached device ID + val cached = _currentDeviceId.value + if (cached == "unknown") { + Log.w(TAG, "No device_id in landmark data and cached value is 'unknown'") + } + cached + } + Log.d(TAG, "Converting ${detections.size} BeeDetections to DetectionEntities with deviceId: $deviceId") val entities = detections.map { d -> diff --git a/app/src/main/java/com/adamaps/varroa/service/ForwardingService.kt b/app/src/main/java/com/adamaps/varroa/service/ForwardingService.kt index 1555984..e54d59d 100644 --- a/app/src/main/java/com/adamaps/varroa/service/ForwardingService.kt +++ b/app/src/main/java/com/adamaps/varroa/service/ForwardingService.kt @@ -145,7 +145,7 @@ class ForwardingService : LifecycleService() { } private fun applySettings(s: VarroaSettings) { - beeClient.updateUrls(s.beeApiUrl, s.beeAltApiUrl) + beeClient.updateUrl(s.beeApiUrl) adamapsClient.updateConfig(s.adamapsApiUrl, s.adamapsApiKey) } @@ -195,13 +195,9 @@ class ForwardingService : LifecycleService() { currentBackoffMs = MIN_BACKOFF_MS _lastError.value = null - // Show which IP we're connected to - val activeUrl = beeClient.getActiveUrl() - _beeStatus.value = if (activeUrl?.contains("155") == true) { - "Connected (home WiFi)" - } else { - "Connected (AP mode)" - } + // Show connection status + val apiUrl = beeClient.getApiUrl() + _beeStatus.value = "Connected" val newDetections = result.data.filter { d -> val key = d.dedupKey() diff --git a/app/src/main/java/com/adamaps/varroa/ui/settings/SettingsScreen.kt b/app/src/main/java/com/adamaps/varroa/ui/settings/SettingsScreen.kt index 6ec4ce4..67ba3ad 100644 --- a/app/src/main/java/com/adamaps/varroa/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/adamaps/varroa/ui/settings/SettingsScreen.kt @@ -32,7 +32,6 @@ fun SettingsScreen( // Local edit state — initialized from current settings var beeApiUrl by remember(currentSettings) { mutableStateOf(currentSettings.beeApiUrl) } - var beeAltApiUrl by remember(currentSettings) { mutableStateOf(currentSettings.beeAltApiUrl) } var adamapsApiUrl by remember(currentSettings) { mutableStateOf(currentSettings.adamapsApiUrl) } var adamapsApiKey by remember(currentSettings) { mutableStateOf(currentSettings.adamapsApiKey) } var pollInterval by remember(currentSettings) { mutableStateOf(currentSettings.pollIntervalSeconds.toString()) } @@ -73,7 +72,6 @@ fun SettingsScreen( vm.save( VarroaSettings( beeApiUrl = beeApiUrl.trim(), - beeAltApiUrl = beeAltApiUrl.trim(), adamapsApiUrl = adamapsApiUrl.trim(), adamapsApiKey = adamapsApiKey.trim(), pollIntervalSeconds = pollInterval.toIntOrNull() ?: 30, @@ -98,28 +96,11 @@ fun SettingsScreen( ) { SettingsSection("BEE DEVICE") { SettingsField( - label = "Bee API URL (Primary)", + label = "Bee API URL", value = beeApiUrl, onValueChange = { beeApiUrl = it }, hint = "http://192.168.0.10:5000" ) - Spacer(Modifier.height(8.dp)) - SettingsField( - label = "Bee Alt URL (Home WiFi)", - value = beeAltApiUrl, - onValueChange = { beeAltApiUrl = it }, - hint = "http://192.168.0.155:5000" - ) - Spacer(Modifier.height(8.dp)) - Text( - "Primary = Bee's own AP (192.168.0.10)\n" + - "Alt = Home WiFi IP when Bee is docked\n" + - "App tries both automatically", - color = Color.Gray, - fontFamily = FontFamily.Monospace, - fontSize = 10.sp, - lineHeight = 16.sp - ) } SettingsSection("ADAMAPS") { diff --git a/app/src/main/java/com/adamaps/varroa/viewmodel/DashboardViewModel.kt b/app/src/main/java/com/adamaps/varroa/viewmodel/DashboardViewModel.kt index 5b4cab7..a680b21 100644 --- a/app/src/main/java/com/adamaps/varroa/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/com/adamaps/varroa/viewmodel/DashboardViewModel.kt @@ -109,7 +109,7 @@ class DashboardViewModel(app: Application) : AndroidViewModel(app) { } private fun applySettings(s: VarroaSettings) { - beeClient.updateUrls(s.beeApiUrl, s.beeAltApiUrl) + beeClient.updateUrl(s.beeApiUrl) // Bind to Bee network if available val beeNet = networkMonitor.getBeeNetworkForBinding() diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index cf0b9bc..81d34f7 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -2,7 +2,6 @@ 192.168.0.10 - 10.0.0.1