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.
161 lines
6.4 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|