diff --git a/MeshStation.py b/MeshStation.py index 478a502..c667bc8 100644 --- a/MeshStation.py +++ b/MeshStation.py @@ -234,6 +234,33 @@ def _fetch_latest_github_release(timeout_sec: float = 10.0) -> dict | None: print(f"Error fetching latest release: {e}") return None +# --- MeshCore Constants --- +MESHCORE_PRESETS = { + "USA_CANADA": { + "RECOMMENDED": {"freq": 910.525, "sf": 7, "bw": 62.5, "cr": 5, "sync": 0x12, "preamble": 16, "description_key": "meshcore.preset.recommended"}, + "LEGACY_WIDE": {"freq": 915.8, "sf": 10, "bw": 250.0, "cr": 5, "sync": 0x12, "preamble": 16, "description_key": "meshcore.preset.legacy_wide"}, + }, +} + +PAYLOAD_TYPE_REQ = 0x00 +PAYLOAD_TYPE_RESPONSE = 0x01 +PAYLOAD_TYPE_TXT_MSG = 0x02 +PAYLOAD_TYPE_ACK = 0x03 +PAYLOAD_TYPE_ADVERT = 0x04 +PAYLOAD_TYPE_GRP_TXT = 0x05 +PAYLOAD_TYPE_GRP_DATA = 0x06 +PAYLOAD_TYPE_ANON_REQ = 0x07 +PAYLOAD_TYPE_PATH = 0x08 +PAYLOAD_TYPE_TRACE = 0x09 +PAYLOAD_TYPE_MULTIPART = 0x0A +PAYLOAD_TYPE_CONTROL = 0x0B +PAYLOAD_TYPE_RAW_CUSTOM = 0x0F + +ROUTE_TYPE_TRANSPORT_FLOOD = 0x00 +ROUTE_TYPE_FLOOD = 0x01 +ROUTE_TYPE_DIRECT = 0x02 +ROUTE_TYPE_TRANSPORT_DIRECT = 0x03 + # --- Meshtastic Region Definitions --- # Fields: freq_start (MHz), freq_end (MHz), dutycycle, spacing (MHz), power_limit (dBm), wide_lora, name MESHTASTIC_REGIONS = { @@ -924,6 +951,7 @@ def score_regions(key: str, s: dict) -> int: class AppState: def __init__(self): + self.network_mode = "meshtastic" # "meshtastic" | "meshcore" self.connect_mode = None # None | "direct" | "external" self.engine_proc = None self.last_rx_ts = 0.0 @@ -943,6 +971,16 @@ def __init__(self): self.direct_port = "20002" self.direct_key_b64 = "AQ==" + # MeshCore Settings + self.meshcore_region = "USA_CANADA" + self.meshcore_preset = "RECOMMENDED" + self.meshcore_custom_freq = 910.525 + self.meshcore_custom_sf = 7 + self.meshcore_custom_bw = 62.5 + self.meshcore_custom_cr = 5 + self.meshcore_custom_sync_word = 0x12 + self.meshcore_custom_preamble_len = 16 + self.external_ip = "127.0.0.1" self.external_port = "20002" self.external_key_b64 = "AQ==" @@ -1763,6 +1801,24 @@ def load_user_config(): if isinstance(v, list): s.channels_order = [x for x in v if isinstance(x, str)] + v = data.get("network_mode") + if v in ("meshtastic", "meshcore"): + s.network_mode = v + v = data.get("meshcore_region") + if isinstance(v, str): + s.meshcore_region = v + v = data.get("meshcore_preset") + if isinstance(v, str): + s.meshcore_preset = v + for field in ["meshcore_custom_freq", "meshcore_custom_sf", "meshcore_custom_bw", + "meshcore_custom_cr", "meshcore_custom_sync_word", "meshcore_custom_preamble_len"]: + v = data.get(field) + if v is not None: + try: + setattr(s, field, type(getattr(s, field))(v)) + except Exception: + pass + def save_user_config(): try: path = get_config_path() @@ -1789,6 +1845,15 @@ def save_user_config(): "map_zoom": getattr(state, "map_zoom", None), "extra_channels": getattr(state, 'extra_channels', []), "channels_order": getattr(state, 'channels_order', []), + "network_mode": state.network_mode, + "meshcore_region": state.meshcore_region, + "meshcore_preset": state.meshcore_preset, + "meshcore_custom_freq": state.meshcore_custom_freq, + "meshcore_custom_sf": state.meshcore_custom_sf, + "meshcore_custom_bw": state.meshcore_custom_bw, + "meshcore_custom_cr": state.meshcore_custom_cr, + "meshcore_custom_sync_word": state.meshcore_custom_sync_word, + "meshcore_custom_preamble_len": state.meshcore_custom_preamble_len, } with open(path, "w") as f: json.dump(data, f, indent=2) @@ -2156,6 +2221,142 @@ def update_node(node_id, **kwargs): if changed and ('short_name' in kwargs or 'long_name' in kwargs): state.chat_force_refresh = True +def resolveMeshCoreIdentity(node_hash): + # node_hash is 1 byte int + target_hex = f"{node_hash:02x}" + for nid, n in state.nodes.items(): + if nid.startswith(target_hex): + return n.get('long_name', nid), nid + return f"Node {target_hex}", target_hex + +def decodeMeshCore(packet_bytes, snr=None, rssi=None): + if not packet_bytes or len(packet_bytes) < 2: + return None + + header = packet_bytes[0] + # version = (header >> 6) & 0x03 + payload_type = (header >> 2) & 0x0F + route_type = header & 0x03 + + offset = 1 + if route_type in (ROUTE_TYPE_TRANSPORT_FLOOD, ROUTE_TYPE_TRANSPORT_DIRECT): + if len(packet_bytes) < offset + 4: return None + # transport_codes = packet_bytes[offset:offset+4] + offset += 4 + + if len(packet_bytes) < offset + 1: return None + path_len_byte = packet_bytes[offset] + offset += 1 + + hop_count = path_len_byte & 0x3F + hash_size = ((path_len_byte >> 6) & 0x03) + 1 + + path_bytes_len = hop_count * hash_size + if len(packet_bytes) < offset + path_bytes_len: return None + path_data = packet_bytes[offset:offset+path_bytes_len] + offset += path_bytes_len + + payload = packet_bytes[offset:] + + # Process Payloads + if payload_type == PAYLOAD_TYPE_ADVERT: + if len(payload) < 32 + 4 + 64: return None + pubkey = payload[0:32] + # timestamp = int.from_bytes(payload[32:36], 'little') + # signature = payload[36:100] + appdata = payload[100:] + + node_id = pubkey.hex() + short_id = node_id[:2] + + name = "Unknown" + lat = None + lon = None + role_label = translate("meshcore.role.companion", "Companion") + role_internal = "Companion" + relay_capable = False + + if len(appdata) > 0: + flags = appdata[0] + ptr = 1 + if flags & 0x10: # has location + if len(appdata) >= ptr + 8: + lat = int.from_bytes(appdata[ptr:ptr+4], 'little', signed=True) / 1_000_000.0 + lon = int.from_bytes(appdata[ptr+4:ptr+8], 'little', signed=True) / 1_000_000.0 + ptr += 8 + if flags & 0x20: ptr += 2 # feature 1 + if flags & 0x40: ptr += 2 # feature 2 + if flags & 0x80: # has name + name = appdata[ptr:].decode('utf-8', errors='ignore') + + type_bits = flags & 0x07 + if type_bits == 0x02: + role_label = translate("meshcore.role.repeater", "Repeater") + role_internal = "Repeater" + relay_capable = True + elif type_bits == 0x03: + role_label = translate("meshcore.role.room_server", "Room Server") + role_internal = "Room Server" + relay_capable = True + elif type_bits == 0x04: + role_label = translate("meshcore.role.sensor", "Sensor") + role_internal = "Sensor" + + # Store path info if this advert came through repeaters + path_list = [] + if hop_count > 0: + for i in range(hop_count): + h = path_data[i*hash_size : (i+1)*hash_size] + path_list.append(h.hex()) + + update_node(node_id, short_name=short_id, long_name=name, hw_model=role_label, role=role_label, lat=lat, lon=lon, public_key=node_id, role_id=role_internal) + state.nodes[node_id]["relay_capable"] = relay_capable + state.nodes[node_id]["last_path"] = path_list + + log_to_console(f"[MESHCORE] ADVERT from {name} ({short_id}) - Role: {role_internal}") + + elif payload_type == PAYLOAD_TYPE_TXT_MSG: + if len(payload) >= 4: + dest_hash = payload[0] + src_hash = payload[1] + + src_name, src_id = resolveMeshCoreIdentity(src_hash) + dest_name, dest_id = resolveMeshCoreIdentity(dest_hash) + + now_dt = datetime.now() + msg_obj = { + "time": now_dt.strftime("%H:%M"), + "date": now_dt.strftime("%d/%m/%Y"), + "from": src_name, + "from_id": src_id, + "to": dest_id, + "text": translate("meshcore.message.encrypted", "[Encrypted Message]"), + "is_me": False, + } + state.messages.append(msg_obj) + state.new_messages.append(msg_obj) + + log_to_console(f"[MESHCORE] TXT from {src_id} to {dest_id} ({msg_obj['text']})") + + elif payload_type == PAYLOAD_TYPE_GRP_TXT: + if len(payload) >= 1: + # channel_hash = payload[0] + now_dt = datetime.now() + msg_obj = { + "time": now_dt.strftime("%H:%M"), + "date": now_dt.strftime("%d/%m/%Y"), + "from": "Group", + "from_id": "group", + "to": "All", + "text": translate("meshcore.message.group_encrypted", "[Encrypted Group Message]"), + "is_me": False, + } + state.messages.append(msg_obj) + state.new_messages.append(msg_obj) + log_to_console(f"[MESHCORE] GRP_TXT ({msg_obj['text']})") + + return True + def decodeProtobuf(packetData, sourceID, destID, cryptplainprefix, *, count_invalid: bool = True, preset_name: str | None = None, channel_hash: int | None = None, forced_channel_id: str | None = None, packet_id: bytes | None = None): try: data = mesh_pb2.Data() @@ -2520,6 +2721,10 @@ def parse_framed_stream_bytes(rx_buf: bytearray): frame_preset_id = body[preset_id_off] if len(body) > preset_id_off else 0 frame_preset_name = PRESET_ID_MAP.get(frame_preset_id) + if state.network_mode == "meshcore": + decodeMeshCore(payload, snr=snr_val, rssi=rssi_val) + continue + # 1) Extract Meshtastic fields extracted = dataExtractor(payload.hex()) @@ -3113,7 +3318,36 @@ def start_engine_direct(): env["CONDA_PREFIX"] = runtime env["PYTHONNOUSERSITE"] = "1" - region = getattr(state, "direct_region", "EU_868") + if state.network_mode == "meshcore": + # MeshCore direct engine start + cmd = [ + py, "-m", "meshtastic_engine.run_engine", + "--host", "127.0.0.1", + "--port", str(state.port), + "--center-freq", str(int(round(state.meshcore_custom_freq * 1_000_000))), + "--samp-rate", "1000000", + "--lora-bw", str(int(round(state.meshcore_custom_bw * 1000))), + "--sf", str(int(state.meshcore_custom_sf)), + "--gain", str(int(state.direct_gain)), + "--ppm", str(int(state.direct_ppm)), + "--sync-word", str(int(state.meshcore_custom_sync_word)), + "--preamble-len", str(int(state.meshcore_custom_preamble_len)), + ] + + raw_device_args = str(getattr(state, "direct_device_args", "") or "").strip() + if raw_device_args: + cmd.extend(["--device-args", raw_device_args]) + if getattr(state, 'direct_bias_tee', False): + cmd.append("--bias-tee") + + log_to_console(f"[ENGINE][MESHCORE] Radio settings: freq={state.meshcore_custom_freq} MHz, bw={state.meshcore_custom_bw} kHz, sf={state.meshcore_custom_sf}") + + primary_calc = {"center_freq_hz": int(round(state.meshcore_custom_freq * 1_000_000)), "bw_khz": state.meshcore_custom_bw, "sf": state.meshcore_custom_sf} + valid_configs = [] + primary_key = "MESHCORE" + primary_preset_id = 0 + else: + region = getattr(state, "direct_region", "EU_868") preset_name = state.direct_preset freq_slot = getattr(state, "direct_frequency_slot", 0) ch_name = getattr(state, "direct_channel_name", "") or None @@ -3157,27 +3391,29 @@ def start_engine_direct(): else: valid_configs.append(entry) - if primary is None: - msg = f"No valid preset found for region {region}." - log_to_console(f"[ENGINE] {msg}") - show_engine_error_dialog(msg) - return + if state.network_mode != "meshcore": + if primary is None: + msg = f"No valid preset found for region {region}." + log_to_console(f"[ENGINE] {msg}") + show_engine_error_dialog(msg) + return - primary_key, primary_preset_id, primary_calc, _ = primary + primary_key, primary_preset_id, primary_calc, _ = primary log_to_console(f"[ENGINE] Primary: {primary_key} (preset_id={primary_preset_id}), extra chains: {len(valid_configs)}") - cmd = [ - py, "-m", "meshtastic_engine.run_engine", - "--host", "127.0.0.1", - "--port", str(state.port), - "--center-freq", str(primary_calc["center_freq_hz"]), - "--samp-rate", "1000000", - "--lora-bw", str(int(round(primary_calc["bw_khz"] * 1000))), - "--sf", str(primary_calc["sf"]), - "--gain", str(int(state.direct_gain)), - "--ppm", str(int(state.direct_ppm)), - "--preset-id", str(primary_preset_id), - ] + if state.network_mode != "meshcore": + cmd = [ + py, "-m", "meshtastic_engine.run_engine", + "--host", "127.0.0.1", + "--port", str(state.port), + "--center-freq", str(primary_calc["center_freq_hz"]), + "--samp-rate", "1000000", + "--lora-bw", str(int(round(primary_calc["bw_khz"] * 1000))), + "--sf", str(primary_calc["sf"]), + "--gain", str(int(state.direct_gain)), + "--ppm", str(int(state.direct_ppm)), + "--preset-id", str(primary_preset_id), + ] if valid_configs: import json as _json cmd.extend(["--extra-demod-configs", _json.dumps(valid_configs)]) @@ -3936,6 +4172,32 @@ def main_page(): } catch (e) { } } catch (e) { } } + window.meshEnsurePathLayer = (map) => { + if (!window.L || !map) return; + if (map._meshPathLayer) return; + map._meshPathLayer = window.L.layerGroup().addTo(map); + }; + + window.meshDrawPaths = (mapElementId, paths) => { + try { + const el = (typeof getElement === 'function') ? getElement(mapElementId) : null; + const map = el && el.map; + if (!window.L || !map) return; + window.meshEnsurePathLayer(map); + map._meshPathLayer.clearLayers(); + + paths.forEach(p => { + if (!p.coords || p.coords.length < 2) return; + window.L.polyline(p.coords, { + color: '#3b82f6', + weight: 2, + opacity: 0.6, + dashArray: '5, 10' + }).addTo(map._meshPathLayer); + }); + } catch (e) { } + }; + window.meshApplyThemeToMapWhenReady = (tries) => { let remaining = Number.isFinite(Number(tries)) ? Number(tries) : 40; const tick = () => { @@ -4223,11 +4485,13 @@ def main_page(): } catch (e) { } }; - window.meshMarkerHtml = (label, cls, hopCls) => { + window.meshMarkerHtml = (label, cls, hopCls, roleCls, relayCls) => { const text = window.meshEscapeHtml(label ?? ''); const c = window.meshEscapeHtml(cls ?? ''); const h = window.meshEscapeHtml(hopCls ?? 'mesh-node-unknown'); - return '
Meshtastic SDR Analyzer & Desktop GUI
+Meshtastic & MeshCore SDR Analyzer & Desktop GUI
@@ -29,12 +29,12 @@
## 🇬🇧 English
-**MeshStation** is an open‑source Meshtastic SDR Analyzer and Desktop GUI.
-It decodes Meshtastic packets using any SDR, displays nodes on a map,
+**MeshStation** is an open‑source Meshtastic and MeshCore SDR Analyzer and Desktop GUI.
+It decodes Meshtastic and MeshCore packets using any SDR, displays nodes on a map,
shows chat messages, node database/list, mesh network overview and quality, raw console data, and will support TX in the future.
-In fact this is a real-time Meshtastic network observatory powered by **Software Defined Radio (SDR)**.
-It passively listens to the RF spectrum and decodes live Meshtastic traffic directly from the air — **no Meshtastic device required**.
+In fact this is a real-time mesh network observatory powered by **Software Defined Radio (SDR)**.
+It passively listens to the RF spectrum and decodes live Meshtastic or MeshCore traffic directly from the air — **no mesh device required**.
This project is designed for **network discovery, coverage analysis, network density, network quality, research, and real-world mapping of Meshtastic nodes**, using only a simple RTL-SDR receiver.
@@ -61,6 +61,12 @@ This enables use cases that are currently hard or impossible with standard Mesht
- **Scan all modem presets at once (LongFast, MediumFast etc.)**
- **Scan all the MeshTastic frequencies in the world**
+#### 🕸️ MeshCore Ecosystem Support
+- **Repeaters & Companions**: Full visualization of MeshCore's structured, repeater-centric hierarchy.
+- **Role-based Markers**: Instant visual identification of Repeaters (Triangle), Companions (Circle), and Room Servers (Square).
+- **Multi-hop Pathing**: Real-time dashed-line visualization of the repeater chain a packet traveled through.
+- **Regional Presets**: One-click configuration for USA/Canada (910.525 MHz) and Legacy Wide (915.8 MHz) deployments.
+
#### 🧠 Network Mapping & Research
- Map the **real Meshtastic network as it actually exists**
- Build a **database of nodes**, including:
@@ -111,6 +117,8 @@ If adequate **financial and community support** is reached, MeshStation aims to
- Supported SDR drivers
- A system capable of real-time SDR processing
+**Are you on Windows 11?** Check out the [Windows 11 Setup Guide](WINDOWS_SETUP.md) for step-by-step instructions on hardware, drivers, and startup.
+
**Visit the wiki if you're having trouble with drivers or to learn how to "install" the app:** [https://github.com/IronGiu/MeshStation/wiki](https://github.com/IronGiu/MeshStation/wiki)
---
@@ -137,12 +145,12 @@ If you find this project useful or exciting:
## 🇮🇹 Italiano
-**MeshStation** è un analizzatore SDR Meshtastic open source e un'interfaccia grafica desktop.
-Decodifica i pacchetti Meshtastic utilizzando qualsiasi SDR, visualizza i nodi su una mappa,
+**MeshStation** è un analizzatore SDR Meshtastic e MeshCore open source e un'interfaccia grafica desktop.
+Decodifica i pacchetti Meshtastic e MeshCore utilizzando qualsiasi SDR, visualizza i nodi su una mappa,
mostra messaggi di chat, database/lista nodi, Panoramica rete mesh e qualità, dati grezzi della console e supporterà la trasmissione in futuro.
-Infatti questo è un osservatorio della rete Meshtastic in tempo reale basato su **Software Defined Radio (SDR)**.
-Ascolta passivamente lo spettro RF e decodifica il traffico Meshtastic **direttamente dall’etere**, senza bisogno di alcun dispositivo Meshtastic.
+Infatti questo è un osservatorio della rete mesh in tempo reale basato su **Software Defined Radio (SDR)**.
+Ascolta passivamente lo spettro RF e decodifica il traffico Meshtastic o MeshCore **direttamente dall’etere**, senza bisogno di alcun dispositivo mesh.
Il progetto è pensato per **scoperta della rete, analisi di copertura, traffico della rete, qualità della rete, studio e mappatura reale dei nodi Meshtastic**, utilizzando solo un semplice ricevitore RTL-SDR.
@@ -169,6 +177,12 @@ Questo permette casi d’uso difficili o impossibili con le configurazioni Mesht
- **Scansionare contemporaneamente tutti i modem-preset (LongFast, MediumFast ecc)**
- **Scansionare tutte le frequenze MeshTastic del mondo**
+#### 🕸️ Supporto per l'Ecosistema MeshCore
+- **Ripetitori e Companion**: Visualizzazione completa della gerarchia strutturata e incentrata sui ripetitori di MeshCore.
+- **Marcatori basati sui ruoli**: Identificazione visiva istantanea di Ripetitori (Triangolo), Companion (Cerchio) e Server di Stanza (Quadrato).
+- **Percorsi Multi-hop**: Visualizzazione in tempo reale (linea tratteggiata) della catena di ripetitori attraversata da un pacchetto.
+- **Preset Regionali**: Configurazione con un clic per i paesi USA/Canada (910.525 MHz) e Legacy Wide (915.8 MHz).
+
#### 🧠 Mappatura & Studio della Rete
- Mappare la **rete Meshtastic reale**, così com’è davvero
- Creare un **database dei nodi**, includendo:
@@ -218,6 +232,8 @@ Con un adeguato **supporto economico e della community**, MeshStation potrà evo
- Driver SDR supportati
- Sistema in grado di gestire SDR in tempo reale
+**Sei su Windows 11?** Consulta la [Guida all'installazione per Windows 11](WINDOWS_SETUP.md) per istruzioni dettagliate su hardware, driver e avvio.
+
**Vai nella wiki se hai problemi con driver o per sapere come "installare" l'app:** [https://github.com/IronGiu/MeshStation/wiki](https://github.com/IronGiu/MeshStation/wiki)
---
diff --git a/WINDOWS_SETUP.md b/WINDOWS_SETUP.md
new file mode 100644
index 0000000..9f04687
--- /dev/null
+++ b/WINDOWS_SETUP.md
@@ -0,0 +1,68 @@
+# 🪟 Windows 11 Setup Guide for MeshStation
+
+Follow these steps to get MeshStation running on your Windows 11 machine.
+
+## 1. Prerequisites
+### Hardware
+- A compatible **RTL-SDR dongle** (e.g., RTL-SDR Blog V3/V4, Noelec, HackRF).
+- An appropriate antenna for the frequency you intend to monitor (e.g., 868MHz or 915MHz).
+
+### Software
+- **Python 3.10 or 3.11**: [Download from python.org](https://www.python.org/downloads/windows/). Ensure you check **"Add Python to PATH"** during installation.
+- **Git**: [Download from git-scm.com](https://git-scm.com/download/win) (if you are cloning the repository).
+
+---
+
+## 2. Installation Steps
+
+### Step A: Clone or Download
+Clone the repository or download the source code as a ZIP and extract it to a folder (e.g., `C:\MeshStation`).
+
+### Step B: Install Python Dependencies
+Open **PowerShell** or **Command Prompt** in the project folder and run:
+```powershell
+pip install -r requirements.txt
+```
+
+### Step C: Build the SDR Radio Engine
+MeshStation uses a high-performance GNU Radio-based engine. On Windows, you need to build the portable runtime:
+1. Navigate to the folder: `engine\os\win_x86_64\`
+2. Double-click on `auto-engine-builder.bat`.
+3. Wait for the process to complete. It will download the necessary components and create a `runtime` folder.
+
+---
+
+## 3. SDR Driver Setup (Zadig)
+Windows often installs a default "DVB-T" driver for RTL-SDR dongles, which prevents them from being used as general-purpose SDRs.
+
+1. Plug in your RTL-SDR dongle.
+2. Download and run **Zadig**: [https://zadig.akeo.ie/](https://zadig.akeo.ie/)
+3. Go to `Options` -> `List All Devices`.
+4. Select your SDR from the dropdown (often called `Bulk-In, Interface (Interface 0)` or `RTL2838UHIDIR`).
+5. Ensure the target driver (right side) is **WinUSB**.
+6. Click **Replace Driver** (or **Install Driver**).
+
+---
+
+## 4. Starting MeshStation
+Go back to the root project folder and run:
+```powershell
+python MeshStation.py
+```
+
+---
+
+## 5. Basic Usage
+1. **Connect**: Click the **Connect** button in the top menu.
+2. **Choose Ecosystem**: In the Connection Settings, use the **Network Ecosystem** toggle to select **Meshtastic** or **MeshCore**.
+3. **Configure Settings**:
+ - **Meshtastic**: Select your Region (e.g., US, EU_868) and Modem Preset.
+ - **MeshCore**: Select a Regional Preset (e.g., USA/Canada) or choose "Custom" to set your own frequency and SF.
+4. **Start**: Click **Connect** inside the settings panel. You should see the console log starting the engine and, eventually, nodes appearing on the map!
+
+---
+
+## 🛠️ Troubleshooting
+- **No device found**: Ensure you performed the Zadig driver replacement. Try a different USB port.
+- **Engine won't start**: Re-run the `auto-engine-builder.bat` script. Ensure no other SDR software (like SDR# or CubicSDR) is using the dongle.
+- **Visual Glitches**: If the map looks strange, try running with the `--nogpu` flag: `python MeshStation.py --nogpu`.
diff --git a/engine/meshtastic_engine/flowgraphs/rx_lora_base_engine.py b/engine/meshtastic_engine/flowgraphs/rx_lora_base_engine.py
index cfe97ba..12e6444 100644
--- a/engine/meshtastic_engine/flowgraphs/rx_lora_base_engine.py
+++ b/engine/meshtastic_engine/flowgraphs/rx_lora_base_engine.py
@@ -51,7 +51,8 @@ def __init__(
payload_length=237,
impl_head=False,
has_crc=True,
- sync_word=(0, 0),
+ sync_word=None,
+ preamble_len=None,
device_args="",
bias_tee=False,
extra_demod_configs=None,
@@ -99,7 +100,20 @@ def _apply_bias_tee_args(device_args: str, enable: bool, dev: str):
samp_rate = int(samp_rate)
self.samp_rate = samp_rate
lora_bw = int(lora_bw)
+
+ # Sync word processing: MeshCore often uses 0x12 (18)
+ if sync_word is None:
+ sync_word = [0, 0] # default for meshtastic
+ elif isinstance(sync_word, int):
+ sync_word = [0x10, sync_word] # gr-lora style
+
sync_word = [int(x) for x in sync_word]
+
+ if preamble_len is None:
+ preamble_length = 17 # default for meshtastic
+ else:
+ preamble_length = int(preamble_len)
+
dc_shift = 0
dc_iq_mode = 0
# Pre-detect from args (may be "unknown" if args is empty = auto mode)
@@ -337,6 +351,15 @@ def _add_demod_chain(self, cfg: dict):
chain_bw = int(cfg['bw'])
chain_freq = int(cfg['center_freq'])
chain_preset_id = int(cfg.get('preset_id', 0))
+ chain_sync_word = cfg.get('sync_word')
+ if chain_sync_word is None:
+ chain_sync_word = self.sync_word
+ elif isinstance(chain_sync_word, int):
+ chain_sync_word = [0x10, chain_sync_word]
+
+ chain_preamble_len = cfg.get('preamble_len')
+ if chain_preamble_len is None:
+ chain_preamble_len = self.preamble_length
# BW-specific bandpass filter taps
chain_taps = firdes.complex_band_pass(
@@ -349,7 +372,7 @@ def _add_demod_chain(self, cfg: dict):
xlat = filter.freq_xlating_fir_filter_ccc(decimation, chain_taps, chain_freq - self.center_freq, self.samp_rate)
xlat.set_min_output_buffer(17000)
- frame_sync = lora_sdr_extra.frame_sync(chain_freq, chain_bw, chain_sf, self.impl_head, list(self.sync_word), 4, 17)
+ frame_sync = lora_sdr_extra.frame_sync(chain_freq, chain_bw, chain_sf, self.impl_head, list(chain_sync_word), 4, chain_preamble_len)
fft_demod = lora_sdr_extra.fft_demod(self.soft_decoding, True)
gray_map = lora_sdr_extra.gray_mapping(self.soft_decoding)
deinterleave = lora_sdr_extra.deinterleaver(self.soft_decoding)
diff --git a/engine/meshtastic_engine/run_engine.py b/engine/meshtastic_engine/run_engine.py
index 356507d..f0018ce 100644
--- a/engine/meshtastic_engine/run_engine.py
+++ b/engine/meshtastic_engine/run_engine.py
@@ -34,6 +34,9 @@ def main(argv=None) -> int:
ap.add_argument("--preset-id", type=int, default=0, help="Preset ID byte embedded in each frame (0=unset)")
ap.add_argument("--extra-demod-configs", default=None, help="JSON list of extra demod chains, e.g. '[{\"sf\":9,\"bw\":250000,...}]'")
+ ap.add_argument("--sync-word", type=int, default=None, help="Override LoRa sync word")
+ ap.add_argument("--preamble-len", type=int, default=None, help="Override LoRa preamble length")
+
args = ap.parse_args(argv)
q = queue.Queue(maxsize=4000)
@@ -60,6 +63,8 @@ def main(argv=None) -> int:
device_args=args.device_args,
bias_tee=args.bias_tee,
extra_demod_configs=extra_demod_configs,
+ sync_word=args.sync_word,
+ preamble_len=args.preamble_len,
)
# Optional: tweak aggregator timings from CLI
diff --git a/languages.json b/languages.json
index 4d6e56b..8d853cc 100644
--- a/languages.json
+++ b/languages.json
@@ -62,6 +62,25 @@
"ui.totalnodes": "Total Nodes",
"ui.model": "Model",
"ui.role": "Role",
+ "ui.public_key": "Public Key",
+ "ui.short_id": "Short ID",
+ "ui.type": "Type",
+ "ui.freshness": "Freshness",
+ "ui.radio_parameters": "Radio Parameters",
+ "ui.frequency": "Frequency",
+ "ui.bandwidth": "Bandwidth",
+ "ui.coding_rate": "Coding Rate",
+ "ui.spreading_factor": "Spreading Factor",
+ "ui.relay_capable": "Relay Capable",
+ "ui.custom": "Custom",
+ "meshcore.preset.recommended": "USA/Canada (Recommended)",
+ "meshcore.preset.legacy_wide": "Legacy 915-region Wide",
+ "meshcore.role.companion": "Companion",
+ "meshcore.role.repeater": "Repeater",
+ "meshcore.role.room_server": "Room Server",
+ "meshcore.role.sensor": "Sensor",
+ "meshcore.message.encrypted": "[Encrypted Message]",
+ "meshcore.message.group_encrypted": "[Encrypted Group Message]",
"ui.resetfilters": "Reset Filters",
"chat.load_older": "Load older messages",
"chat.stored_in_memory": "(all messages kept in memory)",
@@ -111,6 +130,16 @@
"panel.connection.settings.internal.label.port.tooltip": "Only change this if the internal engine cannot bind the default port,\nor if you are running multiple parallel instances of this application from different folders with different SDR devices.",
"panel.connection.settings.internal.label.aes_key": "AES Public Key (Base64) (default: AQ==)",
"panel.connection.settings.internal.label.aes_key.hint": "Key size is auto-detected from the Base64 length (16 bytes = 128-bit, 32 bytes = 256-bit). 'AQ==' = Meshtastic default.",
+ "panel.connection.settings.network_mode": "Network Ecosystem:",
+ "panel.connection.settings.meshcore.internal.title": "MeshCore SDR Engine",
+ "panel.connection.settings.meshcore.internal.help": "Configure MeshCore radio parameters for the internal engine.",
+ "panel.connection.settings.meshcore.label.advanced": "Advanced LoRa Settings",
+ "panel.connection.settings.meshcore.label.sync_word": "Sync Word (hex)",
+ "panel.connection.settings.meshcore.label.preamble": "Preamble",
+ "panel.connection.settings.meshcore.label.freq": "Freq (MHz)",
+ "panel.connection.settings.meshcore.label.sf": "SF",
+ "panel.connection.settings.meshcore.label.bw": "BW (kHz)",
+ "panel.connection.settings.meshcore.label.cr": "CR",
"panel.connection.settings.externaltab": "External",
"panel.connection.settings.external.title": "External GNU Radio / ZMQ Stream",
"panel.connection.settings.external.help1": "Requires an external specific (our custom frame) GNU Radio flowgraph with a ZMQ PUB block.",
@@ -300,6 +329,25 @@
"ui.totalnodes": "Totale Nodi",
"ui.model": "Modello",
"ui.role": "Ruolo",
+ "ui.public_key": "Chiave Pubblica",
+ "ui.short_id": "ID Breve",
+ "ui.type": "Tipo",
+ "ui.freshness": "Freschezza",
+ "ui.radio_parameters": "Parametri Radio",
+ "ui.frequency": "Frequenza",
+ "ui.bandwidth": "Larghezza di Banda",
+ "ui.coding_rate": "Coding Rate",
+ "ui.spreading_factor": "Spreading Factor",
+ "ui.relay_capable": "Capacità Relay",
+ "ui.custom": "Personalizzato",
+ "meshcore.preset.recommended": "USA/Canada (Raccomandato)",
+ "meshcore.preset.legacy_wide": "Legacy 915-regione Wide",
+ "meshcore.role.companion": "Companion",
+ "meshcore.role.repeater": "Ripetitore",
+ "meshcore.role.room_server": "Server di Stanza",
+ "meshcore.role.sensor": "Sensore",
+ "meshcore.message.encrypted": "[Messaggio Criptato]",
+ "meshcore.message.group_encrypted": "[Messaggio di Gruppo Criptato]",
"ui.resetfilters": "Resetta Filtri",
"chat.load_older": "Carica messaggi precedenti",
"chat.stored_in_memory": "(tutti i messaggi sono mantenuti in memoria)",
@@ -349,6 +397,16 @@
"panel.connection.settings.internal.label.aes_key": "Chiave Pubblica AES (Base64) (default: AQ==)",
"panel.connection.settings.internal.label.aes_key.hint": "Dimensione della chiave è auto-determinata dalla lunghezza Base64 (16 bytes = 128-bit, 32 bytes = 256-bit). 'AQ==' = Chiave predefinita Meshtastic.",
"panel.connection.settings.internal.label.rf_gain": "Guadagno RF",
+ "panel.connection.settings.network_mode": "Ecosistema Rete:",
+ "panel.connection.settings.meshcore.internal.title": "Motore SDR MeshCore",
+ "panel.connection.settings.meshcore.internal.help": "Configura i parametri radio MeshCore per il motore interno.",
+ "panel.connection.settings.meshcore.label.advanced": "Impostazioni LoRa Avanzate",
+ "panel.connection.settings.meshcore.label.sync_word": "Parola di Sincronizzazione (hex)",
+ "panel.connection.settings.meshcore.label.preamble": "Preambolo",
+ "panel.connection.settings.meshcore.label.freq": "Freq (MHz)",
+ "panel.connection.settings.meshcore.label.sf": "SF",
+ "panel.connection.settings.meshcore.label.bw": "BW (kHz)",
+ "panel.connection.settings.meshcore.label.cr": "CR",
"panel.connection.settings.externaltab": "Esterno",
"panel.connection.settings.external.title": "GNU Radio Esterno / Stream ZMQ",
"panel.connection.settings.external.help1": "Richiede un flusso GNU Radio specifico (nostro frame personalizzato) con un blocco ZMQ PUB.",