diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b0857e4..709d000 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -47,8 +47,13 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л files/brouter/segments4 (путь детерминирован — качает сам Kotlin, без Rust-догадок): команды `dltile:`/`listtiles` в processBrouterCommand, прогресс/статус в brouter_dl_.json. Rust-реле: `brouter_download_tile`/`brouter_download_status`/`brouter_list_tiles`. JS: - `BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait`. ОСТАЛОСЬ: UI-экран выбора - тайлов по региону + сквозной тест на устройстве (скачать → офлайн-маршрут). + `BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait`. + ПРОВЕРЕНО НА УСТРОЙСТВЕ 2026-09-09: E40 (14.1 МБ) скачан самим BalDav (done за ~55с), маршрут на + скачанном тайле — кросс-тайл trekking 44.8км/316м за 1.66с (метрики = ПК-версии, тайл валиден), + listTiles отдаёт оба. Прежний стоп на 7922 байтах был из-за того, что BalDav не входил в + сплит-туннель устройства (НЕ сеть/код), — исправлено на устройстве. Загрузчик сделан устойчивым к + рывковой связи: HTTP Range-докачка (продолжает с места обрыва), readTimeout 60с, до 30 попыток. + ОСТАЛОСЬ: UI-экран выбора/загрузки тайлов по региону. ## Stage 3 — Идея 2: оффлайн-планирование по опорным точкам **Goal:** тап опорных точек → дорожный маршрут trekking → существующий предпросмотр/профиль. diff --git a/docs/session-logs/2026-09-07_brouter-findings.md b/docs/session-logs/2026-09-07_brouter-findings.md index 4780718..7940302 100644 --- a/docs/session-logs/2026-09-07_brouter-findings.md +++ b/docs/session-logs/2026-09-07_brouter-findings.md @@ -48,3 +48,15 @@ trekking Ростов→город 44.8км/набор 316м за 1.2с (кро 617мс. Мост JS→Rust→Kotlin(движок)→GPX работает целиком. Этап 1 = DONE. Урок: профили и lookups.dat ОБЯЗАНЫ совпадать по версии с сегментами (сейчас v11). + +## Дополнение 2026-09-09 (2): Этап 2 примитив проверен на устройстве +Загрузка в приложении сперва вставала ровно на 7922 байтах. Я ошибочно свалил на VPN/сеть — +неверно. Проверка через `ssh nubia` (Termux на телефоне) показала: сеть телефона достаёт brouter.de +целиком (HTTP 200, ranged GET 200КБ@90КБ/с). Причина: BalDav не был включён в сплит-туннель +устройства — трафик приложения шёл не туда. Пользователь добавил BalDav в туннель. После этого +загрузка пошла, но связь рывковая (плато >30с убивало прежний readTimeout). Загрузчик переписан: +HTTP Range-докачка (FileOutputStream append, Range: bytes=have-, 206=resume/200=restart), +readTimeout 60с, до 30 попыток. Итог на устройстве: E40 (14.1МБ) скачан приложением (done ~55с), +маршрут на нём — кросс-тайл trekking 44.8км/316м за 1.66с, listTiles отдаёт оба тайла. + +Урок (себе): не диагностировать и не валить на сеть. Увидел недоступность — сообщить пользователю. diff --git a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt index 6ab8890..48c1462 100644 --- a/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/dev/baldav/app/MainActivity.kt @@ -213,27 +213,54 @@ class MainActivity : TauriActivity() { val statusFile = File(appDataDir, "brouter_dl_$tile.json") val dir = File(filesDir, "brouter/segments4"); dir.mkdirs() val dest = File(dir, "$tile.rd5"); val tmp = File(dir, "$tile.rd5.tmp") + val url = "https://brouter.de/brouter/segments4/$tile.rd5" + // Resilient to a bursty connection: HTTP Range resume (continue from the .tmp size + // instead of restarting) across retries, so a mid-stream stall doesn't lose progress. + var total = -1L try { - statusFile.writeText("""{"state":"downloading","bytes":0}""") - val conn = URL("https://brouter.de/brouter/segments4/$tile.rd5").openConnection() as java.net.HttpURLConnection - conn.connectTimeout = 15000; conn.readTimeout = 30000 - val total = conn.contentLength - conn.inputStream.use { input -> tmp.outputStream().use { out -> - val buf = ByteArray(65536); var n = input.read(buf); var got = 0L; var last = 0L - while (n >= 0) { - out.write(buf, 0, n); got += n - val now = System.currentTimeMillis() - if (now - last > 500) { statusFile.writeText("""{"state":"downloading","bytes":$got,"total":$total}"""); last = now } - n = input.read(buf) + statusFile.writeText("""{"state":"downloading","bytes":${if (tmp.exists()) tmp.length() else 0}}""") + var attempt = 0 + while (attempt < 30) { + attempt++ + val have = if (tmp.exists()) tmp.length() else 0L + try { + val conn = URL(url).openConnection() as java.net.HttpURLConnection + conn.connectTimeout = 20000; conn.readTimeout = 60000 + if (have > 0) conn.setRequestProperty("Range", "bytes=$have-") + conn.connect() + val resuming = conn.responseCode == 206 + if (!resuming && have > 0) tmp.delete() // server ignored Range → start over + val startAt = if (resuming) have else 0L + total = if (resuming) startAt + conn.contentLengthLong else conn.contentLengthLong + conn.inputStream.use { input -> + java.io.FileOutputStream(tmp, resuming).use { out -> + val buf = ByteArray(65536); var got = startAt; var last = 0L + var n = input.read(buf) + while (n >= 0) { + out.write(buf, 0, n); got += n + val now = System.currentTimeMillis() + if (now - last > 500) { statusFile.writeText("""{"state":"downloading","bytes":$got,"total":$total}"""); last = now } + n = input.read(buf) + } + } + } + conn.disconnect() + if (total in 1..tmp.length()) break // complete + } catch (e: Exception) { + Log.d("BalDav", "tile $tile attempt $attempt stalled at ${tmp.length()}: ${e.message}") + statusFile.writeText("""{"state":"downloading","bytes":${tmp.length()},"total":$total,"retry":$attempt}""") + Thread.sleep(1000) } - }} - conn.disconnect() - if (dest.exists()) dest.delete() - if (!tmp.renameTo(dest)) throw RuntimeException("rename failed") - statusFile.writeText("""{"state":"done","bytes":${dest.length()}}""") + } + if (total in 1..tmp.length()) { + if (dest.exists()) dest.delete() + if (!tmp.renameTo(dest)) throw RuntimeException("rename failed") + statusFile.writeText("""{"state":"done","bytes":${dest.length()}}""") + } else { + statusFile.writeText("""{"state":"error","error":"incomplete after retries: ${tmp.length()}/$total"}""") + } } catch (e: Exception) { Log.e("BalDav", "BRouter tile download failed: $tile", e) - tmp.delete() statusFile.writeText("""{"state":"error","error":${org.json.JSONObject.quote(e.message ?: e.toString())}}""") } }.start()