forked from iarv/Akita-Meshtastic-Meshcore-Bridge
feat: update Meshcore Companion Radio Protocol handler for USB framing and payload processing
This commit is contained in:
@@ -81,7 +81,7 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
|
||||
VALID_LOG_LEVELS = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"}
|
||||
VALID_SERIAL_PROTOCOLS = {"json_newline", "raw_serial"}
|
||||
VALID_SERIAL_PROTOCOLS = {"json_newline", "raw_serial", "companion_radio"}
|
||||
VALID_TRANSPORTS = {"serial", "mqtt"}
|
||||
VALID_MQTT_QOS = {0, 1, 2}
|
||||
|
||||
@@ -107,12 +107,6 @@ def load_config(config_path: str = CONFIG_FILE) -> Optional[BridgeConfig]:
|
||||
logger.info("Reading configuration from: %s", config_path)
|
||||
config.read(config_path)
|
||||
|
||||
if "DEFAULT" not in config.sections():
|
||||
logger.warning(
|
||||
"Configuration file '%s' lacks the [DEFAULT] section.",
|
||||
config_path,
|
||||
)
|
||||
logger.warning("Using only defaults.")
|
||||
cfg_section = config["DEFAULT"] if "DEFAULT" in config else DEFAULT_CONFIG
|
||||
|
||||
# Only set meshtastic_port if explicitly present and not commented out
|
||||
|
||||
115
ammb/protocol.py
115
ammb/protocol.py
@@ -170,37 +170,53 @@ import struct
|
||||
|
||||
class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
"""
|
||||
Handles the Meshcore Companion Radio Protocol (binary, framed).
|
||||
Frame format:
|
||||
[0x7E][len][payload...][CRC16][0x7E]
|
||||
Handles the Meshcore Companion Radio Protocol (USB framing).
|
||||
Outbound (radio -> app): [0x3E][len_le][payload...]
|
||||
Inbound (app -> radio): [0x3C][len_le][payload...]
|
||||
"""
|
||||
FRAME_DELIM = 0x7E
|
||||
CRC_LEN = 2
|
||||
|
||||
OUTBOUND_START = 0x3E # '>'
|
||||
INBOUND_START = 0x3C # '<'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._rx_buffer = bytearray()
|
||||
|
||||
def read(self, serial_port) -> Optional[bytes]:
|
||||
"""Reads a full frame delimited by 0x7E, returns the raw frame (including delimiters)."""
|
||||
frame = bytearray()
|
||||
in_frame = False
|
||||
while serial_port.in_waiting > 0:
|
||||
byte = serial_port.read(1)
|
||||
if not byte:
|
||||
break
|
||||
b = byte[0]
|
||||
if b == self.FRAME_DELIM:
|
||||
if in_frame and len(frame) > 0:
|
||||
# End of frame
|
||||
frame.append(b)
|
||||
return bytes(frame)
|
||||
else:
|
||||
# Start of frame
|
||||
frame = bytearray([b])
|
||||
in_frame = True
|
||||
elif in_frame:
|
||||
frame.append(b)
|
||||
return None
|
||||
"""Reads and returns a single outbound payload frame (without framing)."""
|
||||
if serial_port.in_waiting > 0:
|
||||
chunk = serial_port.read(serial_port.in_waiting)
|
||||
if chunk:
|
||||
self.logger.debug("Companion RAW bytes: %s", chunk.hex())
|
||||
self._rx_buffer.extend(chunk)
|
||||
|
||||
while True:
|
||||
if len(self._rx_buffer) < 3:
|
||||
return None
|
||||
|
||||
if self._rx_buffer[0] != self.OUTBOUND_START:
|
||||
# Resync to next '>'
|
||||
try:
|
||||
next_idx = self._rx_buffer.index(self.OUTBOUND_START)
|
||||
del self._rx_buffer[:next_idx]
|
||||
except ValueError:
|
||||
self._rx_buffer.clear()
|
||||
return None
|
||||
|
||||
if len(self._rx_buffer) < 3:
|
||||
return None
|
||||
|
||||
length = self._rx_buffer[1] | (self._rx_buffer[2] << 8)
|
||||
frame_len = 3 + length
|
||||
if len(self._rx_buffer) < frame_len:
|
||||
return None
|
||||
|
||||
payload = bytes(self._rx_buffer[3:frame_len])
|
||||
del self._rx_buffer[:frame_len]
|
||||
return payload
|
||||
|
||||
def encode(self, data: Dict[str, Any]) -> Optional[bytes]:
|
||||
"""Encodes a payload dict into a framed binary message."""
|
||||
"""Encodes a payload dict into an inbound frame."""
|
||||
try:
|
||||
payload = data.get("payload", b"")
|
||||
if isinstance(payload, str):
|
||||
@@ -209,50 +225,25 @@ class MeshcoreCompanionProtocol(MeshcoreProtocolHandler):
|
||||
return None
|
||||
length = len(payload)
|
||||
frame = bytearray()
|
||||
frame.append(self.FRAME_DELIM)
|
||||
frame.append(length)
|
||||
frame.append(self.INBOUND_START)
|
||||
frame.append(length & 0xFF)
|
||||
frame.append((length >> 8) & 0xFF)
|
||||
frame.extend(payload)
|
||||
crc = self._crc16_ccitt(frame[1:2+length])
|
||||
frame.extend(struct.pack('>H', crc))
|
||||
frame.append(self.FRAME_DELIM)
|
||||
return bytes(frame)
|
||||
except Exception as e:
|
||||
self.logger.error("Error encoding companion frame: %s", e)
|
||||
return None
|
||||
|
||||
def decode(self, raw_data: bytes) -> Optional[Dict[str, Any]]:
|
||||
"""Decodes a framed binary message into a dict."""
|
||||
try:
|
||||
if not raw_data or raw_data[0] != self.FRAME_DELIM or raw_data[-1] != self.FRAME_DELIM:
|
||||
return None
|
||||
length = raw_data[1]
|
||||
payload = raw_data[2:2+length]
|
||||
crc_recv = struct.unpack('>H', raw_data[2+length:2+length+2])[0]
|
||||
crc_calc = self._crc16_ccitt(raw_data[1:2+length])
|
||||
if crc_recv != crc_calc:
|
||||
self.logger.warning("CRC mismatch: recv=%04X calc=%04X", crc_recv, crc_calc)
|
||||
return None
|
||||
return {
|
||||
"destination_meshtastic_id": "^all",
|
||||
"payload": payload,
|
||||
"raw_binary": True,
|
||||
"protocol": "companion_radio"
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error("Error decoding companion frame: %s", e)
|
||||
"""Wraps received payload bytes into a dict."""
|
||||
if not raw_data:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _crc16_ccitt(data: bytes, crc: int = 0xFFFF) -> int:
|
||||
for b in data:
|
||||
crc ^= b << 8
|
||||
for _ in range(8):
|
||||
if crc & 0x8000:
|
||||
crc = (crc << 1) ^ 0x1021
|
||||
else:
|
||||
crc <<= 1
|
||||
crc &= 0xFFFF
|
||||
return crc
|
||||
return {
|
||||
"destination_meshtastic_id": "^all",
|
||||
"payload": raw_data,
|
||||
"raw_binary": True,
|
||||
"protocol": "companion_radio",
|
||||
}
|
||||
|
||||
_serial_protocol_handlers = {
|
||||
"json_newline": JsonNewlineProtocol,
|
||||
|
||||
Reference in New Issue
Block a user