varroa/app/src/main/java/com/adamaps/varroa/viewmodel/DashboardViewModel.kt
Kayos 7f7e857b1d 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.
2026-03-11 10:06:55 -07:00

161 lines
6.4 KiB
Kotlin

package com.adamaps.varroa.viewmodel
import android.app.Application
import android.content.Intent
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.adamaps.varroa.api.BeeApiClient
import com.adamaps.varroa.data.ApiResult
import com.adamaps.varroa.data.BeeDeviceInfo
import com.adamaps.varroa.data.GnssData
import com.adamaps.varroa.data.SettingsDataStore
import com.adamaps.varroa.data.VarroaSettings
import com.adamaps.varroa.service.ForwardingService
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class DashboardViewModel(app: Application) : AndroidViewModel(app) {
private val settingsStore = SettingsDataStore(app)
private val beeClient = BeeApiClient()
// ── Settings ──────────────────────────────────────────────────────────────
val settings: StateFlow<VarroaSettings> = settingsStore.settings
.stateIn(viewModelScope, SharingStarted.Eagerly, VarroaSettings())
// ── Device info ───────────────────────────────────────────────────────────
private val _deviceInfo = MutableStateFlow<BeeDeviceInfo?>(null)
val deviceInfo: StateFlow<BeeDeviceInfo?> = _deviceInfo.asStateFlow()
// ── GPS ───────────────────────────────────────────────────────────────────
private val _gnss = MutableStateFlow<GnssData?>(null)
val gnss: StateFlow<GnssData?> = _gnss.asStateFlow()
// ── Camera ────────────────────────────────────────────────────────────────
private val _cameraBytes = MutableStateFlow<ByteArray?>(null)
val cameraBytes: StateFlow<ByteArray?> = _cameraBytes.asStateFlow()
private val _cameraEndpointWorking = MutableStateFlow<String?>(null)
// ── Forwarding service state (exposed from singleton) ─────────────────────
val stats = ForwardingService.stats
val isForwarding = ForwardingService.isRunning
val beeConnected = ForwardingService.beeConnected
val adamapsReachable = ForwardingService.adamapsReachable
val lastError = ForwardingService.lastError
val beeStatus = ForwardingService.beeStatus
// ── Error ─────────────────────────────────────────────────────────────────
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error.asStateFlow()
private var gpsJob: Job? = null
private var deviceJob: Job? = null
private var cameraJob: Job? = null
init {
viewModelScope.launch {
val s = settings.first()
applySettings(s)
startPolling(s)
}
viewModelScope.launch {
settings.collect { s ->
applySettings(s)
}
}
}
private fun applySettings(s: VarroaSettings) {
beeClient.updateUrls(s.beeApiUrl, s.beeAltApiUrl)
beeClient.bindToWifiNetwork(getApplication())
}
private fun startPolling(s: VarroaSettings) {
startGpsLoop(5_000L)
startDeviceLoop(10_000L)
startCameraLoop(s.cameraRefreshSeconds * 1000L, s.cameraEndpoint)
}
private fun startGpsLoop(intervalMs: Long) {
gpsJob?.cancel()
gpsJob = viewModelScope.launch {
while (true) {
when (val r = beeClient.getGnss()) {
is ApiResult.Success -> _gnss.value = r.data
is ApiResult.Error -> { /* ignore, GPS just won't update */ }
}
delay(intervalMs)
}
}
}
private fun startDeviceLoop(intervalMs: Long) {
deviceJob?.cancel()
deviceJob = viewModelScope.launch {
while (true) {
when (val r = beeClient.getDeviceInfo()) {
is ApiResult.Success -> _deviceInfo.value = r.data
is ApiResult.Error -> { /* ignore */ }
}
delay(intervalMs)
}
}
}
private fun startCameraLoop(intervalMs: Long, configuredEndpoint: String) {
cameraJob?.cancel()
cameraJob = viewModelScope.launch {
while (true) {
val ep = _cameraEndpointWorking.value ?: configuredEndpoint
val (foundEp, result) = beeClient.getCameraFrameAuto(ep)
when (result) {
is ApiResult.Success -> {
_cameraEndpointWorking.value = foundEp
_cameraBytes.value = result.data
}
is ApiResult.Error -> {
// Clear camera bytes when Bee is offline
_cameraBytes.value = null
}
}
delay(intervalMs)
}
}
}
fun startForwarding() {
val ctx = getApplication<Application>()
val intent = Intent(ctx, ForwardingService::class.java).apply {
action = ForwardingService.ACTION_START
}
ctx.startForegroundService(intent)
}
fun stopForwarding() {
val ctx = getApplication<Application>()
val intent = Intent(ctx, ForwardingService::class.java).apply {
action = ForwardingService.ACTION_STOP
}
ctx.startService(intent)
}
fun toggleForwarding() {
if (isForwarding.value) stopForwarding() else startForwarding()
}
// Restart polling loops when settings change
fun refreshPolling() {
viewModelScope.launch {
val s = settings.first()
startCameraLoop(s.cameraRefreshSeconds * 1000L, s.cameraEndpoint)
}
}
}