BRouter: download segments from own mirror (LAN->WAN->upstream)

Per the user's decision, BalDav pulls .rd5 tiles from their own mirror instead
of brouter.de directly. downloadTile now tries, in order: LAN
http://192.168.1.1:88/brouter/segments4, WAN http://mirror.invalid:8088/... (via
the router port-forward), then brouter.de as a last resort; a base that 4xx's
or fails is skipped. The mirror is the Keenetic's existing uhttpd (:88, docroot
/opt/share/www) serving /opt/share/www/brouter/ off USB — always on.

Verified on device: the phone downloads a tile from the LAN mirror in <2s and
routes on it. Routing stays local (embedded engine); the PC RouteServer +
systemd unit are the user's separate remote endpoint, not used by BalDav.

Debug builds allow cleartext; release needs a network-security-config for the
mirror hosts. WAN port-forwards on the router are outward-facing (added
separately with confirmation).
This commit is contained in:
4aiman 2026-09-09 11:48:50 +03:00
commit b40775c9d0
3 changed files with 66 additions and 31 deletions

View file

@ -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:**

View file

@ -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-хосты.

View file

@ -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()) {