mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 17:33:16 +02:00
Merge branch 'pr-128' into feat/companion
# Conflicts: # repeater/engine.py # repeater/web/api_endpoints.py
This commit is contained in:
@@ -117,6 +117,11 @@ mesh:
|
||||
# Individual transport keys can override this setting
|
||||
global_flood_allow: true
|
||||
|
||||
# Path hash mode for flood packets (0-hop): per-hop hash size in path encoding
|
||||
# 0 = 1-byte hashes (legacy), 1 = 2-byte, 2 = 3-byte. Must match mesh convention.
|
||||
# Affects originated adverts and any other flood packets sent by the repeater.
|
||||
path_hash_mode: 0
|
||||
|
||||
# Flood loop detection mode
|
||||
# off = disabled, minimal = allow up to 3 self-hashes, moderate = allow up to 1, strict = allow 0
|
||||
loop_detect: minimal
|
||||
@@ -161,11 +166,13 @@ identities:
|
||||
# node_name: "RepeaterCompanion"
|
||||
# tcp_port: 5000
|
||||
# bind_address: "0.0.0.0"
|
||||
# tcp_timeout: 120 # seconds; default 120 when omitted; 0 = disable (no timeout)
|
||||
# - name: "BotCompanion"
|
||||
# identity_key: "another_companion_identity_key_hex"
|
||||
# settings:
|
||||
# node_name: "meshcore-bot"
|
||||
# tcp_port: 5001
|
||||
# tcp_timeout: 120 # seconds; default 120 when omitted; 0 = disable (no timeout)
|
||||
|
||||
# Radio hardware type
|
||||
# Supported:
|
||||
|
||||
@@ -33,6 +33,7 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
companion_hash: str,
|
||||
port: int = 5000,
|
||||
bind_address: str = "0.0.0.0",
|
||||
client_idle_timeout_sec: Optional[int] = 120,
|
||||
sqlite_handler=None,
|
||||
local_hash: Optional[int] = None,
|
||||
stats_getter=None,
|
||||
@@ -43,6 +44,7 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
companion_hash=companion_hash,
|
||||
port=port,
|
||||
bind_address=bind_address,
|
||||
client_idle_timeout_sec=client_idle_timeout_sec,
|
||||
device_model="pyMC-Repeater-Companion",
|
||||
device_version=None, # use FIRMWARE_VER_CODE from pyMC_core
|
||||
build_date="13 Feb 2026",
|
||||
@@ -164,3 +166,13 @@ class CompanionFrameServer(_BaseFrameServer):
|
||||
self.companion_hash,
|
||||
channels,
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Persist contacts and channels before stopping (so they survive daemon restart)."""
|
||||
if self.sqlite_handler:
|
||||
try:
|
||||
await self._save_contacts()
|
||||
await self._save_channels()
|
||||
except Exception as e:
|
||||
logger.warning("Failed to persist contacts/channels on stop: %s", e)
|
||||
await super().stop()
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
_INVALID_NODE_NAME_CHARS = "\n\r\x00"
|
||||
|
||||
|
||||
def normalize_companion_identity_key(identity_key: str) -> str:
|
||||
"""Strip whitespace and remove optional 0x prefix so fromhex() is consistent across installs."""
|
||||
s = identity_key.strip()
|
||||
if s.lower().startswith("0x"):
|
||||
s = s[2:].strip()
|
||||
return s
|
||||
|
||||
|
||||
def validate_companion_node_name(value: str) -> str:
|
||||
"""Validate node_name for config sync: non-empty, max 31 bytes UTF-8, no control chars."""
|
||||
if not isinstance(value, str):
|
||||
|
||||
@@ -100,6 +100,18 @@ class ConfigManager:
|
||||
if hasattr(self.daemon.advert_helper, 'reload_config'):
|
||||
self.daemon.advert_helper.reload_config()
|
||||
logger.info("Reloaded AdvertHelper config")
|
||||
|
||||
# Re-apply dispatcher path hash mode when mesh section changed
|
||||
if 'mesh' in sections and self.daemon and hasattr(self.daemon, 'dispatcher'):
|
||||
mesh_cfg = self.daemon.config.get("mesh", {})
|
||||
path_hash_mode = mesh_cfg.get("path_hash_mode", 0)
|
||||
if path_hash_mode not in (0, 1, 2):
|
||||
logger.warning(
|
||||
f"Invalid mesh.path_hash_mode={path_hash_mode}, must be 0/1/2; using 0"
|
||||
)
|
||||
path_hash_mode = 0
|
||||
self.daemon.dispatcher.set_default_path_hash_mode(path_hash_mode)
|
||||
logger.info(f"Reloaded path hash mode: mesh.path_hash_mode={path_hash_mode}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -921,6 +921,7 @@ class RepeaterHandler(BaseHandler):
|
||||
"mesh": {
|
||||
"loop_detect": self.config.get("mesh", {}).get("loop_detect", "off"),
|
||||
"global_flood_allow": self.config.get("mesh", {}).get("global_flood_allow", True),
|
||||
"path_hash_mode": self.config.get("mesh", {}).get("path_hash_mode", 0),
|
||||
},
|
||||
},
|
||||
"public_key": None,
|
||||
|
||||
+28
-5
@@ -4,7 +4,7 @@ import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from repeater.companion.utils import validate_companion_node_name
|
||||
from repeater.companion.utils import validate_companion_node_name, normalize_companion_identity_key
|
||||
from repeater.config import get_radio_for_board, load_config, save_config
|
||||
from repeater.config_manager import ConfigManager
|
||||
from repeater.engine import RepeaterHandler
|
||||
@@ -151,6 +151,19 @@ class RepeaterDaemon:
|
||||
self.dispatcher.register_fallback_handler(self._router_callback)
|
||||
logger.info("Packet router registered as fallback (catches all packets)")
|
||||
|
||||
# Set default path hash mode for flood 0-hop packets (adverts, etc.)
|
||||
path_hash_mode = self.config.get("mesh", {}).get("path_hash_mode", 0)
|
||||
if path_hash_mode not in (0, 1, 2):
|
||||
logger.warning(
|
||||
f"Invalid mesh.path_hash_mode={path_hash_mode}, must be 0/1/2; using 0"
|
||||
)
|
||||
path_hash_mode = 0
|
||||
self.dispatcher.set_default_path_hash_mode(path_hash_mode)
|
||||
mode_names = {0: "1-byte", 1: "2-byte", 2: "3-byte"}
|
||||
logger.info(
|
||||
f"Path hash mode set to {mode_names[path_hash_mode]} (mesh.path_hash_mode={path_hash_mode})"
|
||||
)
|
||||
|
||||
# Create processing helpers (handlers created internally)
|
||||
self.trace_helper = TraceHelper(
|
||||
local_hash=self.local_hash,
|
||||
@@ -372,6 +385,10 @@ class RepeaterDaemon:
|
||||
sqlite_handler = None
|
||||
if self.repeater_handler and self.repeater_handler.storage:
|
||||
sqlite_handler = self.repeater_handler.storage.sqlite_handler
|
||||
if not sqlite_handler and companions_config:
|
||||
logger.warning(
|
||||
"Companion persistence disabled: no storage (contacts/channels will not survive restart or disconnect)"
|
||||
)
|
||||
|
||||
radio_config = (
|
||||
self.repeater_handler.radio_config
|
||||
@@ -391,7 +408,7 @@ class RepeaterDaemon:
|
||||
|
||||
if isinstance(identity_key, str):
|
||||
try:
|
||||
identity_key_bytes = bytes.fromhex(identity_key)
|
||||
identity_key_bytes = bytes.fromhex(normalize_companion_identity_key(identity_key))
|
||||
except ValueError as e:
|
||||
logger.error(f"Companion '{name}' identity_key invalid hex: {e}")
|
||||
continue
|
||||
@@ -415,6 +432,8 @@ class RepeaterDaemon:
|
||||
node_name = settings.get("node_name", name)
|
||||
tcp_port = settings.get("tcp_port", 5000)
|
||||
bind_address = settings.get("bind_address", "0.0.0.0")
|
||||
tcp_timeout_raw = settings.get("tcp_timeout", 120)
|
||||
client_idle_timeout_sec = None if tcp_timeout_raw == 0 else int(tcp_timeout_raw)
|
||||
|
||||
def _make_sync_node_name_to_config(companion_name: str):
|
||||
"""Return a callback that syncs node_name to config for this companion (binds name at creation)."""
|
||||
@@ -508,6 +527,7 @@ class RepeaterDaemon:
|
||||
companion_hash=companion_hash_str,
|
||||
port=tcp_port,
|
||||
bind_address=bind_address,
|
||||
client_idle_timeout_sec=client_idle_timeout_sec,
|
||||
sqlite_handler=sqlite_handler,
|
||||
local_hash=self.local_hash,
|
||||
stats_getter=self._get_companion_stats,
|
||||
@@ -527,7 +547,7 @@ class RepeaterDaemon:
|
||||
|
||||
logger.info(
|
||||
f"Loaded companion '{name}': hash=0x{companion_hash:02x}, "
|
||||
f"port={tcp_port}, bind={bind_address}"
|
||||
f"port={tcp_port}, bind={bind_address}, client_idle_timeout_sec={client_idle_timeout_sec}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -554,7 +574,7 @@ class RepeaterDaemon:
|
||||
|
||||
if isinstance(identity_key, str):
|
||||
try:
|
||||
identity_key_bytes = bytes.fromhex(identity_key)
|
||||
identity_key_bytes = bytes.fromhex(normalize_companion_identity_key(identity_key))
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Companion '{name}' identity_key invalid hex: {e}") from e
|
||||
elif isinstance(identity_key, bytes):
|
||||
@@ -592,6 +612,8 @@ class RepeaterDaemon:
|
||||
node_name = settings.get("node_name", name)
|
||||
tcp_port = settings.get("tcp_port", 5000)
|
||||
bind_address = settings.get("bind_address", "0.0.0.0")
|
||||
tcp_timeout_raw = settings.get("tcp_timeout", 120)
|
||||
client_idle_timeout_sec = None if tcp_timeout_raw == 0 else int(tcp_timeout_raw)
|
||||
|
||||
bridge = RepeaterCompanionBridge(
|
||||
identity=identity,
|
||||
@@ -658,6 +680,7 @@ class RepeaterDaemon:
|
||||
companion_hash=companion_hash_str,
|
||||
port=tcp_port,
|
||||
bind_address=bind_address,
|
||||
client_idle_timeout_sec=client_idle_timeout_sec,
|
||||
sqlite_handler=sqlite_handler,
|
||||
local_hash=self.local_hash,
|
||||
stats_getter=self._get_companion_stats,
|
||||
@@ -677,7 +700,7 @@ class RepeaterDaemon:
|
||||
|
||||
logger.info(
|
||||
f"Hot-reload: Loaded companion '{name}': hash=0x{companion_hash:02x}, "
|
||||
f"port={tcp_port}, bind={bind_address}"
|
||||
f"port={tcp_port}, bind={bind_address}, client_idle_timeout_sec={client_idle_timeout_sec}"
|
||||
)
|
||||
|
||||
async def _on_raw_rx_for_companions(self, data: bytes, rssi: int, snr: float) -> None:
|
||||
|
||||
@@ -1470,6 +1470,8 @@ class APIEndpoints:
|
||||
self.config["delays"] = {}
|
||||
if "repeater" not in self.config:
|
||||
self.config["repeater"] = {}
|
||||
if "mesh" not in self.config:
|
||||
self.config["mesh"] = {}
|
||||
|
||||
# Update TX power (up to 30 dBm for high-power radios)
|
||||
if "tx_power" in data:
|
||||
@@ -1587,6 +1589,14 @@ class APIEndpoints:
|
||||
self.config["repeater"]["advert_interval_minutes"] = mins
|
||||
applied.append(f"advert.interval={mins}m")
|
||||
|
||||
# Update path hash mode (mesh: 0=1-byte, 1=2-byte, 2=3-byte)
|
||||
if "path_hash_mode" in data:
|
||||
phm = int(data["path_hash_mode"])
|
||||
if phm not in (0, 1, 2):
|
||||
return self._error("Path hash mode must be 0 (1-byte), 1 (2-byte), or 2 (3-byte)")
|
||||
self.config["mesh"]["path_hash_mode"] = phm
|
||||
applied.append(f"path_hash_mode={phm}")
|
||||
|
||||
# KISS modem settings (only when radio_type is kiss)
|
||||
if "kiss_port" in data or "kiss_baud_rate" in data:
|
||||
if self.config.get("radio_type") != "kiss":
|
||||
@@ -1613,7 +1623,9 @@ class APIEndpoints:
|
||||
if not applied:
|
||||
return self._error("No valid settings provided")
|
||||
|
||||
live_sections = ["repeater", "delays", "radio", "mesh"]
|
||||
live_sections = ["repeater", "delays", "radio"]
|
||||
if "mesh" in self.config and any(k in data for k in ("path_hash_mode", "loop_detect")):
|
||||
live_sections.append("mesh")
|
||||
if "kiss" in self.config:
|
||||
live_sections.append("kiss")
|
||||
# Save to config file and live update daemon in one operation
|
||||
@@ -2411,6 +2423,8 @@ class APIEndpoints:
|
||||
"tcp_port": settings.get("tcp_port", 5000),
|
||||
"bind_address": settings.get("bind_address", "0.0.0.0"),
|
||||
}
|
||||
if "tcp_timeout" in settings:
|
||||
comp_settings["tcp_timeout"] = settings["tcp_timeout"]
|
||||
new_identity = {
|
||||
"name": name,
|
||||
"identity_key": identity_key,
|
||||
@@ -2609,7 +2623,7 @@ class APIEndpoints:
|
||||
identity["settings"] = {}
|
||||
# Only allow companion settings
|
||||
for k, v in data["settings"].items():
|
||||
if k in ("node_name", "tcp_port", "bind_address"):
|
||||
if k in ("node_name", "tcp_port", "bind_address", "tcp_timeout"):
|
||||
identity["settings"][k] = v
|
||||
|
||||
companions[identity_index] = identity
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{a as p,b as n,g as m,e as t,s as g,t as s,j as d,p as l}from"./index-DyUIpN7m.js";const f={class:"flex items-center justify-between mb-4"},w={class:"text-xl font-semibold text-content-primary dark:text-content-primary"},v={class:"mb-6"},h={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},y={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},C={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},B={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},j={class:"flex gap-3"},_=p({__name:"ConfirmDialog",props:{show:{type:Boolean},title:{default:"Confirm Action"},message:{},confirmText:{default:"Confirm"},cancelText:{default:"Cancel"},variant:{default:"warning"}},emits:["close","confirm"],setup(c,{emit:b}){const o=c,r=b,u=i=>{i.target===i.currentTarget&&r("close")},k={danger:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",warning:"bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},x={danger:"bg-red-500 hover:bg-red-600",warning:"bg-yellow-500 hover:bg-yellow-600",info:"bg-blue-500 hover:bg-blue-600"};return(i,e)=>o.show?(l(),n("div",{key:0,onClick:u,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[t("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[3]||(e[3]=g(()=>{},["stop"]))},[t("div",f,[t("h3",w,s(o.title),1),t("button",{onClick:e[0]||(e[0]=a=>r("close")),class:"text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors"},e[4]||(e[4]=[t("svg",{class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)]))]),t("div",v,[t("div",{class:d(["inline-flex p-3 rounded-xl mb-4",k[o.variant]])},[o.variant==="danger"?(l(),n("svg",h,e[5]||(e[5]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):o.variant==="warning"?(l(),n("svg",y,e[6]||(e[6]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):(l(),n("svg",C,e[7]||(e[7]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),t("p",B,s(o.message),1)]),t("div",j,[t("button",{onClick:e[1]||(e[1]=a=>r("close")),class:"flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10"},s(o.cancelText),1),t("button",{onClick:e[2]||(e[2]=a=>r("confirm")),class:d(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",x[o.variant]])},s(o.confirmText),3)])])])):m("",!0)}});export{_};
|
||||
import{a as p,b as n,g as m,e as t,s as g,t as s,j as d,p as l}from"./index-BABkwxNn.js";const f={class:"flex items-center justify-between mb-4"},w={class:"text-xl font-semibold text-content-primary dark:text-content-primary"},v={class:"mb-6"},h={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},y={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},C={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},B={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},j={class:"flex gap-3"},_=p({__name:"ConfirmDialog",props:{show:{type:Boolean},title:{default:"Confirm Action"},message:{},confirmText:{default:"Confirm"},cancelText:{default:"Cancel"},variant:{default:"warning"}},emits:["close","confirm"],setup(c,{emit:b}){const o=c,r=b,u=i=>{i.target===i.currentTarget&&r("close")},k={danger:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",warning:"bg-yellow-100 dark:bg-yellow-500/20 border-yellow-500/30 text-yellow-600 dark:text-yellow-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},x={danger:"bg-red-500 hover:bg-red-600",warning:"bg-yellow-500 hover:bg-yellow-600",info:"bg-blue-500 hover:bg-blue-600"};return(i,e)=>o.show?(l(),n("div",{key:0,onClick:u,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[t("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[3]||(e[3]=g(()=>{},["stop"]))},[t("div",f,[t("h3",w,s(o.title),1),t("button",{onClick:e[0]||(e[0]=a=>r("close")),class:"text-content-secondary dark:text-content-muted hover:text-content-primary dark:hover:text-content-primary transition-colors"},e[4]||(e[4]=[t("svg",{class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)]))]),t("div",v,[t("div",{class:d(["inline-flex p-3 rounded-xl mb-4",k[o.variant]])},[o.variant==="danger"?(l(),n("svg",h,e[5]||(e[5]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):o.variant==="warning"?(l(),n("svg",y,e[6]||(e[6]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"},null,-1)]))):(l(),n("svg",C,e[7]||(e[7]=[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),t("p",B,s(o.message),1)]),t("div",j,[t("button",{onClick:e[1]||(e[1]=a=>r("close")),class:"flex-1 px-4 py-3 rounded-xl bg-background-mute dark:bg-white/5 hover:bg-stroke-subtle dark:hover:bg-white/10 text-content-primary dark:text-content-primary transition-all duration-200 border border-stroke-subtle dark:border-stroke/10"},s(o.cancelText),1),t("button",{onClick:e[2]||(e[2]=a=>r("confirm")),class:d(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",x[o.variant]])},s(o.confirmText),3)])])])):m("",!0)}});export{_};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{a as e,b as r,i as o,p as n}from"./index-DyUIpN7m.js";const d=e({name:"HelpView",__name:"Help",setup(a){return(i,t)=>(n(),r("div",null,t[0]||(t[0]=[o('<div class="glass-card backdrop-blur border border-stroke-subtle dark:border-white/10 rounded-[15px] p-8"><h1 class="text-content-primary dark:text-content-primary text-2xl font-semibold mb-6">Help & Documentation</h1><div class="text-center py-12"><div class="text-primary mb-6"><svg class="w-20 h-20 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg></div><h2 class="text-content-primary dark:text-content-primary text-xl font-medium mb-3">pyMC Repeater Wiki</h2><p class="text-content-secondary dark:text-content-muted mb-8 max-w-md mx-auto"> Access documentation, setup guides, troubleshooting tips, and community resources on our official wiki. </p><a href="https://github.com/rightup/pyMC_Repeater/wiki" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 bg-primary hover:bg-primary/80 text-white dark:text-background font-medium py-3 px-6 rounded-xl transition-colors duration-200"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg> Visit Wiki Documentation </a><div class="mt-8 text-xs text-content-muted dark:text-content-muted"> Opens in a new tab </div></div></div>',1)])))}});export{d as default};
|
||||
import{a as e,b as r,i as o,p as n}from"./index-BABkwxNn.js";const d=e({name:"HelpView",__name:"Help",setup(a){return(i,t)=>(n(),r("div",null,t[0]||(t[0]=[o('<div class="glass-card backdrop-blur border border-stroke-subtle dark:border-white/10 rounded-[15px] p-8"><h1 class="text-content-primary dark:text-content-primary text-2xl font-semibold mb-6">Help & Documentation</h1><div class="text-center py-12"><div class="text-primary mb-6"><svg class="w-20 h-20 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg></div><h2 class="text-content-primary dark:text-content-primary text-xl font-medium mb-3">pyMC Repeater Wiki</h2><p class="text-content-secondary dark:text-content-muted mb-8 max-w-md mx-auto"> Access documentation, setup guides, troubleshooting tips, and community resources on our official wiki. </p><a href="https://github.com/rightup/pyMC_Repeater/wiki" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 bg-primary hover:bg-primary/80 text-white dark:text-background font-medium py-3 px-6 rounded-xl transition-colors duration-200"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg> Visit Wiki Documentation </a><div class="mt-8 text-xs text-content-muted dark:text-content-muted"> Opens in a new tab </div></div></div>',1)])))}});export{d as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{a as k,b as o,g,e as r,j as a,t as p,s as x,p as s}from"./index-DyUIpN7m.js";const f={class:"mb-6"},m={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},v={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},h={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},w={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},C={class:"flex"},B=k({__name:"MessageDialog",props:{show:{type:Boolean},message:{},variant:{default:"success"}},emits:["close"],setup(i,{emit:d}){const t=i,l=d,c=n=>{n.target===n.currentTarget&&l("close")},b={success:"bg-green-100 dark:bg-green-500/20 border-green-600/40 dark:border-green-500/30 text-green-600 dark:text-green-400",error:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},u={success:"bg-green-500 hover:bg-green-600",error:"bg-red-500 hover:bg-red-600",info:"bg-blue-500 hover:bg-blue-600"};return(n,e)=>t.show?(s(),o("div",{key:0,onClick:c,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[r("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[1]||(e[1]=x(()=>{},["stop"]))},[r("div",f,[r("div",{class:a(["inline-flex p-3 rounded-xl mb-4",b[t.variant]])},[t.variant==="success"?(s(),o("svg",m,e[2]||(e[2]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"},null,-1)]))):t.variant==="error"?(s(),o("svg",v,e[3]||(e[3]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"},null,-1)]))):(s(),o("svg",h,e[4]||(e[4]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),r("p",w,p(t.message),1)]),r("div",C,[r("button",{onClick:e[0]||(e[0]=y=>l("close")),class:a(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",u[t.variant]])}," OK ",2)])])])):g("",!0)}});export{B as _};
|
||||
import{a as k,b as o,g,e as r,j as a,t as p,s as x,p as s}from"./index-BABkwxNn.js";const f={class:"mb-6"},m={key:0,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},v={key:1,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},h={key:2,class:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},w={class:"text-content-secondary dark:text-content-primary/80 text-base leading-relaxed"},C={class:"flex"},B=k({__name:"MessageDialog",props:{show:{type:Boolean},message:{},variant:{default:"success"}},emits:["close"],setup(i,{emit:d}){const t=i,l=d,c=n=>{n.target===n.currentTarget&&l("close")},b={success:"bg-green-100 dark:bg-green-500/20 border-green-600/40 dark:border-green-500/30 text-green-600 dark:text-green-400",error:"bg-red-100 dark:bg-red-500/20 border-red-500/30 text-red-600 dark:text-red-400",info:"bg-blue-500/20 border-blue-500/30 text-blue-600 dark:text-blue-400"},u={success:"bg-green-500 hover:bg-green-600",error:"bg-red-500 hover:bg-red-600",info:"bg-blue-500 hover:bg-blue-600"};return(n,e)=>t.show?(s(),o("div",{key:0,onClick:c,class:"fixed inset-0 bg-black/40 backdrop-blur-lg z-[99999] flex items-center justify-center p-4",style:{"backdrop-filter":"blur(8px) saturate(180%)",position:"fixed",top:"0",left:"0",right:"0",bottom:"0"}},[r("div",{class:"bg-white dark:bg-surface-elevated backdrop-blur-xl rounded-[20px] p-6 w-full max-w-md border border-stroke-subtle dark:border-white/10",onClick:e[1]||(e[1]=x(()=>{},["stop"]))},[r("div",f,[r("div",{class:a(["inline-flex p-3 rounded-xl mb-4",b[t.variant]])},[t.variant==="success"?(s(),o("svg",m,e[2]||(e[2]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"},null,-1)]))):t.variant==="error"?(s(),o("svg",v,e[3]||(e[3]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"},null,-1)]))):(s(),o("svg",h,e[4]||(e[4]=[r("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"},null,-1)])))],2),r("p",w,p(t.message),1)]),r("div",C,[r("button",{onClick:e[0]||(e[0]=y=>l("close")),class:a(["flex-1 px-4 py-3 rounded-xl text-white transition-all duration-200",u[t.variant]])}," OK ",2)])])])):g("",!0)}});export{B as _};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{L as J,a as zl,r as ut,o as Hl,$ as Ul,P as Rn,D as ql,b as tt,e as Z,g as Yt,t as Is,w as Kl,v as Vl,X as Ji,j as Tn,s as jl,p as it,x as Gl}from"./index-DyUIpN7m.js";/**
|
||||
import{L as J,a as zl,r as ut,o as Hl,$ as Ul,P as Rn,D as ql,b as tt,e as Z,g as Yt,t as Is,w as Kl,v as Vl,X as Ji,j as Tn,s as jl,p as it,x as Gl}from"./index-BABkwxNn.js";/**
|
||||
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{M as x,c as s}from"./index-DyUIpN7m.js";const l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},d=-116,i=8,u=5;function y(t,e){return t-e}function S(t){return l[t]??l[i]}function f(t,e){const r=e+u;if(t<=e){const o=t<=e-5?0:1;return{bars:o,color:"text-red-600 dark:text-red-400",snr:t,quality:o===0?"none":"poor"}}if(t<r){const n=(t-e)/u<.5?2:3;return{bars:n,color:n===2?"text-orange-600 dark:text-orange-400":"text-yellow-600 dark:text-yellow-400",snr:t,quality:"fair"}}const a=t-r>=10?5:4;return{bars:a,color:a===5?"text-green-600 dark:text-green-400":"text-green-600 dark:text-green-300",snr:t,quality:a===5?"excellent":"good"}}function N(){const t=x(),e=s(()=>t.noiseFloorDbm??d),r=s(()=>t.stats?.config?.radio?.spreading_factor??i),c=s(()=>S(r.value));return{getSignalQuality:o=>{if(!o||o>0||o<-120)return{bars:0,color:"text-gray-400 dark:text-gray-500",snr:-999,quality:"none"};const n=y(o,e.value),g=Math.max(-30,Math.min(20,n));return f(g,c.value)},noiseFloor:e,spreadingFactor:r,minSNR:c}}export{N as u};
|
||||
import{M as x,c as s}from"./index-BABkwxNn.js";const l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},d=-116,i=8,u=5;function y(t,e){return t-e}function S(t){return l[t]??l[i]}function f(t,e){const r=e+u;if(t<=e){const o=t<=e-5?0:1;return{bars:o,color:"text-red-600 dark:text-red-400",snr:t,quality:o===0?"none":"poor"}}if(t<r){const n=(t-e)/u<.5?2:3;return{bars:n,color:n===2?"text-orange-600 dark:text-orange-400":"text-yellow-600 dark:text-yellow-400",snr:t,quality:"fair"}}const a=t-r>=10?5:4;return{bars:a,color:a===5?"text-green-600 dark:text-green-400":"text-green-600 dark:text-green-300",snr:t,quality:a===5?"excellent":"good"}}function N(){const t=x(),e=s(()=>t.noiseFloorDbm??d),r=s(()=>t.stats?.config?.radio?.spreading_factor??i),c=s(()=>S(r.value));return{getSignalQuality:o=>{if(!o||o>0||o<-120)return{bars:0,color:"text-gray-400 dark:text-gray-500",snr:-999,quality:"none"};const n=y(o,e.value),g=Math.max(-30,Math.min(20,n));return f(g,c.value)},noiseFloor:e,spreadingFactor:r,minSNR:c}}export{N as u};
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-DyUIpN7m.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BABkwxNn.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D-3p9FIW.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Tests for path hash mode on repeater-originated adverts (multi-byte path support).
|
||||
|
||||
When mesh.path_hash_mode is 1 or 2, flood 0-hop packets (e.g. adverts) sent via
|
||||
dispatcher.send_packet() must have path_len encoding set so get_path_hash_size()
|
||||
returns 2 or 3. The dispatcher applies this in send_packet() before transmit.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from pymc_core.node.dispatcher import Dispatcher
|
||||
from pymc_core.protocol import Packet
|
||||
from pymc_core.protocol.constants import (
|
||||
PAYLOAD_TYPE_ADVERT,
|
||||
PH_TYPE_SHIFT,
|
||||
ROUTE_TYPE_FLOOD,
|
||||
)
|
||||
|
||||
|
||||
class MockRadio:
|
||||
"""Minimal mock radio: send() stores data and returns True."""
|
||||
|
||||
def __init__(self):
|
||||
self.tx_data = None
|
||||
|
||||
async def send(self, data: bytes) -> bool:
|
||||
self.tx_data = data
|
||||
return True
|
||||
|
||||
def set_rx_callback(self, callback):
|
||||
pass
|
||||
|
||||
|
||||
def _make_advert_packet():
|
||||
"""Build a 0-hop flood ADVERT packet (same shape as PacketBuilder.create_advert)."""
|
||||
pkt = Packet()
|
||||
# Version 1, ROUTE_TYPE_FLOOD, PAYLOAD_TYPE_ADVERT
|
||||
pkt.header = (1 << 6) | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD
|
||||
pkt.path_len = 0
|
||||
pkt.path = bytearray()
|
||||
pkt.payload = bytearray(b"minimal_advert_payload")
|
||||
pkt.payload_len = len(pkt.payload)
|
||||
return pkt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dispatcher():
|
||||
radio = MockRadio()
|
||||
return Dispatcher(radio=radio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_hash_mode_1_sets_2_byte_encoding(dispatcher):
|
||||
"""With path_hash_mode=1, sent advert packet has get_path_hash_size() == 2."""
|
||||
dispatcher.set_default_path_hash_mode(1)
|
||||
packet = _make_advert_packet()
|
||||
await dispatcher.send_packet(packet, wait_for_ack=False)
|
||||
assert packet.get_path_hash_size() == 2
|
||||
assert packet.get_path_hash_count() == 0
|
||||
assert dispatcher.radio.tx_data is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_hash_mode_2_sets_3_byte_encoding(dispatcher):
|
||||
"""With path_hash_mode=2, sent advert packet has get_path_hash_size() == 3."""
|
||||
dispatcher.set_default_path_hash_mode(2)
|
||||
packet = _make_advert_packet()
|
||||
await dispatcher.send_packet(packet, wait_for_ack=False)
|
||||
assert packet.get_path_hash_size() == 3
|
||||
assert packet.get_path_hash_count() == 0
|
||||
assert dispatcher.radio.tx_data is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_hash_mode_0_leaves_1_byte_encoding(dispatcher):
|
||||
"""With path_hash_mode=0 (default), path_len stays 0 (1-byte hash size)."""
|
||||
dispatcher.set_default_path_hash_mode(0)
|
||||
packet = _make_advert_packet()
|
||||
await dispatcher.send_packet(packet, wait_for_ack=False)
|
||||
assert packet.get_path_hash_size() == 1
|
||||
assert packet.path_len == 0
|
||||
Reference in New Issue
Block a user