diff --git a/.agent_memory/session_context.archive.md b/.agent_memory/session_context.archive.md index 90cf98f4c1..bda6af55ef 100644 --- a/.agent_memory/session_context.archive.md +++ b/.agent_memory/session_context.archive.md @@ -2,6 +2,13 @@ # Older handover entries rotated out of session_context.md. Not read by default. # Consult only if you need historical detail on a specific past change. +## 2026-05-21 — Fixed Chirpy Assistant invalid model name and enhanced failure fallback suggestions +- Fixed a 404/Unknown inference error by updating `GeminiNanoDocAssistant.kt`'s `MODEL_NAME` from `"gemini-3.1-flash-lite"` to the correct Firebase AI Logic preview name `"gemini-3.1-flash-lite-preview"`. +- Overhauled multi-turn hybrid chat seeding: eliminated the redundant background `chat.sendMessage` call on the first turn; if the first turn is answered on-device, the session caches the Q&A locally and seeds the subsequent cloud-chat session via `startChat(history = ...)`. +- Expanded the hybrid model's `looksLikeNoAnswer` heuristics to better detect on-device failure and fall back to the grounded cloud model. +- Programmed a smart UI fallback: on inference error (offline, rate limit, model not found), Chirpy displays local keyword search results as recommended page chips. +- Verified 100% compliance with Spotless, Detekt, and unit tests (`:feature:docs:allTests` and `:androidApp:testGoogleDebugUnitTest`). + ## 2026-05-20 — Decoupled and Isolated Flatpak manifest generation logic to build-logic/flatpak - Isolated the optimized `GenerateFlatpakSourcesTask` from monolithic `build-logic/convention` into its own specialized, lightweight `:flatpak` subproject under `build-logic`. - Created `:flatpak` configuration and registered the formal plugin ID `"meshtastic.flatpak"` implemented by `FlatpakConventionPlugin` inside the default package namespace. diff --git a/.agent_memory/session_context.md b/.agent_memory/session_context.md index 912c57e4e6..b558448f0f 100644 --- a/.agent_memory/session_context.md +++ b/.agent_memory/session_context.md @@ -6,6 +6,12 @@ # the oldest entries to `session_context.archive.md` (not read by default). The # "Golden Context" block at the bottom is stable across sessions; keep it here. +## 2026-06-03 — Cluster-marker FATAL: revert shipped map series + in-scope rememberComposeBitmapDescriptor fix +- Reverted ALL google-flavor map changes to before #5684 (per user): restored MapView.kt, NodeClusterMarkers.kt, WaypointMarkers.kt, InlineMap.kt to parent commit bc9f1637; deleted MarkerBitmapRenderer.kt; re-pinned `play-services-maps = 20.0.0` in libs.versions.toml. The shipped #5702–#5719 series (Canvas markers + ViewTree-owner band-aids) had lost the info-window popups + interactions. +- Root cause (verified against maps-compose 8.3.0 + android-maps-utils 4.1.1 SOURCE in gradle cache): ONLY `Clustering(clusterItemContent=…)` crashes — its `ComposeUiClusterRenderer` builds a *detached* `InvalidatingComposeView` with a fake lifecycle owner and NO SavedStateRegistryOwner. `MarkerComposable` already bakes its icon via the safe in-scope `rememberComposeBitmapDescriptor`; info windows render with the live marker compositionContext. So InlineMap/NodeTrack/Traceroute were left untouched. +- Fix (NodeClusterMarkers.kt ONLY): icons baked in-scope via `rememberComposeBitmapDescriptor(node){ PulsingNodeChip }` into a snapshot stateMap; custom `private class NodeClusterRenderer : DefaultClusterRenderer` assigns them in onBeforeClusterItemRendered/onClusterItemUpdated (bg thread, READ-only — never composes, so the crash class is gone). Native info windows (super sets title/snippet) + onClusterItemInfoWindowClick→navigateToNodeDetails; precision circles drawn from the renderer's own `unclusteredItems` MutableState (clusterItemDecoration can't fire — `ClusterRendererItemState` is lib-internal). Strictly better than the elegant-euler Canvas branch — keeps the REAL Compose chip. +- `compileGoogleDebugKotlin` + `spotlessCheck` + `detekt` PASS. NOT committed, NOT device-verified. Next: device-test (clusters show chips + info-window popups + no FATAL), then commit/push. + ## 2026-05-28 — Stabilized DatabaseManager withDb retry host test - Hardened `DatabaseManagerWithDbRetryTest` to remove CI race conditions by running the manager on a `StandardTestDispatcher(testScheduler)` instead of real `Dispatchers.IO`. - Added a `withTimeout(10_000)` guard around the test body to fail fast on coordination stalls instead of hanging/flapping. @@ -33,13 +39,6 @@ - Refactored `GeminiNanoDocAssistant.answer` to reuse `answerStream` flow under the hood, eliminating duplicate prompting code. - Verified that all unit tests (`:feature:docs:allTests`) and static analysis checks (`spotlessApply spotlessCheck detekt`) pass 100% green. -## 2026-05-21 — Fixed Chirpy Assistant invalid model name and enhanced failure fallback suggestions -- Fixed a 404/Unknown inference error by updating `GeminiNanoDocAssistant.kt`'s `MODEL_NAME` from `"gemini-3.1-flash-lite"` to the correct Firebase AI Logic preview name `"gemini-3.1-flash-lite-preview"`. -- Overhauled multi-turn hybrid chat seeding: eliminated the redundant background `chat.sendMessage` call on the first turn; if the first turn is answered on-device, the session caches the Q&A locally and seeds the subsequent cloud-chat session via `startChat(history = ...)`. -- Expanded the hybrid model's `looksLikeNoAnswer` heuristics to better detect on-device failure and fall back to the grounded cloud model. -- Programmed a smart UI fallback: on inference error (offline, rate limit, model not found), Chirpy displays local keyword search results as recommended page chips. -- Verified 100% compliance with Spotless, Detekt, and unit tests (`:feature:docs:allTests` and `:androidApp:testGoogleDebugUnitTest`). - ## Golden Context (stable across sessions) - Always check `.skills/compose-ui/strings-index.txt` before reading `strings.xml`. - Run `python3 scripts/sort-strings.py` after adding strings to keep the index organized. diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c4f0924b..ae536f65dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,9 @@ See [GitHub Releases](https://github.com/meshtastic/Meshtastic-Android/releases) ### Unreleased (not yet in any build) -* chore(agents): prune governance cruft and add Claude token guards (#5706) by James Rich (@jamesarich) in [`813acee71`](https://github.com/meshtastic/Meshtastic-Android/commit/813acee71604686dc4c98b43cf692fe4586cb5c5) -* fix(map): scope cluster-renderer ViewTreeLifecycleOwner to map host view (#5708) by James Rich (@jamesarich) in [`1b661739e`](https://github.com/meshtastic/Meshtastic-Android/commit/1b661739e346f6d64ff06731ee2ac0bc80501d19) +* fix(map): apply kotlinx-serialization compiler plugin to androidApp (#5726) by James Rich (@jamesarich) in [`0e4f12ec0`](https://github.com/meshtastic/Meshtastic-Android/commit/0e4f12ec0dbf2d303d09574698e3ef3c73341cf6) -### Open Beta (v2.7.14-open.18) +### Open Beta (v2.7.14-open.21) Changes since [`v2.7.13`](https://github.com/meshtastic/Meshtastic-Android/releases/tag/v2.7.13): #### 🏗️ Features @@ -239,6 +238,11 @@ Changes since [`v2.7.13`](https://github.com/meshtastic/Meshtastic-Android/relea * fix(firmware): surface error state when BLE OTA connection attempts are exhausted by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5700 * fix(map): replace MarkerComposable with Canvas-rendered bitmaps by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5702 * fix(map): remove manual ViewTree lifecycle owner workarounds by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5704 +* fix(map): scope cluster-renderer ViewTreeLifecycleOwner to map host view by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5708 +* fix(map): initialize Maps SDK before building marker bitmap descriptors by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5709 +* fix(map): eliminate cluster-renderer FATAL and harden black-map paths by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5715 +* fix(map): revert app-side Maps SDK init to library-idiomatic, fix inline-map crash by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5719 +* fix(map): render cluster markers in-scope to kill ClusterRenderer FATAL by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/5723 #### 📝 Other Changes * refactor(ui): compose resources, domain layer by @jamesarich in https://github.com/meshtastic/Meshtastic-Android/pull/4628 * Add per-message transport method icons for new message format by @Kealper in https://github.com/meshtastic/Meshtastic-Android/pull/4643 diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 480989d8a1..bc3697bf8e 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -26,6 +26,7 @@ plugins { alias(libs.plugins.meshtastic.android.application) alias(libs.plugins.meshtastic.android.application.flavors) alias(libs.plugins.meshtastic.android.application.compose) + alias(libs.plugins.meshtastic.kotlinx.serialization) id("meshtastic.koin") alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.secrets) diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt index 40f2756d9d..6ca71eb6a5 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -78,7 +78,7 @@ import com.google.maps.android.compose.MapProperties import com.google.maps.android.compose.MapType import com.google.maps.android.compose.MapUiSettings import com.google.maps.android.compose.MapsComposeExperimentalApi -import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerComposable import com.google.maps.android.compose.MarkerInfoWindowComposable import com.google.maps.android.compose.Polyline import com.google.maps.android.compose.TileOverlay @@ -102,7 +102,6 @@ import org.meshtastic.app.map.component.MapTypeDropdown import org.meshtastic.app.map.component.NodeClusterMarkers import org.meshtastic.app.map.component.NodeMapFilterDropdown import org.meshtastic.app.map.component.WaypointMarkers -import org.meshtastic.app.map.component.rememberNodeChipDescriptor import org.meshtastic.app.map.model.NodeClusterItem import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds @@ -126,6 +125,7 @@ import org.meshtastic.core.resources.sats import org.meshtastic.core.resources.speed import org.meshtastic.core.resources.timestamp import org.meshtastic.core.resources.track_point +import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.icon.Layers import org.meshtastic.core.ui.icon.Map import org.meshtastic.core.ui.icon.MeshtasticIcons @@ -864,17 +864,17 @@ private fun NodeTrackOverlay( } if (index == sortedPositions.lastIndex) { - val chipIcon = rememberNodeChipDescriptor(focusedNode) - Marker( + MarkerComposable( state = markerState, - icon = chipIcon, zIndex = activeNodeZIndex, alpha = if (isHighPriority) 1.0f else 0.9f, onClick = { onPositionSelected?.invoke(position.time) false // Allow default info window behavior }, - ) + ) { + NodeChip(node = focusedNode) + } } else { MarkerInfoWindowComposable( state = markerState, @@ -969,6 +969,7 @@ private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): S // region --- Traceroute Map Content --- +@OptIn(MapsComposeExperimentalApi::class) @Composable private fun TracerouteMapContent( forwardOffsetPoints: List, @@ -997,8 +998,7 @@ private fun TracerouteMapContent( } displayNodes.forEach { node -> val markerState = rememberUpdatedMarkerState(position = node.position.toLatLng()) - val chipIcon = rememberNodeChipDescriptor(node) - Marker(state = markerState, icon = chipIcon, zIndex = 4f) + MarkerComposable(state = markerState, zIndex = 4f) { NodeChip(node = node) } } } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt deleted file mode 100644 index 9b0c161eb8..0000000000 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MarkerBitmapRenderer.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.app.map.component - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.RectF -import android.graphics.Typeface -import android.text.TextPaint -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.core.graphics.createBitmap -import com.google.android.gms.maps.MapsInitializer -import com.google.android.gms.maps.model.BitmapDescriptor -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import org.meshtastic.core.model.Node - -private const val CHIP_CORNER_RADIUS_DP = 4f -private const val CHIP_PADDING_HORIZONTAL_DP = 8f -private const val CHIP_MIN_WIDTH_DP = 64f -private const val CHIP_MIN_HEIGHT_DP = 28f -private const val CHIP_TEXT_SIZE_SP = 14f -private const val EMOJI_TEXT_SIZE_SP = 32f -private const val EMOJI_PADDING_DP = 2f - -/** - * Renders a node chip marker as a [BitmapDescriptor] using Canvas — avoids the off-screen ComposeView pipeline in - * maps-compose's `MarkerComposable`/`rememberComposeBitmapDescriptor` which can crash with "The ComposeView was - * measured to have a width or height of zero" during subcomposition races (googlemaps/android-maps-compose#875). - */ -@Composable -fun rememberNodeChipDescriptor(node: Node): BitmapDescriptor { - val context = LocalContext.current - val density = LocalDensity.current.density - val fontScale = LocalDensity.current.fontScale - return remember(node.num, node.user.short_name, node.colors, node.isIgnored) { - ensureMapsInitialized(context) - renderNodeChipBitmap(node, density, fontScale) - } -} - -/** Renders an emoji waypoint marker as a [BitmapDescriptor] using Canvas. */ -@Composable -fun rememberEmojiMarkerDescriptor(codePoint: Int): BitmapDescriptor { - val context = LocalContext.current - val density = LocalDensity.current.density - val fontScale = LocalDensity.current.fontScale - return remember(codePoint) { - ensureMapsInitialized(context) - renderEmojiBitmap(codePoint, density, fontScale) - } -} - -/** - * [BitmapDescriptorFactory] only works after the Maps SDK has been initialized, which normally happens when a - * GoogleMap/MapView is created. These descriptors are built during composition, and on the node-detail inline map the - * icon is computed before that screen's GoogleMap has loaded the SDK — so [BitmapDescriptorFactory.fromBitmap] crashes - * with "IBitmapDescriptorFactory is not initialized". Initialize explicitly first; [MapsInitializer.initialize] is - * synchronous and idempotent, so it is a no-op once the SDK is already up. - */ -@Suppress("DEPRECATION") -private fun ensureMapsInitialized(context: Context) { - MapsInitializer.initialize(context) -} - -private fun renderNodeChipBitmap(node: Node, density: Float, fontScale: Float): BitmapDescriptor { - val (textColorInt, nodeColorInt) = node.colors - val scaledDensity = density * fontScale - - val textPaint = - TextPaint(Paint.ANTI_ALIAS_FLAG).apply { - textSize = CHIP_TEXT_SIZE_SP * scaledDensity - typeface = Typeface.DEFAULT_BOLD - color = textColorInt - textAlign = Paint.Align.CENTER - isStrikeThruText = node.isIgnored - } - val label = node.user.short_name.ifEmpty { "???" } - - val textWidth = textPaint.measureText(label) - val paddingH = CHIP_PADDING_HORIZONTAL_DP * density - val minWidth = CHIP_MIN_WIDTH_DP * density - val minHeight = CHIP_MIN_HEIGHT_DP * density - - val width = maxOf(minWidth, textWidth + paddingH * 2).toInt() - val height = minHeight.toInt() - - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - - val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = nodeColorInt } - val cornerRadius = CHIP_CORNER_RADIUS_DP * density - canvas.drawRoundRect(RectF(0f, 0f, width.toFloat(), height.toFloat()), cornerRadius, cornerRadius, bgPaint) - - val textX = width / 2f - val textY = (height / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f) - canvas.drawText(label, textX, textY, textPaint) - - return BitmapDescriptorFactory.fromBitmap(bitmap) -} - -private fun renderEmojiBitmap(codePoint: Int, density: Float, fontScale: Float): BitmapDescriptor { - val scaledDensity = density * fontScale - val textPaint = - TextPaint(Paint.ANTI_ALIAS_FLAG).apply { - textSize = EMOJI_TEXT_SIZE_SP * scaledDensity - textAlign = Paint.Align.CENTER - } - val emoji = String(Character.toChars(codePoint)) - val padding = EMOJI_PADDING_DP * density - - val textWidth = textPaint.measureText(emoji) - val metrics = textPaint.fontMetrics - val textHeight = metrics.descent - metrics.ascent - - val width = (textWidth + padding * 2).toInt().coerceAtLeast(1) - val height = (textHeight + padding * 2).toInt().coerceAtLeast(1) - - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - - val textX = width / 2f - val textY = padding - metrics.ascent - canvas.drawText(emoji, textX, textY, textPaint) - - return BitmapDescriptorFactory.fromBitmap(bitmap) -} diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt index 6d38e176af..0e3f872be2 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt @@ -16,28 +16,49 @@ */ package org.meshtastic.app.map.component +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.currentStateAsState -import androidx.lifecycle.findViewTreeLifecycleOwner -import androidx.lifecycle.setViewTreeLifecycleOwner -import androidx.savedstate.compose.LocalSavedStateRegistryOwner -import androidx.savedstate.findViewTreeSavedStateRegistryOwner -import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import androidx.compose.ui.platform.LocalContext +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions import com.google.maps.android.clustering.Cluster +import com.google.maps.android.clustering.ClusterManager import com.google.maps.android.clustering.view.DefaultClusterRenderer import com.google.maps.android.compose.Circle +import com.google.maps.android.compose.MapEffect import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.clustering.Clustering -import com.google.maps.android.compose.clustering.ClusteringMarkerProperties +import com.google.maps.android.compose.clustering.rememberClusterManager +import com.google.maps.android.compose.rememberComposeBitmapDescriptor import org.meshtastic.app.map.model.NodeClusterItem import org.meshtastic.feature.map.BaseMapViewModel +private const val MIN_CLUSTER_SIZE = 10 + +/** + * Renders node markers with clustering. + * + * Marker bitmaps are generated **in the maps compose scope** via [rememberComposeBitmapDescriptor], which composes each + * chip in a `ComposeView` parented to the live host view (a real, attached view that has valid `ViewTreeLifecycleOwner` + * / `SavedStateRegistryOwner`) and renders it synchronously to a [BitmapDescriptor]. + * + * This deliberately avoids the clustering library's `clusterItemContent` path: that renderer + * ([com.google.maps.android.compose.clustering.ComposeUiClusterRenderer]) composes each item in a *detached* + * `ComposeView` that only carries a fake lifecycle owner and no `SavedStateRegistryOwner`, so it crashes when the + * surrounding Navigation 3 / popup hierarchy lacks those owners (the top Crashlytics FATAL). A custom + * [DefaultClusterRenderer] subclass assigns the pre-baked bitmaps instead, keeping native info windows (title/snippet + * from [NodeClusterItem]) and click interactions intact. + */ @OptIn(MapsComposeExperimentalApi::class) @Suppress("NestedBlockDepth") @Composable @@ -47,68 +68,99 @@ fun NodeClusterMarkers( navigateToNodeDetails: (Int) -> Unit, onClusterClick: (Cluster) -> Boolean, ) { - val view = LocalView.current - val lifecycleOwner = LocalLifecycleOwner.current - val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current - val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState() + val context = LocalContext.current + val clusterManager = rememberClusterManager() - // maps-compose renders each non-clustered item to a bitmap through an off-screen ComposeView that - // it attaches under the MapView (see ComposeUiClusterRenderer + NoDrawContainerView in - // MapComposeViewRender). That ComposeView walks up the view tree for a ViewTreeLifecycleOwner and, - // when it finds none, crashes with "Composed into the View which doesn't propagate - // ViewTreeLifecycleOwner!" (googlemaps/android-maps-compose#875 / #325) — a FATAL on the map screen. - // - // Propagate the owners onto this map screen's host view (LocalView.current), which is an ancestor - // of the internally-created MapView, so the renderer's ComposeView can resolve them. We deliberately - // do NOT touch view.rootView (the activity root): attaching a transient NavEntry lifecycle there is - // what caused the node-list popup regression (#5684), which is why #5704 removed the prior, broader - // workaround entirely. Scoping to the map host view and restoring the previous owners on dispose - // keeps the fix local to the map and leaves Popups/DropdownMenus untouched. - DisposableEffect(view, lifecycleOwner, savedStateRegistryOwner) { - val prevLifecycleOwner = view.findViewTreeLifecycleOwner() - val prevSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner() - view.setViewTreeLifecycleOwner(lifecycleOwner) - view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) - onDispose { - view.setViewTreeLifecycleOwner(prevLifecycleOwner) - view.setViewTreeSavedStateRegistryOwner(prevSavedStateRegistryOwner) + // Bake each node's marker icon in-scope. Keyed by node so a bitmap is only re-rendered when that node + // actually changes. The descriptors are stashed in a snapshot map the renderer reads at render time. + val iconDescriptors = remember { mutableStateMapOf() } + nodeClusterItems.forEach { item -> + key(item.node.num) { + val descriptor = rememberComposeBitmapDescriptor(item.node) { PulsingNodeChip(node = item.node) } + DisposableEffect(descriptor) { + iconDescriptors[item.node.num] = descriptor + onDispose { iconDescriptors.remove(item.node.num) } + } } } - // The cluster renderer drives marker rendering from an async Handler (DefaultClusterRenderer's - // MarkerModifier), which can fire after this screen has stopped and the internal ComposeView is - // detached — at which point no owner is reachable regardless of the above. Skip rendering once the - // lifecycle is no longer at least STARTED to close most of that race. - if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) return + if (clusterManager != null) { + val rendererState = remember { mutableStateOf(null) } - Clustering( - items = nodeClusterItems, - onClusterClick = onClusterClick, - onClusterItemInfoWindowClick = { item -> - navigateToNodeDetails(item.node.num) - false - }, - clusterItemContent = { clusterItem -> PulsingNodeChip(node = clusterItem.node) }, - onClusterManager = { clusterManager -> - (clusterManager.renderer as DefaultClusterRenderer).minClusterSize = 10 - }, - clusterItemDecoration = { clusterItem -> - if (mapFilterState.showPrecisionCircle) { - clusterItem.getPrecisionMeters()?.let { precisionMeters -> + // The renderer needs the GoogleMap instance, only available inside the map scope. + MapEffect(clusterManager) { map -> + val renderer = NodeClusterRenderer(context, map, clusterManager) { iconDescriptors[it] } + clusterManager.renderer = renderer + rendererState.value = renderer + } + + // Keep listeners current — the lambdas can change across recompositions. + SideEffect { + clusterManager.setOnClusterClickListener { cluster -> onClusterClick(cluster) } + clusterManager.setOnClusterItemInfoWindowClickListener { item -> navigateToNodeDetails(item.node.num) } + } + + // Re-cluster once the renderer is attached and freshly-baked icons arrive, so markers pick up the bitmaps + // even when the cluster manager became available after the icons were rendered. + val renderer = rendererState.value + LaunchedEffect(renderer, iconDescriptors.size) { if (renderer != null) clusterManager.cluster() } + + Clustering(items = nodeClusterItems, clusterManager = clusterManager) + + // Precision circles for the currently-unclustered items (the renderer tracks them as the zoom changes). + if (mapFilterState.showPrecisionCircle) { + renderer?.unclusteredItems?.value?.forEach { item -> + item.getPrecisionMeters()?.let { precisionMeters -> if (precisionMeters > 0) { Circle( - center = clusterItem.position, + center = item.position, radius = precisionMeters, - fillColor = Color(clusterItem.node.colors.second).copy(alpha = 0.2f), - strokeColor = Color(clusterItem.node.colors.second), + fillColor = Color(item.node.colors.second).copy(alpha = 0.2f), + strokeColor = Color(item.node.colors.second), strokeWidth = 2f, zIndex = 0f, ) } } } - // Use the item's own priority-based zIndex (5f for My Node/Favorites, 4f for others) - ClusteringMarkerProperties(zIndex = clusterItem.getZIndex()) - }, - ) + } + } +} + +/** + * [DefaultClusterRenderer] that assigns the pre-baked [BitmapDescriptor]s (rendered in the maps compose scope) to + * non-clustered item markers, and exposes the set of currently-unclustered items so the caller can decorate them (e.g. + * precision circles). Cluster bubbles keep the library's default rendering. + */ +private class NodeClusterRenderer( + context: Context, + map: GoogleMap, + clusterManager: ClusterManager, + private val iconProvider: (Int) -> BitmapDescriptor?, +) : DefaultClusterRenderer(context, map, clusterManager) { + + val unclusteredItems = mutableStateOf>(emptySet()) + + init { + minClusterSize = MIN_CLUSTER_SIZE + } + + override fun onClustersChanged(clusters: Set>) { + super.onClustersChanged(clusters) + unclusteredItems.value = clusters.filterNot { shouldRenderAsCluster(it) }.flatMap { it.items }.toSet() + } + + override fun onBeforeClusterItemRendered(item: NodeClusterItem, markerOptions: MarkerOptions) { + // super sets title/snippet from the ClusterItem, which drives the native info window. + super.onBeforeClusterItemRendered(item, markerOptions) + iconProvider(item.node.num)?.let { markerOptions.icon(it) } + markerOptions.zIndex(item.getZIndex()) + } + + override fun onClusterItemUpdated(item: NodeClusterItem, marker: Marker) { + // super keeps title/snippet (and the open info window) in sync. + super.onClusterItemUpdated(item, marker) + iconProvider(item.node.num)?.let { marker.setIcon(it) } + marker.zIndex = item.getZIndex() + } } diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt index ed1044fc2e..227805ebdb 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt @@ -16,14 +16,22 @@ */ package org.meshtastic.app.map.component +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.MapsComposeExperimentalApi import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.rememberComposeBitmapDescriptor import com.google.maps.android.compose.rememberUpdatedMarkerState import kotlinx.coroutines.launch +import org.meshtastic.app.map.convertIntToEmoji import org.meshtastic.core.model.util.GeoConstants.DEG_D import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.locked @@ -31,6 +39,7 @@ import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.BaseMapViewModel import org.meshtastic.proto.Waypoint +@OptIn(MapsComposeExperimentalApi::class) @Composable fun WaypointMarkers( displayableWaypoints: List, @@ -56,7 +65,11 @@ fun WaypointMarkers( } val iconCodePoint = if (waypoint.icon == 0) PUSHPIN else waypoint.icon - val icon = rememberEmojiMarkerDescriptor(iconCodePoint) + val emojiText = convertIntToEmoji(iconCodePoint) + val icon = + rememberComposeBitmapDescriptor(iconCodePoint) { + Text(text = emojiText, fontSize = 32.sp, modifier = Modifier.padding(2.dp)) + } Marker( state = markerState, diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt index f77f8aec18..9930820676 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt @@ -27,15 +27,17 @@ import com.google.maps.android.compose.Circle import com.google.maps.android.compose.ComposeMapColorScheme import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.MapUiSettings -import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MapsComposeExperimentalApi +import com.google.maps.android.compose.MarkerComposable import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.rememberUpdatedMarkerState -import org.meshtastic.app.map.component.rememberNodeChipDescriptor import org.meshtastic.core.model.Node +import org.meshtastic.core.ui.component.NodeChip import org.meshtastic.core.ui.component.precisionBitsToMeters private const val DEFAULT_ZOOM = 15f +@OptIn(MapsComposeExperimentalApi::class) @Composable fun InlineMap(node: Node, modifier: Modifier = Modifier) { val dark = isSystemInDarkTheme() @@ -49,7 +51,6 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) { val cameraState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(location, DEFAULT_ZOOM) } - val markerIcon = rememberNodeChipDescriptor(node) GoogleMap( mapColorScheme = mapColorScheme, @@ -78,7 +79,7 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) { strokeWidth = 2f, ) } - Marker(state = rememberUpdatedMarkerState(position = latLng), icon = markerIcon) + MarkerComposable(state = rememberUpdatedMarkerState(position = latLng)) { NodeChip(node = node) } } } } diff --git a/androidApp/src/main/assets/firmware_releases.json b/androidApp/src/main/assets/firmware_releases.json index 543d391238..fa418c0b9c 100644 --- a/androidApp/src/main/assets/firmware_releases.json +++ b/androidApp/src/main/assets/firmware_releases.json @@ -188,6 +188,12 @@ ] }, "pullRequests": [ + { + "id": "10604", + "title": "Add XIAO ESP32C6 + Wio SX1262 variant with hardware SPI support", + "page_url": "https://github.com/meshtastic/firmware/pull/10604", + "zip_url": "https://discord.com/invite/meshtastic" + }, { "id": "10587", "title": "Add SHT2x CRC check to fix i2c scan hang", diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index c4540bb762..6b1ded4396 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit c4540bb762e5c3bdced003c5357b9739a114fc21 +Subproject commit 6b1ded439633cd03d4af85b44231b91d1d106278 diff --git a/core/resources/src/commonMain/composeResources/values-de/strings.xml b/core/resources/src/commonMain/composeResources/values-de/strings.xml index f71a790e73..0ab4b8db8d 100644 --- a/core/resources/src/commonMain/composeResources/values-de/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-de/strings.xml @@ -559,7 +559,7 @@ Verlauf Rückgabewert maximal Zeitraum Rückgabewert Anzahl der Weiterleitungen - Zwischenschritte entfernt + Sprünge entfernt Host Host Kennzahlen Feuchtigkeit @@ -843,7 +843,7 @@ Das Signal ist stark, das Signal-Rauschverhältnis ist über −7 dB und die Signalstärke ist über −115 dBm. Kombiniert SNR und RSSI zu einem Qualitätsniveau, das als farbiges Symbol mit einer Beschreibung angezeigt wird. Kein nutzbares Signal erkannt. Unterhalb aller Qualitätsgrenzen. - Hops Entfernt + Sprünge entfernt Zuletzt gesehen Umweltdaten Keine Knoten für die Vorschau vorhanden. @@ -862,7 +862,7 @@ Sortieroptionen Kanal Entfernung - Zwischenschritte entfernt + Sprünge entfernt Zuletzt gehört Sortieren nach über Favorit diff --git a/core/takserver/build.gradle.kts b/core/takserver/build.gradle.kts index a89201ec3e..bb77c08232 100644 --- a/core/takserver/build.gradle.kts +++ b/core/takserver/build.gradle.kts @@ -56,7 +56,7 @@ kotlin { jvmMain.dependencies { // Desktop JVM: standard JAR bundles native libs for desktop archs. - implementation("com.github.luben:zstd-jni:1.5.7-9") + implementation("com.github.luben:zstd-jni:1.5.7-10") // xpp3 is excluded from jvmAndroidMain (Android ships it as a // platform class), but Desktop JVM still needs it for XmlPullParser. implementation("org.ogce:xpp3:1.1.6") @@ -66,7 +66,7 @@ kotlin { // Android: @aar variant ships .so files for arm/arm64/x86/x86_64. // Without this, zstd-jni's ZstdDictCompress. throws // UnsatisfiedLinkError and poisons TakV2Compressor permanently. - implementation("com.github.luben:zstd-jni:1.5.7-9@aar") + implementation("com.github.luben:zstd-jni:1.5.7-10@aar") } commonTest.dependencies { @@ -81,7 +81,7 @@ kotlin { dependencies { // Host-JVM tests need the platform JAR (not AAR) for zstd native // libs — the @aar from androidMain only ships ARM/x86 .so files. - implementation("com.github.luben:zstd-jni:1.5.7-9") + implementation("com.github.luben:zstd-jni:1.5.7-10") } } } diff --git a/docs/ru-rRU/user/settings-radio-user.md b/docs/ru-rRU/user/settings-radio-user.md index 6f6be20de4..2171e2597f 100644 --- a/docs/ru-rRU/user/settings-radio-user.md +++ b/docs/ru-rRU/user/settings-radio-user.md @@ -113,13 +113,13 @@ aliases: ### Настройка питания -| Настройка | Описание | -| --------------------------------------- | ------------------------------------------------- | -| Энергосбережение | Включить спящий режим с низким энергопотреблением | -| Завершение работы через | Таймер автоотключения при простое | -| Множитель ADC | Коэффициент калибровки напряжения батареи | -| Wait Bluetooth (s) | Time to wait for BLE connection at boot | -| Mesh SDS Timeout (s) | Super-deep-sleep timeout | +| Настройка | Описание | +| ----------------------- | ------------------------------------------------- | +| Энергосбережение | Включить спящий режим с низким энергопотреблением | +| Завершение работы через | Таймер автоотключения при простое | +| Множитель ADC | Коэффициент калибровки напряжения батареи | +| Ожидание Bluetooth | Время ожидания подключения BLE при загрузке | +| Тайм-аут Mesh SDS | Тайм-аут сверхглубокого сна | ### Настройка сети @@ -131,44 +131,44 @@ aliases: | NTP-сервер | Сервер синхронизации времени | | Syslog-сервер | Удалённый сервер логирования | -![IP address field](../../assets/screenshots/settings_ipv4_field.png) +![Поле IP-адреса](../../assets/screenshots/settings_ipv4_field.png) ### Настройка Bluetooth -| Настройка | Описание | -| --------------------- | ------------------------------------------------------------------------- | -| Bluetooth Enabled | Enable/disable BLE radio | -| Pairing Mode | Fixed PIN, Random PIN, or No PIN | -| Фиксированный PIN-код | PIN code for pairing (default: 123456) | +| Настройка | Описание | +| --------------------- | -------------------------------------------------------------------------------- | +| Bluetooth включен | Включение/отключение BLE радиостанции | +| Режим сопряжения | Фиксированный PIN, случайный PIN или без PIN | +| Фиксированный PIN-код | PIN-код для сопряжения (по умолчанию: 123456) | ### Настройки безопасности -| Настройка | Описание | -| ------------------------- | -------------------------------------------------------------------------- | -| Публичный ключ | Your node's public key (read-only) | -| Ключ администратора | Key for remote administration | -| Приватный ключ | Your node's private key (handle securely) | -| ~~Admin Channel Enabled~~ | ⚠️ Removed — now configured automatically when an admin key is set | -| Журнал отладки | Output live debug logging over serial/bluetooth | -| Serial Enabled | Enable serial console access (moved from Device Config) | -| Управляемый режим | Restrict non-admin channel changes | +| Настройка | Описание | +| -------------------------------- | ------------------------------------------------------------------------------------------------- | +| Публичный ключ | Публичный ключ твоей ноды (только для чтения) | +| Ключ администратора | Ключ для удалённого администрирования | +| Приватный ключ | Приватный ключ твоей ноды (обращайся с ним осторожно) | +| ~~Канал администратора включен~~ | ⚠️ Удалено — теперь настраивается автоматически при установке ключа администратора | +| Журнал отладки | Выводить живой отладочный лог через последовательный порт/Bluetooth | +| Последовательная включена | Включить доступ к последовательной консоли (перемещено из настроек устройства) | +| Управляемый режим | Ограничить изменения канала для неадминистраторов | -![Password field](../../assets/screenshots/settings_password_field.png) +![Поле пароля](../../assets/screenshots/settings_password_field.png) -Settings use standard preference controls — dropdowns, toggles, and sliders: +Настройки используют стандартные элементы управления предпочтениями — выпадающие списки, переключатели и ползунки: -| Control | Screenshot | -| -------- | ----------------------------------------------------------- | -| Dropdown | ![Dropdown](../../assets/screenshots/settings_dropdown.png) | -| Toggle | ![Toggle](../../assets/screenshots/settings_switch.png) | -| Slider | ![Slider](../../assets/screenshots/settings_slider.png) | +| Управление | Снимок экрана | +| ----------------- | -------------------------------------------------------------------- | +| Выпадающий список | ![Выпадающий список](../../assets/screenshots/settings_dropdown.png) | +| Переключатель | ![Переключатель](../../assets/screenshots/settings_switch.png) | +| Ползунок | ![Ползунок](../../assets/screenshots/settings_slider.png) | ## Связанные темы -- [Settings — Modules & Admin](settings-module-admin) — optional feature modules and device administration -- [Signal Meter](signal-meter) — how modem presets affect signal quality thresholds -- [LoRa configuration](https://meshtastic.org/docs/configuration/radio/lora) — detailed LoRa settings reference on meshtastic.org -- [Initial configuration](https://meshtastic.org/docs/getting-started/initial-config) — region setup guide on meshtastic.org +- [Настройки — Модули и администрирование](settings-module-admin) — дополнительные модули и управление устройством +- [Измеритель сигнала](signal-meter) — как предустановки модема влияют на пороги качества сигнала +- [Конфигурация LoRa](https://meshtastic.org/docs/configuration/radio/lora) — подробная справка по настройкам LoRa на meshtastic.org +- [Начальная конфигурация](https://meshtastic.org/docs/getting-started/initial-config) — руководство по настройке региона на meshtastic.org --- diff --git a/docs/ru-rRU/user/signal-meter.md b/docs/ru-rRU/user/signal-meter.md index ece65f93ca..26c7852afd 100644 --- a/docs/ru-rRU/user/signal-meter.md +++ b/docs/ru-rRU/user/signal-meter.md @@ -1,9 +1,9 @@ --- -title: How the Meshtastic Signal Meter Works +title: Как работает измеритель сигнала Meshtastic parent: Руководство пользователя nav_order: 15 last_updated: 2026-05-13 -description: How the signal meter calculates quality from RSSI and SNR — LoRa spread spectrum, presets, and what the bars really mean. +description: Как измеритель сигнала вычисляет качество по RSSI и SNR — спектр с расширением LoRa, предустановки и что на самом деле означают эти полосы. aliases: - signal - signal-meter @@ -11,72 +11,72 @@ aliases: - rssi --- -# How the Meshtastic Signal Meter Works +# Как работает измеритель сигнала Meshtastic -The Meshtastic signal meter — the familiar bars or status color in the app — is calculated very differently than the "bars" on a traditional cell phone or Wi-Fi router. +Измеритель сигнала Meshtastic — знакомые полосы или цвет состояния в приложении — рассчитывается совсем иначе, чем "полоски" на традиционном мобильном телефоне или Wi-Fi роутере. -Most consumer devices simply measure how "loud" a signal is. However, because Meshtastic uses **LoRa (Long Range)** technology, its signal meter measures how **clear** the signal is, relative to the specific settings your mesh is using. +Большинство потребительских устройств просто измеряют, насколько "громкий" сигнал. Однако, поскольку Meshtastic использует технологию **LoRa (Long Range)**, его измеритель сигнала оценивает, насколько **чистый** сигнал, относительно конкретных настроек вашей mesh-сети. --- -## 1. The Two Metrics: "Loudness" vs. "Clarity" +## 1. Две метрики: "Громкость" против "Чёткости" -Every time the LoRa radio chip receives a message, it reports two measurements: +Каждый раз, когда радиочип LoRa получает сообщение, он сообщает два измерения: -- **RSSI (Received Signal Strength Indicator):** The **loudness** of the raw power hitting your antenna. -- **SNR (Signal-to-Noise Ratio):** The **clarity** of the signal compared to the background static. +- **RSSI (Индикатор уровня принятого сигнала):** **громкость** необработанного сигнала, поступающего на твою антенну. +- **SNR (Соотношение сигнал/шум):** **Чёткость** сигнала по сравнению с фоновым шумом. -> **Tip — The Analogy:** Imagine you are trying to hear a friend talking to you. +> **Совет — аналогия:** Представь, что ты пытаешься услышать, как друг разговаривает с тобой. > -> - **RSSI** is how loud their voice is. -> - **The Noise Floor** is the background noise in the room (air conditioning, other people talking, traffic). -> - **SNR** is how easily you can distinguish your friend's voice from the background noise. +> - **RSSI** — это громкость его голоса. +> - **Минимальный уровень шума** — это фоновый шум в комнате (кондиционер, разговоры других людей, движение транспорта). +> - **SNR** — это то, насколько легко ты можешь различить голос твоего друга на фоне шума. -If your friend shouts at you at a deafening rock concert, the signal is incredibly loud (High RSSI), but you still can't understand them because the background noise is louder (Bad SNR). Conversely, if your friend whispers to you in a dead-silent library, the signal is very weak (Low RSSI), but you can understand them perfectly (Great SNR). +Если твой друг кричит тебе на оглушительном рок-концерте, сигнал невероятно громкий (высокий RSSI), но ты всё равно не можешь его понять, потому что фоновый шум громче (плохое SNR). И наоборот, если твой друг шепчет тебе в абсолютно тихой библиотеке, сигнал очень слабый (низкий RSSI), но ты можешь его прекрасно понять (отличный SNR). --- -## 2. The Magic of LoRa: Hearing "Below the Noise Floor" +## 2. Магия LoRa: слышит "ниже уровня шума" -For standard radios (like FM or Wi-Fi), if the background noise is louder than the signal (a negative SNR), the receiver just hears static. +Для стандартных радиостанций (таких как FM или Wi-Fi), если фоновый шум громче сигнала (отрицательное отношение сигнал/шум), приемник просто слышит помехи. -LoRa is special. It uses **"Spread Spectrum"** modulation, which allows the radio to mathematically pull a signal out of the air even when it is buried deep _underneath_ the background noise. This is why you will frequently see **negative SNR numbers** in Meshtastic (e.g., -10 dB, which means the signal is 10 decibels weaker than the background static). +LoRa особенная. Она использует модуляцию **"широкополосного спектра"**, которая позволяет радио математически извлекать сигнал из воздуха даже когда он глубоко _под_ фоновым шумом. Вот почему ты часто будете видеть **отрицательные значения SNR** в Meshtastic (например, -10 дБ, что означает, что сигнал на 10 децибел слабее фонового шума). -Depending on which Meshtastic preset you are using (e.g., `LongFast` vs. `ShortFast`), the radio has a specific **SNR Limit** — the absolute maximum amount of noise it can tolerate before the message is completely lost to the static. +В зависимости от того, какой пресет Meshtastic ты используешь (например, `LongFast` или `ShortFast`), у радиоприемника есть определённый **предел SNR** — абсолютное максимальное количество шума, которое он может выдержать, прежде чем сообщение полностью потеряется в помехах. --- -## 3. How the Signal Meter Calculates Quality +## 3. Как измеритель сигнала рассчитывает качество -The Meshtastic apps take both RSSI and SNR and run them through a specific formula to assign your signal a quality rating (None, Bad, Fair, or Good). It specifically scales these values based on the physical limits of the radio preset you are using. +Приложения Meshtastic используют как RSSI, так и SNR и пропускают их через определённую формулу для присвоения сигналу оценки качества (Нет, Плохой, Удовлетворительный или Хороший). Оно специально масштабирует эти значения на основе физических ограничений используемого тобой пресета радиоприемника. -Here is exactly how the app decides how many bars (or what color) to show you: +Вот точно как приложение решает, сколько полосок (или какого цвета) показывать тебе: -| Level | Bars | Criteria | Meaning | -| ----------- | ---- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| Хороший | 3 | RSSI better than `-115 dBm` **AND** SNR better than `-7 dB` | Signal is both loud and clear — healthy connection. | -| Средний | 2 | RSSI better than `-126 dBm` with good SNR, **OR** SNR better than `-15 dB` with good RSSI | Signal getting quieter or noisier, but still decodable. | -| Плохой | 1 | Falls between Fair and None thresholds | At the edge of range or experiencing interference. | -| Отсутствует | 0 | RSSI worse than `-126 dBm` **AND** SNR worse than `-15 dB` | Transmission completely buried in noise. | +| Уровень | Деления | Критерии | Значение | +| ----------- | ------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| Хороший | 3 | RSSI лучше `-115 dBm` **И** SNR лучше `-7 dB` | Сигнал как громкий, так и четкий — здоровое соединение. | +| Средний | 2 | RSSI лучше `-126 dBm` с хорошим SNR, **ИЛИ** SNR лучше `-15 dB` с хорошим RSSI | Сигнал становится тише или громче, но всё ещё читается. | +| Плохой | 1 | Попадает между порогами Fair и None | На границе диапазона или при наличии помех. | +| Отсутствует | 0 | RSSI хуже `-126 dBm` **И** SNR хуже `-15 dB` | Передача полностью погребена в шуме. | --- -## 4. What This Means for You +## 4. Что это значит для тебя -Because Meshtastic's meter acts as a **"Clarity Meter"**, it behaves differently than what most people expect: +Поскольку измеритель Meshtastic действует как **"Измеритель чёткости"**, он ведёт себя иначе, чем ожидает большинство людей: -> **Tip — Don't panic over low RSSI:** You might see a seemingly terrible RSSI value like `-118 dBm`. On a cell phone, you would have zero bars. But if you have an SNR of `+2 dB`, Meshtastic will still show a strong signal! _The library is quiet, so the whisper is heard perfectly._ +> **Совет — не паникуй из-за низкого RSSI:** ты можешь увидеть кажущееся ужасным значение RSSI, например `-118 dBm`. На сотовом телефоне у тебя было бы ноль делений. Но если отношение сигнал/шум (SNR) `+2 дБ`, Meshtastic все равно покажет сильный сигнал! _Библиотека тихая, поэтому шепот слышен идеально._ -> **Warning — Watch out for local noise:** If you hook up a massive antenna and see a great RSSI (e.g., `-90 dBm`) but your signal meter is only showing **1 Bar (Bad)**, you have a problem. It means you have local interference — perhaps a cheap power supply, a noisy computer, or a nearby radio tower — creating so much static that it is drowning out your mesh. +> **Внимание — остерегайся местного шума:** если ты подключишь массивную антенну и увидишь отличный RSSI (например, `-90 dBm`), но индикатор сигнала показывает только **1 полоску (плохо)**, у тебя есть проблема. Это означает, что у тебя есть местные помехи — возможно, дешевый источник питания, шумный компьютер или расположенная рядом радиовышка — создающие столько статического шума, что он заглушает сеть. -## Where Signal Information Appears +## Где появляется информация о сигнале -In the app, signal data is shown in several places: +В приложении данные сигнала отображаются в нескольких местах: -- **Node list** — signal bars icon next to each node -- **Node detail** — SNR, RSSI, and signal quality in the device metrics section -- **Traceroute** — per-hop signal quality for each relay node -- **Signal metrics** — historical SNR and RSSI data in the metrics charts +- **Список нод** — значок полос сигнала рядом с каждой нодой +- **Детали ноды** — SNR, RSSI и качество сигнала в разделе метрик устройства +- **Трассировка** — качество сигнала на каждой промежуточной ноде +- **Метрики сигнала** — история данных SNR и RSSI на графиках метрик -![Node entry showing SNR, RSSI values and colored signal bars](../../assets/screenshots/nodes_signal_info.png) +![Запись ноды, показывающая значения SNR, RSSI и цветные индикаторы сигнала](../../assets/screenshots/nodes_signal_info.png) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8598faa32e..81a65c4bae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ turbine = "1.2.1" compose-screenshot = "0.0.1-alpha15" # Compose Multiplatform -compose-multiplatform = "1.11.0" +compose-multiplatform = "1.11.1" compose-multiplatform-material3 = "1.11.0-alpha07" # `androidx-compose-bom-aligned` tracks androidx.compose.{runtime,ui,foundation,animation} # artifacts that ship in lockstep with CMP. Kept as a separate version ref so Renovate @@ -281,7 +281,7 @@ test-retry-gradlePlugin = { module = "org.gradle:test-retry-gradle-plugin", vers aboutlibraries-gradlePlugin = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlibraries" } jmdns = { module = "org.jmdns:jmdns", version.ref = "jmdns" } -jna = { module = "net.java.dev.jna:jna", version = "5.18.1" } +jna = { module = "net.java.dev.jna:jna", version = "5.19.0" } # TAK takpacket-sdk-kmp = { module = "org.meshtastic:takpacket-sdk", version.ref = "takpacket-sdk" }