Files
Jean-Pierre MartineauandClaude Opus 5.5 b278ccfdc1 In the demo, the TX power of a repeater changes how well it is heard
TX optimize had nothing to find in the demo: the demo device answered each trace from the world's links and ignored the power that the sweep set on a repeater. Now a relay that TX optimize tuned is heard less well on the link that it transmits into, on the simulator's curve with its peak at 19 dBm, and a reading under the decode floor loses the trace. The repeaters of the world read 22 dBm, so the sweep finds a lower power with a better signal. A relay that nobody tuned keeps the readings of the world.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 17:01:01 -04:00

162 lines
6.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The demo world: the same history at each run, and a device that agrees with it."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from pathlib import Path
from meshterm.core.channels import channel_identity
from meshterm.core.models import utcnow
from meshterm.demo import history, world
from meshterm.demo.device import DemoDevice
from meshterm.demo.home import prepare
from meshterm.persistence.repository import Repository
def _seed(path: Path, now) -> Repository: # noqa: ANN001 - a datetime
repo = Repository(path)
history.seed(repo, now)
return repo
def test_each_node_has_its_own_hash() -> None:
"""No two nodes of the world share a path hash, so each hop resolves to one node."""
hashes = [node.hash for node in (world.SELF, *world.NODES)]
assert len(hashes) == len(set(hashes))
def test_the_history_is_the_same_at_each_run(tmp_path: Path) -> None:
"""Two runs give the same rows, with the times moved by the gap between the two starts."""
now = utcnow()
first = _seed(tmp_path / "a.db", now)
second = _seed(tmp_path / "b.db", now + timedelta(hours=5))
assert first.table_counts() == second.table_counts()
assert first.kind_counts() == second.kind_counts()
public = world.CHANNELS[0]
identity = channel_identity(public.name, public.secret)
def lines(repo: Repository, start) -> list[tuple[str, int]]: # noqa: ANN001 - a datetime
rows = repo.recent_chat_messages(is_channel=True, channel_id=identity, limit=500)
return [(m.text, round((start - m.created_at).total_seconds())) for m in rows]
assert lines(first, now) == lines(second, now + timedelta(hours=5))
def test_a_channel_line_has_its_heard_copies(tmp_path: Path) -> None:
"""The packets of a channel line decrypt to the line, so its paths can be found."""
from meshterm.services.message_paths import channel_arrivals
repo = _seed(tmp_path / "h.db", utcnow())
public = world.CHANNELS[0]
identity = channel_identity(public.name, public.secret)
last = repo.recent_chat_messages(is_channel=True, channel_id=identity, limit=1)[-1]
arrivals = channel_arrivals(repo, last, channel_name=public.name, secret=public.secret)
assert arrivals
def test_the_demo_device_traces_a_path() -> None:
"""A forced path through linked repeaters comes back with a reading for each link."""
async def run(): # noqa: ANN202
device = DemoDevice()
await device.connect()
spec = ",".join(world.node(n).hash for n in ("Rosemont", "Mont-Royal", "Rosemont"))
for _ in range(10):
result = await device.run_trace("(path)", path=spec)
if result.success:
return result
return result
result = asyncio.run(run())
assert result.success
assert [h.node for h in result.hops] == ["d8", "2a", "d8", None]
def test_a_tuned_repeater_is_heard_best_at_its_best_power() -> None:
"""TX optimize finds a peak: far from its best power, a relay loses the target."""
from meshterm.demo.device import REMOTE_TX_BEST
async def heard(tx: int) -> list[float]:
device = DemoDevice()
await device.connect()
rosemont = next(c for c in await device.get_contacts() if c.name == "Rosemont")
await device.admin_login(rosemont, world.ADMIN_PASSWORD)
await device.set_remote_tx_power(rosemont, tx)
spec = ",".join(world.node(n).hash for n in ("Rosemont", "Mont-Royal", "Rosemont"))
results = [await device.run_trace("(path)", path=spec) for _ in range(5)]
return [result.hops[1].snr for result in results if result.success]
best = asyncio.run(heard(REMOTE_TX_BEST))
far = asyncio.run(heard(REMOTE_TX_BEST + 8))
assert best
assert not far or max(far) < min(best)
def test_prepare_writes_a_home(tmp_path: Path) -> None:
"""The demo home gets a database and the stores of the joined room and the alerts."""
home = tmp_path / "demo"
prepare(home, utcnow())
prepare(home, utcnow()) # a second start writes it again over the first
assert (home / "meshterm.db").exists()
assert (home / "rooms.json").exists()
assert (home / "watchtower.json").exists()
def test_each_line_is_sent_while_its_sender_transmits() -> None:
"""No node speaks before our node first heard it, or after it went quiet."""
from meshterm.demo import script
groups = (script.CHANNEL_LINES, script.DIRECT_LINES, script.ROOM_LINES)
for group in groups:
for lines in group.values():
for line in lines:
node = world.BY_NAME.get(line.sender)
if node is None or node is world.SELF:
continue # an author that no contact names
age_h = line.age_min / 60.0
assert node.quiet_hours is not None, line
assert node.quiet_hours <= age_h <= node.first_heard_days * 24.0, line
def test_our_node_uses_the_path_width_of_the_mesh() -> None:
"""Our node uses three bytes for each hop, so a trace uses two (the widest it encodes)."""
from meshterm.ui.trace_screen import collapse_trace_width
mode = asyncio.run(DemoDevice().get_path_hash_mode())
assert mode + 1 == world.path_width(world.SELF) == 3
assert collapse_trace_width(mode) == 2
def test_a_mentioned_node_answers(monkeypatch) -> None: # noqa: ANN001 - the pytest fixture
"""A channel message that mentions Dave gets his answer, which mentions our node."""
from meshterm.demo import script
monkeypatch.setattr(script, "REPLY_DELAY_S", 0.0)
async def run(): # noqa: ANN202
device = DemoDevice()
await device.connect()
messages = []
def on_event(event) -> None: # noqa: ANN001 - a MeshEvent
if event.message is not None:
messages.append(event.message)
unsubscribe = await device.subscribe_events(on_event)
await device.send_channel_message(0, "Hey, is @[Dave-Lachine] coming?")
await device.send_channel_message(0, "Hello, @[Félix]") # no answer in the script
for _ in range(100):
if messages:
break
await asyncio.sleep(0.05)
await asyncio.sleep(0.5)
unsubscribe()
return messages
messages = asyncio.run(run())
assert [m.text for m in messages] == [f"Dave-Lachine: {script.REPLIES['Dave-Lachine']}"]
assert messages[0].is_channel and messages[0].channel == 0
assert f"@[{world.SELF.name}]" in messages[0].text