BRouter Stage 2: resilient segment downloader (Range resume), verified on device
In-app download initially stalled at 7922 bytes — not the network: the phone reaches brouter.de fully (confirmed via Termux). BalDav was not in the device's split tunnel, so its traffic took the wrong path; fixed device-side. Over the now-working but bursty link the fixed 30s readTimeout tripped, so the downloader is rewritten to resume: HTTP Range (append to the .tmp, 206=resume / 200=restart), readTimeout 60s, up to 30 retries. Verified on device: E40_N45 (14.1 MB) downloaded by the app (done ~55s), a cross-tile trekking route on the app-downloaded tile (44.8 km / 316 m, 1.66s) matches the PC-pushed tile, and listTiles returns both. Remaining Stage 2 work: the tile-selection UI.
This commit is contained in:
parent
b4046841be
commit
d807e4ec3b
3 changed files with 63 additions and 19 deletions
|
|
@ -47,8 +47,13 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л
|
|||
files/brouter/segments4 (путь детерминирован — качает сам Kotlin, без Rust-догадок): команды
|
||||
`dltile:<tile>`/`listtiles` в processBrouterCommand, прогресс/статус в brouter_dl_<tile>.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 → существующий предпросмотр/профиль.
|
||||
|
|
|
|||
|
|
@ -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 отдаёт оба тайла.
|
||||
|
||||
Урок (себе): не диагностировать и не валить на сеть. Увидел недоступность — сообщить пользователю.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue