From 0571291b69a9b4bd4140e31da394cd7573aabec5 Mon Sep 17 00:00:00 2001 From: grigo Date: Tue, 16 Jun 2026 10:36:18 +0300 Subject: [PATCH] update --- .../loratester/TelemetryUploader.java | 9 + .../loratester/api/DeviceInfo.java | 1 + .../loratester/api/ServerApi.java | 3 + .../loratester/api/TelemetryPayload.java | 17 ++ .../loratester/map/EsriWorldImagery.java | 37 +++ .../loratester/ui/MapFragment.java | 151 ++++++++-- .../loratester/ui/MapsforgeBitmaps.java | 8 + app/src/main/res/layout/fragment_map.xml | 14 + app/src/main/res/values/strings.xml | 3 + .../core/__pycache__/models.cpython-313.pyc | Bin 1292 -> 1345 bytes .../core/__pycache__/storage.cpython-313.pyc | Bin 30619 -> 31245 bytes server/core/models.py | 1 + server/core/storage.py | 32 ++- server/core/telemetry_body.py | 5 + server/static/index.html | 271 +++++++++++++----- 15 files changed, 447 insertions(+), 105 deletions(-) create mode 100644 app/src/main/java/com/grigowashere/loratester/map/EsriWorldImagery.java diff --git a/app/src/main/java/com/grigowashere/loratester/TelemetryUploader.java b/app/src/main/java/com/grigowashere/loratester/TelemetryUploader.java index 47df643..3cc8c61 100644 --- a/app/src/main/java/com/grigowashere/loratester/TelemetryUploader.java +++ b/app/src/main/java/com/grigowashere/loratester/TelemetryUploader.java @@ -1,6 +1,7 @@ package com.grigowashere.loratester; import android.content.Context; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -285,6 +286,7 @@ public class TelemetryUploader implements TelnetClient.Listener { } TelemetryPayload payload = new TelemetryPayload( settings.getOrCreateDeviceId(), + phoneLabel(), validLat(), validLon(), stats.rssi, @@ -297,6 +299,13 @@ public class TelemetryUploader implements TelnetClient.Listener { uploadExecutor.execute(() -> uploadTelemetry(payload)); } + private static String phoneLabel() { + String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER : ""; + String model = Build.MODEL != null ? Build.MODEL : ""; + String label = (manufacturer + " " + model).trim(); + return label.isEmpty() ? null : label; + } + private void uploadTelemetry(TelemetryPayload payload) { if (networkMonitor.isOnline()) { try { diff --git a/app/src/main/java/com/grigowashere/loratester/api/DeviceInfo.java b/app/src/main/java/com/grigowashere/loratester/api/DeviceInfo.java index 19ac7d5..2e470f4 100644 --- a/app/src/main/java/com/grigowashere/loratester/api/DeviceInfo.java +++ b/app/src/main/java/com/grigowashere/loratester/api/DeviceInfo.java @@ -2,6 +2,7 @@ package com.grigowashere.loratester.api; public class DeviceInfo { public String device_id; + public String label; public double last_seen; public Double lat; public Double lon; diff --git a/app/src/main/java/com/grigowashere/loratester/api/ServerApi.java b/app/src/main/java/com/grigowashere/loratester/api/ServerApi.java index 92a88a2..641c987 100644 --- a/app/src/main/java/com/grigowashere/loratester/api/ServerApi.java +++ b/app/src/main/java/com/grigowashere/loratester/api/ServerApi.java @@ -51,6 +51,9 @@ public class ServerApi { public void postTelemetry(TelemetryPayload payload) throws IOException { Map body = new HashMap<>(); body.put("device_id", payload.deviceId); + if (payload.deviceLabel != null && !payload.deviceLabel.isBlank()) { + body.put("device_label", payload.deviceLabel); + } if (payload.lat != null) body.put("lat", payload.lat); if (payload.lon != null) body.put("lon", payload.lon); if (payload.rssi != null) body.put("rssi", payload.rssi); diff --git a/app/src/main/java/com/grigowashere/loratester/api/TelemetryPayload.java b/app/src/main/java/com/grigowashere/loratester/api/TelemetryPayload.java index 66fa1cd..4838056 100644 --- a/app/src/main/java/com/grigowashere/loratester/api/TelemetryPayload.java +++ b/app/src/main/java/com/grigowashere/loratester/api/TelemetryPayload.java @@ -2,6 +2,7 @@ package com.grigowashere.loratester.api; public class TelemetryPayload { public final String deviceId; + public final String deviceLabel; public final Double lat; public final Double lon; public final Double rssi; @@ -22,8 +23,24 @@ public class TelemetryPayload { String meta, String role, Double ts + ) { + this(deviceId, null, lat, lon, rssi, rangeM, rawFrame, meta, role, ts); + } + + public TelemetryPayload( + String deviceId, + String deviceLabel, + Double lat, + Double lon, + Double rssi, + Double rangeM, + String rawFrame, + String meta, + String role, + Double ts ) { this.deviceId = deviceId; + this.deviceLabel = deviceLabel; this.lat = lat; this.lon = lon; this.rssi = rssi; diff --git a/app/src/main/java/com/grigowashere/loratester/map/EsriWorldImagery.java b/app/src/main/java/com/grigowashere/loratester/map/EsriWorldImagery.java new file mode 100644 index 0000000..1c5a848 --- /dev/null +++ b/app/src/main/java/com/grigowashere/loratester/map/EsriWorldImagery.java @@ -0,0 +1,37 @@ +package com.grigowashere.loratester.map; + +import org.mapsforge.core.model.Tile; +import org.mapsforge.map.layer.download.tilesource.OnlineTileSource; + +import java.net.MalformedURLException; +import java.net.URL; + +/** Esri World Imagery — tile path is zoom/y/x (not OSM zoom/x/y). */ +public final class EsriWorldImagery extends OnlineTileSource { + + public static final EsriWorldImagery INSTANCE = new EsriWorldImagery(); + + private EsriWorldImagery() { + super(new String[]{"server.arcgisonline.com"}, 443); + setName("Esri.WorldImagery") + .setAlpha(false) + .setBaseUrl("/ArcGIS/rest/services/World_Imagery/MapServer/tile/") + .setExtension("png") + .setParallelRequestsLimit(4) + .setProtocol("https") + .setTileSize(256) + .setZoomLevelMax((byte) 18) + .setZoomLevelMin((byte) 0); + setUserAgent("LoraTester/1.0"); + } + + @Override + public URL getTileUrl(Tile tile) throws MalformedURLException { + StringBuilder path = new StringBuilder(48); + path.append(getBaseUrl()); + path.append(tile.zoomLevel).append('/'); + path.append(tile.tileY).append('/'); + path.append(tile.tileX).append('.').append(getExtension()); + return new URL(getProtocol(), getHostName(), 443, path.toString()); + } +} diff --git a/app/src/main/java/com/grigowashere/loratester/ui/MapFragment.java b/app/src/main/java/com/grigowashere/loratester/ui/MapFragment.java index 671e900..2341030 100644 --- a/app/src/main/java/com/grigowashere/loratester/ui/MapFragment.java +++ b/app/src/main/java/com/grigowashere/loratester/ui/MapFragment.java @@ -37,6 +37,8 @@ import com.grigowashere.loratester.api.TrackDetail; import com.grigowashere.loratester.api.TrackInfo; import com.grigowashere.loratester.location.GeoUtils; import com.grigowashere.loratester.net.NetworkMonitor; +import com.grigowashere.loratester.map.EsriWorldImagery; +import com.grigowashere.loratester.model.RadioSnapshot; import com.grigowashere.loratester.telnet.StatsExtractor; import com.grigowashere.loratester.track.TrackRecorder; @@ -51,6 +53,7 @@ import org.mapsforge.map.layer.Layer; import org.mapsforge.map.layer.cache.TileCache; import org.mapsforge.map.layer.download.TileDownloadLayer; import org.mapsforge.map.layer.download.tilesource.OpenStreetMapMapnik; +import org.mapsforge.map.layer.download.tilesource.TileSource; import org.mapsforge.map.layer.overlay.Marker; import org.mapsforge.map.layer.overlay.Polyline; import org.mapsforge.map.model.MapViewPosition; @@ -122,6 +125,7 @@ public class MapFragment extends Fragment { private MaterialButtonToggleGroup mapCenterMode; private Spinner trackSpinner; private Spinner mapHeatmapRadius; + private Spinner mapBasemap; private TextView mapHeatmapStatus; private View mapHeatmapLegend; private List savedTracks = new ArrayList<>(); @@ -150,6 +154,8 @@ public class MapFragment extends Fragment { private double lastHeatmapLat = Double.NaN; private double lastHeatmapLon = Double.NaN; private boolean suppressHeatmapSpinner; + private boolean suppressBasemapSpinner; + private TileSource currentTileSource = OpenStreetMapMapnik.INSTANCE; private Runnable heatmapReloadRunnable; private boolean suppressCenterToggle; private boolean mapGestureActive; @@ -196,6 +202,7 @@ public class MapFragment extends Fragment { mapToolDrawer = view.findViewById(R.id.mapToolDrawer); mapCenterMode = view.findViewById(R.id.mapCenterMode); mapHeatmapRadius = view.findViewById(R.id.mapHeatmapRadius); + mapBasemap = view.findViewById(R.id.mapBasemap); mapHeatmapStatus = view.findViewById(R.id.mapHeatmapStatus); mapHeatmapLegend = view.findViewById(R.id.mapHeatmapLegend); trackSpinner = view.findViewById(R.id.trackSpinner); @@ -208,6 +215,7 @@ public class MapFragment extends Fragment { btnFindHill.setOnClickListener(v -> toggleHill()); } setupHeatmapUi(); + setupBasemapUi(); updateConnectionIcons(lastDevices, serverConnected); @@ -347,18 +355,18 @@ public class MapFragment extends Fragment { mapView.getModel().frameBufferModel.getOverdrawFactor() ); - OpenStreetMapMapnik tileSource = OpenStreetMapMapnik.INSTANCE; - tileSource.setUserAgent("LoraTester/1.0"); + OpenStreetMapMapnik.INSTANCE.setUserAgent("LoraTester/1.0"); + currentTileSource = OpenStreetMapMapnik.INSTANCE; downloadLayer = new TileDownloadLayer( tileCache, mapView.getModel().mapViewPosition, - tileSource, + currentTileSource, AndroidGraphicFactory.INSTANCE ); mapView.getLayerManager().getLayers().add(downloadLayer); - mapView.setZoomLevelMin(tileSource.getZoomLevelMin()); - mapView.setZoomLevelMax(tileSource.getZoomLevelMax()); + mapView.setZoomLevelMin(currentTileSource.getZoomLevelMin()); + mapView.setZoomLevelMax(currentTileSource.getZoomLevelMax()); downloadLayer.start(); MapViewPosition position = (MapViewPosition) mapView.getModel().mapViewPosition; @@ -707,29 +715,76 @@ public class MapFragment extends Fragment { } clearTrackLayers(); - List line = new ArrayList<>(); List boundsPoints = new ArrayList<>(); + boolean colorByQuality = isRxTrack(detail); + int defaultColor = colorByQuality ? ARGB_RX : ARGB_TRACK; - for (TrackDetail.TrackPoint p : detail.points) { + for (int i = 0; i < detail.points.size(); i++) { + TrackDetail.TrackPoint p = detail.points.get(i); LatLong latLong = new LatLong(p.lat, p.lon); - line.add(latLong); boundsPoints.add(latLong); - Marker marker = new Marker(latLong, bitmapTrackPoint, 0, 0); + int pointColor = trackPointColor(p.meta, colorByQuality, defaultColor); + Marker marker = new Marker(latLong, MapsforgeBitmaps.dot(pointColor, 12), 0, 0); addTrackLayer(marker); - } - if (line.size() >= 2) { - Polyline polyline = new Polyline( - MapsforgeBitmaps.linePaint(Color.GREEN, 4f), - AndroidGraphicFactory.INSTANCE - ); - polyline.getLatLongs().addAll(line); - addTrackLayer(polyline); + if (i > 0) { + TrackDetail.TrackPoint prev = detail.points.get(i - 1); + Double qa = rxQualityFromMeta(prev.meta); + Double qb = rxQualityFromMeta(p.meta); + Double q = qa != null && qb != null ? (qa + qb) / 2.0 : (qa != null ? qa : qb); + int segColor = colorByQuality && q != null ? qualityArgb(q) : defaultColor; + Polyline segment = new Polyline( + MapsforgeBitmaps.linePaint(segColor, 4f), + AndroidGraphicFactory.INSTANCE + ); + segment.getLatLongs().add(new LatLong(prev.lat, prev.lon)); + segment.getLatLongs().add(latLong); + addTrackLayer(segment); + } } fitBoundsOnce(boundsPoints, detail.points.size() == 1, true); } + private static boolean isRxTrack(TrackDetail detail) { + if (detail.points == null) { + return false; + } + for (TrackDetail.TrackPoint p : detail.points) { + if (StatsExtractor.ROLE_RX.equals(p.role)) { + return true; + } + RadioSnapshot snap = RadioSnapshot.fromMeta(p.meta, null, null); + if (StatsExtractor.ROLE_RX.equals(snap.role)) { + return true; + } + } + return false; + } + + private static Double rxQualityFromMeta(String meta) { + if (meta == null || meta.isBlank()) { + return null; + } + RadioSnapshot snap = RadioSnapshot.fromMeta(meta, null, null); + return snap.rxQualityPercent; + } + + private static int qualityArgb(double pct) { + double p = Math.max(0.0, Math.min(100.0, pct)); + int r = p < 50.0 ? (int) Math.round(255.0 * (p / 50.0)) : 255; + int g = p < 50.0 ? 255 : (int) Math.round(255.0 * (1.0 - (p - 50.0) / 50.0)); + return 0xFF000000 | (r << 16) | (g << 8); + } + + private static int trackPointColor(String meta, boolean colorByQuality, int fallback) { + if (!colorByQuality) { + return fallback; + } + Double q = rxQualityFromMeta(meta); + return q != null ? qualityArgb(q) : fallback; + } + private void addTrackLayer(Layer layer) { mapView.getLayerManager().getLayers().add(layer); trackLayers.add(layer); @@ -1015,6 +1070,68 @@ public class MapFragment extends Fragment { hillPathLine = null; } + private void setupBasemapUi() { + if (mapBasemap == null) { + return; + } + List labels = List.of( + getString(R.string.map_layer_scheme), + getString(R.string.map_layer_satellite) + ); + suppressBasemapSpinner = true; + mapBasemap.setAdapter(new ArrayAdapter<>( + requireContext(), + android.R.layout.simple_spinner_dropdown_item, + labels + )); + mapBasemap.setSelection(0, false); + suppressBasemapSpinner = false; + mapBasemap.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View v, int pos, long id) { + if (suppressBasemapSpinner) { + return; + } + TileSource next = pos == 1 ? EsriWorldImagery.INSTANCE : OpenStreetMapMapnik.INSTANCE; + if (next != currentTileSource) { + switchBasemap(next); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + private void switchBasemap(TileSource tileSource) { + if (!isMapReady() || tileSource == null || tileSource == currentTileSource) { + return; + } + if (tileSource instanceof OpenStreetMapMapnik osm) { + osm.setUserAgent("LoraTester/1.0"); + } + if (downloadLayer != null) { + downloadLayer.onPause(); + mapView.getLayerManager().getLayers().remove(downloadLayer); + } + currentTileSource = tileSource; + downloadLayer = new TileDownloadLayer( + tileCache, + mapView.getModel().mapViewPosition, + currentTileSource, + AndroidGraphicFactory.INSTANCE + ); + mapView.getLayerManager().getLayers().add(0, downloadLayer); + mapView.setZoomLevelMin(currentTileSource.getZoomLevelMin()); + mapView.setZoomLevelMax(currentTileSource.getZoomLevelMax()); + downloadLayer.start(); + if (mapResumed) { + downloadLayer.onResume(); + } + requestMapInvalidate(); + } + private void setupHeatmapUi() { if (mapHeatmapRadius == null) { return; diff --git a/app/src/main/java/com/grigowashere/loratester/ui/MapsforgeBitmaps.java b/app/src/main/java/com/grigowashere/loratester/ui/MapsforgeBitmaps.java index 3e2aa73..0083d9a 100644 --- a/app/src/main/java/com/grigowashere/loratester/ui/MapsforgeBitmaps.java +++ b/app/src/main/java/com/grigowashere/loratester/ui/MapsforgeBitmaps.java @@ -35,4 +35,12 @@ final class MapsforgeBitmaps { paint.setStyle(org.mapsforge.core.graphics.Style.STROKE); return paint; } + + static org.mapsforge.core.graphics.Paint linePaint(int argb, float strokeWidth) { + int a = (argb >> 24) & 0xFF; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = argb & 0xFF; + return linePaint(AndroidGraphicFactory.INSTANCE.createColor(r, g, b, a), strokeWidth); + } } diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 8457162..9cac01f 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -222,6 +222,20 @@ android:textSize="10sp" /> + + + + RX Оба Центр карты + Слой карты + Схема + Спутник Нет координат для выбранного режима Центрировать карту Трекинг пути diff --git a/server/core/__pycache__/models.cpython-313.pyc b/server/core/__pycache__/models.cpython-313.pyc index 74c8aec2fe37e33ce828bc89b647fae3a797ce55..40a7dcc0f66bb69930e9045beafc7b52ab475cf3 100644 GIT binary patch delta 333 zcmeC-I>^QMnU|M~0SJtq>Su{EP2^j}_-NwJGt$gOf-&qdEUZA87}j7ubs!zgUnG>y ztSKCCdVh1loVwq zm6oIy$Hy0mOrF4$$k;#m6H}w`WftihN@@*06EtQRPt?7}qE;k3xq>-6LjWbNB|_N$yFo@VvB(YaS$N^A|yeC6o>$;WC5|HL4*v502?O@V#$FB1sEYR z*_%a~4J@HBxr9Yq9xQ{Z`WA;xZhlH>PO4pz>g0Vaaw6P}>K_<@#Ajv(ChiYRAg;{h K4=fTaAaejVy-X+o delta 308 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 diff --git a/server/core/models.py b/server/core/models.py index ab808db..938252b 100644 --- a/server/core/models.py +++ b/server/core/models.py @@ -14,6 +14,7 @@ class TelemetryIn: role: Optional[str] = None ts: Optional[float] = None source: str = "android" + device_label: Optional[str] = None @dataclass diff --git a/server/core/storage.py b/server/core/storage.py index 2605268..4b72586 100644 --- a/server/core/storage.py +++ b/server/core/storage.py @@ -88,14 +88,27 @@ def record_telemetry(data: TelemetryIn) -> dict[str, Any]: ts = data.ts if data.ts is not None else time.time() lat, lon = _sanitize_coords(data.lat, data.lon) with _db() as conn: - conn.execute( - """ - INSERT INTO devices (device_id, label, last_seen) - VALUES (?, ?, ?) - ON CONFLICT(device_id) DO UPDATE SET last_seen = excluded.last_seen - """, - (data.device_id, data.device_id, ts), - ) + phone_label = (data.device_label or "").strip() + if phone_label: + conn.execute( + """ + INSERT INTO devices (device_id, label, last_seen) + VALUES (?, ?, ?) + ON CONFLICT(device_id) DO UPDATE SET + last_seen = excluded.last_seen, + label = excluded.label + """, + (data.device_id, phone_label, ts), + ) + else: + conn.execute( + """ + INSERT INTO devices (device_id, label, last_seen) + VALUES (?, ?, ?) + ON CONFLICT(device_id) DO UPDATE SET last_seen = excluded.last_seen + """, + (data.device_id, data.device_id, ts), + ) conn.execute( """ INSERT INTO telemetry @@ -138,7 +151,7 @@ def list_devices() -> list[dict[str, Any]]: with _db() as conn: rows = conn.execute( """ - SELECT d.device_id, d.last_seen, + SELECT d.device_id, d.label, d.last_seen, t.lat, t.lon, t.rssi, t.range_m, t.raw_frame, t.meta, t.role, t.ts, t.source FROM devices d INNER JOIN telemetry t ON t.id = ( @@ -164,6 +177,7 @@ def _is_null_island(device: dict[str, Any]) -> bool: def _row_to_device(row: sqlite3.Row) -> dict[str, Any]: return { "device_id": row["device_id"], + "label": row["label"] if "label" in row.keys() else None, "last_seen": row["last_seen"], "lat": row["lat"], "lon": row["lon"], diff --git a/server/core/telemetry_body.py b/server/core/telemetry_body.py index f8c6437..4b1fc58 100644 --- a/server/core/telemetry_body.py +++ b/server/core/telemetry_body.py @@ -48,6 +48,10 @@ def merge_meta(body: dict[str, Any]) -> tuple[Optional[str], Optional[str]]: def telemetry_from_body(body: dict[str, Any]) -> TelemetryIn: meta, role = merge_meta(body) + label = body.get("device_label") or body.get("label") + device_label = str(label).strip() if label else None + if device_label == "": + device_label = None return TelemetryIn( device_id=str(body["device_id"]), lat=_float_or_none(body.get("lat")), @@ -58,4 +62,5 @@ def telemetry_from_body(body: dict[str, Any]) -> TelemetryIn: meta=meta, role=role, ts=_float_or_none(body.get("ts")), + device_label=device_label, ) diff --git a/server/static/index.html b/server/static/index.html index feab380..9ff79c5 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -23,6 +23,8 @@ } #mapWrap { grid-column: 1; grid-row: 1; position: relative; min-height: 0; } #map { width: 100%; height: 100%; } + .leaflet-control-layers { background: #16213e; color: #eee; border: 1px solid #444; } + .leaflet-control-layers label { color: #eee; } #trackTimeline { display: none; grid-column: 1 / -1; grid-row: 2; background: #16213e; padding: 8px 16px; border-top: 1px solid #333; @@ -302,7 +304,7 @@ - +
Линейка высот @@ -336,9 +338,20 @@