3 Commits

6 changed files with 81 additions and 10 deletions
+1
View File
@@ -65,3 +65,4 @@ syncpi.sh
# Docker
/data
Installing
+22 -4
View File
@@ -503,10 +503,19 @@ if [ -n "$ARCH_TAG" ]; then
"$VENV_PIP" install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
fi
# ---- Install pymc_repeater from git ----
exec "$VENV_PIP" install \
if "$VENV_PIP" install \
--upgrade \
--no-cache-dir \
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"; then
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/pyMC_Repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/pymc_repeater"
mkdir -p "$RADIO_STORAGE_DIR"
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
else
exit 1
fi
UPGRADEEOF
chmod 0755 /usr/local/bin/pymc-do-upgrade
@@ -907,10 +916,19 @@ python3 -m pip uninstall -y pymc_core 2>/dev/null || true
"$VENV_PIP" install --find-links "${WHEEL_BASE}/index.html" --no-cache-dir "pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" 2>/dev/null || true
fi
# ---- Install pymc_repeater from git ----
exec "$VENV_PIP" install \
if "$VENV_PIP" install \
--upgrade \
--no-cache-dir \
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"; then
# Keep web/OTA updates aligned with manage.sh install/upgrade defaults.
RADIO_BASE_URL="https://raw.githubusercontent.com/rightup/pyMC_Repeater/${CHANNEL}"
RADIO_STORAGE_DIR="/var/lib/pymc_repeater"
mkdir -p "$RADIO_STORAGE_DIR"
wget -qO "$RADIO_STORAGE_DIR/radio-settings.json" "${RADIO_BASE_URL}/radio-settings.json" 2>/dev/null || true
wget -qO "$RADIO_STORAGE_DIR/radio-presets.json" "${RADIO_BASE_URL}/radio-presets.json" 2>/dev/null || true
else
exit 1
fi
UPGRADEEOF
chmod 0755 /usr/local/bin/pymc-do-upgrade
echo " ✓ Permissions updated"
+1 -1
View File
@@ -70,7 +70,7 @@ class PacketRecord:
origin_id=origin_id,
timestamp=dt.isoformat(),
type="PACKET",
direction="rx",
direction="tx" if bool(packet_record.get("transmitted", False)) else "rx",
time=dt.strftime("%H:%M:%S"),
date=dt.strftime("%-d/%-m/%Y"),
len=str(len(packet_record["raw_packet"]) // 2),
+9 -2
View File
@@ -1083,8 +1083,15 @@ class RepeaterDaemon:
route_type="flood",
)
# Send via dispatcher
await self.dispatcher.send_packet(packet, wait_for_ack=False)
# Route advert TX through router injection so it flows through engine/storage/MQTT.
if not self.router:
logger.error("Cannot send advert: router not initialized")
return False
send_ok = await self.router.inject_packet(packet, wait_for_ack=False)
if not send_ok:
logger.error("Failed to inject advert packet for TX")
return False
if self.repeater_handler:
self.repeater_handler.mark_seen(packet)
+11 -3
View File
@@ -4,7 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from repeater.companion.constants import STATS_TYPE_CORE, STATS_TYPE_PACKETS, STATS_TYPE_RADIO
from repeater.main import RepeaterDaemon, main as repeater_main
from repeater.main import RepeaterDaemon
from repeater.main import main as repeater_main
class _FakeIdentity:
@@ -212,7 +213,7 @@ async def test_register_identity_everywhere_calls_helpers_and_respects_collision
@pytest.mark.asyncio
async def test_send_advert_branches_and_success_path():
async def test_send_advert_branches_and_success_paths():
daemon = RepeaterDaemon(_base_config(), radio=object())
# Missing dispatcher/local identity
@@ -231,12 +232,19 @@ async def test_send_advert_branches_and_success_path():
get_repeater_location=lambda: {"latitude": 9.1, "longitude": 8.2, "source": "gps"}
)
# Router is now required for advert TX processing path.
daemon.router = None
assert await daemon.send_advert() is False
daemon.router = SimpleNamespace(inject_packet=AsyncMock(return_value=True))
packet = SimpleNamespace(calculate_packet_hash=lambda: b"\xab" * 16)
with patch("pymc_core.protocol.PacketBuilder.create_advert", return_value=packet):
ok = await daemon.send_advert()
assert ok is True
daemon.dispatcher.send_packet.assert_awaited_once_with(packet, wait_for_ack=False)
daemon.router.inject_packet.assert_awaited_once_with(packet, wait_for_ack=False)
daemon.dispatcher.send_packet.assert_not_called()
daemon.repeater_handler.mark_seen.assert_called_once_with(packet)
daemon.dispatcher.packet_filter.track_packet.assert_called_once()
+37
View File
@@ -236,3 +236,40 @@ def test_mqtt_published_packet_legacy_mqtt_format_uses_singular_packet_topic():
payload_dict = json.loads(captured[0]["payload"])
# Duration still flows through correctly even on the legacy topic
assert int(payload_dict["duration"]) > 0
def test_mqtt_published_packet_direction_uses_transmitted_flag_for_tx():
"""When engine marks packet_record.transmitted=True, published direction must be tx."""
config = _make_config(format_value="letsmesh", iata_code="LAX")
public_key_hex = "11" * 32
identity = _FakeIdentity(public_key_hex)
pusher = MeshCoreToMqttPusher(local_identity=identity, config=config)
captured = _attach_capturing_client(pusher.connections[0])
raw_bytes = bytes(range(12))
airtime_mgr = AirtimeManager(config)
packet_record = {
"timestamp": 1700000000.0,
"type": 8,
"route": 1,
"rssi": -88,
"snr": 6.0,
"score": 0.4,
"payload_length": 6,
"packet_hash": "FACEFEED" + "00" * 4,
"raw_packet": raw_bytes.hex(),
"airtime_ms": airtime_mgr.calculate_airtime(len(raw_bytes)),
"transmitted": True,
}
record = PacketRecord.from_packet_record(
packet_record, origin="test-node", origin_id=public_key_hex.upper()
)
assert record is not None
pusher.publish_packet(record.to_dict())
assert len(captured) == 1
payload_dict = json.loads(captured[0]["payload"])
assert payload_dict["direction"] == "tx"