diff --git a/AGENTS.md b/AGENTS.md index cf8fce13e..96f2b9734 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ This file is self-contained. ARCHITECTURE.md and CONTRIBUTING.md are human refer Electron: `src/main/` (Node, SQLite, BLE, MQTT), `src/preload/` (bridge), `src/renderer/` (React 19, Vite, Zustand). **Multi-protocol:** Meshtastic, MeshCore, and Reticulum; gate UI with `ProtocolCapabilities` and `useRadioProvider(protocol)` (do not compare `protocol === 'meshcore'`). Routing/diagnostics changes must stay compatible with the Diagnostics panel (Hop Goblins, Hidden Terminals, etc. for LoRa; Reticulum uses `ReticulumDiagnosticEngine.ts`). **pnpm** only for package commands. **Never** add cryptocurrency tech or dependencies. -**Colors:** Use Tailwind CSS utility classes (e.g., `text-green-400`, `bg-slate-700`). Custom theme colors via CSS custom properties in `styles.css` (`--color-brand-green`, etc.). Avoid inline hex colors in JSX. +**Colors:** Use Tailwind CSS utility classes (e.g., `text-green-400`, `bg-slate-700`). Custom theme colors via CSS custom properties in `styles.css` (`--color-brand-green`, etc.). Avoid inline hex colors in JSX. **App → Appearance → Colors** lets users customize theme tokens including chat/RRC **message action** bar/button colors (`themeColors.ts`); **Show background** / **Always show message actions** control action-bar visibility. **Code style and testing:** [Code style & standards](CONTRIBUTING.md#code-style--standards) and [Testing protocols](CONTRIBUTING.md#testing-protocols) in [CONTRIBUTING.md](CONTRIBUTING.md). @@ -158,7 +158,7 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`). - **RNode flasher timeouts:** `RNODE_COMMAND_TIMEOUT_MS` (30 s serial), `RNODE_BT_PAIRING_TIMEOUT_MS` (90 s BLE pairing), `ESP32_FLASH_STALL_TIMEOUT_MS` / `NRF52_DFU_STALL_TIMEOUT_MS` (60 s no-progress → `ESP32_FLASH_STALLED` / `NRF52_DFU_STALLED`); humanized via `flasherErrorHumanize.ts` - **Peer aliases / History vs Contacts:** LXMF/Nomad announce names overlay path-table peers; SQLite `reticulum_destinations.last_heard` = History, `is_contact` = Contacts (Save as contact only — inbound/outbound LXMF does **not** auto-add Contacts; sidecar `/contacts` wire rows are History hints unless SQLite `is_contact=1`); default avatars via vendored LXMFace (`lib/reticulum/lxmface.ts`); renderer refresh + `reticulumContactToNodeRecordPreservingLabel` refuse hash-prefix wipes of Chat/`nodeStore` labels; ingest stamps History via `persistReticulumHistoryFromPayload` + `stampHistoryPeer`; SQL upsert guard preserves real names over hash-prefix aliases; destination upsert requires exact 32-hex (lowercase) and omits `favorited` on icon-only patches so favorites/icons survive path/probe refresh - **Stores/lib:** `reticulumIdentityStore.ts` (session-global sidecar identity status shared by `useReticulumSidecarApi` — distinct from identity-scoped `identityStore`), `reticulumPeerStore.ts` (path-table `peers` + `history` + saved `contacts`; soft-TTL reads, forced `?refresh=1`, incremental `peers_updated` route-field patches, 50ms batching, name/appearance preservation, 30s/60s large-mesh poll), `reticulumDiscoveryMapStore.ts`, `reticulumRmapDiscovery.ts`, `reticulumDiscoveryMapLayout.ts`, `nomadNetworkStore.ts`, `rrcHubStore.ts` / `rrcSessionStore.ts` (RRC hubs + multi-hub sessions; hydrate/clear room history via `rrcRoomHistory.ts`; persist → SQLite `rrc_messages` via `rrcMessagePersist.ts` + `ipc/rrc-db-handlers.ts`; prefs in `rrcHubPrefs` / `rrcRoomPrefs` / `rrcRecentRooms`; notifications in `rrcInactiveNotifications` / `rrcMention`); **Remote (rnsh/rncp):** `rncpTransferStore.ts`, `rnshSessionStore.ts`, `reticulumInboundPolicyStore.ts`, `reticulumRemoteAddressStore.ts`, `rncpEnableRequestStore.ts` + lib `remoteSettingsStorage.ts`, `pushRncpListenerPolicy.ts`, `rncpInboundPolicyLists.ts`, `sendRncpRequestEnable.ts`, `rncpRequestEnableRateLimit.ts`, `applyRncpReceiveDestShare.ts` / `rncpReceiveDestSharePending.ts` (mark pending on request-enable; consume on ingest within TTL), `hooks/useRemotePathCapability.ts`, `components/remote/*`; WS events `rmap.discovery`, `lxmf_outbound_status`, `nomadnetwork.node`, `rrc.*`, `rnsh.*` / `rncp.*` in `useReticulumRuntime` (sidecar also emits `nomad.serving_start` / `nomad.serving_stop`; renderer polls serving status via HTTP, not those WS events) -- **LXMF outbound delivery:** sidecar `lxmf_delivery.rs` / `lxmf_outbound.rs` (Direct-first; **one-shot fallback** to preferred **remote** PN on Direct fail; intermediate WS `sending` + `delivery_method: "propagated"`); renderer `applyReticulumOutboundDeliveryStatus.ts` (WS `lxmf_outbound_status` → Zustand + SQLite `delivery_status` + `delivery_method`; early-status buffer; hash/status allowlist), `reticulumOutboundFailureBridge.ts` (`shouldApplyLinkDeliveryTimeoutFailureBridge` skips the link-timeout Failed bridge when an effective remote PN target exists; also skips `propagated` rows so fallback is not killed), `markStaleReticulumOutbound.ts`. Propagated Completes UI: **Stored at propagation node**. **Paper exception:** `createReticulumPaperMessage` / paper create Completes immediately (`delivery_method: paper`, `ReticulumMessageStatusBadge` **Paper**) via `lxmf_message` — no `lxmf_outbound_status`; shared `reticulumMessageTransport` / `reticulumPaperErrors` keep IPC allowlists and i18n codes aligned. +- **LXMF outbound delivery:** sidecar `lxmf_delivery.rs` / `lxmf_outbound.rs` (Direct-first; **one-shot fallback** to preferred **remote** PN on Direct fail; intermediate WS `sending` + `delivery_method: "propagated"`); renderer `applyReticulumOutboundDeliveryStatus.ts` (WS `lxmf_outbound_status` → Zustand + SQLite `delivery_status` + `delivery_method`; early-status buffer; hash/status allowlist), `reticulumOutboundFailureBridge.ts` (`shouldApplyLinkDeliveryTimeoutFailureBridge` skips the link-timeout Failed bridge when an effective remote PN target exists; also skips `propagated` rows so fallback is not killed), `markStaleReticulumOutbound.ts`. Optimistic pending rows use `reticulum-pending-*`; send-path rekey passes `replaces_message_hash` on SQLite upsert to delete the prior pending hash. Propagated Completes UI: **Stored at propagation node**. **Paper exception:** `createReticulumPaperMessage` / paper create Completes immediately (`delivery_method: paper`, `ReticulumMessageStatusBadge` **Paper**) via `lxmf_message` — no `lxmf_outbound_status`; shared `reticulumMessageTransport` / `reticulumPaperErrors` keep IPC allowlists and i18n codes aligned. - **DM path reachability:** `useReticulumDmPathProbe.ts`, `reticulumDmPathReachability.ts`, `ReticulumDmPathReachabilityBadge.tsx` — Chat **Probe** matches Peer List (sidecar running check → `/probe` → toast → refresh); `applyProbeResult(forHash, …)` applies the settle without a second `/probe` and ignores stale completions after DM switch; manual reprobe forces Checking… even when passive hops look reachable; Peers virtualizes above 100 rows via `reticulumPeerListRows.ts`; peer refresh policy in `reticulumSidecarPeerRefreshEvents.ts` - **Inbound transport labels:** `received_via` resolves the path-table interface name against local interface config type, so a TCP hub display name still renders as TCP. - **Topology:** `via_hash` is an immediate transport id; sidecar synthesizes missing relay nodes. `ReticulumTopologyPanel` uses force layout; sidecar caps graph input at 2,000 peers and renderer caps visible peers at 800 (grid repulsion above 400). @@ -187,15 +187,15 @@ Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`). See **Renderer: hooks vs runtime vs lib** (layout map above). Legacy `useDevice` / `useMeshCore` are removed ([#375](https://github.com/Colorado-Mesh/mesh-client/issues/375), [#377](https://github.com/Colorado-Mesh/mesh-client/issues/377)). Default rules for new UI: -| Concern | Use | -| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Orchestration (App tab) | `useProtocolFacade(protocol)` — connection, `useConnectionView`, panel bundle, nodes, messages | -| Active protocol identity | `useActiveMeshIdentity(protocol)` — focused `identityId` per tab; prefer `capabilities` over `protocol ===` | -| LoRa dual-protocol panel bundles (App) | `useAllProtocolPanelActions` (Meshtastic + MeshCore + Reticulum); prefer this over per-protocol panel-action hooks at the App shell | -| Reads (nodes, messages, connection fields) | Zustand stores + `useNodes` / `useMessages` / `useConnectionView` / `useConnectionStatus` | -| Writes (configure, send, admin, panel callbacks) | `usePanelActions(protocol, identityId, …)` / `useProtocolFacade(protocol).panel` or `useSendMessage(identityId)` | -| Connect / disconnect / auto-connect | `useProtocolConnectionActions(protocol)` (`useProtocolConnect` + `useProtocolDisconnect` + `lib/sessions/*Session.ts`); Meshtastic/MeshCore via `ConnectionDriver`; Reticulum via sidecar start/stop | -| Wire subscriptions, MQTT IPC, reconnect, DB hydration | `useMeshtasticRuntime` / `useMeshcoreRuntime` / `useReticulumRuntime` in `runtime/` — mount **once** from `App.tsx` via context providers | +| Concern | Use | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Orchestration (App tab) | `useProtocolFacade(protocol)` — connection, `useConnectionView`, panel bundle, nodes, messages | +| Active protocol identity | `useActiveMeshIdentity(protocol)` — focused `identityId` per tab; prefer `capabilities` over `protocol ===` | +| LoRa dual-protocol panel bundles (App) | `useAllProtocolPanelActions` (Meshtastic + MeshCore + Reticulum); prefer this over per-protocol panel-action hooks at the App shell | +| Reads (nodes, messages, connection fields) | Zustand stores + `useNodes` / `useMessages` / `useConnectionView` / `useConnectionStatus` | +| Writes (configure, send, admin, panel callbacks) | `usePanelActions(protocol, identityId, …)` / `useProtocolFacade(protocol).panel` or `useSendMessage(identityId)` | +| Connect / disconnect / auto-connect | `useProtocolConnectionActions(protocol)` (`useProtocolConnect` + `useProtocolDisconnect` + `lib/sessions/*Session.ts`); Meshtastic/MeshCore via `ConnectionDriver`; Reticulum via sidecar start/stop. Launch RF auto-connect (serial/BLE/TCP/HTTP) via `ProtocolAutoConnectCoordinator` + `useProtocolRfAutoConnect` + `protocolRfAutoConnectGate` (`cancelProtocolRfAutoConnect` before manual Connect). LoRa reconnect single-owner: `rfReconnectController` | +| Wire subscriptions, MQTT IPC, reconnect, DB hydration | `useMeshtasticRuntime` / `useMeshcoreRuntime` / `useReticulumRuntime` in `runtime/` — mount **once** from `App.tsx` via context providers | Do **not** remount protocol runtimes in child components. Do **not** compare `protocol === 'meshcore'` for feature gates; use `ProtocolCapabilities` / `useRadioProvider(protocol)`. @@ -218,6 +218,7 @@ Meshtastic and MeshCore share LoRa BLE reconnect contracts whenever possible (pl **LoRa BLE reconnect parity (Meshtastic + MeshCore):** +- **`rfReconnectController`** (`lib/rfReconnectController.ts`): single-owner link-lost / schedule / endAttempt for both runtimes (MeshCore TCP uses the same owner; conn side effects must not call `handleConnectionLost` for TCP). - **`gattSetupInflight`** in `noble-ble-manager` (both session ids): after `connectAsync`, mid-GATT disconnect rejects in-flight `connect()` promptly (Noble only). - **Deferred Noble disconnect** while connect/reconnect open is in flight; **flush in reconnect `finally`** in `useMeshtasticRuntime` and `useMeshcoreRuntime` so edge-of-range drops keep retrying. - **`NOBLE_BLE_RECONNECT_ATTEMPT_BUDGET_MS`** (`timeConstants.ts`) + `raceWithDeadline` (`bleReconnectHelper.ts`): hard ceiling per BLE reconnect open+handshake attempt on **all platforms** (unsticks configure/attach hangs; does not add a Linux cross-protocol mutex). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2ba1d16af..29fa0b318 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -87,7 +87,9 @@ Sanitize user-controlled strings before logs and IPC per [AGENTS.md](AGENTS.md). ### App lifecycle (mount once from `App.tsx`) - **`usePowerRecovery`** — sleep/wake IPC, MQTT power suspend/resume, staggered RF reconnect (Meshtastic ~4s, MeshCore ~8s, dual-Noble settle up to ~30s). -- **`useRendererHeartbeat`** / **`useLongSessionMaintenance`** — renderer liveness after resume; long-uptime restart nudge. +- **`useRendererHeartbeat`** / **`useLongSessionMaintenance`** — renderer pings main every 30s; main `rendererHeartbeatWatchdog` warns if no heartbeat within 30s after resume **while visible**, and polls for a **90s visible-window stall**; sticky `rendererUnresponsiveSeen` + `getRendererLiveness()` feed support snapshot `mainLiveness`; long-uptime restart nudge. +- **`ProtocolAutoConnectCoordinator`** / **`useProtocolRfAutoConnect`** — silent launch auto-connect for remembered serial/BLE/TCP/HTTP (cancel gate before manual Connect). +- **`rfReconnectController`** — LoRa single-owner reconnect scheduling shared by Meshtastic/MeshCore runtimes. - **Dual-radio Noble BLE startup** (Meshtastic + MeshCore different peripherals): `lib/meshcoreDualNobleBleInit.ts` initialized from `App.tsx` `useLayoutEffect`; primary order from `mesh-client:protocol` localStorage — see [AGENTS.md](AGENTS.md) **Dual-radio Noble BLE startup**. ### Database diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a59add81c..eac557c15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Thank you for your interest in contributing. - **Prettier:** Semi always, single quotes, trailing commas, print width 100, tab 2, LF. - **TypeScript:** Strict; avoid `any`; prefer `unknown` + guards; export types; prefer interfaces over type aliases. - **React:** Function components only; `exhaustive-deps` is errors; `?.` in JSX; **every interactive control needs `aria-label`**. -- **Colors:** Use Tailwind CSS utility classes (e.g., `text-green-400`, `bg-slate-700`). Custom theme colors via CSS custom properties from `styles.css` (`--color-brand-green`, `--color-deep-black`, etc.). Avoid inline hex colors in JSX. +- **Colors:** Use Tailwind CSS utility classes (e.g., `text-green-400`, `bg-slate-700`). Custom theme colors via CSS custom properties from `styles.css` (`--color-brand-green`, `--color-deep-black`, etc.). Avoid inline hex colors in JSX. End users can also customize theme tokens (including message action colors) under **App → Appearance → Colors**. - **Zustand:** Module-level defaults for stable refs; prefer `useStore(s => s.field)` over broad subscriptions; avoid subscribing to whole Maps when one id suffices; `persist` for localStorage, IPC from an effect for SQLite; extract time constants to `src/renderer/lib/timeConstants.ts` (e.g. `MS_PER_SECOND`). - **Performance:** No hot-path O(n); lazy cleanup when collections grow large. diff --git a/README.md b/README.md index 25e74af3a..83326f3c8 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Mesh-Client supports **three mesh stacks** in one desktop app. Use the header ** - **Single table** from `diagnosticRows` (routing trace rows + RF rows), searchable; rows persist across sessions with an optional restore banner; **max age** (1–168 hours) trims stale routing (24 h default) and RF (1 h default) rows - **Foreign LoRa overhear** (Meshtastic tab): MeshCore-heard, Reticulum RNS, other-Meshtastic, and unknown LoRa classes from decode-fail logs and dual-radio RX; 90-minute window; MeshCore repeater conflict escalation above 5 pkt/min - **Mesh congestion attribution**: orange banner when mesh-wide routing stress is present; duplicate-traffic block in node detail when relevant -- Routing anomaly detection: **hop_goblin** (distance-proven over-hopping), **bad_route** (high duplication), **route_flapping** / **path_instability** (MeshCore PathUpdated events), **impossible_hop**, **weak_link** (MeshCore per-hop SNR from trace); with remediation suggestions and severity levels +- Routing anomaly detection: **hop_goblin** (distance-proven over-hopping; **Meshtastic-only**), **bad_route** (high duplication; close-in distance variant Meshtastic-only), **route_flapping** / **path_instability** (MeshCore PathUpdated events), **impossible_hop**, **weak_link** (MeshCore per-hop SNR from trace); with remediation suggestions and severity levels - **Channel Utilization History**: 24h CU timeline chart for the connected node in DiagnosticsPanel (fed by LocalStats / device-metrics ingest, not only NodeInfo) - Anomaly badges inline in node list; status aura circles on the map; congestion halos toggle; global and per-node MQTT ignore - **Environment Profile** segmented control; Standard (3 km), City (1.6× threshold), Canyon (2.6× threshold) @@ -163,6 +163,7 @@ Mesh-Client supports **three mesh stacks** in one desktop app. Use the header ** - **Tri-protocol switcher**: Meshtastic, MeshCore, and Reticulum run simultaneously; per-protocol unread badges (green / cyan / amber — Reticulum amber = LXMF Chat + **RRC** unread, not Games; Games has its own sidebar badge); passive toast notifications when an inactive protocol receives traffic - **Localization**: 16 languages via static JSON bundles; fully offline — see [Localization & Languages](docs/localization.md) - **Accessibility**: modal focus trap, screen reader labels, reduce-motion and **Use 24-hour time** toggles in App → Appearance — see [Accessibility Checklist](docs/accessibility-checklist.md) +- **Colors** (App → Appearance): customize theme tokens including chat/RRC **message action** bar and button hover colors; optional **Show background** on the action bar and **Always show message actions** - **Log panel**: live stream, **Analyze** heuristics, export/delete; Reticulum sidecar lines tagged `[ReticulumSidecar]` - **SQLite persistence**: protocol-scoped history and settings; DB export/import/clear in the App tab; **Export for GitHub** (zip: debug snapshot + logs) and **Export for Developer** (includes full SQLite — share privately only) - **Updates & tray**: footer update status; system tray unread badge when the window is backgrounded @@ -175,7 +176,7 @@ These sections apply to the two LoRa companion-radio stacks. Reticulum uses the - **Bluetooth LE**: pair wirelessly; on macOS/Windows, startup auto-reconnect can run without a user gesture (Noble backend). On Linux, Web Bluetooth requires user gesture and picker selection. **Reticulum** BLE (RNode / BLE Peer interfaces) uses the sidecar `btleplug` stack and may coexist on a **different** MAC while Meshtastic/MeshCore use Noble/Web Bluetooth — see [Reticulum BLE coexistence](docs/reticulum.md#interface-management-connection-tab). - **USB Serial**: plug in via USB; auto-reconnects silently on startup (saved port signature matches the same physical device across re-enumeration) -- **WiFi / HTTP / TCP**: Meshtastic offers **WiFi/HTTP** (REST, one packet per request) and **WiFi/TCP (fast)** (native binary streaming on port **4403**, same framing as USB serial — much faster NodeDB sync on large networks); MeshCore uses TCP on port **5000** by default; saves last address for quick reconnect +- **WiFi / HTTP / TCP**: Meshtastic offers **WiFi/HTTP** (REST, one packet per request) and **WiFi/TCP (fast)** (native binary streaming on port **4403**, same framing as USB serial — much faster NodeDB sync on large networks); MeshCore uses TCP on port **5000** by default; remembered addresses **auto-connect on launch** (same coordinator as serial/BLE) and support quick reconnect after drops - **Dual LoRa mode**: Meshtastic and MeshCore stay connected while you switch views; per-protocol unread badges; passive toasts on background traffic - **Connection status in header**: device, MQTT, and TAK indicators pulse **red** after an unexpected disconnect; manual stop/disconnect stays gray; in-progress connect keeps yellow @@ -382,7 +383,7 @@ Architecture and API: [docs/reticulum.md](docs/reticulum.md). Games wire parity: - **MQTT → RF (MeshCore JSON)**: Not supported; MeshCore MQTT is chat ingest only. - **Meshtastic - PKC remote admin**: Configure-node-over-MQTT is not supported; a connected local RF radio is required to reach remote nodes (firmware 2.5+). - **MeshCore - MQTT (JSON v1)**: The Connection tab can connect to an MQTT broker in MeshCore mode using a small JSON chat envelope (see [docs/meshcore-meshtastic-parity.md](docs/meshcore-meshtastic-parity.md)). This is separate from Meshtastic's protobuf MQTT pipeline. -- **MeshCore - partial routing diagnostics**: MeshCore now supports `route_flapping` / `path_instability` (PathUpdated events), `hop_goblin` / `bad_route` (when `hasHopCount`), and `weak_link` (when `hasPerHopSnr` and a trace is completed). Full hop-anomaly detection and Meshtastic-style LocalStats RF findings require Meshtastic packets; MeshCore provides its own RF findings (Elevated Noise Floor, Excessive Flooding) from Repeater Status packet stats. **Foreign LoRa** tables render on the Meshtastic tab only (MeshCore may record overhear internally). +- **MeshCore - partial routing diagnostics**: MeshCore supports `route_flapping` / `path_instability` (PathUpdated events) and `weak_link` (when `hasPerHopSnr` and a trace is completed). Distance-based `hop_goblin` / close-in `bad_route` are Meshtastic-only (`hasDistanceBasedHopAnomalies`). Full hop-anomaly detection and Meshtastic-style LocalStats RF findings require Meshtastic packets; MeshCore provides its own RF findings (Elevated Noise Floor, Excessive Flooding) from Repeater Status packet stats. **Foreign LoRa** tables render on the Meshtastic tab only (MeshCore may record overhear internally). - **MeshCore - channel editing**: Can add/edit/delete channels (name + PSK) via the Radio tab, but does not expose Meshtastic-style full protobuf config. Radio parameters (frequency, bandwidth, spreading factor, coding rate, TX power) can be set via the Radio tab. - **MeshCore - remote telemetry availability**: `getTelemetry` requires the remote node to have environment sensors. A timeout is returned if the node has no sensor data. - **MeshCore - neighbor info availability**: `getNeighbours` is supported only by Repeater-type nodes running firmware v1.9.0+. The button is hidden for Chat and Room contacts. @@ -497,7 +498,7 @@ After a successful connection, Mesh-Client remembers your last device per protoc - **Serial**: auto-connects silently in the background (Meshtastic and MeshCore) - **Bluetooth (macOS/Windows)**: auto-scans on launch and reconnects when the last device is discovered (no user gesture required) - **Bluetooth (Linux)**: Web Bluetooth requires a user gesture; click **Reconnect** or **Connect** to open the picker. **MeshCore:** if the device is not paired in BlueZ, enter the PIN from the radio when prompted (OS pairing runs before the connection finishes). -- **WiFi / TCP**: a one-click reconnect card appears; click **Reconnect** +- **WiFi / HTTP / TCP**: remembered Meshtastic HTTP/TCP and MeshCore TCP addresses **auto-connect silently on launch** via `ProtocolAutoConnectCoordinator` (stays alive across tab switches). A one-click **Reconnect** card appears only when auto-connect fails or was cancelled by a manual Connect. Manual Connect cancels any in-flight auto-connect first (`cancelProtocolRfAutoConnect`). - **MQTT**: auto-reconnects using saved broker settings (Meshtastic protobuf pipeline; MeshCore JSON v1 adapter; select transport when connecting) - **Reticulum**: with **Auto-start** enabled, the sidecar starts on launch; otherwise click **Start stack** on the Connection tab after opening the app diff --git a/docs/accessibility-checklist.md b/docs/accessibility-checklist.md index 72e238731..b9e32572b 100644 --- a/docs/accessibility-checklist.md +++ b/docs/accessibility-checklist.md @@ -23,6 +23,7 @@ This is a living document. Check items against VoiceOver (macOS), NVDA (Windows) ## Visual / Perceivable (WCAG 1.x) - [ ] All text passes 4.5:1 contrast ratio (use Colour Contrast Analyser) +- [ ] **Message action colors** (App → Appearance → Colors): with custom **message action** bar/button colors and **Show background** enabled, verify action icons/labels still meet **4.5:1** against the bar fill (and against chat bubble backgrounds when the bar is transparent) - [ ] Icon-only UI elements pass 3:1 against adjacent colors - [ ] Status dots have text alternative (not color-only) - [x] Charts (Recharts) have accessible text summary or table toggle — `TelemetryPanel` charts use `role="img"` + summarized `aria-label` (table toggle still optional) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index ef4f8f772..f80ab47c0 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -49,16 +49,16 @@ The panel also shows a breakdown of error and warning counts, and a "This node" ## 2. Routing Anomalies -Routing anomaly detection runs on Meshtastic nodes. MeshCore also has `hasHopCount: true` (hops via `outPathLen`), so the same hop-based anomalies run for MeshCore contacts; only protocols where `hasHopCount` is `false` (currently Reticulum) skip them. +Routing anomaly detection runs on Meshtastic nodes. MeshCore also has `hasHopCount: true` (hops via `outPathLen`), so hop-count-based findings that remain meaningful on MeshCore still run (`impossible_hop`, `route_flapping` / `path_instability`, `weak_link`). Distance-based GPS+hop heuristics (`hop_goblin`, close-in `bad_route` warning) are gated by `hasDistanceBasedHopAnomalies` — **true** for Meshtastic, **false** for MeshCore (nearby multi-hop contacts are poorly connected / repeater-mediated, not Meshtastic-style critical over-hopping). Protocols where `hasHopCount` is `false` (currently Reticulum) skip hop-based LoRa routing anomalies entirely. Detection is run in priority order; first match wins: 1. `impossible_hop` -2. `bad_route` (error variant) +2. `bad_route` (error variant — e.g. high duplication) 3. `route_flapping` / `path_instability` (MeshCore: prefers PathUpdated event timestamps when available) -4. `hop_goblin` +4. `hop_goblin` (Meshtastic only — requires `hasDistanceBasedHopAnomalies`) 5. `weak_link` (MeshCore only, requires `hasPerHopSnr` capability and a completed trace) -6. `bad_route` (warning variant) +6. `bad_route` (warning variant — close-in over-hopping; Meshtastic only — requires `hasDistanceBasedHopAnomalies`) Routing rows persist for up to **24 hours** by default (configurable 1–168 h in DiagnosticsPanel → Display Settings). @@ -429,7 +429,7 @@ SVG force-directed graph of nodes within direct reach (hops 0–1 from the conne On the **Reticulum** protocol tab, the **Diagnostics** panel includes a **Reticulum interface config** section (below continuous ping). It audits the sidecar rnsd config against the live RNS interface list and surfaces actionable repairs. -Runtime interface-issue rows from the sidecar latch (`interfaceIssueAlert`) are also folded into Diagnostics via `ReticulumDiagnosticEngine` — including TCP connect failures, TX queue drops, link-delivery timeouts, transport saturation, **`bleBondRemoved`** (stale OS Bluetooth bond for an RNode; Forget/re-pair), and **`blePairingTimedOut`** (OS passkey not entered within the TX-read window). +Runtime interface-issue rows from the sidecar latch (`interfaceIssueAlert`) are also folded into Diagnostics via `ReticulumDiagnosticEngine` — including TCP connect failures, TX queue drops (generic / **`txQueueDropsBle`** / **`txQueueDropsBleBondStale`** when the drop interface is a BLE RNode and optionally bond-stale), link-delivery timeouts, transport saturation, **`bleBondRemoved`** (stale OS Bluetooth bond for an RNode; Forget/Clear paired/re-pair — sticky until stack stop), and **`blePairingTimedOut`** (OS passkey not entered within the TX-read window). Additional runtime rows (refreshed from sidecar status + `reticulumPropagationStore`, not only the config audit poll): diff --git a/docs/meshcore-meshtastic-parity.md b/docs/meshcore-meshtastic-parity.md index 5fe68ea66..2250512a4 100644 --- a/docs/meshcore-meshtastic-parity.md +++ b/docs/meshcore-meshtastic-parity.md @@ -14,7 +14,7 @@ Shared UI gates use `ProtocolCapabilities` in [`src/renderer/lib/radio/BaseRadio | Area | Meshtastic | MeshCore | Gap type | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| Transports | BLE, Serial, HTTP (`@meshtastic/core`), WiFi/TCP fast path (`TransportTcpIpc`, port 4403) | BLE, Web Serial, TCP bridge (5000; main `meshcore:tcp-*`, tolerates peer FIN after contact burst, `TCP_NODELAY` + keepalive) | Different stacks | +| Transports | BLE, Serial, HTTP (`@meshtastic/core`), WiFi/TCP fast path (`TransportTcpIpc`, port 4403); launch auto-connect via `useProtocolRfAutoConnect` / `ProtocolAutoConnectCoordinator` + `protocolRfAutoConnectGate` | BLE, Web Serial, TCP bridge (5000; main `meshcore:tcp-*`, tolerates peer FIN after contact burst, `TCP_NODELAY` + keepalive); same RF auto-connect coordinator; TCP reconnect owned by `useMeshcoreRuntime` + `rfReconnectController` (conn side effects skip `handleConnectionLost` for TCP) | Different stacks | | Tab “Modules” / “Repeaters” | `ModulePanel` (protobuf modules; Remote Hardware GPIO, IP Tunnel status) | `RepeatersPanel` (trace, status, neighbors) | Product split | | Tab “Administration” | `AdminPanel` (reboot, shutdown, factory reset, NodeDB reset, OTA/DFU) | `AdminPanel` (reboot via companion; meshcore.js limits for shutdown/factory/OTA) | **App** (implemented; reboot wired; extended admin capability-limited) | | MQTT broker UI | Full (with transport selection) | Same broker fields; transport protocol selected when connecting; MeshCore-only **LetsMesh** / **MeshMapper** / **Colorado Mesh** / **Ripple** / **Custom** presets fill known public brokers | **Post-MQTT** codec on broker path | diff --git a/docs/reticulum-games-parity.md b/docs/reticulum-games-parity.md index c428e9e20..646d0cd17 100644 --- a/docs/reticulum-games-parity.md +++ b/docs/reticulum-games-parity.md @@ -7,7 +7,7 @@ Living matrix for [issue #773](https://github.com/Colorado-Mesh/mesh-client/issu Update this file when Games PRs land. `pnpm run update` warns on new Ratspeak releases with stub-kind `games-parity`. -**Last review:** 2026-08-05 (delivery_state / envelope persist / optimistic board / Chess promotion+claims). +**Last review:** 2026-08-06 (delivery_state / envelope persist / optimistic board / Chess promotion+claims). Status: `done` | `partial` | `wontfix` | `todo` diff --git a/docs/reticulum-sidecar-ipc.md b/docs/reticulum-sidecar-ipc.md index 40dee7236..78ddada71 100644 --- a/docs/reticulum-sidecar-ipc.md +++ b/docs/reticulum-sidecar-ipc.md @@ -298,7 +298,7 @@ Renderer calls `electronAPI.reticulum.*`; main process proxies to this API (sand | `reticulum:gamesStatus` / `gamesApps` / `gamesSessions` / … | Dedicated LRGP games IPC (~600/min); generic proxy rejects `/api/v1/games/*` | | `electronAPI.reticulum.games.*` | Preload: `getStatus` / `listApps` / `listSessions` / `getSession` / `sendAction` / `resend` / `markRead` / `deleteSession` | -`getStatus` / `onStatus` may include `interfaceIssueAlert` (TCP connect failures, TX queue drops, link-delivery timeouts, transport saturation / slow queries, **`bleBondRemoved`** stale RNode bonds, **`blePairingTimedOut`** OS passkey / TX-read timeouts). Per-entry latch timestamps use a **5-minute** stale window (`RETICULUM_INTERFACE_ISSUE_ALERT_STALE_MS`). Connection syncs **enabled** interface names via `syncInterfaceIssueScope` so disabling or removing an interface clears that name immediately and rejects re-latch from lagging log lines. Stopping the stack (or unexpected process exit) clears the tracker. +`getStatus` / `onStatus` may include `interfaceIssueAlert` (TCP connect failures, TX queue drops — including BLE / bond-stale cause keys used by Diagnostics and Connection hints, link-delivery timeouts, transport saturation / slow queries, **`bleBondRemoved`** stale RNode bonds (sticky until stack stop / interface remove; not pruned solely by the 5‑minute window), **`blePairingTimedOut`** OS passkey / TX-read timeouts). Per-entry latch timestamps use a **5-minute** stale window (`RETICULUM_INTERFACE_ISSUE_ALERT_STALE_MS`) for most issues; Connection syncs **enabled** interface names via `syncInterfaceIssueScope` so disabling or removing an interface clears that name immediately and rejects re-latch from lagging log lines. Stopping the stack (or unexpected process exit) clears the tracker. **`propagation_sync` WebSocket payload:** `{ active: boolean, progress: number, message: string | null }`. Progress uses 0–100 (Establishing ≈10, Offering ≈25, …, Complete ≈100). Sticky success after HaveAll emits `active:false, progress:100`; cancel/stall/failure emit `active:false, progress:0` (and must not emit a trailing 100). Sync `POST /api/v1/propagation/sync` may return `PROPAGATION_IDENTITY_UNKNOWN`, `PROPAGATION_TARGET_NOT_PN`, `PROPAGATION_PEERING_STAMP_FAILED`, `PROPAGATION_PEER_COST_EXCEEDS_MAX`, or `LOCAL_PROPAGATION_SYNC_UNSUPPORTED`. Add may return `PROPAGATION_OFFER_UNSUPPORTED` / probe timeout failures. diff --git a/docs/reticulum.md b/docs/reticulum.md index 7474532ce..b8c8755b8 100644 --- a/docs/reticulum.md +++ b/docs/reticulum.md @@ -242,7 +242,7 @@ When multiple enabled local RNode interfaces are connected, the interface list s **Noble BLE yield (macOS/Windows):** When the Reticulum config includes an **enabled BLE RNode** (`ble://…`), sidecar start calls `bleCoexistence:suspendNobleForReticulumBleConnect` — Noble disconnects GATT sessions and holds the scan mutex until the RNode connects or a grace window expires. While `scanOwner === 'reticulum'`, Meshtastic/MeshCore Noble **connect is rejected** (`BleScanBusyError`). After the post-connect grace, yield **stops re-contending** (prepare backoff) so an offline RNode cannot thrash LoRa BLE. mesh-client then dispatches `mesh-client:nobleBleYieldReleased` so Meshtastic/MeshCore can reconnect. If Noble disconnect times out during suspend, the yield is **released** (fail closed) rather than leaving a half-held mutex. Yield lifecycle is owned exclusively by `useReticulumNobleBleYieldWatcher` (not `useReticulumInterfaceSnapshot`) with a shared grace clock (`reticulumBleConnectGrace.ts`) so sidecar pairing is not interrupted. On stack restart, grace may renew when main re-acquires the scan mutex. Meshtastic/MeshCore autostart waits up to ~**65 s** (`awaitReticulumBleCoexistenceClear`) when Reticulum auto-starts with a BLE RNode. -**Stale BLE bond:** Sidecar may latch `bleBondRemoved` when the peer dropped pairing information while the OS still shows Paired. Connection / Diagnostics surface Forget-and-re-pair copy — forget the RNode in System Settings → Bluetooth, start pairing on the radio, **remove and re-add** the BLE interface (**Pick device**) so the saved `ble://` id refreshes, restart the stack, enter the new PIN. +**Stale BLE bond:** Sidecar may latch `bleBondRemoved` when the peer dropped pairing information while the OS still shows Paired (bond-desync overlay also **halts** BLE reconnect until stack restart). Connection / Diagnostics / TX-drop bond-stale hints surface Forget-and-re-pair copy — forget the RNode in System Settings → Bluetooth first, prefer USB/`tcp://` when possible, use Admin → Bluetooth → **Clear paired devices** (USB `CMD_BT_UNPAIR`, ESP32) then **Start pairing**, **remove and re-add** the BLE interface (**Pick device**) so the saved `ble://` id refreshes, restart the stack, enter the new PIN. Connection banners link to Admin Bluetooth. **Pairing timeout:** Sidecar may latch `blePairingTimedOut` when the OS passkey was not entered within ~60s. Admin **Start pairing** shows the PIN in the Admin panel over USB (radio display may stay blank); do not use Meshtastic’s `123456` default. The RNode need not appear in System Settings before mesh-client connects. @@ -283,6 +283,7 @@ When multiple enabled local RNode interfaces are connected, the interface list s - **Replies:** outbound DMs stamp LXMF `FIELD_REPLY_TO` (0x30) and optional `FIELD_REPLY_QUOTE` (0x31, capped) before sign so peers see structured replies; ingest/Chat use `reticulum_reply_to_hash` plus quote preview (store parent when present, else wire quote) and jump-to-parent by message hash - Outbound **Sending** until sidecar emits `lxmf_outbound_status` (`delivered` / `failed`); `/api/v1/lxmf/send` may return `delivery_status: "queued"` or `"sending"` — that is enqueue/acceptance, not delivery confirmation. On Direct failure with a preferred **remote** PN, the sidecar re-emits `sending` with `delivery_method: "propagated"` for the one-shot PN deposit. **Exception — paper:** Chat DM **Share as paper** (`createReticulumPaperMessage` → `POST /api/v1/lxmf/paper/create`) encrypts offline to a QR/`lxm://` URI with **no network send**; Completes immediately (`delivery_method: paper`, badge **Paper**) and does **not** use `lxmf_outbound_status`. Ingest via Chat **Scan paper**, Network **Scan / import**, or OS `lxm://` (`POST /api/v1/lxmf/paper/ingest` — HTTP `message` fallback-ingested when WS lags). Create needs peer pubkey (`identity_unknown` otherwise); ingest needs matching local identity (`decrypt_failed` otherwise); size-capped (`paper_too_large`). - Terminal **Completes** / **Failed** from `lxmf_outbound_status` are persisted to SQLite (`delivery_status` + `delivery_method` on `reticulum_messages`) via `applyReticulumOutboundDeliveryStatus.ts` so restart/DB hydration keeps PN vs Direct labeling; early WS events before provisional id→hash rekey are buffered +- **Optimistic pending rekey:** Chat send creates a `reticulum-pending-*` row; when the sidecar returns the real `message_hash`, ingest/SQLite upsert passes `replaces_message_hash` so the pending row is deleted atomically (avoids orphan Sending duplicates) - Propagated Completes render as **Stored at propagation node** (PN badge) — not recipient **Delivered** - **DM path reachability:** active DM header shows a reachability badge (`ReticulumDmPathReachabilityBadge` + `useReticulumDmPathProbe`) seeded from path-table/contact hops, then settled by peer probe; when settled, **Request path** / **Probe** use the same sidecar endpoints as the Peers tab. Chat **Probe** mirrors Peer List UX: stack-running check → `/probe` → toast → peer refresh; `onProbeSettled` / `applyProbeResult(forHash, …)` applies the result without a second `/probe` (stale hashes after DM switch are ignored); manual reprobe forces Checking… even when passive hops already look reachable @@ -380,10 +381,10 @@ Firmware `.zip` files are selected locally (no in-app GitHub download). Disconne ### SQLite (main process) -| Table | Contents | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `reticulum_destinations` | Destination meta (canonical 32-hex hash, display name, favorited, `icon_name`, `icon_color`, `last_heard` for History, `is_contact` for saved Contacts) | -| `reticulum_messages` | LXMF history (`message_hash`, `reply_to_hash`, `delivery_status` — `sending`/`queued`/`pending`/`delivered`/`failed`; `delivery_method` — `direct`/`propagated`/`opportunistic`/`paper`; `received_via` atoms include `rf`/`ble`/`tcp`/`network`/`mqtt`/`both`/`paper`; terminal outbound status written on `lxmf_outbound_status` except paper Completes from create/ingest; stale `sending` rows marked failed on startup) | +| Table | Contents | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `reticulum_destinations` | Destination meta (canonical 32-hex hash, display name, favorited, `icon_name`, `icon_color`, `last_heard` for History, `is_contact` for saved Contacts) | +| `reticulum_messages` | LXMF history (`message_hash`, `reply_to_hash`, `delivery_status` — `sending`/`queued`/`pending`/`delivered`/`failed`; `delivery_method` — `direct`/`propagated`/`opportunistic`/`paper`; `received_via` atoms include `rf`/`ble`/`tcp`/`network`/`mqtt`/`both`/`paper`; terminal outbound status written on `lxmf_outbound_status` except paper Completes from create/ingest; stale `sending` rows marked failed on startup; optional `replaces_message_hash` on upsert deletes the prior optimistic pending hash) | ### Sidecar `userData` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index fb0957727..9b5f5fd18 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -417,6 +417,14 @@ flatpak run org.coloradomesh.MeshClient ## Database and local data +### Database schema upgrade (forward — first launch after a newer build) + +**Symptom**: On first launch after installing a newer release, a blocking **Quit / Upgrade** dialog asks to confirm an irreversible SQLite schema upgrade. Packaged installers may also ship `SCHEMA-UPGRADE.txt` in the app resources. + +**Cause**: This build’s `user_version` is higher than the existing profile database. + +**Fix**: Choose **Upgrade** to migrate (cannot roll back with an older app against the same profile), or **Quit** and restore a backup / use a fresh profile. CI/E2E may set `MESH_CLIENT_ACCEPT_SCHEMA_UPGRADE=1` to skip the dialog (dev/unpackaged only when restricted). See [release-process.md](release-process.md) for installer notice text. + ### Database schema newer than this app (downgrade blocked) **Symptom**: On launch, a **Startup Error** dialog says the database was upgraded by a newer Mesh-Client, or **Import blocked** when merging a `.db` file. @@ -784,6 +792,8 @@ When the Meshtastic SDK logs a routing / queue failure, mesh-client intercepts m 2. **Disconnect → Connect** on the Connection panel. 3. Confirm logs show `[useMeshcoreRuntime] initConn getContacts …` completing and nodes populating. Main uses `TCP_NODELAY` + keepalive on the `meshcore:tcp-*` bridge. +**Reconnect ownership:** TCP disconnect/reconnect is owned by `useMeshcoreRuntime` + `rfReconnectController` (single-owner scheduler). Conn side effects **skip** `handleConnectionLost` when `connectType === 'tcp'` so the runtime `meshcore:tcp-disconnected` listener does not double-enter the reconnect scheduler. + ### MeshCore contact delete and sticky Rooms badge - Deleting a contact from Chat/Contacts removes the SQLite contact row **and** room BBS messages for that `room_server_id` (so Rooms unread cannot outlive the room server). @@ -1224,12 +1234,21 @@ TCP/network Nomad Links use path-scaled initiator hops (`link_hops = clamp(path_ **Fix**: -1. Forget the RNode in System Settings → Bluetooth. -2. Start pairing on the radio (Admin → Bluetooth, or ~7 s button hold). -3. **Remove and re-add** the BLE interface (**Pick device**) so the saved `ble://` id refreshes. -4. Restart the Reticulum stack and enter the new 6-digit PIN when prompted. +1. Forget the RNode in System Settings → Bluetooth (macOS will not clear Paired automatically). +2. Prefer USB serial or Wi‑Fi (`tcp://`) when possible to avoid BLE bonds. +3. On Admin → Bluetooth: **Clear paired devices** (USB `CMD_BT_UNPAIR`, ESP32) if available, then **Start pairing**. Connection issue banners also link to Admin Bluetooth. +4. **Remove and re-add** the BLE interface (**Pick device**) so the saved `ble://` id refreshes. +5. Restart the Reticulum stack and enter the new 6-digit PIN when prompted. + +Bond-stale **TX queue full** hints (`txQueueDropsHintBleBondStale`) point at the same Forget / Clear paired / Start pairing path. Sidecar overlay `rsReticulum-ble-rnode-bond-desync` stops BLE reconnect until stack restart when bond removal is detected — see [reticulum-sidecar/patches/README.md](../reticulum-sidecar/patches/README.md). `bleBondRemoved` stays sticky until stack stop / interface remove (not only the generic 5‑minute log latch). + +### Reticulum LXMF duplicate Sending / orphaned pending rows + +**Symptoms**: Chat shows a stuck `reticulum-pending-*` Sending row beside the real LXMF hash row after send completes. + +**Cause**: Optimistic pending id was not deleted when the sidecar assigned `message_hash`. -### Reticulum attachment image not showing in Chat +**Fix**: Upgrade to a build that passes `replaces_message_hash` on SQLite upsert (`db:saveReticulumMessage` deletes the prior pending hash atomically). Restarting the app also marks stale `sending` rows failed on startup. **Symptoms**: LXMF `[file:…:image/…]` bubble shows the filename label but no inline image. diff --git a/patches/@liamcottle__meshcore.js@1.13.0.patch b/patches/@liamcottle__meshcore.js@1.13.0.patch index 5df62529d..d5eeae03a 100644 --- a/patches/@liamcottle__meshcore.js@1.13.0.patch +++ b/patches/@liamcottle__meshcore.js@1.13.0.patch @@ -15,22 +15,10 @@ index 9142fbc9fda24c07082d63ced24117a35b29dc93..2f8cf2d68d1c32c1d7cc489f33527c9a readCString(maxLength) { diff --git a/src/connection/connection.js b/src/connection/connection.js -index af6f6d670adc0948d3453df34acbbde46c1b4250..337c18dbf16796a21c9834578589a583a6364f42 100644 +index af6f6d670adc0948d3453df34acbbde46c1b4250..2fb915e4e5cf109afc7772dc380bb016e65af7dd 100644 --- a/src/connection/connection.js +++ b/src/connection/connection.js -@@ -231,7 +231,10 @@ class Connection extends EventEmitter { - const data = new BufferWriter(); - data.writeByte(Constants.CommandCodes.SendLogin); - data.writeBytes(publicKey); // 32 bytes - id of repeater or room server -- data.writeString(password); // password is remainder of frame, max 15 characters -+ // Empty password = read-only ACL (0 bytes, matches official Android). Non-empty uses writeString. -+ if (password.length > 0) { -+ data.writeString(password); -+ } - await this.sendToRadioFrame(data.toBytes()); - } - -@@ -338,6 +341,14 @@ class Connection extends EventEmitter { +@@ -338,6 +338,14 @@ class Connection extends EventEmitter { await this.sendToRadioFrame(data.toBytes()); } @@ -45,16 +33,7 @@ index af6f6d670adc0948d3453df34acbbde46c1b4250..337c18dbf16796a21c9834578589a583 onFrameReceived(frame) { // emit received frame -@@ -400,6 +411,8 @@ class Connection extends EventEmitter { - this.onRawDataPush(bufferReader); - } else if(responseCode === Constants.PushCodes.LoginSuccess){ - this.onLoginSuccessPush(bufferReader); -+ } else if(responseCode === Constants.PushCodes.LoginFail){ -+ this.onLoginFailPush(bufferReader); - } else if(responseCode === Constants.PushCodes.StatusResponse){ - this.onStatusResponsePush(bufferReader); - } else if(responseCode === Constants.PushCodes.LogRxData){ -@@ -412,8 +425,14 @@ class Connection extends EventEmitter { +@@ -412,8 +420,14 @@ class Connection extends EventEmitter { this.onNewAdvertPush(bufferReader); } else if(responseCode === Constants.PushCodes.BinaryResponse){ this.onBinaryResponsePush(bufferReader); @@ -70,21 +49,7 @@ index af6f6d670adc0948d3453df34acbbde46c1b4250..337c18dbf16796a21c9834578589a583 } } -@@ -459,6 +476,13 @@ class Connection extends EventEmitter { - }); - } - -+ onLoginFailPush(bufferReader) { -+ this.emit(Constants.PushCodes.LoginFail, { -+ reserved: bufferReader.readByte(), // reserved -+ pubKeyPrefix: bufferReader.readBytes(6), // 6 bytes of public key this login fail is from -+ }); -+ } -+ - onStatusResponsePush(bufferReader) { - this.emit(Constants.PushCodes.StatusResponse, { - reserved: bufferReader.readByte(), // reserved -@@ -494,15 +518,25 @@ class Connection extends EventEmitter { +@@ -494,15 +508,25 @@ class Connection extends EventEmitter { onTraceDataPush(bufferReader) { const reserved = bufferReader.readByte(); const pathLen = bufferReader.readUInt8(); @@ -116,7 +81,7 @@ index af6f6d670adc0948d3453df34acbbde46c1b4250..337c18dbf16796a21c9834578589a583 }); } -@@ -582,11 +616,30 @@ class Connection extends EventEmitter { +@@ -582,11 +606,30 @@ class Connection extends EventEmitter { } onDeviceInfoResponse(bufferReader) { @@ -151,7 +116,7 @@ index af6f6d670adc0948d3453df34acbbde46c1b4250..337c18dbf16796a21c9834578589a583 }); } -@@ -2375,6 +2428,33 @@ class Connection extends EventEmitter { +@@ -2375,6 +2418,33 @@ class Connection extends EventEmitter { }); } diff --git a/patches/README.md b/patches/README.md index fc3aab71c..5c06162ef 100644 --- a/patches/README.md +++ b/patches/README.md @@ -4,7 +4,7 @@ Local overlays applied via `pnpm-workspace.yaml` → `patchedDependencies`. When | Patch | Upstream | Upstream PR / status | | ----- | -------- | -------------------- | -| `@liamcottle__meshcore.js@1.13.0.patch` | [meshcore-dev/meshcore.js](https://github.com/meshcore-dev/meshcore.js) | Split: [#29](https://github.com/meshcore-dev/meshcore.js/pull/29), [#30](https://github.com/meshcore-dev/meshcore.js/pull/30), [#31](https://github.com/meshcore-dev/meshcore.js/pull/31), [#32](https://github.com/meshcore-dev/meshcore.js/pull/32), [#33](https://github.com/meshcore-dev/meshcore.js/pull/33) | +| `@liamcottle__meshcore.js@1.13.0.patch` | [meshcore-dev/meshcore.js](https://github.com/meshcore-dev/meshcore.js) | Open: [#30](https://github.com/meshcore-dev/meshcore.js/pull/30), [#31](https://github.com/meshcore-dev/meshcore.js/pull/31), [#33](https://github.com/meshcore-dev/meshcore.js/pull/33); [#29](https://github.com/meshcore-dev/meshcore.js/pull/29) closed (unnecessary); [#32](https://github.com/meshcore-dev/meshcore.js/pull/32) closed (not carried — firmware does not push LoginFail) | | `@jsr__meshtastic__core@2.6.6.patch` | [meshtastic/web](https://github.com/meshtastic/web) (`packages/sdk`) | [#1312](https://github.com/meshtastic/web/pull/1312) | | `@jsr__meshtastic__transport-web-serial@0.2.5.patch` | [meshtastic/web](https://github.com/meshtastic/web) (`packages/transport-web-serial`) | Fixed on upstream `main` (per-instance `toDeviceStream` + abort); keep patch until npm/`@jsr` package bump includes it | | `usb@2.18.0.patch` | [node-usb/node-usb](https://github.com/node-usb/node-usb) | [#964](https://github.com/node-usb/node-usb/pull/964) | @@ -13,21 +13,21 @@ Local overlays applied via `pnpm-workspace.yaml` → `patchedDependencies`. When ## @liamcottle/meshcore.js@1.13.0 -Protocol / companion-radio fixes. Upstreamed as five focused PRs (npm package name remains `@liamcottle/meshcore.js`; repo lives under `meshcore-dev`). +Protocol / companion-radio fixes. Upstreamed as focused PRs (npm package name remains `@liamcottle/meshcore.js`; repo lives under `meshcore-dev`). -| PR | Change | -| -- | ------ | -| [#29](https://github.com/meshcore-dev/meshcore.js/pull/29) | Empty login password → zero-byte payload (read-only ACL) | -| [#30](https://github.com/meshcore-dev/meshcore.js/pull/30) | TraceData SNR count from `path_sz` flags | -| [#31](https://github.com/meshcore-dev/meshcore.js/pull/31) | DeviceInfo v3+ fields + `setPathHashMode` (cmd 61) | -| [#32](https://github.com/meshcore-dev/meshcore.js/pull/32) | `LoginFail` (0x86) push handler | -| [#33](https://github.com/meshcore-dev/meshcore.js/pull/33) | `readString` stops at embedded NUL | +| PR | Change | Status | +| -- | ------ | ------ | +| [#29](https://github.com/meshcore-dev/meshcore.js/pull/29) | Empty login password → zero-byte payload (read-only ACL) | **Closed unnecessary** — stock `writeString("")` already emits 0 bytes; hunk removed from this patch. Room/repeater login uses in-app `buildSendLoginFrame`. | +| [#30](https://github.com/meshcore-dev/meshcore.js/pull/30) | TraceData SNR count from `path_sz` flags | Open — still in this patch | +| [#31](https://github.com/meshcore-dev/meshcore.js/pull/31) | DeviceInfo v3+ fields + `setPathHashMode` (cmd 61) | Open — still in this patch | +| [#32](https://github.com/meshcore-dev/meshcore.js/pull/32) | `LoginFail` (0x86) push handler | **Closed** — not carried; stock room/repeater firmware does not reply on bad credentials (client timeout). Hunk removed from this patch. | +| [#33](https://github.com/meshcore-dev/meshcore.js/pull/33) | `readString` stops at embedded NUL | Open — still in this patch | **Kept local-only (not upstreamed):** silence companion push codes `25` / `0x8E` / `0x8F`, and downgrade unhandled-frame `console.log` → `console.debug`. ### Sunset -When the five PRs merge and a release newer than `1.13.0` includes them, drop the corresponding hunks (or the whole patch if only local-only hunks remain), bump the dependency, and remove this entry from `WATCH_ENTRIES` if no patch remains. +When [#30](https://github.com/meshcore-dev/meshcore.js/pull/30), [#31](https://github.com/meshcore-dev/meshcore.js/pull/31), and [#33](https://github.com/meshcore-dev/meshcore.js/pull/33) merge and a release newer than `1.13.0` includes them, drop the corresponding hunks (or the whole patch if only local-only hunks remain), bump the dependency, and remove this entry from `WATCH_ENTRIES` if no patch remains. Do not re-add the #29 empty-password or #32 LoginFail hunks. ## @jsr/meshtastic__core@2.6.6 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b02f3e0b6..a3a1f668e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ overrides: patchedDependencies: '@jsr/meshtastic__core@2.6.6': 93604a080fa754cbde3dc78ef46ee50c2996c34d6d85720acacecfd6ebd7c652 '@jsr/meshtastic__transport-web-serial@0.2.5': 27a2418bae8605e0e5ab6f1fcfd391abd9dc3110433da5754e934f38475dab74 - '@liamcottle/meshcore.js@1.13.0': 1f95835871af6026efa9def5d5bed54749cdb34ff0ff013a9d52005db9db3220 + '@liamcottle/meshcore.js@1.13.0': 9e66b8389ac6dcbd308a7498167c5440d25e51a670837a039cac0c0aca368e44 debug@4.4.3: cf37fa96f5df733456b16c82c9e1c9054a92f6216692aa33d5e2e1e208888e37 readable-stream@4.7.0: 96023d9278085d7490d08bce1a5079dd202f7782a0daa66fa81c6b1424ef8ab1 usb@2.18.0: 6b746e2d49b9b006a88aec5bed7a13c629d7f5ba7b40e9f1e039136754c32533 @@ -128,7 +128,7 @@ importers: version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@liamcottle/meshcore.js': specifier: ^1.13.0 - version: 1.13.0(patch_hash=1f95835871af6026efa9def5d5bed54749cdb34ff0ff013a9d52005db9db3220)(supports-color@8.1.1) + version: 1.13.0(patch_hash=9e66b8389ac6dcbd308a7498167c5440d25e51a670837a039cac0c0aca368e44)(supports-color@8.1.1) '@meshtastic/core': specifier: npm:@jsr/meshtastic__core@^2.6.6 version: '@jsr/meshtastic__core@2.6.6(patch_hash=93604a080fa754cbde3dc78ef46ee50c2996c34d6d85720acacecfd6ebd7c652)(buffer@6.0.3)' @@ -5237,7 +5237,7 @@ snapshots: transitivePeerDependencies: - buffer - '@liamcottle/meshcore.js@1.13.0(patch_hash=1f95835871af6026efa9def5d5bed54749cdb34ff0ff013a9d52005db9db3220)(supports-color@8.1.1)': + '@liamcottle/meshcore.js@1.13.0(patch_hash=9e66b8389ac6dcbd308a7498167c5440d25e51a670837a039cac0c0aca368e44)(supports-color@8.1.1)': dependencies: '@noble/curves': 1.9.7 serialport: 13.0.0(supports-color@8.1.1) diff --git a/reticulum-sidecar/patches/README.md b/reticulum-sidecar/patches/README.md index f6be25472..955f7b76a 100644 --- a/reticulum-sidecar/patches/README.md +++ b/reticulum-sidecar/patches/README.md @@ -243,6 +243,26 @@ git diff -- crates/rns-interface/src/ble_rnode.rs \ When [ratspeak/rsReticulum#20](https://github.com/ratspeak/rsReticulum/pull/20) merges and floated `origin/main` includes it, remove this patch and drop the apply step from `clone-ratspeak-stack.sh` / `ensure-rsReticulum-patches.sh`. +## rsReticulum-ble-rnode-bond-desync.patch + +Halt BLE RNode reconnect when CoreBluetooth reports **Peer removed pairing information**, and skip the TX-char SMP probe on reconnect after a successful session in the same task (fall back to pairing if subscribe fails with auth). Apply **after** the pairing-transition debounce overlay. + +| Field | Value | +| ----- | ----- | +| **Base commit** | tip after `rsReticulum-ble-rnode-pairing-transition-debounce.patch` | +| **Upstream PR** | (mesh-client local; no upstream PR yet) | + +**Modifies (1 file):** + +- `crates/rns-interface/src/ble_rnode.rs` — `is_bond_removed_error`, `session_already_bonded`, reconnect halt + +### Apply locally + +```bash +./scripts/apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh +./scripts/apply-rsReticulum-ble-rnode-bond-desync.sh +``` + ## rsReticulum-discovery-announce-egress.patch Register `rnstransport.discovery.interface` as a local destination before announcing, and defer `Announcer::register` until the discoverable interface online latch is true. Without this, Boundary hubs such as **rmap.world** silently drop discovery announces (non-local + no path), and BLE RNode can consume a multi-hour `announce_interval` on a no-op TX while still connecting. diff --git a/reticulum-sidecar/patches/rsReticulum-ble-rnode-bond-desync.patch b/reticulum-sidecar/patches/rsReticulum-ble-rnode-bond-desync.patch new file mode 100644 index 000000000..10d50ebb7 --- /dev/null +++ b/reticulum-sidecar/patches/rsReticulum-ble-rnode-bond-desync.patch @@ -0,0 +1,165 @@ +diff --git a/crates/rns-interface/src/ble_rnode.rs b/crates/rns-interface/src/ble_rnode.rs +index 1111111..2222222 100644 +--- a/crates/rns-interface/src/ble_rnode.rs ++++ b/crates/rns-interface/src/ble_rnode.rs +@@ -190,6 +190,14 @@ + ) + } + ++/// CoreBluetooth / WinRT: peripheral cleared LTK while the OS still shows Paired. ++fn is_bond_removed_error(error: &InterfaceError) -> bool { ++ matches!( ++ error, ++ InterfaceError::SendFailed(message) if message.contains("Peer removed pairing information") ++ ) ++} ++ + /// Android's native bridge can come up immediately after SMP completes, while + /// rsCardputer's RNode BLE stack is still settling. Probe detect a few times + /// inside one connection attempt so a single dropped early frame does not cost +@@ -1319,6 +1327,7 @@ + async fn connect_rnode( + adapter: &Adapter, + ble_uri: &str, ++ session_already_bonded: bool, + ) -> Result { + ble_diag(format!("[ble] connect_rnode start uri={ble_uri}")); + let peripheral = resolve_ble_target(adapter, ble_uri).await?; +@@ -1403,20 +1412,68 @@ + // kills any pending subscribe. iOS/macOS share CoreBluetooth; Windows + // (WinRT) and Android auto-prompt and retry on encrypted-char reads. + // Linux used explicit BlueZ pairing before `connect()`, above. ++ // ++ // When this process already completed a successful bond/session, skip the ++ // TX-char SMP probe on reconnect so we do not re-fire the OS passkey ++ // dialog (or stress fragile bonds). Fall back to pairing if subscribe ++ // fails with an auth/encryption error. + #[cfg(any( + target_os = "ios", + target_os = "macos", + target_os = "windows", + target_os = "android" + ))] +- desktop_trigger_pairing(&peripheral, &tx_char).await?; ++ if !session_already_bonded { ++ desktop_trigger_pairing(&peripheral, &tx_char).await?; ++ } + + ble_diag("[ble] subscribe TX start"); +- peripheral.subscribe(&tx_char).await.map_err(|e| { +- ble_diag(format!("[ble] subscribe TX err: {e}")); +- InterfaceError::SendFailed(format!("BLE subscribe TX: {e}")) +- })?; +- ble_diag("[ble] subscribe TX ok"); ++ match peripheral.subscribe(&tx_char).await { ++ Ok(()) => { ++ ble_diag("[ble] subscribe TX ok"); ++ } ++ Err(e) => { ++ #[cfg(any( ++ target_os = "ios", ++ target_os = "macos", ++ target_os = "windows", ++ target_os = "android" ++ ))] ++ if session_already_bonded { ++ let msg = format!("{e}"); ++ let needs_pair = msg.to_lowercase().contains("auth") ++ || msg.to_lowercase().contains("encrypt") ++ || msg.to_lowercase().contains("insufficient"); ++ if needs_pair { ++ ble_diag(format!( ++ "[ble] subscribe TX err after bonded session ({e}) — re-triggering SMP" ++ )); ++ desktop_trigger_pairing(&peripheral, &tx_char).await?; ++ peripheral.subscribe(&tx_char).await.map_err(|e2| { ++ ble_diag(format!("[ble] subscribe TX err after re-pair: {e2}")); ++ InterfaceError::SendFailed(format!("BLE subscribe TX: {e2}")) ++ })?; ++ ble_diag("[ble] subscribe TX ok after re-pair"); ++ } else { ++ ble_diag(format!("[ble] subscribe TX err: {e}")); ++ return Err(InterfaceError::SendFailed(format!("BLE subscribe TX: {e}"))); ++ } ++ } else { ++ ble_diag(format!("[ble] subscribe TX err: {e}")); ++ return Err(InterfaceError::SendFailed(format!("BLE subscribe TX: {e}"))); ++ } ++ #[cfg(not(any( ++ target_os = "ios", ++ target_os = "macos", ++ target_os = "windows", ++ target_os = "android" ++ )))] ++ { ++ ble_diag(format!("[ble] subscribe TX err: {e}")); ++ return Err(InterfaceError::SendFailed(format!("BLE subscribe TX: {e}"))); ++ } ++ } ++ } + + // 244 = ATT MTU 247 - 3-byte header. Larger writes silently drop on + // peripherals with smaller negotiated MTU; 512 (GATT ceiling) isn't +@@ -1963,6 +2020,9 @@ + let mut tries: usize = 0; + let mut backoff = RECONNECT_WAIT; + let mut initial_attempt = true; ++ // After one successful GATT+SMP session, skip TX-char pairing probe on ++ // reconnect unless subscribe fails with auth (desktop bond-aware path). ++ let mut session_already_bonded = false; + + // Drop guard: every early return must clear the running-flag map + // entry, or stale entries confuse later spawns reusing the id. +@@ -2009,9 +2069,25 @@ + } + }; + +- let conn = match connect_rnode(&adapter, &ble_uri).await { +- Ok(c) => c, ++ let conn = match connect_rnode(&adapter, &ble_uri, session_already_bonded).await { ++ Ok(c) => { ++ session_already_bonded = true; ++ c ++ } + Err(e) => { ++ if is_bond_removed_error(&e) { ++ snapshot_publisher.connection_attempt_failed(); ++ tracing::warn!( ++ name = %log_name, ++ error = %e, ++ "BLE RNode bond removed — stopping reconnect until stack restart (Forget OS bond + re-pair)" ++ ); ++ ble_diag(format!( ++ "[ble] connect_rnode bond-removed: {e} — exiting reconnect loop" ++ )); ++ snapshot_publisher.stopped(RNodeRuntimeReason::DriverTerminated); ++ return; ++ } + let pairing_transition = is_pairing_transition_error(&e); + let retry_wait = if pairing_transition { + PAIRING_TRANSITION_RETRY_WAIT +@@ -5432,7 +5508,7 @@ + #[ignore] + async fn test_ble_connect_to_rnode() { + let adapter = get_adapter().await.expect("No BLE adapter"); +- let conn = connect_rnode(&adapter, "ble://") ++ let conn = connect_rnode(&adapter, "ble://", false) + .await + .expect("No RNode found. Pair an RNode first."); + assert!(conn.peripheral.is_connected().await.unwrap_or(false)); +@@ -5466,4 +5542,15 @@ + "BLE device not found: RNode".into() + ))); + } ++ ++ #[test] ++ fn test_bond_removed_error_detected() { ++ assert!(is_bond_removed_error(&InterfaceError::SendFailed( ++ "BLE connect failed after 3 attempts: Runtime Error: Peer removed pairing information" ++ .into() ++ ))); ++ assert!(!is_bond_removed_error(&InterfaceError::SendFailed( ++ "BLE pairing in progress: Authentication required".into() ++ ))); ++ } + } diff --git a/reticulum-sidecar/src/api/mod.rs b/reticulum-sidecar/src/api/mod.rs index 30f56c02e..89326d34c 100644 --- a/reticulum-sidecar/src/api/mod.rs +++ b/reticulum-sidecar/src/api/mod.rs @@ -246,6 +246,10 @@ pub fn router(stack: Arc) -> Router { ) .route("/api/v1/remote/identity", get(remote::remote_identity)) .route("/api/v1/stack/restart", post(system::stack_restart)) + .route( + "/api/v1/stack/prepare-stop", + post(system::stack_prepare_stop), + ) .route("/api/v1/system/factory-reset", post(system::factory_reset)) .route("/api/v1/diagnostics", get(system::diagnostics)) .route("/api/v1/voice/status", get(voice::voice_status)) diff --git a/reticulum-sidecar/src/api/system.rs b/reticulum-sidecar/src/api/system.rs index ee1846477..1de32e554 100644 --- a/reticulum-sidecar/src/api/system.rs +++ b/reticulum-sidecar/src/api/system.rs @@ -12,6 +12,14 @@ pub async fn stack_restart(State(stack): State>) -> Json>) -> Json { + match stack.prepare_stop().await { + Ok(()) => Json(serde_json::json!({ "ok": true })), + Err(e) => Json(serde_json::json!({ "ok": false, "error": e })), + } +} + pub async fn factory_reset(State(stack): State>) -> Json { match stack.factory_reset().await { Ok(()) => Json(serde_json::json!({ "ok": true })), diff --git a/reticulum-sidecar/src/stack/games_outbound_store.rs b/reticulum-sidecar/src/stack/games_outbound_store.rs index ad5c93f40..68903f191 100644 --- a/reticulum-sidecar/src/stack/games_outbound_store.rs +++ b/reticulum-sidecar/src/stack/games_outbound_store.rs @@ -10,6 +10,8 @@ use rusqlite::{Connection, OptionalExtension, params}; /// Cap rows so abandoned sessions cannot retain resend bytes forever. pub const MAX_OUTBOUND_ROWS: usize = 256; +/// Cap a single resend envelope (align with LXMF / proxy body budgets). +pub const MAX_OUTBOUND_ENVELOPE_BYTES: usize = 256 * 1024; #[derive(Debug, Clone)] pub struct OutboundRow { @@ -71,6 +73,12 @@ impl GamesOutboundStore { delivery_state: &str, app_id: Option<&str>, ) -> Result<(), String> { + if envelope.len() > MAX_OUTBOUND_ENVELOPE_BYTES { + return Err(format!( + "games_outbound envelope too large ({} > {MAX_OUTBOUND_ENVELOPE_BYTES})", + envelope.len() + )); + } let updated_at = now_secs(); let conn = self .conn @@ -388,4 +396,15 @@ mod tests { ); assert!(rows.iter().any(|r| r.session_id == keep)); } + + #[test] + fn upsert_rejects_oversized_envelope() { + let dir = tempfile::tempdir().expect("tempdir"); + let store = GamesOutboundStore::open(dir.path().join("big.db")).expect("open"); + let big = vec![0u8; MAX_OUTBOUND_ENVELOPE_BYTES + 1]; + let err = store + .upsert("s1", "id1", &big, None, "pending", Some("chess")) + .expect_err("oversized"); + assert!(err.contains("envelope too large"), "unexpected err: {err}"); + } } diff --git a/reticulum-sidecar/src/stack/live.rs b/reticulum-sidecar/src/stack/live.rs index adecae679..8c44f34f8 100644 --- a/reticulum-sidecar/src/stack/live.rs +++ b/reticulum-sidecar/src/stack/live.rs @@ -146,6 +146,12 @@ pub struct LiveBridge { } impl LiveBridge { + /// Orderly RNS drain so BLE RNode tasks detach (radio-off) before process kill. + pub async fn prepare_stop(&self) { + tracing::info!("prepare_stop: shutting down RNS runtime for graceful BLE detach"); + self.handle.shutdown_and_wait().await; + } + fn primary_local_serial_id(&self) -> Option { let state = PersistedState::load(&self.config_dir, &self.storage_dir); let config_ifaces = diff --git a/reticulum-sidecar/src/stack/lxmf_delivery.rs b/reticulum-sidecar/src/stack/lxmf_delivery.rs index 79915f876..425ffe637 100644 --- a/reticulum-sidecar/src/stack/lxmf_delivery.rs +++ b/reticulum-sidecar/src/stack/lxmf_delivery.rs @@ -281,13 +281,33 @@ pub fn spawn_lxmf_inbound_receiver( /// (`LinkDeliveryManager::set_inbound_packet_sender`). Peer replies on our outbound-initiated /// reusable Direct links are Ack'd (LinkProof) even when this sender is unset — without /// wiring, Chat never sees those payloads. +/// +/// LinkDeliveryManager requires [`mpsc::UnboundedSender`]; we bridge into a bounded worker +/// queue and drop newest on saturation (same policy as opportunistic inbound raw). pub fn spawn_lxmf_outbound_backchannel( lxmf_dest_hash: [u8; 16], router: Arc>, ) -> mpsc::UnboundedSender<(Vec, [u8; 16])> { - let (tx, mut rx) = mpsc::unbounded_channel::<(Vec, [u8; 16])>(); + let (outer_tx, mut outer_rx) = mpsc::unbounded_channel::<(Vec, [u8; 16])>(); + let (inner_tx, mut inner_rx) = + mpsc::channel::<(Vec, [u8; 16])>(OUTBOUND_BACKCHANNEL_CAPACITY); tokio::spawn(async move { - while let Some((plaintext, link_id)) = rx.recv().await { + while let Some(pkt) = outer_rx.recv().await { + if let Err(e) = inner_tx.try_send(pkt) { + match e { + mpsc::error::TrySendError::Full(_) => { + tracing::warn!( + capacity = OUTBOUND_BACKCHANNEL_CAPACITY, + "LXMF outbound backchannel saturated — dropping newest packet" + ); + } + mpsc::error::TrySendError::Closed(_) => break, + } + } + } + }); + tokio::spawn(async move { + while let Some((plaintext, link_id)) = inner_rx.recv().await { tracing::debug!( link_id = %hex::encode(link_id), len = plaintext.len(), @@ -296,9 +316,12 @@ pub fn spawn_lxmf_outbound_backchannel( handle_link_delivered_data(&router, lxmf_dest_hash, &plaintext).await; } }); - tx + outer_tx } +/// Bounded worker capacity behind the UnboundedSender API required by LinkDeliveryManager. +pub(crate) const OUTBOUND_BACKCHANNEL_CAPACITY: usize = 256; + /// Capacity for opportunistic inbound raw frames (`LinkManager` `try_send`s into this queue). pub(crate) const INBOUND_RAW_CHANNEL_CAPACITY: usize = 256; diff --git a/reticulum-sidecar/src/stack/mod.rs b/reticulum-sidecar/src/stack/mod.rs index 7b49f98da..d8db6319e 100644 --- a/reticulum-sidecar/src/stack/mod.rs +++ b/reticulum-sidecar/src/stack/mod.rs @@ -2488,6 +2488,16 @@ impl StackHandle { Ok(()) } + /// Graceful RNS shutdown (BLE RNode detach) before the process is SIGTERM'd. + pub async fn prepare_stop(&self) -> Result<(), String> { + #[cfg(feature = "rns-stack")] + if let Some(live) = &self.live { + live.prepare_stop().await; + return Ok(()); + } + Ok(()) + } + pub async fn factory_reset(&self) -> Result<(), String> { let mut inner = self.inner.write().await; inner.factory_reset_state()?; diff --git a/scripts/apply-rsReticulum-ble-rnode-bond-desync.sh b/scripts/apply-rsReticulum-ble-rnode-bond-desync.sh new file mode 100755 index 000000000..e1aad1c89 --- /dev/null +++ b/scripts/apply-rsReticulum-ble-rnode-bond-desync.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Apply mesh-client rsReticulum BLE RNode bond-desync overlay (after pairing-transition debounce). +# - Halt reconnect when CoreBluetooth reports "Peer removed pairing information" +# - Skip TX-char SMP probe on reconnect after a successful session in this task +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=lib/apply-ratspeak-overlay.sh +source "${SCRIPT_DIR}/lib/apply-ratspeak-overlay.sh" +PATCH_FILE="${REPO_ROOT}/reticulum-sidecar/patches/rsReticulum-ble-rnode-bond-desync.patch" +RNS_DIR="${RS_RETICULUM_DIR:-$(cd "${REPO_ROOT}/.." && pwd)/rsReticulum}" +BLE_RNODE_RS="${RNS_DIR}/crates/rns-interface/src/ble_rnode.rs" + +if [[ ! -d "${RNS_DIR}/.git" ]]; then + echo "error: rsReticulum not found at ${RNS_DIR}" >&2 + echo "Clone: git clone https://github.com/ratspeak/rsReticulum.git ${RNS_DIR}" >&2 + exit 1 +fi + +if [[ ! -f "${PATCH_FILE}" ]]; then + echo "error: patch not found at ${PATCH_FILE}" >&2 + exit 1 +fi + +overlay_already_present() { + [[ -f "${BLE_RNODE_RS}" ]] || return 1 + grep -qE '^fn is_bond_removed_error\(' "${BLE_RNODE_RS}" \ + && grep -qE 'session_already_bonded' "${BLE_RNODE_RS}" \ + && grep -qE 'Peer removed pairing information' "${BLE_RNODE_RS}" +} + +if overlay_already_present; then + echo "ble_rnode bond-desync overlay already present on rsReticulum @ $(git -C "${RNS_DIR}" rev-parse --short HEAD)" + exit 0 +fi + +if apply_ratspeak_overlay_or_die "${RNS_DIR}" "${PATCH_FILE}" "ble-rnode-bond-desync"; then + exit 0 +fi +exit 1 diff --git a/scripts/check-i18n-quality.mjs b/scripts/check-i18n-quality.mjs index 5b9c91390..a66704c9f 100644 --- a/scripts/check-i18n-quality.mjs +++ b/scripts/check-i18n-quality.mjs @@ -435,6 +435,20 @@ export const FLASHER_PROVISION_RESERVATION_FALSE_FRIENDS = export const FLASHER_PROVISION_PHYSICAL_WIPE_FALSE_FRIENDS = /\b(wischen|veeg|протри|протер|拭|닦|擦拭)\b/i; +/** flasher.clearPairedDevices* — Bluetooth bond table, not financial bonds/securities. */ +export const FLASHER_CLEAR_PAIRED_DEVICES_KEYS = new Set([ + 'flasher.clearPairedDevicesConfirm', + 'flasher.clearPairedDevicesConfirmMessage', + 'flasher.clearPairedDevicesSuccess', +]); + +/** MT often maps Bluetooth "bond" → financial bond/obligation/securities. */ +export const FLASHER_BT_BOND_FINANCIAL_FALSE_FRIENDS = + /(?:\b(?:Bonos|obligaciones|obligaties|Obligationen|tahvil|obligacje|cautionnement|títulos de rádio|radiobonos)\b)|(?:облигаци|債券|债券)/i; + +/** Must preserve the USB unpair command token verbatim. */ +export const FLASHER_CMD_BT_UNPAIR_TOKEN = 'CMD_BT_UNPAIR'; + /** flasher.errors.rnodeCommandTimeout — MT garbles "close apps using the serial port". */ export const RNODE_TIMEOUT_BAD_UNPLUG_RE = new Map([ ['de', /über den port/i], @@ -2887,6 +2901,23 @@ function checkFlasherIssues(ctx) { } } + if (locale !== 'en' && FLASHER_CLEAR_PAIRED_DEVICES_KEYS.has(flatKey)) { + if (FLASHER_BT_BOND_FINANCIAL_FALSE_FRIENDS.test(val)) { + issues.push( + 'flasher clearPairedDevices must use Bluetooth bond/pairing wording, not financial bonds', + ); + } + if ( + flatKey === 'flasher.clearPairedDevicesConfirmMessage' && + enVal.includes(FLASHER_CMD_BT_UNPAIR_TOKEN) && + !val.includes(FLASHER_CMD_BT_UNPAIR_TOKEN) + ) { + issues.push( + `flasher.clearPairedDevicesConfirmMessage must preserve wire token ${FLASHER_CMD_BT_UNPAIR_TOKEN}`, + ); + } + } + if ( locale !== 'en' && flatKey === 'meshcore.errors.removeContactFailed' && diff --git a/scripts/lib/ratspeak-overlay-apply-list.sh b/scripts/lib/ratspeak-overlay-apply-list.sh index 47ea400fc..10639e8e2 100644 --- a/scripts/lib/ratspeak-overlay-apply-list.sh +++ b/scripts/lib/ratspeak-overlay-apply-list.sh @@ -9,6 +9,7 @@ RS_RETICULUM_APPLY_SCRIPTS=( apply-rsReticulum-link-client-nomad.sh apply-rsReticulum-link-client-proof-budget.sh apply-rsReticulum-ble-rnode-pairing-transition-debounce.sh + apply-rsReticulum-ble-rnode-bond-desync.sh apply-rsReticulum-discovery-announce-egress.sh apply-rsReticulum-path-medium-slots.sh apply-rsReticulum-inbound-raw-saturation-log.sh diff --git a/scripts/update.sh b/scripts/update.sh index 466e97fde..06de96821 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -216,6 +216,7 @@ check_ratspeak_patches() { 'rsReticulum-link-client-nomad.patch|ratspeak/rsReticulum|14|rsReticulum LinkClient Nomad|https://github.com/ratspeak/rsReticulum/pull/14' 'rsReticulum-link-client-proof-budget.patch|ratspeak/rsReticulum||rsReticulum LinkClient proof-budget remaining-deadline|' 'rsReticulum-ble-rnode-pairing-transition-debounce.patch|ratspeak/rsReticulum|20|rsReticulum BLE RNode pairing-transition debounce|https://github.com/ratspeak/rsReticulum/pull/20' + 'rsReticulum-ble-rnode-bond-desync.patch|ratspeak/rsReticulum||rsReticulum BLE RNode bond-desync halt + bond-aware reconnect|' 'rsReticulum-discovery-announce-egress.patch|ratspeak/rsReticulum|19|rsReticulum discovery announce egress|https://github.com/ratspeak/rsReticulum/pull/19' 'rsReticulum-inbound-raw-saturation-log.patch|ratspeak/rsReticulum||rsReticulum inbound-raw saturation log|' 'rsLXMF-propagation-sync-peering.patch|ratspeak/rsLXMF|4|rsLXMF propagation sync peering|https://github.com/ratspeak/rsLXMF/pull/4' diff --git a/src/main/index.ipc-security.test.ts b/src/main/index.ipc-security.test.ts index e9ff3c9e0..c9fc45101 100644 --- a/src/main/index.ipc-security.test.ts +++ b/src/main/index.ipc-security.test.ts @@ -453,6 +453,26 @@ describe('meshtastic:tcp-connect hostname validation (source contract)', () => { it('uses an independent socket ref from meshcore:tcp-connect', () => { expect(INDEX_SOURCE).toContain('let meshtasticTcpSocket: net.Socket | null = null;'); }); + + it('destroys the socket on oversized meshtastic:tcp-data chunks without emitting', () => { + const handlerIdx = INDEX_SOURCE.indexOf("ipcMain.handle('meshtastic:tcp-connect'"); + expect(handlerIdx).toBeGreaterThan(-1); + const dataIdx = INDEX_SOURCE.indexOf("socket.on('data'", handlerIdx); + expect(dataIdx).toBeGreaterThan(handlerIdx); + const dataBody = INDEX_SOURCE.slice(dataIdx, dataIdx + 900); + expect(dataBody).toContain('MESHTASTIC_TCP_DATA_MAX_BYTES'); + expect(dataBody).toContain('meshtastic:tcp-data oversized chunk'); + expect(dataBody).toContain('socket.destroy()'); + const oversizeIdx = dataBody.indexOf('chunk.length > MESHTASTIC_TCP_DATA_MAX_BYTES'); + const destroyIdx = dataBody.indexOf('socket.destroy()'); + const emitIdx = dataBody.indexOf("mainWindow?.webContents.send('meshtastic:tcp-data'"); + expect(oversizeIdx).toBeGreaterThan(-1); + expect(destroyIdx).toBeGreaterThan(oversizeIdx); + expect(emitIdx).toBeGreaterThan(destroyIdx); + // Oversized branch returns before the emit (emit is only on the success path after return). + const returnAfterDestroy = dataBody.slice(destroyIdx, emitIdx); + expect(returnAfterDestroy).toContain('return;'); + }); }); // ─── IPC sender validation (gps / tak) ────────────────────────────── diff --git a/src/main/index.ts b/src/main/index.ts index 4d802fd3b..f6356ea6a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6276,6 +6276,8 @@ ipcMain.handle('meshtastic:tcp-connect', (event, host: string, port: number) => } const socketHost = formatHostForSocket(host); const socket = new net.Socket(); + socket.setNoDelay(true); + socket.setKeepAlive(true, MESHTASTIC_TCP_KEEPALIVE_INITIAL_DELAY_MS); meshtasticTcpSocket = socket; const connectTimeout = setTimeout(() => { if (settled) return; @@ -6297,6 +6299,20 @@ ipcMain.handle('meshtastic:tcp-connect', (event, host: string, port: number) => }); socket.on('data', (data) => { const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data); + if (chunk.length > MESHTASTIC_TCP_DATA_MAX_BYTES) { + console.warn( + `[IPC] meshtastic:tcp-data oversized chunk (${chunk.length} > ${MESHTASTIC_TCP_DATA_MAX_BYTES}); dropping socket`, + ); + try { + socket.destroy(); + } catch (e) { + console.debug( + '[IPC] meshtastic:tcp-data destroy after oversize ' + + sanitizeLogMessage(e instanceof Error ? e.message : String(e)), + ); + } + return; + } mainWindow?.webContents.send('meshtastic:tcp-data', new Uint8Array(chunk)); }); socket.on('close', (hadError) => { @@ -6427,8 +6443,12 @@ const MESHCORE_TCP_CONNECT_TIMEOUT_MS = 20_000; /** Initial TCP keepalive probe delay for MeshCore companion sockets (ms). */ const MESHCORE_TCP_KEEPALIVE_INITIAL_DELAY_MS = 30_000; const MESHTASTIC_TCP_CONNECT_TIMEOUT_MS = 20_000; +/** Initial TCP keepalive probe delay for Meshtastic WiFi/TCP sockets (ms). */ +const MESHTASTIC_TCP_KEEPALIVE_INITIAL_DELAY_MS = 30_000; /** Max Meshtastic TCP toRadio write payload (aligned with meshcore:tcp-write cap). */ const MESHTASTIC_TCP_WRITE_MAX_BYTES = 256 * 1024; +/** Cap inbound Meshtastic TCP chunks before IPC fan-out (same as write max). */ +const MESHTASTIC_TCP_DATA_MAX_BYTES = MESHTASTIC_TCP_WRITE_MAX_BYTES; const CHAT_EXPORT_MAX_MESSAGES = 10_000; const DB_SAVE_NODE_PATH_MAX_BYTES = 16 * 1024; /** Max Meshtastic HTTP toRadio payload (aligned with meshcore:tcp-write cap). */ diff --git a/src/main/ipc/reticulum-db-handlers.test.ts b/src/main/ipc/reticulum-db-handlers.test.ts index 8307c0d77..ce29029b7 100644 --- a/src/main/ipc/reticulum-db-handlers.test.ts +++ b/src/main/ipc/reticulum-db-handlers.test.ts @@ -334,6 +334,38 @@ describe('reticulum destination / activity prune IPC', () => { expect(rows).toEqual([{ message_hash: 'ab'.repeat(32), delivery_status: 'sending' }]); }); + it('saveReticulumMessage ignores invalid replaces_message_hash (no accidental DELETE)', () => { + const identityId = 'id-rt-bad-replaces'; + const senderId = 'ee'.repeat(16); + const payload = 'keep me'; + const ts = 1_700_000_200_000; + const victimHash = 'reticulum-pending-victim'; + const save = handlers.get('db:saveReticulumMessage'); + save?.(event, { + identity_id: identityId, + sender_id: senderId, + sender_name: 'Me', + payload, + timestamp: ts, + message_hash: victimHash, + delivery_status: 'sending', + }); + save?.(event, { + identity_id: identityId, + sender_id: senderId, + sender_name: 'Me', + payload: 'new', + timestamp: ts + 1, + message_hash: 'ff'.repeat(32), + replaces_message_hash: '../etc/passwd', + delivery_status: 'sending', + }); + const rows = db! + .prepareOnce('SELECT message_hash FROM reticulum_messages WHERE identity_id = ? ORDER BY id') + .all(identityId) as { message_hash: string }[]; + expect(rows.map((r) => r.message_hash)).toEqual([victimHash, 'ff'.repeat(32)]); + }); + it('saveReticulumMessage replaces only the named pending when two identical payloads exist', () => { const identityId = 'id-rt-twin-payload'; const senderId = 'dd'.repeat(16); diff --git a/src/main/ipc/reticulum-db-handlers.ts b/src/main/ipc/reticulum-db-handlers.ts index b162c1bc6..d0f522432 100644 --- a/src/main/ipc/reticulum-db-handlers.ts +++ b/src/main/ipc/reticulum-db-handlers.ts @@ -25,6 +25,18 @@ const REMOTE_ADDRESS_SERVICES = new Set(['rnsh', 'rncp']); const REMOTE_INBOUND_DECISIONS = new Set(['allow', 'block']); const ALLOWED_DELIVERY_METHOD = new Set(RETICULUM_DELIVERY_METHODS); +/** + * Prior-row delete key for optimistic LXMF rekey: pending ids or hex message hashes only. + * Rejects arbitrary strings so a malformed save cannot DELETE an unintended row. + */ +export function sanitizeReplacesMessageHash(raw: unknown): string | null { + if (typeof raw !== 'string' || raw.length === 0 || raw.length > 128) return null; + if (/^reticulum-pending-[A-Za-z0-9_-]+$/.test(raw)) return raw; + const hex = raw.toLowerCase(); + if (/^[0-9a-f]{32}$/.test(hex) || /^[0-9a-f]{64}$/.test(hex)) return hex; + return null; +} + interface ParsedRemoteAddressUpsert { id: string; label: string; @@ -171,12 +183,7 @@ export function registerReticulumDbIpcHandlers({ ipcMain }: ReticulumDbIpcDeps): m.next_delivery_attempt_at != null && Number.isFinite(Number(m.next_delivery_attempt_at)) ? Math.trunc(Number(m.next_delivery_attempt_at)) : null; - const replacesMessageHash = - typeof m.replaces_message_hash === 'string' && - m.replaces_message_hash.length > 0 && - m.replaces_message_hash.length <= 128 - ? m.replaces_message_hash.slice(0, 128) - : null; + const replacesMessageHash = sanitizeReplacesMessageHash(m.replaces_message_hash); // Exact prior-hash delete + upsert must be atomic so a failed write rolls back cleanup. const run = db.transaction(() => { diff --git a/src/main/reticulum-sidecar-manager.test.ts b/src/main/reticulum-sidecar-manager.test.ts index 58a5fdaad..65dc63ff0 100644 --- a/src/main/reticulum-sidecar-manager.test.ts +++ b/src/main/reticulum-sidecar-manager.test.ts @@ -637,6 +637,55 @@ describe('ReticulumSidecarManager', () => { )?.[0]; expect(stopProc).toBeDefined(); expect(stopProc).toContain('finalizeStopped()'); + expect(stopProc).toContain('prepareStopBestEffort()'); expect(stopProc).not.toMatch(/\.start\(/); }); + + it('stop calls prepare-stop HTTP before SIGTERM when sidecar is running', async () => { + const existsSpy = vi.spyOn(fs, 'existsSync').mockReturnValue(true); + const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); + const proc = mockSidecarProc(); + proc.kill.mockImplementation(() => { + proc.emit('exit', 0, null); + }); + spawnMock.mockReturnValue(proc); + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + status: 'ok', + version: '0.1.0', + rns_ready: false, + lxmf_ready: false, + }), + text: () => Promise.resolve('ok'), + }); + vi.stubGlobal('fetch', fetchMock); + + const manager = new ReticulumSidecarManager(); + await manager.start(); + fetchMock.mockClear(); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + text: () => Promise.resolve('{"ok":true}'), + }); + + await manager.stop(); + + const prepareCallIndex = fetchMock.mock.calls.findIndex( + (args) => typeof args[0] === 'string' && args[0].includes('/api/v1/stack/prepare-stop'), + ); + expect(prepareCallIndex).toBeGreaterThanOrEqual(0); + expect(fetchMock.mock.calls[prepareCallIndex]?.[1]).toMatchObject({ method: 'POST' }); + expect(proc.kill).toHaveBeenCalledWith('SIGTERM'); + const prepareOrder = fetchMock.mock.invocationCallOrder[prepareCallIndex]; + const killOrder = proc.kill.mock.invocationCallOrder[0]; + expect(prepareOrder).toBeDefined(); + expect(killOrder).toBeDefined(); + expect(prepareOrder).toBeLessThan(killOrder); + + existsSpy.mockRestore(); + mkdirSpy.mockRestore(); + }); }); diff --git a/src/main/reticulum-sidecar-manager.ts b/src/main/reticulum-sidecar-manager.ts index 43d499aca..957efd1d0 100644 --- a/src/main/reticulum-sidecar-manager.ts +++ b/src/main/reticulum-sidecar-manager.ts @@ -39,6 +39,8 @@ import { startSidecarWatchdog } from './reticulumSidecarWatchdog'; const HEALTH_POLL_INTERVAL_MS = 250; const HEALTH_POLL_TIMEOUT_MS = 30 * MS_PER_SECOND; +/** Wait for BLE RNode detach via POST /api/v1/stack/prepare-stop before SIGTERM. */ +const PREPARE_STOP_TIMEOUT_MS = 8 * MS_PER_SECOND; const STOP_GRACE_MS = 5 * MS_PER_SECOND; /** After yielding Noble BLE, allow CoreBluetooth/btleplug to settle before sidecar connect. */ const RETICULUM_BLE_RNODE_NOBLE_SETTLE_MS = 500; @@ -171,6 +173,8 @@ export class ReticulumSidecarManager extends EventEmitter { /** True after the first successful WS open for this sidecar process (reconnects set reconnect=true). */ private wsEverConnected = false; private startPromise: Promise | null = null; + /** In-flight stop — start must await so a fresh spawn does not race SIGTERM exit. */ + private stopPromise: Promise | null = null; private readonly stderrDedupe = new ReticulumSidecarStderrDedupe(); private readonly autoBeaconTracker = new ReticulumSidecarAutoBeaconTracker(); private readonly interfaceIssueTracker = new ReticulumSidecarInterfaceIssueTracker(); @@ -264,6 +268,9 @@ export class ReticulumSidecarManager extends EventEmitter { private async startOnce( opts: ReticulumSidecarStartOptions = {}, ): Promise { + if (this.stopPromise) { + await this.stopPromise; + } if (opts.reuseIfRunning && this._status.running && this.proc) { try { await pollSidecarHealth(this._status.port); @@ -433,8 +440,8 @@ export class ReticulumSidecarManager extends EventEmitter { }, restartFn: async () => { // Hung-only: process still alive but HTTP dead. Renderer owns exit/crash reconnect. - this.stopWatchdog(); - await this.stopProc(); + // Use stop() so stopPromise stays set and concurrent start() awaits the guard. + await this.stop(); await this.start(); }, }); @@ -451,12 +458,19 @@ export class ReticulumSidecarManager extends EventEmitter { // catch-no-log-ok: in-flight start may fail; explicit stop still runs afterward }); } - await this.stopProc(); + if (this.stopPromise) { + return this.stopPromise; + } + this.stopPromise = this.stopProc().finally(() => { + this.stopPromise = null; + }); + return this.stopPromise; } private async stopProc(): Promise { this.stopWatchdog(); this.teardownWs(); + await this.prepareStopBestEffort(); if (bleCoexistenceCoordinator.getState().scanOwner === 'reticulum') { bleCoexistenceCoordinator.releaseScan('reticulum'); } @@ -494,6 +508,32 @@ export class ReticulumSidecarManager extends EventEmitter { this.finalizeStopped(); } + /** Ask the sidecar to detach BLE RNode before process kill (best-effort). */ + private async prepareStopBestEffort(): Promise { + const status = this.getStatus(); + if (!status.running || status.port <= 0 || !this.proc) { + return; + } + try { + const res = await fetch(`http://127.0.0.1:${status.port}/api/v1/stack/prepare-stop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{}', + signal: AbortSignal.timeout(PREPARE_STOP_TIMEOUT_MS), + }); + if (!res.ok) { + console.debug( + `[ReticulumSidecar] prepare-stop HTTP ${res.status} — continuing with SIGTERM`, + ); + } + } catch (e: unknown) { + console.debug( + '[ReticulumSidecar] prepare-stop failed — continuing with SIGTERM:', + sanitizeLogMessage(e instanceof Error ? e.message : String(e)), + ); + } + } + async proxyGet(apiPath: string): Promise { const status = this.getStatus(); if (!status.running || status.port <= 0) { diff --git a/src/main/reticulumSidecarIssueTracker.test.ts b/src/main/reticulumSidecarIssueTracker.test.ts index 9657e5cf7..262d17440 100644 --- a/src/main/reticulumSidecarIssueTracker.test.ts +++ b/src/main/reticulumSidecarIssueTracker.test.ts @@ -31,6 +31,9 @@ const SLOW_TRANSPORT_LINE = const BLE_BOND_REMOVED_LINE = 'BLE RNode connect failed name = RNode 41F4 error = send failed: BLE connect failed after 3 attempts: Runtime Error: Peer removed pairing information'; +const BLE_BOND_REMOVED_STOP_LINE = + 'BLE RNode bond removed — stopping reconnect until stack restart (Forget OS bond + re-pair) name = RNode 41F4 error = send failed: BLE connect failed after 3 attempts: Runtime Error: Peer removed pairing information'; + const BLE_BOND_ATTEMPT_LINE = 'BLE RNode connect attempt failed attempt = 1 error = Runtime Error: Peer removed pairing information'; @@ -61,8 +64,9 @@ describe('ReticulumSidecarInterfaceIssueTracker', () => { ); }); - it('parses BLE bond-removed interface names only from named connect-failed lines', () => { + it('parses BLE bond-removed interface names from connect-failed and halt-loop lines', () => { expect(parseBleBondRemovedIfaceForTests(BLE_BOND_REMOVED_LINE)).toBe('RNode 41F4'); + expect(parseBleBondRemovedIfaceForTests(BLE_BOND_REMOVED_STOP_LINE)).toBe('RNode 41F4'); expect(parseBleBondRemovedIfaceForTests(BLE_BOND_ATTEMPT_LINE)).toBeNull(); }); @@ -79,6 +83,18 @@ describe('ReticulumSidecarInterfaceIssueTracker', () => { expect(alert?.lastAtMs).toBe(1_000); }); + it('builds alert from bond-removed halt-loop log without connect-failed prefix', () => { + tracker.recordLine(BLE_BOND_REMOVED_STOP_LINE, 1_000); + const alert = tracker.getAlert(1_500); + expect(alert?.bleBondRemoved).toEqual(['RNode 41F4']); + }); + + it('keeps bleBondRemoved sticky past the generic alert stale window', () => { + tracker.recordLine(BLE_BOND_REMOVED_STOP_LINE, 0); + const alert = tracker.getAlert(RETICULUM_INTERFACE_ISSUE_ALERT_STALE_MS + 60_000); + expect(alert?.bleBondRemoved).toEqual(['RNode 41F4']); + }); + it('builds alert with BLE pairing-timed-out issues', () => { tracker.recordLine(BLE_PAIRING_IN_PROGRESS_LINE, 500); tracker.recordLine(BLE_PAIRING_TIMED_OUT_LINE, 1_000); diff --git a/src/main/reticulumSidecarIssueTracker.ts b/src/main/reticulumSidecarIssueTracker.ts index b65b3ac52..e56be9500 100644 --- a/src/main/reticulumSidecarIssueTracker.ts +++ b/src/main/reticulumSidecarIssueTracker.ts @@ -9,6 +9,8 @@ const LINK_DELIVERY_TIMEOUT_MARKER = 'link delivery timed out'; const LXMF_PATH_REQUEST_SATURATED_MARKER = 'failed to queue path request for LXMF delivery'; const SLOW_TRANSPORT_QUERY_MARKER = 'transport query slow or failed'; const BLE_BOND_REMOVED_MARKER = 'Peer removed pairing information'; +/** rsReticulum bond-desync overlay: halt reconnect loop (tracing::warn). */ +const BLE_BOND_REMOVED_STOP_MARKER = 'BLE RNode bond removed'; const BLE_PAIRING_TIMED_OUT_MARKER = 'BLE pairing timed out'; const TCP_CONNECT_IFACE_RE = /TCP connect failed.*?name\s*=\s*(.+?)(?:\s+error\s*=|$)/; @@ -28,19 +30,30 @@ function normalizeSidecarLogLine(text: string): string { return stripAnsi(text).replace(/\s+/g, ' ').trim(); } -/** Deterministic parse — avoids super-linear regex backtracking on long sidecar lines. */ -function parseBleRNodeConnectFailedIfaceName(plain: string): string | null { - const failedIdx = plain.toLowerCase().indexOf(BLE_RNODE_CONNECT_FAILED_PREFIX.toLowerCase()); - if (failedIdx < 0) return null; - const afterFailed = plain.slice(failedIdx + BLE_RNODE_CONNECT_FAILED_PREFIX.length); - const nameKey = /\bname[ \t]{0,16}=[ \t]{0,16}/i.exec(afterFailed); +/** + * Deterministic `name=` parse — avoids super-linear regex backtracking on long sidecar lines. + * When `requirePrefix` is set, only search after that substring (case-insensitive). + */ +function parseSidecarIfaceNameField(plain: string, requirePrefix?: string): string | null { + let searchIn = plain; + if (requirePrefix) { + const failedIdx = plain.toLowerCase().indexOf(requirePrefix.toLowerCase()); + if (failedIdx < 0) return null; + searchIn = plain.slice(failedIdx + requirePrefix.length); + } + const nameKey = /\bname[ \t]{0,16}=[ \t]{0,16}/i.exec(searchIn); if (nameKey?.index == null) return null; - const rest = afterFailed.slice(nameKey.index + nameKey[0].length); + const rest = searchIn.slice(nameKey.index + nameKey[0].length); const errorMatch = /[ \t]+error[ \t]{0,16}=/i.exec(rest); const name = (errorMatch?.index != null ? rest.slice(0, errorMatch.index) : rest).trim(); return name.length > 0 ? name : null; } +/** Deterministic parse — avoids super-linear regex backtracking on long sidecar lines. */ +function parseBleRNodeConnectFailedIfaceName(plain: string): string | null { + return parseSidecarIfaceNameField(plain, BLE_RNODE_CONNECT_FAILED_PREFIX); +} + function parseTcpConnectFailedIface(line: string): string | null { const match = TCP_CONNECT_IFACE_RE.exec(normalizeSidecarLogLine(line)); return match?.[1]?.trim() ?? null; @@ -74,10 +87,17 @@ function parseSlowTransportQuery(line: string): string | null { function parseBleBondRemovedIface(line: string): string | null { const plain = normalizeSidecarLogLine(line); - if (!plain.includes(BLE_BOND_REMOVED_MARKER)) { - return null; + // Legacy connect-failed line: "BLE RNode connect failed name=… error=… Peer removed pairing information" + if (plain.includes(BLE_BOND_REMOVED_MARKER)) { + const fromConnectFailed = parseBleRNodeConnectFailedIfaceName(plain); + if (fromConnectFailed) return fromConnectFailed; } - return parseBleRNodeConnectFailedIfaceName(plain); + // Overlay halt path: "BLE RNode bond removed — stopping reconnect… name=… error=…" + // (no longer emits the connect-failed prefix when exiting the reconnect loop). + if (plain.includes(BLE_BOND_REMOVED_STOP_MARKER)) { + return parseSidecarIfaceNameField(plain); + } + return null; } function parseBlePairingTimedOutIface(line: string): string | null { @@ -230,7 +250,8 @@ export class ReticulumSidecarInterfaceIssueTracker { pruneStaleMap(this.tcpConnectFailed, nowMs, (atMs) => atMs); pruneStaleMap(this.txQueueDrops, nowMs, (entry) => entry.atMs); pruneStaleMap(this.linkDeliveryTimeouts, nowMs, (entry) => entry.atMs); - pruneStaleMap(this.bleBondRemoved, nowMs, (atMs) => atMs); + // bleBondRemoved is sticky until stack stop / retainInterfaces / clear — sidecar has + // halted BLE reconnect for that interface; a 5‑min log TTL must not clear UI/Noble yield. pruneStaleMap(this.blePairingTimedOut, nowMs, (atMs) => atMs); if ( diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index ea28f0ffe..3155fceff 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -52,6 +52,7 @@ import { ConnectIcon } from '@/renderer/lib/icons/connectIcon'; import { MqttGlobeIcon } from '@/renderer/lib/icons/connectionIcons'; import { ICON_MD } from '@/renderer/lib/icons/iconClass'; import { useIconTrigger } from '@/renderer/lib/icons/iconMotionContext'; +import { isMeshcoreTcpSoftApDeadAccepted } from '@/renderer/lib/meshcore/meshcoreTcpInitBurst'; import { meshcoreConfiguredChannelIndexSet, meshcoreConfiguredChatChannels, @@ -2508,6 +2509,12 @@ function AppContent() { if (!meshcoreIdentityId || !connectionDriver.getHandle(meshcoreIdentityId)) return; const sendScheduledAdvert = () => { + // SoftAP/OpenHop: configured session may have a dead TCP bridge after contacts FIN. + // Flood advert would tcp-write-fail and thrash reconnect. + if (isMeshcoreTcpSoftApDeadAccepted()) { + console.debug('[App] auto flood advert skipped (SoftAP dead bridge)'); + return; + } const action = autoFloodAdvertType === 'zeroHop' ? meshcorePanelActions.sendZeroHopAdvert @@ -3109,6 +3116,15 @@ function AppContent() { setActiveTab(appTabIdx); } }} + onOpenAdminBluetooth={() => { + const adminTabIdx = findFilteredTabIndexForPanel( + selectByProtocol(tabsByProtocol, protocol), + ADMIN_PANEL_INDEX, + ); + if (adminTabIdx >= 0) { + setActiveTab(adminTabIdx); + } + }} suppressMountAutoConnect /> )} diff --git a/src/renderer/components/ConnectionPanel.tsx b/src/renderer/components/ConnectionPanel.tsx index 2a8a227f5..30ca0ee8b 100644 --- a/src/renderer/components/ConnectionPanel.tsx +++ b/src/renderer/components/ConnectionPanel.tsx @@ -348,6 +348,8 @@ interface Props { onOpenReticulumRmapSettings?: () => void; /** Reticulum: open App tab GPS settings for RMAP coordinates. */ onOpenAppGpsSettings?: () => void; + /** Reticulum: open Admin Bluetooth for USB Clear paired / Start pairing. */ + onOpenAdminBluetooth?: () => void; /** Auto-connect is owned by ProtocolAutoConnectCoordinator when panels are tab-mounted. */ suppressMountAutoConnect?: boolean; } @@ -368,6 +370,7 @@ export default function ConnectionPanel({ onStartReticulumStack, onOpenReticulumRmapSettings, onOpenAppGpsSettings, + onOpenAdminBluetooth, suppressMountAutoConnect = false, }: Props) { const { t } = useTranslation(); @@ -3088,6 +3091,7 @@ export default function ConnectionPanel({ stackError={reticulumStackError} onOpenReticulumRmapSettings={onOpenReticulumRmapSettings} onOpenAppGpsSettings={onOpenAppGpsSettings} + onOpenAdminBluetooth={onOpenAdminBluetooth} onStartStack={async () => { setReticulumStackError(null); try { diff --git a/src/renderer/components/ReticulumSidecarIssueAlertsBlock.test.tsx b/src/renderer/components/ReticulumSidecarIssueAlertsBlock.test.tsx index e3afeef7e..1099edfae 100644 --- a/src/renderer/components/ReticulumSidecarIssueAlertsBlock.test.tsx +++ b/src/renderer/components/ReticulumSidecarIssueAlertsBlock.test.tsx @@ -114,6 +114,37 @@ describe('ReticulumSidecarIssueAlertsBlock', () => { expect( screen.getByText('connectionPanel.reticulumSidecarIssues.bleBondRemoved:RNode 41F4'), ).toBeInTheDocument(); + expect( + screen.queryByRole('button', { + name: 'connectionPanel.reticulumSidecarIssues.bleBondRemovedStopStack', + }), + ).not.toBeInTheDocument(); + }); + + it('offers Stop stack and Open Admin actions when bleBondRemoved handlers are provided', async () => { + const onStopStack = vi.fn().mockResolvedValue(undefined); + const onOpenAdminBluetooth = vi.fn(); + const { container } = render( + , + ); + const stopBtn = screen.getByRole('button', { + name: 'connectionPanel.reticulumSidecarIssues.bleBondRemovedStopStack', + }); + const adminBtn = screen.getByRole('button', { + name: 'connectionPanel.reticulumSidecarIssues.bleBondRemovedOpenAdmin', + }); + stopBtn.click(); + adminBtn.click(); + expect(onStopStack).toHaveBeenCalledTimes(1); + expect(onOpenAdminBluetooth).toHaveBeenCalledTimes(1); + hydrateAxeThemeColors(container); + expect(await axe(container)).toHaveNoViolations(); }); it('shows BLE pairing-timed-out issue from sidecar alert', async () => { @@ -133,4 +164,117 @@ describe('ReticulumSidecarIssueAlertsBlock', () => { hydrateAxeThemeColors(container); expect(await axe(container)).toHaveNoViolations(); }); + + const tcpIface = { + id: 'rmap', + name: 'RMAP World', + type: 'tcp', + enabled: true, + status: 'down', + serial_port: null as string | null, + }; + const bleIface = { + id: 'rnode-41f4', + name: 'RNode 41F4', + type: 'rnode', + enabled: true, + status: 'down', + serial_port: 'ble://eccf2847-e1fd-3f5f-0811-064db1639a3d', + }; + + it('uses TCP TX-drop hint for TCP hubs', () => { + render( + , + ); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'), + ).toBeInTheDocument(); + expect( + screen.queryByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBle'), + ).not.toBeInTheDocument(); + }); + + it('uses BLE TX-drop hint for BLE RNodes', () => { + render( + , + ); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBle'), + ).toBeInTheDocument(); + expect( + screen.queryByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'), + ).not.toBeInTheDocument(); + }); + + it('uses per-drop hints for mixed TCP + BLE (log regression)', async () => { + const { container } = render( + , + ); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.heading:2'), + ).toBeInTheDocument(); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'), + ).toBeInTheDocument(); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBle'), + ).toBeInTheDocument(); + hydrateAxeThemeColors(container); + expect(await axe(container)).toHaveNoViolations(); + }); + + it('uses bond-stale TX hint and keeps Admin actions when bleBondRemoved co-occurs', () => { + const onStopStack = vi.fn(); + const onOpenAdminBluetooth = vi.fn(); + render( + , + ); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBleBondStale'), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: 'connectionPanel.reticulumSidecarIssues.bleBondRemovedStopStack', + }), + ).toBeInTheDocument(); + }); + + it('falls back to neutral TX hint when interfaces are omitted', () => { + render( + , + ); + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintNeutral'), + ).toBeInTheDocument(); + }); }); diff --git a/src/renderer/components/ReticulumSidecarIssueAlertsBlock.tsx b/src/renderer/components/ReticulumSidecarIssueAlertsBlock.tsx index 00fa5d608..c65cabe21 100644 --- a/src/renderer/components/ReticulumSidecarIssueAlertsBlock.tsx +++ b/src/renderer/components/ReticulumSidecarIssueAlertsBlock.tsx @@ -1,11 +1,20 @@ import { useTranslation } from 'react-i18next'; +import { + resolveReticulumTxDropHintKind, + type ReticulumLocalInterfaceInput, + reticulumTxDropConnectionHintKey, +} from '@/renderer/lib/reticulum/reticulumLocalInterfaceHealth'; import type { ReticulumInterfaceIssueAlert } from '@/shared/reticulum-types'; export interface ReticulumSidecarIssueAlertsBlockProps { alert: ReticulumInterfaceIssueAlert; /** When true, hint that other Reticulum apps may conflict via shared instance. */ shareInstanceEnabled?: boolean; + /** Local interface rows for transport-aware TX-drop hints. */ + interfaces?: readonly Pick[]; + onStopStack?: () => void | Promise; + onOpenAdminBluetooth?: () => void; } /** Stack-health issues for Connection; per-peer link timeouts stay in Diagnostics/Chat. */ @@ -24,6 +33,9 @@ function countSidecarInterfaceIssues(alert: ReticulumInterfaceIssueAlert): numbe export function ReticulumSidecarIssueAlertsBlock({ alert, shareInstanceEnabled = false, + interfaces, + onStopStack, + onOpenAdminBluetooth, }: ReticulumSidecarIssueAlertsBlockProps) { const { t } = useTranslation(); const issueCount = countSidecarInterfaceIssues(alert); @@ -35,6 +47,8 @@ export function ReticulumSidecarIssueAlertsBlock({ shareInstanceEnabled && (alert.transportSaturatedCount > 0 || alert.txQueueDrops.length > 0); const bleBondRemoved = alert.bleBondRemoved ?? []; const blePairingTimedOut = alert.blePairingTimedOut ?? []; + const showBleBondActions = + bleBondRemoved.length > 0 && (onStopStack != null || onOpenAdminBluetooth != null); return (
))} - {alert.txQueueDrops.map(({ name, dropCount }) => ( -
  • -

    - {t('connectionPanel.reticulumSidecarIssues.txQueueDrops', { - name, - count: dropCount, - })} -

    -

    - {t('connectionPanel.reticulumSidecarIssues.txQueueDropsHint')} -

    -
  • - ))} + {alert.txQueueDrops.map(({ name, dropCount }) => { + const hintKind = resolveReticulumTxDropHintKind(name, interfaces, bleBondRemoved); + const hintKey = reticulumTxDropConnectionHintKey(hintKind); + let hintText: string; + switch (hintKey) { + case 'txQueueDropsHintBleBondStale': + hintText = t('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBleBondStale'); + break; + case 'txQueueDropsHintBle': + hintText = t('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBle'); + break; + case 'txQueueDropsHintNeutral': + hintText = t('connectionPanel.reticulumSidecarIssues.txQueueDropsHintNeutral'); + break; + case 'txQueueDropsHint': + default: + hintText = t('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'); + break; + } + return ( +
  • +

    + {t('connectionPanel.reticulumSidecarIssues.txQueueDrops', { + name, + count: dropCount, + })} +

    +

    {hintText}

    +
  • + ); + })} {bleBondRemoved.map((name) => (
  • {t('connectionPanel.reticulumSidecarIssues.bleBondRemoved', { name })}

    @@ -74,6 +106,39 @@ export function ReticulumSidecarIssueAlertsBlock({

  • ))} + {showBleBondActions ? ( +
  • + {onStopStack ? ( + + ) : null} + {onOpenAdminBluetooth ? ( + + ) : null} +
  • + ) : null} {blePairingTimedOut.map((name) => (
  • {t('connectionPanel.reticulumSidecarIssues.blePairingTimedOut', { name })}

    @@ -83,7 +148,7 @@ export function ReticulumSidecarIssueAlertsBlock({
  • ))} {alert.transportSaturatedCount > 0 ? ( -
  • +
  • {t('connectionPanel.reticulumSidecarIssues.transportSaturated', { count: alert.transportSaturatedCount, @@ -95,7 +160,7 @@ export function ReticulumSidecarIssueAlertsBlock({

  • ) : null} {alert.slowTransportQueryCount > 0 ? ( -
  • +
  • {t('connectionPanel.reticulumSidecarIssues.slowTransportQuery', { count: alert.slowTransportQueryCount, @@ -106,19 +171,23 @@ export function ReticulumSidecarIssueAlertsBlock({

  • ) : null} + {showShareInstanceHint ? ( +
  • +

    + {t('connectionPanel.reticulumSidecarIssues.shareInstanceHint')} +

    +
  • + ) : null} + {(alert.suppressedCount ?? 0) > 0 ? ( +
  • +

    + {t('connectionPanel.reticulumSidecarIssues.suppressed', { + count: alert.suppressedCount, + })} +

    +
  • + ) : null} - {showShareInstanceHint ? ( -

    - {t('connectionPanel.reticulumSidecarIssues.shareInstanceHint')} -

    - ) : null} - {alert.suppressedCount > 0 ? ( -

    - {t('connectionPanel.reticulumSidecarIssues.suppressed', { - count: alert.suppressedCount, - })} -

    - ) : null}
    ); } diff --git a/src/renderer/components/ReticulumStackPanel.test.tsx b/src/renderer/components/ReticulumStackPanel.test.tsx index 85fb4303d..3528d01d2 100644 --- a/src/renderer/components/ReticulumStackPanel.test.tsx +++ b/src/renderer/components/ReticulumStackPanel.test.tsx @@ -274,6 +274,35 @@ describe('ReticulumStackPanel', () => { vi.mocked(window.electronAPI.reticulum.syncInterfaceIssueScope).mockResolvedValue( statusWithAlert, ); + window.electronAPI.reticulum.proxyGet = vi.fn().mockImplementation((path: string) => { + if (path === '/api/v1/interfaces') { + return Promise.resolve({ + interfaces: [ + { + id: 'ham', + name: 'RNS HAM RADIO', + type: 'tcpclient', + enabled: true, + status: 'down', + host: '135.125.238.229', + port: 4242, + }, + ], + }); + } + if (path === '/api/v1/serial/ports') { + return Promise.resolve({ ports: [] }); + } + if (path === '/api/v1/stack/settings') { + return Promise.resolve({ + enable_transport: true, + share_instance: false, + loglevel: 4, + announce_interval_sec: 3600, + }); + } + return Promise.resolve({}); + }); let statusCb: | ((status: { running: boolean; @@ -307,6 +336,96 @@ describe('ReticulumStackPanel', () => { expect( screen.getByText('connectionPanel.reticulumSidecarIssues.tcpConnectFailed:RNS HAM RADIO'), ).toBeInTheDocument(); + await waitFor(() => { + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'), + ).toBeInTheDocument(); + }); + }); + + it('shows BLE TX-drop hint when interfaces include a ble:// RNode', async () => { + const issueAlert = { + tcpConnectFailed: [], + txQueueDrops: [{ name: 'RNode 41F4', dropCount: 512 }], + linkDeliveryTimeouts: [], + bleBondRemoved: [], + blePairingTimedOut: [], + transportSaturatedCount: 0, + slowTransportQueryCount: 0, + suppressedCount: 0, + lastAtMs: Date.now(), + }; + const statusWithAlert = { + running: true, + port: 19437, + pid: 1, + interfaceIssueAlert: issueAlert, + }; + vi.mocked(window.electronAPI.reticulum.getStatus).mockResolvedValue(statusWithAlert); + vi.mocked(window.electronAPI.reticulum.syncInterfaceIssueScope).mockResolvedValue( + statusWithAlert, + ); + window.electronAPI.reticulum.proxyGet = vi.fn().mockImplementation((path: string) => { + if (path === '/api/v1/interfaces') { + return Promise.resolve({ + interfaces: [ + { + id: 'rnode-41f4', + name: 'RNode 41F4', + type: 'rnode', + enabled: true, + status: 'down', + serial_port: 'ble://eccf2847-e1fd-3f5f-0811-064db1639a3d', + }, + ], + }); + } + if (path === '/api/v1/serial/ports') { + return Promise.resolve({ ports: [] }); + } + if (path === '/api/v1/stack/settings') { + return Promise.resolve({ + enable_transport: true, + share_instance: false, + loglevel: 4, + announce_interval_sec: 3600, + }); + } + return Promise.resolve({}); + }); + let statusCb: + | ((status: { + running: boolean; + port: number; + pid: number | null; + interfaceIssueAlert?: typeof issueAlert | null; + }) => void) + | null = null; + window.electronAPI.reticulum.onStatus = vi.fn((cb) => { + statusCb = cb; + return () => {}; + }); + + render( + {}} + onStopStack={async () => {}} + />, + ); + + act(() => { + statusCb?.(statusWithAlert); + }); + + await waitFor(() => { + expect( + screen.getByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHintBle'), + ).toBeInTheDocument(); + }); + expect( + screen.queryByText('connectionPanel.reticulumSidecarIssues.txQueueDropsHint'), + ).not.toBeInTheDocument(); }); it('syncs enabled interface names into main issue scope after hydrate', async () => { diff --git a/src/renderer/components/ReticulumStackPanel.tsx b/src/renderer/components/ReticulumStackPanel.tsx index 6bd6f8a3a..bea02a533 100644 --- a/src/renderer/components/ReticulumStackPanel.tsx +++ b/src/renderer/components/ReticulumStackPanel.tsx @@ -32,6 +32,8 @@ export interface ReticulumStackPanelProps { onStopStack: () => Promise; onOpenReticulumRmapSettings?: () => void; onOpenAppGpsSettings?: () => void; + /** Open Admin → Bluetooth (USB Clear paired / Start pairing) for BLE bond recovery. */ + onOpenAdminBluetooth?: () => void; } /** Connection tab: stack lifecycle, interface CRUD, and local interface health. */ @@ -42,6 +44,7 @@ export function ReticulumStackPanel({ onStopStack, onOpenReticulumRmapSettings, onOpenAppGpsSettings, + onOpenAdminBluetooth, }: ReticulumStackPanelProps) { const { t } = useTranslation(); const [restartError, setRestartError] = useState(null); @@ -266,6 +269,13 @@ export function ReticulumStackPanel({ { + notifyManualStackStop(); + await onStopStack(); + await refreshSidecarStatus(); + }} + onOpenAdminBluetooth={onOpenAdminBluetooth} /> ) : null} void; onDisable: () => void; onStartPairing: () => void; + onClearPairedDevices?: () => void; } export function BluetoothConfig({ @@ -17,6 +18,7 @@ export function BluetoothConfig({ onEnable, onDisable, onStartPairing, + onClearPairedDevices, }: BluetoothConfigProps) { const { t } = useTranslation(); const pinLabel = pairingPin !== null ? String(pairingPin).padStart(6, '0') : null; @@ -53,6 +55,17 @@ export function BluetoothConfig({ > {t('flasher.startPairing')} + {onClearPairedDevices ? ( + + ) : null} {pairingPending && pairingPin === null ? ( {t('flasher.pairingWaiting')} diff --git a/src/renderer/components/flasher/RNodeFlasherSection.tsx b/src/renderer/components/flasher/RNodeFlasherSection.tsx index ed86f78b6..490950c7a 100644 --- a/src/renderer/components/flasher/RNodeFlasherSection.tsx +++ b/src/renderer/components/flasher/RNodeFlasherSection.tsx @@ -92,6 +92,7 @@ export function RNodeFlasherSection({ portBlocked }: RNodeFlasherSectionProps) { const [wifiConfigSummary, setWifiConfigSummary] = useState(null); const [displayImage, setDisplayImage] = useState(null); const [showWipeConfirm, setShowWipeConfirm] = useState(false); + const [showClearBondsConfirm, setShowClearBondsConfirm] = useState(false); const [busy, setBusy] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [statusIsError, setStatusIsError] = useState(false); @@ -567,6 +568,9 @@ export function RNodeFlasherSection({ portBlocked }: RNodeFlasherSectionProps) { } }); }} + onClearPairedDevices={() => { + setShowClearBondsConfirm(true); + }} /> ) : null} + {showClearBondsConfirm ? ( + { + setShowClearBondsConfirm(false); + void runWithRNode(async (rnode) => { + await rnode.clearBluetoothBonds(); + showStatus(t('flasher.clearPairedDevicesSuccess')); + }); + }} + onCancel={() => { + setShowClearBondsConfirm(false); + }} + /> + ) : null} ); } diff --git a/src/renderer/components/reticulum/ReticulumInterfacesPanel.test.tsx b/src/renderer/components/reticulum/ReticulumInterfacesPanel.test.tsx index 397f99abc..ac77e6f14 100644 --- a/src/renderer/components/reticulum/ReticulumInterfacesPanel.test.tsx +++ b/src/renderer/components/reticulum/ReticulumInterfacesPanel.test.tsx @@ -590,6 +590,54 @@ describe('ReticulumInterfacesPanel', () => { }); }); + it('shows rnodeTransportBleHint only for BLE transport', async () => { + const user = userEvent.setup(); + window.electronAPI.reticulum.proxyGet = vi.fn().mockImplementation((path: string) => { + if (path === '/api/v1/ble/availability') { + return Promise.resolve({ available: true }); + } + if (path === '/api/v1/rnode/presets') { + return Promise.resolve({ presets: [] }); + } + if (path === '/api/v1/config/audit') { + return Promise.resolve({ issues: [] }); + } + if (path === '/api/v1/serial/ports') { + return Promise.resolve({ ports: [] }); + } + return Promise.resolve({}); + }); + + render(); + + await waitFor(() => { + expect(window.electronAPI.reticulum.proxyGet).toHaveBeenCalledWith( + '/api/v1/ble/availability', + ); + }); + + const typeSelect = screen.getByLabelText('connectionPanel.reticulumInterfaces.type'); + await user.selectOptions(typeSelect, 'rnode'); + const transportSelect = screen.getByLabelText( + 'connectionPanel.reticulumInterfaces.rnodeTransport', + ); + + await user.selectOptions(transportSelect, 'serial'); + expect( + screen.queryByText('connectionPanel.reticulumInterfaces.rnodeTransportBleHint'), + ).not.toBeInTheDocument(); + + await user.selectOptions(transportSelect, 'ble'); + expect( + screen.getByText('connectionPanel.reticulumInterfaces.rnodeTransportBleHint'), + ).toBeInTheDocument(); + + await user.selectOptions(transportSelect, 'wifi'); + expect( + screen.queryByText('connectionPanel.reticulumInterfaces.rnodeTransportBleHint'), + ).not.toBeInTheDocument(); + }); + it('does not flag Wi-Fi RNode tcp:// interface rows as stale USB serial ports', () => { render( + {rnodeTransport === 'ble' ? ( +

    + {t('connectionPanel.reticulumInterfaces.rnodeTransportBleHint')} +

    + ) : null} ) : null} {showRnodeWifi ? ( diff --git a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts index 88a3e3bd1..84958bcdd 100644 --- a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts +++ b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.test.ts @@ -435,6 +435,22 @@ describe('attachMeshcoreConnSideEffects', () => { expect(h.handleConnectionLost).toHaveBeenCalled(); }); + it('ignores TCP device_status disconnect (runtime owns TCP bridge + reconnect)', async () => { + const h = makeHarness(); + h.ctx.meshcoreConnectTypeRef.current = 'tcp'; + detach = attachMeshcoreConnSideEffects(h.conn, h.ctx); + + dispatch({ type: 'device_status', payload: { status: 'disconnected' } }); + + // SoftAP FIN must not strip the ConnectionDriver handle — write-dead / tcp.onDisconnected + // own recovery after "accepting dead bridge". + expect(h.state.status).toBe('configured'); + expect(h.teardownConn).not.toHaveBeenCalled(); + expect(h.ctx.connRef.current).toBe(h.conn); + await Promise.resolve(); + expect(h.handleConnectionLost).not.toHaveBeenCalled(); + }); + it('stops handling events after detach', () => { const h = makeHarness({ handleResponseResult: true }); const stop = attachMeshcoreConnSideEffects(h.conn, h.ctx); diff --git a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts index fab56b553..581cc575f 100644 --- a/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts +++ b/src/renderer/hooks/meshcore/meshcoreConnSideEffects.ts @@ -643,6 +643,14 @@ export function attachMeshcoreConnSideEffects( // --- Disconnect --- const handleDisconnected = () => { + // TCP: runtime meshcore.tcp.onDisconnected / write-dead own bridge-dead + reconnect (#792). + // SoftAP/OpenHop often FINs after contacts; TcpOverIpc still emits device_status disconnected. + // Tearing down the driver here left "accepting dead bridge" with no ConnectionDriver handle + // and no scheduled reconnect (deferred flag cleared without schedule) — send then logs + // "no handle for offline-meshcore" and never reaches write-dead recovery. + if (meshcoreConnectTypeRef.current === 'tcp') { + return; + } let shouldReconnect = false; setState((prev) => { const wasOperational = @@ -683,13 +691,7 @@ export function attachMeshcoreConnSideEffects( console.debug('[meshcoreConnSideEffects] stale conn close ' + errLikeToLogString(e)); }); } - if ( - shouldReconnect && - !meshcoreExplicitDisconnectRef.current && - // TCP reconnect is owned by the runtime meshcore.tcp.onDisconnected listener (#792). - // Calling lost here as well double-entered the scheduler (n7eal dual backoff). - meshcoreConnectTypeRef.current !== 'tcp' - ) { + if (shouldReconnect && !meshcoreExplicitDisconnectRef.current) { handleConnectionLostRef.current(); } }); diff --git a/src/renderer/hooks/useSendMessage.test.ts b/src/renderer/hooks/useSendMessage.test.ts index 379ff3587..1f169b868 100644 --- a/src/renderer/hooks/useSendMessage.test.ts +++ b/src/renderer/hooks/useSendMessage.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mergeAppSetting } from '../lib/appSettingsStorage'; import { connectionDriver } from '../lib/drivers/ConnectionDriver'; +import { setMeshcoreTcpSoftApDeadAccepted } from '../lib/meshcore/meshcoreTcpInitBurst'; import { meshcoreProtocol } from '../lib/protocols/MeshCoreProtocol'; import { meshtasticProtocol } from '../lib/protocols/MeshtasticProtocol'; import { reticulumProtocol } from '../lib/protocols/ReticulumProtocol'; @@ -52,6 +53,7 @@ describe('useSendMessage', () => { registerMeshtasticSession(null); registerMeshcoreSession(null); registerReticulumSession(null); + setMeshcoreTcpSoftApDeadAccepted(false); useIdentityStore.setState({ identities: {}, activeIdentityId: null }); useMessageStore.setState({ messages: {} }); vi.mocked(connectionDriver.getHandle).mockReturnValue(null); @@ -348,7 +350,7 @@ describe('useSendMessage', () => { it('marks optimistic message failed when protocol send rejects', async () => { const sendSpy = vi .spyOn(meshcoreProtocol, 'sendMessage') - .mockRejectedValue(new Error('rf down')); + .mockImplementation(() => Promise.reject(new Error('rf down'))); const handle = { kind: 'rf' }; vi.mocked(connectionDriver.getHandle).mockReturnValue(handle); addIdentity({ diff --git a/src/renderer/hooks/useSendMessage.ts b/src/renderer/hooks/useSendMessage.ts index 26ef6b8c7..a32e88b13 100644 --- a/src/renderer/hooks/useSendMessage.ts +++ b/src/renderer/hooks/useSendMessage.ts @@ -6,6 +6,10 @@ import { messageToDbRow } from '../hooks/meshcore/meshcoreHookPreamble'; import { isMeshcoreOpenWireCompatEnabled } from '../lib/appSettingsStorage'; import { connectionDriver } from '../lib/drivers/ConnectionDriver'; import { errLikeToLogString } from '../lib/errLikeToLogString'; +import { + isMeshcoreTcpSoftApDeadAccepted, + trackMeshcoreTcpUserTxSend, +} from '../lib/meshcore/meshcoreTcpInitBurst'; import { resolveMeshcoreOutboundWireText } from '../lib/meshcoreChannelText'; import { listChatMessagesFromStore } from '../lib/meshcoreStoreDedup'; import { sendReticulumChatMessage } from '../lib/reticulum/sendReticulumChatMessage'; @@ -200,72 +204,102 @@ export function useSendMessage( const wireText = resolvedOutbound.wireText; - void identity.protocol - .sendMessage(handle, { + const finishSend = ( + sendHandle: NonNullable, + opts?: { trackForSoftApLiveWindow?: boolean }, + ): void => { + const sendPromise = identity.protocol.sendMessage(sendHandle, { text: wireText, channelIndex, destination, destinationPubKey, replyTo, - }) - .then((res) => { - const resolvedId = res.packetId != null ? String(res.packetId >>> 0) : provisionalId; - if (res.packetId != null && resolvedId !== provisionalId) { - renameMessageId(identityId, provisionalId, resolvedId); + }); + if (opts?.trackForSoftApLiveWindow) { + trackMeshcoreTcpUserTxSend(sendPromise); + } + void sendPromise.then( + (res) => { + const resolvedId = res.packetId != null ? String(res.packetId >>> 0) : provisionalId; + if (res.packetId != null && resolvedId !== provisionalId) { + renameMessageId(identityId, provisionalId, resolvedId); + if (isMeshtastic && meshtasticTempPacketId != null) { + void window.electronAPI.db + .updateMessagePacketId(meshtasticTempPacketId, res.packetId >>> 0, myNodeNum) + .catch((e: unknown) => { + console.debug( + '[useSendMessage] updateMessagePacketId failed ' + errLikeToLogString(e), + ); + }); + } + } + + updateMessageStatus(identityId, resolvedId, 'acked'); + if (identity.protocol.type === 'meshcore') { + const rowForDb: MessageRecord = { + ...record, + id: resolvedId, + status: 'acked', + }; + persistMeshcoreOutboundRow( + rowForDb, + myNodeNum, + meshcoreSenderName, + 'acked', + res.packetId != null ? res.packetId >>> 0 : undefined, + ); + } if (isMeshtastic && meshtasticTempPacketId != null) { + const rowPacketId = res.packetId ?? meshtasticTempPacketId; void window.electronAPI.db - .updateMessagePacketId(meshtasticTempPacketId, res.packetId >>> 0, myNodeNum) + .updateMessageStatus(rowPacketId, 'acked') .catch((e: unknown) => { console.debug( - '[useSendMessage] updateMessagePacketId failed ' + errLikeToLogString(e), + '[useSendMessage] updateMessageStatus failed ' + errLikeToLogString(e), ); }); } - } + }, + (e: unknown) => { + const errMsg = errLikeToLogString(e); + console.warn('[useSendMessage] send failed ' + errMsg); + updateMessageStatus(identityId, provisionalId, 'failed', errMsg); + if (identity.protocol.type === 'meshcore') { + persistMeshcoreOutboundRow(record, myNodeNum, meshcoreSenderName, 'failed'); + } + if (isMeshtastic && meshtasticTempPacketId != null) { + void window.electronAPI.db + .updateMessageStatus(meshtasticTempPacketId, 'failed', errMsg) + .catch((dbErr: unknown) => { + console.debug( + '[useSendMessage] updateMessageStatus failed ' + errLikeToLogString(dbErr), + ); + }); + } + }, + ); + }; - updateMessageStatus(identityId, resolvedId, 'acked'); - if (identity.protocol.type === 'meshcore') { - const rowForDb: MessageRecord = { - ...record, - id: resolvedId, - status: 'acked', - }; - persistMeshcoreOutboundRow( - rowForDb, - myNodeNum, - meshcoreSenderName, - 'acked', - res.packetId != null ? res.packetId >>> 0 : undefined, - ); - } - if (isMeshtastic && meshtasticTempPacketId != null) { - const rowPacketId = res.packetId ?? meshtasticTempPacketId; - void window.electronAPI.db - .updateMessageStatus(rowPacketId, 'acked') - .catch((e: unknown) => { - console.debug( - '[useSendMessage] updateMessageStatus failed ' + errLikeToLogString(e), - ); - }); - } - }) - .catch((e: unknown) => { - const errMsg = errLikeToLogString(e); - console.warn('[useSendMessage] send failed ' + errMsg); - updateMessageStatus(identityId, provisionalId, 'failed', errMsg); - if (identity.protocol.type === 'meshcore') { + if (isMeshcore && isMeshcoreTcpSoftApDeadAccepted()) { + void (async () => { + try { + await tryGetMeshcoreSession()?.ensureTcpLiveForUserTx?.(); + const liveHandle = connectionDriver.getHandle(identityId); + if (!liveHandle) { + throw new Error('MeshCore TCP live reopen produced no handle'); + } + finishSend(liveHandle, { trackForSoftApLiveWindow: true }); + } catch (e: unknown) { + const errMsg = errLikeToLogString(e); + console.warn('[useSendMessage] SoftAP live reopen failed ' + errMsg); + updateMessageStatus(identityId, provisionalId, 'failed', errMsg); persistMeshcoreOutboundRow(record, myNodeNum, meshcoreSenderName, 'failed'); } - if (isMeshtastic && meshtasticTempPacketId != null) { - void window.electronAPI.db - .updateMessageStatus(meshtasticTempPacketId, 'failed', errMsg) - .catch((dbErr: unknown) => { - console.debug( - '[useSendMessage] updateMessageStatus failed ' + errLikeToLogString(dbErr), - ); - }); - } - }); + })(); + return; + } + + finishSend(handle); }, [identityId, addToast, t], ); diff --git a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts index 69a270ef2..2dca3c5c7 100644 --- a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts +++ b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.test.ts @@ -104,6 +104,76 @@ describe('ReticulumDiagnosticEngine', () => { (r): r is RfDiagnosticRow => r.kind === 'rf' && r.condition === 'reticulum/tx-queue-drops', ); expect(dropRow?.severity).toBe('error'); + expect(dropRow?.causeI18n?.key).toBe('diagnosticsPanel.reticulum.runtime.txQueueDrops'); + }); + + it('uses BLE TX-drop cause key for ble:// RNodes', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + interfaces: [ + { + id: 'rnode-41f4', + name: 'RNode 41F4', + type: 'rnode', + enabled: true, + status: 'down', + serial_port: 'ble://eccf2847-e1fd-3f5f-0811-064db1639a3d', + }, + ], + interfaceIssueAlert: { + tcpConnectFailed: [], + txQueueDrops: [{ name: 'RNode 41F4', dropCount: 512 }], + linkDeliveryTimeouts: [], + bleBondRemoved: [], + blePairingTimedOut: [], + transportSaturatedCount: 0, + slowTransportQueryCount: 0, + suppressedCount: 0, + lastAtMs: Date.now(), + }, + }, + ); + const dropRow = rows.find( + (r): r is RfDiagnosticRow => r.kind === 'rf' && r.condition === 'reticulum/tx-queue-drops', + ); + expect(dropRow?.causeI18n?.key).toBe('diagnosticsPanel.reticulum.runtime.txQueueDropsBle'); + expect(dropRow?.reticulumRepairKind).toBe('edit'); + }); + + it('uses bond-stale TX-drop cause when bleBondRemoved co-occurs', () => { + const rows = buildReticulumDiagnosticRows( + { rns_ready: true, lxmf_ready: true, interface_count: 1, peer_count: 1 }, + { + interfaces: [ + { + id: 'rnode-41f4', + name: 'RNode 41F4', + type: 'rnode', + enabled: true, + status: 'down', + serial_port: 'ble://eccf2847-e1fd-3f5f-0811-064db1639a3d', + }, + ], + interfaceIssueAlert: { + tcpConnectFailed: [], + txQueueDrops: [{ name: 'RNode 41F4', dropCount: 512 }], + linkDeliveryTimeouts: [], + bleBondRemoved: ['RNode 41F4'], + blePairingTimedOut: [], + transportSaturatedCount: 0, + slowTransportQueryCount: 0, + suppressedCount: 0, + lastAtMs: Date.now(), + }, + }, + ); + const dropRow = rows.find( + (r): r is RfDiagnosticRow => r.kind === 'rf' && r.condition === 'reticulum/tx-queue-drops', + ); + expect(dropRow?.causeI18n?.key).toBe( + 'diagnosticsPanel.reticulum.runtime.txQueueDropsBleBondStale', + ); }); it('adds bleBondRemoved runtime rows from sidecar alerts', () => { diff --git a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts index e5f7a507d..be7ef4ae4 100644 --- a/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts +++ b/src/renderer/lib/diagnostics/ReticulumDiagnosticEngine.ts @@ -14,7 +14,9 @@ import { isReticulumInterfaceOnlineStatus, isReticulumLocalSerialInterface, isReticulumRemoteInterfaceType, + resolveReticulumTxDropHintKind, type ReticulumLocalInterfaceInput, + reticulumTxDropDiagnosticsCauseKey, } from '@/renderer/lib/reticulum/reticulumLocalInterfaceHealth'; import { isPropagationSyncEstablishingStuck, @@ -107,6 +109,9 @@ export const RETICULUM_RUNTIME_CAUSE_I18N_KEYS = [ 'diagnosticsPanel.reticulum.runtime.interfaceDown', 'diagnosticsPanel.reticulum.runtime.tcpConnectFailed', 'diagnosticsPanel.reticulum.runtime.txQueueDrops', + 'diagnosticsPanel.reticulum.runtime.txQueueDropsBle', + 'diagnosticsPanel.reticulum.runtime.txQueueDropsBleBondStale', + 'diagnosticsPanel.reticulum.runtime.txQueueDropsNeutral', 'diagnosticsPanel.reticulum.runtime.bleBondRemoved', 'diagnosticsPanel.reticulum.runtime.blePairingTimedOut', 'diagnosticsPanel.reticulum.runtime.noPeers', @@ -326,20 +331,26 @@ export function buildReticulumDiagnosticRows( } for (const drop of interfaceIssueAlert.txQueueDrops) { const iface = ifaceByName.get(drop.name); + const hintKind = resolveReticulumTxDropHintKind( + drop.name, + healthInterfaces, + interfaceIssueAlert.bleBondRemoved, + ); + const causeKey = reticulumTxDropDiagnosticsCauseKey(hintKind); rows.push({ kind: 'rf', id: rfRowId(homeNodeId, `reticulum/tx-queue-drops/${drop.name}`), nodeId: homeNodeId, condition: 'reticulum/tx-queue-drops', cause: `Interface "${drop.name}" dropped ${drop.dropCount} outbound packets (TX queue full)`, - causeI18n: runtimeCauseI18n('txQueueDrops', { + causeI18n: runtimeCauseI18n(causeKey, { name: drop.name, count: String(drop.dropCount), }), severity: 'error', detectedAt: now, reticulumInterfaceId: iface?.id, - reticulumRepairKind: 'disable', + reticulumRepairKind: hintKind === 'ble' || hintKind === 'bleBondStale' ? 'edit' : 'disable', }); } for (const name of interfaceIssueAlert.bleBondRemoved) { diff --git a/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.test.ts b/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.test.ts index f43d186a1..9aa918105 100644 --- a/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.test.ts +++ b/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest'; +import { MESHCORE_CAPABILITIES, MESHTASTIC_CAPABILITIES } from '../radio/BaseRadioProvider'; import type { MeshNode } from '../types'; import { + analyzeNode, detectBadRoute, detectHopGoblin, detectImpossibleHop, @@ -274,3 +276,130 @@ describe('detectPathInstability', () => { expect(detectPathInstability(1, timestamps)).toBeNull(); }); }); + +describe('analyzeNode distance-based hop anomalies', () => { + const nearbyNode = () => + baseNode({ + snr: 6, + hops_away: 5, + latitude: 37.0, + longitude: -122.0, + }); + const home = () => + baseNode({ + node_id: 1, + latitude: 37.001, + longitude: -122.001, + }); + + it('returns hop_goblin for Meshtastic nearby multi-hop node', () => { + const a = analyzeNode( + nearbyNode(), + undefined, + home(), + [], + false, + 1, + 0, + 2, + MESHTASTIC_CAPABILITIES, + ); + expect(a).not.toBeNull(); + expect(a!.type).toBe('hop_goblin'); + }); + + it('skips hop_goblin for MeshCore nearby multi-hop node', () => { + const a = analyzeNode( + nearbyNode(), + undefined, + home(), + [], + false, + 1, + 0, + 2, + MESHCORE_CAPABILITIES, + ); + expect(a).toBeNull(); + }); + + it('skips close-in bad_route warning for MeshCore', () => { + // ~5 km away: outside hop_goblin's 3 km floor, inside close-in bad_route (~5 mi / ~8 km) + // with hops > hopsThreshold+2. + const node = baseNode({ + hops_away: 5, + latitude: 37.045, + longitude: -122.0, + }); + const homeNode = baseNode({ + node_id: 1, + latitude: 37.0, + longitude: -122.0, + }); + const meshtastic = analyzeNode( + node, + undefined, + homeNode, + [], + false, + 1, + 0, + 2, + MESHTASTIC_CAPABILITIES, + ); + expect(meshtastic).not.toBeNull(); + expect(meshtastic!.type).toBe('bad_route'); + expect(meshtastic!.severity).toBe('warning'); + + const meshcore = analyzeNode( + node, + undefined, + homeNode, + [], + false, + 1, + 0, + 2, + MESHCORE_CAPABILITIES, + ); + expect(meshcore).toBeNull(); + }); + + it('still returns weak_link for MeshCore when trace SNR is present', () => { + const a = analyzeNode( + nearbyNode(), + undefined, + home(), + [], + false, + 1, + 0, + 2, + MESHCORE_CAPABILITIES, + undefined, + [5, -8, 3], + ); + expect(a).not.toBeNull(); + expect(a!.type).toBe('weak_link'); + }); + + it('still returns path_instability for MeshCore when PathUpdated bursts', () => { + const now = Date.now(); + const a = analyzeNode( + nearbyNode(), + undefined, + home(), + [], + false, + 1, + 0, + 2, + MESHCORE_CAPABILITIES, + undefined, + undefined, + [now - 1000, now - 2000, now - 3000, now - 4000], + ); + expect(a).not.toBeNull(); + expect(a!.type).toBe('route_flapping'); + }); +}); diff --git a/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts b/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts index 304cc3c6c..2ffe7dc31 100644 --- a/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts +++ b/src/renderer/lib/diagnostics/RoutingDiagnosticEngine.ts @@ -284,13 +284,12 @@ export function analyzeNode( : null; if (flapping) return flapping; - const hopGoblin = detectHopGoblin( - node, - homeNode, - distanceMultiplier, - distanceOffsetKm, - hopsThreshold, - ); + // MeshCore: nearby multi-hop contacts are poorly connected / repeater-mediated, + // not Meshtastic-style critical over-hopping — skip GPS+hop heuristics. + const hopGoblin = + capabilities?.hasDistanceBasedHopAnomalies === false + ? null + : detectHopGoblin(node, homeNode, distanceMultiplier, distanceOffsetKm, hopsThreshold); if (hopGoblin) return hopGoblin; // MeshCore per-hop SNR weak link (only when trace data is present) @@ -299,7 +298,9 @@ export function analyzeNode( if (weakLink) return weakLink; } - if (badRoute?.severity === 'warning') return badRoute; + if (badRoute?.severity === 'warning' && capabilities?.hasDistanceBasedHopAnomalies !== false) { + return badRoute; + } if (noisyNode) return noisyNode; diff --git a/src/renderer/lib/flasher/rnode.test.ts b/src/renderer/lib/flasher/rnode.test.ts index 19daf6139..4b2edb59b 100644 --- a/src/renderer/lib/flasher/rnode.test.ts +++ b/src/renderer/lib/flasher/rnode.test.ts @@ -2,6 +2,45 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { RNode, RNODE_BT_PAIRING_TIMEOUT_MS, RNODE_COMMAND_TIMEOUT_MS } from './rnode'; +function createMockSerialPort(): { + port: SerialPort; + pushFromDevice: (bytes: Uint8Array) => void; + written: Uint8Array[]; +} { + const written: Uint8Array[] = []; + let readableController: ReadableStreamDefaultController | null = null; + const readable = new ReadableStream({ + start(controller) { + readableController = controller; + }, + }); + const writable = new WritableStream({ + write(chunk) { + written.push(chunk); + }, + }); + const port = { + open: () => Promise.resolve(), + close: () => { + readableController?.close(); + return Promise.resolve(); + }, + get readable() { + return readable; + }, + get writable() { + return writable; + }, + } as unknown as SerialPort; + return { + port, + written, + pushFromDevice: (bytes) => { + readableController?.enqueue(bytes); + }, + }; +} + describe('RNode command timeouts', () => { it('exports positive default KISS and BT pairing timeouts', () => { expect(RNODE_COMMAND_TIMEOUT_MS).toBeGreaterThan(5_000); @@ -28,6 +67,22 @@ describe('RNode WiFi payloads', () => { }); }); +describe('RNode Bluetooth bond clear', () => { + it('writes framed CMD_BT_UNPAIR 0x01 via clearBluetoothBonds', async () => { + expect(RNode.CMD_BT_UNPAIR).toBe(0x70); + const { port, written } = createMockSerialPort(); + const rnode = await RNode.fromSerialPort(port); + await rnode.clearBluetoothBonds(); + const expected = RNode.createKissFrame([RNode.CMD_BT_UNPAIR, 0x01]); + expect( + written.some( + (chunk) => chunk.length === expected.length && chunk.every((b, i) => b === expected[i]), + ), + ).toBe(true); + await rnode.close(); + }); +}); + describe('RNode.startBluetoothPairing', () => { beforeEach(() => { vi.useFakeTimers(); @@ -37,45 +92,6 @@ describe('RNode.startBluetoothPairing', () => { vi.useRealTimers(); }); - function createMockSerialPort(): { - port: SerialPort; - pushFromDevice: (bytes: Uint8Array) => void; - written: Uint8Array[]; - } { - const written: Uint8Array[] = []; - let readableController: ReadableStreamDefaultController | null = null; - const readable = new ReadableStream({ - start(controller) { - readableController = controller; - }, - }); - const writable = new WritableStream({ - write(chunk) { - written.push(chunk); - }, - }); - const port = { - open: () => Promise.resolve(), - close: () => { - readableController?.close(); - return Promise.resolve(); - }, - get readable() { - return readable; - }, - get writable() { - return writable; - }, - } as unknown as SerialPort; - return { - port, - written, - pushFromDevice: (bytes) => { - readableController?.enqueue(bytes); - }, - }; - } - it('keeps the session open until CMD_BT_PIN arrives (not just the KISS write)', async () => { const { port, pushFromDevice, written } = createMockSerialPort(); const rnode = await RNode.fromSerialPort(port); diff --git a/src/renderer/lib/flasher/rnode.ts b/src/renderer/lib/flasher/rnode.ts index a23f86e6f..6cd74bb52 100644 --- a/src/renderer/lib/flasher/rnode.ts +++ b/src/renderer/lib/flasher/rnode.ts @@ -49,6 +49,8 @@ export class RNode { static readonly CMD_DISP_RCND = 0x68; static readonly CMD_BT_CTRL = 0x46; static readonly CMD_BT_PIN = 0x62; + /** Clear bonded hosts on the radio (`bt_debond_all` on ESP32 BLE). */ + static readonly CMD_BT_UNPAIR = 0x70; static readonly CMD_WIFI_MODE = 0x6a; static readonly CMD_WIFI_SSID = 0x6b; static readonly CMD_WIFI_PSK = 0x6c; @@ -432,6 +434,11 @@ export class RNode { await this.sendKissCommand([RNode.CMD_BT_CTRL, 0x01]); } + /** Clear the radio's BLE bond table (ESP32). Payload `0x01` matches RNode firmware. */ + async clearBluetoothBonds(): Promise { + await this.sendKissCommand([RNode.CMD_BT_UNPAIR, 0x01]); + } + async startBluetoothPairing(pinCallback: (pin: number) => void): Promise { // Wait for CMD_BT_PIN (or timeout) — not merely the KISS write — so callers that // close the port after this promise settles still receive the PIN over USB. diff --git a/src/renderer/lib/ingest/reticulumIngest.test.ts b/src/renderer/lib/ingest/reticulumIngest.test.ts index fadaf7a79..f2b4b0560 100644 --- a/src/renderer/lib/ingest/reticulumIngest.test.ts +++ b/src/renderer/lib/ingest/reticulumIngest.test.ts @@ -222,6 +222,49 @@ describe('reticulumIngest side effects — history stamp, no auto-contact', () = expect(stampHistoryPeer).toHaveBeenCalled(); }); + it('passes replacesMessageHash through to saveReticulumMessage as replaces_message_hash', async () => { + const selfHash = 'deadbeef'.repeat(4); + const peerHash = 'cafebabe'.repeat(4); + const messageHash = 'aa'.repeat(16); + const pendingId = 'reticulum-pending-1'; + const ingested = ingestReticulumLxmfPayloadWithSideEffects( + 'id-1', + { + sender_hash: selfHash, + sender_name: 'Me', + to_hash: peerHash, + text: 'outbound', + direction: 'outbound', + timestamp: 1_700_000_000_000, + message_hash: messageHash, + }, + { replacesMessageHash: pendingId }, + ); + expect(ingested).toBe(true); + await vi.waitFor(() => { + expect(saveReticulumMessage).toHaveBeenCalled(); + }); + const saveArg = saveReticulumMessage.mock.calls[0]?.[0] as Record; + expect(saveArg.message_hash).toBe(messageHash); + expect(saveArg.replaces_message_hash).toBe(pendingId); + }); + + it('omits replaces_message_hash when replacesMessageHash is absent', async () => { + const hash = 'allowedhash1234567890allowedhash12'; + ingestReticulumLxmfPayloadWithSideEffects('id-1', { + sender_hash: hash, + sender_name: 'Alice', + text: 'hello', + direction: 'inbound', + timestamp: 1_700_000_000_000, + }); + await vi.waitFor(() => { + expect(saveReticulumMessage).toHaveBeenCalled(); + }); + const saveArg = saveReticulumMessage.mock.calls[0]?.[0] as Record; + expect(saveArg).not.toHaveProperty('replaces_message_hash'); + }); + it('outbound LXMF stamps recipient history without is_contact', async () => { const selfHash = 'deadbeef'.repeat(4); const peerHash = 'cafebabe'.repeat(4); diff --git a/src/renderer/lib/meshcore/meshcoreTcpInitBurst.test.ts b/src/renderer/lib/meshcore/meshcoreTcpInitBurst.test.ts index cac0afca2..1e1a1aed9 100644 --- a/src/renderer/lib/meshcore/meshcoreTcpInitBurst.test.ts +++ b/src/renderer/lib/meshcore/meshcoreTcpInitBurst.test.ts @@ -2,9 +2,16 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { isMeshcoreTcpBurstDeadBridge, + isMeshcoreTcpSoftApDeadAccepted, + notifyMeshcoreTcpLiveForUserTx, notifyMeshcoreTcpWriteDead, + rejectMeshcoreTcpLiveForUserTx, + setMeshcoreTcpSoftApDeadAccepted, setMeshcoreTcpWriteDeadListener, shouldDeferMeshcoreTcpReconnectAfterBurst, + trackMeshcoreTcpUserTxSend, + waitForMeshcoreTcpLiveForUserTx, + yieldToMeshcoreTcpUserTxSends, } from './meshcoreTcpInitBurst'; describe('isMeshcoreTcpBurstDeadBridge', () => { @@ -107,6 +114,45 @@ describe('shouldDeferMeshcoreTcpReconnectAfterBurst', () => { }); }); +describe('meshcoreTcpSoftApDeadAccepted', () => { + afterEach(() => { + setMeshcoreTcpSoftApDeadAccepted(false); + }); + + it('defaults false and toggles', () => { + expect(isMeshcoreTcpSoftApDeadAccepted()).toBe(false); + setMeshcoreTcpSoftApDeadAccepted(true); + expect(isMeshcoreTcpSoftApDeadAccepted()).toBe(true); + setMeshcoreTcpSoftApDeadAccepted(false); + expect(isMeshcoreTcpSoftApDeadAccepted()).toBe(false); + }); +}); + +describe('SoftAP user-TX live window', () => { + afterEach(() => { + setMeshcoreTcpSoftApDeadAccepted(false); + rejectMeshcoreTcpLiveForUserTx(new Error('test cleanup')); + }); + + it('notifies waiters and yields tracked sends before continuing', async () => { + const order: string[] = []; + const live = waitForMeshcoreTcpLiveForUserTx(5_000).then(() => { + order.push('live'); + const send = Promise.resolve().then(() => { + order.push('sent'); + }); + trackMeshcoreTcpUserTxSend(send); + return send; + }); + await Promise.resolve(); + notifyMeshcoreTcpLiveForUserTx(); + await yieldToMeshcoreTcpUserTxSends(); + order.push('after-yield'); + await live; + expect(order).toEqual(['live', 'sent', 'after-yield']); + }); +}); + describe('notifyMeshcoreTcpWriteDead', () => { afterEach(() => { setMeshcoreTcpWriteDeadListener(null); diff --git a/src/renderer/lib/meshcore/meshcoreTcpInitBurst.ts b/src/renderer/lib/meshcore/meshcoreTcpInitBurst.ts index 4aeaddc3a..3fc9f2860 100644 --- a/src/renderer/lib/meshcore/meshcoreTcpInitBurst.ts +++ b/src/renderer/lib/meshcore/meshcoreTcpInitBurst.ts @@ -3,6 +3,8 @@ * Shared predicates for initConn / getChannels skip paths and reconnect deferral. */ +import { MS_PER_SECOND } from '@/shared/timeConstants'; + export function isMeshcoreTcpBurstDeadBridge(opts: { transportType: string; burstCaptured: boolean; @@ -44,6 +46,110 @@ type MeshcoreTcpWriteDeadListener = () => void; let meshcoreTcpWriteDeadListener: MeshcoreTcpWriteDeadListener | null = null; +/** + * SoftAP/OpenHop: peer FIN after contacts dump left a configured session with a dead bridge. + * Background writes (flood advert, outbox) must not call handleConnectionLost — that reconnects, + * companion FINs again after contacts, and loops forever. + */ +let meshcoreTcpSoftApDeadAccepted = false; + +export function setMeshcoreTcpSoftApDeadAccepted(accepted: boolean): void { + meshcoreTcpSoftApDeadAccepted = accepted; +} + +export function isMeshcoreTcpSoftApDeadAccepted(): boolean { + return meshcoreTcpSoftApDeadAccepted; +} + +/** SoftAP user TX: wait for getSelfInfo live window before getContacts / peer FIN. */ +export const MESHCORE_TCP_USER_TX_LIVE_TIMEOUT_MS = 20 * MS_PER_SECOND; + +interface TcpLiveWaiter { + resolve: () => void; + reject: (err: Error) => void; + timer: ReturnType; +} + +let tcpLiveWaiters: TcpLiveWaiter[] = []; +let inFlightUserTxSends: Promise[] = []; + +/** Chat send waits here until initConn releases the SoftAP live window (post-getSelfInfo). */ +export function waitForMeshcoreTcpLiveForUserTx( + timeoutMs: number = MESHCORE_TCP_USER_TX_LIVE_TIMEOUT_MS, +): Promise { + return new Promise((resolve, reject) => { + const waiter: TcpLiveWaiter = { + resolve: () => {}, + reject: () => {}, + timer: setTimeout(() => { + tcpLiveWaiters = tcpLiveWaiters.filter((w) => w !== waiter); + reject(new Error('MeshCore TCP live window timed out')); + }, timeoutMs), + }; + waiter.resolve = () => { + clearTimeout(waiter.timer); + resolve(); + }; + waiter.reject = (err) => { + clearTimeout(waiter.timer); + reject(err); + }; + tcpLiveWaiters.push(waiter); + }); +} + +/** initConn: unblock SoftAP user-TX waiters while the TCP socket is still live. */ +export function notifyMeshcoreTcpLiveForUserTx(): void { + const waiters = tcpLiveWaiters; + tcpLiveWaiters = []; + for (const w of waiters) { + w.resolve(); + } +} + +/** Reject waiters (reconnect exhausted / aborted). */ +export function rejectMeshcoreTcpLiveForUserTx(err: Error): void { + const waiters = tcpLiveWaiters; + tcpLiveWaiters = []; + for (const w of waiters) { + w.reject(err); + } +} + +/** Track an in-flight SoftAP user send so initConn can await it before getContacts. */ +export function trackMeshcoreTcpUserTxSend(sendPromise: Promise): void { + // Attach immediately so mockRejectedValue / sync rejects are not unhandled before await. + void sendPromise.then( + () => undefined, + () => undefined, + ); + inFlightUserTxSends.push(sendPromise); + void sendPromise.finally(() => { + inFlightUserTxSends = inFlightUserTxSends.filter((p) => p !== sendPromise); + }); +} + +/** + * After notifying live waiters, yield microtasks then await any tracked user sends. + * SoftAP companions often FIN immediately after getContacts — send must finish first. + * + * Ordering vs `ensureTcpLiveForUserTx` / `useSendMessage`: + * 1. initConn calls `notifyMeshcoreTcpLiveForUserTx()` (resolves waiters), + * 2. then `yieldToMeshcoreTcpUserTxSends()`. + * Waiters resume in `useSendMessage` after `ensureTcpLiveForUserTx` and only then call + * `trackMeshcoreTcpUserTxSend`. Two microtask hops let those continuations register before + * we snapshot `inFlightUserTxSends` — do not remove the hops without reworking registration + * to happen before the waiter resolves. + */ +export async function yieldToMeshcoreTcpUserTxSends(): Promise { + await Promise.resolve(); + await Promise.resolve(); + const pending = inFlightUserTxSends.slice(); + if (pending.length > 0) { + await Promise.allSettled(pending); + } +} + /** Runtime registers a latch so IpcTcpConnection write failures mark the bridge dead without waiting for IPC. */ export function setMeshcoreTcpWriteDeadListener( listener: MeshcoreTcpWriteDeadListener | null, diff --git a/src/renderer/lib/radio/BaseRadioProvider.ts b/src/renderer/lib/radio/BaseRadioProvider.ts index f8fd0d392..8e80ae093 100644 --- a/src/renderer/lib/radio/BaseRadioProvider.ts +++ b/src/renderer/lib/radio/BaseRadioProvider.ts @@ -10,7 +10,7 @@ import { RETICULUM_LXMF_PAYLOAD_LIMIT } from '../chatComposerLimits'; */ export interface ProtocolCapabilities { protocol: MeshProtocol; - /** Whether hops_away is populated for peers (Meshtastic: true; MeshCore: false) */ + /** Whether hops_away is populated for peers (Meshtastic / MeshCore: true; Reticulum: false) */ hasHopCount: boolean; /** [min, max] valid hop limit for this protocol */ hopLimitRange: [number, number]; @@ -32,6 +32,12 @@ export interface ProtocolCapabilities { hasTraceRoute: boolean; /** Whether per-hop SNR from tracePath is available (MeshCore unique strength) */ hasPerHopSnr: boolean; + /** + * Whether GPS+hop distance heuristics (hop_goblin, close-in bad_route warning) apply. + * Meshtastic: true. MeshCore: false — multi-hop nearby contacts are poorly connected / + * repeater-mediated, not Meshtastic-style critical over-hopping. + */ + hasDistanceBasedHopAnomalies: boolean; /** Whether battery level / voltage telemetry is available */ hasBatteryTelemetry: boolean; /** Whether repeater status (noise floor, air time, packet counts) is available */ @@ -170,6 +176,7 @@ export const MESHTASTIC_CAPABILITIES: ProtocolCapabilities = { hasModemPresets: true, hasTraceRoute: true, hasPerHopSnr: false, + hasDistanceBasedHopAnomalies: true, hasBatteryTelemetry: true, hasRepeaterStatus: false, hasOnDemandNodeStatus: false, @@ -247,6 +254,7 @@ export const MESHCORE_CAPABILITIES: ProtocolCapabilities = { hasModemPresets: false, hasTraceRoute: true, hasPerHopSnr: true, + hasDistanceBasedHopAnomalies: false, hasBatteryTelemetry: true, hasRepeaterStatus: true, hasOnDemandNodeStatus: true, @@ -323,6 +331,7 @@ export const RETICULUM_CAPABILITIES: ProtocolCapabilities = { hasModemPresets: false, hasTraceRoute: true, hasPerHopSnr: false, + hasDistanceBasedHopAnomalies: false, hasBatteryTelemetry: false, hasRepeaterStatus: true, hasOnDemandNodeStatus: false, diff --git a/src/renderer/lib/radio/protocol-capabilities.test.ts b/src/renderer/lib/radio/protocol-capabilities.test.ts index 8f3c4398e..3b3370176 100644 --- a/src/renderer/lib/radio/protocol-capabilities.test.ts +++ b/src/renderer/lib/radio/protocol-capabilities.test.ts @@ -31,6 +31,7 @@ const REQUIRED_CAPABILITY_KEYS: (keyof ProtocolCapabilities)[] = [ 'hasModemPresets', 'hasTraceRoute', 'hasPerHopSnr', + 'hasDistanceBasedHopAnomalies', 'hasBatteryTelemetry', 'hasRepeaterStatus', 'hasRoomServersPanel', @@ -133,6 +134,7 @@ describe('ProtocolCapabilities contract', () => { "hasDeviceRoleConfig": true, "hasDiagnosticsPanel": true, "hasDisplayConfig": true, + "hasDistanceBasedHopAnomalies": true, "hasEnvironmentTelemetry": true, "hasFactoryReset": true, "hasFirmwareUpdateCheck": true, @@ -216,6 +218,7 @@ describe('ProtocolCapabilities contract', () => { "hasDeviceRoleConfig": false, "hasDiagnosticsPanel": true, "hasDisplayConfig": false, + "hasDistanceBasedHopAnomalies": false, "hasEnvironmentTelemetry": true, "hasFactoryReset": false, "hasFirmwareUpdateCheck": true, @@ -305,6 +308,7 @@ describe('ProtocolCapabilities contract', () => { "hasDeviceRoleConfig": false, "hasDiagnosticsPanel": true, "hasDisplayConfig": false, + "hasDistanceBasedHopAnomalies": false, "hasEnvironmentTelemetry": false, "hasFactoryReset": false, "hasFirmwareUpdateCheck": false, diff --git a/src/renderer/lib/reticulum/reticulumBleBondDesync.test.ts b/src/renderer/lib/reticulum/reticulumBleBondDesync.test.ts new file mode 100644 index 000000000..42c64ea77 --- /dev/null +++ b/src/renderer/lib/reticulum/reticulumBleBondDesync.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { + getReticulumBleBondDesyncActive, + setReticulumBleBondDesyncActive, + subscribeReticulumBleBondDesync, +} from './reticulumBleBondDesync'; + +describe('reticulumBleBondDesync', () => { + afterEach(() => { + setReticulumBleBondDesyncActive(false); + }); + + it('notifies subscribers when the flag changes', () => { + const seen: boolean[] = []; + const unsub = subscribeReticulumBleBondDesync(() => { + seen.push(getReticulumBleBondDesyncActive()); + }); + setReticulumBleBondDesyncActive(true); + setReticulumBleBondDesyncActive(true); + setReticulumBleBondDesyncActive(false); + unsub(); + setReticulumBleBondDesyncActive(true); + setReticulumBleBondDesyncActive(false); + expect(seen).toEqual([true, false]); + }); +}); diff --git a/src/renderer/lib/reticulum/reticulumBleBondDesync.ts b/src/renderer/lib/reticulum/reticulumBleBondDesync.ts new file mode 100644 index 000000000..f0841ecc5 --- /dev/null +++ b/src/renderer/lib/reticulum/reticulumBleBondDesync.ts @@ -0,0 +1,26 @@ +/** + * Sticky flag while sidecar latched `bleBondRemoved` — yield watcher must not + * re-contend for Noble, and reconnect thrash should not hold the scan mutex. + */ + +let bondDesyncActive = false; +const listeners = new Set<() => void>(); + +export function getReticulumBleBondDesyncActive(): boolean { + return bondDesyncActive; +} + +export function setReticulumBleBondDesyncActive(active: boolean): void { + if (bondDesyncActive === active) return; + bondDesyncActive = active; + for (const listener of listeners) { + listener(); + } +} + +export function subscribeReticulumBleBondDesync(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} diff --git a/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.test.ts b/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.test.ts index 424a36474..b7058f5af 100644 --- a/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.test.ts +++ b/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.test.ts @@ -6,7 +6,10 @@ import { collectReticulumLocalInterfaceAlerts, collectReticulumLocalInterfaceConnecting, collectReticulumRemoteInterfaceAlerts, + resolveReticulumTxDropHintKind, reticulumLocalOfflineDisplayKind, + reticulumTxDropConnectionHintKey, + reticulumTxDropDiagnosticsCauseKey, } from './reticulumLocalInterfaceHealth'; const heltec: Parameters[0] = { @@ -211,3 +214,67 @@ describe('reticulumLocalInterfaceHealth', () => { expect(alerts.map((a) => a.reason).sort()).toEqual(['enabled_down', 'tcp_unreachable']); }); }); + +describe('resolveReticulumTxDropHintKind', () => { + const tcpHub = { + id: 'rmap', + name: 'RMAP World', + type: 'tcp', + enabled: true, + status: 'down', + host: 'rmap.world', + port: 4242, + }; + const bleRnode = { + id: 'rnode-41f4', + name: 'RNode 41F4', + type: 'rnode', + enabled: true, + status: 'down', + serial_port: 'ble://eccf2847-e1fd-3f5f-0811-064db1639a3d', + }; + const usbRnode = { + ...heltec, + name: 'Heltec USB', + serial_port: '/dev/cu.usbserial-7', + }; + const wifiRnode = { + ...heltec, + name: 'RNode WiFi', + serial_port: 'tcp://192.168.1.10:7633', + }; + + it('classifies TCP hubs as tcp', () => { + expect(resolveReticulumTxDropHintKind('RMAP World', [tcpHub])).toBe('tcp'); + expect(reticulumTxDropConnectionHintKey('tcp')).toBe('txQueueDropsHint'); + expect(reticulumTxDropDiagnosticsCauseKey('tcp')).toBe('txQueueDrops'); + }); + + it('classifies BLE RNodes as ble', () => { + expect(resolveReticulumTxDropHintKind('RNode 41F4', [bleRnode])).toBe('ble'); + expect(reticulumTxDropConnectionHintKey('ble')).toBe('txQueueDropsHintBle'); + }); + + it('prefers bleBondStale when name is in bleBondRemoved', () => { + expect(resolveReticulumTxDropHintKind('RNode 41F4', [bleRnode], ['RNode 41F4'])).toBe( + 'bleBondStale', + ); + expect(resolveReticulumTxDropHintKind('RNode 41F4', undefined, ['RNode 41F4'])).toBe( + 'bleBondStale', + ); + expect(resolveReticulumTxDropHintKind('RMAP World', [tcpHub], ['RMAP World'])).toBe( + 'bleBondStale', + ); + }); + + it('classifies USB and Wi-Fi RNodes as neutral', () => { + expect(resolveReticulumTxDropHintKind('Heltec USB', [usbRnode])).toBe('neutral'); + expect(resolveReticulumTxDropHintKind('RNode WiFi', [wifiRnode])).toBe('neutral'); + }); + + it('returns neutral when the row is missing or name mismatches', () => { + expect(resolveReticulumTxDropHintKind('Missing', [])).toBe('neutral'); + expect(resolveReticulumTxDropHintKind('Missing', undefined)).toBe('neutral'); + expect(resolveReticulumTxDropHintKind('Other', [tcpHub, bleRnode])).toBe('neutral'); + }); +}); diff --git a/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.ts b/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.ts index d8c76639d..930f05cdf 100644 --- a/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.ts +++ b/src/renderer/lib/reticulum/reticulumLocalInterfaceHealth.ts @@ -113,6 +113,71 @@ export function reticulumLocalOfflineDisplayKind( return isReticulumBleRnodeSerialPort(iface.serial_port) ? 'ble' : 'serial'; } +/** Transport/bond-aware remediation kind for sidecar TX-queue-full alerts. */ +export type ReticulumTxDropHintKind = 'bleBondStale' | 'ble' | 'tcp' | 'neutral'; + +/** + * Classify a TX-drop interface name for Connection/Diagnostics hints. + * `bleBondRemoved` wins even when the row is missing or mis-typed. + */ +export function resolveReticulumTxDropHintKind( + name: string, + interfaces: + readonly Pick[] | undefined, + bleBondRemovedNames?: readonly string[], +): ReticulumTxDropHintKind { + if (bleBondRemovedNames?.includes(name)) { + return 'bleBondStale'; + } + const row = interfaces?.find((iface) => iface.name === name); + if (!row) { + return 'neutral'; + } + if (isReticulumBleRnodeSerialPort(row.serial_port)) { + return 'ble'; + } + if (isReticulumRemoteInterfaceType(row.type)) { + return 'tcp'; + } + return 'neutral'; +} + +/** Connection-panel hint key for a TX-drop kind (under `connectionPanel.reticulumSidecarIssues`). */ +export function reticulumTxDropConnectionHintKey( + kind: ReticulumTxDropHintKind, +): + | 'txQueueDropsHint' + | 'txQueueDropsHintBle' + | 'txQueueDropsHintBleBondStale' + | 'txQueueDropsHintNeutral' { + switch (kind) { + case 'bleBondStale': + return 'txQueueDropsHintBleBondStale'; + case 'ble': + return 'txQueueDropsHintBle'; + case 'tcp': + return 'txQueueDropsHint'; + case 'neutral': + return 'txQueueDropsHintNeutral'; + } +} + +/** Diagnostics `diagnosticsPanel.reticulum.runtime.*` cause key suffix for a TX-drop kind. */ +export function reticulumTxDropDiagnosticsCauseKey( + kind: ReticulumTxDropHintKind, +): 'txQueueDrops' | 'txQueueDropsBle' | 'txQueueDropsBleBondStale' | 'txQueueDropsNeutral' { + switch (kind) { + case 'bleBondStale': + return 'txQueueDropsBleBondStale'; + case 'ble': + return 'txQueueDropsBle'; + case 'tcp': + return 'txQueueDrops'; + case 'neutral': + return 'txQueueDropsNeutral'; + } +} + export function classifyReticulumLocalInterface( iface: ReticulumLocalInterfaceInput, osSerialPorts: readonly string[], diff --git a/src/renderer/lib/reticulum/reticulumLxmfOutboundBackchannel.contract.test.ts b/src/renderer/lib/reticulum/reticulumLxmfOutboundBackchannel.contract.test.ts index 8add5dc31..baebb2c5f 100644 --- a/src/renderer/lib/reticulum/reticulumLxmfOutboundBackchannel.contract.test.ts +++ b/src/renderer/lib/reticulum/reticulumLxmfOutboundBackchannel.contract.test.ts @@ -32,6 +32,10 @@ describe('reticulum LXMF outbound Direct backchannel contracts', () => { expect(source).toContain('fn spawn_lxmf_outbound_backchannel'); expect(source).toContain('LXMF outbound-link backchannel packet'); expect(source).toContain('handle_link_delivered_data'); + expect(source).toContain('OUTBOUND_BACKCHANNEL_CAPACITY'); + expect(source).toMatch( + /mpsc::channel::<\(Vec, \[u8; 16\]\)>\(OUTBOUND_BACKCHANNEL_CAPACITY\)/, + ); }); it.skipIf(!existsSync(LXMF_LINK))( diff --git a/src/renderer/lib/reticulum/reticulumNobleBleYield.test.ts b/src/renderer/lib/reticulum/reticulumNobleBleYield.test.ts index 37f69144f..ad9585e06 100644 --- a/src/renderer/lib/reticulum/reticulumNobleBleYield.test.ts +++ b/src/renderer/lib/reticulum/reticulumNobleBleYield.test.ts @@ -262,6 +262,76 @@ describe('syncReticulumNobleBleYield', () => { expect(releaseReticulumBleRnodeConnect).not.toHaveBeenCalled(); expect(state.yieldActive).toBe(true); }); + + it('releases yield and does not re-prepare when bondDesyncActive', async () => { + const state: ReticulumNobleBleYieldMutableState = { yieldActive: true }; + await syncReticulumNobleBleYield( + { + sidecarActive: true, + interfaces: [BLE_ROW], + nowMs: Date.now(), + bleConnectGraceExpiresAt: Date.now() + 60_000, + bondDesyncActive: true, + }, + state, + ); + expect(state.yieldActive).toBe(false); + expect(releaseReticulumBleRnodeConnect).toHaveBeenCalled(); + expect(prepareReticulumBleRnodeConnect).not.toHaveBeenCalled(); + }); + + it('releases scanOwner=reticulum when bondDesyncActive even if yield inactive', async () => { + vi.mocked(window.electronAPI.bleCoexistence.getState).mockResolvedValue({ + connections: [], + scanOwner: 'reticulum', + }); + const state: ReticulumNobleBleYieldMutableState = { yieldActive: false }; + await syncReticulumNobleBleYield( + { + sidecarActive: true, + interfaces: [BLE_ROW], + nowMs: Date.now(), + bleConnectGraceExpiresAt: Date.now() + 60_000, + bondDesyncActive: true, + }, + state, + ); + expect(releaseReticulumBleRnodeConnect).toHaveBeenCalled(); + expect(prepareReticulumBleRnodeConnect).not.toHaveBeenCalled(); + }); + + it('does not reacquire lease when a stale prepare resolves after abort (bond-desync race)', async () => { + let resolvePrepare: ((value: boolean) => void) | undefined; + vi.mocked(prepareReticulumBleRnodeConnect).mockImplementation( + () => + new Promise((resolve) => { + resolvePrepare = resolve; + }), + ); + vi.mocked(window.electronAPI.bleCoexistence.getState).mockResolvedValue({ + connections: [], + scanOwner: null, + }); + const abort = new AbortController(); + const state: ReticulumNobleBleYieldMutableState = { yieldActive: false }; + const syncPromise = syncReticulumNobleBleYield( + { + sidecarActive: true, + interfaces: [BLE_ROW], + nowMs: Date.now(), + bleConnectGraceExpiresAt: Date.now() + 60_000, + signal: abort.signal, + }, + state, + ); + await Promise.resolve(); + expect(prepareReticulumBleRnodeConnect).toHaveBeenCalled(); + abort.abort(); + resolvePrepare?.(true); + await syncPromise; + expect(state.yieldActive).toBe(false); + expect(releaseReticulumBleRnodeConnect).toHaveBeenCalled(); + }); }); describe('reticulumBleRnodeOnline helpers', () => { diff --git a/src/renderer/lib/reticulum/reticulumNobleBleYield.ts b/src/renderer/lib/reticulum/reticulumNobleBleYield.ts index 7f67a1aa1..832c96fcc 100644 --- a/src/renderer/lib/reticulum/reticulumNobleBleYield.ts +++ b/src/renderer/lib/reticulum/reticulumNobleBleYield.ts @@ -17,6 +17,8 @@ export interface SyncReticulumNobleBleYieldInput { interfaces: readonly ReticulumInterfaceRow[]; nowMs: number; bleConnectGraceExpiresAt: number; + /** When true, never re-acquire Noble — stale OS bond must be Forget/re-paired first. */ + bondDesyncActive?: boolean; /** When aborted (e.g. watcher flipped active again), skip releasing after awaits. */ signal?: AbortSignal; } @@ -38,7 +40,28 @@ export async function syncReticulumNobleBleYield( input: SyncReticulumNobleBleYieldInput, state: ReticulumNobleBleYieldMutableState, ): Promise { - const { sidecarActive, interfaces, nowMs, bleConnectGraceExpiresAt, signal } = input; + const { sidecarActive, interfaces, nowMs, bleConnectGraceExpiresAt, bondDesyncActive, signal } = + input; + + if (bondDesyncActive) { + if (state.yieldActive) { + if (signal?.aborted) { + return; + } + state.yieldActive = false; + state.lastPrepareFailedAtMs = undefined; + await releaseReticulumBleRnodeConnect(); + return; + } + const coexist = await window.electronAPI.bleCoexistence.getState(); + if (signal?.aborted) { + return; + } + if (coexist.scanOwner === 'reticulum') { + await releaseReticulumBleRnodeConnect(); + } + return; + } if (!sidecarActive) { if (state.yieldActive) { @@ -75,12 +98,18 @@ export async function syncReticulumNobleBleYield( const graceExpired = nowMs >= bleConnectGraceExpiresAt; const coexist = await window.electronAPI.bleCoexistence.getState(); + if (signal?.aborted) { + return; + } const scanHeldByReticulum = coexist.scanOwner === 'reticulum'; // After connect grace, never re-contend for the adapter: an offline BLE RNode would // otherwise loop suspendNoble ↔ yield-released forever and starve Meshtastic/MeshCore. if (graceExpired && !scanHeldByReticulum) { if (state.yieldActive) { + if (signal?.aborted) { + return; + } state.yieldActive = false; state.lastPrepareFailedAtMs = undefined; await releaseReticulumBleRnodeConnect(); @@ -95,12 +124,21 @@ export async function syncReticulumNobleBleYield( return; } const acquired = await prepareReticulumBleRnodeConnect(); + if (signal?.aborted) { + if (acquired) { + await releaseReticulumBleRnodeConnect(); + } + return; + } if (!acquired) { state.lastPrepareFailedAtMs = nowMs; return; } state.lastPrepareFailedAtMs = undefined; } + if (signal?.aborted) { + return; + } state.yieldActive = true; } @@ -110,6 +148,9 @@ export async function syncReticulumNobleBleYield( // "BLE peripheral not found". const confirmedNoEnabledBleRnode = !hasEnabledBleRnode && interfaces.length > 0; if (state.yieldActive && (bleRnodeOnline || graceExpired || confirmedNoEnabledBleRnode)) { + if (signal?.aborted) { + return; + } state.yieldActive = false; state.lastPrepareFailedAtMs = undefined; await releaseReticulumBleRnodeConnect(); diff --git a/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.test.ts b/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.test.ts index 9634dc961..e9de2e7fa 100644 --- a/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.test.ts +++ b/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.test.ts @@ -258,4 +258,19 @@ describe('useReticulumNobleBleYieldWatcher lifecycle', () => { expect(inactiveInput?.signal?.aborted).toBe(true); resolveInactive?.(); }); + + it('passes AbortSignal on active ticks and aborts it on cleanup', async () => { + const { unmount } = renderHook(() => { + useReticulumNobleBleYieldWatcher(true); + }); + await vi.advanceTimersByTimeAsync(0); + const activeCall = syncReticulumNobleBleYieldMock.mock.calls.find( + (c) => (c[0] as { sidecarActive?: boolean }).sidecarActive === true, + ); + const activeInput = activeCall?.[0] as { signal?: AbortSignal } | undefined; + expect(activeInput?.signal).toBeInstanceOf(AbortSignal); + expect(activeInput?.signal?.aborted).toBe(false); + unmount(); + expect(activeInput?.signal?.aborted).toBe(true); + }); }); diff --git a/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.ts b/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.ts index 0b1973803..6798eee1e 100644 --- a/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.ts +++ b/src/renderer/lib/reticulum/useReticulumNobleBleYieldWatcher.ts @@ -3,6 +3,10 @@ import { useEffect, useRef, useState } from 'react'; import { useNowMs } from '@/renderer/hooks/useNowMs'; import { errLikeToLogString } from '@/renderer/lib/errLikeToLogString'; +import { + getReticulumBleBondDesyncActive, + subscribeReticulumBleBondDesync, +} from '@/renderer/lib/reticulum/reticulumBleBondDesync'; import { beginReticulumBleConnectGrace, clearReticulumBleConnectGrace, @@ -18,6 +22,7 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { const [bleConnectGraceExpiresAt, setBleConnectGraceExpiresAt] = useState(() => getReticulumBleConnectGraceExpiresAt(), ); + const [bondDesyncActive, setBondDesyncActive] = useState(() => getReticulumBleBondDesyncActive()); /** Synchronous mirror — React state lags a frame and can release a fresh suspend. */ const graceExpiresAtRef = useRef(getReticulumBleConnectGraceExpiresAt()); const yieldStateRef = useRef({ yieldActive: false }); @@ -31,6 +36,12 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { }); }, []); + useEffect(() => { + return subscribeReticulumBleBondDesync(() => { + setBondDesyncActive(getReticulumBleBondDesyncActive()); + }); + }, []); + useEffect(() => { if (sidecarActive) { const expires = beginReticulumBleConnectGrace(); @@ -64,7 +75,8 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { return; } - let cancelled = false; + const abort = new AbortController(); + const cancelledRef: { current: boolean } = { current: false }; const tick = async () => { try { @@ -75,6 +87,7 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { // grace stale). Do NOT renew while yield is still active — that infinitely extends // the hold when an offline BLE RNode never comes up and starves Meshtastic. if ( + !cancelledRef.current && coexist?.scanOwner === 'reticulum' && !yieldStateRef.current.yieldActive && (graceExpiresAt <= 0 || Date.now() >= graceExpiresAt) @@ -85,13 +98,15 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { } const interfaces = await fetchReticulumInterfaces(); - if (cancelled) return; + if (cancelledRef.current) return; await syncReticulumNobleBleYield( { sidecarActive: true, interfaces, nowMs: Date.now(), bleConnectGraceExpiresAt: graceExpiresAtRef.current, + bondDesyncActive: getReticulumBleBondDesyncActive(), + signal: abort.signal, }, yieldStateRef.current, ); @@ -106,8 +121,9 @@ export function useReticulumNobleBleYieldWatcher(sidecarActive: boolean): void { }, RETICULUM_LOCAL_HEALTH_FAST_POLL_MS); return () => { - cancelled = true; + cancelledRef.current = true; + abort.abort(); window.clearInterval(intervalId); }; - }, [sidecarActive, bleConnectGraceExpiresAt, nowMs]); + }, [sidecarActive, bleConnectGraceExpiresAt, nowMs, bondDesyncActive]); } diff --git a/src/renderer/lib/sessions/meshcoreSession.ts b/src/renderer/lib/sessions/meshcoreSession.ts index 2d7847c28..a2627b01e 100644 --- a/src/renderer/lib/sessions/meshcoreSession.ts +++ b/src/renderer/lib/sessions/meshcoreSession.ts @@ -27,6 +27,11 @@ export interface MeshcoreSessionApi { ) => Promise; /** RF contact pubkey for DM send when nodeStore has not been hydrated yet. */ getDestinationPubKey?: (nodeId: number) => Uint8Array | undefined; + /** + * SoftAP/OpenHop: when the TCP bridge was accepted dead after contacts FIN, reopen a live + * socket (reconnect) and resolve once getSelfInfo completes — before getContacts / peer FIN. + */ + ensureTcpLiveForUserTx?: () => Promise; } let activeSession: MeshcoreSessionApi | null = null; diff --git a/src/renderer/locales/cs/translation.json b/src/renderer/locales/cs/translation.json index 1f15e2d1a..04ac69321 100644 --- a/src/renderer/locales/cs/translation.json +++ b/src/renderer/locales/cs/translation.json @@ -1189,7 +1189,8 @@ "addDefaultHubsFailed": "Přidání výchozích páteří se nezdařilo.", "backboneEnableGuidanceLead": "Povolit maximálně 1 až 3 páteřní brány", "backboneEnableGuidanceBody": "—2 je ideální místo pro redundanci bez zbytečného nafukování šířky pásma. Zaměřte se na 1 globální rozbočovač a 1 regionální bránu a povolte speciální rozhraní (jako je I2P nebo Yggdrasil) přesně podle potřeby. Lokálně připojené RNodes a místní LAN rozhraní se do tohoto limitu páteřní sítě nezapočítávají.", - "defaultHubsPickerHint": "Začněte primární a globální páteří plus svým regionem. Zapněte nejvýše 1 až 3 páteřní brány (2 je optimum). Místní RNode a LAN rozhraní se nepočítají. Nové položky se přidávají vypnuté ke kontrole. Sync také vypne zbývající vyřazené oficiální testnet huby (Amsterdam)." + "defaultHubsPickerHint": "Začněte primární a globální páteří plus svým regionem. Zapněte nejvýše 1 až 3 páteřní brány (2 je optimum). Místní RNode a LAN rozhraní se nepočítají. Nové položky se přidávají vypnuté ke kontrole. Sync také vypne zbývající vyřazené oficiální testnet huby (Amsterdam).", + "rnodeTransportBleHint": "Ble vazby se mohou desynchronizovat s macOS (Forget + re-pair). Preferujte USB sériové nebo Wi-Fi (tcp://), pokud je rádio v blízkosti." }, "reticulumPeers": { "hops": "Hops", @@ -1290,9 +1291,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Sdílená instance je povolena — zcela ukončete ostatní aplikace Reticulum (MeshChatX, Ratspeak, samostatný rnsd) a restartujte tento zásobník před opakováním DM.", "bleBondRemoved": "{{name}}: Připojení Bluetooth je zastaralé (OS stále zobrazuje Spárováno)", - "bleBondRemovedHint": "Zapomeňte na tento RNode v Nastavení systému → Bluetooth, zahajte párování na rádiu (Admin → Bluetooth nebo podržení tlačítka ~7 s), restartujte zásobník a zadejte nový 6místný PIN.", + "bleBondRemovedHint": "Nejprve zapomeňte tento RNode v Nastavení systému → Bluetooth (macOS Paired automaticky nevymaže). Pokud můžete, preferujte USB serial nebo Wi‑Fi (tcp://), abyste se vyhnuli BLE vazbám. Poté: Admin → Bluetooth → Vymazat spárovaná zařízení (USB, ESP32) pokud je k dispozici, Spustit párování, odeberte a znovu přidejte toto BLE rozhraní (Vybrat zařízení), restartujte stack a zadejte nový 6místný PIN.", "blePairingTimedOut": "{{name}}: Vypršel časový limit výměny přístupového klíče Bluetooth", - "blePairingTimedOutHint": "Zastavte zásobník, spusťte párování znovu (Správce → Bluetooth zobrazí PIN na panelu — displej rádia může zůstat prázdný — nebo tlačítko ~7 s podržte pro PIN na obrazovce), restartujte zásobník jednou a zadejte tento PIN v dialogovém okně OS během ~60 sekund. Nikdy nepoužívejte 123456 (výchozí Meshtastic)." + "blePairingTimedOutHint": "Zastavte zásobník, spusťte párování znovu (Správce → Bluetooth zobrazí PIN na panelu — displej rádia může zůstat prázdný — nebo tlačítko ~7 s podržte pro PIN na obrazovce), restartujte zásobník jednou a zadejte tento PIN v dialogovém okně OS během ~60 sekund. Nikdy nepoužívejte 123456 (výchozí Meshtastic).", + "bleBondRemovedStopStack": "Zastavit zásobník", + "bleBondRemovedOpenAdmin": "Otevřít Bluetooth správce", + "txQueueDropsHintBle": "Tento ble RNode nemůže vypustit frontu TX (uvíznutý odkaz nebo mávání). Preferujte USB sériové nebo Wi-Fi (tcp://), nebo Zastavte zásobník a znovu spárujte po Zapomenout v Nastavení systému → Bluetooth.", + "txQueueDropsHintBleBondStale": "Fronta TX je plná, protože vazba ble je zastaralá. Zapomeňte na tento RNode v Systémovém nastavení → Bluetooth a poté použijte Správce → Bluetooth (Vymazat spárování / Spustit párování) nebo přepněte na USB.", + "txQueueDropsHintNeutral": "Tento kanál TX rozhraní je plný. Chcete-li zastavit kapky, zkontrolujte stav odkazu rozhraní (nebo jej zakažte)." }, "reticulumSidecarBundledMissing": "V této instalaci chybí přibalený Reticulum sidecar. Upgradujte na novější verzi nebo nahlaste problém – sestavení ze zdroje není v balených instalacích k dispozici.", "radioMqttRootDivergesWarning": "Kořenové téma rádia MQTT je {{radioRoot}}, ale tento panel odebírá {{appPrefix}}. Přizpůsobením rádia omezíte celostátní provoz uzlů — aktualizujte předponu tématu a znovu připojte MQTT.", @@ -1658,7 +1664,7 @@ "autoBeaconPhysicalFailures": "Beacon AutoInterface TX selhává na {{ifaces}} — lokální zjišťování LAN nemusí fungovat, dokud to nebude opraveno.", "tcpUnreachable": "Centrum TCP „{{name}}“ je nedosažitelné ({{host}}:{{port}})", "tcpConnectFailed": "Připojení TCP hub „{{name}}“ bylo odmítnuto nebo vypršel časový limit", - "txQueueDrops": "Rozhraní „{{name}}“ upustilo {{count}} odchozí pakety (fronta TX je plná)", + "txQueueDrops": "Rozhraní „{{name}}“ zahodilo {{count}} odchozích paketů (fronta TX plná) — často je stále zapnutý mrtvý TCP hub", "linkDeliveryTimeout": "Přímý odkaz LXMF na {{hash}}… vypršel ({{count}}×)", "transportSaturated": "Nasycený transport RNS ({{count}} LXMF kapek vyžadujících cestu)", "transportSaturatedShareInstance": "Nasycený transport RNS ({{count}} kapek s požadavkem na cestu). Sdílená instance je zapnutá — ukončete ostatní aplikace Reticulum (MeshChatX, Ratspeak, rnsd) a restartujte zásobník.", @@ -1678,7 +1684,10 @@ "announceBusPressureTipBoundaryHubs": "Povolené hraniční / páteřní rozbočovače: {{hubs}}.", "announceBusPressureTipTxSaturated": "Fronta TX klesá na: {{names}} — toto rozhraní může být nasycené.", "tooManyDefaultBackbones": "Máte zapnutou výchozí páteř {{count}}. Zvažte vypnutí doplňků a ponechání asi 2 pro nadbytečnost.", - "decommissionedHubEnabled": "Vyřazený hub „{{name}}“ je stále zapnutý — mesh-client ho při každém startu zásobníku vypne. Použijte «Přidat výchozí páteř» nebo adresář páteří pro živé koncové body." + "decommissionedHubEnabled": "Vyřazený hub „{{name}}“ je stále zapnutý — mesh-client ho při každém startu zásobníku vypne. Použijte «Přidat výchozí páteř» nebo adresář páteří pro živé koncové body.", + "txQueueDropsBle": "Rozhraní „{{name}}“ upustilo {{count}} odchozí pakety (fronta TX je plná) — ble RNode link uvízl; raději USB nebo znovu spárovat", + "txQueueDropsBleBondStale": "Rozhraní „{{name}}“ upustilo {{count}} odchozí pakety (fronta TX je plná) — zastaralá vazba ble; Zapomeňte v OS Bluetooth a znovu spárujte", + "txQueueDropsNeutral": "Rozhraní „{{name}}“ upustilo {{count}} odchozí pakety (fronta TX je plná) — zkontrolujte odkaz rozhraní" } }, "routingPort": { @@ -1851,7 +1860,12 @@ "pairingWaiting": "Čekání na spárování PIN z rádia přes USB...", "pairingPinLabel": "Párování prostřednictvím Bluetooth", "pairingPinEnterHint": "Po připojení zásobníku zadejte tento kód do dialogu Bluetooth operačního systému. Nepoužívejte 123456.", - "bluetoothHint": "Povolte Bluetooth v zařízení a pak použijte Spustit párování. Na tomto panelu se přes USB zobrazí 6místný PIN — displej rádia často zůstává prázdný. Když se Reticulum připojí přes ble, zadejte tento PIN do dialogu Bluetooth OS. RNode nemá žádný výchozí PIN (nepoužívejte 123456 — to je Meshtastic). Na zobrazovacích deskách může podržení tlačítka ~7 s také zobrazit ŠPENDLÍK na obrazovce. RNode se nemusí zobrazit v systémových nastaveních → Bluetooth až do úspěšného spojení." + "bluetoothHint": "Zapněte Bluetooth na zařízení a použijte Spustit párování. 6místný PIN se zobrazí v tomto panelu přes USB — displej rádia často zůstane prázdný. Zadejte tento PIN v dialogu Bluetooth OS, když se Reticulum připojí přes BLE. RNode nemá výchozí PIN (nepoužívejte 123456 — to je Meshtastic). Na deskách s displejem může ~7 s podržení tlačítka také zobrazit PIN. RNode se v Nastavení systému → Bluetooth nemusí objevit před úspěšnou vazbou. Vymazat spárovaná zařízení (ESP32) vymaže tabulku vazeb rádia přes USB před opětovným párováním; zařízení musíte stále Zapomenout v nastavení systému.", + "clearPairedDevices": "Vymazat spárovaná zařízení", + "clearPairedDevicesConfirmTitle": "Vymazat vazby RNode Bluetooth?", + "clearPairedDevicesConfirmMessage": "Odešle CMD_BT_UNPAIR přes USB a vymaže spárované hostitele na ESP32 RNode (bez efektu na některých nRF52 buildech). Před opětovným párováním musíte zařízení stále Zapomenout v Nastavení systému → Bluetooth.", + "clearPairedDevicesConfirm": "Vymazat Bluetooth vazby", + "clearPairedDevicesSuccess": "Požadováno vymazání tabulky Bluetooth vazeb rádia." }, "identityVault": { "title": "Trezor identity", diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index 57704149d..b26ec2c57 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -1186,7 +1186,8 @@ "addDefaultHubsFailed": "Standard-Backbones konnten nicht hinzugefügt werden.", "backboneEnableGuidanceLead": "Aktivieren Sie höchstens 1 bis 3 Backbone-Gateways", "backboneEnableGuidanceBody": "—2 ist optimal für Redundanz ohne unnötigen Bandbreitenverbrauch. Streben Sie 1 globalen Hub und 1 regionales Gateway an, und aktivieren Sie Spezialschnittstellen (wie I2P oder Yggdrasil) nur bei Bedarf. Lokal verbundene RNodes und lokale LAN-Schnittstellen zählen nicht zu diesem Backbone-Limit.", - "defaultHubsPickerHint": "Beginnen Sie mit Primär & Global plus Ihrer Region. Aktivieren Sie höchstens 1 bis 3 Backbone-Gateways (2 ist optimal). Lokale RNodes und LAN-Schnittstellen zählen nicht. Neue Einträge werden deaktiviert hinzugefügt, damit Sie sie prüfen können. Sync deaktiviert außerdem verbleibende außer Betrieb genommene offizielle Testnet-Hubs (Amsterdam)." + "defaultHubsPickerHint": "Beginnen Sie mit Primär & Global plus Ihrer Region. Aktivieren Sie höchstens 1 bis 3 Backbone-Gateways (2 ist optimal). Lokale RNodes und LAN-Schnittstellen zählen nicht. Neue Einträge werden deaktiviert hinzugefügt, damit Sie sie prüfen können. Sync deaktiviert außerdem verbleibende außer Betrieb genommene offizielle Testnet-Hubs (Amsterdam).", + "rnodeTransportBleHint": "BLE-Bindungen können mit macOS desynchronisiert werden (Forget + Re-Pair). Bevorzugen Sie USB seriell oder Wi-Fi (tcp://), wenn das Radio in der Nähe ist." }, "reticulumPeers": { "hops": "Hops", @@ -1287,9 +1288,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Share-Instanz ist aktiviert — beenden Sie vollständig andere Reticulum-Apps (MeshChatX, Ratspeak, Standalone-RNSD) und starten Sie diesen Stack neu, bevor Sie DMs erneut versuchen.", "bleBondRemoved": "{{name}}: Bluetooth-Verbindung ist veraltet (das Betriebssystem zeigt immer noch „Paired“ an)", - "bleBondRemovedHint": "Vergessen Sie diesen RNode in Systemeinstellungen → Bluetooth, starten Sie die Kopplung am Radio (Admin → Bluetooth oder ~7 Sekunden gedrückt halten), starten Sie den Stack neu und geben Sie die neue 6-stellige PIN ein.", + "bleBondRemovedHint": "Vergessen Sie diesen RNode zuerst in Systemeinstellungen → Bluetooth (macOS entfernt Paired nicht automatisch). Bevorzugen Sie wenn möglich USB-Serial oder Wi‑Fi (tcp://), um BLE-Bindungen zu vermeiden. Dann: Admin → Bluetooth → Gekoppelte Geräte löschen (USB, ESP32) falls verfügbar, Kopplung starten, diese BLE-Schnittstelle entfernen und neu hinzufügen (Gerät wählen), Stack neu starten und die neue 6-stellige PIN eingeben.", "blePairingTimedOut": "{{name}}: Zeitüberschreitung beim Austausch des Bluetooth-Passschlüssels", - "blePairingTimedOutHint": "Stoppen Sie den Stack, starten Sie die Kopplung erneut (Admin → Bluetooth zeigt die PIN im Bedienfeld an — das Radio-Display bleibt möglicherweise leer — oder halten Sie die ~7 s-Taste für eine PIN auf dem Bildschirm gedrückt), starten Sie den Stack einmal neu und geben Sie diese PIN innerhalb von ~60 Sekunden in das OS-Dialogfeld ein. Verwenden Sie niemals 123456 (Meshtastic-Standard)." + "blePairingTimedOutHint": "Stoppen Sie den Stack, starten Sie die Kopplung erneut (Admin → Bluetooth zeigt die PIN im Bedienfeld an — das Radio-Display bleibt möglicherweise leer — oder halten Sie die ~7 s-Taste für eine PIN auf dem Bildschirm gedrückt), starten Sie den Stack einmal neu und geben Sie diese PIN innerhalb von ~60 Sekunden in das OS-Dialogfeld ein. Verwenden Sie niemals 123456 (Meshtastic-Standard).", + "bleBondRemovedStopStack": "Stapel stoppen", + "bleBondRemovedOpenAdmin": "Admin-Bluetooth öffnen", + "txQueueDropsHintBle": "Dieser BLE-RNode kann seine TX-Warteschlange nicht leeren (Link hängt oder flattert). Bevorzugen Sie USB-Serial oder Wi‑Fi (tcp://), oder stoppen Sie den Stack und koppeln Sie erneut nach Vergessen in Systemeinstellungen → Bluetooth.", + "txQueueDropsHintBleBondStale": "Die TX-Warteschlange ist voll, weil die BLE-Bindung veraltet ist. Vergessen Sie diesen RNode in Systemeinstellungen → Bluetooth und nutzen Sie Admin → Bluetooth (Gekoppelte löschen / Kopplung starten) oder wechseln Sie zu USB.", + "txQueueDropsHintNeutral": "Dieser Schnittstellen-TX-Kanal ist voll. Überprüfen Sie die Verknüpfungsgesundheit der Schnittstelle (oder deaktivieren Sie sie), um das Herunterfallen zu stoppen." }, "reticulumSidecarBundledMissing": "Bei dieser Installation fehlt der mitgelieferte Reticulum-Sidecar. Führen Sie ein Upgrade auf eine neuere Version durch oder melden Sie das Problem – das Erstellen aus dem Quellcode ist bei Paketinstallationen nicht verfügbar.", "radioMqttRootDivergesWarning": "Das Hauptthema von Radio MQTT ist {{radioRoot}}, aber dieses Panel abonniert {{appPrefix}}. Passen Sie das Funkgerät an, um den landesweiten Knotenverkehr zu begrenzen — aktualisieren Sie das Themenpräfix und verbinden Sie MQTT erneut.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface-Beacon TX schlägt auf {{ifaces}} fehl — die lokale LAN-Erkennung funktioniert möglicherweise nicht, bis dies behoben ist.", "tcpUnreachable": "TCP-Hub „{{name}}“ ist nicht erreichbar ({{host}}:{{port}})", "tcpConnectFailed": "TCP Hub \"{{name}}\" Verbindung abgelehnt oder Zeitüberschreitung", - "txQueueDrops": "Schnittstelle „{{name}}“ abgeworfen {{count}} ausgehende Pakete (TX-Warteschlange voll)", + "txQueueDrops": "Schnittstelle „{{name}}“ hat {{count}} ausgehende Pakete verworfen (TX-Warteschlange voll) — oft ist ein toter TCP-Hub noch aktiviert", "linkDeliveryTimeout": "Direkter LXMF-Link zu {{hash}}… Zeitüberschreitung ({{count}}×)", "transportSaturated": "RNS-Transport gesättigt ({{count}} LXMF-Pfadanforderung fällt ab)", "transportSaturatedShareInstance": "RNS-Transport gesättigt ({{count}} Path-Request-Drops). Share-Instanz ist aktiviert — beenden Sie andere Reticulum-Apps (MeshChatX, Ratspeak, rnsd) und starten Sie den Stack neu.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Aktivierte Grenz- / Backbone-Hubs: {{hubs}}.", "announceBusPressureTipTxSaturated": "TX-Warteschlange fällt auf: {{names}} — diese Schnittstelle kann gesättigt sein.", "tooManyDefaultBackbones": "Du hast {{count}} Standard-Backbones aktiviert. Erwägen Sie, Extras zu deaktivieren und etwa 2 für die Redundanz aufzubewahren.", - "decommissionedHubEnabled": "Außer Betrieb genommener Hub „{{name}}“ ist noch aktiviert — mesh-client schaltet ihn bei jedem Stack-Start aus. Nutzen Sie „Standard-Backbones hinzufügen“ oder das Backbone-Verzeichnis für aktuelle Endpunkte." + "decommissionedHubEnabled": "Außer Betrieb genommener Hub „{{name}}“ ist noch aktiviert — mesh-client schaltet ihn bei jedem Stack-Start aus. Nutzen Sie „Standard-Backbones hinzufügen“ oder das Backbone-Verzeichnis für aktuelle Endpunkte.", + "txQueueDropsBle": "Schnittstelle \"{{name}}\" abgelegt {{count}} ausgehende Pakete (TX-Warteschlange voll) — BLE RNode-Link klemmt; lieber USB oder Re-Pair", + "txQueueDropsBleBondStale": "Schnittstelle „{{name}}“ abgelegt {{count}} ausgehende Pakete (TX-Warteschlange voll) — BLE-Bond veraltet; Bluetooth im Betriebssystem vergessen und neu koppeln", + "txQueueDropsNeutral": "Schnittstelle \"{{name}}\" hat {{count}} ausgehende Pakete abgelegt (TX-Warteschlange voll) — überprüfen Sie den Link dieser Schnittstelle" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Warten auf PAIRING-PIN vom Radio über USB...", "pairingPinLabel": "PIN-Code der Bluetooth-Kopplung", "pairingPinEnterHint": "Geben Sie diesen Code im Bluetooth-Dialog des Betriebssystems ein, wenn der Stack eine Verbindung herstellt. Verwenden Sie 123456 nicht.", - "bluetoothHint": "Aktivieren Sie Bluetooth auf dem Gerät und verwenden Sie dann Start Pairing. Die 6-stellige PIN wird in diesem Bedienfeld über USB angezeigt — das Radio-Display bleibt oft leer. Geben Sie diese PIN in den Bluetooth-Dialog Ihres Betriebssystems ein, wenn sich Reticulum über BLE verbindet. RNode hat keine Standard-PIN (verwenden Sie 123456 nicht — das ist Meshtastic). Auf Anzeigetafeln kann mit einer ~7 s-Taste auch ein PIN auf dem Bildschirm angezeigt werden. Der RNode wird möglicherweise erst nach einer erfolgreichen Verbindung in den Systemeinstellungen von → Bluetooth angezeigt." + "bluetoothHint": "Aktivieren Sie Bluetooth am Gerät und nutzen Sie Kopplung starten. Die 6-stellige PIN erscheint in diesem Panel über USB — das Funkdisplay bleibt oft leer. Geben Sie diese PIN im Bluetooth-Dialog des OS ein, wenn Reticulum über BLE verbindet. RNode hat keine Standard-PIN (nicht 123456 verwenden — das ist Meshtastic). Auf Display-Boards kann ~7 s Tastendruck ebenfalls eine PIN zeigen. Der RNode erscheint in Systemeinstellungen → Bluetooth möglicherweise erst nach erfolgreicher Bindung. Gekoppelte Geräte löschen (ESP32) löscht die Funk-Bindungstabelle über USB vor dem erneuten Koppeln; Sie müssen das Gerät in den Systemeinstellungen trotzdem Vergessen.", + "clearPairedDevices": "Gekoppelte Geräte löschen", + "clearPairedDevicesConfirmTitle": "RNode-Bluetooth-Bindungen löschen?", + "clearPairedDevicesConfirmMessage": "Sendet CMD_BT_UNPAIR über USB und löscht gebundene Hosts auf ESP32-RNodes (keine Wirkung bei manchen nRF52-Builds). Sie müssen das Gerät in Systemeinstellungen → Bluetooth trotzdem noch Vergessen, bevor Sie erneut koppeln.", + "clearPairedDevicesConfirm": "Bluetooth-Bindungen löschen", + "clearPairedDevicesSuccess": "Löschen der Funk-Bluetooth-Bindungstabelle angefordert." }, "identityVault": { "title": "Identitätstresor", diff --git a/src/renderer/locales/en/translation.json b/src/renderer/locales/en/translation.json index 158f914b2..8acf64410 100644 --- a/src/renderer/locales/en/translation.json +++ b/src/renderer/locales/en/translation.json @@ -936,8 +936,13 @@ "tcpConnectFailedHint": "Disable this interface in the list below or confirm the remote hub is running.", "txQueueDrops": "{{name}}: outbound packets dropped ({{count}} queued)", "txQueueDropsHint": "Usually caused by a dead TCP interface still enabled. Disable unreachable hubs to stop queue overflow.", + "txQueueDropsHintBle": "This BLE RNode cannot drain its TX queue (link stuck or flapping). Prefer USB serial or Wi-Fi (tcp://), or Stop stack and re-pair after Forget in System Settings → Bluetooth.", + "txQueueDropsHintBleBondStale": "TX queue is full because the BLE bond is stale. Forget this RNode in System Settings → Bluetooth, then use Admin → Bluetooth (Clear paired / Start pairing) or switch to USB.", + "txQueueDropsHintNeutral": "This interface TX channel is full. Check that interface’s link health (or disable it) to stop drops.", "bleBondRemoved": "{{name}}: Bluetooth bond is stale (OS still shows Paired)", - "bleBondRemovedHint": "Forget this RNode in System Settings → Bluetooth, start pairing on the radio (Admin → Bluetooth or ~7 s button hold), restart the stack, and enter the new 6-digit PIN.", + "bleBondRemovedHint": "Forget this RNode in System Settings → Bluetooth first (macOS will not clear Paired automatically). Prefer USB serial or Wi-Fi (tcp://) when possible to avoid BLE bonds. Then: Admin → Bluetooth → Clear paired devices (USB, ESP32) if available, Start pairing, remove and re-add this BLE interface (Pick device), restart the stack, and enter the new 6-digit PIN.", + "bleBondRemovedStopStack": "Stop stack", + "bleBondRemovedOpenAdmin": "Open Admin Bluetooth", "blePairingTimedOut": "{{name}}: Bluetooth passkey exchange timed out", "blePairingTimedOutHint": "Stop the stack, start pairing again (Admin → Bluetooth shows the PIN in the panel — the radio display may stay blank — or ~7 s button hold for an on-screen PIN), restart the stack once, and enter that PIN in the OS dialog within ~60 seconds. Never use 123456 (Meshtastic default).", "transportSaturated": "RNS transport saturated ({{count}} path-request drops)", @@ -1083,6 +1088,7 @@ "rnodeTransport": "Transport", "rnodeTransportSerial": "USB serial", "rnodeTransportBle": "Bluetooth", + "rnodeTransportBleHint": "BLE bonds can desync with macOS (Forget + re-pair). Prefer USB serial or Wi-Fi (tcp://) when the radio is nearby.", "rnodeTransportWifi": "Wi-Fi", "rnodeWifiHost": "Host or IP", "rnodeWifiHostPlaceholder": "192.168.1.42, rnode.local, or [fd00::1]", @@ -1698,7 +1704,10 @@ "tcpUnreachable": "TCP hub \"{{name}}\" is unreachable ({{host}}:{{port}})", "interfaceDown": "{{type}} interface \"{{name}}\" is enabled but {{status}}", "tcpConnectFailed": "TCP hub \"{{name}}\" connection refused or timed out", - "txQueueDrops": "Interface \"{{name}}\" dropped {{count}} outbound packets (TX queue full)", + "txQueueDrops": "Interface \"{{name}}\" dropped {{count}} outbound packets (TX queue full) — often a dead TCP hub still enabled", + "txQueueDropsBle": "Interface \"{{name}}\" dropped {{count}} outbound packets (TX queue full) — BLE RNode link stuck; prefer USB or re-pair", + "txQueueDropsBleBondStale": "Interface \"{{name}}\" dropped {{count}} outbound packets (TX queue full) — BLE bond stale; Forget in OS Bluetooth and re-pair", + "txQueueDropsNeutral": "Interface \"{{name}}\" dropped {{count}} outbound packets (TX queue full) — check that interface’s link", "bleBondRemoved": "BLE RNode \"{{name}}\" bond is stale — Forget in OS Bluetooth settings and re-pair", "blePairingTimedOut": "BLE RNode \"{{name}}\" passkey exchange timed out — enter the current PIN (not 123456) and re-pair", "linkDeliveryTimeout": "Direct LXMF link to {{hash}}… timed out ({{count}}×)", @@ -1833,10 +1842,15 @@ "readDisplay": "Read display", "dumpEeprom": "Dump EEPROM to log", "bluetoothTitle": "Bluetooth", - "bluetoothHint": "Enable Bluetooth on the device, then use Start pairing. The 6-digit PIN appears in this panel over USB — the radio display often stays blank. Enter that PIN in your OS Bluetooth dialog when Reticulum connects over BLE. RNode has no default PIN (do not use 123456 — that is Meshtastic). On display boards, a ~7 s button hold can also show a PIN on screen. The RNode may not appear in System Settings → Bluetooth until after a successful bond.", + "bluetoothHint": "Enable Bluetooth on the device, then use Start pairing. The 6-digit PIN appears in this panel over USB — the radio display often stays blank. Enter that PIN in your OS Bluetooth dialog when Reticulum connects over BLE. RNode has no default PIN (do not use 123456 — that is Meshtastic). On display boards, a ~7 s button hold can also show a PIN on screen. The RNode may not appear in System Settings → Bluetooth until after a successful bond. Clear paired devices (ESP32) clears the radio bond table over USB before re-pairing; you must still Forget the device in System Settings.", "enableBluetooth": "Enable Bluetooth", "disableBluetooth": "Disable Bluetooth", "startPairing": "Start pairing", + "clearPairedDevices": "Clear paired devices", + "clearPairedDevicesConfirmTitle": "Clear RNode Bluetooth bonds?", + "clearPairedDevicesConfirmMessage": "This sends CMD_BT_UNPAIR over USB and clears bonded hosts on ESP32 RNodes (no-op on some nRF52 builds). You must still Forget the device in System Settings → Bluetooth before re-pairing.", + "clearPairedDevicesConfirm": "Clear bonds", + "clearPairedDevicesSuccess": "Radio bond table clear requested.", "pairingWaiting": "Waiting for pairing PIN from the radio over USB…", "pairingPinLabel": "Bluetooth pairing PIN", "pairingPin": "Bluetooth pairing PIN: {{pin}}", diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index 7c57aeec7..7776262b0 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "No se han podido añadir los backbones predeterminados.", "backboneEnableGuidanceLead": "Habilite de 1 a 3 puertas de enlace troncales como máximo", "backboneEnableGuidanceBody": "—2 es el punto óptimo para la redundancia sin una saturación innecesaria del ancho de banda. Apunte a 1 centro global y 1 puerta de enlace regional, y habilite interfaces especializadas (como I2P o Yggdrasil) estrictamente según sea necesario. Los RNodes conectados localmente y las interfaces LAN locales no cuentan para este límite de red troncal.", - "defaultHubsPickerHint": "Empiece con Primario y global más su región. Active como máximo 1 a 3 puertas de enlace de backbone (2 es el punto óptimo). Los RNodes locales y las interfaces LAN no cuentan. Las entradas nuevas se añaden desactivadas para que pueda revisarlas. Sync también desactiva hubs de testnet oficiales fuera de servicio restantes (Ámsterdam)." + "defaultHubsPickerHint": "Empiece con Primario y global más su región. Active como máximo 1 a 3 puertas de enlace de backbone (2 es el punto óptimo). Los RNodes locales y las interfaces LAN no cuentan. Las entradas nuevas se añaden desactivadas para que pueda revisarlas. Sync también desactiva hubs de testnet oficiales fuera de servicio restantes (Ámsterdam).", + "rnodeTransportBleHint": "Los enlaces ble pueden desincronizarse con macOS (olvidar + volver a emparejar). Prefiera USB serie o Wi-Fi (tcp://) cuando la radio esté cerca." }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "La instancia compartida está habilitada: salga por completo de otras aplicaciones de Reticulum (MeshChatX, Ratspeak, rnsd independiente) y reinicie esta pila antes de volver a intentar DM.", "bleBondRemoved": "{{name}}: El enlace Bluetooth está obsoleto (el sistema operativo todavía muestra Emparejado)", - "bleBondRemovedHint": "Olvídese de este RNode en Configuración del sistema → Bluetooth, comience a emparejar en la radio (Admin → Bluetooth o mantenga presionado el botón durante ~7 s), reinicie la pila e ingrese el nuevo PIN de 6 dígitos.", + "bleBondRemovedHint": "Olvide primero este RNode en Configuración del sistema → Bluetooth (macOS no quita Paired automáticamente). Prefiera USB serie o Wi‑Fi (tcp://) cuando pueda para evitar vínculos BLE. Luego: Admin → Bluetooth → Borrar dispositivos emparejados (USB, ESP32) si está disponible, Iniciar emparejamiento, quite y vuelva a añadir esta interfaz BLE (Elegir dispositivo), reinicie la pila e introduzca el nuevo PIN de 6 dígitos.", "blePairingTimedOut": "{{name}}: Se agotó el tiempo de espera para el intercambio de clave de acceso de Bluetooth", - "blePairingTimedOutHint": "Detenga la pila, comience a emparejar nuevamente (Admin → Bluetooth muestra el PIN en el panel; la pantalla de la radio puede permanecer en blanco, o mantenga presionado el botón ~7 s para obtener un PIN en pantalla), reinicie la pila una vez e ingrese ese PIN en el cuadro de diálogo del sistema operativo dentro de ~60 segundos. Nunca use 123456 (valor predeterminado de Meshtastic)." + "blePairingTimedOutHint": "Detenga la pila, comience a emparejar nuevamente (Admin → Bluetooth muestra el PIN en el panel; la pantalla de la radio puede permanecer en blanco, o mantenga presionado el botón ~7 s para obtener un PIN en pantalla), reinicie la pila una vez e ingrese ese PIN en el cuadro de diálogo del sistema operativo dentro de ~60 segundos. Nunca use 123456 (valor predeterminado de Meshtastic).", + "bleBondRemovedStopStack": "Detener pila", + "bleBondRemovedOpenAdmin": "Abrir Admin Bluetooth", + "txQueueDropsHintBle": "Este RNode ble no puede drenar su cola TX (enlace atascado o aleteando). Prefiera USB serie o Wi-Fi (tcp://), o Detenga la pila y vuelva a emparejar después de Olvidar en la configuración del sistema → Bluetooth.", + "txQueueDropsHintBleBondStale": "La cola TX está llena porque el vínculo BLE está obsoleto. Olvide este RNode en Configuración del sistema → Bluetooth y use Admin → Bluetooth (Borrar emparejados / Iniciar emparejamiento) o cambie a USB.", + "txQueueDropsHintNeutral": "Este canal TX de interfaz está lleno. Compruebe el estado del enlace de esa interfaz (o desactívelo) para detener las caídas." }, "reticulumSidecarBundledMissing": "A esta instalación le falta el sidecar Reticulum incluido. Actualice a una versión más reciente o informe el problema: la compilación desde el código fuente no está disponible en instalaciones empaquetadas.", "radioMqttRootDivergesWarning": "El tema raíz de MQTT de radio es {{radioRoot}}, pero este panel se suscribe a {{appPrefix}}. Haga coincidir la radio para limitar el tráfico de nodos a nivel nacional: actualice el prefijo del tema y vuelva a conectar MQTT.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "La baliza de interfaz automática TX está fallando en {{ifaces}}; es posible que el descubrimiento de LAN local no funcione hasta que se solucione.", "tcpUnreachable": "No se puede acceder al centro TCP \"{{name}}\" ({{host}}:{{port}})", "tcpConnectFailed": "Conexión TCP Hub \"{{name}}\" rechazada o agotada", - "txQueueDrops": "La interfaz \"{{name}}\" eliminó {{count}} paquetes salientes (cola TX llena)", + "txQueueDrops": "La interfaz \"{{name}}\" descartó {{count}} paquetes salientes (cola TX llena) — a menudo un hub TCP muerto sigue habilitado", "linkDeliveryTimeout": "Enlace directo de LXMF a {{hash}}… agotado ({{count}}×)", "transportSaturated": "Transporte RNS saturado (caídas de solicitud de ruta {{count}} LXMF)", "transportSaturatedShareInstance": "Transporte RNS saturado (caídas de solicitud de ruta {{count}}). La instancia de Share está activada: salga de otras aplicaciones de Reticulum (MeshChatX, Ratspeak, rnsd) y reinicie la pila.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Hubs de límite / backbone habilitados: {{hubs}}.", "announceBusPressureTipTxSaturated": "La cola TX cae en: {{names}} — esa interfaz puede estar saturada.", "tooManyDefaultBackbones": "Tienes {{count}} backbones predeterminados habilitados. Considere deshabilitar los extras y mantener alrededor de 2 por redundancia.", - "decommissionedHubEnabled": "El hub fuera de servicio «{{name}}» sigue habilitado: mesh-client lo desactiva en cada arranque del stack. Use «Añadir backbones predeterminados» o el directorio de backbones para endpoints activos." + "decommissionedHubEnabled": "El hub fuera de servicio «{{name}}» sigue habilitado: mesh-client lo desactiva en cada arranque del stack. Use «Añadir backbones predeterminados» o el directorio de backbones para endpoints activos.", + "txQueueDropsBle": "Interfaz \"{{name}}\" eliminada {{count}} paquetes salientes (cola TX llena) — Enlace ble RNode bloqueado; prefiera USB o volver a emparejar", + "txQueueDropsBleBondStale": "Interfaz \"{{name}}\" eliminada {{count}} paquetes salientes (cola TX llena) — enlace ble obsoleto; Olvídese de Bluetooth del sistema operativo y vuelva a emparejar", + "txQueueDropsNeutral": "La interfaz \"{{name}}\" ha eliminado {{count}} paquetes salientes (cola TX llena): comprueba el enlace de esa interfaz" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Esperando el PIN de emparejamiento de la radio a través de USB...", "pairingPinLabel": "PIN de emparejamiento Bluetooth", "pairingPinEnterHint": "Ingrese este código en el cuadro de diálogo Bluetooth del sistema operativo cuando se conecte la pila. No utilice 123456.", - "bluetoothHint": "Habilite Bluetooth en el dispositivo, luego use Iniciar emparejamiento. El PIN de 6 dígitos aparece en este panel a través de USB; la pantalla de la radio a menudo permanece en blanco. Ingrese ese PIN en el cuadro de diálogo Bluetooth de su sistema operativo cuando Reticulum se conecte a través de BLE. RNode no tiene un PIN predeterminado (no use 123456, es decir, Meshtastic). En los paneles de visualización, mantener presionado un botón durante aproximadamente 7 segundos también puede mostrar un PIN en la pantalla. Es posible que el RNode no aparezca en Configuración del sistema → Bluetooth hasta después de una vinculación exitosa." + "bluetoothHint": "Habilite Bluetooth en el dispositivo y use Iniciar emparejamiento. El PIN de 6 dígitos aparece en este panel por USB — la pantalla de la radio suele quedar en blanco. Introduzca ese PIN en el diálogo Bluetooth del sistema cuando Reticulum se conecte por BLE. RNode no tiene PIN predeterminado (no use 123456 — eso es Meshtastic). En placas con pantalla, mantener ~7 s el botón también puede mostrar un PIN. Es posible que el RNode no aparezca en Configuración del sistema → Bluetooth hasta después de un vínculo exitoso. Borrar dispositivos emparejados (ESP32) limpia la tabla de vínculos de la radio por USB antes de re-emparejar; aún debe Olvidar el dispositivo en Configuración del sistema.", + "clearPairedDevices": "Borrar dispositivos emparejados", + "clearPairedDevicesConfirmTitle": "¿Borrar enlaces Bluetooth RNode?", + "clearPairedDevicesConfirmMessage": "Esto envía CMD_BT_UNPAIR a través de USB y borra los hosts vinculados en los RNodes ESP32 (sin efecto en algunas compilaciones nRF52). Aun así debe olvidar el dispositivo en Configuración del sistema → Bluetooth antes de volver a emparejarlo.", + "clearPairedDevicesConfirm": "Borrar vínculos Bluetooth", + "clearPairedDevicesSuccess": "Se solicitó borrar la tabla de vínculos Bluetooth de la radio." }, "identityVault": { "title": "Repositorio de Identidades", @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "¿Quieres eliminar esta sesión de juego de tu lista? Esto no se puede deshacer.", "claimThreefold": "Reclamación triple", "claimThreefoldAria": "Reivindicar el sorteo de repetición triple", - "claimFiftyMove": "Reclamar 50-move", + "claimFiftyMove": "Reclamar regla de 50 movimientos", "claimFiftyMoveAria": "Reclama el sorteo de la regla de cincuenta movimientos", "delivery": { "sending": "Enviando", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index 855182ff9..1cbaa267f 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -1187,7 +1187,8 @@ "addDefaultHubsFailed": "Échec de l'ajout des backbones par défaut.", "backboneEnableGuidanceLead": "Activer 1 à 3 passerelles dorsales au maximum", "backboneEnableGuidanceBody": "—2 est l’optimum pour la redondance sans gaspiller la bande passante. Visez 1 hub mondial et 1 passerelle régionale, et activez les interfaces spécialisées (comme I2P ou Yggdrasil) seulement si besoin. Les RNodes connectés localement et les interfaces LAN locales ne comptent pas dans cette limite de backbone.", - "defaultHubsPickerHint": "Commencez par Primaire et mondial plus votre région. Activez au plus 1 à 3 passerelles backbone (2 est l’optimum). Les RNodes locaux et les interfaces LAN ne comptent pas. Les nouvelles entrées sont ajoutées désactivées pour révision. Sync désactive aussi les hubs de testnet officiels hors service restants (Amsterdam)." + "defaultHubsPickerHint": "Commencez par Primaire et mondial plus votre région. Activez au plus 1 à 3 passerelles backbone (2 est l’optimum). Les RNodes locaux et les interfaces LAN ne comptent pas. Les nouvelles entrées sont ajoutées désactivées pour révision. Sync désactive aussi les hubs de testnet officiels hors service restants (Amsterdam).", + "rnodeTransportBleHint": "Les liaisons ble peuvent se désynchroniser avec macOS (Forget + re-pair). Préférez USB série ou Wi-Fi (tcp ://) lorsque la radio est à proximité." }, "reticulumPeers": { "hops": "Hops", @@ -1288,9 +1289,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "L'instance de partage est activée — quittez complètement les autres applications Reticulum (MeshChatX, Ratspeak, rnsd autonome) et redémarrez cette pile avant de réessayer les DM.", "bleBondRemoved": "{{name}} : la liaison Bluetooth est obsolète (le système d'exploitation affiche toujours \"Jumelé\")", - "bleBondRemovedHint": "Oubliez ce RNode dans Paramètres système → Bluetooth, démarrez l'appairage sur la radio (Admin → Bluetooth ou maintien du bouton ~ 7 s), redémarrez la pile et saisissez le nouveau code PIN à 6 chiffres.", + "bleBondRemovedHint": "Oubliez d’abord ce RNode dans Réglages Système → Bluetooth (macOS n’efface pas Paired automatiquement). Préférez si possible USB série ou Wi‑Fi (tcp://) pour éviter les liaisons BLE. Puis : Admin → Bluetooth → Effacer les appareils appairés (USB, ESP32) si disponible, Démarrer l’appairage, retirez et rajoutez cette interface BLE (Choisir l’appareil), redémarrez la pile et saisissez le nouveau PIN à 6 chiffres.", "blePairingTimedOut": "{{name}} : Échange de clé Bluetooth expiré", - "blePairingTimedOutHint": "Arrêtez la pile, recommencez le couplage (Admin → Bluetooth affiche le CODE PIN dans le panneau — l'écran de la radio peut rester vide — ou maintenez le bouton enfoncé pendant ~7 s pour un CODE PIN à l'écran), redémarrez la pile une fois et entrez ce CODE PIN dans la boîte de dialogue du système d'exploitation dans ~60 secondes. N'utilisez jamais 123456 (Meshtastic par défaut)." + "blePairingTimedOutHint": "Arrêtez la pile, recommencez le couplage (Admin → Bluetooth affiche le CODE PIN dans le panneau — l'écran de la radio peut rester vide — ou maintenez le bouton enfoncé pendant ~7 s pour un CODE PIN à l'écran), redémarrez la pile une fois et entrez ce CODE PIN dans la boîte de dialogue du système d'exploitation dans ~60 secondes. N'utilisez jamais 123456 (Meshtastic par défaut).", + "bleBondRemovedStopStack": "Arrêt de la pile", + "bleBondRemovedOpenAdmin": "Ouvrir Admin Bluetooth", + "txQueueDropsHintBle": "Ce RNode ble ne peut pas vider sa file d'attente TX (lien bloqué ou battement). Préférez USB série ou Wi-Fi (tcp ://), ou Arrêter la pile et réappairer après Oublier dans les paramètres système → Bluetooth.", + "txQueueDropsHintBleBondStale": "La file TX est pleine car la liaison BLE est obsolète. Oubliez ce RNode dans Réglages Système → Bluetooth, puis utilisez Admin → Bluetooth (Effacer appairés / Démarrer l’appairage) ou passez en USB.", + "txQueueDropsHintNeutral": "Ce canal TX d'interface est plein. Vérifiez la santé du lien de cette interface (ou désactivez-la) pour arrêter les gouttes." }, "reticulumSidecarBundledMissing": "Cette installation ne dispose pas du side-car Reticulum fourni. Effectuez une mise à niveau vers une version plus récente ou signalez le problème : la création à partir des sources n'est pas disponible dans les installations packagées.", "radioMqttRootDivergesWarning": "Le sujet racine du MQTT radio est {{radioRoot}}, mais ce panneau est abonné à {{appPrefix}}. Faites correspondre la radio pour limiter le trafic des nœuds à l'échelle nationale — mettez à jour le préfixe du sujet et reconnectez le MQTT.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "La balise AutoInterface TX échoue sur {{ifaces}} — la découverte du LAN local peut ne pas fonctionner tant que cela n'est pas corrigé.", "tcpUnreachable": "Le concentrateur TCP « {{name}} » est inaccessible ({{host}} :{{port}})", "tcpConnectFailed": "La connexion du concentrateur TCP « {{name}} » a été refusée ou a expiré", - "txQueueDrops": "L'interface « {{name}} » a supprimé {{count}} paquets sortants (file d'attente TX pleine)", + "txQueueDrops": "L’interface « {{name}} » a abandonné {{count}} paquets sortants (file TX pleine) — souvent un hub TCP mort encore activé", "linkDeliveryTimeout": "Lien LXMF direct vers {{hash}}… expiré ({{count}}×)", "transportSaturated": "Transport RNS saturé ({{count}} LXMF path-request drops)", "transportSaturatedShareInstance": "RNS transport saturé ({{count}} path-request drops). L'instance de partage est activée — quittez les autres applications Reticulum (MeshChatX, Ratspeak, rnsd) et redémarrez la pile.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Hubs de frontière / dorsale activés : {{hubs}}.", "announceBusPressureTipTxSaturated": "La file d'attente TX tombe sur : {{names}} — cette interface peut être saturée.", "tooManyDefaultBackbones": "Vous avez activé les backbones par défaut {{count}}. Envisagez de désactiver les extras et de conserver environ 2 pour la redondance.", - "decommissionedHubEnabled": "Le hub hors service « {{name}} » est encore activé — mesh-client le désactive à chaque démarrage de la pile. Utilisez « Ajouter des backbones par défaut » ou le répertoire des backbones pour des points de terminaison actifs." + "decommissionedHubEnabled": "Le hub hors service « {{name}} » est encore activé — mesh-client le désactive à chaque démarrage de la pile. Utilisez « Ajouter des backbones par défaut » ou le répertoire des backbones pour des points de terminaison actifs.", + "txQueueDropsBle": "Interface \"{{name}}\" abandonnée {{count}} paquets sortants (file d'attente TX pleine) — liaison ble RNode bloquée ; préférez USB ou réappairer", + "txQueueDropsBleBondStale": "Interface \"{{name}}\" abandonnée {{count}} paquets sortants (file d'attente TX pleine) — ble bond stale ; Oubliez dans le système d'exploitation Bluetooth et réappareillez", + "txQueueDropsNeutral": "L'interface \"{{name}}\" a abandonné {{count}} paquets sortants (file d'attente TX pleine) — vérifiez le lien de cette interface" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Attente du CODE PIN d'appairage de la radio sur USB…", "pairingPinLabel": "APPAIRAGE BLUETOOTH", "pairingPinEnterHint": "Entrez ce code dans la boîte de dialogue Bluetooth du système d'exploitation lorsque la pile se connecte. N'utilisez pas 123456.", - "bluetoothHint": "Activez le Bluetooth sur l'appareil, puis utilisez l'appairage Démarrer. Le code PIN à 6 chiffres apparaît dans ce panneau via USB — l'écran de la radio reste souvent vide. Entrez ce CODE PIN dans la boîte de dialogue Bluetooth de votre système d'exploitation lorsque Reticulum se connecte via ble. RNode n'a pas de CODE PIN par défaut (n'utilisez pas 123456 — c'est Meshtastic). Sur les tableaux d'affichage, un maintien de bouton de ~7 s peut également afficher un CODE PIN à l'écran. Le RNode peut ne pas apparaître dans les paramètres système → Bluetooth avant une liaison réussie." + "bluetoothHint": "Activez le Bluetooth sur l’appareil, puis utilisez Démarrer l’appairage. Le PIN à 6 chiffres apparaît dans ce panneau via USB — l’écran radio reste souvent vide. Saisissez ce PIN dans la boîte de dialogue Bluetooth de l’OS lorsque Reticulum se connecte en BLE. RNode n’a pas de PIN par défaut (n’utilisez pas 123456 — c’est Meshtastic). Sur les cartes avec écran, un appui ~7 s peut aussi afficher un PIN. Le RNode peut ne pas apparaître dans Réglages Système → Bluetooth avant une liaison réussie. Effacer les appareils appairés (ESP32) efface la table de liaisons radio via USB avant réappariement ; vous devez toujours Oublier l’appareil dans les réglages système.", + "clearPairedDevices": "Effacer les appareils appariés", + "clearPairedDevicesConfirmTitle": "Effacer les liaisons Bluetooth RNode ?", + "clearPairedDevicesConfirmMessage": "Envoie CMD_BT_UNPAIR via USB et efface les hôtes liés sur les RNodes ESP32 (sans effet sur certaines builds nRF52). Vous devez quand même Oublier l’appareil dans Réglages Système → Bluetooth avant de réapparier.", + "clearPairedDevicesConfirm": "Effacer les liaisons Bluetooth", + "clearPairedDevicesSuccess": "Effacement de la table de liaisons Bluetooth de la radio demandé." }, "identityVault": { "title": "Coffre-fort d'identité", @@ -5281,7 +5295,7 @@ "promotionChooserAria": "Choisir un article promotionnel", "promoteTo": { "q": "Promouvoir en reine", - "r": "Promouvoir auprès de", + "r": "Promouvoir en tour", "b": "Promouvoir évêque", "n": "Promouvoir au rang de chevalier" } @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "Supprimer cette session de jeu de votre liste ? Cela ne peut pas être annulé.", "claimThreefold": "Réclamer trois fois", "claimThreefoldAria": "Réclamez trois fois le tirage de répétition", - "claimFiftyMove": "Revendication 50-move", + "claimFiftyMove": "Réclamer la règle des 50 coups", "claimFiftyMoveAria": "Réclamer le tirage au sort de la règle de", "delivery": { "sending": "Envoi en cours...", diff --git a/src/renderer/locales/id/translation.json b/src/renderer/locales/id/translation.json index c868d2e39..3778d6bf2 100644 --- a/src/renderer/locales/id/translation.json +++ b/src/renderer/locales/id/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "Gagal menambahkan tulang punggung default.", "backboneEnableGuidanceLead": "Aktifkan 1 hingga 3 gateway backbone paling banyak", "backboneEnableGuidanceBody": "—2 adalah titik ideal untuk redundansi tanpa pemborosan bandwidth. Bidik 1 hub global dan 1 gateway regional, dan aktifkan antarmuka khusus (seperti I2P atau Yggdrasil) hanya jika diperlukan. RNode yang terhubung lokal dan antarmuka LAN lokal tidak dihitung dalam batas backbone ini.", - "defaultHubsPickerHint": "Mulai dengan Utama & Global plus wilayah Anda. Aktifkan paling banyak 1 hingga 3 gateway backbone (2 adalah titik ideal). RNode lokal dan antarmuka LAN tidak dihitung. Entri baru ditambahkan dalam keadaan nonaktif agar bisa ditinjau. Sync juga menonaktifkan sisa hub testnet resmi yang sudah dinonaktifkan (Amsterdam)." + "defaultHubsPickerHint": "Mulai dengan Utama & Global plus wilayah Anda. Aktifkan paling banyak 1 hingga 3 gateway backbone (2 adalah titik ideal). RNode lokal dan antarmuka LAN tidak dihitung. Entri baru ditambahkan dalam keadaan nonaktif agar bisa ditinjau. Sync juga menonaktifkan sisa hub testnet resmi yang sudah dinonaktifkan (Amsterdam).", + "rnodeTransportBleHint": "Ikatan BLE dapat desinkronisasi dengan macOS (Forget + re-pair). Lebih suka USB serial atau Wi-Fi (tcp://) saat radio ada di dekatnya." }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "Aktor transportasi sidecar dapat terjepit — nyalakan kembali tumpukan Reticulum.", "shareInstanceHint": "Contoh berbagi diaktifkan — sepenuhnya keluar dari aplikasi Reticulum lainnya (MeshChatX, Ratspeak, rnsd mandiri) dan mulai ulang tumpukan ini sebelum mencoba DM lagi.", "bleBondRemoved": "{{name}}: Ikatan Bluetooth sudah basi (OS masih menunjukkan Dipasangkan)", - "bleBondRemovedHint": "Lupakan RNode ini di Pengaturan Sistem → Bluetooth, mulai memasangkan di radio (Admin → Bluetooth atau tombol tahan ~7 detik), mulai ulang tumpukan, dan masukkan PIN 6 digit baru.", + "bleBondRemovedHint": "Lupakan dulu RNode ini di Pengaturan Sistem → Bluetooth (macOS tidak menghapus Paired otomatis). Lebih baik USB serial atau Wi‑Fi (tcp://) jika memungkinkan agar menghindari ikatan BLE. Lalu: Admin → Bluetooth → Hapus perangkat terpasang (USB, ESP32) jika tersedia, Mulai pairing, hapus dan tambahkan lagi antarmuka BLE ini (Pilih perangkat), mulai ulang stack, dan masukkan PIN 6 digit baru.", "blePairingTimedOut": "{{name}}: Waktu pertukaran kunci sandi Bluetooth habis", - "blePairingTimedOutHint": "Hentikan tumpukan, mulai memasangkan lagi (Admin → Bluetooth menampilkan PIN di panel — tampilan radio mungkin tetap kosong — atau tombol ~7 detik ditahan untuk PIN di layar), mulai ulang tumpukan satu kali, dan masukkan PIN tersebut dalam dialog OS dalam waktu ~60 detik. Jangan pernah menggunakan 123456 (default Meshtastic)." + "blePairingTimedOutHint": "Hentikan tumpukan, mulai memasangkan lagi (Admin → Bluetooth menampilkan PIN di panel — tampilan radio mungkin tetap kosong — atau tombol ~7 detik ditahan untuk PIN di layar), mulai ulang tumpukan satu kali, dan masukkan PIN tersebut dalam dialog OS dalam waktu ~60 detik. Jangan pernah menggunakan 123456 (default Meshtastic).", + "bleBondRemovedStopStack": "Hentikan tumpukan", + "bleBondRemovedOpenAdmin": "Buka Bluetooth Admin", + "txQueueDropsHintBle": "BLE RNode ini tidak dapat menguras antrean TX-nya (tautan macet atau flapping). Lebih suka USB serial atau Wi-Fi (tcp://), atau Stop stack dan re-pair setelah Forget di Pengaturan Sistem → Bluetooth.", + "txQueueDropsHintBleBondStale": "Antrian TX penuh karena ikatan BLE sudah usang. Lupakan RNode ini di Pengaturan Sistem → Bluetooth, lalu gunakan Admin → Bluetooth (Hapus terpasang / Mulai pairing) atau beralih ke USB.", + "txQueueDropsHintNeutral": "Saluran TX antarmuka ini penuh. Periksa kesehatan tautan antarmuka itu (atau nonaktifkan) untuk menghentikan tetesan." }, "reticulumSidecarBundledMissing": "Instalasi ini tidak memiliki sespan Reticulum yang dibundel. Tingkatkan versi ke rilis yang lebih baru atau laporkan masalahnya — pembuatan dari sumber tidak tersedia dalam paket instalasi.", "radioMqttRootDivergesWarning": "Topik utama Radio MQTT adalah {{radioRoot}} tetapi panel ini berlangganan {{appPrefix}}. Cocokkan radio untuk membatasi lalu lintas simpul nasional — perbarui Prefiks Topik dan sambungkan kembali MQTT.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "Suar Antarmuka Otomatis TX gagal pada {{ifaces}} — penemuan Lan lokal mungkin tidak berfungsi sampai ini diperbaiki.", "tcpUnreachable": "Hub TCP \"{{name}}\" tidak dapat dijangkau ({{host}}:{{port}})", "tcpConnectFailed": "Koneksi hub TCP \"{{name}}\" ditolak atau kehabisan waktu", - "txQueueDrops": "Antarmuka \"{{name}}\" menjatuhkan paket keluar {{count}} (antrean TX penuh)", + "txQueueDrops": "Antarmuka \"{{name}}\" menjatuhkan {{count}} paket keluar (antrian TX penuh) — sering hub TCP mati yang masih diaktifkan", "linkDeliveryTimeout": "Tautan LXMF langsung ke {{hash}}… waktu habis ({{count}}×)", "transportSaturated": "Transportasi RNS jenuh ({{count}} permintaan jalur LXMF turun)", "transportSaturatedShareInstance": "Transportasi RNS jenuh ({{count}} permintaan jalur turun). Bagikan instance aktif — keluar dari aplikasi Reticulum lainnya (MeshChatX, Ratspeak, rnsd) dan mulai ulang tumpukan.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Mengaktifkan hub batas / tulang punggung: {{hubs}}.", "announceBusPressureTipTxSaturated": "Antrean TX turun pada: {{names}} — antarmuka itu mungkin jenuh.", "tooManyDefaultBackbones": "Anda telah mengaktifkan {{count}} tulang punggung default. Pertimbangkan untuk menonaktifkan ekstra dan menyimpan sekitar 2 untuk redundansi.", - "decommissionedHubEnabled": "Hub yang dinonaktifkan «{{name}}» masih aktif — mesh-client mematikannya setiap kali stack dimulai. Gunakan «Tambahkan backbone default» atau direktori backbone untuk endpoint aktif." + "decommissionedHubEnabled": "Hub yang dinonaktifkan «{{name}}» masih aktif — mesh-client mematikannya setiap kali stack dimulai. Gunakan «Tambahkan backbone default» atau direktori backbone untuk endpoint aktif.", + "txQueueDropsBle": "Antarmuka \"{{name}}\" menjatuhkan paket keluar {{count}} (antrean TX penuh) — tautan BLE RNode macet; lebih suka USB atau pasang ulang", + "txQueueDropsBleBondStale": "Antarmuka \"{{name}}\" menjatuhkan paket keluar {{count}} (antrean TX penuh) — ikatan BLE basi; Lupakan di OS Bluetooth dan pasangkan kembali", + "txQueueDropsNeutral": "Antarmuka \"{{name}}\" menjatuhkan paket keluar {{count}} (antrean TX penuh) — periksa tautan antarmuka tersebut" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Menunggu penyandingan PIN dari radio melalui USB…", "pairingPinLabel": "PIN penyandingan Bluetooth", "pairingPinEnterHint": "Masukkan kode ini dalam dialog OS Bluetooth ketika tumpukan terhubung. Jangan gunakan 123456.", - "bluetoothHint": "Aktifkan Bluetooth di perangkat, lalu gunakan Mulai memasangkan. PIN 6 digit muncul di panel ini melalui USB — tampilan radio sering kali tetap kosong. Masukkan PIN tersebut dalam dialog Bluetooth OS Anda saat Reticulum terhubung melalui BLE. RNode tidak memiliki PIN default (jangan gunakan 123456 — yaitu Meshtastic). Pada papan tampilan, tombol yang ditahan ~7 detik juga dapat menampilkan PIN di layar. RNode mungkin tidak muncul di Pengaturan Sistem → Bluetooth sampai ikatan berhasil." + "bluetoothHint": "Aktifkan Bluetooth di perangkat, lalu gunakan Mulai pairing. PIN 6 digit muncul di panel ini melalui USB — layar radio sering tetap kosong. Masukkan PIN itu di dialog Bluetooth OS saat Reticulum terhubung lewat BLE. RNode tidak punya PIN bawaan (jangan gunakan 123456 — itu Meshtastic). Di papan berlayar, tahan tombol ~7 dtk juga bisa menampilkan PIN. RNode mungkin belum muncul di Pengaturan Sistem → Bluetooth sebelum ikatan berhasil. Hapus perangkat terpasang (ESP32) membersihkan tabel ikatan radio lewat USB sebelum pairing ulang; Anda tetap harus Lupakan perangkat di Pengaturan Sistem.", + "clearPairedDevices": "Bersihkan perangkat yang dipasangkan", + "clearPairedDevicesConfirmTitle": "Bersihkan ikatan Bluetooth RNode?", + "clearPairedDevicesConfirmMessage": "Ini mengirim CMD_BT_UNPAIR melalui USB dan menghapus host yang terikat di RNode ESP32 (tanpa efek pada beberapa build nRF52). Anda tetap harus Lupakan perangkat di Pengaturan Sistem → Bluetooth sebelum memasangkan ulang.", + "clearPairedDevicesConfirm": "Hapus ikatan Bluetooth", + "clearPairedDevicesSuccess": "Permintaan hapus tabel ikatan Bluetooth radio dikirim." }, "identityVault": { "title": "Kubah identitas", @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "Hapus sesi permainan ini dari daftar Anda? Ini tidak dapat diurungkan.", "claimThreefold": "Klaim tiga kali lipat", "claimThreefoldAria": "Klaim undian pengulangan tiga kali lipat", - "claimFiftyMove": "Klaim 50 - move", + "claimFiftyMove": "Klaim aturan 50 langkah", "claimFiftyMoveAria": "Klaim undian aturan lima puluh langkah", "delivery": { "sending": "Mengirim...", diff --git a/src/renderer/locales/it/translation.json b/src/renderer/locales/it/translation.json index ef7659518..6ced3c3b6 100644 --- a/src/renderer/locales/it/translation.json +++ b/src/renderer/locales/it/translation.json @@ -1186,7 +1186,8 @@ "addDefaultHubsFailed": "Impossibile aggiungere dorsali predefinite.", "backboneEnableGuidanceLead": "Abilita da 1 a 3 gateway backbone al massimo", "backboneEnableGuidanceBody": "—2 è l’optimum per la ridondanza senza spreco di banda. Punta a 1 hub globale e 1 gateway regionale, e abilita interfacce speciali (come I2P o Yggdrasil) solo se necessario. Gli RNode collegati in locale e le interfacce LAN locali non contano verso questo limite di dorsale.", - "defaultHubsPickerHint": "Inizia con Primaria e globale più la tua regione. Abilita al massimo da 1 a 3 gateway di dorsale (2 è l’optimum). Gli RNode locali e le interfacce LAN non contano. Le nuove voci vengono aggiunte disabilitate per la revisione. Sync disabilita anche gli hub testnet ufficiali fuori servizio rimanenti (Amsterdam)." + "defaultHubsPickerHint": "Inizia con Primaria e globale più la tua regione. Abilita al massimo da 1 a 3 gateway di dorsale (2 è l’optimum). Gli RNode locali e le interfacce LAN non contano. Le nuove voci vengono aggiunte disabilitate per la revisione. Sync disabilita anche gli hub testnet ufficiali fuori servizio rimanenti (Amsterdam).", + "rnodeTransportBleHint": "I legami BLE possono essere desincronizzati con macOS (Forget + re-pair). Preferire USB seriale o Wi-Fi (tcp://) quando la radio è nelle vicinanze." }, "reticulumPeers": { "hops": "Hops", @@ -1287,9 +1288,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "L'istanza di condivisione è abilitata: chiudi completamente altre app Reticulum (MeshChatX, Ratspeak, rnsd autonomo) e riavvia questo stack prima di riprovare i DM.", "bleBondRemoved": "{{name}}: il collegamento Bluetooth è obsoleto (il sistema operativo mostra ancora Accoppiato)", - "bleBondRemovedHint": "Dimentica questo RNode in Impostazioni di sistema → Bluetooth, avvia l'accoppiamento sulla radio (Amministrazione → Bluetooth o tieni premuto il pulsante per ~7 s), riavvia lo stack e inserisci il nuovo PIN di 6 cifre.", + "bleBondRemovedHint": "Dimentica prima questo RNode in Impostazioni di Sistema → Bluetooth (macOS non rimuove Paired automaticamente). Preferisci se possibile USB seriale o Wi‑Fi (tcp://) per evitare legami BLE. Poi: Admin → Bluetooth → Cancella dispositivi accoppiati (USB, ESP32) se disponibile, Avvia accoppiamento, rimuovi e riaggiungi questa interfaccia BLE (Scegli dispositivo), riavvia lo stack e inserisci il nuovo PIN a 6 cifre.", "blePairingTimedOut": "{{name}}: Scambio passkey Bluetooth scaduto", - "blePairingTimedOutHint": "Arresta lo stack, avvia nuovamente l'accoppiamento (Admin → Bluetooth mostra il PIN nel pannello — il display della radio potrebbe rimanere vuoto — o tieni premuto il pulsante per circa 7 secondi per un PIN sullo schermo), riavvia lo stack una volta e inserisci il PIN nella finestra di dialogo del sistema operativo entro ~60 secondi. Non utilizzare mai 123456 (impostazione predefinita Meshtastic)." + "blePairingTimedOutHint": "Arresta lo stack, avvia nuovamente l'accoppiamento (Admin → Bluetooth mostra il PIN nel pannello — il display della radio potrebbe rimanere vuoto — o tieni premuto il pulsante per circa 7 secondi per un PIN sullo schermo), riavvia lo stack una volta e inserisci il PIN nella finestra di dialogo del sistema operativo entro ~60 secondi. Non utilizzare mai 123456 (impostazione predefinita Meshtastic).", + "bleBondRemovedStopStack": "Arresta stack", + "bleBondRemovedOpenAdmin": "Apri Admin Bluetooth", + "txQueueDropsHintBle": "Questo BLE RNode non può scaricare la sua coda TX (collegamento bloccato o sbattimento). Preferire USB seriale o Wi-Fi (tcp://), o interrompere lo stack e ricollegare dopo aver dimenticato in Impostazioni di sistema → Bluetooth.", + "txQueueDropsHintBleBondStale": "La coda TX è piena perché il legame BLE è scaduto. Dimenticare questo RNode in Impostazioni di sistema → Bluetooth, quindi utilizzare Admin → Bluetooth (Cancella accoppiamento / Avvia accoppiamento) o passare a USB.", + "txQueueDropsHintNeutral": "Questo canale TX di interfaccia è pieno. Controllare lo stato del collegamento dell'interfaccia (o disabilitarlo) per arrestare le cadute." }, "reticulumSidecarBundledMissing": "In questa installazione manca il sidecar Reticulum in bundle. Esegui l'upgrade a una versione più recente o segnala il problema: la creazione dal codice sorgente non è disponibile nelle installazioni in pacchetto.", "radioMqttRootDivergesWarning": "L'argomento principale di Radio MQTT è {{radioRoot}}, ma questo panel si iscrive a {{appPrefix}}. Abbina la radio per limitare il traffico dei nodi a livello nazionale: aggiorna il prefisso dell'argomento e ricollega MQTT.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface beacon TX non funziona su {{ifaces}} — il rilevamento della LAN locale potrebbe non funzionare fino a quando questo problema non viene risolto.", "tcpUnreachable": "L'hub TCP \"{{name}}\" non è raggiungibile ({{host}}:{{port}})", "tcpConnectFailed": "Connessione TCP hub \"{{name}}\" rifiutata o scaduta", - "txQueueDrops": "L'interfaccia \"{{name}}\" ha rilasciato {{count}} pacchetti in uscita (coda TX piena)", + "txQueueDrops": "L’interfaccia \"{{name}}\" ha scartato {{count}} pacchetti in uscita (coda TX piena) — spesso un hub TCP morto ancora abilitato", "linkDeliveryTimeout": "Collegamento LXMF diretto a {{hash}}… scaduto ({{count}}×)", "transportSaturated": "Trasporto RNS saturo ({{count}} LXMF path-request drop)", "transportSaturatedShareInstance": "Trasporto RNS saturo ({{count}} gocce path-request). L'istanza di condivisione è attiva: esci da altre app Reticulum (MeshChatX, Ratspeak, rnsd) e riavvia lo stack.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Hub di confine / backbone abilitati: {{hubs}}.", "announceBusPressureTipTxSaturated": "La coda TX cade su: {{names}} — l'interfaccia potrebbe essere satura.", "tooManyDefaultBackbones": "Hai {{count}} dorsali predefinite abilitate. Prendi in considerazione la possibilità di disabilitare gli extra e di tenerne circa 2 per ridondanza.", - "decommissionedHubEnabled": "L’hub fuori servizio «{{name}}» è ancora abilitato — mesh-client lo spegne a ogni avvio dello stack. Usa «Aggiungi dorsali predefinite» o la directory delle dorsali per endpoint attivi." + "decommissionedHubEnabled": "L’hub fuori servizio «{{name}}» è ancora abilitato — mesh-client lo spegne a ogni avvio dello stack. Usa «Aggiungi dorsali predefinite» o la directory delle dorsali per endpoint attivi.", + "txQueueDropsBle": "Interfaccia \"{{name}}\" drop {{count}} pacchetti in uscita (coda TX piena) — collegamento BLE RNode bloccato; preferire USB o ri-accoppiare", + "txQueueDropsBleBondStale": "Interfaccia \"{{name}}\" drop {{count}} pacchetti in uscita (coda TX piena) — legame BLE stantio; dimenticare nel sistema operativo Bluetooth e ri-accoppiare", + "txQueueDropsNeutral": "Interfaccia \"{{name}}\" ha lasciato cadere {{count}} pacchetti in uscita (coda TX piena) — controllare il collegamento di tale interfaccia" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "In attesa del PIN di abbinamento dalla radio tramite USB...", "pairingPinLabel": "PIN di accoppiamento Bluetooth", "pairingPinEnterHint": "Immettere questo codice nella finestra di dialogo Bluetooth del sistema operativo quando lo stack si connette. Non utilizzare 123456.", - "bluetoothHint": "Abilita il Bluetooth sul dispositivo, quindi utilizza Avvia accoppiamento. Il PIN di 6 cifre viene visualizzato in questo pannello tramite USB: il display della radio spesso rimane vuoto. Inserisci il PIN nella finestra di dialogo Bluetooth del tuo sistema operativo quando Reticulum si connette tramite BLE. RNode non ha un PIN predefinito (non utilizzare 123456 — ovvero Meshtastic). Sui tabelloni, tenendo premuto il pulsante per circa 7 s è anche possibile visualizzare un PIN sullo schermo. L'RNode potrebbe non essere visualizzato in Impostazioni di sistema → Bluetooth finché non viene stabilito con successo il collegamento." + "bluetoothHint": "Abilita Bluetooth sul dispositivo, poi usa Avvia accoppiamento. Il PIN a 6 cifre compare in questo pannello via USB — il display radio spesso resta vuoto. Inserisci quel PIN nel dialogo Bluetooth del SO quando Reticulum si connette via BLE. RNode non ha PIN predefinito (non usare 123456 — è Meshtastic). Su schede con display, tenere premuto ~7 s può anche mostrare un PIN. L’RNode potrebbe non comparire in Impostazioni di Sistema → Bluetooth prima di un legame riuscito. Cancella dispositivi accoppiati (ESP32) cancella la tabella dei legami radio via USB prima di riaccoppiare; devi comunque Dimenticare il dispositivo nelle impostazioni di sistema.", + "clearPairedDevices": "Cancella dispositivi accoppiati", + "clearPairedDevicesConfirmTitle": "Cancellare i legami Bluetooth RNode?", + "clearPairedDevicesConfirmMessage": "Invia CMD_BT_UNPAIR via USB e cancella gli host associati sugli RNode ESP32 (nessun effetto su alcune build nRF52). Devi comunque Dimenticare il dispositivo in Impostazioni di Sistema → Bluetooth prima di riaccoppiare.", + "clearPairedDevicesConfirm": "Cancella legami Bluetooth", + "clearPairedDevicesSuccess": "Richiesta di cancellazione della tabella dei legami Bluetooth della radio." }, "identityVault": { "title": "Caveau identità", @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "Vuoi rimuovere questa sessione di gioco dalla tua lista? Questa operazione non può essere annullata.", "claimThreefold": "Riscuoti il triplo", "claimThreefoldAria": "Richiedi il triplo sorteggio di ripetizione", - "claimFiftyMove": "Claim 50-move", + "claimFiftyMove": "Richiedi regola dei 50 mosse", "claimFiftyMoveAria": "Richiedi l'estrazione della regola delle cinquanta mosse", "delivery": { "sending": "Invio in corso…", diff --git a/src/renderer/locales/ja/translation.json b/src/renderer/locales/ja/translation.json index a64f58284..3cbc15a85 100644 --- a/src/renderer/locales/ja/translation.json +++ b/src/renderer/locales/ja/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "デフォルトのバックボーンを追加できませんでした。", "backboneEnableGuidanceLead": "最大1〜3つのバックボーンゲートウェイを有効にする", "backboneEnableGuidanceBody": "—冗長性を保ちつつ不要な帯域を増やさない最適は 2 です。グローバルハブ 1 つと地域ゲートウェイ 1 つを目安にし、I2P や Yggdrasil などの特殊インターフェースは必要なときだけ有効にしてください。ローカル接続の RNode とローカル LAN インターフェースはこのバックボーン上限に数えません。", - "defaultHubsPickerHint": "プライマリおよびグローバルに加え、自分の地域から始めてください。バックボーンゲートウェイは最大 1〜3(最適は 2)まで有効にします。ローカル RNode と LAN インターフェースは数えません。新規エントリは確認しやすいよう無効で追加されます。Sync は残っている廃止済み公式テストネットハブ(アムステルダム)も無効化します。" + "defaultHubsPickerHint": "プライマリおよびグローバルに加え、自分の地域から始めてください。バックボーンゲートウェイは最大 1〜3(最適は 2)まで有効にします。ローカル RNode と LAN インターフェースは数えません。新規エントリは確認しやすいよう無効で追加されます。Sync は残っている廃止済み公式テストネットハブ(アムステルダム)も無効化します。", + "rnodeTransportBleHint": "BLEボンドはmacOSと同期ずれすることがあります(忘れる+再ペアリング)。ラジオが近くにある場合は、USBシリアルまたはWi-Fi(tcp://)を優先してください。" }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "サイドカーの輸送アクターがくさび形になっている可能性があります。Reticulumスタックを再起動してください。", "shareInstanceHint": "共有インスタンスが有効になっています—他のReticulumアプリ( MeshChatX、Ratspeak、スタンドアロンrnsd )を完全に終了し、DMを再試行する前にこのスタックを再起動します。", "bleBondRemoved": "{{name}}: Bluetooth ボンディングが無効です (OS にはペアリング済みと表示されたままです)", - "bleBondRemovedHint": "[システム設定] → [Bluetooth] でこの RNode を忘れ、無線でペアリングを開始し ([管理者] → [Bluetooth] または 7 秒ボタンを長押し)、スタックを再起動し、新しい 6 桁の PIN を入力します。", + "bleBondRemovedHint": "まずシステム設定 → BluetoothでこのRNodeを忘れてください(macOSはPairedを自動では消しません)。可能ならUSBシリアルまたはWi‑Fi(tcp://)を優先し、BLEボンドを避けてください。次に:Admin → Bluetooth → ペアリング済みデバイスを消去(USB、ESP32、利用可能な場合)、ペアリング開始、このBLEインターフェースを削除して再追加(デバイス選択)、スタックを再起動し、新しい6桁PINを入力します。", "blePairingTimedOut": "{{name}}: Bluetooth パスキー交換がタイムアウトしました", - "blePairingTimedOutHint": "スタックを停止し、ペアリングを再度開始し ([管理] → [Bluetooth] を選択すると、パネルに PIN が表示されます。無線ディスプレイは空白のままになる場合があります。または、画面上の PIN を表示するためにボタンを約 7 秒押し続けます)、スタックを 1 回再起動し、OS ダイアログに約 60 秒以内にその PIN を入力します。 123456 (Meshtastic のデフォルト) は決して使用しないでください。" + "blePairingTimedOutHint": "スタックを停止し、ペアリングを再度開始し ([管理] → [Bluetooth] を選択すると、パネルに PIN が表示されます。無線ディスプレイは空白のままになる場合があります。または、画面上の PIN を表示するためにボタンを約 7 秒押し続けます)、スタックを 1 回再起動し、OS ダイアログに約 60 秒以内にその PIN を入力します。 123456 (Meshtastic のデフォルト) は決して使用しないでください。", + "bleBondRemovedStopStack": "スタックを停止", + "bleBondRemovedOpenAdmin": "管理者Bluetoothを開く", + "txQueueDropsHintBle": "このBLE RNodeはTXキューを排出できません(リンクがスタックまたはフラッピング)。USBシリアルまたはWi-Fi (tcp://)を優先するか、システム設定→BluetoothでForgetした後にスタックを停止して再ペアリングしてください。", + "txQueueDropsHintBleBondStale": "BLEボンドが古くなっているため、TXキューがいっぱいです。システム設定→BluetoothでこのRNodeを忘れ、管理→Bluetooth (ペアリングを解除/ペアリングを開始)を使用するか、USBに切り替えます。", + "txQueueDropsHintNeutral": "このインターフェースTXチャネルは満杯です。ドロップを停止するには、そのインターフェイスのリンクの状態を確認するか、無効にします。" }, "reticulumSidecarBundledMissing": "このインストールには、バンドルされている Reticulum サイドカーがありません。新しいリリースにアップグレードするか、問題を報告してください。パッケージ化されたインストールではソースからのビルドは利用できません。", "radioMqttRootDivergesWarning": "ラジオMQTTルートトピックは{{radioRoot}}ですが、このパネルは{{appPrefix}}に加入しています。全国のノードトラフィックを制限するために無線を一致させます—トピックプレフィックスを更新し、MQTTを再接続します。", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "オートインターフェイスビーコンTXが{{ifaces}}で失敗しています—これが修正されるまで、ローカルLAN検出が機能しない可能性があります。", "tcpUnreachable": "TCPハブ\"{{name}}\"に到達できません({{host}}: {{port}})", "tcpConnectFailed": "TCPハブ\"{{name}}\"接続が拒否されたか、タイムアウトしました", - "txQueueDrops": "インターフェース「{{name}}」が{{count}}個のアウトバウンドパケットをドロップしました( TXキューがいっぱいです)", + "txQueueDrops": "インターフェース「{{name}}」が送信パケットを{{count}}件破棄しました(TXキュー満杯)— 無効なTCPハブが有効のままであることが多いです", "linkDeliveryTimeout": "{{hash}} …への直接LXMFリンクがタイムアウトしました( {{count}} × )", "transportSaturated": "RNSトランスポート飽和( {{count}} LXMFパス要求ドロップ)", "transportSaturatedShareInstance": "RNSトランスポートが飽和しました( {{count}}パス要求ドロップ)。共有インスタンスがオンになっています—他のReticulumアプリ( MeshChatX、Ratspeak、rnsd )を終了し、スタックを再起動します。", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "境界/バックボーンハブを有効にしました: {{hubs}}。", "announceBusPressureTipTxSaturated": "TXキューは次の日付にドロップします: {{names}} —そのインターフェイスが飽和している可能性があります。", "tooManyDefaultBackbones": "{{count}}デフォルトのバックボーンが有効になっています。余分なものを無効にし、冗長性を保つために約2つを保管することを検討してください。", - "decommissionedHubEnabled": "廃止されたハブ「{{name}}」がまだ有効です — mesh-client はスタック起動のたびにオフにします。稼働中のエンドポイントには「デフォルトのバックボーンを追加」またはバックボーン一覧を使ってください。" + "decommissionedHubEnabled": "廃止されたハブ「{{name}}」がまだ有効です — mesh-client はスタック起動のたびにオフにします。稼働中のエンドポイントには「デフォルトのバックボーンを追加」またはバックボーン一覧を使ってください。", + "txQueueDropsBle": "インターフェース「{{name}}」が{{count}}送信パケットをドロップしました( TXキューがいっぱいです) — BLE RNodeリンクが固着しています。USBを使用するか、再ペアリングしてください", + "txQueueDropsBleBondStale": "インターフェース「{{name}}」が{{count}}個のアウトバウンドパケットをドロップしました( TXキューがいっぱいです) — BLEボンドが古くなっています。OS Bluetoothで忘れてから再ペアリングしてください", + "txQueueDropsNeutral": "インターフェース「{{name}}」が{{count}}送信パケットをドロップしました( TXキューがいっぱいです) —そのインターフェースのリンクを確認してください" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "USB 経由で無線からのペアリング PIN を待っています…", "pairingPinLabel": "Bluetooth ペアリング PIN", "pairingPinEnterHint": "スタックが接続するときに、OS の Bluetooth ダイアログにこのコードを入力します。 123456 は使用しないでください。", - "bluetoothHint": "デバイスで Bluetooth を有効にし、[ペアリングの開始] を使用します。 6 桁の PIN は USB 経由でこのパネルに表示されますが、無線ディスプレイは空白のままになることがよくあります。 Reticulum が BLE 経由で接続するときに、OS の Bluetooth ダイアログにその PIN を入力します。 RNode にはデフォルトの PIN がありません (123456、つまり Meshtastic を使用しないでください)。ディスプレイ ボードでは、ボタンを 7 秒間押し続けると、画面に PIN が表示されることもあります。 RNode は、結合が成功するまで [システム設定] → [Bluetooth] に表示されない場合があります。" + "bluetoothHint": "デバイスでBluetoothを有効にし、「ペアリング開始」を使います。6桁のPINはこのパネルにUSB経由で表示されます(ラジオ画面は空白のままのことが多い)。ReticulumがBLE接続するときにOSのBluetoothダイアログへそのPINを入力してください。RNodeにデフォルトPINはありません(123456は使わないでください — それはMeshtasticです)。ディスプレイ付きボードでは約7秒のボタン長押しでもPINが表示されることがあります。成功したボンドの前は、システム設定 → BluetoothにRNodeが表示されないことがあります。「ペアリング済みデバイスを消去」(ESP32)は再ペアリング前にUSB経由でラジオのボンド表を消去します。システム設定でデバイスを忘れる操作は別途必要です。", + "clearPairedDevices": "ペアリングされたデバイスをクリアする", + "clearPairedDevicesConfirmTitle": "RNode Bluetoothボンドをクリアしますか?", + "clearPairedDevicesConfirmMessage": "USB経由でCMD_BT_UNPAIRを送信し、ESP32 RNode上のボンディング済みホストを消去します(一部のnRF52ビルドでは無効)。再ペアリング前に、システム設定 → Bluetoothでデバイスを忘れ(Forget)る必要があります。", + "clearPairedDevicesConfirm": "Bluetoothボンドを消去", + "clearPairedDevicesSuccess": "ラジオのBluetoothボンド表の消去を要求しました。" }, "identityVault": { "title": "アイデンティティ保管庫", diff --git a/src/renderer/locales/ko/translation.json b/src/renderer/locales/ko/translation.json index 61acc9c0a..b00ba2a45 100644 --- a/src/renderer/locales/ko/translation.json +++ b/src/renderer/locales/ko/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "기본 백본을 추가하지 못했습니다.", "backboneEnableGuidanceLead": "최대 1 ~ 3개의 백본 게이트웨이 활성화", "backboneEnableGuidanceBody": "-2는 불필요한 대역폭 부풀림이 없는 이중화를 위한 스윗 스팟입니다. 글로벌 허브 1개와 지역 게이트웨이 1개를 목표로 하고 필요에 따라 특수 인터페이스 (예: I2P 또는 Yggdrasil) 를 엄격하게 활성화합니다. 로컬로 연결된 RNode 및 로컬 LAN 인터페이스는 이 백본 제한에 포함되지 않습니다.", - "defaultHubsPickerHint": "기본 및 글로벌 백본에 지역을 더해 시작하세요. 백본 게이트웨이는 최대 1~3개(이상적은 2개)만 사용하세요. 로컬 RNode와 LAN 인터페이스는 포함되지 않습니다. 새 항목은 검토할 수 있도록 비활성으로 추가됩니다. Sync는 남은 폐기된 공식 테스트넷 허브(암스테르담)도 비활성화합니다." + "defaultHubsPickerHint": "기본 및 글로벌 백본에 지역을 더해 시작하세요. 백본 게이트웨이는 최대 1~3개(이상적은 2개)만 사용하세요. 로컬 RNode와 LAN 인터페이스는 포함되지 않습니다. 새 항목은 검토할 수 있도록 비활성으로 추가됩니다. Sync는 남은 폐기된 공식 테스트넷 허브(암스테르담)도 비활성화합니다.", + "rnodeTransportBleHint": "BLE 본드는 macOS와 비동기화할 수 있습니다 (Forget + re-pair). 라디오가 근처에 있을 때는 USB 직렬 또는 Wi-Fi (tcp://) 를 선호합니다." }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "사이드카 트랜스포트 액터에 쐐기를 박을 수 있습니다. Reticulum 스택을 다시 시작하십시오.", "shareInstanceHint": "공유 인스턴스가 활성화되었습니다. DM을 다시 시도하기 전에 다른 Reticulum 앱 (MeshChatX, Ratspeak, 독립 실행형 rnsd) 을 완전히 종료하고 이 스택을 다시 시작하십시오.", "bleBondRemoved": "{{name}}: Bluetooth 연결이 오래되었습니다(OS에 여전히 페어링됨이 표시됨).", - "bleBondRemovedHint": "시스템 설정 → Bluetooth에서 이 RNode를 삭제하고 무선에서 페어링을 시작한 다음(관리자 → Bluetooth 또는 ~7초 버튼 누르기) 스택을 다시 시작한 다음 새 6자리 PIN을 입력합니다.", + "bleBondRemovedHint": "먼저 시스템 설정 → Bluetooth에서 이 RNode를 잊으세요(macOS는 Paired를 자동으로 지우지 않음). 가능하면 BLE 본드를 피하기 위해 USB 시리얼 또는 Wi‑Fi(tcp://)를 우선하세요. 그다음: Admin → Bluetooth → 페어링된 장치 지우기(USB, ESP32, 가능한 경우), 페어링 시작, 이 BLE 인터페이스를 제거 후 다시 추가(장치 선택), 스택을 다시 시작하고 새 6자리 PIN을 입력하세요.", "blePairingTimedOut": "{{name}}: Bluetooth 암호 키 교환 시간이 초과되었습니다.", - "blePairingTimedOutHint": "스택을 중지하고 페어링을 다시 시작하고(관리자 → Bluetooth는 패널에 PIN을 표시합니다. 라디오 디스플레이는 비어 있을 수 있습니다. 또는 화면 PIN을 보려면 ~7초 동안 버튼을 누르십시오.) 스택을 한 번 다시 시작하고 ~60초 내에 OS 대화 상자에 해당 PIN을 입력하십시오. 123456(Meshtastic 기본값)을 사용하지 마십시오." + "blePairingTimedOutHint": "스택을 중지하고 페어링을 다시 시작하고(관리자 → Bluetooth는 패널에 PIN을 표시합니다. 라디오 디스플레이는 비어 있을 수 있습니다. 또는 화면 PIN을 보려면 ~7초 동안 버튼을 누르십시오.) 스택을 한 번 다시 시작하고 ~60초 내에 OS 대화 상자에 해당 PIN을 입력하십시오. 123456(Meshtastic 기본값)을 사용하지 마십시오.", + "bleBondRemovedStopStack": "스택 중지", + "bleBondRemovedOpenAdmin": "관리자 블루투스 열기", + "txQueueDropsHintBle": "이 BLE RNode는 TX 대기열을 배출할 수 없습니다 (링크 정지 또는 플랩핑). USB 직렬 또는 Wi-Fi (tcp://) 를 선호하거나 시스템 설정 → 블루투스에서 잊어버린 후 스택을 중지하고 다시 페어링하십시오.", + "txQueueDropsHintBleBondStale": "BLE 결합이 오래되어 TX 대기열이 가득 찼습니다. 시스템 설정 → 블루투스에서 이 RNode를 지운 다음 관리자 → 블루투스를 사용하거나 (페어링 지우기/페어링 시작) USB로 전환하십시오.", + "txQueueDropsHintNeutral": "이 인터페이스 TX 채널이 가득 찼습니다. 해당 인터페이스의 링크 상태를 확인하거나 비활성화하여 드롭을 중지하십시오." }, "reticulumSidecarBundledMissing": "이 설치에는 번들로 제공되는 Reticulum 사이드카가 없습니다. 최신 릴리스로 업그레이드하거나 문제를 보고하세요. 패키지 설치에서는 소스에서 빌드할 수 없습니다.", "radioMqttRootDivergesWarning": "라디오 MQTT 루트 주제는 {{radioRoot}} 이지만 이 패널은 {{appPrefix}} 을 (를) 구독합니다. 라디오를 일치시켜 전국 노드 트래픽을 제한하십시오 — 주제 접두사를 업데이트하고 MQTT를 다시 연결하십시오.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface 비콘 TX가 {{ifaces}} 에서 실패하고 있습니다. 이 문제가 해결될 때까지 로컬 LAN 검색이 작동하지 않을 수 있습니다.", "tcpUnreachable": "TCP 허브 \"{{name}}\" 에 연결할 수 없습니다 ({{host}}: {{port}})", "tcpConnectFailed": "TCP 허브 \"{{name}}\" 연결이 거부되었거나 시간이 초과되었습니다", - "txQueueDrops": "인터페이스 \"{{name}}\" 삭제됨 {{count}} 아웃바운드 패킷 (TX 대기열 가득 참)", + "txQueueDrops": "인터페이스 \"{{name}}\"이(가) 송신 패킷 {{count}}개를 폐기했습니다(TX 큐 가득 참) — 종종 죽은 TCP 허브가 여전히 활성화됨", "linkDeliveryTimeout": "{{hash}} 에 대한 직접 LXMF 링크… 시간 초과 ({{count}} ×)", "transportSaturated": "RNS 전송 포화 ({{count}} LXMF 경로 요청 드롭)", "transportSaturatedShareInstance": "RNS 전송 포화 ({{count}} 경로 요청 드롭). 인스턴스 공유 켜짐 — 다른 Reticulum 앱 (MeshChatX, Ratspeak, rnsd) 을 종료하고 스택을 다시 시작합니다.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "활성화된 경계/백본 허브: {{hubs}}.", "announceBusPressureTipTxSaturated": "TX 대기열 드롭: {{names}} — 해당 인터페이스가 포화 상태일 수 있습니다.", "tooManyDefaultBackbones": "기본 백본이 {{count}} 개 활성화되어 있습니다. 엑스트라를 비활성화하고 이중화를 위해 약 2 개를 유지하는 것이 좋습니다.", - "decommissionedHubEnabled": "폐기된 허브 «{{name}}»가 아직 사용 중입니다 — mesh-client는 스택을 시작할 때마다 끕니다. 활성 엔드포인트는 «기본 백본 추가» 또는 백본 목록을 사용하세요." + "decommissionedHubEnabled": "폐기된 허브 «{{name}}»가 아직 사용 중입니다 — mesh-client는 스택을 시작할 때마다 끕니다. 활성 엔드포인트는 «기본 백본 추가» 또는 백본 목록을 사용하세요.", + "txQueueDropsBle": "인터페이스 \"{{name}}\" 삭제됨 {{count}} 아웃바운드 패킷 (TX 대기열 가득 참) — BLE RNode 링크가 중단됨, USB 또는 재페어링 선호", + "txQueueDropsBleBondStale": "인터페이스 \"{{name}}\" 삭제됨 {{count}} 아웃바운드 패킷 (TX 대기열 가득 참) — BLE 본드 오래됨; OS 블루투스는 잊어버리고 다시 페어링", + "txQueueDropsNeutral": "인터페이스 \"{{name}}\" 삭제됨 {{count}} 아웃바운드 패킷 (TX 대기열 가득 참) — 해당 인터페이스의 링크 확인" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "USB를 통해 라디오에서 PIN 페어링을 기다리는 중…", "pairingPinLabel": "블루투스 페어링 PIN", "pairingPinEnterHint": "스택이 연결되면 OS Bluetooth 대화 상자에 이 코드를 입력하세요. 123456을 사용하지 마십시오.", - "bluetoothHint": "장치에서 Bluetooth를 활성화한 다음 페어링 시작을 사용하십시오. 6자리 PIN은 USB를 통해 이 패널에 표시됩니다. 라디오 디스플레이는 종종 공백으로 유지됩니다. Reticulum이 BLE를 통해 연결되면 OS Bluetooth 대화 상자에 해당 PIN을 입력하세요. RNode에는 기본 PIN이 없습니다(123456을 사용하지 마십시오. 이는 Meshtastic입니다). 디스플레이 보드에서 버튼을 7초 정도 길게 누르면 화면에 PIN이 표시될 수도 있습니다. 연결이 성공적으로 완료될 때까지 RNode가 시스템 설정 → Bluetooth에 나타나지 않을 수 있습니다." + "bluetoothHint": "장치에서 Bluetooth를 켠 뒤 페어링 시작을 사용하세요. 6자리 PIN은 USB로 이 패널에 표시됩니다 — 라디오 화면은 종종 비어 있습니다. Reticulum이 BLE로 연결될 때 OS Bluetooth 대화상자에 그 PIN을 입력하세요. RNode에는 기본 PIN이 없습니다(123456 사용 금지 — Meshtastic용). 디스플레이 보드에서는 ~7초 버튼 길게 누르기로도 PIN이 표시될 수 있습니다. 성공적인 본드 전에는 시스템 설정 → Bluetooth에 RNode가 나타나지 않을 수 있습니다. 페어링된 장치 지우기(ESP32)는 재페어링 전에 USB로 라디오 본드 테이블을 지웁니다. 시스템 설정에서 장치를 잊는 작업은 별도로 필요합니다.", + "clearPairedDevices": "페어링된 장치 지우기", + "clearPairedDevicesConfirmTitle": "RNode 블루투스 본드를 지우시겠습니까?", + "clearPairedDevicesConfirmMessage": "USB로 CMD_BT_UNPAIR를 보내고 ESP32 RNode의 본딩된 호스트를 지웁니다(일부 nRF52 빌드에서는 무효). 다시 페어링하기 전에 시스템 설정 → Bluetooth에서 장치를 여전히 잊어야 합니다.", + "clearPairedDevicesConfirm": "블루투스 본드 지우기", + "clearPairedDevicesSuccess": "라디오 블루투스 본드 테이블 지우기가 요청되었습니다." }, "identityVault": { "title": "신원 정보 보관소", @@ -5281,8 +5295,8 @@ "promotionChooserAria": "프로모션 조각 선택", "promoteTo": { "q": "여왕으로 승급하기", - "r": "ROOK로 승격", - "b": "감독으로 승진", + "r": "룩으로 승급", + "b": "비숍으로 승급", "n": "기사로 승급하기" } }, @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "목록에서 이 게임 세션을 제거하시겠습니까? 이 작업은 취소할 수 없습니다.", "claimThreefold": "3배 받기", "claimThreefoldAria": "3배 반복 추첨 요청", - "claimFiftyMove": "청구항 50-이동", + "claimFiftyMove": "50수 규칙 주장", "claimFiftyMoveAria": "오십이동 규칙 추첨 요청", "delivery": { "sending": "전송 중…", diff --git a/src/renderer/locales/nl/translation.json b/src/renderer/locales/nl/translation.json index 5129b3d12..3cea8f5ef 100644 --- a/src/renderer/locales/nl/translation.json +++ b/src/renderer/locales/nl/translation.json @@ -1187,7 +1187,8 @@ "addDefaultHubsFailed": "Kan standaard backbones niet toevoegen.", "backboneEnableGuidanceLead": "Maximaal 1 tot 3 backbone-gateways inschakelen", "backboneEnableGuidanceBody": "—2 is ideaal voor redundantie zonder onnodige bandbreedte. Streef naar 1 globale hub en 1 regionale gateway, en schakel speciale interfaces (zoals I2P of Yggdrasil) alleen in als nodig. Lokaal verbonden RNodes en lokale LAN-interfaces tellen niet mee voor deze backbone-limiet.", - "defaultHubsPickerHint": "Begin met Primair & globaal plus uw regio. Schakel hoogstens 1 tot 3 backbone-gateways in (2 is ideaal). Lokale RNodes en LAN-interfaces tellen niet mee. Nieuwe items worden uitgeschakeld toegevoegd zodat u ze kunt beoordelen. Sync schakelt ook resterende buiten gebruik gestelde officiële testnet-hubs uit (Amsterdam)." + "defaultHubsPickerHint": "Begin met Primair & globaal plus uw regio. Schakel hoogstens 1 tot 3 backbone-gateways in (2 is ideaal). Lokale RNodes en LAN-interfaces tellen niet mee. Nieuwe items worden uitgeschakeld toegevoegd zodat u ze kunt beoordelen. Sync schakelt ook resterende buiten gebruik gestelde officiële testnet-hubs uit (Amsterdam).", + "rnodeTransportBleHint": "BLE-bindingen kunnen desynchroniseren met macOS (vergeten + opnieuw koppelen). Geef de voorkeur aan USB-serieel of Wi-Fi (tcp://) wanneer de radio in de buurt is." }, "reticulumPeers": { "hops": "Hops", @@ -1288,9 +1289,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Deel instantie is ingeschakeld — sluit andere Reticulum-apps (MeshChatX, Ratspeak, standalone rnsd) volledig af en start deze stapel opnieuw voordat u DM's opnieuw probeert.", "bleBondRemoved": "{{name}}: Bluetooth-verbinding is verouderd (OS toont nog steeds Gekoppeld)", - "bleBondRemovedHint": "Vergeet deze RNode in Systeeminstellingen → Bluetooth, begin met koppelen op de radio (Admin → Bluetooth of ~7 s knop ingedrukt houden), start de stapel opnieuw op en voer de nieuwe 6-cijferige pincode in.", + "bleBondRemovedHint": "Vergeet deze RNode eerst in Systeeminstellingen → Bluetooth (macOS wist Paired niet automatisch). Geef waar mogelijk de voorkeur aan USB-serie of Wi‑Fi (tcp://) om BLE-koppelingen te vermijden. Daarna: Admin → Bluetooth → Gekoppelde apparaten wissen (USB, ESP32) indien beschikbaar, Koppeling starten, verwijder en voeg deze BLE-interface opnieuw toe (Apparaat kiezen), herstart de stack en voer de nieuwe 6-cijferige pincode in.", "blePairingTimedOut": "{{name}}: Time-out voor uitwisseling van Bluetooth-wachtwoord", - "blePairingTimedOutHint": "Stop de stapel, begin opnieuw met koppelen (Beheer → Bluetooth toont de pincode in het paneel - het radiodisplay kan leeg blijven - of houd de knop ~7 seconden ingedrukt voor een pincode op het scherm), start de stapel opnieuw op en voer die pincode binnen ~60 seconden in het dialoogvenster van het besturingssysteem in. Gebruik nooit 123456 (standaard Meshtastic)." + "blePairingTimedOutHint": "Stop de stapel, begin opnieuw met koppelen (Beheer → Bluetooth toont de pincode in het paneel - het radiodisplay kan leeg blijven - of houd de knop ~7 seconden ingedrukt voor een pincode op het scherm), start de stapel opnieuw op en voer die pincode binnen ~60 seconden in het dialoogvenster van het besturingssysteem in. Gebruik nooit 123456 (standaard Meshtastic).", + "bleBondRemovedStopStack": "Stop stack", + "bleBondRemovedOpenAdmin": "Open Beheerder Bluetooth", + "txQueueDropsHintBle": "Deze BLE RNode kan zijn TX-wachtrij niet legen (link vast of flapperend). Geef de voorkeur aan USB-serie of Wi‑Fi (tcp://), of stop de stack en koppel opnieuw na Vergeten in Systeeminstellingen → Bluetooth.", + "txQueueDropsHintBleBondStale": "TX-wachtrij is vol omdat de BLE-koppeling verouderd is. Vergeet deze RNode in Systeeminstellingen → Bluetooth en gebruik Admin → Bluetooth (Gekoppelde wissen / Koppeling starten) of schakel over naar USB.", + "txQueueDropsHintNeutral": "Dit interface TX-kanaal is vol. Controleer de linkstatus van die interface (of schakel deze uit) om vallen te stoppen." }, "reticulumSidecarBundledMissing": "Bij deze installatie ontbreekt het gebundelde Reticulum sidecar. Upgrade naar een nieuwere release of rapporteer het probleem: bouwen vanaf de broncode is niet beschikbaar in pakketinstallaties.", "radioMqttRootDivergesWarning": "Radio MQTT-hoofdonderwerp is {{radioRoot}}, maar dit paneel is geabonneerd op {{appPrefix}}. Koppel de radio om landelijk knooppuntverkeer te beperken — werk het onderwerpvoorvoegsel bij en sluit MQTT opnieuw aan.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface-baken TX faalt op {{ifaces}} — lokale LAN-detectie werkt mogelijk niet totdat dit is opgelost.", "tcpUnreachable": "TCP-hub \"{{name}}\" is onbereikbaar ({{host}}:{{port}})", "tcpConnectFailed": "TCP-hub \"{{name}}\" -verbinding geweigerd of er is een time-out opgetreden", - "txQueueDrops": "Interface \"{{name}}\" liet {{count}} uitgaande pakketten vallen (TX-wachtrij vol)", + "txQueueDrops": "Interface \"{{name}}\" liet {{count}} uitgaande pakketten vallen (TX-wachtrij vol) — vaak een dode TCP-hub die nog aan staat", "linkDeliveryTimeout": "Directe LXMF-link naar {{hash}}… time-out ({{count}}×)", "transportSaturated": "RNS-transport verzadigd ({{count}} LXMF-pad-aanvraagdruppels)", "transportSaturatedShareInstance": "RNS-transport verzadigd ({{count}} pad-aanvraagdruppels). Deel instantie is ingeschakeld — sluit andere Reticulum-apps (MeshChatX, Ratspeak, rnsd) en start de stapel opnieuw.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Ingeschakelde grens- / ruggengraathubs: {{hubs}}.", "announceBusPressureTipTxSaturated": "TX-wachtrij valt op: {{names}} — die interface kan verzadigd zijn.", "tooManyDefaultBackbones": "Je hebt {{count}} standaard backbones ingeschakeld. Overweeg om extra's uit te schakelen en er ongeveer 2 te houden voor redundantie.", - "decommissionedHubEnabled": "Buiten gebruik gestelde hub „{{name}}” staat nog aan — mesh-client zet hem bij elke stackstart uit. Gebruik «Standaard backbones toevoegen» of de backbone-directory voor actieve eindpunten." + "decommissionedHubEnabled": "Buiten gebruik gestelde hub „{{name}}” staat nog aan — mesh-client zet hem bij elke stackstart uit. Gebruik «Standaard backbones toevoegen» of de backbone-directory voor actieve eindpunten.", + "txQueueDropsBle": "Interface \"{{name}}\" liet {{count}} uitgaande pakketten vallen (TX-wachtrij vol) — BLE RNode-link vastgelopen; geef de voorkeur aan USB of opnieuw koppelen", + "txQueueDropsBleBondStale": "Interface \"{{name}}\" liet {{count}} uitgaande pakketten vallen (TX-wachtrij vol) — BLE-binding verouderd; Vergeet in OS Bluetooth en koppel opnieuw", + "txQueueDropsNeutral": "Interface \"{{name}}\" liet {{count}} uitgaande pakketten vallen (TX-wachtrij vol) — controleer de link van die interface" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Wachten op koppelingspincode van de radio via USB...", "pairingPinLabel": "Bluetooth-koppelingspincode", "pairingPinEnterHint": "Voer deze code in het OS Bluetooth-dialoogvenster in wanneer de stapel verbinding maakt. Gebruik 123456 niet.", - "bluetoothHint": "Schakel Bluetooth in op het apparaat en gebruik vervolgens Koppeling starten. De 6-cijferige pincode verschijnt in dit paneel via USB - het radiodisplay blijft vaak leeg. Voer die pincode in het Bluetooth-dialoogvenster van uw besturingssysteem in wanneer Reticulum verbinding maakt via BLE. RNode heeft geen standaardpincode (gebruik niet 123456 – dat is Meshtastic). Op displayborden kan het ingedrukt houden van de knop ~7 seconden ook een pincode op het scherm weergeven. De RNode verschijnt mogelijk pas in Systeeminstellingen → Bluetooth na een succesvolle verbinding." + "bluetoothHint": "Schakel Bluetooth in op het apparaat en gebruik Koppeling starten. De 6-cijferige pincode verschijnt in dit paneel via USB — het radiodisplay blijft vaak leeg. Voer die pincode in het Bluetooth-dialoogvenster van uw OS in wanneer Reticulum via BLE verbindt. RNode heeft geen standaardpincode (gebruik niet 123456 — dat is Meshtastic). Op displayborden kan ~7 s knop vasthouden ook een pincode tonen. De RNode verschijnt mogelijk pas in Systeeminstellingen → Bluetooth na een geslaagde koppeling. Gekoppelde apparaten wissen (ESP32) wist de radiokoppelingstabel via USB vóór herkoppelen; u moet het apparaat nog steeds Vergeten in Systeeminstellingen.", + "clearPairedDevices": "Gekoppelde apparaten wissen", + "clearPairedDevicesConfirmTitle": "RNode Bluetooth-bindingen wissen?", + "clearPairedDevicesConfirmMessage": "Dit stuurt CMD_BT_UNPAIR via USB en wist gekoppelde hosts op ESP32 RNodes (geen effect op sommige nRF52-builds). U moet het apparaat nog steeds Vergeten in Systeeminstellingen → Bluetooth voordat u opnieuw koppelt.", + "clearPairedDevicesConfirm": "Bluetooth-koppelingen wissen", + "clearPairedDevicesSuccess": "Wissen van radiokoppelingstabel aangevraagd." }, "identityVault": { "title": "Identiteitskluis", @@ -5281,7 +5295,7 @@ "promotionChooserAria": "Promotiestuk kiezen", "promoteTo": { "q": "Promoveer tot koningin", - "r": "Promoot tot rook", + "r": "Promoveer tot toren", "b": "Promoveer tot bisschop", "n": "Promoveer tot ridder" } @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "Deze spelsessie uit je lijst verwijderen? Dit kan niet ongedaan worden gemaakt.", "claimThreefold": "Claim drievoudig", "claimThreefoldAria": "Claim drievoudige herhalingstekening", - "claimFiftyMove": "Claim 50-move", + "claimFiftyMove": "Claim 50-zettenregel", "claimFiftyMoveAria": "Regeltrekking voor vijftig zetten claimen", "delivery": { "sending": "Versturen...", diff --git a/src/renderer/locales/pl/translation.json b/src/renderer/locales/pl/translation.json index 5ab026c8e..22f363f28 100644 --- a/src/renderer/locales/pl/translation.json +++ b/src/renderer/locales/pl/translation.json @@ -1190,7 +1190,8 @@ "addDefaultHubsFailed": "Nie udało się dodać domyślnych kości kręgosłupa.", "backboneEnableGuidanceLead": "Włącz maksymalnie 1 do 3 bramek szkieletowych", "backboneEnableGuidanceBody": "—2 to optimum dla nadmiarowości bez zbędnego zużycia pasma. Celuj w 1 globalny hub i 1 regionalną bramę, a specjalne interfejsy (jak I2P lub Yggdrasil) włączaj tylko gdy trzeba. Lokalnie podłączone RNode i lokalne interfejsy LAN nie wliczają się do tego limitu magistrali.", - "defaultHubsPickerHint": "Zacznij od Podstawowej i globalnej magistrali oraz swojego regionu. Włącz co najwyżej 1–3 bramy magistrali (2 to optimum). Lokalne RNode i interfejsy LAN się nie liczą. Nowe wpisy są dodawane wyłączone, by je przejrzeć. Sync wyłącza też pozostałe wycofane oficjalne huby testnetu (Amsterdam)." + "defaultHubsPickerHint": "Zacznij od Podstawowej i globalnej magistrali oraz swojego regionu. Włącz co najwyżej 1–3 bramy magistrali (2 to optimum). Lokalne RNode i interfejsy LAN się nie liczą. Nowe wpisy są dodawane wyłączone, by je przejrzeć. Sync wyłącza też pozostałe wycofane oficjalne huby testnetu (Amsterdam).", + "rnodeTransportBleHint": "Wiązania BLE mogą być desynchronizowane z macOS (Forget + repair). Preferuj port szeregowy USB lub Wi-Fi (tcp://), gdy radio znajduje się w pobliżu." }, "reticulumPeers": { "hops": "Hops", @@ -1291,9 +1292,14 @@ "slowTransportQueryHint": "Aktor transportowy wózka bocznego może być zaklinowany — uruchom ponownie stos Reticulum.", "shareInstanceHint": "Instancja udostępniania jest włączona — całkowicie zamknij inne aplikacje Reticulum (MeshChatX, Ratspeak, standalone rnsd) i uruchom ponownie ten stos przed ponowną próbą wysłania wiadomości SMS.", "bleBondRemoved": "{{name}}: Połączenie Bluetooth jest nieaktualne (system operacyjny nadal wyświetla komunikat Sparowano)", - "bleBondRemovedHint": "Zapomnij o tym RNode w Ustawieniach systemowych → Bluetooth, rozpocznij parowanie w radiu (Admin → Bluetooth lub przytrzymanie przycisku ~7 s), zrestartuj stos i wprowadź nowy 6-cyfrowy PIN.", + "bleBondRemovedHint": "Najpierw zapomnij ten RNode w Ustawieniach systemowych → Bluetooth (macOS nie usuwa Paired automatycznie). Jeśli możesz, preferuj USB serial lub Wi‑Fi (tcp://), by uniknąć powiązań BLE. Potem: Admin → Bluetooth → Wyczyść sparowane urządzenia (USB, ESP32) jeśli dostępne, Rozpocznij parowanie, usuń i dodaj ponownie ten interfejs BLE (Wybierz urządzenie), zrestartuj stos i wpisz nowy 6-cyfrowy PIN.", "blePairingTimedOut": "{{name}}: Upłynął limit czasu wymiany hasła Bluetooth", - "blePairingTimedOutHint": "Zatrzymaj stos, rozpocznij parowanie ponownie (Administrator → Bluetooth wyświetli PIN na panelu — wyświetlacz radia może pozostać pusty — lub przytrzymanie przycisku przez ~7 s, aby wprowadzić PIN na ekranie), ponownie uruchom stos i wprowadź ten kod PIN w oknie dialogowym systemu operacyjnego w ciągu ~60 sekund. Nigdy nie używaj numeru 123456 (domyślny Meshtastic)." + "blePairingTimedOutHint": "Zatrzymaj stos, rozpocznij parowanie ponownie (Administrator → Bluetooth wyświetli PIN na panelu — wyświetlacz radia może pozostać pusty — lub przytrzymanie przycisku przez ~7 s, aby wprowadzić PIN na ekranie), ponownie uruchom stos i wprowadź ten kod PIN w oknie dialogowym systemu operacyjnego w ciągu ~60 sekund. Nigdy nie używaj numeru 123456 (domyślny Meshtastic).", + "bleBondRemovedStopStack": "Stos ograniczników", + "bleBondRemovedOpenAdmin": "Otwórz Admin Bluetooth", + "txQueueDropsHintBle": "Ten ble RNode nie może opróżnić swojej kolejki TX (link zablokowany lub trzepoczący). Preferuj port szeregowy USB lub Wi-Fi (tcp://) lub zatrzymaj stos i ponownie sparuj po zapomnieniu w ustawieniach systemu → Bluetooth.", + "txQueueDropsHintBleBondStale": "Kolejka TX jest pełna, bo powiązanie BLE jest nieaktualne. Zapomnij ten RNode w Ustawieniach systemowych → Bluetooth, potem użyj Admin → Bluetooth (Wyczyść sparowane / Rozpocznij parowanie) lub przełącz na USB.", + "txQueueDropsHintNeutral": "Ten kanał interfejsu TX jest pełny. Sprawdź stan łącza w tym interfejsie (lub wyłącz je), aby zatrzymać spadki." }, "reticulumSidecarBundledMissing": "W tej instalacji brakuje dołączonego sidecar Reticulum. Uaktualnij do nowszej wersji lub zgłoś problem — budowanie ze źródła nie jest możliwe w przypadku instalacji pakietowych.", "radioMqttRootDivergesWarning": "Główny temat radia MQTT to {{radioRoot}}, ale ten panel subskrybuje {{appPrefix}}. Dopasuj radio, aby ograniczyć ogólnokrajowy ruch w węzłach — zaktualizuj prefiks tematu i ponownie połącz MQTT.", @@ -1660,7 +1666,7 @@ "autoBeaconPhysicalFailures": "Lampa sygnalizacyjna AutoInterface TX nie działa w {{ifaces}} — lokalne wykrywanie sieci LAN może nie działać, dopóki nie zostanie to naprawione.", "tcpUnreachable": "Koncentrator TCP „{{name}}” jest nieosiągalny ({{host}}:{{port}})", "tcpConnectFailed": "Odmowa lub przekroczenie limitu czasu połączenia koncentratora TCP „{{name}}”", - "txQueueDrops": "Interfejs \"{{name}}\" upuścił pakiety wychodzące {{count}} (kolejka TX pełna)", + "txQueueDrops": "Interfejs „{{name}}” odrzucił {{count}} pakietów wychodzących (kolejka TX pełna) — często nadal włączony martwy hub TCP", "linkDeliveryTimeout": "Upłynął limit czasu bezpośredniego łącza LXMF do {{hash}}… ({{count}}×)", "transportSaturated": "Transport RNS nasycony (spadki {{count}} LXMF)", "transportSaturatedShareInstance": "Transport RNS nasycony (krople z żądaniem ścieżki {{count}}). Udostępnij instancję jest włączona — zamknij inne aplikacje Reticulum (MeshChatX, Ratspeak, rnsd) i uruchom ponownie stos.", @@ -1680,7 +1686,10 @@ "announceBusPressureTipBoundaryHubs": "Włączone koncentratory brzegowe / szkieletowe: {{hubs}}.", "announceBusPressureTipTxSaturated": "Kolejka TX spada w dniu: {{names}} — ten interfejs może być nasycony.", "tooManyDefaultBackbones": "Masz włączone {{count}} domyślne kręgosłupy. Rozważ wyłączenie dodatków i zachowanie około 2 na potrzeby redundancji.", - "decommissionedHubEnabled": "Wycofany hub „{{name}}” jest nadal włączony — mesh-client wyłącza go przy każdym starcie stosu. Użyj «Dodaj domyślne magistrale» lub katalogu magistrali dla aktywnych punktów końcowych." + "decommissionedHubEnabled": "Wycofany hub „{{name}}” jest nadal włączony — mesh-client wyłącza go przy każdym starcie stosu. Użyj «Dodaj domyślne magistrale» lub katalogu magistrali dla aktywnych punktów końcowych.", + "txQueueDropsBle": "Interfejs \"{{name}}\" upuścił pakiety wychodzące {{count}} (kolejka TX pełna) — BLE RNode link zablokowany; wolisz USB lub ponownie sparować", + "txQueueDropsBleBondStale": "Interfejs \"{{name}}\" odrzucił pakiety wychodzące {{count}} (kolejka TX pełna) — Nieaktualne połączenie BLE; Zapomnij w systemie operacyjnym Bluetooth i sparuj ponownie", + "txQueueDropsNeutral": "Interfejs \"{{name}}\" upuścił pakiety wychodzące {{count}} (kolejka TX pełna) — sprawdź link do tego interfejsu" } }, "routingPort": { @@ -1853,7 +1862,12 @@ "pairingWaiting": "Oczekiwanie na PIN parowania z radia przez USB…", "pairingPinLabel": "Kod PIN parowania Bluetooth", "pairingPinEnterHint": "Wprowadź ten kod w oknie dialogowym Bluetooth systemu operacyjnego, gdy stos się łączy. Nie używaj 123456.", - "bluetoothHint": "Włącz Bluetooth na urządzeniu, a następnie użyj opcji Rozpocznij parowanie. 6-cyfrowy PIN pojawia się w tym panelu przez USB – wyświetlacz radia często pozostaje pusty. Wprowadź ten kod PIN w oknie dialogowym Bluetooth systemu operacyjnego, gdy Reticulum połączy się przez BLE. RNode nie ma domyślnego PIN-u (nie używaj 123456 — czyli Meshtastic). Na wyświetlaczach przytrzymanie przycisku przez ~7 s może również spowodować wyświetlenie kodu PIN na ekranie. RNode może nie pojawić się w Ustawieniach systemu → Bluetooth do czasu pomyślnego nawiązania połączenia." + "bluetoothHint": "Włącz Bluetooth na urządzeniu, potem użyj Rozpocznij parowanie. 6-cyfrowy PIN pojawia się w tym panelu przez USB — wyświetlacz radia często pozostaje pusty. Wpisz ten PIN w oknie Bluetooth systemu, gdy Reticulum łączy się przez BLE. RNode nie ma domyślnego PIN-u (nie używaj 123456 — to Meshtastic). Na płytach z wyświetlaczem przytrzymanie przycisku ~7 s też może pokazać PIN. RNode może nie pojawić się w Ustawieniach systemowych → Bluetooth przed udanym powiązaniem. Wyczyść sparowane urządzenia (ESP32) czyści tabelę powiązań radia przez USB przed ponownym parowaniem; nadal musisz Zapomnieć urządzenie w ustawieniach systemu.", + "clearPairedDevices": "Wyczyść sparowane urządzenia", + "clearPairedDevicesConfirmTitle": "Wyczyścić powiązania Bluetooth RNode?", + "clearPairedDevicesConfirmMessage": "Wysyła CMD_BT_UNPAIR przez USB i czyści powiązane hosty na RNode ESP32 (bez efektu na niektórych buildach nRF52). Przed ponownym parowaniem nadal musisz Zapomnieć urządzenie w Ustawieniach systemowych → Bluetooth.", + "clearPairedDevicesConfirm": "Wyczyść powiązania Bluetooth", + "clearPairedDevicesSuccess": "Zażądano wyczyszczenia tabeli powiązań Bluetooth radia." }, "identityVault": { "title": "Skarbiec tożsamości", diff --git a/src/renderer/locales/pt-BR/translation.json b/src/renderer/locales/pt-BR/translation.json index 60370c52b..900db034b 100644 --- a/src/renderer/locales/pt-BR/translation.json +++ b/src/renderer/locales/pt-BR/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "Falha ao adicionar backbones padrão.", "backboneEnableGuidanceLead": "Habilite de 1 a 3 gateways de backbone no máximo", "backboneEnableGuidanceBody": "—2 é o ponto ideal para redundância sem inchaço desnecessário da largura de banda. Aponte para 1 hub global e 1 gateway regional e habilite interfaces especializadas (como I2P ou Yggdrasil) estritamente conforme necessário. Os RNodes conectados localmente e as interfaces de LAN locais não contam para esse limite de backbone.", - "defaultHubsPickerHint": "Comece com Primário e global mais sua região. Ative no máximo 1 a 3 gateways de backbone (2 é o ponto ideal). RNodes locais e interfaces LAN não contam. Novas entradas são adicionadas desativadas para revisão. Sync também desativa hubs de testnet oficiais desativados restantes (Amsterdã)." + "defaultHubsPickerHint": "Comece com Primário e global mais sua região. Ative no máximo 1 a 3 gateways de backbone (2 é o ponto ideal). RNodes locais e interfaces LAN não contam. Novas entradas são adicionadas desativadas para revisão. Sync também desativa hubs de testnet oficiais desativados restantes (Amsterdã).", + "rnodeTransportBleHint": "As ligações BLE podem dessincronizar com o macOS (Forget + re-pair). Prefira USB serial ou Wi-Fi (tcp://) quando o rádio estiver próximo." }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Share instance is enabled — fully quit other Reticulum apps (MeshChatX, Ratspeak, standalone rnsd) and restart this stack before retrying DMs.", "bleBondRemoved": "{{name}}: A ligação Bluetooth está obsoleta (o sistema operacional ainda mostra Emparelhado)", - "bleBondRemovedHint": "Esqueça este RNode em Configurações do sistema → Bluetooth, inicie o emparelhamento no rádio (Admin → Bluetooth ou mantenha o botão pressionado por ~ 7 s), reinicie a pilha e insira o novo PIN de 6 dígitos.", + "bleBondRemovedHint": "Esqueça primeiro este RNode em Ajustes do Sistema → Bluetooth (macOS não remove Paired automaticamente). Prefira USB serial ou Wi‑Fi (tcp://) quando possível para evitar vínculos BLE. Depois: Admin → Bluetooth → Limpar dispositivos emparelhados (USB, ESP32) se disponível, Iniciar emparelhamento, remova e adicione de novo esta interface BLE (Escolher dispositivo), reinicie a pilha e digite o novo PIN de 6 dígitos.", "blePairingTimedOut": "{{name}}: A troca de senha do Bluetooth expirou", - "blePairingTimedOutHint": "Pare a pilha, comece o emparelhamento novamente (Admin → Bluetooth mostra o PIN no painel - o display do rádio pode ficar em branco - ou pressione o botão por aproximadamente 7 s para obter um PIN na tela), reinicie a pilha uma vez e insira esse PIN na caixa de diálogo do sistema operacional dentro de aproximadamente 60 segundos. Nunca use 123456 (padrão Meshtastic)." + "blePairingTimedOutHint": "Pare a pilha, comece o emparelhamento novamente (Admin → Bluetooth mostra o PIN no painel - o display do rádio pode ficar em branco - ou pressione o botão por aproximadamente 7 s para obter um PIN na tela), reinicie a pilha uma vez e insira esse PIN na caixa de diálogo do sistema operacional dentro de aproximadamente 60 segundos. Nunca use 123456 (padrão Meshtastic).", + "bleBondRemovedStopStack": "Parar pilha", + "bleBondRemovedOpenAdmin": "Abrir Admin Bluetooth", + "txQueueDropsHintBle": "Este BLE RNode não pode drenar sua fila TX (link preso ou flapping). Prefira USB serial ou Wi-Fi (tcp://), ou Pare de empilhar e emparelhe novamente depois de Esquecer nas Configurações do Sistema → Bluetooth.", + "txQueueDropsHintBleBondStale": "A fila TX está cheia porque a ligação BLE está obsoleta. Esqueça este RNode nas Configurações do Sistema → Bluetooth e use Admin → Bluetooth (Limpar emparelhado / Iniciar emparelhamento) ou mude para USB.", + "txQueueDropsHintNeutral": "Este canal TX de interface está cheio. Verifique a integridade do link dessa interface (ou desative-a) para interromper as quedas." }, "reticulumSidecarBundledMissing": "Esta instalação não contém o sidecar Reticulum incluído. Atualize para uma versão mais recente ou relate o problema – a compilação a partir do código-fonte não está disponível em instalações empacotadas.", "radioMqttRootDivergesWarning": "O tópico raiz do MQTT de rádio é {{radioRoot}}, mas este painel assina {{appPrefix}}. Combine o rádio para limitar o tráfego de nós em todo o país — atualize o Prefixo do Tópico e reconecte o MQTT.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface beacon TX está falhando em {{ifaces}} — a descoberta de LAN local pode não funcionar até que isso seja corrigido.", "tcpUnreachable": "O hub TCP \"{{name}}\" está inacessível ({{host}}:{{port}})", "tcpConnectFailed": "Ligação TCP Hub \"{{name}}\" recusada ou tempo esgotado", - "txQueueDrops": "Interface \"{{name}}\" descartou {{count}} pacotes de saída (fila TX cheia)", + "txQueueDrops": "A interface \"{{name}}\" descartou {{count}} pacotes de saída (fila TX cheia) — muitas vezes um hub TCP morto ainda habilitado", "linkDeliveryTimeout": "O link LXMF direto para {{hash}}… expirou ({{count}}×)", "transportSaturated": "RNS transport saturated ({{count}} LXMF path-request drops)", "transportSaturatedShareInstance": "RNS transport saturated ({{count}} path-request drops). Share instance is on — quit other Reticulum apps (MeshChatX, Ratspeak, rnsd) and restart the stack.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Hubs de limite / backbone ativados: {{hubs}}.", "announceBusPressureTipTxSaturated": "A fila TX cai em: {{names}} — essa interface pode estar saturada.", "tooManyDefaultBackbones": "Você tem {{count}} backbones padrão ativados. Considere desativar extras e manter cerca de 2 para redundância.", - "decommissionedHubEnabled": "O hub desativado «{{name}}» ainda está habilitado — o mesh-client o desliga a cada início do stack. Use «Adicionar backbones padrão» ou o diretório de backbones para endpoints ativos." + "decommissionedHubEnabled": "O hub desativado «{{name}}» ainda está habilitado — o mesh-client o desliga a cada início do stack. Use «Adicionar backbones padrão» ou o diretório de backbones para endpoints ativos.", + "txQueueDropsBle": "Interface \"{{name}}\" descartou {{count}} pacotes de saída (fila TX cheia) — link BLE RNode travado; prefira USB ou emparelhe novamente", + "txQueueDropsBleBondStale": "Interface \"{{name}}\" descartou {{count}} pacotes de saída (fila TX cheia) — BLE Bond obsoleto; Esqueça no OS Bluetooth e emparelhe novamente", + "txQueueDropsNeutral": "Interface \"{{name}}\" descartou {{count}} pacotes de saída (fila TX cheia) — verifique o link dessa interface" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Aguardando o PIN de emparelhamento do rádio via USB…", "pairingPinLabel": "PIN de emparelhamento Bluetooth", "pairingPinEnterHint": "Insira este código na caixa de diálogo Bluetooth do sistema operacional quando a pilha se conectar. Não use 123456.", - "bluetoothHint": "Ative o Bluetooth no dispositivo e use Iniciar emparelhamento. O PIN de 6 dígitos aparece neste painel via USB – o display do rádio geralmente fica em branco. Insira esse PIN na caixa de diálogo Bluetooth do seu sistema operacional quando o Reticulum se conectar via BLE. RNode não tem PIN padrão (não use 123456 – isso é Meshtastic). Em painéis de exibição, pressionar o botão por aproximadamente 7 segundos também pode mostrar um PIN na tela. O RNode pode não aparecer em Configurações do sistema → Bluetooth até após uma ligação bem-sucedida." + "bluetoothHint": "Ative o Bluetooth no dispositivo e use Iniciar emparelhamento. O PIN de 6 dígitos aparece neste painel via USB — a tela do rádio costuma ficar em branco. Digite esse PIN no diálogo Bluetooth do SO quando o Reticulum conectar por BLE. RNode não tem PIN padrão (não use 123456 — isso é Meshtastic). Em placas com tela, segurar o botão ~7 s também pode mostrar um PIN. O RNode pode não aparecer em Ajustes do Sistema → Bluetooth antes de um vínculo bem-sucedido. Limpar dispositivos emparelhados (ESP32) limpa a tabela de vínculos do rádio via USB antes de reemparelhar; você ainda deve Esquecer o dispositivo nos ajustes do sistema.", + "clearPairedDevices": "Limpar dispositivos emparelhados", + "clearPairedDevicesConfirmTitle": "Limpar ligações Bluetooth RNode?", + "clearPairedDevicesConfirmMessage": "Isso envia CMD_BT_UNPAIR via USB e limpa hosts vinculados em RNodes ESP32 (sem efeito em alguns builds nRF52). Você ainda deve Esquecer o dispositivo em Ajustes do Sistema → Bluetooth antes de emparelhar de novo.", + "clearPairedDevicesConfirm": "Limpar vínculos Bluetooth", + "clearPairedDevicesSuccess": "Limpeza da tabela de vínculos Bluetooth do rádio solicitada." }, "identityVault": { "title": "Identity Vault", @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "Remover esta sessão de jogo da sua lista? Isso não pode ser desfeito.", "claimThreefold": "Reivindicação tripla", "claimThreefoldAria": "Reivindicar triplo sorteio de repetição", - "claimFiftyMove": "Reivindicação 50-mover", + "claimFiftyMove": "Reivindicar regra dos 50 movimentos", "claimFiftyMoveAria": "Reivindicar sorteio de regra de cinquenta movimentos", "delivery": { "sending": "Enviando…", diff --git a/src/renderer/locales/ru/translation.json b/src/renderer/locales/ru/translation.json index 05331cd95..59a0ee870 100644 --- a/src/renderer/locales/ru/translation.json +++ b/src/renderer/locales/ru/translation.json @@ -1190,7 +1190,8 @@ "addDefaultHubsFailed": "Не удалось добавить магистрали по умолчанию.", "backboneEnableGuidanceLead": "Включить не более 1-3 магистральных шлюзов", "backboneEnableGuidanceBody": "—2 — оптимум для избыточности без лишней нагрузки на канал. Цель: 1 глобальный хаб и 1 региональный шлюз; специальные интерфейсы (I2P или Yggdrasil) включайте только по необходимости. Локально подключённые RNode и локальные LAN-интерфейсы не входят в этот лимит магистрали.", - "defaultHubsPickerHint": "Начните с первичной и глобальной магистрали плюс ваш регион. Включайте не более 1–3 магистральных шлюзов (2 — оптимум). Локальные RNode и LAN-интерфейсы не учитываются. Новые записи добавляются выключенными для проверки. Sync также отключает оставшиеся выведенные из эксплуатации официальные тестовые хабы (Амстердам)." + "defaultHubsPickerHint": "Начните с первичной и глобальной магистрали плюс ваш регион. Включайте не более 1–3 магистральных шлюзов (2 — оптимум). Локальные RNode и LAN-интерфейсы не учитываются. Новые записи добавляются выключенными для проверки. Sync также отключает оставшиеся выведенные из эксплуатации официальные тестовые хабы (Амстердам).", + "rnodeTransportBleHint": "Связи BLE могут рассинхронизироваться с macOS (Forget + re-pair). Предпочитайте последовательный USB или Wi-Fi (tcp://), когда радиостанция находится поблизости." }, "reticulumPeers": { "hops": "Hops", @@ -1291,9 +1292,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Экземпляр Share включен — полностью закройте другие приложения Reticulum (MeshChatX, Ratspeak, автономный rnsd) и перезапустите этот стек перед повторной попыткой DM.", "bleBondRemoved": "{{name}}: Связь Bluetooth устарела (ОС по-прежнему показывает «Сопряжено»)", - "bleBondRemovedHint": "Забудьте этот RNode в Системных настройках → Bluetooth, запустите сопряжение по радио (Администратор → Bluetooth или удерживайте кнопку ~7 секунд), перезапустите стек и введите новый 6-значный PIN-код.", + "bleBondRemovedHint": "Сначала забудьте этот RNode в «Системные настройки» → Bluetooth (macOS не снимает Paired автоматически). По возможности предпочитайте USB serial или Wi‑Fi (tcp://), чтобы избежать BLE-связей. Затем: Admin → Bluetooth → Очистить спаренные устройства (USB, ESP32) если доступно, Начать сопряжение, удалите и снова добавьте этот BLE-интерфейс (Выбрать устройство), перезапустите стек и введите новый 6-значный PIN.", "blePairingTimedOut": "{{name}}: истекло время ожидания обмена ключами доступа Bluetooth", - "blePairingTimedOutHint": "Остановите стек, снова запустите сопряжение (Администратор → Bluetooth отобразит PIN-код на панели — дисплей радиостанции может оставаться пустым — или удерживайте кнопку ~7 секунд для ввода PIN-кода на экране), перезапустите стек один раз и введите этот PIN-код в диалоговом окне ОС в течение ~60 секунд. Никогда не используйте 123456 (по умолчанию Meshtastic)." + "blePairingTimedOutHint": "Остановите стек, снова запустите сопряжение (Администратор → Bluetooth отобразит PIN-код на панели — дисплей радиостанции может оставаться пустым — или удерживайте кнопку ~7 секунд для ввода PIN-кода на экране), перезапустите стек один раз и введите этот PIN-код в диалоговом окне ОС в течение ~60 секунд. Никогда не используйте 123456 (по умолчанию Meshtastic).", + "bleBondRemovedStopStack": "Стоп стек", + "bleBondRemovedOpenAdmin": "Открыть Bluetooth для администратора", + "txQueueDropsHintBle": "Этот RNode BLE не может истощить свою очередь TX (ссылка застряла или перекинулась). Предпочитайте последовательный USB или Wi-Fi (tcp://) или остановку стека и повторную сопряжение после «Забыть» в настройках системы → Bluetooth.", + "txQueueDropsHintBleBondStale": "Очередь TX заполнена, потому что связь BLE устарела. Забудьте об этом RNode в настройках системы → Bluetooth, затем используйте → функцию Admin Bluetooth (Очистить сопряжение / Начать сопряжение) или переключитесь на USB.", + "txQueueDropsHintNeutral": "Этот канал интерфейса TX заполнен. Проверьте работоспособность канала этого интерфейса (или отключите его), чтобы остановить отбрасывания." }, "reticulumSidecarBundledMissing": "В этой установке отсутствует входящая в комплект Reticulum sidecar. Обновите версию до более новой или сообщите о проблеме — сборка из исходного кода недоступна при пакетной установке.", "radioMqttRootDivergesWarning": "Корневая тема радио MQTT - {{radioRoot}}, но эта панель подписана на {{appPrefix}}. Сопоставьте радиостанцию, чтобы ограничить трафик узлов по всей стране — обновите префикс темы и повторно подключите MQTT.", @@ -1658,7 +1664,7 @@ "autoBeaconPhysicalFailures": "Сбой маяка AutoInterface TX на {{ifaces}} — обнаружение локальной сети может не работать, пока это не будет исправлено.", "tcpUnreachable": "Концентратор TCP \"{{name}}\" недоступен ({{host}}:{{port}})", "tcpConnectFailed": "TCP Hub \"{{name}}\" соединение отклонено или истекло", - "txQueueDrops": "Интерфейс \"{{name}}\" сбросил {{count}} исходящих пакетов (очередь TX заполнена)", + "txQueueDrops": "Интерфейс «{{name}}» сбросил {{count}} исходящих пакетов (очередь TX заполнена) — часто включён мёртвый TCP-хаб", "linkDeliveryTimeout": "Время ожидания прямой связи LXMF с {{hash}}… истекло ({{count}}×)", "transportSaturated": "Насыщенный транспорт RNS ({{count}} LXMF path-request drops)", "transportSaturatedShareInstance": "RNS транспорт насыщенный ({{count}} path-request drops). Экземпляр Share включен — закройте другие приложения Reticulum (MeshChatX, Ratspeak, rnsd) и перезапустите стек.", @@ -1678,7 +1684,10 @@ "announceBusPressureTipBoundaryHubs": "Включенные граничные / магистральные концентраторы: {{hubs}}.", "announceBusPressureTipTxSaturated": "Очередь TX падает на: {{names}} — этот интерфейс может быть насыщенным.", "tooManyDefaultBackbones": "У вас включена {{count}} магистраль по умолчанию. Подумайте о том, чтобы отключить дополнительные услуги и оставить около 2 для резервирования.", - "decommissionedHubEnabled": "Выведенный из эксплуатации хаб «{{name}}» всё ещё включён — mesh-client отключает его при каждом запуске стека. Используйте «Добавить магистрали по умолчанию» или каталог магистралей для рабочих конечных точек." + "decommissionedHubEnabled": "Выведенный из эксплуатации хаб «{{name}}» всё ещё включён — mesh-client отключает его при каждом запуске стека. Используйте «Добавить магистрали по умолчанию» или каталог магистралей для рабочих конечных точек.", + "txQueueDropsBle": "Интерфейс \"{{name}}\" сбросил {{count}} исходящих пакетов (очередь TX заполнена) — канал BLE RNode застрял; предпочтительнее USB или повторная сопряжение", + "txQueueDropsBleBondStale": "Интерфейс \"{{name}}\" сбросил {{count}} исходящих пакетов (очередь TX заполнена) — связь BLE устарела; забыть в ОС Bluetooth и выполнить повторную сопряжение", + "txQueueDropsNeutral": "Интерфейс \"{{name}}\" сбросил {{count}} исходящих пакетов (очередь TX заполнена) — проверьте ссылку на этот интерфейс" } }, "routingPort": { @@ -1851,7 +1860,12 @@ "pairingWaiting": "Ожидание сопряжения PIN-кода с радио через USB…", "pairingPinLabel": "PIN-код сопряжения Bluetooth", "pairingPinEnterHint": "Введите этот код в диалоговом окне Bluetooth ОС при подключении стека. Не используйте 123456.", - "bluetoothHint": "Включите Bluetooth на устройстве, затем используйте «Начать сопряжение». На этой панели через USB отображается 6-значный PIN-код — дисплей магнитолы часто остается пустым. Введите этот PIN-код в диалоговом окне Bluetooth вашей ОС, когда Reticulum подключается через BLE. У RNode нет PIN-кода по умолчанию (не используйте 123456 — это Meshtastic). На дисплеях удержание кнопки в течение ~7 секунд также может отобразить PIN-код на экране. RNode может не появиться в меню «Системные настройки» → «Bluetooth» до тех пор, пока не будет выполнено успешное соединение." + "bluetoothHint": "Включите Bluetooth на устройстве, затем «Начать сопряжение». 6-значный PIN появляется в этой панели по USB — дисплей радио часто пуст. Введите PIN в диалоге Bluetooth ОС, когда Reticulum подключается по BLE. У RNode нет PIN по умолчанию (не используйте 123456 — это Meshtastic). На платах с дисплеем удержание кнопки ~7 с тоже может показать PIN. RNode может не появиться в «Системные настройки» → Bluetooth до успешной связи. «Очистить спаренные устройства» (ESP32) очищает таблицу связей радио по USB перед повторным сопряжением; устройство всё равно нужно Забыть в системных настройках.", + "clearPairedDevices": "Очистить сопряженные устройства", + "clearPairedDevicesConfirmTitle": "Очистить Bluetooth-связи RNode?", + "clearPairedDevicesConfirmMessage": "Это отправляет CMD_BT_UNPAIR по USB и очищает связанные хосты на ESP32 RNode (без эффекта на некоторых сборках nRF52). Перед повторным сопряжением всё равно нужно забыть устройство в «Системные настройки» → Bluetooth.", + "clearPairedDevicesConfirm": "Очистить Bluetooth-связи", + "clearPairedDevicesSuccess": "Запрошена очистка таблицы Bluetooth-связей радио." }, "identityVault": { "title": "Хранилище личных данных", @@ -5280,7 +5294,7 @@ "n": "Чёрный рыцарь", "p": "Черная пешка" }, - "promotionChooserAria": "Выберите элемент промоакции", + "promotionChooserAria": "Выберите фигуру для превращения", "promoteTo": { "q": "Превращение в ферзя", "r": "Повысить до ладьи", @@ -5299,7 +5313,7 @@ "deleteConfirmMessage": "Удалить эту игровую сессию из списка? Это действие нельзя отменить.", "claimThreefold": "Претензия в три раза", "claimThreefoldAria": "Заявить о трехкратном повторении жеребьевки", - "claimFiftyMove": "Пункт 50 - ход", + "claimFiftyMove": "Заявить правило 50 ходов", "claimFiftyMoveAria": "Розыгрыш правила пятидесяти ходов", "delivery": { "sending": "отправка…", diff --git a/src/renderer/locales/tr/translation.json b/src/renderer/locales/tr/translation.json index 0c2b424ad..cd1f9c6cc 100644 --- a/src/renderer/locales/tr/translation.json +++ b/src/renderer/locales/tr/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "Varsayılan omurgalar eklenemedi.", "backboneEnableGuidanceLead": "En fazla 1 ila 3 omurga ağ geçidini etkinleştirin", "backboneEnableGuidanceBody": "—2 gereksiz bant genişliği şişmesi olmadan fazlalık için en uygun noktadır. 1 küresel merkez ve 1 bölgesel ağ geçidini hedefleyin ve kesinlikle gerektiğinde özel arayüzleri (I2P veya Yggdrasil gibi) etkinleştirin. Yerel olarak bağlı RNode'lar ve yerel lan arayüzleri bu omurga sınırına dahil edilmez.", - "defaultHubsPickerHint": "Birincil ve küresel omurga ile bölgenizden başlayın. En fazla 1–3 omurga ağ geçidi etkinleştirin (2 idealdir). Yerel RNode ve LAN arayüzleri sayılmaz. Yeni girdiler inceleme için kapalı eklenir. Sync ayrıca kalan kullanım dışı resmi testnet hub’larını da kapatır (Amsterdam)." + "defaultHubsPickerHint": "Birincil ve küresel omurga ile bölgenizden başlayın. En fazla 1–3 omurga ağ geçidi etkinleştirin (2 idealdir). Yerel RNode ve LAN arayüzleri sayılmaz. Yeni girdiler inceleme için kapalı eklenir. Sync ayrıca kalan kullanım dışı resmi testnet hub’larını da kapatır (Amsterdam).", + "rnodeTransportBleHint": "BLE bağları macOS ile senkronize olmayabilir (Forget + re-pair). Radyo yakındayken USB seri veya Wi-Fi (tcp://) tercih edin." }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "Sepet taşıma aktörü sıkışmış olabilir — Reticulum yığınını yeniden başlatın.", "shareInstanceHint": "Paylaşım örneği etkinleştirildi — diğer Reticulum uygulamalarından (MeshChatX, Ratspeak, standalone rnsd) tamamen çıkın ve DM'leri yeniden denemeden önce bu yığını yeniden başlatın.", "bleBondRemoved": "{{name}}: Bluetooth bağlantısı eski (İşletim sistemi hâlâ Eşlendi gösteriyor)", - "bleBondRemovedHint": "Sistem Ayarları → Bluetooth'ta bu RNode'u unutun, radyoda eşleştirmeyi başlatın (Yönetici → Bluetooth veya ~7 saniye düğmeyi basılı tutun), yığını yeniden başlatın ve yeni 6 haneli PIN'i girin.", + "bleBondRemovedHint": "Önce Sistem Ayarları → Bluetooth’ta bu RNode’u unutun (macOS Paired’i otomatik silmez). Mümkünse BLE bağlarından kaçınmak için USB seri veya Wi‑Fi (tcp://) tercih edin. Sonra: Admin → Bluetooth → Eşleşmiş cihazları temizle (USB, ESP32) varsa, Eşleştirmeyi başlat, bu BLE arayüzünü kaldırıp yeniden ekleyin (Cihaz seç), yığını yeniden başlatın ve yeni 6 haneli PIN’i girin.", "blePairingTimedOut": "{{name}}: Bluetooth şifre değişimi zaman aşımına uğradı", - "blePairingTimedOutHint": "Yığını durdurun, eşleştirmeyi yeniden başlatın (Yönetici → Bluetooth panelde PIN'i gösterir - radyo ekranı boş kalabilir - veya ekrandaki PIN için ~7 saniye basılı tutun), yığını bir kez yeniden başlatın ve bu PIN'i ~60 saniye içinde işletim sistemi iletişim kutusuna girin. Asla 123456 kullanmayın (Meshtastic varsayılanı)." + "blePairingTimedOutHint": "Yığını durdurun, eşleştirmeyi yeniden başlatın (Yönetici → Bluetooth panelde PIN'i gösterir - radyo ekranı boş kalabilir - veya ekrandaki PIN için ~7 saniye basılı tutun), yığını bir kez yeniden başlatın ve bu PIN'i ~60 saniye içinde işletim sistemi iletişim kutusuna girin. Asla 123456 kullanmayın (Meshtastic varsayılanı).", + "bleBondRemovedStopStack": "Yığını durdur", + "bleBondRemovedOpenAdmin": "Yönetici Bluetooth'unu aç", + "txQueueDropsHintBle": "Bu BLE RNode TX kuyruğunu boşaltamaz (bağlantı sıkışmış veya flapping). USB seri veya Wi-Fi (tcp://) tercih edin, veya Sistem Ayarları → Bluetooth'ta Forget sonrası yığını durdurup yeniden eşleştirin.", + "txQueueDropsHintBleBondStale": "BLE bağı bayat olduğu için TX kuyruğu dolu. Sistem Ayarları → Bluetooth'ta bu RNode'u unutun, ardından Yönetici → Bluetooth'unu (Eşleştirmeyi temizle/Eşleştirmeyi başlat) kullanın veya USB'ye geçin.", + "txQueueDropsHintNeutral": "Bu arayüz TX kanalı dolu. Düşmeleri durdurmak için bu arayüzün bağlantı sağlığını kontrol edin (veya devre dışı bırakın)." }, "reticulumSidecarBundledMissing": "Bu kurulumda birlikte verilen Reticulum sidecar eksik. Daha yeni bir sürüme yükseltin veya sorunu bildirin; kaynaktan derleme, paketli kurulumlarda mevcut değildir.", "radioMqttRootDivergesWarning": "Radyo MQTT kök konusu {{radioRoot}} 'dır, ancak bu panel {{appPrefix}}' e abone olur. Ülke çapındaki düğüm trafiğini sınırlamak için radyoyu eşleştirin — Konu Önekini güncelleyin ve MQTT'yi yeniden bağlayın.", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "AutoInterface beacon TX {{ifaces}} üzerinde başarısız oluyor — bu düzeltilinceye kadar yerel lan keşfi çalışmayabilir.", "tcpUnreachable": "TCP merkezi \"{{name}}\" ulaşılamıyor ({{host}}:{{port}})", "tcpConnectFailed": "TCP hub \"{{name}}\" bağlantısı reddedildi veya zaman aşımına uğradı", - "txQueueDrops": "Arayüz \"{{name}}\" düştü {{count}} giden paketler (TX kuyruğu dolu)", + "txQueueDrops": "\"{{name}}\" arayüzü {{count}} giden paketi düşürdü (TX kuyruğu dolu) — sıkça hâlâ etkin ölü bir TCP hub’ı", "linkDeliveryTimeout": "{{hash}} 'a doğrudan LXMF bağlantısı… zaman aşımına uğradı ({{count}}×)", "transportSaturated": "RNS transport saturated ({{count}} LXMF path - request drops)", "transportSaturatedShareInstance": "RNS nakli doymuş ({{count}} yol talebi düşer). Paylaşım örneği açık — diğer Reticulum uygulamalarından (MeshChatX, Ratspeak, rnsd) çıkın ve yığını yeniden başlatın.", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "Etkin sınır / omurga göbekleri: {{hubs}}.", "announceBusPressureTipTxSaturated": "TX kuyruğu şu tarihte düşer: {{names}} — bu arayüz doymuş olabilir.", "tooManyDefaultBackbones": "{{count}} varsayılan omurganız etkin. Ekstraları devre dışı bırakmayı ve fazlalık için yaklaşık 2 tane tutmayı düşünün.", - "decommissionedHubEnabled": "Kullanım dışı hub «{{name}}» hâlâ açık — mesh-client her yığın başlangıcında kapatır. Canlı uç noktalar için «Varsayılan omurga bağlantıları ekle» veya omurga dizinini kullanın." + "decommissionedHubEnabled": "Kullanım dışı hub «{{name}}» hâlâ açık — mesh-client her yığın başlangıcında kapatır. Canlı uç noktalar için «Varsayılan omurga bağlantıları ekle» veya omurga dizinini kullanın.", + "txQueueDropsBle": "Arabirim \"{{name}}\" düştü {{count}} giden paketler (TX kuyruğu dolu) — BLE RNode bağlantısı takıldı; USB'yi tercih edin veya yeniden eşleştirin", + "txQueueDropsBleBondStale": "Arayüz \"{{name}}\" düştü {{count}} giden paketler (TX kuyruğu dolu) — BLE bond bayat; İşletim sistemi Bluetooth'unu unutun ve yeniden eşleştirin", + "txQueueDropsNeutral": "Arabirim \"{{name}}\" düştü {{count}} giden paketler (TX kuyruğu dolu) — arabirimin bağlantısını kontrol edin" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "Radyodan USB üzerinden eşleştirme PIN'i bekleniyor…", "pairingPinLabel": "Bluetooth eşleştirme PIN'i", "pairingPinEnterHint": "Yığın bağlandığında bu kodu OS Bluetooth iletişim kutusuna girin. 123456'yı kullanmayın.", - "bluetoothHint": "Cihazda Bluetooth'u etkinleştirin ve ardından Eşleştirmeyi başlat seçeneğini kullanın. 6 haneli PIN, USB üzerinden bu panelde görünür; radyo ekranı genellikle boş kalır. Reticulum BLE üzerinden bağlandığında bu PIN'i işletim sisteminizin Bluetooth iletişim kutusuna girin. RNode'un varsayılan PIN'i yoktur (123456 kullanmayın; bu Meshtastic'tir). Ekran kartlarında, yaklaşık 7 saniyelik bir düğme basılı tutulduğunda ekranda bir PIN görüntülenebilir. RNode, başarılı bir bağlantı sağlanana kadar Sistem Ayarları → Bluetooth'ta görünmeyebilir." + "bluetoothHint": "Cihazda Bluetooth’u açın, sonra Eşleştirmeyi başlat’ı kullanın. 6 haneli PIN bu panelde USB üzerinden görünür — radyo ekranı çoğu zaman boş kalır. Reticulum BLE ile bağlandığında o PIN’i OS Bluetooth iletişim kutusuna girin. RNode’un varsayılan PIN’i yoktur (123456 kullanmayın — o Meshtastic’tir). Ekranlı kartlarda ~7 sn düğme basılı tutmak da PIN gösterebilir. Başarılı bağdan önce RNode Sistem Ayarları → Bluetooth’ta görünmeyebilir. Eşleşmiş cihazları temizle (ESP32), yeniden eşleştirmeden önce USB üzerinden radyo bağ tablosunu temizler; Sistem Ayarları’nda cihazı hâlâ Unutmanız gerekir.", + "clearPairedDevices": "Eşleştirilmiş cihazları temizle", + "clearPairedDevicesConfirmTitle": "RNode Bluetooth bağları temizlensin mi?", + "clearPairedDevicesConfirmMessage": "Bu, USB üzerinden CMD_BT_UNPAIR gönderir ve ESP32 RNode’lardaki bağlı ana bilgisayarları temizler (bazı nRF52 derlemelerinde etkisiz). Yeniden eşleştirmeden önce Sistem Ayarları → Bluetooth’ta cihazı hâlâ Unutmanız gerekir.", + "clearPairedDevicesConfirm": "Bluetooth bağlarını temizle", + "clearPairedDevicesSuccess": "Radyo Bluetooth bağ tablosu temizleme istendi." }, "identityVault": { "title": "Kimlik kasası", diff --git a/src/renderer/locales/uk/translation.json b/src/renderer/locales/uk/translation.json index b71d4785f..2c4c4aff5 100644 --- a/src/renderer/locales/uk/translation.json +++ b/src/renderer/locales/uk/translation.json @@ -1190,7 +1190,8 @@ "addDefaultHubsFailed": "Не вдалося додати магістралі за замовчуванням.", "backboneEnableGuidanceLead": "Увімкнути щонайбільше від 1 до 3 магістральних шлюзів", "backboneEnableGuidanceBody": "—2 - це солодка пляма для надлишковості без зайвого роздування пропускної здатності. Створіть 1 глобальний хаб та 1 регіональний шлюз, а також увімкніть спеціалізовані інтерфейси (такі як I2P або Yggdrasil) строго за потреби. Місцеві з'єднані RNodes та локальні інтерфейси локальної мережі не враховуються до цієї межі магістралі.", - "defaultHubsPickerHint": "Почніть з первинної та глобальної магістралі плюс ваш регіон. Увімкніть не більше 1–3 магістральних шлюзів (2 — оптимум). Локальні RNode та LAN-інтерфейси не враховуються. Нові записи додаються вимкненими для перевірки. Sync також вимикає залишкові виведені з експлуатації офіційні тестові хаби (Амстердам)." + "defaultHubsPickerHint": "Почніть з первинної та глобальної магістралі плюс ваш регіон. Увімкніть не більше 1–3 магістральних шлюзів (2 — оптимум). Локальні RNode та LAN-інтерфейси не враховуються. Нові записи додаються вимкненими для перевірки. Sync також вимикає залишкові виведені з експлуатації офіційні тестові хаби (Амстердам).", + "rnodeTransportBleHint": "Зв'язки BLE можуть розсинхронізуватися з macOS (Forget + re-pair). Якщо радіоприймач поруч, віддайте перевагу USB serial або Wi-Fi (tcp://)." }, "reticulumPeers": { "hops": "Hops", @@ -1291,9 +1292,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "Екземпляр Share увімкнено — повністю закрийте інші програми Reticulum (MeshChatX, Ratspeak, автономний rnsd) і перезапустіть цей стек перед повторною спробою DM.", "bleBondRemoved": "{{name}}: з’єднання Bluetooth застаріле (ОС все ще відображає сполучення)", - "bleBondRemovedHint": "Забудьте про цей вузол RNode у системних налаштуваннях → Bluetooth, почніть сполучення на радіо (Адміністратор → Bluetooth або утримання кнопки ~7 с), перезапустіть стек і введіть новий 6-значний PIN-код.", + "bleBondRemovedHint": "Спочатку забудьте цей RNode у Системних параметрах → Bluetooth (macOS не знімає Paired автоматично). За можливості надавайте перевагу USB serial або Wi‑Fi (tcp://), щоб уникнути BLE-зв’язків. Далі: Admin → Bluetooth → Очистити спарені пристрої (USB, ESP32) якщо доступно, Почати сполучення, видаліть і знову додайте цей BLE-інтерфейс (Вибрати пристрій), перезапустіть стек і введіть новий 6-значний PIN.", "blePairingTimedOut": "{{name}}: час очікування обміну паролем Bluetooth минув", - "blePairingTimedOutHint": "Зупиніть стек, почніть сполучення знову (Адміністратор → Bluetooth показує PIN-код на панелі — дисплей радіо може залишатися порожнім — або утримуйте кнопку ~7 с для введення PIN-коду на екрані), перезапустіть стек один раз і введіть цей PIN-код у діалоговому вікні ОС протягом ~60 секунд. Ніколи не використовуйте 123456 (Meshtastic за умовчанням)." + "blePairingTimedOutHint": "Зупиніть стек, почніть сполучення знову (Адміністратор → Bluetooth показує PIN-код на панелі — дисплей радіо може залишатися порожнім — або утримуйте кнопку ~7 с для введення PIN-коду на екрані), перезапустіть стек один раз і введіть цей PIN-код у діалоговому вікні ОС протягом ~60 секунд. Ніколи не використовуйте 123456 (Meshtastic за умовчанням).", + "bleBondRemovedStopStack": "Зупинити стек", + "bleBondRemovedOpenAdmin": "Відкрити Bluetooth для адміністратора", + "txQueueDropsHintBle": "Цей вузол BLE RNode не може зливати свою чергу TX (посилання застрягло або перекидається). Віддайте перевагу послідовному USB-з'єднанню або Wi-Fi (tcp://), або зупиніть стек і повторіть з'єднання після «Забути» в системних налаштуваннях → Bluetooth.", + "txQueueDropsHintBleBondStale": "Черга TX заповнена, оскільки зв'язок BLE застарів. Забудьте про цей вузол RNode у системних налаштуваннях → Bluetooth, а потім скористайтеся → функцією Admin Bluetooth (Очистити з'єднання/ Почати з'єднання) або переключіться на USB.", + "txQueueDropsHintNeutral": "Цей канал інтерфейсу TX заповнений. Перевірте здоров'я зв'язку цього інтерфейсу (або вимкніть його), щоб зупинити падіння." }, "reticulumSidecarBundledMissing": "У цій інсталяції відсутній Reticulum sidecar. Оновіть до новішого випуску або повідомте про проблему — збірка з вихідного коду недоступна в пакетних встановленнях.", "radioMqttRootDivergesWarning": "Коренева тема радіо MQTT - {{radioRoot}}, але ця панель підписується на {{appPrefix}}. Підключіть радіоприймач, щоб обмежити загальнонаціональний трафік вузлів — оновіть префікс теми та повторно підключіть MQTT.", @@ -1658,7 +1664,7 @@ "autoBeaconPhysicalFailures": "AutoInterface beacon TX зазнає невдачі на {{ifaces}} — виявлення локальної мережі може не працювати, поки це не буде виправлено.", "tcpUnreachable": "Концентратор TCP \"{{name}}\" недоступний ({{host}}:{{port}})", "tcpConnectFailed": "З'єднання TCP Hub \"{{name}}\" відхилено або вичерпано час очікування", - "txQueueDrops": "Інтерфейс \"{{name}}\" скинув {{count}} вихідних пакетів (черга TX заповнена)", + "txQueueDrops": "Інтерфейс «{{name}}» відкинув {{count}} вихідних пакетів (черга TX заповнена) — часто увімкнений мертвий TCP-хаб", "linkDeliveryTimeout": "Пряме посилання LXMF на {{hash}}… вичерпано час очікування ({{count}}×)", "transportSaturated": "Транспорт RNS насичений ({{count}} LXMF path-request drops)", "transportSaturatedShareInstance": "Транспорт RNS насичений ({{count}} краплі запиту шляху). Екземпляр Share увімкнено — закрийте інші програми Reticulum (MeshChatX, Ratspeak, rnsd) і перезапустіть стек.", @@ -1678,7 +1684,10 @@ "announceBusPressureTipBoundaryHubs": "Увімкнені граничні / магістральні концентратори: {{hubs}}.", "announceBusPressureTipTxSaturated": "Черга TX починається: {{names}} — цей інтерфейс може бути насиченим.", "tooManyDefaultBackbones": "У вас увімкнено магістралі за замовчуванням {{count}}. Розгляньте можливість вимкнення додаткових послуг і збереження близько 2 для надмірності.", - "decommissionedHubEnabled": "Виведений з експлуатації хаб «{{name}}» досі ввімкнений — mesh-client вимикає його при кожному запуску стека. Використовуйте «Додати магістралі за замовчуванням» або каталог магістралей для робочих кінцевих точок." + "decommissionedHubEnabled": "Виведений з експлуатації хаб «{{name}}» досі ввімкнений — mesh-client вимикає його при кожному запуску стека. Використовуйте «Додати магістралі за замовчуванням» або каталог магістралей для робочих кінцевих точок.", + "txQueueDropsBle": "Інтерфейс \"{{name}}\" відкинув вихідні пакети {{count}} (черга TX заповнена) — посилання BLE RNode застрягло; віддайте перевагу USB або повторному з'єднанню", + "txQueueDropsBleBondStale": "Інтерфейс \"{{name}}\" скинув {{count}} вихідних пакетів (черга TX заповнена) — BLE зв'язок застарів; Забути в ОС Bluetooth і повторно з'єднати", + "txQueueDropsNeutral": "Інтерфейс \"{{name}}\" скинув {{count}} вихідних пакетів (черга TX заповнена) — перевірте посилання цього інтерфейсу" } }, "routingPort": { @@ -1851,7 +1860,12 @@ "pairingWaiting": "Очікування підключення PIN-коду з радіо через USB…", "pairingPinLabel": "PIN-код підключення Bluetooth", "pairingPinEnterHint": "Введіть цей код у діалоговому вікні ОС Bluetooth під час підключення стека. Не використовуйте 123456.", - "bluetoothHint": "Увімкніть Bluetooth на пристрої, а потім скористайтеся Почати з’єднання. 6-значний PIN-код з’являється на цій панелі через USB — дисплей радіо часто залишається порожнім. Введіть цей PIN-код у діалоговому вікні Bluetooth ОС, коли Reticulum підключається через BLE. RNode не має PIN-коду за замовчуванням (не використовуйте 123456 — це Meshtastic). На панелях дисплеїв утримання кнопки ~7 с також може показати PIN-код на екрані. RNode може не з’явитися в системних налаштуваннях → Bluetooth, доки не буде встановлено успішне з’єднання." + "bluetoothHint": "Увімкніть Bluetooth на пристрої, потім «Почати сполучення». 6-значний PIN з’являється в цій панелі через USB — дисплей радіо часто порожній. Введіть цей PIN у діалозі Bluetooth ОС, коли Reticulum з’єднується через BLE. У RNode немає PIN за замовчуванням (не використовуйте 123456 — це Meshtastic). На платах з дисплеєм утримання кнопки ~7 с також може показати PIN. RNode може не з’явитися в Системних параметрах → Bluetooth до успішного зв’язку. «Очистити спарені пристрої» (ESP32) очищає таблицю зв’язків радіо через USB перед повторним сполученням; пристрій все одно потрібно Забути в системних параметрах.", + "clearPairedDevices": "Очистити спарені пристрої", + "clearPairedDevicesConfirmTitle": "Очистити Bluetooth-зв'язки RNode?", + "clearPairedDevicesConfirmMessage": "Надсилає CMD_BT_UNPAIR через USB і очищає пов’язані хости на ESP32 RNode (без ефекту на деяких збірках nRF52). Перед повторним сполученням все одно потрібно Забути пристрій у Системних параметрах → Bluetooth.", + "clearPairedDevicesConfirm": "Очистити Bluetooth-зв’язки", + "clearPairedDevicesSuccess": "Запитано очищення таблиці Bluetooth-зв’язків радіо." }, "identityVault": { "title": "Сховище ідентифікаційних даних", diff --git a/src/renderer/locales/zh/translation.json b/src/renderer/locales/zh/translation.json index 7ea66c0f7..0c0dbdfc7 100644 --- a/src/renderer/locales/zh/translation.json +++ b/src/renderer/locales/zh/translation.json @@ -1188,7 +1188,8 @@ "addDefaultHubsFailed": "添加默认骨干网失败。", "backboneEnableGuidanceLead": "最多启用1至3个骨干网关", "backboneEnableGuidanceBody": "—2是冗余的最佳选择,不会造成不必要的带宽膨胀。瞄准1个全球集线器和1个区域网关,并严格根据需要启用专业接口(如I2P或Yggdrasil )。本地连接的RNode和本地LAN接口不计入此骨干限制。", - "defaultHubsPickerHint": "从“主要和全球骨干网”加上您的区域开始。最多启用 1 到 3 个骨干网关(2 个较理想)。本地 RNode 和局域网接口不计入。新条目默认禁用,便于您审阅后再启用。同步还会禁用剩余已停用的官方测试网枢纽(阿姆斯特丹)。" + "defaultHubsPickerHint": "从“主要和全球骨干网”加上您的区域开始。最多启用 1 到 3 个骨干网关(2 个较理想)。本地 RNode 和局域网接口不计入。新条目默认禁用,便于您审阅后再启用。同步还会禁用剩余已停用的官方测试网枢纽(阿姆斯特丹)。", + "rnodeTransportBleHint": "BLE绑定可以与macOS取消同步(忘记+重新配对)。当收音机在附近时,首选USB串行或Wi-Fi ( tcp://)。" }, "reticulumPeers": { "hops": "Hops", @@ -1289,9 +1290,14 @@ "slowTransportQueryHint": "The sidecar transport actor may be wedged — restart the Reticulum stack.", "shareInstanceHint": "共享实例已启用—在重试DM之前,完全退出其他Reticulum应用程序( MeshChatX、Ratspeak、独立rnsd )并重新启动此堆栈。", "bleBondRemoved": "{{name}}:蓝牙绑定已过时(操作系统仍显示已配对)", - "bleBondRemovedHint": "在“系统设置”→“蓝牙”中忘记此 RNode,开始在无线电上进行配对(管理→蓝牙或按住约 7 秒按钮),重新启动堆栈,然后输入新的 6 位 PIN。", + "bleBondRemovedHint": "请先在系统设置 → 蓝牙中忘记此 RNode(macOS 不会自动清除“已配对”)。尽量优先使用 USB 串口或 Wi‑Fi(tcp://)以避免 BLE 绑定。然后:管理 → 蓝牙 → 清除已配对设备(USB,ESP32,如可用)、开始配对、移除此 BLE 接口并重新添加(选择设备)、重启协议栈,并输入新的 6 位 PIN。", "blePairingTimedOut": "{{name}}:蓝牙密钥交换超时", - "blePairingTimedOutHint": "停止堆栈,再次开始配对(管理 → 蓝牙在面板中显示 PIN — 无线电显示屏可能保持空白 — 或按住按钮约 7 秒以获取屏幕上的 PIN),重新启动堆栈一次,然后在约 60 秒内在操作系统对话框中输入该 PIN。切勿使用 123456(Meshtastic 默认值)。" + "blePairingTimedOutHint": "停止堆栈,再次开始配对(管理 → 蓝牙在面板中显示 PIN — 无线电显示屏可能保持空白 — 或按住按钮约 7 秒以获取屏幕上的 PIN),重新启动堆栈一次,然后在约 60 秒内在操作系统对话框中输入该 PIN。切勿使用 123456(Meshtastic 默认值)。", + "bleBondRemovedStopStack": "停止堆栈", + "bleBondRemovedOpenAdmin": "打开管理员蓝牙", + "txQueueDropsHintBle": "此BLE RNode无法排出其TX队列(链接卡住或抖动)。首选USB串行或Wi-Fi (tcp://) ,或在忘记系统设置→蓝牙后停止堆栈并重新配对。", + "txQueueDropsHintBleBondStale": "TX队列已满,因为BLE绑定已过时。在系统设置→蓝牙中忘记此RNode ,然后使用管理→蓝牙(清除配对/开始配对)或切换到USB。", + "txQueueDropsHintNeutral": "此接口TX通道已满。检查该接口的链接健康状况(或禁用它)以停止丢弃。" }, "reticulumSidecarBundledMissing": "此安装缺少捆绑的 Reticulum sidecar。升级到较新的版本或报告问题 - 在打包安装中无法从源代码构建。", "radioMqttRootDivergesWarning": "无线电MQTT根主题为{{radioRoot}} ,但此面板订阅了{{appPrefix}}。匹配无线电以限制全国节点流量—更新主题前缀并重新连接MQTT。", @@ -1656,7 +1662,7 @@ "autoBeaconPhysicalFailures": "自动接口信标TX在{{ifaces}}失败—在修复此问题之前,本地局域网发现可能无法正常工作。", "tcpUnreachable": "无法访问TCP集线器「{{name}}」( {{host}} : {{port}} )", "tcpConnectFailed": "TCP集线器“{{name}}”连接被拒绝或超时", - "txQueueDrops": "接口“{{name}}”丢弃了{{count}}个出站数据包( TX队列已满)", + "txQueueDrops": "接口“{{name}}”丢弃了 {{count}} 个出站数据包(TX 队列已满)— 通常是仍启用的失效 TCP 中继", "linkDeliveryTimeout": "直接LXMF链接到{{hash}} …超时( {{count}} × )", "transportSaturated": "RNS传输饱和( {{count}} LXMF路径请求丢弃)", "transportSaturatedShareInstance": "RNS传输饱和( {{count}}路径请求丢弃)。共享实例已打开—退出其他Reticulum应用程序( MeshChatX、Ratspeak、rnsd )并重新启动堆栈。", @@ -1676,7 +1682,10 @@ "announceBusPressureTipBoundaryHubs": "已启用边界/骨干集线器: {{hubs}}。", "announceBusPressureTipTxSaturated": "TX队列丢弃时间: {{names}} —该接口可能已饱和。", "tooManyDefaultBackbones": "您已启用{{count}}默认骨干网。请考虑禁用额外功能,并保留约2个以备不时之需。", - "decommissionedHubEnabled": "已停用的枢纽“{{name}}”仍处于启用状态 — mesh-client 会在每次启动协议栈时关闭它。请使用“添加默认骨干网”或骨干网目录获取可用端点。" + "decommissionedHubEnabled": "已停用的枢纽“{{name}}”仍处于启用状态 — mesh-client 会在每次启动协议栈时关闭它。请使用“添加默认骨干网”或骨干网目录获取可用端点。", + "txQueueDropsBle": "接口“{{name}}”丢弃了{{count}}个出站数据包( TX队列已满) — BLE RNode链接卡住;首选USB或重新配对", + "txQueueDropsBleBondStale": "接口“{{name}}”丢弃了{{count}}个出站数据包( TX队列已满) — BLE绑定已过期;忘记操作系统蓝牙并重新配对", + "txQueueDropsNeutral": "接口“{{name}}”丢弃了{{count}}个出站数据包( TX队列已满) —检查该接口的链路" } }, "routingPort": { @@ -1849,7 +1858,12 @@ "pairingWaiting": "正在等待通过 USB 从无线电配对 PIN...", "pairingPinLabel": "蓝牙配对 PIN 码", "pairingPinEnterHint": "当堆栈连接时,在操作系统蓝牙对话框中输入此代码。不要使用 123456。", - "bluetoothHint": "启用设备上的蓝牙,然后使用开始配对。 6 位 PIN 码通过 USB 出现在该面板中 — 收音机显示屏通常保持空白。当 Reticulum 通过 BLE 连接时,在操作系统蓝牙对话框中输入该 PIN。 RNode 没有默认 PIN(不要使用 123456 — 即 Meshtastic)。在显示板上,按住约 7 秒的按钮也可以在屏幕上显示 PIN。在成功绑定之前,RNode 可能不会出现在系统设置 → 蓝牙中。" + "bluetoothHint": "在设备上启用蓝牙,然后使用“开始配对”。6 位 PIN 通过 USB 显示在此面板中 — 电台屏幕通常保持空白。当 Reticulum 通过 BLE 连接时,在操作系统蓝牙对话框中输入该 PIN。RNode 没有默认 PIN(不要使用 123456 — 那是 Meshtastic)。在带显示屏的板上,按住按钮约 7 秒也可能显示 PIN。成功绑定前,RNode 可能不会出现在系统设置 → 蓝牙中。“清除已配对设备”(ESP32)会通过 USB 清除电台绑定表后再重新配对;仍须在系统设置中“忘记”该设备。", + "clearPairedDevices": "清除已配对的设备", + "clearPairedDevicesConfirmTitle": "清除RNode蓝牙绑定?", + "clearPairedDevicesConfirmMessage": "这将通过 USB 发送 CMD_BT_UNPAIR,并清除 ESP32 RNode 上的已绑定主机(部分 nRF52 固件无操作)。重新配对前,仍须在系统设置 → 蓝牙中“忘记”该设备。", + "clearPairedDevicesConfirm": "清除蓝牙绑定", + "clearPairedDevicesSuccess": "已请求清除电台蓝牙绑定表。" }, "identityVault": { "title": "身份保管库", @@ -5278,12 +5292,12 @@ "n": "黑骑士", "p": "黑兵" }, - "promotionChooserAria": "选择促销片段", + "promotionChooserAria": "选择升变棋子", "promoteTo": { - "q": "晋升为大床房", - "r": "晋升为车队", - "b": "晋升为主教", - "n": "晋升为骑士" + "q": "升变为后", + "r": "升变为车", + "b": "升变为象", + "n": "升变为马" } }, "errors": { @@ -5297,7 +5311,7 @@ "deleteConfirmMessage": "从您的列表中删除此游戏会话?此操作无法撤消。", "claimThreefold": "领取三倍奖励", "claimThreefoldAria": "领取三重重复抽奖", - "claimFiftyMove": "领取50步", + "claimFiftyMove": "声明五十步规则", "claimFiftyMoveAria": "领取五十步规则抽奖", "delivery": { "sending": "发送中…", diff --git a/src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts b/src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts index 08b64c395..8b492b1b4 100644 --- a/src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts +++ b/src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts @@ -255,13 +255,13 @@ describe('useMeshcoreRuntime auto-reconnect (regression)', () => { const finallyBody = reconnectBody.slice(reconnectBody.indexOf('finally {')); expect(finallyBody).toContain('bleConnectInProgressRef.current = false'); expect(RUNTIME_SOURCE).toMatch( - /meshcoreReconnectAttemptRef\.current >= maxReconnectAttempts[\s\S]{0,400}bleConnectInProgressRef\.current = false/, + /meshcoreReconnectAttemptRef\.current >= maxReconnectAttempts[\s\S]{0,800}bleConnectInProgressRef\.current = false/, ); }); it('escalates serial reconnect exhaustion with forget and re-select UI flag', () => { expect(RUNTIME_SOURCE).toMatch( - /meshcoreReconnectAttemptRef\.current >= maxReconnectAttempts[\s\S]{0,500}escalateSerialReconnectExhaustion/, + /meshcoreReconnectAttemptRef\.current >= maxReconnectAttempts[\s\S]{0,900}escalateSerialReconnectExhaustion/, ); expect(RUNTIME_SOURCE).toContain('serialNeedsReselect'); expect(RUNTIME_SOURCE).toContain('attachMeshcoreSerialTransportLossWatch'); @@ -285,7 +285,7 @@ describe('useMeshcoreRuntime auto-reconnect (regression)', () => { /window\.electronAPI\.meshcore\.tcp\.onDisconnected\(\(\) => \{[\s\S]*?latchTcpBridgeDeadForBurst\('ipc'\)/, ); expect(RUNTIME_SOURCE).toContain("meshcoreConnectTypeRef.current === 'tcp'"); - expect(RUNTIME_SOURCE).toContain("source === 'ipc'"); + expect(RUNTIME_SOURCE).toContain("latchTcpBridgeDeadForBurst('write')"); expect(RUNTIME_SOURCE).toContain('handleMeshcoreConnectionLostRef.current()'); }); @@ -299,9 +299,18 @@ describe('useMeshcoreRuntime auto-reconnect (regression)', () => { 'TCP write-dead after init burst — latch bridge dead, defer reconnect', ); expect(RUNTIME_SOURCE).toContain('setMeshcoreTcpWriteDeadListener'); + expect(RUNTIME_SOURCE).toContain('setMeshcoreTcpSoftApDeadAccepted'); + expect(RUNTIME_SOURCE).toContain( + 'TCP write-dead on SoftAP-accepted dead bridge — keep configured', + ); + expect(RUNTIME_SOURCE).toContain('ensureTcpLiveForUserTx'); + expect(RUNTIME_SOURCE).toContain('notifyMeshcoreTcpLiveForUserTx'); + expect(RUNTIME_SOURCE).toContain('yieldToMeshcoreTcpUserTxSends'); expect(RUNTIME_SOURCE).toMatch( /shouldDeferMeshcoreTcpReconnectAfterBurst\(\{[\s\S]*?burstCaptured:[\s\S]*?everConfigured:[\s\S]*?deviceConfigured:[\s\S]*?\}\)[\s\S]*?meshcoreDeferredReconnectRef\.current = true;[\s\S]*?return;[\s\S]*?handleMeshcoreConnectionLostRef\.current\(\)/, ); + // SoftAP: accept dead bridge without immediate reconnect (avoid FIN-after-contacts loop). + // SoftAP-accepted suppresses background write-dead → lost; mid-session death still reconnects. expect(RUNTIME_SOURCE).toContain('TCP burst-complete configure — accepting dead bridge'); expect(RUNTIME_SOURCE).toContain( 'TCP burst-complete reconnect attach — accepting dead bridge (configured)', @@ -558,18 +567,17 @@ describe('meshcoreConnSideEffects disconnected handler (regression)', () => { it('triggers handleConnectionLost when an operational session drops', () => { expect(CONN_EVENTS_SOURCE).toMatch(/case 'device_status':[\s\S]{0,400}handleDisconnected\(\)/); expect(CONN_EVENTS_SOURCE).toMatch( - /handleDisconnected[\s\S]{0,2000}handleConnectionLostRef\.current\(\)/, + /handleDisconnected[\s\S]{0,3000}handleConnectionLostRef\.current\(\)/, ); }); - it('skips handleConnectionLost on explicit user disconnect or MeshCore TCP', () => { - // TCP reconnect is owned by runtime meshcore.tcp.onDisconnected (avoid dual entry). - expect(CONN_EVENTS_SOURCE).toMatch( - /shouldReconnect &&\s*!meshcoreExplicitDisconnectRef\.current &&[\s\S]*?meshcoreConnectTypeRef\.current !== 'tcp'/, - ); + it('skips TCP device_status disconnect teardown (runtime owns SoftAP bridge recovery)', () => { + // SoftAP FIN emits device_status via TcpOverIpc; tearing down the driver here left + // "accepting dead bridge" with no handle and no scheduled reconnect. + expect(CONN_EVENTS_SOURCE).toMatch(/meshcoreConnectTypeRef\.current === 'tcp'[\s\S]*?return;/); }); - it('tears down ConnectionDriver on disconnect when driver path was active', () => { + it('tears down ConnectionDriver on non-TCP disconnect when driver path was active', () => { expect(CONN_EVENTS_SOURCE).toContain('meshcoreDriverConnectedRef.current'); expect(CONN_EVENTS_SOURCE).toMatch( /teardownMeshcoreConnEventListeners\(\{ driverDisconnect: usedDriverConnect \}\)/, @@ -616,6 +624,18 @@ describe('useMeshcoreRuntime prepareRfConnect driver teardown (regression)', () expect(prepareBody).toContain('meshcorePendingDriverIdentityRef.current'); }); + it('clears SoftAP dead-bridge latch on every prepareRfConnect (not TCP-only)', () => { + const prepareBody = extractUseCallbackBody(RUNTIME_SOURCE, 'prepareRfConnect'); + const softApClearIdx = prepareBody.indexOf('setMeshcoreTcpSoftApDeadAccepted(false)'); + const bridgeClearIdx = prepareBody.indexOf('meshcoreTcpBridgeDeadRef.current = false'); + const tcpGuardIdx = prepareBody.indexOf("if (type === 'tcp')"); + expect(softApClearIdx).toBeGreaterThan(-1); + expect(bridgeClearIdx).toBeGreaterThan(-1); + expect(tcpGuardIdx).toBeGreaterThan(-1); + expect(softApClearIdx).toBeLessThan(tcpGuardIdx); + expect(bridgeClearIdx).toBeLessThan(tcpGuardIdx); + }); + it('clears connection params on manual prepare and latches provisional params before open', () => { const prepareBody = extractUseCallbackBody(RUNTIME_SOURCE, 'prepareRfConnect'); expect(prepareBody).toMatch( diff --git a/src/renderer/runtime/useMeshcoreRuntime.ts b/src/renderer/runtime/useMeshcoreRuntime.ts index a72ed895a..9d4070a4c 100644 --- a/src/renderer/runtime/useMeshcoreRuntime.ts +++ b/src/renderer/runtime/useMeshcoreRuntime.ts @@ -169,8 +169,14 @@ import { import { attachMeshcoreSerialTransportLossWatch } from '../lib/meshcore/meshcoreSerialTransportLoss'; import { isMeshcoreTcpBurstDeadBridge, + isMeshcoreTcpSoftApDeadAccepted, + notifyMeshcoreTcpLiveForUserTx, + rejectMeshcoreTcpLiveForUserTx, + setMeshcoreTcpSoftApDeadAccepted, setMeshcoreTcpWriteDeadListener, shouldDeferMeshcoreTcpReconnectAfterBurst, + waitForMeshcoreTcpLiveForUserTx, + yieldToMeshcoreTcpUserTxSends, } from '../lib/meshcore/meshcoreTcpInitBurst'; import { buildMeshcoreOutboundTapbackWire, @@ -2292,6 +2298,13 @@ export function useMeshcoreRuntime() { }); } + // SoftAP user TX: release waiters while the socket is still live — companions often + // FIN immediately after getContacts. Await tracked sends before starting the dump. + if (transportType === 'tcp' && !meshcoreTcpBridgeDeadRef.current) { + notifyMeshcoreTcpLiveForUserTx(); + await yieldToMeshcoreTcpUserTxSends(); + } + const promoteConfiguredAfterContactsDump = (): void => { setState((prev) => ({ ...prev, @@ -2852,8 +2865,11 @@ export function useMeshcoreRuntime() { // and reconnect deferral cannot stick after BLE/serial prepare aborts a prior open. meshcoreInitConnInFlightRef.current = false; meshcoreInitConnInFlightSetupGenRef.current = null; + // SoftAP dead-bridge latch can outlive a TCP session — clear on every prepare so + // BLE/serial opens do not inherit a stale "accepted dead bridge" TX path. + meshcoreTcpBridgeDeadRef.current = false; + setMeshcoreTcpSoftApDeadAccepted(false); if (type === 'tcp') { - meshcoreTcpBridgeDeadRef.current = false; meshcoreTcpInitBurstCapturedRef.current = false; meshcoreTcpContactsDumpInFlightRef.current = false; if (!opts?.preserveReconnectState) { @@ -3076,6 +3092,9 @@ export function useMeshcoreRuntime() { meshcoreIsReconnectingRef.current = false; meshcoreReconnectAttemptRef.current = 0; meshcoreRfReconnectRef.current.markExhausted(); + if (params.rfType === 'tcp') { + rejectMeshcoreTcpLiveForUserTx(new Error('MeshCore TCP reconnect exhausted')); + } if (params.rfType === 'ble') { bleConnectInProgressRef.current = false; } @@ -3290,9 +3309,10 @@ export function useMeshcoreRuntime() { // Burst-complete attach left a dead bridge (OpenHop FIN after contacts). UI is configured // from the contacts burst — accept that session. Forcing an immediate live-socket retry // loops forever on companions that FIN after every contacts dump (WAN :5054 / SoftAP). - // Live recovery happens on write-fail / later tcp-disconnected once deviceConfigured. + // SoftAP-accepted: keep configured; background write-dead must not reconnect-loop. if (params.rfType === 'tcp' && meshcoreDeferredReconnectRef.current) { meshcoreDeferredReconnectRef.current = false; + setMeshcoreTcpSoftApDeadAccepted(true); console.debug( '[useMeshcoreRuntime] TCP burst-complete reconnect attach — accepting dead bridge (configured)', ); @@ -3306,7 +3326,10 @@ export function useMeshcoreRuntime() { serialNeedsReselect: false, connectionLoss: false, })); - requestChatOutboxDrain('meshcore'); + // SoftAP dead bridge: outbox drain would tcp-write-fail → reconnect thrash. + if (!(params.rfType === 'tcp' && meshcoreTcpBridgeDeadRef.current)) { + requestChatOutboxDrain('meshcore'); + } }; const reconnectWork = runReconnectAttempt(); void reconnectWork.catch((e: unknown) => { @@ -3508,6 +3531,26 @@ export function useMeshcoreRuntime() { handleMeshcoreConnectionLostRef.current = handleMeshcoreConnectionLost; + const ensureTcpLiveForUserTx = useCallback(async (): Promise => { + const bridgeDead = meshcoreTcpBridgeDeadRef.current; + const softAp = isMeshcoreTcpSoftApDeadAccepted(); + if (!softAp && !bridgeDead) { + return; + } + // Already opening — just wait for the post-getSelfInfo gate. + if ( + meshcoreConnectTypeRef.current === 'tcp' && + (meshcoreInitConnInFlightRef.current || meshcoreIsReconnectingRef.current) && + !meshcoreTcpBridgeDeadRef.current + ) { + await waitForMeshcoreTcpLiveForUserTx(); + return; + } + setMeshcoreTcpSoftApDeadAccepted(false); + handleMeshcoreConnectionLostRef.current(); + await waitForMeshcoreTcpLiveForUserTx(); + }, []); + const onPowerSuspend = useCallback(() => { meshcoreReconnectGenerationRef.current += 1; meshcoreIsReconnectingRef.current = false; @@ -3635,10 +3678,11 @@ export function useMeshcoreRuntime() { meshcoreEverConfiguredRef.current = true; // Neal OpenHop: peer FIN after contacts — initConn completed configured from the burst // with a dead bridge. Do not force an immediate live-socket reconnect (companions that - // FIN after every contacts dump would loop forever). Write-fail / later IPC close - // reconnect once deviceConfigured is latched. + // FIN after every contacts dump would loop forever). SoftAP-accepted suppresses + // background write-dead → reconnect (flood advert / outbox thrash). if (type === 'tcp' && meshcoreDeferredReconnectRef.current) { meshcoreDeferredReconnectRef.current = false; + setMeshcoreTcpSoftApDeadAccepted(true); console.debug( '[useMeshcoreRuntime] TCP burst-complete configure — accepting dead bridge', ); @@ -7420,6 +7464,7 @@ export function useMeshcoreRuntime() { meshcoreTcpBridgeDeadRef.current = true; // Post-configure contacts dump: keep the configured session; do not reconnect-loop. if (meshcoreTcpContactsDumpInFlightRef.current) { + setMeshcoreTcpSoftApDeadAccepted(true); console.debug( source === 'write' ? '[useMeshcoreRuntime] TCP write-dead during post-configure contacts dump — keep configured' @@ -7435,8 +7480,16 @@ export function useMeshcoreRuntime() { deviceConfigured: meshcoreDeviceConfiguredRef.current, initConnInFlight: meshcoreInitConnInFlightRef.current, }); + // SoftAP-accepted dead bridge: flood advert / outbox write-fail must not reconnect-loop. + const softApAccepted = isMeshcoreTcpSoftApDeadAccepted(); + const suppressWriteLost = softApAccepted && source === 'write'; if (defer) { meshcoreDeferredReconnectRef.current = true; + // Latch SoftAP-accepted as soon as configured+deferred so flood advert cannot + // write-dead→lost in the gap before connect() clears deferredReconnect. + if (meshcoreDeviceConfiguredRef.current && meshcoreEverConfiguredRef.current) { + setMeshcoreTcpSoftApDeadAccepted(true); + } console.debug( source === 'write' ? '[useMeshcoreRuntime] TCP write-dead after init burst — latch bridge dead, defer reconnect' @@ -7444,11 +7497,15 @@ export function useMeshcoreRuntime() { ); return; } - if (source === 'ipc') { - handleMeshcoreConnectionLostRef.current(); + if (suppressWriteLost) { + console.debug( + '[useMeshcoreRuntime] TCP write-dead on SoftAP-accepted dead bridge — keep configured', + ); + return; } - // write-dead while fully configured: leave reconnect to meshcore:tcp-disconnected IPC - // so we do not double-enter the owner. + // Mid-session TCP death (not SoftAP-accepted): ipc/write own recovery. SoftAP accept + // intentionally leaves a dead bridge — do not immediate-reconnect on accept. + handleMeshcoreConnectionLostRef.current(); }; setMeshcoreTcpWriteDeadListener(() => { @@ -7472,6 +7529,7 @@ export function useMeshcoreRuntime() { finalizeDriverDisconnect, connectAutomatic, getDestinationPubKey: (nodeId) => pubKeyMapRef.current.get(nodeId), + ensureTcpLiveForUserTx, }); return () => registerMeshcoreSession(null); }, [ @@ -7481,6 +7539,7 @@ export function useMeshcoreRuntime() { handleRfConnectFailure, finalizeDriverDisconnect, connectAutomatic, + ensureTcpLiveForUserTx, ]); return useMemo( diff --git a/src/renderer/runtime/useMeshtasticRuntime.ts b/src/renderer/runtime/useMeshtasticRuntime.ts index f34d4b1f4..a7e21773f 100644 --- a/src/renderer/runtime/useMeshtasticRuntime.ts +++ b/src/renderer/runtime/useMeshtasticRuntime.ts @@ -2073,7 +2073,9 @@ export function useMeshtasticRuntime() { return; } scheduleMeshtasticReconnectAttemptRef.current(); - })(); + })().catch((e: unknown) => { + console.warn('[useMeshtasticRuntime] handleConnectionLost async ' + errLikeToLogString(e)); + }); }, [ clearConfigureTimeout, cleanupSubscriptions, diff --git a/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts b/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts index c40d54069..e249cbe66 100644 --- a/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts +++ b/src/renderer/runtime/useReticulumRuntime.reconnect-hardening.test.ts @@ -28,6 +28,20 @@ describe('useReticulumRuntime reconnect hardening (regression)', () => { ); }); + it('re-runs connect after coalescing when reuseIfRunning is false', () => { + const connectBody = extractUseCallbackBody(SOURCE, 'connect'); + expect(connectBody).toContain('opts?.reuseIfRunning !== false'); + const awaitPendingIdx = connectBody.indexOf('await pending.catch'); + const coalescedReturnIdx = connectBody.indexOf( + 'if (opts?.reuseIfRunning !== false)', + awaitPendingIdx, + ); + expect(awaitPendingIdx).toBeGreaterThan(-1); + expect(coalescedReturnIdx).toBeGreaterThan(awaitPendingIdx); + // Fresh-start falls through into a new flight rather than returning early. + expect(connectBody.slice(coalescedReturnIdx, coalescedReturnIdx + 120)).toContain('return;'); + }); + it('restartStack awaits in-flight connect before restarting', () => { expect(SOURCE).toMatch( /const restartStack = useCallback\(async \(\) => \{[\s\S]*?if \(connectInFlightRef\.current\) \{[\s\S]*?await pending/, @@ -122,7 +136,7 @@ describe('useReticulumRuntime resume-generation cancel (H7)', () => { it('connect captures the resume generation before starting the async flight', () => { const connectBody = extractUseCallbackBody(SOURCE, 'connect'); expect(connectBody).toMatch( - /connectInFlightRef\.current = true;\s*const generation = resumeGenerationRef\.current;\s*const flight = \(async \(\) => \{/, + /connectInFlightRef\.current = true;\s*const generation = resumeGenerationRef\.current;\s*const reuseIfRunning = opts\?\.reuseIfRunning \?\? true;\s*const flight = \(async \(\) => \{/, ); }); @@ -152,6 +166,37 @@ describe('useReticulumRuntime resume-generation cancel (H7)', () => { expect(resumeBody).toContain('suppressReconnectRef.current'); expect(resumeBody).not.toContain('resumeGenerationRef'); }); + + it('onPowerSuspend stops the sidecar when an enabled BLE RNode is configured', () => { + expect(SOURCE).toMatch(/powerSuspendHadBleRnodeRef/); + expect(SOURCE).toMatch(/isReticulumBleRnodeInterfaceRow\(row\)/); + const suspendRe = /const onPowerSuspend = useCallback\([\s\S]*?\}, \[\]\);/; + const suspendBody = suspendRe.exec(SOURCE)?.[0]; + expect(suspendBody).toBeDefined(); + expect(suspendBody).toContain('electronAPI.reticulum.stop()'); + expect(suspendBody).toContain('powerSuspendHadBleRnodeRef.current = hadBleRnode'); + }); + + it('onPowerResume forces reuseIfRunning false after BLE RNode suspend', () => { + const resumeRe = /const onPowerResume = useCallback\([\s\S]*?\}, \[connect\]\);/; + const resumeBody = resumeRe.exec(SOURCE)?.[0]; + expect(resumeBody).toBeDefined(); + expect(resumeBody).toContain('reuseIfRunning: !forceFresh'); + expect(resumeBody).toContain('powerSuspendHadBleRnodeRef.current'); + }); + + it('latches bleBondRemoved to release Noble and set bond-desync sticky flag', () => { + expect(SOURCE).toMatch(/setReticulumBleBondDesyncActive\(true\)/); + expect(SOURCE).toMatch(/releaseReticulumBleRnodeConnect\(\)/); + expect(SOURCE).toMatch(/status\.interfaceIssueAlert\?\.bleBondRemoved/); + }); + + it('wires LXMF send rekey with replacesMessageHash for pending orphan cleanup', () => { + expect(SOURCE).toMatch(/shouldDeletePriorReticulumOutboundHash\(pendingId, hash\)/); + expect(SOURCE).toMatch( + /replacesMessageHash[\s\S]*?ingestReticulumLxmfPayloadWithSideEffects\([\s\S]*?replacesMessageHash/, + ); + }); }); describe('useReticulumRuntime RMAP discovery map', () => { diff --git a/src/renderer/runtime/useReticulumRuntime.ts b/src/renderer/runtime/useReticulumRuntime.ts index 6197586c7..334291c20 100644 --- a/src/renderer/runtime/useReticulumRuntime.ts +++ b/src/renderer/runtime/useReticulumRuntime.ts @@ -50,6 +50,9 @@ import { recordReticulumPeerInterfaceSamplesFromPeersUpdated, } from '@/renderer/lib/reticulum/reticulumAnnounceIfaceAttribution'; import { cacheReticulumInboundAttachment } from '@/renderer/lib/reticulum/reticulumAttachmentCache'; +import { isReticulumBleRnodeInterfaceRow } from '@/renderer/lib/reticulum/reticulumBleAdapterConflict'; +import { releaseReticulumBleRnodeConnect } from '@/renderer/lib/reticulum/reticulumBleAdapterLease'; +import { setReticulumBleBondDesyncActive } from '@/renderer/lib/reticulum/reticulumBleBondDesync'; import { fetchReticulumConfigAudit } from '@/renderer/lib/reticulum/reticulumConfigAudit'; import { maybeNotifyInboundGamesChallenge } from '@/renderer/lib/reticulum/reticulumGamesNotifications'; import { refreshGamesSessions } from '@/renderer/lib/reticulum/reticulumGamesSession'; @@ -265,11 +268,13 @@ export function useReticulumRuntime(): ProtocolRuntime { ); const unsubEventRef = useRef<(() => void) | null>(null); const unsubVoiceAudioRef = useRef<(() => void) | null>(null); - const connectRef = useRef<(() => Promise) | null>(null); + const connectRef = useRef<((opts?: { reuseIfRunning?: boolean }) => Promise) | null>(null); const restartStackRef = useRef<(() => Promise) | null>(null); const connectInFlightRef = useRef(false); const connectInFlightDoneRef = useRef | null>(null); const suppressReconnectRef = useRef(false); + /** Set on power-suspend when an enabled BLE RNode was configured — wake must not reuseIfRunning. */ + const powerSuspendHadBleRnodeRef = useRef(false); /** * Bumped on every power-suspend so a `connect()` flight started before an earlier suspend * (and still in flight when a *later* suspend/resume pair fires) can detect it has been @@ -1426,6 +1431,7 @@ export function useReticulumRuntime(): ProtocolRuntime { setRawPackets([]); clearReticulumSessionStores(); processedLinkTimeoutDestsRef.current.clear(); + setReticulumBleBondDesyncActive(false); setReticulumAnnounceBusPressureActive(false); setState(INITIAL_STATE); syncConnectionStore(INITIAL_STATE); @@ -1433,6 +1439,16 @@ export function useReticulumRuntime(): ProtocolRuntime { useEffect(() => { const unsubStatus = window.electronAPI.reticulum.onStatus((status) => { + const bondRemoved = status.interfaceIssueAlert?.bleBondRemoved ?? []; + // Sticky: set true when latched; clear only on sidecar stop / tearDown (not empty alert). + if (bondRemoved.length > 0) { + setReticulumBleBondDesyncActive(true); + void releaseReticulumBleRnodeConnect().catch((e: unknown) => { + console.debug( + '[useReticulumRuntime] release Noble after bleBondRemoved ' + errLikeToLogString(e), + ); + }); + } if (status.interfaceIssueAlert || status.autoBeaconAlert || status.healthy === false) { void syncDiagnosticsFromSidecar(); const timeouts = status.interfaceIssueAlert?.linkDeliveryTimeouts; @@ -1503,97 +1519,106 @@ export function useReticulumRuntime(): ProtocolRuntime { }; }, []); - const connect = useCallback(async () => { - if (connectInFlightRef.current) { - const pending = connectInFlightDoneRef.current; - if (pending) { - await pending.catch((e: unknown) => { + const connect = useCallback( + async (opts?: { reuseIfRunning?: boolean }) => { + if (connectInFlightRef.current) { + const pending = connectInFlightDoneRef.current; + if (pending) { + await pending.catch((e: unknown) => { + console.debug( + '[useReticulumRuntime] coalesced connect waited on failed in-flight attempt ' + + errLikeToLogString(e), + ); + }); + // Fresh-start callers (e.g. BLE RNode power-resume) must not reuse the settled flight — + // fall through so this invocation starts a new connect (in-flight flag cleared in finally). + if (opts?.reuseIfRunning !== false) { + return; + } + } else { + throw new Error('Reticulum connect already in progress'); + } + } + // Defense in depth: AutostartCoordinator (or any stale caller) must not defeat Stop. + // Intentional Start clears this via notifyManualStackStart / clear helpers first. + if (isReticulumManualStackStopSuppress()) { + console.debug('[useReticulumRuntime] connect skipped — manual stack stop suppress'); + return; + } + // Intentional Start / reconnect clears sticky user-disconnect suppress. + suppressReconnectRef.current = false; + connectInFlightRef.current = true; + const generation = resumeGenerationRef.current; + const reuseIfRunning = opts?.reuseIfRunning ?? true; + const flight = (async () => { + setState((s) => ({ ...s, status: 'connecting', connectionType: null })); + syncConnectionStore({ status: 'connecting', connectionType: null }); + await window.electronAPI.reticulum.start({ reuseIfRunning }); + subscribeSidecarEventBridges(); + const lxmfHash = await refreshIdentityFromSidecar(); + const connectedNodeId = lxmfHash ? reticulumHashToNodeId(lxmfHash) : 0; + if (connectedNodeId > 0) { + syncConnectionStore({ myNodeNum: connectedNodeId }); + } + await refreshContactsFromSidecar(); + await refreshLocalInterfacesFromSidecar(); + await syncDiagnosticsFromSidecar(); + await hydrateRawPackets(); + if (identityId) { + await markStaleReticulumOutboundMessages(identityId, RETICULUM_STALE_OUTBOUND_MS); + markStaleReticulumOutboundInStore(identityId, RETICULUM_STALE_OUTBOUND_MS); + // Merge — do not wipe live WS ingest whose fire-and-forget DB persist is still in flight. + await loadMessagesFromDb('merge'); + } + // Catch up any inbound LXMF that arrived while WS was lagging or before subscribe. + await catchUpRecentInboundLxmf({ reason: 'connect' }); + if (resumeGenerationRef.current !== generation) { + // A later power-suspend fired while this connect attempt was still in flight — the + // sidecar keeps running (no RF link to go stale), but a fresher resume/suspend cycle + // now owns the UI state; applying this stale "configured" result could clobber it. console.debug( - '[useReticulumRuntime] coalesced connect waited on failed in-flight attempt ' + - errLikeToLogString(e), + '[useReticulumRuntime] connect superseded by newer power-suspend generation — skip applying stale configured state', ); + return; + } + setState({ status: 'configured', myNodeNum: connectedNodeId, connectionType: null }); + syncConnectionStore({ + status: 'configured', + connectionType: null, + myNodeNum: connectedNodeId, }); - return; - } - throw new Error('Reticulum connect already in progress'); - } - // Defense in depth: AutostartCoordinator (or any stale caller) must not defeat Stop. - // Intentional Start clears this via notifyManualStackStart / clear helpers first. - if (isReticulumManualStackStopSuppress()) { - console.debug('[useReticulumRuntime] connect skipped — manual stack stop suppress'); - return; - } - // Intentional Start / reconnect clears sticky user-disconnect suppress. - suppressReconnectRef.current = false; - connectInFlightRef.current = true; - const generation = resumeGenerationRef.current; - const flight = (async () => { - setState((s) => ({ ...s, status: 'connecting', connectionType: null })); - syncConnectionStore({ status: 'connecting', connectionType: null }); - await window.electronAPI.reticulum.start({ reuseIfRunning: true }); - subscribeSidecarEventBridges(); - const lxmfHash = await refreshIdentityFromSidecar(); - const connectedNodeId = lxmfHash ? reticulumHashToNodeId(lxmfHash) : 0; - if (connectedNodeId > 0) { - syncConnectionStore({ myNodeNum: connectedNodeId }); - } - await refreshContactsFromSidecar(); - await refreshLocalInterfacesFromSidecar(); - await syncDiagnosticsFromSidecar(); - await hydrateRawPackets(); - if (identityId) { - await markStaleReticulumOutboundMessages(identityId, RETICULUM_STALE_OUTBOUND_MS); - markStaleReticulumOutboundInStore(identityId, RETICULUM_STALE_OUTBOUND_MS); - // Merge — do not wipe live WS ingest whose fire-and-forget DB persist is still in flight. - await loadMessagesFromDb('merge'); - } - // Catch up any inbound LXMF that arrived while WS was lagging or before subscribe. - await catchUpRecentInboundLxmf({ reason: 'connect' }); - if (resumeGenerationRef.current !== generation) { - // A later power-suspend fired while this connect attempt was still in flight — the - // sidecar keeps running (no RF link to go stale), but a fresher resume/suspend cycle - // now owns the UI state; applying this stale "configured" result could clobber it. - console.debug( - '[useReticulumRuntime] connect superseded by newer power-suspend generation — skip applying stale configured state', - ); - return; + scheduleLocalInterfaceStatusBurst(); + void reconcileRncpListenerFromSidecar().catch((e: unknown) => { + console.debug('[useReticulumRuntime] rncp reconcile ' + errLikeToLogString(e)); + }); + })(); + connectInFlightDoneRef.current = flight; + try { + await flight; + } catch (e) { + console.error('[useReticulumRuntime] connect failed ' + errLikeToLogString(e)); + setState(INITIAL_STATE); + syncConnectionStore(INITIAL_STATE); + throw e instanceof Error ? e : new Error(String(e)); + } finally { + connectInFlightRef.current = false; + connectInFlightDoneRef.current = null; } - setState({ status: 'configured', myNodeNum: connectedNodeId, connectionType: null }); - syncConnectionStore({ - status: 'configured', - connectionType: null, - myNodeNum: connectedNodeId, - }); - scheduleLocalInterfaceStatusBurst(); - void reconcileRncpListenerFromSidecar().catch((e: unknown) => { - console.debug('[useReticulumRuntime] rncp reconcile ' + errLikeToLogString(e)); - }); - })(); - connectInFlightDoneRef.current = flight; - try { - await flight; - } catch (e) { - console.error('[useReticulumRuntime] connect failed ' + errLikeToLogString(e)); - setState(INITIAL_STATE); - syncConnectionStore(INITIAL_STATE); - throw e instanceof Error ? e : new Error(String(e)); - } finally { - connectInFlightRef.current = false; - connectInFlightDoneRef.current = null; - } - }, [ - subscribeSidecarEventBridges, - refreshContactsFromSidecar, - refreshIdentityFromSidecar, - refreshLocalInterfacesFromSidecar, - loadMessagesFromDb, - syncDiagnosticsFromSidecar, - hydrateRawPackets, - catchUpRecentInboundLxmf, - identityId, - syncConnectionStore, - scheduleLocalInterfaceStatusBurst, - ]); + }, + [ + subscribeSidecarEventBridges, + refreshContactsFromSidecar, + refreshIdentityFromSidecar, + refreshLocalInterfacesFromSidecar, + loadMessagesFromDb, + syncDiagnosticsFromSidecar, + hydrateRawPackets, + catchUpRecentInboundLxmf, + identityId, + syncConnectionStore, + scheduleLocalInterfaceStatusBurst, + ], + ); const disconnect = useCallback(async () => { suppressReconnectRef.current = true; @@ -1622,6 +1647,7 @@ export function useReticulumRuntime(): ProtocolRuntime { setRawPackets([]); clearReticulumSessionStores(); processedLinkTimeoutDestsRef.current.clear(); + setReticulumBleBondDesyncActive(false); setReticulumAnnounceBusPressureActive(false); setState(INITIAL_STATE); syncConnectionStore(INITIAL_STATE); @@ -2082,6 +2108,18 @@ export function useReticulumRuntime(): ProtocolRuntime { // Pause shell auto-reconnect / transfer retry storms while the machine sleeps. useRnshSessionStore.getState().clearAll(); useRncpTransferStore.getState().clearAll(); + const hadBleRnode = localInterfacesRef.current.some( + (row) => row.enabled && isReticulumBleRnodeInterfaceRow(row), + ); + powerSuspendHadBleRnodeRef.current = hadBleRnode; + if (!hadBleRnode) { + return; + } + // Stop the sidecar so CoreBluetooth/btleplug does not keep a zombie BLE RNode session + // across sleep (macOS tears GATT without a cooperative detach). + void window.electronAPI.reticulum.stop().catch((e: unknown) => { + console.debug('[useReticulumRuntime] power suspend stop ' + errLikeToLogString(e)); + }); }, []); const onPowerResume = useCallback(() => { @@ -2089,7 +2127,9 @@ export function useReticulumRuntime(): ProtocolRuntime { console.debug('[useReticulumRuntime] power resume — skip reconnect (user disconnect)'); return; } - void connect().catch((e: unknown) => { + const forceFresh = powerSuspendHadBleRnodeRef.current; + powerSuspendHadBleRnodeRef.current = false; + void connect({ reuseIfRunning: !forceFresh }).catch((e: unknown) => { console.warn('[useReticulumRuntime] power resume reconnect failed ' + errLikeToLogString(e)); }); }, [connect]);