Root cause: Server expects flat array of detections with device_id on each
item, but client was sending nested {device_id, detections: [...]}.
The /health endpoint worked (ADAMaps showed green) but /api/ingest failed
validation: 'Missing required fields: device_id, lat, lon' on every request.
Items queued indefinitely, sent=0.
Fixed by:
- Adding device_id field to AdaMapsDetection
- Changed AdaMapsIngestRequest to typealias for List<AdaMapsDetection>
- Updated toAdaMapsDetection() to accept deviceId parameter
- Updated ForwardingService to send flat array format
89 lines
3 KiB
Kotlin
89 lines
3 KiB
Kotlin
package com.adamaps.varroa.api
|
|
|
|
import com.adamaps.varroa.data.AdaMapsIngestRequest
|
|
import com.adamaps.varroa.data.ApiResult
|
|
import com.google.gson.Gson
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
import okhttp3.Dns
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
import java.net.InetAddress
|
|
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(
|
|
private var apiUrl: String = "https://api.adamaps.org",
|
|
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()
|
|
.dns(AdaMapsDns)
|
|
.connectTimeout(15, TimeUnit.SECONDS)
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
.writeTimeout(30, TimeUnit.SECONDS)
|
|
.build()
|
|
|
|
private val gson = Gson()
|
|
private val json = "application/json; charset=utf-8".toMediaType()
|
|
|
|
fun updateConfig(url: String, key: String) {
|
|
apiUrl = url.trimEnd('/')
|
|
apiKey = key
|
|
}
|
|
|
|
suspend fun ingest(detections: AdaMapsIngestRequest): ApiResult<String> = withContext(Dispatchers.IO) {
|
|
try {
|
|
// Server expects flat array of detections, each with device_id
|
|
val body = gson.toJson(detections).toRequestBody(json)
|
|
val req = Request.Builder()
|
|
.url("$apiUrl/api/ingest")
|
|
.addHeader("X-MapNet-Key", apiKey)
|
|
.addHeader("Content-Type", "application/json")
|
|
.post(body)
|
|
.build()
|
|
client.newCall(req).execute().use { resp ->
|
|
val respBody = resp.body?.string() ?: ""
|
|
if (resp.isSuccessful) ApiResult.Success(respBody)
|
|
else ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code)
|
|
}
|
|
} catch (e: Exception) {
|
|
ApiResult.Error(e.message ?: "Network error")
|
|
}
|
|
}
|
|
|
|
suspend fun checkReachability(): Boolean = withContext(Dispatchers.IO) {
|
|
try {
|
|
val req = Request.Builder()
|
|
.url("$apiUrl/health")
|
|
.head()
|
|
.build()
|
|
client.newCall(req).execute().use { resp ->
|
|
resp.code < 500
|
|
}
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
}
|