v5: restore WiFi routing fix, fix send queue

FIXES:
- Restored port 5000 (Bee's odc-api runs on 5000, not 5001)
- Restored multi-IP discovery (primary: Bee AP, alt: home WiFi)
- Added custom DNS resolver for ADAMaps (hardcodes 142.44.213.229)
  This fixes the '0 sent' bug - DNS fails on Bee AP (no upstream)
- Restored alt URL field in settings
- Kept exponential backoff and retry queue from v3

The DNS fix is the key: when connected to Bee's AP (192.168.0.10),
Android's DNS resolver can't resolve api.adamaps.org because
the AP has no internet. Custom Dns object bypasses this.
This commit is contained in:
Kayos 2026-03-11 10:06:55 -07:00
parent 3cac92fbfd
commit 2b8c19eef1
7 changed files with 217 additions and 34 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId = "com.adamaps.varroa" applicationId = "com.adamaps.varroa"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 4 versionCode = 5
versionName = "1.3.0" versionName = "1.4.0"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View file

@ -5,18 +5,41 @@ import com.adamaps.varroa.data.ApiResult
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Dns
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/**
* Custom DNS resolver that hardcodes api.adamaps.org to bypass Android's DNS
* when connected to Bee's AP (which has no upstream DNS server).
*/
private object AdaMapsDns : Dns {
// api.adamaps.org resolves to this IP
private val ADAMAPS_IP = "142.44.213.229"
override fun lookup(hostname: String): List<InetAddress> {
return if (hostname.equals("api.adamaps.org", ignoreCase = true)) {
// Return hardcoded IP to bypass DNS lookup
listOf(InetAddress.getByName(ADAMAPS_IP))
} else {
// Fall back to system DNS for other hosts
Dns.SYSTEM.lookup(hostname)
}
}
}
class AdaMapsApiClient( class AdaMapsApiClient(
private var apiUrl: String = "https://api.adamaps.org", private var apiUrl: String = "https://api.adamaps.org",
private var apiKey: String = "mapnet-ingest-2026" private var apiKey: String = "mapnet-ingest-2026"
) { ) {
// Use custom DNS resolver to handle Bee AP's lack of upstream DNS
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.dns(AdaMapsDns)
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS)

View file

@ -11,28 +11,40 @@ import com.adamaps.varroa.data.GnssData
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class BeeApiClient( class BeeApiClient(
private var baseUrl: String = "http://192.168.0.10:5001" private var primaryUrl: String = "http://192.168.0.10:5000",
private var altUrl: String = "http://192.168.0.155:5000"
) { ) {
private var client = buildClient(null) private var client = buildClient(null)
private var fastClient = buildFastClient(null)
private val gson = Gson() private val gson = Gson()
// Track which URL is currently active (null = unknown/offline)
private var activeUrl: String? = null
// Connection state // Connection state
var isConnected: Boolean = false var isConnected: Boolean = false
private set private set
fun updateBaseUrl(url: String) { fun updateUrls(primary: String, alt: String) {
baseUrl = url.trimEnd('/') primaryUrl = primary.trimEnd('/')
altUrl = alt.trimEnd('/')
// Reset active URL when settings change
activeUrl = null
} }
fun bindToWifiNetwork(context: Context) { fun bindToWifiNetwork(context: Context) {
client = buildClient(getWifiNetwork(context)) val net = getWifiNetwork(context)
client = buildClient(net)
fastClient = buildFastClient(net)
} }
private fun buildClient(net: Network?): OkHttpClient { private fun buildClient(net: Network?): OkHttpClient {
@ -44,6 +56,15 @@ class BeeApiClient(
return b.build() return b.build()
} }
private fun buildFastClient(net: Network?): OkHttpClient {
val b = OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
net?.let { b.socketFactory(it.socketFactory) }
return b.build()
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun getWifiNetwork(context: Context): Network? { private fun getWifiNetwork(context: Context): Network? {
return try { return try {
@ -58,25 +79,27 @@ class BeeApiClient(
} catch (e: Exception) { null } } catch (e: Exception) { null }
} }
private suspend fun getRaw(path: String): ApiResult<String> = withContext(Dispatchers.IO) { private suspend fun getRaw(path: String, useActiveUrl: Boolean = true): ApiResult<String> = withContext(Dispatchers.IO) {
val baseUrl = if (useActiveUrl && activeUrl != null) activeUrl!! else primaryUrl
try { try {
val req = Request.Builder().url("$baseUrl$path").get().build() val req = Request.Builder().url("$baseUrl$path").get().build()
client.newCall(req).execute().use { resp -> client.newCall(req).execute().use { resp ->
val body = resp.body?.string() ?: "" val body = resp.body?.string() ?: ""
if (resp.isSuccessful) { if (resp.isSuccessful) {
isConnected = true isConnected = true
activeUrl = baseUrl
ApiResult.Success(body) ApiResult.Success(body)
} else { } else {
ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code) ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
isConnected = false
ApiResult.Error(e.message ?: "Unknown error") ApiResult.Error(e.message ?: "Unknown error")
} }
} }
private suspend fun getBytes(path: String): ApiResult<ByteArray> = withContext(Dispatchers.IO) { private suspend fun getBytes(path: String): ApiResult<ByteArray> = withContext(Dispatchers.IO) {
val baseUrl = activeUrl ?: primaryUrl
try { try {
val req = Request.Builder().url("$baseUrl$path").get().build() val req = Request.Builder().url("$baseUrl$path").get().build()
client.newCall(req).execute().use { resp -> client.newCall(req).execute().use { resp ->
@ -89,12 +112,95 @@ class BeeApiClient(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
isConnected = false
ApiResult.Error(e.message ?: "Unknown error") ApiResult.Error(e.message ?: "Unknown error")
} }
} }
/**
* 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) {
try {
val req = Request.Builder().url("$url/api/1/info").get().build()
fastClient.newCall(req).execute().use { resp ->
if (resp.isSuccessful) url else null
}
} catch (e: Exception) {
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) {
val result = tryPing(activeUrl!!)
if (result != null) {
isConnected = true
return@withContext result
}
// Active URL failed, clear it and try both
activeUrl = null
}
// Try both URLs in parallel
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) {
altDeferred.cancel()
return@withTimeoutOrNull primary
}
altDeferred.await()
}
if (result != null) {
activeUrl = result
isConnected = true
} else {
isConnected = false
}
result
}
/**
* Check if Bee is reachable (with discovery fallback).
* Updates internal connection state.
*/
suspend fun ping(): Boolean {
val url = discoverActiveUrl()
return url != null
}
/**
* Get the currently active URL (or null if offline).
*/
fun getActiveUrl(): String? = activeUrl
/**
* Force offline state (for exponential backoff scenarios).
*/
fun setOffline() {
isConnected = false
activeUrl = null
}
suspend fun getLandmarks(): ApiResult<List<BeeDetection>> = withContext(Dispatchers.IO) { suspend fun getLandmarks(): ApiResult<List<BeeDetection>> = withContext(Dispatchers.IO) {
// If no active URL, try discovery first
if (activeUrl == null) {
if (discoverActiveUrl() == null) {
isConnected = false
return@withContext ApiResult.Error("Bee offline")
}
}
when (val r = getRaw("/api/1/landmarks/last/200")) { when (val r = getRaw("/api/1/landmarks/last/200")) {
is ApiResult.Success -> try { is ApiResult.Success -> try {
val type = object : TypeToken<List<BeeDetection>>() {}.type val type = object : TypeToken<List<BeeDetection>>() {}.type
@ -105,13 +211,24 @@ class BeeApiClient(
ApiResult.Error("Parse error: ${e.message}") ApiResult.Error("Parse error: ${e.message}")
} }
is ApiResult.Error -> { is ApiResult.Error -> {
isConnected = false // Connection failed, clear active URL for next attempt
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)) {
activeUrl = null
isConnected = false
}
r r
} }
} }
} }
suspend fun getGnss(): ApiResult<GnssData> = withContext(Dispatchers.IO) { suspend fun getGnss(): ApiResult<GnssData> = withContext(Dispatchers.IO) {
if (activeUrl == null && discoverActiveUrl() == null) {
return@withContext ApiResult.Error("Bee offline")
}
when (val r = getRaw("/api/1/gnssConcise/latestValid")) { when (val r = getRaw("/api/1/gnssConcise/latestValid")) {
is ApiResult.Success -> try { is ApiResult.Success -> try {
ApiResult.Success(gson.fromJson(r.data, GnssData::class.java)) ApiResult.Success(gson.fromJson(r.data, GnssData::class.java))
@ -123,6 +240,10 @@ class BeeApiClient(
} }
suspend fun getDeviceInfo(): ApiResult<BeeDeviceInfo> = withContext(Dispatchers.IO) { suspend fun getDeviceInfo(): ApiResult<BeeDeviceInfo> = withContext(Dispatchers.IO) {
if (activeUrl == null && discoverActiveUrl() == null) {
return@withContext ApiResult.Error("Bee offline")
}
when (val r = getRaw("/api/1/info")) { when (val r = getRaw("/api/1/info")) {
is ApiResult.Success -> try { is ApiResult.Success -> try {
ApiResult.Success(gson.fromJson(r.data, BeeDeviceInfo::class.java)) ApiResult.Success(gson.fromJson(r.data, BeeDeviceInfo::class.java))
@ -135,13 +256,23 @@ class BeeApiClient(
/** /**
* Try the given endpoint; returns raw image bytes. * Try the given endpoint; returns raw image bytes.
* The caller is responsible for trying fallback endpoints.
*/ */
suspend fun getCameraFrame(endpoint: String): ApiResult<ByteArray> = getBytes(endpoint) suspend fun getCameraFrame(endpoint: String): ApiResult<ByteArray> {
if (activeUrl == null && discoverActiveUrl() == null) {
return ApiResult.Error("Bee offline")
}
return getBytes(endpoint)
}
/** /**
* Try multiple camera endpoints in order, return first success. * Try multiple camera endpoints in order, return first success.
*/ */
suspend fun getCameraFrameAuto(configured: String): Pair<String, ApiResult<ByteArray>> { suspend fun getCameraFrameAuto(configured: String): Pair<String, ApiResult<ByteArray>> {
if (activeUrl == null && discoverActiveUrl() == null) {
return configured to ApiResult.Error("Bee offline")
}
val candidates = listOf( val candidates = listOf(
configured, configured,
"/api/1/camera/frame", "/api/1/camera/frame",
@ -155,6 +286,4 @@ class BeeApiClient(
} }
return configured to ApiResult.Error("No camera endpoint responded") return configured to ApiResult.Error("No camera endpoint responded")
} }
suspend fun ping(): Boolean = getDeviceInfo() is ApiResult.Success
} }

View file

@ -3,6 +3,7 @@ package com.adamaps.varroa.data
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
@ -13,44 +14,52 @@ import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "varroa_settings") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "varroa_settings")
data class VarroaSettings( data class VarroaSettings(
val beeApiUrl: String = "http://192.168.0.10:5001", 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 adamapsApiUrl: String = "https://api.adamaps.org",
val adamapsApiKey: String = "mapnet-ingest-2026", val adamapsApiKey: String = "mapnet-ingest-2026",
val pollIntervalSeconds: Int = 30, val pollIntervalSeconds: Int = 30,
val cameraEndpoint: String = "/api/1/camera/frame",
val cameraRefreshSeconds: Int = 30, val cameraRefreshSeconds: Int = 30,
val cameraEndpoint: String = "/api/1/camera/frame" val forwardingEnabled: Boolean = true
) )
class SettingsDataStore(private val context: Context) { class SettingsDataStore(private val context: Context) {
companion object { companion object {
private val KEY_BEE_URL = stringPreferencesKey("bee_api_url") 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_URL = stringPreferencesKey("adamaps_api_url")
private val KEY_ADAMAPS_KEY = stringPreferencesKey("adamaps_api_key") private val KEY_ADAMAPS_KEY = stringPreferencesKey("adamaps_api_key")
private val KEY_POLL_INTERVAL = intPreferencesKey("poll_interval_seconds") private val KEY_POLL_INTERVAL = intPreferencesKey("poll_interval_seconds")
private val KEY_CAMERA_REFRESH = intPreferencesKey("camera_refresh_seconds")
private val KEY_CAMERA_ENDPOINT = stringPreferencesKey("camera_endpoint") private val KEY_CAMERA_ENDPOINT = stringPreferencesKey("camera_endpoint")
private val KEY_CAMERA_REFRESH = intPreferencesKey("camera_refresh_seconds")
private val KEY_FORWARDING_ENABLED = booleanPreferencesKey("forwarding_enabled")
} }
val settings: Flow<VarroaSettings> = context.dataStore.data.map { prefs -> val settings: Flow<VarroaSettings> = context.dataStore.data.map { prefs ->
VarroaSettings( VarroaSettings(
beeApiUrl = prefs[KEY_BEE_URL] ?: "http://192.168.0.10:5001", 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", adamapsApiUrl = prefs[KEY_ADAMAPS_URL] ?: "https://api.adamaps.org",
adamapsApiKey = prefs[KEY_ADAMAPS_KEY] ?: "mapnet-ingest-2026", adamapsApiKey = prefs[KEY_ADAMAPS_KEY] ?: "mapnet-ingest-2026",
pollIntervalSeconds = prefs[KEY_POLL_INTERVAL] ?: 30, pollIntervalSeconds = prefs[KEY_POLL_INTERVAL] ?: 30,
cameraEndpoint = prefs[KEY_CAMERA_ENDPOINT] ?: "/api/1/camera/frame",
cameraRefreshSeconds = prefs[KEY_CAMERA_REFRESH] ?: 30, cameraRefreshSeconds = prefs[KEY_CAMERA_REFRESH] ?: 30,
cameraEndpoint = prefs[KEY_CAMERA_ENDPOINT] ?: "/api/1/camera/frame" forwardingEnabled = prefs[KEY_FORWARDING_ENABLED] ?: true
) )
} }
suspend fun save(s: VarroaSettings) { suspend fun save(s: VarroaSettings) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[KEY_BEE_URL] = s.beeApiUrl prefs[KEY_BEE_URL] = s.beeApiUrl
prefs[KEY_BEE_ALT_URL] = s.beeAltApiUrl
prefs[KEY_ADAMAPS_URL] = s.adamapsApiUrl prefs[KEY_ADAMAPS_URL] = s.adamapsApiUrl
prefs[KEY_ADAMAPS_KEY] = s.adamapsApiKey prefs[KEY_ADAMAPS_KEY] = s.adamapsApiKey
prefs[KEY_POLL_INTERVAL] = s.pollIntervalSeconds prefs[KEY_POLL_INTERVAL] = s.pollIntervalSeconds
prefs[KEY_CAMERA_REFRESH] = s.cameraRefreshSeconds
prefs[KEY_CAMERA_ENDPOINT] = s.cameraEndpoint prefs[KEY_CAMERA_ENDPOINT] = s.cameraEndpoint
prefs[KEY_CAMERA_REFRESH] = s.cameraRefreshSeconds
prefs[KEY_FORWARDING_ENABLED] = s.forwardingEnabled
} }
} }
} }

View file

@ -139,7 +139,7 @@ class ForwardingService : LifecycleService() {
} }
private fun applySettings(s: VarroaSettings) { private fun applySettings(s: VarroaSettings) {
beeClient.updateBaseUrl(s.beeApiUrl) beeClient.updateUrls(s.beeApiUrl, s.beeAltApiUrl)
adamapsClient.updateConfig(s.adamapsApiUrl, s.adamapsApiKey) adamapsClient.updateConfig(s.adamapsApiUrl, s.adamapsApiKey)
} }
@ -187,7 +187,14 @@ class ForwardingService : LifecycleService() {
consecutiveFailures = 0 consecutiveFailures = 0
currentBackoffMs = MIN_BACKOFF_MS currentBackoffMs = MIN_BACKOFF_MS
_lastError.value = null _lastError.value = null
_beeStatus.value = "Connected"
// 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)"
}
val newDetections = result.data.filter { d -> val newDetections = result.data.filter { d ->
val key = d.dedupKey() val key = d.dedupKey()
@ -213,15 +220,19 @@ class ForwardingService : LifecycleService() {
MAX_BACKOFF_MS MAX_BACKOFF_MS
) )
// Clean offline status // Set clean offline status instead of ugly error
_beeStatus.value = "Bee offline (retry in ${currentBackoffMs / 1000}s)" if (result.message == "Bee offline") {
// Don't show red error banner for connection timeouts _beeStatus.value = "Bee offline (retry in ${currentBackoffMs / 1000}s)"
if (!result.message.contains("timeout", ignoreCase = true) && _lastError.value = null // Don't show red error for expected offline state
!result.message.contains("connect", ignoreCase = true) && } else if (result.message.contains("timeout", ignoreCase = true) ||
!result.message.contains("refused", ignoreCase = true)) { result.message.contains("connect", ignoreCase = true) ||
_lastError.value = result.message result.message.contains("refused", ignoreCase = true) ||
} else { result.message.contains("unreachable", ignoreCase = true)) {
_beeStatus.value = "Bee offline (retry in ${currentBackoffMs / 1000}s)"
_lastError.value = null _lastError.value = null
} else {
_beeStatus.value = "Connection error"
_lastError.value = result.message
} }
} }
} }

View file

@ -32,6 +32,7 @@ fun SettingsScreen(
// Local edit state — initialized from current settings // Local edit state — initialized from current settings
var beeApiUrl by remember(currentSettings) { mutableStateOf(currentSettings.beeApiUrl) } 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 adamapsApiUrl by remember(currentSettings) { mutableStateOf(currentSettings.adamapsApiUrl) }
var adamapsApiKey by remember(currentSettings) { mutableStateOf(currentSettings.adamapsApiKey) } var adamapsApiKey by remember(currentSettings) { mutableStateOf(currentSettings.adamapsApiKey) }
var pollInterval by remember(currentSettings) { mutableStateOf(currentSettings.pollIntervalSeconds.toString()) } var pollInterval by remember(currentSettings) { mutableStateOf(currentSettings.pollIntervalSeconds.toString()) }
@ -72,6 +73,7 @@ fun SettingsScreen(
vm.save( vm.save(
VarroaSettings( VarroaSettings(
beeApiUrl = beeApiUrl.trim(), beeApiUrl = beeApiUrl.trim(),
beeAltApiUrl = beeAltApiUrl.trim(),
adamapsApiUrl = adamapsApiUrl.trim(), adamapsApiUrl = adamapsApiUrl.trim(),
adamapsApiKey = adamapsApiKey.trim(), adamapsApiKey = adamapsApiKey.trim(),
pollIntervalSeconds = pollInterval.toIntOrNull() ?: 30, pollIntervalSeconds = pollInterval.toIntOrNull() ?: 30,
@ -96,15 +98,23 @@ fun SettingsScreen(
) { ) {
SettingsSection("BEE DEVICE") { SettingsSection("BEE DEVICE") {
SettingsField( SettingsField(
label = "Bee API URL", label = "Bee API URL (Primary)",
value = beeApiUrl, value = beeApiUrl,
onValueChange = { beeApiUrl = it }, onValueChange = { beeApiUrl = it },
hint = "http://192.168.0.10:5001" 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)) Spacer(Modifier.height(8.dp))
Text( Text(
"Port 5001 = socat proxy to odc-api\n" + "Primary = Bee's own AP (192.168.0.10)\n" +
"Must run proxy setup on Bee first", "Alt = Home WiFi IP when Bee is docked\n" +
"App tries both automatically",
color = Color.Gray, color = Color.Gray,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 10.sp, fontSize = 10.sp,

View file

@ -74,7 +74,8 @@ class DashboardViewModel(app: Application) : AndroidViewModel(app) {
} }
private fun applySettings(s: VarroaSettings) { private fun applySettings(s: VarroaSettings) {
beeClient.updateBaseUrl(s.beeApiUrl) beeClient.updateUrls(s.beeApiUrl, s.beeAltApiUrl)
beeClient.bindToWifiNetwork(getApplication())
} }
private fun startPolling(s: VarroaSettings) { private fun startPolling(s: VarroaSettings) {