meshai/work/meshai/responder.py
malice f3df7a0a6d
feat(transport): CompositeTransport for dual Meshtastic+MeshCore (Phase 4) (#6)
* feat(transport): CompositeTransport for dual Meshtastic+MeshCore (Phase 4)

Adds CompositeTransport (transport: both) that fans broadcasts to both
meshes, sizes to min(children) for uniform messages, and routes DM
replies back over the originating mesh via a transport hint threaded from
the inbound MeshMessage. Per-child self-filtering; supervisor watchdog now
resolves the Meshtastic child inside the composite. Additive/optional
throughout; single-transport behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(sizing): fixed universal mesh budget (mesh_max_chars=140)

Replace per-transport / min-of-active max_chars with a single fixed
universal constant (mesh_max_chars, default 140 = MeshCore LCD). Every
message is built once against one deterministic budget regardless of
which radios are connected; no runtime variance, no per-transport
retooling. Meshtastic sizing intentionally moves 200 -> 140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:33:17 -06:00

67 lines
2.1 KiB
Python

"""Response handling - delays and message delivery."""
import asyncio
import logging
import random
from typing import Optional
from .config import ResponseConfig
from .connector import MeshConnector
logger = logging.getLogger(__name__)
class Responder:
"""Handles response delivery with pacing."""
def __init__(self, config: ResponseConfig, connector: MeshConnector):
self.config = config
self.connector = connector
async def send_response(
self,
messages: list[str] | str,
destination: Optional[str] = None,
channel: int = 0,
transport: Optional[str] = None,
) -> bool:
"""Send response messages with randomized delay pacing.
Args:
messages: One or more message strings to send.
destination: Node ID for a DM, or None for broadcast.
channel: Channel index to send on.
transport: Optional routing hint threaded from the originating
MeshMessage. Passed through to connector.send_message
so CompositeTransport can route DM replies back over
the mesh they arrived on. Single-transport connectors
accept and ignore it; defaults to None so all existing
call sites are unaffected.
"""
if isinstance(messages, str):
messages = [messages]
if not messages:
return True
success = True
for i, msg in enumerate(messages):
if i > 0:
delay = random.uniform(self.config.delay_min, self.config.delay_max)
await asyncio.sleep(delay)
sent = self.connector.send_message(
text=msg,
destination=destination,
channel=channel,
transport=transport,
)
if not sent:
logger.error(f"Failed to send message {i+1}/{len(messages)}")
success = False
break
logger.debug(f"Sent msg {i+1}/{len(messages)}: {msg[:50]}...")
return success