diff --git a/ammb/meshcore_handler.py b/ammb/meshcore_handler.py index fe60074..a9499be 100644 --- a/ammb/meshcore_handler.py +++ b/ammb/meshcore_handler.py @@ -91,6 +91,7 @@ class MeshcoreHandler: ) self._protocol_name = config.serial_protocol.lower() + self.logger.info(f"Selected serial protocol: {self._protocol_name}") self._failure_count = 0 self._auto_switched = False self._auto_switch_enabled = getattr(config, "serial_auto_switch", True) @@ -99,6 +100,7 @@ class MeshcoreHandler: self.protocol_handler: MeshcoreProtocolHandler = ( get_serial_protocol_handler(self._protocol_name) ) + self.logger.info(f"Initialized protocol handler: {type(self.protocol_handler).__name__}") except ValueError as e: self.logger.critical( "Failed to initialize serial protocol handler '%s': %s.", @@ -113,6 +115,7 @@ class MeshcoreHandler: def decode(self, line): return None self.protocol_handler = DummyHandler() + self.logger.warning("Using DummyHandler for protocol; no serial communication will occur.") self.logger.info("Serial Handler (MeshcoreHandler) Initialized.") def _switch_protocol(self): @@ -151,7 +154,47 @@ class MeshcoreHandler: "Serial port %s already connected.", self.config.serial_port, ) - # ...existing code... + self._is_connected.set() + return True + # Try to open the serial port + try: + self.serial_port = serial.Serial( + port=self.config.serial_port, + baudrate=self.config.serial_baud, + timeout=1, + write_timeout=1 + ) + if self.serial_port.is_open: + self.logger.info( + "Serial port %s opened successfully.", + self.config.serial_port, + ) + self._is_connected.set() + return True + else: + self.logger.error( + "Failed to open serial port %s.", + self.config.serial_port, + ) + self._is_connected.clear() + return False + except serial.SerialException as e: + self.logger.error( + "SerialException opening port %s: %s", + self.config.serial_port, + e, + ) + self._is_connected.clear() + return False + except Exception as e: + self.logger.error( + "Unexpected error opening serial port %s: %s", + self.config.serial_port, + e, + exc_info=True, + ) + self._is_connected.clear() + return False def stop(self): self.logger.info("Stopping Serial handler...") @@ -189,6 +232,7 @@ class MeshcoreHandler: and queues.""" self.logger.info("Serial receiver loop started.") while not self.shutdown_event.is_set(): + # Placeholder: ACK/event handling logic could be added here # --- Connection Check --- if not self._is_connected.is_set(): self.logger.warning( @@ -217,18 +261,16 @@ class MeshcoreHandler: continue if raw_data: - self.logger.debug("Serial RAW RX: %r", raw_data) - # Update health on every receive + self.logger.debug(f"Serial RAW RX: {raw_data!r}") self.health_monitor.update_component( "external", HealthStatus.HEALTHY, "Serial RX received" ) - # Decode using the selected protocol handler decoded_msg: Optional[Dict[str, Any]] = ( self.protocol_handler.decode(raw_data) ) + self.logger.debug(f"Decoded serial message: {decoded_msg}") if decoded_msg: - # Validate message is_valid, error_msg = ( self.validator.validate_external_message( decoded_msg @@ -236,13 +278,11 @@ class MeshcoreHandler: ) if not is_valid: self.logger.warning( - "Invalid external message rejected: %s", - error_msg + f"Invalid external message rejected: {error_msg}" ) self.metrics.record_error("external") continue - # Check rate limit if not self.rate_limiter.check_rate_limit( "serial_receiver" ): @@ -254,12 +294,10 @@ class MeshcoreHandler: ) continue - # Sanitize message decoded_msg = self.validator.sanitize_external_message( decoded_msg ) - # Basic Translation Logic (Serial -> Meshtastic) dest_meshtastic_id = decoded_msg.get( "destination_meshtastic_id" ) @@ -276,12 +314,13 @@ class MeshcoreHandler: text_payload_str = json.dumps(payload_json) except (TypeError, ValueError) as e: self.logger.error( - "Failed to serialize payload_json: %s", - e + f"Failed to serialize payload_json: {e}" ) elif payload is not None: text_payload_str = str(payload) + self.logger.debug(f"Translation: dest={dest_meshtastic_id}, text={text_payload_str}, channel={channel_index}, want_ack={want_ack}") + if dest_meshtastic_id and text_payload_str is not None: meshtastic_msg = { "destination": dest_meshtastic_id, @@ -289,7 +328,6 @@ class MeshcoreHandler: "channel_index": channel_index, "want_ack": want_ack, } - try: self.to_meshtastic_queue.put_nowait( meshtastic_msg @@ -303,8 +341,7 @@ class MeshcoreHandler: payload_size ) self.logger.info( - "Queued message for Meshtastic %s", - dest_meshtastic_id, + f"Queued message for Meshtastic {dest_meshtastic_id}" ) except Full: self.logger.warning( @@ -315,7 +352,7 @@ class MeshcoreHandler: self.logger.warning( "Decoded serial message missing fields." ) - self.logger.debug("Decoded: %s", decoded_msg) + self.logger.debug(f"Decoded: {decoded_msg}") else: # No data available - sleep briefly to avoid CPU spin time.sleep(0.1) diff --git a/ammb/protocol.py b/ammb/protocol.py index 5acbe16..892c5bb 100644 --- a/ammb/protocol.py +++ b/ammb/protocol.py @@ -164,9 +164,100 @@ class RawSerialProtocol(MeshcoreProtocolHandler): # --- Factory Function --- + +# --- Meshcore Companion Radio Protocol Handler --- +import struct + +class MeshcoreCompanionProtocol(MeshcoreProtocolHandler): + """ + Handles the Meshcore Companion Radio Protocol (binary, framed). + Frame format: + [0x7E][len][payload...][CRC16][0x7E] + """ + FRAME_DELIM = 0x7E + CRC_LEN = 2 + + 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 + + def encode(self, data: Dict[str, Any]) -> Optional[bytes]: + """Encodes a payload dict into a framed binary message.""" + try: + payload = data.get("payload", b"") + if isinstance(payload, str): + payload = payload.encode("utf-8") + elif not isinstance(payload, (bytes, bytearray)): + return None + length = len(payload) + frame = bytearray() + frame.append(self.FRAME_DELIM) + frame.append(length) + 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) + 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 + _serial_protocol_handlers = { "json_newline": JsonNewlineProtocol, "raw_serial": RawSerialProtocol, + "companion_radio": MeshcoreCompanionProtocol, }