mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-11 19:32:55 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ab04907be | |||
| cb98211445 | |||
| afb303af78 |
@@ -65,3 +65,4 @@ syncpi.sh
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
/data
|
/data
|
||||||
|
Installing
|
||||||
|
|||||||
@@ -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
|
"$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
|
fi
|
||||||
# ---- Install pymc_repeater from git ----
|
# ---- Install pymc_repeater from git ----
|
||||||
exec "$VENV_PIP" install \
|
if "$VENV_PIP" install \
|
||||||
--upgrade \
|
--upgrade \
|
||||||
--no-cache-dir \
|
--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
|
UPGRADEEOF
|
||||||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
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
|
"$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
|
fi
|
||||||
# ---- Install pymc_repeater from git ----
|
# ---- Install pymc_repeater from git ----
|
||||||
exec "$VENV_PIP" install \
|
if "$VENV_PIP" install \
|
||||||
--upgrade \
|
--upgrade \
|
||||||
--no-cache-dir \
|
--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
|
UPGRADEEOF
|
||||||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
||||||
echo " ✓ Permissions updated"
|
echo " ✓ Permissions updated"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class PacketRecord:
|
|||||||
origin_id=origin_id,
|
origin_id=origin_id,
|
||||||
timestamp=dt.isoformat(),
|
timestamp=dt.isoformat(),
|
||||||
type="PACKET",
|
type="PACKET",
|
||||||
direction="rx",
|
direction="tx" if bool(packet_record.get("transmitted", False)) else "rx",
|
||||||
time=dt.strftime("%H:%M:%S"),
|
time=dt.strftime("%H:%M:%S"),
|
||||||
date=dt.strftime("%-d/%-m/%Y"),
|
date=dt.strftime("%-d/%-m/%Y"),
|
||||||
len=str(len(packet_record["raw_packet"]) // 2),
|
len=str(len(packet_record["raw_packet"]) // 2),
|
||||||
|
|||||||
+9
-2
@@ -1083,8 +1083,15 @@ class RepeaterDaemon:
|
|||||||
route_type="flood",
|
route_type="flood",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send via dispatcher
|
# Route advert TX through router injection so it flows through engine/storage/MQTT.
|
||||||
await self.dispatcher.send_packet(packet, wait_for_ack=False)
|
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:
|
if self.repeater_handler:
|
||||||
self.repeater_handler.mark_seen(packet)
|
self.repeater_handler.mark_seen(packet)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from repeater.companion.constants import STATS_TYPE_CORE, STATS_TYPE_PACKETS, STATS_TYPE_RADIO
|
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:
|
class _FakeIdentity:
|
||||||
@@ -212,7 +213,7 @@ async def test_register_identity_everywhere_calls_helpers_and_respects_collision
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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())
|
daemon = RepeaterDaemon(_base_config(), radio=object())
|
||||||
|
|
||||||
# Missing dispatcher/local identity
|
# 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"}
|
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)
|
packet = SimpleNamespace(calculate_packet_hash=lambda: b"\xab" * 16)
|
||||||
with patch("pymc_core.protocol.PacketBuilder.create_advert", return_value=packet):
|
with patch("pymc_core.protocol.PacketBuilder.create_advert", return_value=packet):
|
||||||
ok = await daemon.send_advert()
|
ok = await daemon.send_advert()
|
||||||
|
|
||||||
assert ok is True
|
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.repeater_handler.mark_seen.assert_called_once_with(packet)
|
||||||
daemon.dispatcher.packet_filter.track_packet.assert_called_once()
|
daemon.dispatcher.packet_filter.track_packet.assert_called_once()
|
||||||
|
|
||||||
|
|||||||
@@ -236,3 +236,40 @@ def test_mqtt_published_packet_legacy_mqtt_format_uses_singular_packet_topic():
|
|||||||
payload_dict = json.loads(captured[0]["payload"])
|
payload_dict = json.loads(captured[0]["payload"])
|
||||||
# Duration still flows through correctly even on the legacy topic
|
# Duration still flows through correctly even on the legacy topic
|
||||||
assert int(payload_dict["duration"]) > 0
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user