Fix freq and BW values, add geofence calc to dry run log

This commit is contained in:
Kizniche
2026-03-25 18:39:27 -04:00
parent f93844a01b
commit bab1693c82
3 changed files with 78 additions and 19 deletions
+17 -14
View File
@@ -84,14 +84,10 @@ def _ed25519_sign_expanded(
def _get_radio_params() -> dict:
"""Read radio frequency parameters from the connected radio's self_info.
The standalone AU divides raw freq/bw values by 1000 before sending.
The meshcore Python library returns radio_freq and radio_bw at the same
scale as the JS library, so we apply the same /1000 division.
IMPORTANT: verify the actual values in dry_run logs before enabling live
sends. The units reported by the Python library should be confirmed; if
the logged freq/bw look wrong (e.g. 0.915 instead of 915000), the
division factor here may need adjusting.
The Python meshcore library returns radio_freq in MHz (e.g. 910.525) and
radio_bw in kHz (e.g. 62.5). These are exactly the units the map API
expects, matching what the JS reference uploader produces after its own
/1000 division on raw integer values. No further scaling is applied here.
"""
try:
mc = radio_runtime.meshcore
@@ -105,10 +101,10 @@ def _get_radio_params() -> dict:
sf = info.get("radio_sf", 0) or 0
cr = info.get("radio_cr", 0) or 0
return {
"freq": freq / 1000.0 if freq else 0,
"freq": freq,
"cr": cr,
"sf": sf,
"bw": bw / 1000.0 if bw else 0,
"bw": bw,
}
except Exception as exc:
logger.debug("MapUpload: could not read radio params: %s", exc)
@@ -222,16 +218,17 @@ class MapUploadModule(FanoutModule):
lon: float,
) -> None:
# Geofence check: if enabled, skip nodes outside the configured radius
geofence_dist_km: float | None = None
if self.config.get("geofence_enabled"):
fence_lat = float(self.config.get("geofence_lat", 0) or 0)
fence_lon = float(self.config.get("geofence_lon", 0) or 0)
fence_radius_km = float(self.config.get("geofence_radius_km", 0) or 0)
dist_km = _haversine_km(fence_lat, fence_lon, lat, lon)
if dist_km > fence_radius_km:
geofence_dist_km = _haversine_km(fence_lat, fence_lon, lat, lon)
if geofence_dist_km > fence_radius_km:
logger.debug(
"MapUpload: skipping %s — outside geofence (%.2f km > %.2f km)",
pubkey[:12],
dist_km,
geofence_dist_km,
fence_radius_km,
)
return
@@ -271,10 +268,16 @@ class MapUploadModule(FanoutModule):
}
if dry_run:
geofence_note = (
f" | geofence: {geofence_dist_km:.2f} km from observer"
if geofence_dist_km is not None
else ""
)
logger.info(
"MapUpload [DRY RUN] %s (%s) → would POST to %s\n payload: %s",
"MapUpload [DRY RUN] %s (%s)%s → would POST to %s\n payload: %s",
pubkey[:12],
role_name,
geofence_note,
api_url,
json.dumps(request_payload, separators=(",", ":")),
)
@@ -294,7 +294,7 @@ const CREATE_INTEGRATION_DEFINITIONS: readonly CreateIntegrationDefinition[] = [
label: 'Map Upload',
section: 'Bulk Forwarding',
description:
'Upload node positions to map.meshcore.dev or a compatible map API endpoint.',
'Upload repeaters and room servers to map.meshcore.dev or a compatible map API endpoint.',
defaultName: 'Map Upload',
nameMode: 'counted',
defaults: {
@@ -1093,7 +1093,7 @@ function MapUploadConfigEditor({
return (
<div className="space-y-3">
<p className="text-xs text-muted-foreground">
Automatically upload heard repeater and room advertisements to{' '}
Automatically upload heard repeater and room server advertisements to{' '}
<a
href="https://map.meshcore.dev"
target="_blank"
+59 -3
View File
@@ -676,11 +676,12 @@ class TestGetRadioParams:
params = _get_radio_params()
assert params == {"freq": 0, "cr": 0, "sf": 0, "bw": 0}
def test_divides_freq_and_bw_by_1000(self):
def test_passes_freq_and_bw_directly(self):
"""Python lib already returns freq in MHz and bw in kHz — no division needed."""
mock_rt = MagicMock()
mock_rt.meshcore.self_info = {
"radio_freq": 915000,
"radio_bw": 125000,
"radio_freq": 915.0,
"radio_bw": 125.0,
"radio_sf": 10,
"radio_cr": 5,
}
@@ -893,3 +894,58 @@ class TestGeofence:
assert ("ab" * 32) in mod._seen
await mod.stop()
@pytest.mark.asyncio
async def test_dry_run_geofence_logs_distance(self):
"""dry_run + geofence_enabled must include the calculated distance in the log line."""
import logging
mod = _make_module({
"dry_run": True,
"geofence_enabled": True,
"geofence_lat": 51.5,
"geofence_lon": -0.1,
"geofence_radius_km": 100.0,
})
await mod.start()
fake_private = bytes(range(64))
fake_public = bytes(range(32))
with (
patch("app.fanout.map_upload.get_private_key", return_value=fake_private),
patch("app.fanout.map_upload.get_public_key", return_value=fake_public),
patch("app.fanout.map_upload._get_radio_params", return_value={"freq": 0, "cr": 0, "sf": 0, "bw": 0}),
):
with patch("app.fanout.map_upload.logger") as mock_logger:
# ~50 km north — inside the fence
await mod._upload("ab" * 32, 1000, 2, "aabb", 51.95, -0.1)
mock_logger.info.assert_called_once()
log_message = mock_logger.info.call_args[0][0] % mock_logger.info.call_args[0][1:]
assert "geofence:" in log_message
assert "km from observer" in log_message
await mod.stop()
@pytest.mark.asyncio
async def test_dry_run_no_geofence_no_distance_in_log(self):
"""dry_run without geofence_enabled must not include a distance note in the log."""
mod = _make_module({"dry_run": True, "geofence_enabled": False})
await mod.start()
fake_private = bytes(range(64))
fake_public = bytes(range(32))
with (
patch("app.fanout.map_upload.get_private_key", return_value=fake_private),
patch("app.fanout.map_upload.get_public_key", return_value=fake_public),
patch("app.fanout.map_upload._get_radio_params", return_value={"freq": 0, "cr": 0, "sf": 0, "bw": 0}),
):
with patch("app.fanout.map_upload.logger") as mock_logger:
await mod._upload("ab" * 32, 1000, 2, "aabb", 51.5, -0.1)
mock_logger.info.assert_called_once()
log_message = mock_logger.info.call_args[0][0] % mock_logger.info.call_args[0][1:]
assert "geofence:" not in log_message
await mod.stop()