diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 709d000..f684c17 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -55,6 +55,16 @@ app data dir; профиль trekking (идея 2) и shortest (идея 1); л рывковой связи: HTTP Range-докачка (продолжает с места обрыва), readTimeout 60с, до 30 попыток. ОСТАЛОСЬ: UI-экран выбора/загрузки тайлов по региону. + ЗЕРКАЛО (по решению пользователя, 2026-09-09): данные качаются НЕ с brouter.de напрямую, а со + своего зеркала. Источники в загрузчике по порядку: LAN `http://192.168.1.1:88/brouter/segments4` + → WAN `http://mirror.invalid:8088/brouter/segments4` → brouter.de (запас). Зеркало на роутере + (Keenetic uhttpd :88, docroot /opt/share/www → файлы в /opt/share/www/brouter/{segments4,profiles2}, + USB, всегда включён). ПРОВЕРЕНО: телефон качает тайл с LAN-зеркала за <2с, маршрут на нём ок. + Плюс BRouter RouteServer на ПК (jar, /mnt/02F6E7F6F6E7E7BD/brouter-mirror, systemd brouter.service — + автостарт без логина) как удалённый роутинг-эндпоинт пользователя (НЕ для BalDav — тот роутит + встроенным движком). WAN-пробросы на Keenetic (PPPoE0): :8088→192.168.1.1:88 (зеркало), + :17777→192.168.1.18:17777 (RouteServer) — outward-facing, ставятся с подтверждением. + ## Stage 3 — Идея 2: оффлайн-планирование по опорным точкам **Goal:** тап опорных точек → дорожный маршрут trekking → существующий предпросмотр/профиль. **Changes:** diff --git a/docs/session-logs/2026-09-07_brouter-findings.md b/docs/session-logs/2026-09-07_brouter-findings.md index 7940302..aa154f7 100644 --- a/docs/session-logs/2026-09-07_brouter-findings.md +++ b/docs/session-logs/2026-09-07_brouter-findings.md @@ -60,3 +60,17 @@ readTimeout 60с, до 30 попыток. Итог на устройстве: E4 маршрут на нём — кросс-тайл trekking 44.8км/316м за 1.66с, listTiles отдаёт оба тайла. Урок (себе): не диагностировать и не валить на сеть. Увидел недоступность — сообщить пользователю. + +## Дополнение 2026-09-09 (3): своё зеркало вместо прямого brouter.de +По решению пользователя данные берём со своего зеркала (не с brouter.de напрямую). Инфраструктура: +- Роутер Keenetic (белый IP mirror.invalid): на :88 уже uhttpd (docroot /opt/share/www, USB 23ГБ free). + Положил тайлы+профили в /opt/share/www/brouter/{segments4,profiles2} — раздаётся как + http://192.168.1.1:88/brouter/... (uhttpd игнорирует Range → отдаёт файл целиком; на LAN неважно). +- ПК: BRouter RouteServer (jar) в /mnt/02F6E7F6F6E7E7BD/brouter-mirror (segments4/profiles2/customs + + run-routeserver.sh + brouter.service). Проверен: GPX/GeoJSON 200. systemd-юнит подготовлен + (автостарт без логина, multi-user.target) — пользователь ставит sudo-командами. Это удалённый + роутинг-эндпоинт пользователя, НЕ источник для BalDav (телефон роутит встроенным движком). +- BalDav (downloadTile): источники по порядку LAN→WAN→brouter.de. Проверено на устройстве: тайл с + LAN-зеркала за <2с, маршрут на нём ок. +Осталось: WAN-пробросы на Keenetic (PPPoE0 :8088→роутер:88, :17777→ПК:17777) — outward-facing, +с подтверждением; для release добавить network-security-config под cleartext на mirror-хосты. 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 48c1462..f783d4c 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,43 +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. + // Segment source: own mirror first (router uhttpd, always on — LAN then WAN via the + // white IP), then brouter.de upstream as a last resort. Range resume survives a + // bursty link; a server that ignores Range (uhttpd → 200) just restarts, which is + // fine on the fast LAN. + val bases = listOf( + "http://192.168.1.1:88/brouter/segments4", // LAN mirror + "http://mirror.invalid:8088/brouter/segments4", // WAN mirror (router port-forward) + "https://brouter.de/brouter/segments4" // upstream fallback + ) var total = -1L try { 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) + for (base in bases) { + if (total in 1..tmp.length()) break + val url = "$base/$tile.rd5" + var attempt = 0 + while (attempt < 8) { + attempt++ + val have = if (tmp.exists()) tmp.length() else 0L + try { + val conn = URL(url).openConnection() as java.net.HttpURLConnection + conn.connectTimeout = 12000; conn.readTimeout = 60000 + if (have > 0) conn.setRequestProperty("Range", "bytes=$have-") + conn.connect() + if (conn.responseCode >= 400) { conn.disconnect(); break } // try next base + 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 from $base attempt $attempt at ${tmp.length()}: ${e.message}") + statusFile.writeText("""{"state":"downloading","bytes":${tmp.length()},"total":$total,"retry":$attempt}""") + Thread.sleep(1000) } - 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) } } if (total in 1..tmp.length()) {