mirror of
https://github.com/AkitaEngineering/Akita-Meshtastic-Meshcore-Bridge.git
synced 2026-10-31 01:42:01 +01:00
feat: implement message polling and handling for companion protocol
This commit is contained in:
1 parent
920b433f97
commit
f23069df17
3 files changed
+99
-26
No files matched your search
@@ -113,6 +113,7 @@ class MeshcoreHandler:
|
||||
self._companion_debug = bool(
|
||||
getattr(config, "companion_debug", False)
|
||||
)
|
||||
self._companion_msg_polling = False
|
||||
self._failure_count = 0
|
||||
self._auto_switched = False
|
||||
self._auto_switch_enabled = config.serial_auto_switch if config.serial_auto_switch is not None else True
|
||||
@@ -233,6 +234,9 @@ class MeshcoreHandler:
|
||||
app_name = b"AMMB"
|
||||
payload = bytes([1, 3]) + (b"\x00" * 6) + app_name
|
||||
self._send_companion_command(payload, "CMD_APP_START")
|
||||
# Initial poll for any pending messages
|
||||
self._companion_msg_polling = True
|
||||
self._send_companion_command(bytes([10]), "CMD_SYNC_NEXT_MESSAGE")
|
||||
except Exception as e:
|
||||
self.logger.warning("Failed to send companion handshake: %s", e)
|
||||
|
||||
@@ -250,6 +254,21 @@ class MeshcoreHandler:
|
||||
except Exception as e:
|
||||
self.logger.warning("Error sending companion command %s: %s", label, e)
|
||||
|
||||
def _send_sync_next_message(self):
|
||||
"""Send CMD_SYNC_NEXT_MESSAGE to poll queued messages from MeshCore."""
|
||||
try:
|
||||
payload = bytes([10]) # CMD_SYNC_NEXT_MESSAGE
|
||||
encoded = self.protocol_handler.encode({"payload": payload})
|
||||
if not encoded:
|
||||
return
|
||||
with self._lock:
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
self.serial_port.write(encoded)
|
||||
self.serial_port.flush()
|
||||
self.logger.debug("Sent CMD_SYNC_NEXT_MESSAGE")
|
||||
except Exception as e:
|
||||
self.logger.warning("Failed to send CMD_SYNC_NEXT_MESSAGE: %s", e)
|
||||
|
||||
def _contacts_poll_loop(self) -> None:
|
||||
"""Periodically request contacts from MeshCore to surface adverts."""
|
||||
self.logger.info(
|
||||
@@ -280,6 +299,7 @@ class MeshcoreHandler:
|
||||
self.logger.info("Serial handler stopped.")
|
||||
|
||||
def _close_serial(self):
|
||||
self._companion_msg_polling = False
|
||||
with self._lock:
|
||||
if self.serial_port and self.serial_port.is_open:
|
||||
port_name = self.config.serial_port
|
||||
@@ -350,10 +370,17 @@ class MeshcoreHandler:
|
||||
# Reset failure count on successful decode
|
||||
self._failure_count = 0
|
||||
if decoded_msg.get("internal_only"):
|
||||
kind = decoded_msg.get("companion_kind")
|
||||
self.logger.info(
|
||||
"Companion event: %s",
|
||||
decoded_msg.get("companion_kind"),
|
||||
"Companion event: %s", kind
|
||||
)
|
||||
# Handle message polling
|
||||
if kind == "msg_waiting":
|
||||
if not self._companion_msg_polling:
|
||||
self._companion_msg_polling = True
|
||||
self._send_sync_next_message()
|
||||
elif kind == "no_more_messages":
|
||||
self._companion_msg_polling = False
|
||||
continue
|
||||
if self._protocol_name == "companion_radio" and "payload" not in decoded_msg:
|
||||
self.logger.debug("Skipping non-message companion frame.")
|
||||
@@ -430,6 +457,12 @@ class MeshcoreHandler:
|
||||
self.logger.info(
|
||||
f"Queued message for Meshtastic {dest_meshtastic_id}"
|
||||
)
|
||||
# Continue polling if active
|
||||
if (
|
||||
self._protocol_name == "companion_radio"
|
||||
and self._companion_msg_polling
|
||||
):
|
||||
self._send_sync_next_message()
|
||||
except Full:
|
||||
self.logger.warning(
|
||||
"Meshtastic queue full; dropping message."
|
||||
|
||||
+33
-15
@@ -237,17 +237,19 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
def decode(self, raw_data: bytes) -> Optional[Dict[str, Any]]:
|
||||
"""Decode companion payload bytes into a dict.
|
||||
|
||||
Only forwards text-message frames; other frames are ignored.
|
||||
Handles RESP codes (from CMD_SYNC_NEXT_MESSAGE) and PUSH codes
|
||||
(unsolicited events from the radio). PUSH codes (>=0x80) have
|
||||
their own meanings and must NOT be mapped via base_code masking.
|
||||
"""
|
||||
if not raw_data:
|
||||
return None
|
||||
|
||||
code = raw_data[0]
|
||||
base_code = code & 0x7F if code & 0x80 else code
|
||||
|
||||
# Text message responses (also handle PUSH variants with high bit set)
|
||||
# --- RESP codes (responses to CMD_SYNC_NEXT_MESSAGE) ---
|
||||
|
||||
# V3 contact message (code 16 / 0x10)
|
||||
if base_code == 16: # RESP_CODE_CONTACT_MSG_RECV_V3
|
||||
if code == 16: # RESP_CODE_CONTACT_MSG_RECV_V3
|
||||
if len(raw_data) < 1 + 1 + 2 + 6 + 1 + 1 + 4:
|
||||
return None
|
||||
snr = raw_data[1]
|
||||
@@ -273,7 +275,7 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
}
|
||||
|
||||
# Legacy contact message (code 7)
|
||||
if base_code == 7: # RESP_CODE_CONTACT_MSG_RECV
|
||||
if code == 7: # RESP_CODE_CONTACT_MSG_RECV
|
||||
if len(raw_data) < 1 + 6 + 1 + 1 + 4:
|
||||
return None
|
||||
pubkey_prefix = raw_data[1:7]
|
||||
@@ -295,7 +297,7 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
}
|
||||
|
||||
# V3 channel message (code 17 / 0x11)
|
||||
if base_code == 17: # RESP_CODE_CHANNEL_MSG_RECV_V3
|
||||
if code == 17: # RESP_CODE_CHANNEL_MSG_RECV_V3
|
||||
if len(raw_data) < 1 + 1 + 2 + 1 + 1 + 1 + 4:
|
||||
return None
|
||||
snr = raw_data[1]
|
||||
@@ -320,7 +322,7 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
}
|
||||
|
||||
# Legacy channel message (code 8)
|
||||
if base_code == 8: # RESP_CODE_CHANNEL_MSG_RECV
|
||||
if code == 8: # RESP_CODE_CHANNEL_MSG_RECV
|
||||
if len(raw_data) < 1 + 1 + 1 + 1 + 4:
|
||||
return None
|
||||
channel_idx = raw_data[1]
|
||||
@@ -371,6 +373,15 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
if code == 10: # RESP_CODE_NO_MORE_MESSAGES
|
||||
return {
|
||||
"companion_kind": "no_more_messages",
|
||||
"internal_only": True,
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
# --- PUSH codes (unsolicited events from the radio) ---
|
||||
|
||||
if code == 0x82: # PUSH_CODE_SEND_CONFIRMED
|
||||
if len(raw_data) < 1 + 4 + 4:
|
||||
return None
|
||||
@@ -384,15 +395,22 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
if code == 0x83: # PUSH_CODE_MSG_WAITING
|
||||
return {
|
||||
"companion_kind": "msg_waiting",
|
||||
"internal_only": True,
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
if code == 0x80: # PUSH_CODE_ADVERT
|
||||
if len(raw_data) < 1 + 32:
|
||||
return None
|
||||
pubkey = raw_data[1:33]
|
||||
self.logger.info("MeshCore advert from: %s", pubkey[:4].hex())
|
||||
return {
|
||||
"destination_meshtastic_id": "^all",
|
||||
"payload": f"MC_ADVERT:{pubkey.hex()}",
|
||||
"channel_index": 0,
|
||||
"companion_kind": "advert",
|
||||
"pubkey": pubkey.hex(),
|
||||
"internal_only": True,
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
@@ -400,16 +418,16 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
if len(raw_data) < 1 + 32:
|
||||
return None
|
||||
pubkey = raw_data[1:33]
|
||||
self.logger.info("MeshCore new advert from: %s", pubkey[:4].hex())
|
||||
return {
|
||||
"destination_meshtastic_id": "^all",
|
||||
"payload": f"MC_NEW_ADVERT:{pubkey.hex()}",
|
||||
"channel_index": 0,
|
||||
"companion_kind": "new_advert",
|
||||
"pubkey": pubkey.hex(),
|
||||
"internal_only": True,
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
# Ignore non-message frames (device info, acks, errors, etc.)
|
||||
self.logger.debug("Ignoring companion frame code: %s", code)
|
||||
# Ignore non-message frames (device info, log data, etc.)
|
||||
self.logger.debug("Ignoring companion frame code: 0x%02x", code)
|
||||
return None
|
||||
|
||||
_serial_protocol_handlers = {
|
||||
|
||||
+31
-9
@@ -261,24 +261,21 @@ class TestMeshcoreCompanionProtocol:
|
||||
assert result["snr"] == 5
|
||||
|
||||
def test_decode_push_channel_msg(self, handler):
|
||||
"""PUSH variant (high bit set): code 0x88 = 8 | 0x80, base_code=8"""
|
||||
"""0x88 is PUSH_CODE_LOG_RX_DATA (not a channel msg) and should be ignored."""
|
||||
ts = 4000
|
||||
ts_bytes = ts.to_bytes(4, "little")
|
||||
raw = bytes([0x88, 1, 0, 0]) + ts_bytes + b"push channel"
|
||||
result = handler.decode(raw)
|
||||
assert result is not None
|
||||
assert result["payload"] == "push channel"
|
||||
assert result["channel_index"] == 1
|
||||
assert result is None # 0x88 is not a channel message
|
||||
|
||||
def test_decode_push_contact_msg(self, handler):
|
||||
"""PUSH variant (high bit set): code 0x87 = 7 | 0x80, base_code=7"""
|
||||
"""0x87 is PUSH_CODE_STATUS_RESPONSE (not a contact msg) and should be ignored."""
|
||||
pubkey = b"\x01\x02\x03\x04\x05\x06"
|
||||
ts = 5000
|
||||
ts_bytes = ts.to_bytes(4, "little")
|
||||
raw = bytes([0x87]) + pubkey + bytes([0, 0]) + ts_bytes + b"push contact"
|
||||
result = handler.decode(raw)
|
||||
assert result is not None
|
||||
assert result["payload"] == "push contact"
|
||||
assert result is None # 0x87 is not a contact message
|
||||
|
||||
# --- decode: control frames ---
|
||||
def test_decode_ok_response(self, handler):
|
||||
@@ -333,7 +330,9 @@ class TestMeshcoreCompanionProtocol:
|
||||
result = handler.decode(raw)
|
||||
assert result is not None
|
||||
assert result["companion_kind"] == "advert"
|
||||
assert "MC_ADVERT:" in result["payload"]
|
||||
assert result["internal_only"] is True
|
||||
assert result["pubkey"] == pubkey.hex()
|
||||
assert "payload" not in result
|
||||
|
||||
def test_decode_advert_too_short(self, handler):
|
||||
result = handler.decode(bytes([0x80]) + b"\x00" * 10)
|
||||
@@ -345,7 +344,9 @@ class TestMeshcoreCompanionProtocol:
|
||||
result = handler.decode(raw)
|
||||
assert result is not None
|
||||
assert result["companion_kind"] == "new_advert"
|
||||
assert "MC_NEW_ADVERT:" in result["payload"]
|
||||
assert result["internal_only"] is True
|
||||
assert result["pubkey"] == pubkey.hex()
|
||||
assert "payload" not in result
|
||||
|
||||
def test_decode_unknown_frame_returns_none(self, handler):
|
||||
"""Unknown/unhandled frame codes should return None."""
|
||||
@@ -361,6 +362,27 @@ class TestMeshcoreCompanionProtocol:
|
||||
result = handler.decode(bytes([8, 0, 1]))
|
||||
assert result is None
|
||||
|
||||
# --- decode: new companion protocol frames ---
|
||||
def test_decode_msg_waiting(self, handler):
|
||||
"""PUSH_CODE_MSG_WAITING (0x83) should return internal msg_waiting event."""
|
||||
result = handler.decode(bytes([0x83]))
|
||||
assert result is not None
|
||||
assert result["companion_kind"] == "msg_waiting"
|
||||
assert result["internal_only"] is True
|
||||
|
||||
def test_decode_no_more_messages(self, handler):
|
||||
"""RESP_CODE_NO_MORE_MESSAGES (10) should return internal no_more_messages event."""
|
||||
result = handler.decode(bytes([10]))
|
||||
assert result is not None
|
||||
assert result["companion_kind"] == "no_more_messages"
|
||||
assert result["internal_only"] is True
|
||||
|
||||
def test_decode_log_rx_data_ignored(self, handler):
|
||||
"""PUSH_CODE_LOG_RX_DATA (0x88) should be ignored (return None)."""
|
||||
raw = bytes([0x88]) + b"\x00" * 50
|
||||
result = handler.decode(raw)
|
||||
assert result is None
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Protocol Tests: Factory
|
||||
|
||||
Reference in new issue
Block a user