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>
This commit is contained in:
malice 2026-07-02 12:33:17 -06:00 committed by GitHub
commit f3df7a0a6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1140 additions and 76 deletions

View file

@ -1,8 +1,8 @@
"""Tests for Phase 3 uniform mesh-packet sizing.
"""Tests for uniform mesh-packet sizing.
Verifies that every size-sensitive code path uses the active transport's
max_chars rather than a hardcoded 200. Meshtastic default stays 200
(byte-identical); MeshCore uses 140.
Verifies that every size-sensitive code path uses the single universal
mesh_max_chars constant (140 = MeshCore LCD). All transports Meshtastic,
MeshCore, and the composite report the same fixed value.
"""
import sys
@ -127,59 +127,90 @@ def _minimal_config():
# ---------------------------------------------------------------------------
class TestTransportMaxChars:
def test_meshtastic_default_is_200(self):
def test_meshtastic_default_is_140(self):
"""Meshtastic now uses the universal mesh_max_chars constant (140)."""
cfg = _mt_config()
t = MeshtasticTransport(cfg)
assert t.max_chars == 200
def test_meshtastic_respects_config_field(self):
cfg = _mt_config(meshtastic_max_chars=160)
t = MeshtasticTransport(cfg)
assert t.max_chars == 160
assert t.max_chars == 140
def test_meshcore_default_is_140(self):
cfg = _mc_config()
t = MeshCoreTransport(cfg)
assert t.max_chars == 140
def test_meshcore_respects_config_field(self):
cfg = _mc_config(meshcore_max_chars=120)
t = MeshCoreTransport(cfg)
assert t.max_chars == 120
def test_build_transport_meshtastic_max_chars(self):
"""build_transport(meshtastic) → 140 (universal constant)."""
t = build_transport(_mt_config())
assert t.max_chars == 200
assert t.max_chars == 140
def test_build_transport_meshcore_max_chars(self):
t = build_transport(_mc_config())
assert t.max_chars == 140
def test_mesh_max_chars_config_field_governs_all(self):
"""A non-default mesh_max_chars propagates to both transport types."""
cfg_mt = _mt_config(mesh_max_chars=120)
assert MeshtasticTransport(cfg_mt).max_chars == 120
cfg_mc = _mc_config(mesh_max_chars=120)
assert MeshCoreTransport(cfg_mc).max_chars == 120
def test_all_three_transports_and_composite_report_140(self):
"""Sanity: all transports + CompositeTransport report mesh_max_chars=140.
This is the canonical single-budget guarantee: Meshtastic, MeshCore,
and the composite (transport=both) all resolve to the same constant.
"""
from meshai.config import ConnectionConfig
from meshai.transport.composite_transport import CompositeTransport
# Meshtastic
assert MeshtasticTransport(_mt_config()).max_chars == 140
# MeshCore
assert MeshCoreTransport(_mc_config()).max_chars == 140
# Composite (built via factory with transport="both")
cfg_both = ConnectionConfig(
transport="both",
type="tcp",
tcp_host="127.0.0.1",
tcp_port=4403,
meshcore_host="127.0.0.1",
meshcore_port=5050,
meshcore_channel_index=0,
)
comp = build_transport(cfg_both)
assert isinstance(comp, CompositeTransport)
assert comp.max_chars == 140
# ---------------------------------------------------------------------------
# 2. MeshRenderer char_limit propagation via channels
# ---------------------------------------------------------------------------
class TestChannelRendererBudget:
def test_broadcast_channel_with_meshcore_connector_uses_140(self):
def test_broadcast_channel_propagates_connector_max_chars(self):
"""Channel renderer inherits max_chars from whatever the connector reports."""
conn = _fake_connector(140)
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
assert ch._renderer._limit == 140
def test_broadcast_channel_with_meshtastic_connector_uses_200(self):
conn = _fake_connector(200)
def test_broadcast_channel_with_meshtastic_connector_uses_140(self):
"""After the universal budget refactor, a meshtastic connector reports 140."""
conn = _fake_connector(140)
ch = MeshBroadcastChannel(connector=conn, channel_index=0)
assert ch._renderer._limit == 200
assert ch._renderer._limit == 140
def test_dm_channel_with_meshcore_connector_uses_140(self):
def test_dm_channel_propagates_connector_max_chars(self):
conn = _fake_connector(140)
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
assert ch._renderer._limit == 140
def test_dm_channel_with_meshtastic_connector_uses_200(self):
conn = _fake_connector(200)
def test_dm_channel_with_meshtastic_connector_uses_140(self):
conn = _fake_connector(140)
ch = MeshDMChannel(connector=conn, node_ids=["!aabbccdd"])
assert ch._renderer._limit == 200
assert ch._renderer._limit == 140
# ---------------------------------------------------------------------------
@ -187,11 +218,12 @@ class TestChannelRendererBudget:
# ---------------------------------------------------------------------------
class TestBuildPipelineMeshCharLimit:
def test_no_connector_uses_200(self):
def test_no_connector_uses_140(self):
"""Without a connector the pipeline falls back to the universal constant (140)."""
cfg = _minimal_config()
bus = build_pipeline(cfg, llm_backend=None, connector=None)
acc = bus._pipeline_components["accumulator"]
assert acc._mesh_char_limit == 200
assert acc._mesh_char_limit == 140
def test_meshcore_connector_uses_140(self):
cfg = _minimal_config()
@ -200,12 +232,13 @@ class TestBuildPipelineMeshCharLimit:
acc = bus._pipeline_components["accumulator"]
assert acc._mesh_char_limit == 140
def test_meshtastic_connector_uses_200(self):
def test_meshtastic_connector_uses_140(self):
"""After universal budget, a meshtastic connector also reports 140."""
cfg = _minimal_config()
conn = _fake_connector(200)
conn = _fake_connector(140)
bus = build_pipeline(cfg, llm_backend=None, connector=conn)
acc = bus._pipeline_components["accumulator"]
assert acc._mesh_char_limit == 200
assert acc._mesh_char_limit == 140
# ---------------------------------------------------------------------------