v7.4: fetch all landmarks + cleanup after upload

- Updated version to 1.7.4 (versionCode 10)
- Modified BeeApiClient.getLandmarks() to fetch 50,000 landmarks instead of 200
- Added cleanupLandmarks() method to attempt deletion from Bee device after upload
- Enhanced AdaMapsUploadWorker to call cleanup after successful upload to ADAMaps
- Cleanup tries multiple potential DELETE endpoints and cmd interface
- Documents limitation: Bee API may not support landmark deletion

Note: Cleanup functionality is exploratory - no confirmed DELETE endpoint found in Bee API docs
This commit is contained in:
Kayos 2026-03-11 14:56:53 -07:00
parent 19ab72d8ea
commit 1ae0657bb0
3 changed files with 166 additions and 4 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId = "com.adamaps.varroa"
minSdk = 26
targetSdk = 34
versionCode = 9
versionName = "1.7.3"
versionCode = 10
versionName = "1.7.4"
vectorDrawables {
useSupportLibrary = true

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
@ -204,9 +205,9 @@ class BeeApiClient(
}
suspend fun getLandmarks(): ApiResult<List<BeeDetection>> = withContext(Dispatchers.IO) {
Log.d(TAG, "getLandmarks() called")
Log.d(TAG, "getLandmarks() called - fetching ALL landmarks")
when (val r = getRaw("/api/1/landmarks/last/200")) {
when (val r = getRaw("/api/1/landmarks/last/50000")) {
is ApiResult.Success -> try {
Log.d(TAG, "Raw landmarks response received - parsing JSON...")
val type = object : TypeToken<List<BeeDetection>>() {}.type
@ -323,4 +324,140 @@ class BeeApiClient(
}
return configured to ApiResult.Error("No camera endpoint responded")
}
/**
* Attempt to delete landmarks from Bee device after successful upload.
* This tries various potential DELETE endpoints.
*
* @param landmarkIds List of landmark IDs to delete
* @return ApiResult indicating success/failure of cleanup operation
*/
suspend fun cleanupLandmarks(landmarkIds: List<Long>): ApiResult<String> = withContext(Dispatchers.IO) {
Log.d(TAG, "cleanupLandmarks() called with ${landmarkIds.size} IDs: ${landmarkIds.take(10)}...")
if (landmarkIds.isEmpty()) {
Log.d(TAG, "No landmark IDs provided - skipping cleanup")
return@withContext ApiResult.Success("No landmarks to clean up")
}
// Try various potential DELETE endpoints
val endpoints = listOf(
"/api/1/landmarks/delete",
"/api/1/landmarks/clear",
"/api/1/landmarks/cleanup",
"/api/1/landmarks/remove",
"/api/1/cmd" // As a last resort using the cmd endpoint
)
for (endpoint in endpoints) {
Log.d(TAG, "Trying cleanup endpoint: $endpoint")
val result = when (endpoint) {
"/api/1/cmd" -> {
// Use the cmd endpoint to try deleting landmarks via system commands
Log.d(TAG, "Attempting cleanup via cmd endpoint...")
tryCleanupViaCmd(landmarkIds)
}
else -> {
// Try standard DELETE/POST requests
tryCleanupEndpoint(endpoint, landmarkIds)
}
}
when (result) {
is ApiResult.Success -> {
Log.i(TAG, "Cleanup successful via $endpoint: ${result.data}")
return@withContext result
}
is ApiResult.Error -> {
Log.w(TAG, "Cleanup failed via $endpoint: ${result.message}")
// Continue trying other endpoints
}
}
}
Log.w(TAG, "All cleanup endpoints failed - Bee may not support landmark deletion")
return@withContext ApiResult.Error("No working DELETE endpoint found - cleanup not supported by Bee device")
}
private suspend fun tryCleanupEndpoint(endpoint: String, landmarkIds: List<Long>): ApiResult<String> = withContext(Dispatchers.IO) {
try {
val jsonBody = gson.toJson(mapOf("ids" to landmarkIds))
val requestBody = okhttp3.RequestBody.create(
"application/json".toMediaType(),
jsonBody
)
// Try both DELETE and POST methods
for (method in listOf("DELETE", "POST")) {
Log.d(TAG, "Trying $method $endpoint")
val request = Request.Builder()
.url("$apiUrl$endpoint")
.method(method, if (method == "DELETE") null else requestBody)
.build()
client.newCall(request).execute().use { resp ->
val body = resp.body?.string() ?: ""
if (resp.isSuccessful) {
Log.i(TAG, "$method $endpoint succeeded: $body")
return@withContext ApiResult.Success("$method $endpoint: $body")
} else {
Log.d(TAG, "$method $endpoint failed: ${resp.code} ${resp.message}")
}
}
}
return@withContext ApiResult.Error("$endpoint not supported (tried DELETE and POST)")
} catch (e: Exception) {
Log.d(TAG, "Exception trying $endpoint: ${e.message}")
return@withContext ApiResult.Error("Exception: ${e.message}")
}
}
private suspend fun tryCleanupViaCmd(landmarkIds: List<Long>): ApiResult<String> = withContext(Dispatchers.IO) {
try {
// Try to find where landmarks are stored and delete them via filesystem commands
val commands = listOf(
"find /data -name '*landmark*' -type f -ls",
"find /tmp -name '*landmark*' -type f -ls",
"ls -la /data/recording/",
"redis-cli KEYS '*landmark*'",
"redis-cli KEYS '*detection*'"
)
for (cmd in commands) {
Log.d(TAG, "Trying cmd: $cmd")
val jsonBody = gson.toJson(mapOf("cmd" to cmd))
val requestBody = okhttp3.RequestBody.create(
"application/json".toMediaType(),
jsonBody
)
val request = Request.Builder()
.url("$apiUrl/api/1/cmd")
.post(requestBody)
.build()
client.newCall(request).execute().use { resp ->
val body = resp.body?.string() ?: ""
if (resp.isSuccessful) {
Log.d(TAG, "Cmd '$cmd' result: $body")
// For now, just log the results to understand the data structure
if (body.contains("landmark") || body.contains("detection")) {
Log.i(TAG, "Found potential landmark storage: $body")
}
}
}
}
// Note: We're not actually deleting anything via cmd yet, just exploring
Log.w(TAG, "Cleanup via cmd endpoint: exploration complete, actual deletion not implemented yet")
return@withContext ApiResult.Error("Cleanup via cmd: exploration only, deletion not yet implemented")
} catch (e: Exception) {
Log.e(TAG, "Failed to explore via cmd endpoint", e)
return@withContext ApiResult.Error("Cmd exploration failed: ${e.message}")
}
}
}

View file

@ -11,6 +11,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.adamaps.varroa.api.AdaMapsApiClient
import com.adamaps.varroa.api.BeeApiClient
import com.adamaps.varroa.data.AdaMapsDetection
import com.adamaps.varroa.data.ApiResult
import com.adamaps.varroa.data.SettingsDataStore
@ -108,6 +109,7 @@ class AdaMapsUploadWorker(
private val database = VarroaDatabase.getInstance(applicationContext)
private val adamapsClient = AdaMapsApiClient()
private val beeClient = BeeApiClient()
private val settingsStore = SettingsDataStore(applicationContext)
override suspend fun doWork(): Result {
@ -148,6 +150,29 @@ class AdaMapsUploadWorker(
totalUploaded += batch.size
_totalUploaded.value += batch.size
Log.i(TAG, "Batch #$batchNum uploaded successfully - running total: $totalUploaded")
// Attempt cleanup from Bee device after successful upload
val beeDetectionIds = batch.map { it.beeDetectionId }
Log.d(TAG, "Attempting to cleanup ${beeDetectionIds.size} landmarks from Bee device...")
// Configure BeeClient - this may fail if not connected to Bee network
try {
val settings = settingsStore.settings.first()
beeClient.updateUrl(settings.beeApiUrl)
when (val cleanupResult = beeClient.cleanupLandmarks(beeDetectionIds)) {
is ApiResult.Success -> {
Log.i(TAG, "Cleanup successful: ${cleanupResult.data}")
}
is ApiResult.Error -> {
Log.w(TAG, "Cleanup failed (expected - likely no DELETE endpoint): ${cleanupResult.message}")
// This is not a failure of the upload process
}
}
} catch (e: Exception) {
Log.w(TAG, "Cleanup attempt failed (likely not connected to Bee network): ${e.message}")
// This is expected when uploading via internet connection, not Bee AP
}
}
is Result.Retry -> {
// Network error, WorkManager will retry