mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Get closer to parity with meshcore packet capture
This commit is contained in:
@@ -196,10 +196,9 @@ On connect and every 5 minutes thereafter, the community publisher sends a retai
|
||||
"timestamp": "2024-01-15T10:30:00.000000",
|
||||
"origin": "NodeName",
|
||||
"origin_id": "PUBKEY_HEX_UPPER",
|
||||
"client": "RemoteTerm (github.com/...)",
|
||||
"model": "T-Deck",
|
||||
"firmware_version": "v2.2.2 (Build: 2025-01-15)",
|
||||
"radio": "915.0MHz BW250.0 SF10 CR8",
|
||||
"radio": "915.0,250.0,10,8",
|
||||
"client_version": "RemoteTerm/2.4.0",
|
||||
"stats": {
|
||||
"battery_mv": 4200,
|
||||
@@ -216,7 +215,7 @@ On connect and every 5 minutes thereafter, the community publisher sends a retai
|
||||
```
|
||||
|
||||
- `model` and `firmware_version` are fetched once per connection via `send_device_query()` (requires firmware version >= 3)
|
||||
- `radio` is formatted from `self_info` radio parameters (freq, BW, SF, CR)
|
||||
- `radio` is comma-separated raw values from `self_info` (freq, BW, SF, CR) matching the reference format
|
||||
- `client_version` is read from Python package metadata (`remoteterm-meshcore`)
|
||||
- `stats` is fetched from `get_stats_core()` + `get_stats_radio()` every 5 minutes; omitted if firmware doesn't support stats commands
|
||||
- All radio queries use `blocking=False` — if the radio is busy, cached values are used. No user-facing operations are ever blocked.
|
||||
|
||||
@@ -265,21 +265,24 @@ def _build_status_topic(settings: AppSettings, pubkey_hex: str) -> str:
|
||||
|
||||
|
||||
def _build_radio_info() -> str:
|
||||
"""Format the radio parameters string from self_info, or 'unknown'."""
|
||||
"""Format the radio parameters string from self_info.
|
||||
|
||||
Matches the reference format: ``"freq,bw,sf,cr"`` (comma-separated raw
|
||||
values). Falls back to ``"0,0,0,0"`` when unavailable.
|
||||
"""
|
||||
from app.radio import radio_manager
|
||||
|
||||
try:
|
||||
if radio_manager.meshcore and radio_manager.meshcore.self_info:
|
||||
info = radio_manager.meshcore.self_info
|
||||
freq = info.get("radio_freq")
|
||||
bw = info.get("radio_bw")
|
||||
sf = info.get("radio_sf")
|
||||
cr = info.get("radio_cr")
|
||||
if freq is not None and bw is not None and sf is not None and cr is not None:
|
||||
return f"{freq}MHz BW{bw} SF{sf} CR{cr}"
|
||||
freq = info.get("radio_freq", 0)
|
||||
bw = info.get("radio_bw", 0)
|
||||
sf = info.get("radio_sf", 0)
|
||||
cr = info.get("radio_cr", 0)
|
||||
return f"{freq},{bw},{sf},{cr}"
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
return "0,0,0,0"
|
||||
|
||||
|
||||
def _get_client_version() -> str:
|
||||
@@ -340,6 +343,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
|
||||
|
||||
def _build_client_kwargs(self, settings: AppSettings) -> dict[str, Any]:
|
||||
from app.keystore import get_private_key, get_public_key
|
||||
from app.radio import radio_manager
|
||||
|
||||
private_key = get_private_key()
|
||||
public_key = get_public_key()
|
||||
@@ -357,12 +361,17 @@ class CommunityMqttPublisher(BaseMqttPublisher):
|
||||
|
||||
tls_context = ssl.create_default_context()
|
||||
|
||||
device_name = ""
|
||||
if radio_manager.meshcore and radio_manager.meshcore.self_info:
|
||||
device_name = radio_manager.meshcore.self_info.get("name", "")
|
||||
|
||||
status_topic = _build_status_topic(settings, pubkey_hex)
|
||||
offline_payload = json.dumps(
|
||||
{
|
||||
"status": "offline",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"origin": device_name or "MeshCore Device",
|
||||
"origin_id": pubkey_hex,
|
||||
"client": _CLIENT_ID,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -494,7 +503,6 @@ class CommunityMqttPublisher(BaseMqttPublisher):
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"origin": device_name or "MeshCore Device",
|
||||
"origin_id": pubkey_hex,
|
||||
"client": _CLIENT_ID,
|
||||
"model": device_info.get("model", "unknown"),
|
||||
"firmware_version": device_info.get("firmware_version", "unknown"),
|
||||
"radio": _build_radio_info(),
|
||||
|
||||
@@ -341,7 +341,16 @@ export function SettingsMqttSection({
|
||||
<div className="px-4 pb-4 space-y-3 border-t border-input">
|
||||
<p className="text-xs text-muted-foreground pt-3">
|
||||
Share raw packet data with the MeshCore community for coverage mapping and network
|
||||
analysis. Only raw RF packets are shared — never decrypted messages.
|
||||
analysis. Only raw RF packets are shared — never decrypted messages. General parity
|
||||
with{' '}
|
||||
<a
|
||||
href="https://github.com/agessaman/meshcore-packet-capture"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
meshcore-packet-capture
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
|
||||
@@ -463,9 +463,14 @@ class TestLwtAndStatusPublish:
|
||||
community_mqtt_iata="SFO",
|
||||
)
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = MagicMock()
|
||||
mock_radio.meshcore.self_info = {"name": "TestNode"}
|
||||
|
||||
with (
|
||||
patch("app.keystore.get_private_key", return_value=private_key),
|
||||
patch("app.keystore.get_public_key", return_value=public_key),
|
||||
patch("app.radio.radio_manager", mock_radio),
|
||||
):
|
||||
kwargs = pub._build_client_kwargs(settings)
|
||||
|
||||
@@ -475,8 +480,10 @@ class TestLwtAndStatusPublish:
|
||||
assert will.retain is True
|
||||
payload = json.loads(will.payload)
|
||||
assert payload["status"] == "offline"
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert payload["client"] == _CLIENT_ID
|
||||
assert "timestamp" in payload
|
||||
assert "client" not in payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_connected_async_publishes_online_status(self):
|
||||
@@ -505,7 +512,7 @@ class TestLwtAndStatusPublish:
|
||||
patch.object(
|
||||
pub, "_fetch_stats", new_callable=AsyncMock, return_value={"battery_mv": 4200}
|
||||
),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0MHz BW250.0 SF10 CR8"),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm/2.4.0"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
@@ -521,11 +528,11 @@ class TestLwtAndStatusPublish:
|
||||
assert payload["status"] == "online"
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert payload["client"] == _CLIENT_ID
|
||||
assert "client" not in payload
|
||||
assert "timestamp" in payload
|
||||
assert payload["model"] == "T-Deck"
|
||||
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
|
||||
assert payload["radio"] == "915.0MHz BW250.0 SF10 CR8"
|
||||
assert payload["radio"] == "915.0,250.0,10,8"
|
||||
assert payload["client_version"] == "RemoteTerm/2.4.0"
|
||||
assert payload["stats"] == {"battery_mv": 4200}
|
||||
|
||||
@@ -539,9 +546,13 @@ class TestLwtAndStatusPublish:
|
||||
community_mqtt_iata="JFK",
|
||||
)
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
|
||||
with (
|
||||
patch("app.keystore.get_private_key", return_value=private_key),
|
||||
patch("app.keystore.get_public_key", return_value=public_key),
|
||||
patch("app.radio.radio_manager", mock_radio),
|
||||
):
|
||||
kwargs = pub._build_client_kwargs(settings)
|
||||
|
||||
@@ -583,7 +594,7 @@ class TestLwtAndStatusPublish:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="unknown"),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm/unknown"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
@@ -822,7 +833,7 @@ class TestFetchStats:
|
||||
|
||||
class TestBuildRadioInfo:
|
||||
def test_formatted_string(self):
|
||||
"""Should return formatted radio info string."""
|
||||
"""Should return comma-separated radio info matching reference format."""
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = MagicMock()
|
||||
mock_radio.meshcore.self_info = {
|
||||
@@ -835,20 +846,20 @@ class TestBuildRadioInfo:
|
||||
with patch("app.radio.radio_manager", mock_radio):
|
||||
result = _build_radio_info()
|
||||
|
||||
assert result == "915.0MHz BW250.0 SF10 CR8"
|
||||
assert result == "915.0,250.0,10,8"
|
||||
|
||||
def test_fallback_when_no_meshcore(self):
|
||||
"""Should return 'unknown' when meshcore is None."""
|
||||
"""Should return '0,0,0,0' when meshcore is None."""
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
|
||||
with patch("app.radio.radio_manager", mock_radio):
|
||||
result = _build_radio_info()
|
||||
|
||||
assert result == "unknown"
|
||||
assert result == "0,0,0,0"
|
||||
|
||||
def test_fallback_when_self_info_missing_fields(self):
|
||||
"""Should return 'unknown' when self_info lacks radio fields."""
|
||||
"""Should use 0 defaults when self_info lacks radio fields."""
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = MagicMock()
|
||||
mock_radio.meshcore.self_info = {"name": "TestNode"}
|
||||
@@ -856,7 +867,7 @@ class TestBuildRadioInfo:
|
||||
with patch("app.radio.radio_manager", mock_radio):
|
||||
result = _build_radio_info()
|
||||
|
||||
assert result == "unknown"
|
||||
assert result == "0,0,0,0"
|
||||
|
||||
|
||||
class TestGetClientVersion:
|
||||
@@ -905,7 +916,7 @@ class TestPublishStatus:
|
||||
return_value={"model": "T-Deck", "firmware_version": "v2.2.2 (Build: 2025-01-15)"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=stats),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0MHz BW250.0 SF10 CR8"),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm/2.4.0"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
@@ -915,10 +926,10 @@ class TestPublishStatus:
|
||||
assert payload["status"] == "online"
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert payload["client"] == _CLIENT_ID
|
||||
assert "client" not in payload
|
||||
assert payload["model"] == "T-Deck"
|
||||
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
|
||||
assert payload["radio"] == "915.0MHz BW250.0 SF10 CR8"
|
||||
assert payload["radio"] == "915.0,250.0,10,8"
|
||||
assert payload["client_version"] == "RemoteTerm/2.4.0"
|
||||
assert payload["stats"] == stats
|
||||
|
||||
@@ -942,7 +953,7 @@ class TestPublishStatus:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="unknown"),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm/unknown"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
@@ -973,7 +984,7 @@ class TestPublishStatus:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="unknown"),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm/unknown"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user