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:
parent
6fc1d55ab4
commit
109d7c19df
3 changed files with 166 additions and 4 deletions
|
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId = "com.adamaps.varroa"
|
applicationId = "com.adamaps.varroa"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 9
|
versionCode = 10
|
||||||
versionName = "1.7.3"
|
versionName = "1.7.4"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
@ -204,9 +205,9 @@ class BeeApiClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLandmarks(): ApiResult<List<BeeDetection>> = withContext(Dispatchers.IO) {
|
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 {
|
is ApiResult.Success -> try {
|
||||||
Log.d(TAG, "Raw landmarks response received - parsing JSON...")
|
Log.d(TAG, "Raw landmarks response received - parsing JSON...")
|
||||||
val type = object : TypeToken<List<BeeDetection>>() {}.type
|
val type = object : TypeToken<List<BeeDetection>>() {}.type
|
||||||
|
|
@ -323,4 +324,140 @@ class BeeApiClient(
|
||||||
}
|
}
|
||||||
return configured to ApiResult.Error("No camera endpoint responded")
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.adamaps.varroa.api.AdaMapsApiClient
|
import com.adamaps.varroa.api.AdaMapsApiClient
|
||||||
|
import com.adamaps.varroa.api.BeeApiClient
|
||||||
import com.adamaps.varroa.data.AdaMapsDetection
|
import com.adamaps.varroa.data.AdaMapsDetection
|
||||||
import com.adamaps.varroa.data.ApiResult
|
import com.adamaps.varroa.data.ApiResult
|
||||||
import com.adamaps.varroa.data.SettingsDataStore
|
import com.adamaps.varroa.data.SettingsDataStore
|
||||||
|
|
@ -108,6 +109,7 @@ class AdaMapsUploadWorker(
|
||||||
|
|
||||||
private val database = VarroaDatabase.getInstance(applicationContext)
|
private val database = VarroaDatabase.getInstance(applicationContext)
|
||||||
private val adamapsClient = AdaMapsApiClient()
|
private val adamapsClient = AdaMapsApiClient()
|
||||||
|
private val beeClient = BeeApiClient()
|
||||||
private val settingsStore = SettingsDataStore(applicationContext)
|
private val settingsStore = SettingsDataStore(applicationContext)
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
|
|
@ -148,6 +150,29 @@ class AdaMapsUploadWorker(
|
||||||
totalUploaded += batch.size
|
totalUploaded += batch.size
|
||||||
_totalUploaded.value += batch.size
|
_totalUploaded.value += batch.size
|
||||||
Log.i(TAG, "Batch #$batchNum uploaded successfully - running total: $totalUploaded")
|
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 -> {
|
is Result.Retry -> {
|
||||||
// Network error, WorkManager will retry
|
// Network error, WorkManager will retry
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue